diff --git a/frontend/app/components/angular/angular-injector-bridge.functions.ts b/frontend/app/components/angular/angular-injector-bridge.functions.ts index 15b30b7189..bdafdfc80e 100644 --- a/frontend/app/components/angular/angular-injector-bridge.functions.ts +++ b/frontend/app/components/angular/angular-injector-bridge.functions.ts @@ -1,9 +1,10 @@ - - /** +/** * Returns the currently bootstrapped injector from the application. * Not applicable until after the application bootstrapping is done. */ - export function $currentInjector() { +import {Injector} from '@angular/core'; + +export function $currentInjector() { return (window as any).ngInjector || angular.element(document.body).injector(); } @@ -29,6 +30,7 @@ * * @param target The target to inject into * @param dependencies A set of dependencies to inject + * @deprecated */ export function $injectFields(target:any, ...dependencies:string[]) { let $injector = $currentInjector(); @@ -36,3 +38,5 @@ target[dep] = $injector.get(dep); }); } + + diff --git a/frontend/app/components/routing/ui-router.config.ts b/frontend/app/components/routing/ui-router.config.ts index f7386bdc37..64819a7324 100644 --- a/frontend/app/components/routing/ui-router.config.ts +++ b/frontend/app/components/routing/ui-router.config.ts @@ -29,6 +29,8 @@ import {openprojectModule} from '../../angular-modules'; import {FirstRouteService} from 'app/components/routing/first-route-service'; import {Transition, TransitionService, UrlMatcherFactory, UrlService} from '@uirouter/core'; +import {WorkPackageSplitViewComponent} from 'core-components/routing/wp-split-view/wp-split-view.component'; +import {WorkPackagesListComponent} from 'core-components/routing/wp-list/wp-list.component'; const panels = { get overview() { @@ -81,7 +83,7 @@ openprojectModule $urlMatcherFactoryProvider.strictMode(false); - console.error("Config ui-router.config"); + console.error('Config ui-router.config'); // Prepend the baseurl to the route to avoid using a base tag // For more information, see @@ -100,7 +102,6 @@ openprojectModule query_id: { dynamic: true }, query_props: { dynamic: true } }, - onEnter: () => console.error("ENTERING!"), templateUrl: '/components/routing/main/work-packages.html', controller: 'WorkPackagesController' }) @@ -149,8 +150,7 @@ openprojectModule .state('work-packages.list', { url: '', - controller: 'WorkPackagesListRouter', - template: '', + component: WorkPackagesListComponent, reloadOnSearch: false, onEnter: () => angular.element('body').addClass('action-index'), onExit: () => angular.element('body').removeClass('action-index') @@ -182,9 +182,7 @@ openprojectModule .state('work-packages.list.details', { redirectTo: 'work-packages.list.details.overview', url: '/details/{workPackageId:[0-9]+}', - templateUrl: '/components/routing/wp-details/wp.list.details.html', - controller: 'WorkPackageDetailsController', - controllerAs: '$ctrl', + component: WorkPackageSplitViewComponent, reloadOnSearch: false, params: { focus: { @@ -211,7 +209,7 @@ openprojectModule $transitions:TransitionService, $window:ng.IWindowService) => { - console.error("RUN ui-router config"); + console.error('RUN ui-router config'); $trace.enable(1); // Our application is still a hybrid one, meaning most routes are still diff --git a/frontend/app/components/routing/wp-details/wp-details.controller.test.ts b/frontend/app/components/routing/wp-details/wp-details.controller.test.ts deleted file mode 100644 index 6cfc552f68..0000000000 --- a/frontend/app/components/routing/wp-details/wp-details.controller.test.ts +++ /dev/null @@ -1,233 +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 { - opApiModule, opServicesModule, openprojectModule, - wpControllersModule, wpServicesModule -} from "../../../angular-modules"; - -describe('WorkPackageDetailsController', () => { - var scope:any; - var promise:any; - var buildController:any; - var ctrl:any; - var I18n:any = {t: angular.identity}; - - var workPackage:any = { - props: {}, - embedded: { - author: { - props: { - id: 1, - status: 'active' - } - }, - id: 99, - project: { - props: { - id: 1 - } - }, - activities: { - links: { - self: {href: "/api/v3/work_packages/820/activities"} - }, - _type: "Collection", - total: 0, - count: 0, - embedded: { - elements: [] - } - }, - watchers: [], - attachments: { - links: { - self: {href: "/api/v3/work_packages/820/attachments"} - }, - _type: "Collection", - total: 0, - count: 0, - embedded: { - elements: [] - } - }, - type: { - props: { - name: 'Milestone' - } - }, - relations: [ - { - props: { - _type: "Relation::Relates" - }, - links: { - relatedFrom: { - fetch: sinon.spy() - }, - relatedTo: { - fetch: sinon.spy() - } - } - } - ] - }, - links: { - self: {href: "it's a me, it's... you know..."}, - availableWatchers: { - fetch: () => { - return {then: angular.noop}; - } - }, - schema: { - fetch: () => { - return {then: angular.noop}; - } - } - }, - link: { - addWatcher: { - fetch: () => { - return {then: angular.noop}; - } - } - } - }; - - beforeEach(angular.mock.module(openprojectModule.name, opApiModule.name, 'openproject.layout', - wpControllersModule.name, wpServicesModule.name, opServicesModule.name)); - - beforeEach(angular.mock.module('openproject.templates', function ($provide:any) { - $provide.constant('ConfigurationService', { - isTimezoneSet: sinon.stub().returns(false), - warnOnLeavingUnsaved: sinon.stub().returns(false) - }); - })); - - beforeEach(angular.mock.inject(($rootScope:any, - $controller:any, - $injector:ng.auto.IInjectorService, - $state:any, - $q:any, - $httpBackend:any, - WorkPackageService:any) => { - $httpBackend.when('GET', '/api/v3/work_packages/99').respond(workPackage); - - (window as any).ngInjector = $injector; - - WorkPackageService.getWorkPackage = () => { - return $q.when(workPackage) - }; - - buildController = () => { - var testState = { - params: {workPackageId: 99}, - includes: sinon.stub().returns(true), - go: sinon.stub(), - current: {url: '/activity'} - }; - scope = $rootScope.$new(); - - ctrl = $controller("WorkPackageDetailsController", { - $scope: scope, - $state: testState, - I18n: I18n, - ConfigurationService: { - commentsSortedInDescendingOrder: () => { - return false; - } - }, - workPackage: workPackage, - }); - - promise = ctrl.initialized.promise; - }; - })); - - describe('initialisation', () => { - it('should initialise', () => { - return buildController(); - }); - }); - - describe('#scope.canViewWorkPackageWatchers', () => { - describe('when the work package does not contain the embedded watchers property', () => { - beforeEach(() => { - workPackage.embedded.watchers = undefined; - buildController(); - }); - - it('returns false', () => { - expect(promise).to.eventually.be.fulfilled.then(() => { - expect(scope.canViewWorkPackageWatchers()).to.be.false; - }); - }); - }); - - describe('when the work package contains the embedded watchers property', () => { - beforeEach(() => { - workPackage.embedded.watchers = []; - return buildController(); - }); - - it('returns true', () => { - expect(promise).to.eventually.be.fulfilled.then(() => { - expect(scope.canViewWorkPackageWatchers()).to.be.true; - }); - }); - }); - }); - - describe('work package properties', () => { - describe('relations', () => { - beforeEach(() => { - return buildController(); - }); - - it('Relation::Relates', () => { - expect(promise).to.eventually.be.fulfilled.then(() => { - expect(scope.relatedTo).to.be.ok; - }); - }); - - it('is the embedded type', () => { - expect(promise).to.eventually.be.fulfilled.then(() => { - expect(scope.type.props.name).to.eql('Milestone'); - }); - }); - }); - }); - - describe('showStaticPagePath', () => { - it('points to old show page', () => { - expect(promise).to.eventually.be.fulfilled.then(() => { - expect(scope.showStaticPagePath).to.eql('/work_packages/99'); - }); - }); - }); -}); diff --git a/frontend/app/components/routing/wp-list/wp-list.component.ts b/frontend/app/components/routing/wp-list/wp-list.component.ts index 54ef6526b8..f0141b2811 100644 --- a/frontend/app/components/routing/wp-list/wp-list.component.ts +++ b/frontend/app/components/routing/wp-list/wp-list.component.ts @@ -27,7 +27,7 @@ // ++ import {Component, Inject, OnDestroy, OnInit} from '@angular/core'; -import {StateService} from '@uirouter/core'; +import {StateService, StateParams, TransitionService} from '@uirouter/core'; import {untilComponentDestroyed} from 'ng2-rx-componentdestroyed'; import {auditTime, distinctUntilChanged, filter, withLatestFrom} from 'rxjs/operators'; import {debugLog} from '../../../helpers/debug_output'; @@ -131,6 +131,20 @@ export class WorkPackagesListComponent implements OnInit, OnDestroy { this.wpTableRefresh.clear('Table controller scope destroyed.'); } + /** + * Callback from ui-router when params in this state changed. + * @param {StateParams} params + */ + public uiOnParamsChanged(params:StateParams) { + console.log('params changed to %O', params); + let newChecksum = params.query_props; + let newId = params.query_id && parseInt(params.query_id); + + this.wpListChecksumService.executeIfOutdated(newId, newChecksum, () => { + this.wpListService.loadCurrentQueryFromParams(params['projectPath']); + }); + } + private setupQueryObservers() { this.states.tableRendering.onQueryUpdated.values$().pipe() .take(1) @@ -152,7 +166,8 @@ export class WorkPackagesListComponent implements OnInit, OnDestroy { this.wpListChecksumService.setToQuery(query, pagination); }); - this.states.query.context.fireOnStateChange(this.wpTablePagination.state, 'Query loaded').values$().pipe( + this.states.query.context.fireOnStateChange(this.wpTablePagination.state, + 'Query loaded').values$().pipe( untilComponentDestroyed(this), withLatestFrom(this.states.query.resource.values$()) ).subscribe(([pagination, query]) => { @@ -227,7 +242,6 @@ export class WorkPackagesListComponent implements OnInit, OnDestroy { } } - updateTitle(query:QueryResource) { if (query.id) { this.selectedTitle = query.name; diff --git a/frontend/app/components/routing/wp-list/wp-list.router.directive.ts b/frontend/app/components/routing/wp-list/wp-list.router.directive.ts deleted file mode 100644 index e5d5402999..0000000000 --- a/frontend/app/components/routing/wp-list/wp-list.router.directive.ts +++ /dev/null @@ -1,56 +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 {WorkPackagesListService} from 'core-components/wp-list/wp-list.service'; -import {WorkPackagesListChecksumService} from 'core-components/wp-list/wp-list-checksum.service'; -import {StateParams} from '@uirouter/core'; - -function WorkPackagesListRouterController($scope:any, - $stateParams:StateParams, - wpListChecksumService:WorkPackagesListChecksumService, - wpListService:WorkPackagesListService) { - $scope.$watchCollection( - () => { - return { - query_id: $stateParams['query_id'], - query_props: $stateParams['query_props'] - }; - }, - (params:{ query_id:any, query_props:any }) => { - let newChecksum = params.query_props; - let newId = params.query_id && parseInt(params.query_id); - - wpListChecksumService.executeIfOutdated(newId, newChecksum, function() { - wpListService.loadCurrentQueryFromParams($stateParams['projectPath']); - }); - }); -} - -angular - .module('openproject.workPackages.controllers') - .controller('WorkPackagesListRouter', WorkPackagesListRouterController); diff --git a/frontend/app/components/routing/wp-show/wp-show.controller.ts b/frontend/app/components/routing/wp-show/wp-show.controller.ts index fc4f1dcbd8..edc18b45cf 100644 --- a/frontend/app/components/routing/wp-show/wp-show.controller.ts +++ b/frontend/app/components/routing/wp-show/wp-show.controller.ts @@ -27,15 +27,14 @@ // ++ import {wpControllersModule} from "../../../angular-modules"; -import {HalResource} from "../../api/api-v3/hal-resources/hal-resource.service"; import {UserResource} from "../../api/api-v3/hal-resources/user-resource.service"; import {WorkPackageResourceInterface} from "../../api/api-v3/hal-resources/work-package-resource.service"; import {WorkPackageViewController} from "../wp-view-base/wp-view-base.controller"; -import {WorkPackagesListChecksumService} from "../../wp-list/wp-list-checksum.service"; import {WorkPackageMoreMenuService} from '../../work-packages/work-package-more-menu.service' import {WorkPackageTableFocusService} from "core-components/wp-fast-table/state/wp-table-focus.service"; import {StateService} from '@uirouter/core'; import {TypeResource} from "core-components/api/api-v3/hal-resources/type-resource.service"; +import {Injector} from '@angular/core'; export class WorkPackageShowController extends WorkPackageViewController { @@ -54,10 +53,11 @@ export class WorkPackageShowController extends WorkPackageViewController { private wpMoreMenu:WorkPackageMoreMenuService; constructor(public $scope:any, + public injector:Injector, public $state:StateService, public wpTableFocus:WorkPackageTableFocusService, protected wpMoreMenuService:WorkPackageMoreMenuService) { - super($scope, $state.params['workPackageId']); + super(injector, $state.params['workPackageId']); this.observeWorkPackage(); } diff --git a/frontend/app/components/routing/wp-details/wp-details.controller.ts b/frontend/app/components/routing/wp-split-view/wp-split-view.component.ts similarity index 71% rename from frontend/app/components/routing/wp-details/wp-details.controller.ts rename to frontend/app/components/routing/wp-split-view/wp-split-view.component.ts index 3855d7daaf..3a60edf9eb 100644 --- a/frontend/app/components/routing/wp-details/wp-details.controller.ts +++ b/frontend/app/components/routing/wp-split-view/wp-split-view.component.ts @@ -26,27 +26,31 @@ // See doc/COPYRIGHT.rdoc for more details. // ++ -import {wpControllersModule} from "../../../angular-modules"; -import {scopedObservable} from "../../../helpers/angular-rx-utils"; -import {States} from "../../states.service"; -import {WorkPackageTableSelection} from "../../wp-fast-table/state/wp-table-selection.service"; -import {KeepTabService} from "../../wp-panels/keep-tab/keep-tab.service"; -import {WorkPackageViewController} from "../wp-view-base/wp-view-base.controller"; -import {WorkPackageEditingService} from '../../wp-edit-form/work-package-editing-service'; -import {FirstRouteService} from "core-components/routing/first-route-service"; -import {WorkPackageTableFocusService} from "core-components/wp-fast-table/state/wp-table-focus.service"; +import {States} from '../../states.service'; +import {WorkPackageTableSelection} from '../../wp-fast-table/state/wp-table-selection.service'; +import {KeepTabService} from '../../wp-panels/keep-tab/keep-tab.service'; +import {WorkPackageViewController} from '../wp-view-base/wp-view-base.controller'; +import {FirstRouteService} from 'core-components/routing/first-route-service'; +import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; import {StateService} from '@uirouter/core'; +import {Component, Inject, Injector} from '@angular/core'; +import {componentDestroyed} from 'ng2-rx-componentdestroyed'; +import {$stateToken} from 'core-app/angular4-transition-utils'; -export class WorkPackageDetailsController extends WorkPackageViewController { +@Component({ + template: require('!!raw-loader!./wp-split-view.html'), + selector: 'wp-split-view-entry', +}) +export class WorkPackageSplitViewComponent extends WorkPackageViewController { - constructor(public $scope:ng.IScope, + constructor(public injector:Injector, public states:States, public firstRoute:FirstRouteService, public keepTab:KeepTabService, public wpTableSelection:WorkPackageTableSelection, public wpTableFocus:WorkPackageTableFocusService, - public $state:StateService) { - super($scope, $state.params['workPackageId']); + @Inject($stateToken) readonly $state:StateService) { + super(injector, $state.params['workPackageId']); this.observeWorkPackage(); let wpId = $state.params['workPackageId']; @@ -65,15 +69,14 @@ export class WorkPackageDetailsController extends WorkPackageViewController { this.wpTableSelection.setRowState(wpId, true); } - scopedObservable( - $scope, - this.wpTableFocus.whenChanged() - ).subscribe(newId => { + this.wpTableFocus.whenChanged() + .takeUntil(componentDestroyed(this)) + .subscribe(newId => { const idSame = wpId.toString() === newId.toString(); if (!idSame && $state.includes('work-packages.list.details')) { $state.go( ($state.current.name as string), - {workPackageId: newId, focus: false } + {workPackageId: newId, focus: false} ); } }); @@ -97,5 +100,3 @@ export class WorkPackageDetailsController extends WorkPackageViewController { this.text.goTofullScreen = this.I18n.t('js.work_packages.message_successful_show_in_fullscreen'); } } - -wpControllersModule.controller('WorkPackageDetailsController', WorkPackageDetailsController); diff --git a/frontend/app/components/routing/wp-details/wp.list.details.html b/frontend/app/components/routing/wp-split-view/wp-split-view.html similarity index 100% rename from frontend/app/components/routing/wp-details/wp.list.details.html rename to frontend/app/components/routing/wp-split-view/wp-split-view.html 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 1a0ab88515..b8604570c1 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 @@ -37,25 +37,22 @@ import {$injectFields} from '../../angular/angular-injector-bridge.functions'; import {WorkPackageEditingService} from '../../wp-edit-form/work-package-editing-service'; import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; import {StateService} from '@uirouter/core'; - -export class WorkPackageViewController { - - protected $q:ng.IQService; - protected $state:StateService; - protected states:States; - protected $rootScope:ng.IRootScopeService; - protected keepTab:KeepTabService; - protected wpCacheService:WorkPackageCacheService; - protected WorkPackageService:any; - protected PathHelper:op.PathHelper; - protected I18n:op.I18n; - protected wpTableRefresh:WorkPackageTableRefreshService; - protected wpEditing:WorkPackageEditingService; - protected wpTableFocus:WorkPackageTableFocusService; - - // Helper promise to detect when the controller has been initialized - // (when a WP has loaded). - public initialized:ng.IDeferred; +import {Injector, OnDestroy} from '@angular/core'; +import {I18nToken} from 'core-app/angular4-transition-utils'; +import PathHelper = op.PathHelper; +import {PathHelperService} from 'core-components/common/path-helper/path-helper.service'; +import {componentDestroyed} from 'ng2-rx-componentdestroyed'; + +export class WorkPackageViewController implements OnDestroy { + + public wpCacheService:WorkPackageCacheService = this.injector.get(WorkPackageCacheService); + public states:States = this.injector.get(States); + public I18n:op.I18n = this.injector.get(I18nToken); + public keepTab:KeepTabService = this.injector.get(KeepTabService); + public PathHelper:PathHelperService = this.injector.get(PathHelperService); + public wpTableRefresh:WorkPackageTableRefreshService = this.injector.get(WorkPackageTableRefreshService); + protected wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService); + protected wpTableFocus:WorkPackageTableFocusService = this.injector.get(WorkPackageTableFocusService); // Static texts public text:any = {}; @@ -67,16 +64,11 @@ export class WorkPackageViewController { protected focusAnchorLabel:string; public showStaticPagePath:string; - constructor(public $scope:ng.IScope, - protected workPackageId:string) { - $injectFields(this, '$q', '$state', 'keepTab', 'wpCacheService', 'WorkPackageService', - 'states', 'wpEditing', 'PathHelper', 'I18n', 'wpTableRefresh', 'wpTableFocus'); - - this.initialized = this.$q.defer(); + constructor(public injector:Injector, protected workPackageId:string) { this.initializeTexts(); } - public $onInit() { + ngOnDestroy():void { // Created for interface compliance } @@ -85,11 +77,11 @@ export class WorkPackageViewController { * Needs to be run explicitly by descendants. */ protected observeWorkPackage() { - scopedObservable(this.$scope, this.wpCacheService.loadWorkPackage(this.workPackageId).values$()) + this.wpCacheService.loadWorkPackage(this.workPackageId).values$() + .takeUntil(componentDestroyed(this)) .subscribe((wp:WorkPackageResourceInterface) => { this.workPackage = wp; this.init(); - this.initialized.resolve(); }); } @@ -113,10 +105,12 @@ export class WorkPackageViewController { }); // Preselect this work package for future list operations - this.showStaticPagePath = this.PathHelper.workPackagePath(this.workPackage); + this.showStaticPagePath = this.PathHelper.workPackagePath(this.workPackageId); // Listen to tab changes to update the tab label - scopedObservable(this.$scope, this.keepTab.observable).subscribe((tabs:any) => { + this.keepTab.observable + .takeUntil(componentDestroyed(this)) + .subscribe((tabs:any) => { this.updateFocusAnchorLabel(tabs.active); }); } @@ -142,5 +136,3 @@ export class WorkPackageViewController { return this.workPackage.isEditable; } } - -wpControllersModule.controller('WorkPackageViewController', WorkPackageViewController);