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);