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.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