From f3d3bc3a5fc488142af86a856eeb875fbda81e4c Mon Sep 17 00:00:00 2001 From: Jonas Jabari Date: Thu, 23 Feb 2023 14:35:10 +0800 Subject: [PATCH] moved ical sharing UI to calendar detail page (angular scope) as requested in specification --- app/policies/query_policy.rb | 11 +- .../wp-calendar-page.component.ts | 1 + .../wp-settings-button.component.html | 3 +- .../wp-settings-button.component.ts | 1 + .../query-get-ical-url.modal.html | 30 +++++ .../query-get-ical-url.modal.sass | 4 + .../query-get-ical-url.modal.ts | 113 ++++++++++++++++++ .../op-settings-dropdown-menu.directive.ts | 16 +++ lib/api/v3/queries/queries_api.rb | 18 +++ lib/api/v3/queries/query_representer.rb | 10 ++ lib/api/v3/utilities/path_helper.rb | 4 + .../calendar/app/cells/calendar/row_cell.rb | 14 +-- .../calendar/calendars_controller.rb | 24 ---- modules/calendar/config/routes.rb | 2 - 14 files changed, 210 insertions(+), 41 deletions(-) create mode 100644 frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.html create mode 100644 frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.sass create mode 100644 frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.ts diff --git a/app/policies/query_policy.rb b/app/policies/query_policy.rb index dfb11c1d3f0..f99f8b79a53 100644 --- a/app/policies/query_policy.rb +++ b/app/policies/query_policy.rb @@ -41,7 +41,8 @@ class QueryPolicy < BasePolicy depublicize: depublicize_allowed?(cached_query), star: persisted_and_own_or_public?(cached_query), unstar: persisted_and_own_or_public?(cached_query), - reorder_work_packages: reorder_work_packages?(cached_query) + reorder_work_packages: reorder_work_packages?(cached_query), + create_ical_url: create_ical_url_allowed?(cached_query) } end @@ -119,4 +120,12 @@ class QueryPolicy < BasePolicy @manage_public_queries_cache[query.project] end + + def create_ical_url_allowed?(query) + @create_ical_url_cache ||= Hash.new do |hash, project| + hash[project] = user.allowed_to?(:share_calendars, project, global: project.nil?) + end + + @create_ical_url_cache[query.project] + end end diff --git a/frontend/src/app/features/calendar/wp-calendar-page/wp-calendar-page.component.ts b/frontend/src/app/features/calendar/wp-calendar-page/wp-calendar-page.component.ts index 4deadefc240..b884fbe9fb3 100644 --- a/frontend/src/app/features/calendar/wp-calendar-page/wp-calendar-page.component.ts +++ b/frontend/src/app/features/calendar/wp-calendar-page/wp-calendar-page.component.ts @@ -112,6 +112,7 @@ export class WorkPackagesCalendarPageComponent extends PartitionedQuerySpacePage show: ():boolean => this.authorisationService.can('query', 'updateImmediately'), inputs: { hideTableOptions: true, + showCalendarSharingOption: true }, }, ]; diff --git a/frontend/src/app/features/work-packages/components/wp-buttons/wp-settings-button/wp-settings-button.component.html b/frontend/src/app/features/work-packages/components/wp-buttons/wp-settings-button/wp-settings-button.component.html index b8eef0a5adb..ea425933a30 100644 --- a/frontend/src/app/features/work-packages/components/wp-buttons/wp-settings-button/wp-settings-button.component.html +++ b/frontend/src/app/features/work-packages/components/wp-buttons/wp-settings-button/wp-settings-button.component.html @@ -3,6 +3,7 @@ class="button last work-packages-settings-button toolbar-icon" opSettingsContextMenu opSettingsContextMenu-query="query" - [hideTableOptions]="hideTableOptions"> + [hideTableOptions]="hideTableOptions" + [showCalendarSharingOption]="showCalendarSharingOption"> diff --git a/frontend/src/app/features/work-packages/components/wp-buttons/wp-settings-button/wp-settings-button.component.ts b/frontend/src/app/features/work-packages/components/wp-buttons/wp-settings-button/wp-settings-button.component.ts index 66ad76cb176..7957f7c4207 100644 --- a/frontend/src/app/features/work-packages/components/wp-buttons/wp-settings-button/wp-settings-button.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-buttons/wp-settings-button/wp-settings-button.component.ts @@ -39,6 +39,7 @@ import { I18nService } from 'core-app/core/i18n/i18n.service'; }) export class WorkPackageSettingsButtonComponent { @Input() hideTableOptions = false; + @Input() showCalendarSharingOption = false; public text = { more_actions: this.I18n.t('js.button_more_actions'), diff --git a/frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.html b/frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.html new file mode 100644 index 00000000000..54e2fdb7070 --- /dev/null +++ b/frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.html @@ -0,0 +1,30 @@ +
+
{{text.label_ical_sharing}}
+ +
+ +
+

+ {{text.description_ical_sharing}} +

+

+ {{icalUrl}} +

+
+ +
+
+ + +
+
+
diff --git a/frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.sass b/frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.sass new file mode 100644 index 00000000000..0eb61622ae6 --- /dev/null +++ b/frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.sass @@ -0,0 +1,4 @@ +.op-query-get-ical-url + .ical-url + word-break: break-word + diff --git a/frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.ts b/frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.ts new file mode 100644 index 00000000000..40bfd005554 --- /dev/null +++ b/frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.ts @@ -0,0 +1,113 @@ +// -- copyright +// OpenProject is an open source project management software. +// Copyright (C) 2012-2023 the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See COPYRIGHT and LICENSE files for more details. +//++ + +import { WorkPackagesListService } from 'core-app/features/work-packages/components/wp-list/wp-list.service'; +import { States } from 'core-app/core/states/states.service'; +import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; +import { QueryResource } from 'core-app/features/hal/resources/query-resource'; +import { ToastService } from 'core-app/shared/components/toaster/toast.service'; +import { OpModalComponent } from 'core-app/shared/components/modal/modal.component'; +import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service'; +import { OpModalLocalsMap } from 'core-app/shared/components/modal/modal.types'; +import { + ChangeDetectorRef, Component, ElementRef, Inject, OnInit, resolveForwardRef, +} from '@angular/core'; +import { I18nService } from 'core-app/core/i18n/i18n.service'; +import { IsolatedQuerySpace } from 'core-app/features/work-packages/directives/query-space/isolated-query-space'; +import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; + +@Component({ + templateUrl: './query-get-ical-url.modal.html', + styleUrls: ['./query-get-ical-url.modal.sass'] +}) +export class QueryGetIcalUrlModalComponent extends OpModalComponent implements OnInit { + public query:QueryResource; + + public isBusy = false; + + public icalUrl: string; + + public text = { + label_ical_sharing: 'Share calendar', // TODO: translate + description_ical_sharing: 'You can share and import this calendar by using the following iCalendar URL:', // TODO: translate + button_copy: 'Copy URL', // TODO: translate + copy_success_text: 'URL copied to clipboard', // TODO: translate + button_cancel: this.I18n.t('js.button_cancel'), + close_popup: this.I18n.t('js.close_popup_title') + }; + + constructor( + readonly elementRef:ElementRef, + @Inject(OpModalLocalsToken) public locals:OpModalLocalsMap, + readonly I18n:I18nService, + readonly states:States, + readonly querySpace:IsolatedQuerySpace, + readonly cdRef:ChangeDetectorRef, + readonly wpListService:WorkPackagesListService, + readonly halNotification:HalResourceNotificationService, + readonly toastService:ToastService, + protected apiV3Service:ApiV3Service + ) { + super(locals, cdRef, elementRef); + } + + ngOnInit():void { + super.ngOnInit(); + + this.query = this.querySpace.query.value!; + + this.isBusy = true; + + this + .query + .createIcalUrl() + .then((response:HalResource) => { + this.icalUrl = response.icalUrl; + this.isBusy = false; + this.cdRef.detectChanges(); + // or would that be better? + // this.ngZone.run(() => { + // this.icalUrl = response.icalUrl; + // this.isBusy = false; + // }); + }) + } + + public copyUrl($event:Event):void { + if (this.isBusy) { + return; + } + + navigator.clipboard.writeText(this.icalUrl) + .then(() => { + this.toastService.addSuccess(this.text.copy_success_text); + }) + + } +} diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts index 6286dd8c242..bf5b20454cc 100644 --- a/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts @@ -44,6 +44,7 @@ import { triggerEditingEvent, } from 'core-app/shared/components/editable-toolbar-title/editable-toolbar-title.component'; import { QuerySharingModalComponent } from 'core-app/shared/components/modals/share-modal/query-sharing.modal'; +import { QueryGetIcalUrlModalComponent } from 'core-app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal'; import { WpTableExportModalComponent } from 'core-app/shared/components/modals/export-modal/wp-table-export.modal'; import { SaveQueryModalComponent } from 'core-app/shared/components/modals/save-modal/save-query.modal'; import { QueryFormResource } from 'core-app/features/hal/resources/query-form-resource'; @@ -56,6 +57,7 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger { @Input('opSettingsContextMenu-query') public query:QueryResource; @Input() public hideTableOptions:boolean; + @Input() public showCalendarSharingOption:boolean; private form:QueryFormResource; @@ -313,6 +315,20 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger { icon: 'icon-custom-fields', onClick: () => false, }, + { + // Calendar sharing modal + hidden: !this.showCalendarSharingOption, + disabled: this.authorisationService.cannot('query', 'createIcalUrl'), + linkText: "Share Calendar", // TODO: translate + icon: 'icon-link', // TODO: find sharing icons + onClick: ($event:JQuery.TriggeredEvent) => { + if (this.authorisationService.can('query', 'createIcalUrl')) { + this.opModalService.show(QueryGetIcalUrlModalComponent, this.injector); + } + + return true; + }, + }, ]; } } diff --git a/lib/api/v3/queries/queries_api.rb b/lib/api/v3/queries/queries_api.rb index 5a02401eb46..9769f9aae41 100644 --- a/lib/api/v3/queries/queries_api.rb +++ b/lib/api/v3/queries/queries_api.rb @@ -159,6 +159,24 @@ module API .mount end + namespace :create_ical_url do + post do + authorize_by_policy(:create_ical_url) + + # currently the generated URL points to controller action in calendar module + # correct approach? or should it be implemented as a API here? + call = ::Calendar::GenerateIcalUrl.new().call( + user: current_user, + query_id: @query.id, + project_id: @query.project_id + ) + + { + icalUrl: call.result + } + end + end + mount API::V3::Queries::Order::QueryOrderAPI end end diff --git a/lib/api/v3/queries/query_representer.rb b/lib/api/v3/queries/query_representer.rb index ec95bd6b9d8..5007c519c45 100644 --- a/lib/api/v3/queries/query_representer.rb +++ b/lib/api/v3/queries/query_representer.rb @@ -146,6 +146,16 @@ module API method: :delete } end + + link :createIcalUrl do + next if represented.new_record? || + !allowed_to?(:create_ical_url) + + { + href: api_v3_paths.query_create_ical_url(represented.id), + method: :post + } + end associated_resource :user diff --git a/lib/api/v3/utilities/path_helper.rb b/lib/api/v3/utilities/path_helper.rb index 48bdfdc21aa..8b7cb1c22e1 100644 --- a/lib/api/v3/utilities/path_helper.rb +++ b/lib/api/v3/utilities/path_helper.rb @@ -329,6 +329,10 @@ module API "#{query(id)}/order" end + def self.query_create_ical_url(id) + "#{query(id)}/create_ical_url" + end + def self.query_column(name) "#{queries}/columns/#{name}" end diff --git a/modules/calendar/app/cells/calendar/row_cell.rb b/modules/calendar/app/cells/calendar/row_cell.rb index 0c638bbc145..ca2b5fad5da 100644 --- a/modules/calendar/app/cells/calendar/row_cell.rb +++ b/modules/calendar/app/cells/calendar/row_cell.rb @@ -46,19 +46,7 @@ module Calendar end def button_links - [share_link, delete_link].compact - end - - def share_link - if table.current_user.allowed_to?(:share_calendars, project) - link_to( - '', - generate_ical_url_project_calendar_path(project, query.id), - method: :post, - class: 'icon icon-link', # TODO: use proper share icon - title: "Share via iCal" # TODO: use translations - ) - end + [delete_link].compact end def delete_link diff --git a/modules/calendar/app/controllers/calendar/calendars_controller.rb b/modules/calendar/app/controllers/calendar/calendars_controller.rb index 8075ef26011..70cbc983d94 100644 --- a/modules/calendar/app/controllers/calendar/calendars_controller.rb +++ b/modules/calendar/app/controllers/calendar/calendars_controller.rb @@ -42,30 +42,6 @@ module ::Calendar render layout: 'angular/angular' end - def generate_ical_url - begin - call = ::Calendar::GenerateIcalUrl.new().call( - user: current_user, - query_id: params[:id], - project_id: @project.id - ) - rescue ActiveRecord::RecordNotFound - render_404 - return - end - - if call.present? && call.success? - # TODO: Use translations - flash[:info] = " - You can share and import this calendar by using the following iCalendar URL: - #{call.result} - " - redirect_to action: :index - else - render_404 - end - end - def ical begin call = ::Calendar::IcalResponseService.new().call( diff --git a/modules/calendar/config/routes.rb b/modules/calendar/config/routes.rb index f249ee25172..d54290698ea 100644 --- a/modules/calendar/config/routes.rb +++ b/modules/calendar/config/routes.rb @@ -6,8 +6,6 @@ OpenProject::Application.routes.draw do as: :calendars do get '/new' => 'calendar/calendars#show', on: :collection, as: 'new' # TODO: discuss if other controller should be used - post '/generate_ical_url' => 'calendar/calendars#generate_ical_url', on: :member, as: 'generate_ical_url' - # TODO: discuss if other controller should be used get '/ical' => 'calendar/calendars#ical', on: :member, as: 'ical' get '(/*state)' => 'calendar/calendars#show', on: :member, as: '' end