From 1bfabd9599945b189901d417fd6563c89b32f0f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 7 May 2018 21:16:48 +0200 Subject: [PATCH 1/5] Improve and reverse rails html titles --- app/helpers/application_helper.rb | 15 ----- app/helpers/meta_tags_helper.rb | 62 +++++++++++++++++++ app/views/layouts/base.html.erb | 2 +- app/views/layouts/help.html.erb | 2 +- app/views/timelog/index.html.erb | 2 +- .../app/components/html/op-title.service.ts | 27 ++++++++ 6 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 app/helpers/meta_tags_helper.rb create mode 100644 frontend/app/components/html/op-title.service.ts diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 62f57494be..3c0f154f4a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -322,21 +322,6 @@ module ApplicationHelper end end - def html_title(*args) - title = [] - - if args.empty? - title << h(@project.name) if @project - title += @html_title if @html_title - else - @html_title ||= [] - @html_title += args - title += @html_title - end - - title.select { |t| !t.blank? }.join(' - ').html_safe - end - # Returns the theme, controller name, and action as css classes for the # HTML body. def body_css_classes diff --git a/app/helpers/meta_tags_helper.rb b/app/helpers/meta_tags_helper.rb new file mode 100644 index 0000000000..03f05f9dec --- /dev/null +++ b/app/helpers/meta_tags_helper.rb @@ -0,0 +1,62 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module MetaTagsHelper + + ## + # Use meta-tags to output title and site name + def output_title_and_meta_tags + + display_meta_tags site: Setting.app_title, + title: html_title_parts, + separator: ' | ', # Update the TitleService when changing this! + reverse: true + end + + ## + # Writer of html_title as string + def html_title(*args) + title = [] + + raise "Don't use html_title getter" if args.empty? + + @html_title ||= [] + @html_title += args + end + + ## + # The html title parts currently defined + def html_title_parts + [].tap do |parts| + parts << h(@project.name) if @project + parts.concat @html_title if @html_title + end + end +end diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index 9bdfabbe34..1149d39223 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -31,7 +31,7 @@ See docs/COPYRIGHT.rdoc for more details. - <%= display_meta_tags site: Setting.app_title, title: html_title %> + <%= output_title_and_meta_tags %> <% if @project %> diff --git a/app/views/layouts/help.html.erb b/app/views/layouts/help.html.erb index 7615a62901..c481b30963 100644 --- a/app/views/layouts/help.html.erb +++ b/app/views/layouts/help.html.erb @@ -29,7 +29,7 @@ See docs/COPYRIGHT.rdoc for more details. - <%= h html_title %> + <%= output_title_and_meta_tags %> <%= nonced_style_tag do %> <%= yield(:styles) %> diff --git a/app/views/timelog/index.html.erb b/app/views/timelog/index.html.erb index d9d1dc3d3e..28e8b52a5e 100644 --- a/app/views/timelog/index.html.erb +++ b/app/views/timelog/index.html.erb @@ -60,7 +60,7 @@ See docs/COPYRIGHT.rdoc for more details. <% end %> <% end %> -<% html_title l(:label_spent_time), l(:label_details) %> +<% html_title l(:label_spent_time) %> <% content_for :header_tags do %> <%= auto_discovery_link_tag(:atom, {issue_id: @issue, format: 'atom', key: User.current.rss_key}, title: l(:label_spent_time)) %> <% end %> diff --git a/frontend/app/components/html/op-title.service.ts b/frontend/app/components/html/op-title.service.ts new file mode 100644 index 0000000000..70493d5de4 --- /dev/null +++ b/frontend/app/components/html/op-title.service.ts @@ -0,0 +1,27 @@ +import {Title} from "@angular/platform-browser"; +import {Injectable} from "@angular/core"; + +const titlePartsSeparator = ' | '; + +@Injectable() +export class OpTitleService { + constructor(readonly titleService:Title) { + + } + + public get current():string { + return this.titleService.getTitle(); + } + + public get titleParts():string[] { + return this.current.split(titlePartsSeparator); + } + + public setFirstPart(value:string) { + let parts = this.titleParts; + parts[0] = value; + + this.titleService.setTitle(parts.join(titlePartsSeparator)); + } + +} From fd368cd0c03764aa467cf4495e8735bb0e4394a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 7 May 2018 21:16:56 +0200 Subject: [PATCH 2/5] Improve frontend titles --- app/helpers/meta_tags_helper.rb | 3 +- config/locales/js-en.yml | 4 ++- frontend/app/angular4-modules.ts | 2 ++ .../app/components/html/op-title.service.ts | 2 +- .../wp-view-base/wp-view-base.controller.ts | 6 ++++ .../components/wp-copy/wp-copy.controller.ts | 4 +++ .../components/wp-new/wp-create.controller.ts | 7 +++++ .../wp-query-menu/wp-query-menu.ng2.test.ts | 7 +++++ .../wp-query-menu/wp-query-menu.service.ts | 30 +++++++++++++++---- .../wp-relations-parent.html | 2 +- .../wp-single-relation.directive.ts | 2 +- .../hal/resources/work-package-resource.ts | 12 ++++---- 12 files changed, 63 insertions(+), 18 deletions(-) diff --git a/app/helpers/meta_tags_helper.rb b/app/helpers/meta_tags_helper.rb index 03f05f9dec..93932c2f66 100644 --- a/app/helpers/meta_tags_helper.rb +++ b/app/helpers/meta_tags_helper.rb @@ -33,7 +33,6 @@ module MetaTagsHelper ## # Use meta-tags to output title and site name def output_title_and_meta_tags - display_meta_tags site: Setting.app_title, title: html_title_parts, separator: ' | ', # Update the TitleService when changing this! @@ -56,7 +55,7 @@ module MetaTagsHelper def html_title_parts [].tap do |parts| parts << h(@project.name) if @project - parts.concat @html_title if @html_title + parts.concat @html_title.map(&:to_s) if @html_title end end end diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 3823c930fc..3d6edca59a 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -394,7 +394,6 @@ en: grouping_other: "Other" noneSelection: "(none)" name: "Name" - new_work_package: "New work package" outline: "Reset Outline" outlines: aggregation: "Show aggregations only" @@ -486,10 +485,13 @@ en: inline_create: title: 'Click here to add a new work package to this list' create: + title: 'New work package' header: 'New %{type}' header_no_type: 'New work package (Type not yet set)' header_with_parent: 'New %{type} (Child of %{parent_type} #%{id})' button: 'Create' + copy: + title: 'Copy work package' hierarchy: show: "Show hierarchy mode" hide: "Hide hierarchy mode" diff --git a/frontend/app/angular4-modules.ts b/frontend/app/angular4-modules.ts index 775089f604..9bcbeda3a1 100644 --- a/frontend/app/angular4-modules.ts +++ b/frontend/app/angular4-modules.ts @@ -220,6 +220,7 @@ import {WpDestroyModal} from "core-components/modals/wp-destroy-modal/wp-destroy import {FocusWithinDirective} from "core-components/common/focus/focus-within.upgraded.directive"; import {AccessibleClickDirective} from "core-components/a11y/accessible-click.directive"; import {WorkPackageChildrenQueryComponent} from 'core-components/wp-relations/wp-relation-children/wp-children-query.component'; +import {OpTitleService} from 'core-components/html/op-title.service'; @NgModule({ imports: [ @@ -249,6 +250,7 @@ import {WorkPackageChildrenQueryComponent} from 'core-components/wp-relations/wp FocusHelperService, PathHelperService, upgradeServiceWithToken('wpMoreMenuService', wpMoreMenuServiceToken), + OpTitleService, TimezoneService, upgradeService('wpRelations', WorkPackageRelationsService), UrlParamsHelperService, diff --git a/frontend/app/components/html/op-title.service.ts b/frontend/app/components/html/op-title.service.ts index 70493d5de4..f5a354f9b1 100644 --- a/frontend/app/components/html/op-title.service.ts +++ b/frontend/app/components/html/op-title.service.ts @@ -5,7 +5,7 @@ const titlePartsSeparator = ' | '; @Injectable() export class OpTitleService { - constructor(readonly titleService:Title) { + constructor(private titleService:Title) { } diff --git a/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts b/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts index c47efdafe2..02c8d1bf79 100644 --- a/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts +++ b/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts @@ -39,6 +39,7 @@ import {KeepTabService} from '../../wp-single-view-tabs/keep-tab/keep-tab.servic import {WorkPackageTableRefreshService} from '../../wp-table/wp-table-refresh-request.service'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {ProjectCacheService} from 'core-components/projects/project-cache.service'; +import {OpTitleService} from 'core-components/html/op-title.service'; export class WorkPackageViewController implements OnDestroy { @@ -62,6 +63,8 @@ export class WorkPackageViewController implements OnDestroy { protected focusAnchorLabel:string; public showStaticPagePath:string; + readonly titleService:OpTitleService = this.injector.get(OpTitleService); + constructor(public injector:Injector, protected workPackageId:string) { this.initializeTexts(); } @@ -106,6 +109,9 @@ export class WorkPackageViewController implements OnDestroy { this.projectIdentifier = this.workPackage.project.identifier; }); + // Push the current title + this.titleService.setFirstPart(this.workPackage.subjectWithType(20)); + // Preselect this work package for future list operations this.showStaticPagePath = this.PathHelper.workPackagePath(this.workPackageId); diff --git a/frontend/app/components/wp-copy/wp-copy.controller.ts b/frontend/app/components/wp-copy/wp-copy.controller.ts index 490005269b..ba324d2baf 100644 --- a/frontend/app/components/wp-copy/wp-copy.controller.ts +++ b/frontend/app/components/wp-copy/wp-copy.controller.ts @@ -45,6 +45,10 @@ export class WorkPackageCopyController extends WorkPackageCreateController { }); } + protected setTitle() { + this.titleService.setFirstPart(this.I18n.t('js.work_packages.copy.title')); + } + private async createCopyFrom(wp:WorkPackageResource) { const changeset = this.wpEditing.changesetFor(wp); return changeset.getForm().then(async (form:any) => { diff --git a/frontend/app/components/wp-new/wp-create.controller.ts b/frontend/app/components/wp-new/wp-create.controller.ts index 5dcf9dc24f..0fa566139d 100644 --- a/frontend/app/components/wp-new/wp-create.controller.ts +++ b/frontend/app/components/wp-new/wp-create.controller.ts @@ -43,6 +43,7 @@ import {WorkPackageTableFiltersService} from '../wp-fast-table/state/wp-table-fi import {WorkPackageCreateService} from './wp-create.service'; import {takeUntil} from 'rxjs/operators'; import {RootDmService} from 'core-app/modules/hal/dm-services/root-dm.service'; +import {OpTitleService} from 'core-components/html/op-title.service'; export class WorkPackageCreateController implements OnInit, OnDestroy { @@ -59,6 +60,7 @@ export class WorkPackageCreateController implements OnInit, OnDestroy { constructor(readonly $transition:Transition, @Inject($stateToken) readonly $state:StateService, @Inject(I18nToken) readonly I18n:op.I18n, + readonly titleService:OpTitleService, protected wpNotificationsService:WorkPackageNotificationService, protected states:States, protected wpCreate:WorkPackageCreateService, @@ -76,6 +78,7 @@ export class WorkPackageCreateController implements OnInit, OnDestroy { this.changeset = changeset; this.newWorkPackage = changeset.workPackage; + this.setTitle(); this.wpCacheService.updateWorkPackage(this.newWorkPackage); this.wpEditing.updateValue('new', changeset); @@ -122,6 +125,10 @@ export class WorkPackageCreateController implements OnInit, OnDestroy { this.$state.go('work-packages.new', this.$state.params); } + protected setTitle() { + this.titleService.setFirstPart(this.I18n.t('js.work_packages.create.title')); + } + protected async newWorkPackageFromParams(stateParams:any):Promise { const type = parseInt(stateParams.type); diff --git a/frontend/app/components/wp-query-menu/wp-query-menu.ng2.test.ts b/frontend/app/components/wp-query-menu/wp-query-menu.ng2.test.ts index a397119d31..5cc0de7003 100644 --- a/frontend/app/components/wp-query-menu/wp-query-menu.ng2.test.ts +++ b/frontend/app/components/wp-query-menu/wp-query-menu.ng2.test.ts @@ -34,6 +34,8 @@ import {Component} from '@angular/core'; import {WorkPackagesListChecksumService} from 'core-components/wp-list/wp-list-checksum.service'; import {TransitionService} from '@uirouter/core'; import {$stateToken} from 'core-app/angular4-transition-utils'; +import {I18nToken} from 'core-app/angular4-transition-utils'; +import {OpTitleService} from 'core-components/html/op-title.service'; @Component({ template: ` @@ -57,6 +59,9 @@ describe('wp-query-menu', () => { const $transitionStub = { onStart: (criteria:any, callback:(transition:any) => any) => { transitionCallback = (id:any) => callback({ + to: () => { + return { name: 'asdf' }; + }, params: (val:string) => { return { query_id: id }; } } as any); } @@ -70,6 +75,8 @@ describe('wp-query-menu', () => { WpQueryMenuTestComponent ], providers: [ + { provide: I18nToken, useValue: I18n }, + { provide: OpTitleService, useValue: { setFirstPart: () => { return; } } }, { provide: $stateToken, useValue: { params: { query_id: null }, go: (...args:any[]) => undefined } }, { provide: WorkPackagesListChecksumService, useValue: { clear: () => undefined } }, { provide: TransitionService, useValue: $transitionStub }, diff --git a/frontend/app/components/wp-query-menu/wp-query-menu.service.ts b/frontend/app/components/wp-query-menu/wp-query-menu.service.ts index 10c1e28957..b67aab2bad 100644 --- a/frontend/app/components/wp-query-menu/wp-query-menu.service.ts +++ b/frontend/app/components/wp-query-menu/wp-query-menu.service.ts @@ -28,9 +28,11 @@ import {Inject, Injectable} from '@angular/core'; import {StateService, Transition, TransitionService} from '@uirouter/core'; -import {$stateToken} from 'core-app/angular4-transition-utils'; +import {$stateToken, I18nToken} from 'core-app/angular4-transition-utils'; import {LinkHandling} from 'core-components/common/link-handling/link-handling'; import {WorkPackagesListChecksumService} from 'core-components/wp-list/wp-list-checksum.service'; +import {Title} from '@angular/platform-browser'; +import {OpTitleService} from 'core-components/html/op-title.service'; export const QUERY_MENU_ITEM_TYPE = 'query-menu-item'; @@ -48,13 +50,23 @@ export class QueryMenuService { private uiRouteStateName = 'work-packages.list'; private container:JQuery; - constructor(@Inject($stateToken) protected $state:StateService, - protected $transitions:TransitionService, - protected wpListChecksumService:WorkPackagesListChecksumService) { + constructor(@Inject($stateToken) readonly $state:StateService, + @Inject(I18nToken) readonly I18n:op.I18n, + readonly titleService:OpTitleService, + readonly $transitions:TransitionService, + readonly wpListChecksumService:WorkPackagesListChecksumService) { this.$transitions.onStart({}, (transition:Transition) => { const queryId = transition.params('to').query_id; - this.onQueryIdChanged(queryId); + + // Update query menu and title when either + // the query menu id changed + const queryIdChanged = this.currentQueryId !== queryId; + // we're moving to the work-packges.list state + const movingToWPList = transition.to().name === 'work-packages.list'; + if (movingToWPList || queryIdChanged) { + this.onQueryIdChanged(queryId); + } }); this.initialize(); @@ -122,9 +134,15 @@ export class QueryMenuService { // Update all queries children const queries = this.container.find('.query-menu-item'); queries.toggleClass('selected', false); + if (this.currentQueryId) { - queries.filter(`#wp-query-menu-item-${this.currentQueryId}`).addClass('selected'); + let current = queries.filter(`#wp-query-menu-item-${this.currentQueryId}`) + current.addClass('selected'); + + // Set the page title + this.titleService.setFirstPart(current.text()); } + } private buildItem(queryId:string, name:string) { diff --git a/frontend/app/components/wp-relations/wp-relations-parent/wp-relations-parent.html b/frontend/app/components/wp-relations/wp-relations-parent/wp-relations-parent.html index 2ae46bb38e..062eff4e55 100644 --- a/frontend/app/components/wp-relations/wp-relations-parent/wp-relations-parent.html +++ b/frontend/app/components/wp-relations/wp-relations-parent/wp-relations-parent.html @@ -9,7 +9,7 @@ class="wp-relations--subject-field" [attr.aria-label]="text.parent" [attr.id]="wp-relations-parent-element" - [textContent]="workPackage.parent.subjectWithType"> + [textContent]="workPackage.parent.subjectWithType()"> diff --git a/frontend/app/components/wp-relations/wp-single-relation.directive.ts b/frontend/app/components/wp-relations/wp-single-relation.directive.ts index 94989be9b3..657728fdbc 100644 --- a/frontend/app/components/wp-relations/wp-single-relation.directive.ts +++ b/frontend/app/components/wp-relations/wp-single-relation.directive.ts @@ -44,7 +44,7 @@ export class WorkPackageSingleRelationController { return workPackage.subject; } - return workPackage.subjectWithType; + return workPackage.subjectWithType(); } } diff --git a/frontend/app/modules/hal/resources/work-package-resource.ts b/frontend/app/modules/hal/resources/work-package-resource.ts index 24b69b6016..9f5a718bd1 100644 --- a/frontend/app/modules/hal/resources/work-package-resource.ts +++ b/frontend/app/modules/hal/resources/work-package-resource.ts @@ -146,12 +146,12 @@ export class WorkPackageResource extends HalResource { /** * Return ": " if the type is known. */ - public get subjectWithType():string { - if (this.type) { - return `${this.type.name}: ${this.subject}`; - } else { - return this.subject; - } + public subjectWithType(truncateSubject:number = 40):string { + const type = this.type ? `${this.type.name}: ` : ''; + const id = this.isNew ? '' : ` (#${this.id})`; + const subject = _.truncate(this.subject, { length: truncateSubject }); + + return `${type}${subject}${id}`; } public get isNew():boolean { From 15418bacfd4fbb2786d24eebb8cb15446fee5563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 14 May 2018 11:39:43 +0200 Subject: [PATCH 3/5] Convert wp-relations to angular (#6310) [ci skip] --- frontend/app/angular4-modules.ts | 21 ++- .../wp-edit-field-controls-ng1-wrapper.ts | 69 ++++++++ .../ng1-wp-relations-wrapper.directive.ts | 26 --- ...ective.ts => wp-relation-row.component.ts} | 84 ++++----- .../wp-relation-row.template.html | 161 +++++++++--------- .../add-fixed-type.template.html | 27 --- .../dynamic-relation-types.template.html | 58 ------- .../ng1-wp-relations-create.directive.ts | 28 --- .../wp-relation-create.template.html | 64 +++++++ .../wp-relations-autocomplete.directive.ts | 113 ------------ .../wp-relations-autocomplete.template.html | 15 -- ...ve.ts => wp-relations-create.component.ts} | 74 +++----- .../wp-relations-group.component.ts} | 63 ++++--- .../wp-relations-group.directive.ts | 86 ---------- .../wp-relations-group.template.html | 48 +++--- .../wp-relations-hierarchy-row.directive.ts | 112 ------------ .../wp-relations-hierarchy.directive.ts | 5 - ...directive.ts => wp-relations.component.ts} | 71 ++++---- .../wp-relations/wp-relations.interfaces.ts | 9 +- .../wp-relations/wp-relations.service.ts | 33 ++-- .../wp-relations/wp-relations.template.html | 42 ++--- .../relations-tab/relations-tab.html | 2 +- .../hal/dm-services/relations-dm.service.ts | 2 - 23 files changed, 417 insertions(+), 796 deletions(-) create mode 100644 frontend/app/components/wp-edit/field-controls/wp-edit-field-controls-ng1-wrapper.ts delete mode 100644 frontend/app/components/wp-relations/ng1-wp-relations-wrapper.directive.ts rename frontend/app/components/wp-relations/wp-relation-row/{wp-relation-row.directive.ts => wp-relation-row.component.ts} (69%) delete mode 100644 frontend/app/components/wp-relations/wp-relations-create/add-fixed-type.template.html delete mode 100644 frontend/app/components/wp-relations/wp-relations-create/dynamic-relation-types.template.html delete mode 100644 frontend/app/components/wp-relations/wp-relations-create/ng1-wp-relations-create.directive.ts create mode 100644 frontend/app/components/wp-relations/wp-relations-create/wp-relation-create.template.html delete mode 100644 frontend/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.directive.ts delete mode 100644 frontend/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.template.html rename frontend/app/components/wp-relations/wp-relations-create/{wp-relations-create.directive.ts => wp-relations-create.component.ts} (56%) rename frontend/app/components/wp-relations/{wp-single-relation.directive.ts => wp-relations-group/wp-relations-group.component.ts} (51%) delete mode 100644 frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.directive.ts delete mode 100644 frontend/app/components/wp-relations/wp-relations-hierarchy-row/wp-relations-hierarchy-row.directive.ts rename frontend/app/components/wp-relations/{wp-relations.directive.ts => wp-relations.component.ts} (76%) diff --git a/frontend/app/angular4-modules.ts b/frontend/app/angular4-modules.ts index 9bcbeda3a1..7eb8ad3b7d 100644 --- a/frontend/app/angular4-modules.ts +++ b/frontend/app/angular4-modules.ts @@ -48,7 +48,7 @@ import {WorkPackageTableRelationColumnsService} from 'core-components/wp-fast-ta import {WorkPackageTableSelection} from 'core-components/wp-fast-table/state/wp-table-selection.service'; import {WorkPackageTableSortByService} from 'core-components/wp-fast-table/state/wp-table-sort-by.service'; import {WorkPackageTableTimelineService} from 'core-components/wp-fast-table/state/wp-table-timeline.service'; -import {WorkPackageInlineCreateComponent,} from 'core-components/wp-inline-create/wp-inline-create.component'; +import {WorkPackageInlineCreateComponent} from 'core-components/wp-inline-create/wp-inline-create.component'; import {KeepTabService} from 'core-components/wp-single-view-tabs/keep-tab/keep-tab.service'; import {WpResizerDirective} from 'core-components/wp-resizer/wp-resizer.component'; import {SortHeaderDirective} from 'core-components/wp-table/sort-header/sort-header.directive'; @@ -73,7 +73,8 @@ import { I18nToken, TextileServiceToken, upgradeService, - upgradeServiceWithToken, WorkPackageServiceToken, + upgradeServiceWithToken, + WorkPackageServiceToken, wpMoreMenuServiceToken } from './angular4-transition-utils'; import {WpCustomActionComponent} from 'core-components/wp-custom-actions/wp-custom-actions/wp-custom-action.component'; @@ -129,7 +130,6 @@ import {WorkPackagesFullViewComponent} from 'core-components/routing/wp-full-vie import {WorkPackageActivityTabComponent} from 'core-components/wp-single-view-tabs/activity-panel/activity-tab.component'; import {WorkPackageRelationsTabComponent} from 'core-components/wp-single-view-tabs/relations-tab/relations-tab.component'; import {WorkPackageWatchersTabComponent} from 'core-components/wp-single-view-tabs/watchers-tab/watchers-tab.component'; -import {Ng1RelationsDirectiveWrapper} from 'core-components/wp-relations/ng1-wp-relations-wrapper.directive'; import {WorkPackageWatcherEntryComponent} from 'core-components/wp-single-view-tabs/watchers-tab/wp-watcher-entry.component'; import {WorkPackageNewFullViewComponent} from 'core-components/wp-new/wp-new-full-view.component'; import {WorkPackageTypeStatusComponent} from 'core-components/work-packages/wp-type-status/wp-type-status.component'; @@ -164,7 +164,6 @@ import {WorkPackageEditFieldService} from 'core-components/wp-edit/wp-edit-field import {WorkPackageEmbeddedTableComponent} from 'core-components/wp-table/embedded/wp-embedded-table.component'; import {OpTableActionsService} from 'core-components/wp-table/table-actions/table-actions.service'; import {WorkPackageRelationsHierarchyComponent} from 'core-components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive'; -import {Ng1RelationsCreateWrapper} from 'core-components/wp-relations/wp-relations-create/ng1-wp-relations-create.directive'; import {WpRelationsAutocompleteComponent} from 'core-components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.upgraded.component'; import {WpRelationAddChildComponent} from 'core-components/wp-relations/wp-relation-add-child/wp-relation-add-child'; import {WpRelationParentComponent} from 'core-components/wp-relations/wp-relations-parent/wp-relations-parent.component'; @@ -221,6 +220,11 @@ import {FocusWithinDirective} from "core-components/common/focus/focus-within.up import {AccessibleClickDirective} from "core-components/a11y/accessible-click.directive"; import {WorkPackageChildrenQueryComponent} from 'core-components/wp-relations/wp-relation-children/wp-children-query.component'; import {OpTitleService} from 'core-components/html/op-title.service'; +import {WorkPackageRelationsComponent} from "core-components/wp-relations/wp-relations.component"; +import {WorkPackageRelationsGroupComponent,} from "core-components/wp-relations/wp-relations-group/wp-relations-group.component"; +import {WorkPackageRelationRowComponent} from "core-components/wp-relations/wp-relation-row/wp-relation-row.component"; +import {Ng1FieldControlsWrapper} from "core-components/wp-edit/field-controls/wp-edit-field-controls-ng1-wrapper"; +import {WorkPackageRelationsCreateComponent} from "core-components/wp-relations/wp-relations-create/wp-relations-create.component"; @NgModule({ imports: [ @@ -252,7 +256,7 @@ import {OpTitleService} from 'core-components/html/op-title.service'; upgradeServiceWithToken('wpMoreMenuService', wpMoreMenuServiceToken), OpTitleService, TimezoneService, - upgradeService('wpRelations', WorkPackageRelationsService), + WorkPackageRelationsService, UrlParamsHelperService, WorkPackageCacheService, WorkPackageEditingService, @@ -398,8 +402,11 @@ import {OpTitleService} from 'core-components/html/op-title.service'; // Relations Tab WorkPackageRelationsTabComponent, - Ng1RelationsDirectiveWrapper, - Ng1RelationsCreateWrapper, + WorkPackageRelationsComponent, + WorkPackageRelationsGroupComponent, + WorkPackageRelationRowComponent, + WorkPackageRelationsCreateComponent, + Ng1FieldControlsWrapper, WorkPackageRelationsHierarchyComponent, WpRelationsAutocompleteComponent, WpRelationAddChildComponent, diff --git a/frontend/app/components/wp-edit/field-controls/wp-edit-field-controls-ng1-wrapper.ts b/frontend/app/components/wp-edit/field-controls/wp-edit-field-controls-ng1-wrapper.ts new file mode 100644 index 0000000000..776c8e6778 --- /dev/null +++ b/frontend/app/components/wp-edit/field-controls/wp-edit-field-controls-ng1-wrapper.ts @@ -0,0 +1,69 @@ +//-- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +// +// 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 doc/COPYRIGHT.rdoc for more details. +//++ + +// This Angular directive will act as an interface to the "upgraded" AngularJS component +import { + Directive, + DoCheck, + ElementRef, + EventEmitter, + Inject, + Injector, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges +} from '@angular/core'; +import {UpgradeComponent} from '@angular/upgrade/static'; + +@Directive({selector: 'ng1-wp-field-controls-wrapper'}) +export class Ng1FieldControlsWrapper extends UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy { + @Input() public fieldController:any; + @Input() public cancelTitle:string; + @Input() public saveTitle:string; + @Output() public onSave = new EventEmitter(); + @Output() public onCancel = new EventEmitter(); + + constructor(@Inject(ElementRef) elementRef:ElementRef, @Inject(Injector) injector:Injector) { + // We must pass the name of the directive as used by AngularJS to the super + super('wpEditFieldControls', elementRef, injector); + } + + // For this class to work when compiled with AoT, we must implement these lifecycle hooks + // because the AoT compiler will not realise that the super class implements them + ngOnInit() { super.ngOnInit(); } + + ngOnChanges(changes:SimpleChanges) { super.ngOnChanges(changes); } + + ngDoCheck() { super.ngDoCheck(); } + + ngOnDestroy() { super.ngOnDestroy(); } +} + diff --git a/frontend/app/components/wp-relations/ng1-wp-relations-wrapper.directive.ts b/frontend/app/components/wp-relations/ng1-wp-relations-wrapper.directive.ts deleted file mode 100644 index d2ac181504..0000000000 --- a/frontend/app/components/wp-relations/ng1-wp-relations-wrapper.directive.ts +++ /dev/null @@ -1,26 +0,0 @@ -// This Angular directive will act as an interface to the "upgraded" AngularJS component -// query-filters -import { - Directive, - DoCheck, - ElementRef, - Inject, - Injector, - Input, - OnChanges, - OnDestroy, - OnInit -} from '@angular/core'; -import {UpgradeComponent} from '@angular/upgrade/static'; -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; - -@Directive({selector: 'ng1-wp-relations-wrapper'}) -export class Ng1RelationsDirectiveWrapper extends UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy { - @Input('workPackage') workPackage:WorkPackageResource; - - constructor(@Inject(ElementRef) elementRef:ElementRef, @Inject(Injector) injector:Injector) { - // We must pass the name of the directive as used by AngularJS to the super - super('wpRelations', elementRef, injector); - } -} - diff --git a/frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.directive.ts b/frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.component.ts similarity index 69% rename from frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.directive.ts rename to frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.component.ts index d2788b0469..711607fa7b 100644 --- a/frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.directive.ts +++ b/frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.component.ts @@ -1,17 +1,24 @@ -import {wpDirectivesModule} from '../../../angular-modules'; import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service'; import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {WorkPackageRelationsService} from '../wp-relations.service'; -import {keyCodes} from 'core-components/common/keyCodes.enum'; import {PathHelperService} from 'core-components/common/path-helper/path-helper.service'; import {RelationResource} from 'core-app/modules/hal/resources/relation-resource'; +import {Component, ElementRef, Inject, Input, OnInit, ViewChild} from "@angular/core"; +import {I18nToken} from "core-app/angular4-transition-utils"; + +@Component({ + selector: 'wp-relation-row', + template: require('!!raw-loader!./wp-relation-row.template.html') +}) +export class WorkPackageRelationRowComponent implements OnInit { + @Input() public workPackage:WorkPackageResource; + @Input() public relatedWorkPackage:WorkPackageResource; + @Input() public groupByWorkPackageType:boolean; + + @ViewChild('relationDescriptionTextarea') readonly relationDescriptionTextarea:ElementRef; -class WpRelationRowDirectiveController { - public workPackage:WorkPackageResource; - public relatedWorkPackage:WorkPackageResource; public relationType:string; - public groupByWorkPackageType:boolean; public showRelationInfo:boolean = false; public showEditForm:boolean = false; public availableRelationTypes:{ name:string }[]; @@ -33,36 +40,33 @@ class WpRelationRowDirectiveController { }; public relation:RelationResource; - public text:Object; + public text = { + cancel: this.I18n.t('js.button_cancel'), + save: this.I18n.t('js.button_save'), + removeButton: this.I18n.t('js.relation_buttons.remove'), + description_label: this.I18n.t('js.relation_buttons.update_description'), + toggleDescription: this.I18n.t('js.relation_buttons.toggle_description'), + updateRelation: this.I18n.t('js.relation_buttons.update_relation'), + placeholder: { + description: this.I18n.t('js.placeholders.relation_description') + } + }; - constructor(protected $scope:ng.IScope, - protected $element:ng.IAugmentedJQuery, - protected $timeout:ng.ITimeoutService, - protected $http:ng.IHttpService, - protected wpCacheService:WorkPackageCacheService, + constructor(protected wpCacheService:WorkPackageCacheService, protected wpNotificationsService:WorkPackageNotificationService, protected wpRelations:WorkPackageRelationsService, - protected I18n:op.I18n, + @Inject(I18nToken) protected I18n:op.I18n, protected PathHelper:PathHelperService) { + } + ngOnInit() { this.relation = this.relatedWorkPackage.relatedBy as RelationResource; - this.text = { - cancel: I18n.t('js.button_cancel'), - save: I18n.t('js.button_save'), - removeButton: I18n.t('js.relation_buttons.remove'), - description_label: I18n.t('js.relation_buttons.update_description'), - toggleDescription: I18n.t('js.relation_buttons.toggle_description'), - updateRelation: I18n.t('js.relation_buttons.update_relation'), - placeholder: { - description: I18n.t('js.placeholders.relation_description') - } - }; this.userInputs.newRelationText = this.relation.description || ''; this.availableRelationTypes = RelationResource.LOCALIZED_RELATION_TYPES(false); this.selectedRelationType = _.find(this.availableRelationTypes, {'name': this.relation.normalizedType(this.workPackage)}) as RelationResource; - }; + } /** * Return the normalized relation type for the work package we're viewing. @@ -79,9 +83,9 @@ class WpRelationRowDirectiveController { public startDescriptionEdit() { this.userInputs.showDescriptionEditForm = true; - this.$timeout(() => { - var textarea = this.$element.find('.wp-relation--description-textarea'); - var textlen = textarea.val().length; + setTimeout(() => { + const textarea = jQuery(this.relationDescriptionTextarea.nativeElement); + const textlen = textarea.val().length; // Focus and set cursor to end textarea.focus(); @@ -124,9 +128,7 @@ class WpRelationRowDirectiveController { } public cancelRelationTypeEditOnEscape(evt:JQueryEventObject) { - if (evt.which === keyCodes.ESCAPE) { - this.userInputs.showRelationTypesForm = false; - } + this.userInputs.showRelationTypesForm = false; } public saveRelationType() { @@ -154,28 +156,8 @@ class WpRelationRowDirectiveController { .then(() => { this.wpCacheService.updateWorkPackage(this.relatedWorkPackage); this.wpNotificationsService.showSave(this.relatedWorkPackage); - this.$timeout(() => { - angular.element('#relation--add-relation').focus(); - }); }) .catch((err:any) => this.wpNotificationsService.handleErrorResponse(err, this.relatedWorkPackage)); } } - -function WpRelationRowDirective($timeout:ng.ITimeoutService):any { - return { - restrict: 'E', - templateUrl: '/components/wp-relations/wp-relation-row/wp-relation-row.template.html', - scope: { - workPackage: '=', - groupByWorkPackageType: '=', - relatedWorkPackage: '=' - }, - controller: WpRelationRowDirectiveController, - controllerAs: '$ctrl', - bindToController: true - }; -} - -wpDirectivesModule.directive('wpRelationRow', WpRelationRowDirective); diff --git a/frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.template.html b/frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.template.html index 95607838d6..edf4430993 100644 --- a/frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.template.html +++ b/frontend/app/components/wp-relations/wp-relation-row/wp-relation-row.template.html @@ -1,95 +1,96 @@ -
-
+
-
+
+ *ngIf="!userInputs.showRelationTypesForm"> - - - + + + -
- -
-
+
+ +
+
- +
+ + +
-
- -
- - -
-
-
+
+ + + +
-
- - - - - - -
+
+ + + + + +
+
-
-
-
-
+
+
+
+
- - -
+ #relationDescriptionTextarea + autofocus + class="wp-relation--description-textarea" + name="description" + (keyup)="handleDescriptionKey($event)" + [(ngModel)]="userInputs.newRelationText"> + +
+
diff --git a/frontend/app/components/wp-relations/wp-relations-create/add-fixed-type.template.html b/frontend/app/components/wp-relations/wp-relations-create/add-fixed-type.template.html deleted file mode 100644 index 21c46c81b5..0000000000 --- a/frontend/app/components/wp-relations/wp-relations-create/add-fixed-type.template.html +++ /dev/null @@ -1,27 +0,0 @@ -
-
-
- - -
-
- - - - - - -
-
-
diff --git a/frontend/app/components/wp-relations/wp-relations-create/dynamic-relation-types.template.html b/frontend/app/components/wp-relations/wp-relations-create/dynamic-relation-types.template.html deleted file mode 100644 index 9bbd770aec..0000000000 --- a/frontend/app/components/wp-relations/wp-relations-create/dynamic-relation-types.template.html +++ /dev/null @@ -1,58 +0,0 @@ -
- -
-
-
- - -
-
- - -
-
- - - - - - -
-
-
-
- - - - diff --git a/frontend/app/components/wp-relations/wp-relations-create/ng1-wp-relations-create.directive.ts b/frontend/app/components/wp-relations/wp-relations-create/ng1-wp-relations-create.directive.ts deleted file mode 100644 index 277536bdaa..0000000000 --- a/frontend/app/components/wp-relations/wp-relations-create/ng1-wp-relations-create.directive.ts +++ /dev/null @@ -1,28 +0,0 @@ -// This Angular directive will act as an interface to the "upgraded" AngularJS component -// query-filters -import { - Directive, - DoCheck, - ElementRef, - Inject, - Injector, - Input, - OnChanges, - OnDestroy, - OnInit -} from '@angular/core'; -import {UpgradeComponent} from '@angular/upgrade/static'; -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; - -@Directive({selector: 'ng1-wp-relations-create'}) -export class Ng1RelationsCreateWrapper extends UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy { - @Input('workPackage') workPackage:WorkPackageResource; - @Input('fixedRelationType') fixedRelationType:string; - @Input('externalFormToggle') externalFormToggle:boolean; - - constructor(@Inject(ElementRef) elementRef:ElementRef, @Inject(Injector) injector:Injector) { - // We must pass the name of the directive as used by AngularJS to the super - super('wpRelationsCreate', elementRef, injector); - } -} - diff --git a/frontend/app/components/wp-relations/wp-relations-create/wp-relation-create.template.html b/frontend/app/components/wp-relations/wp-relations-create/wp-relation-create.template.html new file mode 100644 index 0000000000..98879e1ca6 --- /dev/null +++ b/frontend/app/components/wp-relations/wp-relations-create/wp-relation-create.template.html @@ -0,0 +1,64 @@ +
+
+
+ +
+
+
+
+
+ + +
+
+ + +
+
+ + + + + + +
+
+
+
+ + + + diff --git a/frontend/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.directive.ts b/frontend/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.directive.ts deleted file mode 100644 index f3fc0e8cf0..0000000000 --- a/frontend/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.directive.ts +++ /dev/null @@ -1,113 +0,0 @@ -//-- copyright -// OpenProject is a project management system. -// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) -// -// 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 doc/COPYRIGHT.rdoc for more details. -//++ - -import {wpDirectivesModule} from '../../../../angular-modules'; -import {CollectionResource} from 'core-app/modules/hal/resources/collection-resource'; -import {LoadingIndicatorService} from '../../../common/loading-indicator/loading-indicator.service'; -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; - -function wpRelationsAutocompleteDirective( - $q:ng.IQService, - PathHelper:any, - $http:ng.IHttpService, - loadingIndicator:LoadingIndicatorService, - I18n:op.I18n) { - return { - restrict: 'E', - templateUrl: '/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.template.html', - scope: { - selectedWpId: '=', - loadingPromiseName: '@', - selectedRelationType: '=', - filterCandidatesFor: '@', - workPackage: '=' - }, - link: function (scope:any, element:ng.IAugmentedJQuery, attrs:ng.IAttributes) { - scope.text = { - placeholder: I18n.t('js.relations_autocomplete.placeholder') - }; - scope.options = []; - scope.relatedWps = []; - - let input = jQuery('.wp-relations--autocomplete'); - let selected = false; - - input.autocomplete({ - delay: 250, - autoFocus: false, // Accessibility! - appendTo: '.detail-panel--autocomplete-target', - source: (request:{ term:string }, response:Function) => { - autocompleteWorkPackages(request.term).then((values) => { - selected = false; - response(values.map(wp => { - return { workPackage: wp, value: getIdentifier(wp) }; - })); - }); - }, - select: (evt, ui:any) => { - scope.$evalAsync(() => { - selected = true; - scope.selectedWpId = ui.item.workPackage.id; - }); - }, - minLength: 0 - }).focus(() => !selected && input.autocomplete('search', input.val())); - - function getIdentifier(workPackage:WorkPackageResource):string { - if (workPackage) { - return `#${workPackage.id} - ${workPackage.subject}`; - } else { - return ''; - } - } - - async function autocompleteWorkPackages(query:string):Promise { - element.find('.ui-autocomplete--loading').show(); - return scope.workPackage.available_relation_candidates.$link.$fetch({ - query: query, - type: scope.filterCandidatesFor || scope.selectedRelationType - }).then((collection:CollectionResource) => { - scope.noResults = collection.count === 0; - element.find('.ui-autocomplete--loading').hide(); - return collection.elements || []; - }).catch(() => { - element.find('.ui-autocomplete--loading').hide(); - return []; - }); - }; - - scope.$watch('noResults', (noResults:boolean) => { - if (noResults) { - scope.selectedWpId = null; - } - }); - } - }; -} - -wpDirectivesModule.directive('wpRelationsAutocomplete', wpRelationsAutocompleteDirective); diff --git a/frontend/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.template.html b/frontend/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.template.html deleted file mode 100644 index 29c9c3db4f..0000000000 --- a/frontend/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.template.html +++ /dev/null @@ -1,15 +0,0 @@ -
- - -
diff --git a/frontend/app/components/wp-relations/wp-relations-create/wp-relations-create.directive.ts b/frontend/app/components/wp-relations/wp-relations-create/wp-relations-create.component.ts similarity index 56% rename from frontend/app/components/wp-relations/wp-relations-create/wp-relations-create.directive.ts rename to frontend/app/components/wp-relations/wp-relations-create/wp-relations-create.component.ts index 6bfba1521a..d0e583ee70 100644 --- a/frontend/app/components/wp-relations/wp-relations-create/wp-relations-create.directive.ts +++ b/frontend/app/components/wp-relations/wp-relations-create/wp-relations-create.component.ts @@ -1,50 +1,39 @@ -import {wpDirectivesModule} from '../../../angular-modules'; import {RelationResource} from 'core-app/modules/hal/resources/relation-resource'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service'; import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service'; -import {WorkPackageRelationsHierarchyService} from '../wp-relations-hierarchy/wp-relations-hierarchy.service'; import {WorkPackageRelationsService} from '../wp-relations.service'; +import {Component, ElementRef, Inject, Input, ViewChild} from "@angular/core"; +import {I18nToken} from "../../../angular4-transition-utils"; -export class WorkPackageRelationsCreateController { +@Component({ + selector: 'wp-relations-create', + template: require('!!raw-loader!./wp-relation-create.template.html') +}) +export class WorkPackageRelationsCreateComponent { + @Input() readonly workPackage:WorkPackageResource; + @ViewChild('focusAfterSave') readonly focusAfterSave:ElementRef; public showRelationsCreateForm:boolean = false; - public workPackage:WorkPackageResource; public selectedRelationType:string = RelationResource.DEFAULT(); public selectedWpId:string; - public externalFormToggle:boolean; - public fixedRelationType:string; public relationTypes = RelationResource.LOCALIZED_RELATION_TYPES(false); public isDisabled = false; - constructor(protected I18n:op.I18n, - protected $scope:ng.IScope, - protected $rootScope:ng.IRootScopeService, - protected $element:ng.IAugmentedJQuery, - protected $timeout:ng.ITimeoutService, - protected wpRelations:WorkPackageRelationsService, - protected wpRelationsHierarchyService:WorkPackageRelationsHierarchyService, - protected wpNotificationsService:WorkPackageNotificationService, - protected wpCacheService:WorkPackageCacheService) { - } - - $onInit() { - if (this.fixedRelationType) { - this.selectedRelationType = this.fixedRelationType; - } - - if (this.externalFormToggle) { - this.showRelationsCreateForm = this.externalFormToggle; - } - } - public text = { save: this.I18n.t('js.relation_buttons.save'), abort: this.I18n.t('js.relation_buttons.abort'), addNewRelation: this.I18n.t('js.relation_buttons.add_new_relation') }; + constructor(@Inject(I18nToken) protected I18n:op.I18n, + protected wpRelations:WorkPackageRelationsService, + protected wpNotificationsService:WorkPackageNotificationService, + protected wpCacheService:WorkPackageCacheService) { + } + + public createRelation() { if (!this.selectedRelationType || !this.selectedWpId) { @@ -57,6 +46,10 @@ export class WorkPackageRelationsCreateController { .then(() => this.isDisabled = false); } + public updateSelectedId(workPackageId:string) { + this.selectedWpId = workPackageId; + } + protected async createCommonRelation() { return this.wpRelations.addCommonRelation(this.workPackage, this.selectedRelationType, @@ -73,36 +66,13 @@ export class WorkPackageRelationsCreateController { public toggleRelationsCreateForm() { this.showRelationsCreateForm = !this.showRelationsCreateForm; - this.externalFormToggle = !this.externalFormToggle; - this.$timeout(() => { + setTimeout(() => { if (!this.showRelationsCreateForm) { // Reset value this.selectedWpId = ''; - this.$element.find('.-focus-after-save').first().focus(); + this.focusAfterSave.nativeElement.focus(); } }); } } - -function wpRelationsCreate():any { - return { - restrict: 'E', - - templateUrl: (el:ng.IAugmentedJQuery, attrs:ng.IAttributes) => { - return '/components/wp-relations/wp-relations-create/' + attrs['template'] + '.template.html'; - }, - - scope: { - workPackage: '=?', - fixedRelationType: '@?', - externalFormToggle: '=?' - }, - - controller: WorkPackageRelationsCreateController, - bindToController: true, - controllerAs: '$ctrl', - }; -} - -wpDirectivesModule.directive('wpRelationsCreate', wpRelationsCreate); diff --git a/frontend/app/components/wp-relations/wp-single-relation.directive.ts b/frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.component.ts similarity index 51% rename from frontend/app/components/wp-relations/wp-single-relation.directive.ts rename to frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.component.ts index 657728fdbc..c471e33fad 100644 --- a/frontend/app/components/wp-relations/wp-single-relation.directive.ts +++ b/frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.component.ts @@ -26,35 +26,48 @@ // See doc/COPYRIGHT.rdoc for more details. //++ -import {wpDirectivesModule} from '../../angular-modules'; -import {PathHelperService} from 'core-components/common/path-helper/path-helper.service'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -/** - * Contains methods and attributes shared - * between common relations and parent-child relations - */ -export class WorkPackageSingleRelationController { - public workPackagePath = this.PathHelper.workPackagePath.bind(this.PathHelper); - - constructor(protected PathHelper:PathHelperService) { +import {Component, ElementRef, EventEmitter, Inject, Input, Output, ViewChild} from "@angular/core"; +import {I18nToken} from "core-app/angular4-transition-utils"; + + +@Component({ + selector: 'wp-relations-group', + template: require('!!raw-loader!./wp-relations-group.template.html') +}) +export class WorkPackageRelationsGroupComponent { + @Input() public relatedWorkPackages:WorkPackageResource[]; + @Input() public workPackage:WorkPackageResource; + @Input() public header:string; + @Input() public firstGroup:boolean; + @Input() public groupByWorkPackageType:boolean; + + @Output() public onToggleGroupBy = new EventEmitter(); + + @ViewChild('wpRelationGroupByToggler') readonly toggleElement:ElementRef; + + public text = { + groupByType: this.I18n.t('js.relation_buttons.group_by_wp_type'), + groupByRelation: this.I18n.t('js.relation_buttons.group_by_relation_type') + }; + + constructor( + @Inject(I18nToken) public I18n:op.I18n) { } - public getFullIdentifier(workPackage:WorkPackageResource, hideType?:boolean) { - if (hideType) { - return workPackage.subject; + public get togglerText() { + if (this.groupByWorkPackageType) { + return this.text.groupByRelation; + } else { + return this.text.groupByType; } - - return workPackage.subjectWithType(); } -} -function wpSingleRelationDirective():any { - return { - restrict: 'A', - controller: WorkPackageSingleRelationController, - controllerAs: 'singleRelationCtrl', - bindToController: true, - }; -} + public toggleButton() { + this.onToggleGroupBy.emit(); -wpDirectivesModule.directive('wpSingleRelation', wpSingleRelationDirective); + setTimeout(() => { + this.toggleElement.nativeElement.focus(); + }, 20); + } +} diff --git a/frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.directive.ts b/frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.directive.ts deleted file mode 100644 index 67aff47f41..0000000000 --- a/frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.directive.ts +++ /dev/null @@ -1,86 +0,0 @@ -//-- copyright -// OpenProject is a project management system. -// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) -// -// 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 doc/COPYRIGHT.rdoc for more details. -//++ - -import {wpDirectivesModule} from '../../../angular-modules'; -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {WorkPackageRelationsController} from '../wp-relations.directive'; - - -export class WorkPackageRelationsGroupController { - public relatedWorkPackages:Array; - public workPackage:WorkPackageResource; - public header:string; - public firstGroup:boolean; - public groupByWorkPackageType:boolean; - public text:Object; - public relationsCtrl: WorkPackageRelationsController; - - constructor(public $element:ng.IAugmentedJQuery, - public $timeout:ng.ITimeoutService, - public I18n:op.I18n) { - this.text = { - groupByType: I18n.t('js.relation_buttons.group_by_wp_type'), - groupByRelation: I18n.t('js.relation_buttons.group_by_relation_type') - }; - } - - public toggleButton() { - this.relationsCtrl.toggleGroupBy(); - this.$timeout(() => { - this.$element.find('#wp-relation-group-by-toggle').focus(); - }); - } -} - -function wpRelationsGroupDirective():any { - return { - restrict: 'E', - templateUrl: '/components/wp-relations/wp-relations-group/wp-relations-group.template.html', - - scope: { - header: '=', - firstGroup: '=', - workPackage: '=', - groupByWorkPackageType: '=', - relatedWorkPackages: '=' - }, - - link: (scope:any, - element:ng.IAugmentedJQuery, - attrs:any, - controllers: [WorkPackageRelationsController]) => { - scope.$ctrl.relationsCtrl = controllers[0]; - }, - controller: WorkPackageRelationsGroupController, - controllerAs: '$ctrl', - require: ['^wpRelations'], - bindToController: true, - }; -} - -wpDirectivesModule.directive('wpRelationsGroup', wpRelationsGroupDirective); diff --git a/frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.template.html b/frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.template.html index 6ac2cbb8d3..c83f584287 100644 --- a/frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.template.html +++ b/frontend/app/components/wp-relations/wp-relations-group/wp-relations-group.template.html @@ -1,28 +1,30 @@
-
-
-

- {{ $ctrl.header }} -

-
-
-
- - - - -
-
+
+
+

+

- -
- +
+
+ + + +
+
+ +
+ +
diff --git a/frontend/app/components/wp-relations/wp-relations-hierarchy-row/wp-relations-hierarchy-row.directive.ts b/frontend/app/components/wp-relations/wp-relations-hierarchy-row/wp-relations-hierarchy-row.directive.ts deleted file mode 100644 index 6c1a7ddaf9..0000000000 --- a/frontend/app/components/wp-relations/wp-relations-hierarchy-row/wp-relations-hierarchy-row.directive.ts +++ /dev/null @@ -1,112 +0,0 @@ -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {wpDirectivesModule} from '../../../angular-modules'; -import {WorkPackageRelationsHierarchyService} from '../wp-relations-hierarchy/wp-relations-hierarchy.service'; -import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service'; -import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service'; -import {scopedObservable} from '../../../helpers/angular-rx-utils'; -import {PathHelperService} from 'core-components/common/path-helper/path-helper.service'; - -class WpRelationsHierarchyRowDirectiveController { - public workPackage:WorkPackageResource; - public relatedWorkPackage:WorkPackageResource; - public relationType:any; - public showEditForm:boolean = false; - public workPackagePath = this.PathHelper.workPackagePath.bind(this.PathHelper); - public canModifyHierarchy:boolean = false; - - constructor(protected $scope:ng.IScope, - protected $timeout:ng.ITimeoutService, - protected wpRelationsHierarchyService:WorkPackageRelationsHierarchyService, - protected wpCacheService:WorkPackageCacheService, - protected wpNotificationsService:WorkPackageNotificationService, - protected PathHelper:PathHelperService, - protected I18n:op.I18n, - protected $q:ng.IQService) { - - this.canModifyHierarchy = !!this.workPackage.changeParent; - - if (this.relatedWorkPackage) { - scopedObservable($scope, this.wpCacheService.state(this.relatedWorkPackage.id).values$()) - .subscribe((wp) => this.relatedWorkPackage = wp); - } - } - - public text = { - change_parent:this.I18n.t('js.relation_buttons.change_parent'), - remove_parent:this.I18n.t('js.relation_buttons.remove_parent'), - remove_child:this.I18n.t('js.relation_buttons.remove_child'), - remove:this.I18n.t('js.relation_buttons.remove'), - parent:this.I18n.t('js.relation_labels.parent'), - children:this.I18n.t('js.relation_labels.children') - }; - - public get relationReady() { - return this.relatedWorkPackage && this.relatedWorkPackage.$loaded; - } - - public get relationClassName() { - if (this.isCurrentElement()) { - return 'self'; - } - - return this.relationType; - } - - public removeRelation() { - if (this.relationType === 'child') { - this.removeChild(); - - } else if (this.relationType === 'parent') { - this.removeParent(); - } - } - - public isCurrentElement():boolean { - return (this.relationType !== 'child' && this.relationType !== 'parent'); - } - - public isParent() { - return this.relationType === 'parent'; - } - - protected removeChild() { - this.wpRelationsHierarchyService - .removeChild(this.relatedWorkPackage) - .then(() => { - this.wpCacheService.loadWorkPackage(this.workPackage.id, true); - this.wpNotificationsService.showSave(this.workPackage); - this.$timeout(() => { - angular.element('#hierarchy--add-exisiting-child').focus(); - }); - }); - } - - protected removeParent() { - this.wpRelationsHierarchyService - .removeParent(this.workPackage) - .then(() => { - this.wpNotificationsService.showSave(this.workPackage); - this.$timeout(() => { - angular.element('#hierarchy--add-parent').focus(); - }); - }); - } -} - -function WpRelationsHierarchyRowDirective():any { - return { - restrict:'E', - templateUrl:'/components/wp-relations/wp-relations-hierarchy-row/wp-relations-hierarchy-row.template.html', - scope:{ - indentBy:'@?', - workPackage:'=', - relatedWorkPackage:'=?', - relationType:'@' - }, - controller:WpRelationsHierarchyRowDirectiveController, - controllerAs:'$ctrl', - bindToController:true - }; -} - -wpDirectivesModule.directive('wpRelationsHierarchyRow', WpRelationsHierarchyRowDirective); diff --git a/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts b/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts index 7b8ab2263f..c8bfead94f 100644 --- a/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts +++ b/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts @@ -102,8 +102,3 @@ export class WorkPackageRelationsHierarchyComponent implements OnInit, OnDestroy // nothing to do } } - -opWorkPackagesModule.directive( - 'wpRelationsHierarchy', - downgradeComponent({ component: WorkPackageRelationsHierarchyComponent }) -); diff --git a/frontend/app/components/wp-relations/wp-relations.directive.ts b/frontend/app/components/wp-relations/wp-relations.component.ts similarity index 76% rename from frontend/app/components/wp-relations/wp-relations.directive.ts rename to frontend/app/components/wp-relations/wp-relations.component.ts index 60c2c9ce78..721f3ce8ea 100644 --- a/frontend/app/components/wp-relations/wp-relations.directive.ts +++ b/frontend/app/components/wp-relations/wp-relations.component.ts @@ -26,23 +26,28 @@ // See doc/COPYRIGHT.rdoc for more details. //++ -import {StateService} from '@uirouter/core'; import {Observable} from 'rxjs/Observable'; import {zip} from 'rxjs/observable/zip'; -import {take} from 'rxjs/operators'; -import {wpDirectivesModule} from '../../angular-modules'; -import {scopedObservable} from '../../helpers/angular-rx-utils'; +import {take, takeUntil} from 'rxjs/operators'; import {RelationResource} from 'core-app/modules/hal/resources/relation-resource'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {WorkPackageCacheService} from '../work-packages/work-package-cache.service'; import {RelatedWorkPackagesGroup} from './wp-relations.interfaces'; import {RelationsStateValue, WorkPackageRelationsService} from './wp-relations.service'; - - -export class WorkPackageRelationsController { - public relationGroups:RelatedWorkPackagesGroup; +import {Component, Inject, Input, OnDestroy, OnInit} from "@angular/core"; +import {I18nToken} from "core-app/angular4-transition-utils"; +import {componentDestroyed} from "ng2-rx-componentdestroyed"; + + +@Component({ + selector: 'wp-relations', + template: require('!!raw-loader!./wp-relations.template.html') +}) +export class WorkPackageRelationsComponent implements OnInit, OnDestroy { + @Input() public workPackage:WorkPackageResource; + public relationGroups:RelatedWorkPackagesGroup = {}; + public relationGroupKeys:string[] = []; public relationsPresent:boolean = false; - public workPackage:WorkPackageResource; public canAddRelation:boolean; // By default, group by relation type @@ -53,19 +58,19 @@ export class WorkPackageRelationsController { }; public currentRelations:WorkPackageResource[] = []; - constructor(protected $scope:ng.IScope, - protected $q:ng.IQService, - protected $state:StateService, - protected I18n:op.I18n, - protected wpRelations:WorkPackageRelationsService, - protected wpCacheService:WorkPackageCacheService) { + constructor( + @Inject(I18nToken) readonly I18n:op.I18n, + readonly wpRelations:WorkPackageRelationsService, + readonly wpCacheService:WorkPackageCacheService) { } - $onInit() { + ngOnInit() { this.canAddRelation = !!this.workPackage.addRelation; - scopedObservable(this.$scope, - this.wpRelations.state(this.workPackage.id).values$()) + this.wpRelations.state(this.workPackage.id).values$() + .pipe( + takeUntil(componentDestroyed(this)) + ) .subscribe((relations:RelationsStateValue) => { this.loadedRelations(relations); }); @@ -73,16 +78,22 @@ export class WorkPackageRelationsController { this.wpRelations.require(this.workPackage.id); // Listen for changes to this WP. - scopedObservable(this.$scope, - this.wpCacheService.loadWorkPackage(this.workPackage.id).values$()) + this.wpCacheService.loadWorkPackage(this.workPackage.id).values$() + .pipe( + takeUntil(componentDestroyed(this)) + ) .subscribe((wp:WorkPackageResource) => { this.workPackage = wp; }); } + ngOnDestroy() { + // Nothing to do, interface compliance. + } + private getRelatedWorkPackages(workPackageIds:string[]):Observable { let observablesToGetZipped:Observable[] = workPackageIds.map(wpId => { - return scopedObservable(this.$scope, this.wpCacheService.loadWorkPackage(wpId).values$()); + return this.wpCacheService.loadWorkPackage(wpId).values$(); }); return zip(...observablesToGetZipped); @@ -112,6 +123,7 @@ export class WorkPackageRelationsController { return this.I18n.t('js.relation_labels.' + normalizedType); } }); + this.relationGroupKeys = _.keys(this.relationGroups); this.relationsPresent = _.size(this.relationGroups) > 0; } @@ -144,20 +156,3 @@ export class WorkPackageRelationsController { }); } } - -function wpRelationsDirective():any { - return { - restrict: 'E', - templateUrl: '/components/wp-relations/wp-relations.template.html', - - scope: { - workPackage: '=' - }, - - controller: WorkPackageRelationsController, - controllerAs: '$ctrl', - bindToController: true - }; -} - -wpDirectivesModule.directive('wpRelations', wpRelationsDirective); diff --git a/frontend/app/components/wp-relations/wp-relations.interfaces.ts b/frontend/app/components/wp-relations/wp-relations.interfaces.ts index 0f8a66e9a8..97c59739f8 100644 --- a/frontend/app/components/wp-relations/wp-relations.interfaces.ts +++ b/frontend/app/components/wp-relations/wp-relations.interfaces.ts @@ -1,11 +1,4 @@ -import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {RelationResource} from 'core-app/modules/hal/resources/relation-resource'; - export interface RelatedWorkPackagesGroup { - [key: string] : any; -} - -export interface RelationTitle { - [key: string]: string; + [key:string]:any; } diff --git a/frontend/app/components/wp-relations/wp-relations.service.ts b/frontend/app/components/wp-relations/wp-relations.service.ts index 835d535815..92e4ef9bf4 100644 --- a/frontend/app/components/wp-relations/wp-relations.service.ts +++ b/frontend/app/components/wp-relations/wp-relations.service.ts @@ -6,6 +6,7 @@ import {StateCacheService} from '../states/state-cache.service'; import {RelationResource} from 'core-app/modules/hal/resources/relation-resource'; import {RelationsDmService} from 'core-app/modules/hal/dm-services/relations-dm.service'; import {PathHelperService} from 'core-components/common/path-helper/path-helper.service'; +import {Injectable} from "@angular/core"; export type RelationsStateValue = { [relationId:number]:RelationResource }; @@ -20,6 +21,7 @@ class RelationStateGroup extends StatesGroup { } } +@Injectable() export class WorkPackageRelationsService extends StateCacheService { private relationStates:RelationStateGroup; @@ -27,7 +29,6 @@ export class WorkPackageRelationsService extends StateCacheService reject(error)); + .catch(reject); }); } - protected loadAll(ids:string[]) { - const deferred = this.$q.defer(); - - this.relationsDm - .loadInvolved(ids) - .then((elements:RelationResource[]) => { - this.clearSome(...ids); - this.accumulateRelationsFromInvolved(ids, elements); - deferred.resolve(); - }); - - return deferred.promise; + protected async loadAll(ids:string[]) { + return new Promise((resolve, reject) => { + this.relationsDm + .loadInvolved(ids) + .then((elements:RelationResource[]) => { + this.clearSome(...ids); + this.accumulateRelationsFromInvolved(ids, elements); + resolve(); + }) + .catch(reject); + }); } /** @@ -95,7 +95,7 @@ export class WorkPackageRelationsService extends StateCacheService { this.insertIntoStates(savedRelation); @@ -186,7 +186,4 @@ export class WorkPackageRelationsService extends StateCacheService -
-
-
-

-

-
-
+
+
+
+

+

+
+
-
- -
- +
+ +
+ + - +
diff --git a/frontend/app/components/wp-single-view-tabs/relations-tab/relations-tab.html b/frontend/app/components/wp-single-view-tabs/relations-tab/relations-tab.html index 728982f20b..270c2c0761 100644 --- a/frontend/app/components/wp-single-view-tabs/relations-tab/relations-tab.html +++ b/frontend/app/components/wp-single-view-tabs/relations-tab/relations-tab.html @@ -1,6 +1,6 @@
- +
diff --git a/frontend/app/modules/hal/dm-services/relations-dm.service.ts b/frontend/app/modules/hal/dm-services/relations-dm.service.ts index 3a1c2c0f0f..9e7455df75 100644 --- a/frontend/app/modules/hal/dm-services/relations-dm.service.ts +++ b/frontend/app/modules/hal/dm-services/relations-dm.service.ts @@ -66,5 +66,3 @@ export class RelationsDmService { .then((collection:CollectionResource) => collection.elements); } } - -opServicesModule.service('relationsDm', downgradeInjectable(RelationsDmService)); From 1ffeedbd5d93c38e8a23fc8119b316ff06dcce99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 15 May 2018 11:20:09 +0200 Subject: [PATCH 4/5] [27606] Allow inline-create in embedded children tables https://community.openproject.com/wp/27606 --- .../layout/_work_package_table_embedded.sass | 1 + app/policies/work_package_policy.rb | 2 + .../wp-view-base/wp-view-base.controller.ts | 5 +++ .../work-package-filter-values.ts | 36 +++++++++++++----- .../wp-inline-create.component.ts | 19 +++++----- .../wp-children-query.html | 3 +- .../wp-relations-hierarchy.template.html | 38 +++++++++---------- .../embedded/wp-embedded-table.component.ts | 10 ++++- .../wp-table/wp-table-configuration.ts | 7 +++- .../details/relations/hierarchy_spec.rb | 21 +++++++++- .../components/work_packages/relations.rb | 13 +++++++ spec/support/pages/work_packages_table.rb | 8 ++++ 12 files changed, 119 insertions(+), 44 deletions(-) diff --git a/app/assets/stylesheets/layout/_work_package_table_embedded.sass b/app/assets/stylesheets/layout/_work_package_table_embedded.sass index fad7cb1263..9038b0dc37 100644 --- a/app/assets/stylesheets/layout/_work_package_table_embedded.sass +++ b/app/assets/stylesheets/layout/_work_package_table_embedded.sass @@ -33,6 +33,7 @@ .work-package-table--container contain: initial !important + overflow: visible .generic-table--header, .generic-table--sort-header diff --git a/app/policies/work_package_policy.rb b/app/policies/work_package_policy.rb index da5bce0ad0..464be260e4 100644 --- a/app/policies/work_package_policy.rb +++ b/app/policies/work_package_policy.rb @@ -102,6 +102,8 @@ class WorkPackagePolicy < BasePolicy end def type_active_in_project?(work_package) + return false unless work_package.project + @type_active_cache ||= Hash.new do |hash, project| hash[project] = project.types.pluck(:id) end diff --git a/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts b/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts index 02c8d1bf79..c92a1fc52f 100644 --- a/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts +++ b/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts @@ -40,6 +40,7 @@ import {WorkPackageTableRefreshService} from '../../wp-table/wp-table-refresh-re import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {ProjectCacheService} from 'core-components/projects/project-cache.service'; import {OpTitleService} from 'core-components/html/op-title.service'; +import {AuthorisationService} from "core-components/common/model-auth/model-auth.service"; export class WorkPackageViewController implements OnDestroy { @@ -52,6 +53,7 @@ export class WorkPackageViewController implements OnDestroy { protected wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService); protected wpTableFocus:WorkPackageTableFocusService = this.injector.get(WorkPackageTableFocusService); protected projectCacheService:ProjectCacheService = this.injector.get(ProjectCacheService); + protected authorisationService:AuthorisationService = this.injector.get(AuthorisationService); // Static texts public text:any = {}; @@ -109,6 +111,9 @@ export class WorkPackageViewController implements OnDestroy { this.projectIdentifier = this.workPackage.project.identifier; }); + // Set authorisation data + this.authorisationService.initModelAuth('work_package', this.workPackage.$links); + // Push the current title this.titleService.setFirstPart(this.workPackage.subjectWithType(20)); diff --git a/frontend/app/components/wp-edit-form/work-package-filter-values.ts b/frontend/app/components/wp-edit-form/work-package-filter-values.ts index 56d37f2167..8aaef6932e 100644 --- a/frontend/app/components/wp-edit-form/work-package-filter-values.ts +++ b/frontend/app/components/wp-edit-form/work-package-filter-values.ts @@ -3,6 +3,7 @@ import {CollectionResource} from 'core-app/modules/hal/resources/collection-reso import {FormResource} from 'core-app/modules/hal/resources/form-resource'; import {WorkPackageChangeset} from './work-package-changeset'; import {QueryFilterInstanceResource} from 'core-app/modules/hal/resources/query-filter-instance-resource'; +import {all} from "@uirouter/core"; export class WorkPackageFilterValues { @@ -43,16 +44,7 @@ export class WorkPackageFilterValues { private async setAllowedValueFor(form:FormResource, field:string, value:string|HalResource) { return this.allowedValuesFor(form, field).then((allowedValues) => { - let newValue; - - if ((value as HalResource)['$href']) { - newValue = _.find(allowedValues, - (entry:any) => entry.$href === (value as HalResource).$href); - } else if (allowedValues) { - newValue = _.find(allowedValues, (entry:any) => entry === value); - } else { - newValue = value; - } + let newValue = this.findSpecialValue(value, field) || this.findAllowedValue(value, allowedValues); if (newValue) { this.changeset.setValue(field, newValue); @@ -61,6 +53,30 @@ export class WorkPackageFilterValues { }); } + /** + * Returns special values for which no allowed values exist (e.g., parent ID in embedded queries) + * @param {string | HalResource} value + * @param {string} field + */ + private findSpecialValue(value:string|HalResource, field:string):string|HalResource|undefined { + if (field === 'parent') { + return value; + } + + return undefined; + } + + private findAllowedValue(value:string|HalResource, allowedValues:HalResource[]) { + if (value instanceof HalResource && !!value.$href) { + return _.find(allowedValues, + (entry:any) => entry.$href === value.$href); + } else if (allowedValues) { + return _.find(allowedValues, (entry:any) => entry === value); + } else { + return value; + } + } + private async allowedValuesFor(form:FormResource, field:string):Promise { const fieldSchema = form.schema[field]; diff --git a/frontend/app/components/wp-inline-create/wp-inline-create.component.ts b/frontend/app/components/wp-inline-create/wp-inline-create.component.ts index f652edb671..da7bcd961e 100644 --- a/frontend/app/components/wp-inline-create/wp-inline-create.component.ts +++ b/frontend/app/components/wp-inline-create/wp-inline-create.component.ts @@ -28,7 +28,7 @@ import { Component, - ElementRef, + ElementRef, HostListener, Inject, Injector, Input, @@ -122,7 +122,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe this.timelineBuilder = new TimelineRowBuilder(this.injector, this.table); // Mirror the row height in timeline - const container = jQuery('.wp-table-timeline--body'); + const container = jQuery(this.table.timelineBody); container.addClass('-inline-create-mirror'); // Remove temporary rows on creation of new work package @@ -137,7 +137,9 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe this.addWorkPackageRow(); // Focus on the last inserted id - this.wpTableFocus.updateFocus(wp.id); + if (!this.table.configuration.isEmbedded) { + this.wpTableFocus.updateFocus(wp.id); + } } else { // Remove current row this.table.editing.stopEditing('new'); @@ -171,11 +173,6 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe evt.stopImmediatePropagation(); return false; }); - - // Additionally, cancel on escape - Mousetrap(this.$element[0]).bind('escape', () => { - this.resetRow(); - }); } public handleAddRowClick() { @@ -192,7 +189,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe const wp = this.currentWorkPackage = changeset.workPackage; // Apply filter values - const filter = new WorkPackageFilterValues(changeset, this.wpTableFilters.current); + const filter = new WorkPackageFilterValues(changeset, this.tableState.query.value!.filters); filter.applyDefaultsFromFilters().then(() => { this.wpEditing.updateValue('new', changeset); this.wpCacheService.updateWorkPackage(this.currentWorkPackage!); @@ -222,6 +219,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe /** * Reset the new work package row and refocus on the button */ + @HostListener('keydown.escape') public resetRow() { this.focus = true; this.removeWorkPackageRow(); @@ -251,6 +249,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe } public get isAllowed():boolean { - return this.authorisationService.can('work_packages', 'createWorkPackage'); + return this.authorisationService.can('work_packages', 'createWorkPackage') || + this.authorisationService.can('work_package', 'addChild'); } } diff --git a/frontend/app/components/wp-relations/wp-relation-children/wp-children-query.html b/frontend/app/components/wp-relations/wp-relation-children/wp-children-query.html index 10b0f0af34..95cf2dd9a4 100644 --- a/frontend/app/components/wp-relations/wp-relation-children/wp-children-query.html +++ b/frontend/app/components/wp-relations/wp-relation-children/wp-children-query.html @@ -4,9 +4,10 @@ [tableActions]="childrenTableActions" [configuration]="{ actionsColumnEnabled: true, hierarchyToggleEnabled: false, - inlineCreateEnabled: false, + inlineCreateEnabled: true, columnMenuEnabled: false, contextMenuEnabled: false, + projectIdentifier: workPackage.project.idFromLink, projectContext: false }" > diff --git a/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.template.html b/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.template.html index 80771befba..7a73dd41fd 100644 --- a/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.template.html +++ b/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.template.html @@ -1,24 +1,24 @@
-
-
-

-

-
+
+
+

+

+
- - -
-
-

-

-
+ +
+
+
+
+

+

- - - +
+ +
diff --git a/frontend/app/components/wp-table/embedded/wp-embedded-table.component.ts b/frontend/app/components/wp-table/embedded/wp-embedded-table.component.ts index 3f84581f80..51e255e014 100644 --- a/frontend/app/components/wp-table/embedded/wp-embedded-table.component.ts +++ b/frontend/app/components/wp-table/embedded/wp-embedded-table.component.ts @@ -95,6 +95,12 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, AfterViewInit, // Load initial query this.loadQuery(); + // Reload results on refresh requests + this.tableState.refreshRequired + .values$() + .pipe(untilComponentDestroyed(this)) + .subscribe(async () => this.refresh()); + // Reload results on changes to pagination this.tableState.ready.fireOnStateChange(this.wpTablePagination.state, 'Query loaded').values$().pipe( @@ -126,6 +132,8 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, AfterViewInit, if (this.configuration.projectContext) { identifier = this.currentProject.identifier; + } else { + identifier = this.configuration.projectIdentifier; } return identifier || undefined; @@ -188,5 +196,5 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, AfterViewInit, // TODO: remove as this should also work by angular2 only opUiComponentsModule.directive( 'wpEmbeddedTable', - downgradeComponent({ component: WorkPackageEmbeddedTableComponent }) + downgradeComponent({component: WorkPackageEmbeddedTableComponent}) ); diff --git a/frontend/app/components/wp-table/wp-table-configuration.ts b/frontend/app/components/wp-table/wp-table-configuration.ts index e2faae9810..d5d1b8d602 100644 --- a/frontend/app/components/wp-table/wp-table-configuration.ts +++ b/frontend/app/components/wp-table/wp-table-configuration.ts @@ -27,7 +27,7 @@ // ++ -export type WorkPackageTableConfigurationObject = Partial<{ [field in keyof WorkPackageTableConfiguration]:boolean }>; +export type WorkPackageTableConfigurationObject = Partial<{ [field in keyof WorkPackageTableConfiguration]:string|boolean }>; export class WorkPackageTableConfiguration { /** Render the table results, set to false when only wanting the table initialization */ @@ -45,6 +45,9 @@ export class WorkPackageTableConfiguration { /** Whether the query should be resolved using the current project identifier */ public projectContext:boolean = true; + /** Whether the embedded table should live within a specific project context (e.g., given by its parent) */ + public projectIdentifier:string|null = null; + /** Whether inline create is enabled*/ public inlineCreateEnabled:boolean = true; @@ -57,7 +60,7 @@ export class WorkPackageTableConfiguration { constructor(private providedConfig:WorkPackageTableConfigurationObject) { _.each(providedConfig, (value, k) => { let key = (k as keyof WorkPackageTableConfiguration); - this[key] = !!value; + this[key] = value as any; }); } } diff --git a/spec/features/work_packages/details/relations/hierarchy_spec.rb b/spec/features/work_packages/details/relations/hierarchy_spec.rb index 87f1c318d8..78e53ac18d 100644 --- a/spec/features/work_packages/details/relations/hierarchy_spec.rb +++ b/spec/features/work_packages/details/relations/hierarchy_spec.rb @@ -5,7 +5,8 @@ describe 'Work package relations tab', js: true, selenium: true do let(:user) { FactoryBot.create :admin } - let(:project) { FactoryBot.create :project } + + let(:project) { FactoryBot.create(:project) } let(:work_package) { FactoryBot.create(:work_package, project: project) } let(:work_packages_page) { ::Pages::SplitWorkPackage.new(work_package) } let(:full_wp) { ::Pages::FullWorkPackage.new(work_package) } @@ -56,6 +57,24 @@ describe 'Work package relations tab', js: true, selenium: true do relations.add_existing_child(child2) end + + describe 'inline create' do + let!(:status) { FactoryBot.create(:status, is_default: true) } + let!(:priority) { FactoryBot.create(:priority, is_default: true) } + let(:type_bug) { FactoryBot.create(:type_bug) } + let!(:project) do + FactoryBot.create(:project, types: [type_bug]) + end + + it 'can inline-create children' do + relations.inline_create_child 'my new child' + table = relations.children_table + + table.expect_work_package_subject 'my new child' + work_package.reload + expect(work_package.children.count).to eq(1) + end + end end describe 'relation group-by toggler' do diff --git a/spec/support/components/work_packages/relations.rb b/spec/support/components/work_packages/relations.rb index 9f002b7fad..3cdb6bcf6c 100644 --- a/spec/support/components/work_packages/relations.rb +++ b/spec/support/components/work_packages/relations.rb @@ -144,6 +144,15 @@ module Components expect(page).to have_no_selector('.relation-row--parent', text: removed_text, wait: 10) end + def inline_create_child(subject_text) + container = find('.wp-relations--children') + scroll_to_and_click(container.find('.wp-inline-create-button-row .wp-inline-create--add-link')) + + subject = ::WorkPackageField.new(container, 'subject') + subject.expect_active! + subject.update subject_text + end + def add_existing_child(work_package) # Locate the create row container container = find('.wp-relations--child-form') @@ -155,6 +164,10 @@ module Components container.find('.wp-create-relation--save').click end + def children_table + ::Pages::EmbeddedWorkPackagesTable.new find('.work-packages-embedded-view--container') + end + def remove_child(work_package) page.within('.work-packages-embedded-view--container') do row = ".wp-row-#{work_package.id}-table" diff --git a/spec/support/pages/work_packages_table.rb b/spec/support/pages/work_packages_table.rb index 357830136d..d34f148e2b 100644 --- a/spec/support/pages/work_packages_table.rb +++ b/spec/support/pages/work_packages_table.rb @@ -54,6 +54,14 @@ module Pages end end + def expect_work_package_subject(subject) + within(table_container) do + expect(page).to have_selector("td.subject", + text: subject, + wait: 20) + end + end + def expect_work_package_count(n) within(table_container) do expect(page).to have_selector(".wp--row", count: n, wait: 20) From 60527f050032251973eee45573d6782b013fe1ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 16 May 2018 09:34:35 +0200 Subject: [PATCH 5/5] [26874] Raise required FF version to 60 (latest ESR) (#6312) [26874] Raise required FF version to 60 (latest ESR) [ci skip] --- browserslist | 2 +- docs/installation/system-requirements.md | 2 +- frontend/app/globals/unsupported-browsers.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/browserslist b/browserslist index bd5b0ca27a..bca799f8d6 100644 --- a/browserslist +++ b/browserslist @@ -4,4 +4,4 @@ last 2 Chrome versions last 2 Safari versions last 2 Edge versions -Firefox >= 45 +Firefox >= 60 diff --git a/docs/installation/system-requirements.md b/docs/installation/system-requirements.md index 10eb3c5f9f..f808c8ef4c 100644 --- a/docs/installation/system-requirements.md +++ b/docs/installation/system-requirements.md @@ -43,7 +43,7 @@ OpenProject supports the latest versions of the major browsers. In our strive to make OpenProject easy and fun to use we had to drop support for some older browsers (e.g. IE 11). -* [Mozilla Firefox](https://www.mozilla.org/en-US/firefox/products/) +* [Mozilla Firefox](https://www.mozilla.org/en-US/firefox/products/) (At least ESR version 60) * [Microsoft Edge](https://www.microsoft.com/de-de/windows/microsoft-edge) * [Google Chrome](https://www.google.com/chrome/browser/desktop/) diff --git a/frontend/app/globals/unsupported-browsers.ts b/frontend/app/globals/unsupported-browsers.ts index ea27bc3ae8..b487653987 100644 --- a/frontend/app/globals/unsupported-browsers.ts +++ b/frontend/app/globals/unsupported-browsers.ts @@ -33,13 +33,13 @@ declare const I18n:op.I18n; $(function () { // Specifies minimum versions to be supported // As we don't support ANY version of msie, so treat 11 (last ie before edge) as unsupported - const unsupported = { + const minimumSupported = { msie: '12', - firefox: '52' + firefox: '60' }; let additionalMessage = I18n.t("js.unsupported_browser.update_message"); - if (bowser.isUnsupportedBrowser(unsupported, window.navigator.userAgent)) { + if (bowser.isUnsupportedBrowser(minimumSupported, window.navigator.userAgent)) { if (bowser.msie) { additionalMessage = I18n.t("js.unsupported_browser.update_ie_user"); }