moved ical sharing UI to calendar detail page (angular scope) as requested in specification

This commit is contained in:
Jonas Jabari
2023-02-23 14:35:10 +08:00
parent fca22462ab
commit f3d3bc3a5f
14 changed files with 210 additions and 41 deletions
+10 -1
View File
@@ -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
@@ -112,6 +112,7 @@ export class WorkPackagesCalendarPageComponent extends PartitionedQuerySpacePage
show: ():boolean => this.authorisationService.can('query', 'updateImmediately'),
inputs: {
hideTableOptions: true,
showCalendarSharingOption: true
},
},
];
@@ -3,6 +3,7 @@
class="button last work-packages-settings-button toolbar-icon"
opSettingsContextMenu
opSettingsContextMenu-query="query"
[hideTableOptions]="hideTableOptions">
[hideTableOptions]="hideTableOptions"
[showCalendarSharingOption]="showCalendarSharingOption">
<op-icon icon-classes="button--icon icon-show-more"></op-icon>
</button>
@@ -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'),
@@ -0,0 +1,30 @@
<div
class="spot-modal loading-indicator--location op-query-get-ical-url"
data-indicator-name="modal"
>
<div class="spot-modal--header">{{text.label_ical_sharing}}</div>
<div class="spot-divider"></div>
<div class="spot-modal--body spot-container">
<p>
{{text.description_ical_sharing}}
</p>
<p class="ical-url">
<a href="{{icalUrl}}" target="_blank">{{icalUrl}}</a>
</p>
</div>
<div class="spot-action-bar">
<div class="spot-action-bar--right">
<button class="button button_no-margin spot-modal--cancel-button spot-action-bar--action"
[textContent]="text.button_cancel"
(click)="closeMe($event)">
</button>
<button class="button button_no-margin -highlight -with-icon icon-copy spot-action-bar--action"
(click)="copyUrl($event)"
[textContent]="text.button_copy">
</button>
</div>
</div>
</div>
@@ -0,0 +1,4 @@
.op-query-get-ical-url
.ical-url
word-break: break-word
@@ -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);
})
}
}
@@ -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;
},
},
];
}
}
+18
View File
@@ -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
+10
View File
@@ -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
+4
View File
@@ -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
@@ -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
@@ -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(
-2
View File
@@ -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