diff --git a/frontend/src/app/components/wp-table/embedded/wp-embedded-base.component.ts b/frontend/src/app/components/wp-table/embedded/wp-embedded-base.component.ts index 3b332a7d55..06310ed270 100644 --- a/frontend/src/app/components/wp-table/embedded/wp-embedded-base.component.ts +++ b/frontend/src/app/components/wp-table/embedded/wp-embedded-base.component.ts @@ -11,8 +11,9 @@ import {QueryDmService} from 'core-app/modules/hal/dm-services/query-dm.service' import {UrlParamsHelperService} from 'core-components/wp-query/url-params-helper'; import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space"; +import {WorkPackagesViewBase} from "core-app/modules/work_packages/routing/wp-view-base/work-packages-view.base"; -export abstract class WorkPackageEmbeddedBaseComponent implements OnInit, AfterViewInit, OnDestroy { +export abstract class WorkPackageEmbeddedBaseComponent extends WorkPackagesViewBase implements AfterViewInit { @Input('configuration') protected providedConfiguration:WorkPackageTableConfigurationObject; @Input() public uniqueEmbeddedTableName:string = `embedded-table-${Date.now()}`; @Input() public initialLoadingIndicator:boolean = true; @@ -32,10 +33,9 @@ export abstract class WorkPackageEmbeddedBaseComponent implements OnInit, AfterV readonly wpStatesInitialization:WorkPackageStatesInitializationService = this.injector.get(WorkPackageStatesInitializationService); readonly currentProject:CurrentProjectService = this.injector.get(CurrentProjectService); - protected constructor(protected injector:Injector) { - } - ngOnInit() { + super.ngOnInit(); + this.configuration = new WorkPackageTableConfiguration(this.providedConfiguration); // Set embedded status in configuration this.configuration.isEmbedded = true; @@ -45,16 +45,10 @@ export abstract class WorkPackageEmbeddedBaseComponent implements OnInit, AfterV ngAfterViewInit():void { // Load initially this.refresh(this.initialLoadingIndicator); - - // Reload results on refresh requests - this.querySpace.refreshRequired - .values$() - .pipe(untilComponentDestroyed(this)) - .subscribe(() => this.refresh(false)); } ngOnDestroy():void { - // noting to do + super.ngOnInit(); } ngOnChanges(changes:SimpleChanges) { @@ -86,8 +80,8 @@ export abstract class WorkPackageEmbeddedBaseComponent implements OnInit, AfterV this.renderTable = this.configuration.tableVisible; } - public refresh(visible:boolean = true):Promise { - return this.loadQuery(visible); + public refresh(visible:boolean = true, firstPage:boolean = false):Promise { + return this.loadQuery(visible, firstPage); } public get isInitialized() { @@ -102,7 +96,7 @@ export abstract class WorkPackageEmbeddedBaseComponent implements OnInit, AfterV } } - protected abstract loadQuery(visible:boolean):Promise; + protected abstract loadQuery(visible:boolean, firstPage:boolean):Promise; protected get queryProjectScope() { if (!this.configuration.projectContext) { diff --git a/frontend/src/app/components/wp-table/embedded/wp-embedded-table.component.ts b/frontend/src/app/components/wp-table/embedded/wp-embedded-table.component.ts index d026ca30ff..fb7329101a 100644 --- a/frontend/src/app/components/wp-table/embedded/wp-embedded-table.component.ts +++ b/frontend/src/app/components/wp-table/embedded/wp-embedded-table.component.ts @@ -1,4 +1,4 @@ -import {AfterViewInit, Component, EventEmitter, Injector, Input, OnDestroy, OnInit, Output} from '@angular/core'; +import {AfterViewInit, Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core'; import {WorkPackageTableTimelineService} from 'core-components/wp-fast-table/state/wp-table-timeline.service'; import {WorkPackageTablePaginationService} from 'core-components/wp-fast-table/state/wp-table-pagination.service'; import {withLatestFrom} from 'rxjs/operators'; @@ -14,7 +14,6 @@ import {WorkPackageEmbeddedBaseComponent} from "core-components/wp-table/embedde import {WorkPackageTableFilters} from "core-components/wp-fast-table/wp-table-filters"; import {QueryFormResource} from "core-app/modules/hal/resources/query-form-resource"; import {QueryFormDmService} from "core-app/modules/hal/dm-services/query-form-dm.service"; -import {FormResource} from "core-app/modules/hal/resources/form-resource"; @Component({ selector: 'wp-embedded-table', @@ -39,10 +38,6 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo // Cache the form promise private formPromise:Promise|undefined; - constructor(readonly injector:Injector) { - super(injector); - } - ngAfterViewInit():void { super.ngAfterViewInit(); @@ -50,16 +45,6 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo if (this.tableActions) { this.tableActionsService.setActions(...this.tableActions); } - - // Reload results on changes to pagination - this.querySpace.ready.fireOnStateChange(this.wpTablePagination.state, - 'Query loaded').values$().pipe( - untilComponentDestroyed(this), - withLatestFrom(this.querySpace.query.values$()) - ).subscribe(([pagination, query]) => { - this.loadingIndicator = this.QueryDm.loadResults(query, this.wpTablePagination.paginationObject) - .then((results) => this.initializeStates(query, results)); - }); } public openConfigurationModal(onUpdated:() => void) { @@ -113,7 +98,7 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo .catch(() => this.formPromise = undefined); } - protected loadQuery(visible:boolean = true):Promise { + protected loadQuery(visible:boolean = true, firstPage:boolean = false):Promise { if (this.loadedQuery) { const query = this.loadedQuery; this.loadedQuery = undefined; @@ -131,6 +116,11 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo this.queryProps.pageSize = this.configuration.forcePerPageOption; } + // Set first page + if (firstPage) { + this.queryProps.page = 1; + } + this.error = null; const promise = this.QueryDm .find( diff --git a/frontend/src/app/modules/calendar/wp-calendar-entry/wp-calendar-entry.component.html b/frontend/src/app/modules/calendar/wp-calendar-entry/wp-calendar-entry.component.html index e43c422fde..1a2d0bf9f1 100644 --- a/frontend/src/app/modules/calendar/wp-calendar-entry/wp-calendar-entry.component.html +++ b/frontend/src/app/modules/calendar/wp-calendar-entry/wp-calendar-entry.component.html @@ -1,4 +1,5 @@ -
+
@@ -7,8 +8,7 @@
-
    +
    • diff --git a/frontend/src/app/modules/calendar/wp-calendar-entry/wp-calendar-entry.component.ts b/frontend/src/app/modules/calendar/wp-calendar-entry/wp-calendar-entry.component.ts index d57217316b..f727ac432a 100644 --- a/frontend/src/app/modules/calendar/wp-calendar-entry/wp-calendar-entry.component.ts +++ b/frontend/src/app/modules/calendar/wp-calendar-entry/wp-calendar-entry.component.ts @@ -26,16 +26,26 @@ // See doc/COPYRIGHT.rdoc for more details. // ++ -import {Component} from '@angular/core'; -import {WorkPackagesSetComponent} from "core-app/modules/work_packages/routing/wp-set/wp-set.component"; +import {Component, ViewChild} from '@angular/core'; +import {WorkPackagesViewBase} from "core-app/modules/work_packages/routing/wp-view-base/work-packages-view.base"; +import {WorkPackagesCalendarController} from "core-app/modules/calendar/wp-calendar/wp-calendar.component"; @Component({ templateUrl: './wp-calendar-entry.component.html' }) -export class WorkPackagesCalendarEntryComponent extends WorkPackagesSetComponent { - // overrides super - protected initialQueryLoading() { - // nothing +export class WorkPackagesCalendarEntryComponent extends WorkPackagesViewBase { + @ViewChild(WorkPackagesCalendarController) calendarElement:WorkPackagesCalendarController; + + /** Project identifier of the list */ + projectIdentifier = this.$state.params['projectPath'] || null; + + protected set loadingIndicator(promise:Promise) { + this.loadingIndicatorService.indicator('calendar-entry').promise = promise; + } + + public refresh(visibly:boolean, firstPage:boolean):Promise { + return this.loadingIndicator = + this.wpListService.loadCurrentQueryFromParams(this.projectIdentifier); } } diff --git a/frontend/src/app/modules/grids/widgets/wp-widget/wp-widget.component.ts b/frontend/src/app/modules/grids/widgets/wp-widget/wp-widget.component.ts index 1c3156874f..815f58d566 100644 --- a/frontend/src/app/modules/grids/widgets/wp-widget/wp-widget.component.ts +++ b/frontend/src/app/modules/grids/widgets/wp-widget/wp-widget.component.ts @@ -15,7 +15,7 @@ export class WidgetWpListComponent extends AbstractWidgetComponent implements On public configuration:Partial = { actionsColumnEnabled: false, columnMenuEnabled: false, - hierarchyToggleEnabled: false, + hierarchyToggleEnabled: true, contextMenuEnabled: false }; diff --git a/frontend/src/app/modules/work_packages/routing/wp-full-view/wp-full-view.component.ts b/frontend/src/app/modules/work_packages/routing/wp-full-view/wp-full-view.component.ts index f5ef2fd04a..9fac3787c7 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-full-view/wp-full-view.component.ts +++ b/frontend/src/app/modules/work_packages/routing/wp-full-view/wp-full-view.component.ts @@ -28,7 +28,6 @@ import {UserResource} from 'core-app/modules/hal/resources/user-resource'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; -import {WorkPackageViewController} from '../wp-view-base/wp-view-base.controller'; import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; import {StateService} from '@uirouter/core'; import {TypeResource} from 'core-app/modules/hal/resources/type-resource'; @@ -37,6 +36,7 @@ import {WorkPackageTableSelection} from 'core-components/wp-fast-table/state/wp- import {States} from 'core-components/states.service'; import {KeepTabService} from 'core-components/wp-single-view-tabs/keep-tab/keep-tab.service'; import {FirstRouteService} from "core-app/modules/router/first-route-service"; +import {WorkPackageSingleViewBase} from "core-app/modules/work_packages/routing/wp-view-base/work-package-single-view.base"; @Component({ templateUrl: './wp-full-view.html', @@ -44,7 +44,7 @@ import {FirstRouteService} from "core-app/modules/router/first-route-service"; // Required class to support inner scrolling on page host: { 'class': 'work-packages-page--ui-view' } }) -export class WorkPackagesFullViewComponent extends WorkPackageViewController { +export class WorkPackagesFullViewComponent extends WorkPackageSingleViewBase { // Watcher properties public isWatched:boolean; diff --git a/frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts b/frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts index 434c347745..f13515f063 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts +++ b/frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts @@ -30,13 +30,14 @@ import {Component, OnDestroy} from "@angular/core"; import {untilComponentDestroyed} from 'ng2-rx-componentdestroyed'; import {QueryResource} from 'core-app/modules/hal/resources/query-resource'; import {OpTitleService} from "core-components/html/op-title.service"; -import {WorkPackagesSetComponent} from "core-app/modules/work_packages/routing/wp-set/wp-set.component"; +import {WorkPackagesViewBase} from "core-app/modules/work_packages/routing/wp-view-base/work-packages-view.base"; +import {take} from "rxjs/operators"; @Component({ selector: 'wp-list', templateUrl: './wp.list.component.html' }) -export class WorkPackagesListComponent extends WorkPackagesSetComponent implements OnDestroy { +export class WorkPackagesListComponent extends WorkPackagesViewBase implements OnDestroy { text = { 'jump_to_pagination': this.I18n.t('js.work_packages.jump_marks.pagination'), 'text_jump_to_pagination': this.I18n.t('js.work_packages.jump_marks.label_pagination'), @@ -52,13 +53,32 @@ export class WorkPackagesListComponent extends WorkPackagesSetComponent implemen /** Whether we're saving the query */ querySaving:boolean; + + /** Listener callbacks */ unRegisterTitleListener:Function; + removeTransitionSubscription:Function; + + /** Determine when query is initially loaded */ + tableInformationLoaded = false; + + /** Project identifier of the list */ + projectIdentifier = this.$state.params['projectPath'] || null; private readonly titleService:OpTitleService = this.injector.get(OpTitleService); ngOnInit() { super.ngOnInit(); + // Load query initially + this.wpTableRefresh.clear('Impending query loading.'); + this.loadCurrentQuery(); + + // Load query on URL transitions + this.updateQueryOnParamsChanges(); + + // Mark tableInformationLoaded when initially loading done + this.setupInformationLoadedListener(); + // Update title on entering this state this.unRegisterTitleListener = this.$transitions.onSuccess({to: 'work-packages.list'}, () => { if (this.selectedTitle) { @@ -78,6 +98,7 @@ export class WorkPackagesListComponent extends WorkPackagesSetComponent implemen ngOnDestroy():void { super.ngOnDestroy(); this.unRegisterTitleListener(); + this.removeTransitionSubscription(); } public setAnchorToNextElement() { @@ -119,10 +140,60 @@ export class WorkPackagesListComponent extends WorkPackagesSetComponent implemen } } - protected loadCurrentQuery() { - return super.loadCurrentQuery() - .then(() => { - return this.querySpace.rendered.valuesPromise(); - }); + public refresh(visibly:boolean = false, firstPage:boolean = false):Promise { + let promise:Promise; + + if (firstPage) { + promise = this.wpListService.loadCurrentResultsListFirstPage(); + } else { + promise = this.wpListService.reloadCurrentResultsList(); + } + + if (visibly) { + this.loadingIndicator = promise; + } + + return promise; + } + + protected updateQueryOnParamsChanges() { + // Listen for param changes + this.removeTransitionSubscription = this.$transitions.onSuccess({}, (transition):any => { + let options = transition.options(); + + // Avoid performing any changes when we're going to reload + if (options.reload || (options.custom && options.custom.notify === false)) { + return true; + } + + const params = transition.params('to'); + let newChecksum = this.wpListService.getCurrentQueryProps(params); + let newId = params.query_id && parseInt(params.query_id); + + this.wpListChecksumService + .executeIfOutdated(newId, + newChecksum, + () => this.loadCurrentQuery()); + }); + } + + protected setupInformationLoadedListener() { + this.querySpace.tableRendering.onQueryUpdated + .values$() + .pipe( + take(1) + ) + .subscribe(() => this.tableInformationLoaded = true); + } + + protected set loadingIndicator(promise:Promise) { + this.loadingIndicatorService.table.promise = promise; + } + + protected loadCurrentQuery():Promise { + return this.loadingIndicator = + this.wpListService + .loadCurrentQueryFromParams(this.projectIdentifier) + .then(() => this.querySpace.rendered.valuesPromise()); } } diff --git a/frontend/src/app/modules/work_packages/routing/wp-split-view/wp-split-view.component.ts b/frontend/src/app/modules/work_packages/routing/wp-split-view/wp-split-view.component.ts index 3354d96805..7be33472db 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-split-view/wp-split-view.component.ts +++ b/frontend/src/app/modules/work_packages/routing/wp-split-view/wp-split-view.component.ts @@ -31,17 +31,17 @@ import {StateService} from '@uirouter/core'; import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; import {componentDestroyed} from 'ng2-rx-componentdestroyed'; import {takeUntil} from 'rxjs/operators'; -import {WorkPackageViewController} from '../wp-view-base/wp-view-base.controller'; import {States} from "core-components/states.service"; import {FirstRouteService} from "core-app/modules/router/first-route-service"; import {KeepTabService} from "core-components/wp-single-view-tabs/keep-tab/keep-tab.service"; import {WorkPackageTableSelection} from "core-components/wp-fast-table/state/wp-table-selection.service"; +import {WorkPackageSingleViewBase} from "core-app/modules/work_packages/routing/wp-view-base/work-package-single-view.base"; @Component({ templateUrl: './wp-split-view.html', selector: 'wp-split-view-entry', }) -export class WorkPackageSplitViewComponent extends WorkPackageViewController { +export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase { constructor(public injector:Injector, public states:States, diff --git a/frontend/src/app/modules/work_packages/routing/wp-view-base/wp-view-base.controller.ts b/frontend/src/app/modules/work_packages/routing/wp-view-base/work-package-single-view.base.ts similarity index 98% rename from frontend/src/app/modules/work_packages/routing/wp-view-base/wp-view-base.controller.ts rename to frontend/src/app/modules/work_packages/routing/wp-view-base/work-package-single-view.base.ts index 53bf797e5d..ee9ddec480 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-view-base/wp-view-base.controller.ts +++ b/frontend/src/app/modules/work_packages/routing/wp-view-base/work-package-single-view.base.ts @@ -45,7 +45,7 @@ import { IWorkPackageEditingServiceToken } from "core-components/wp-edit-form/work-package-editing.service.interface"; -export class WorkPackageViewController implements OnDestroy { +export class WorkPackageSingleViewBase implements OnDestroy { public wpCacheService:WorkPackageCacheService = this.injector.get(WorkPackageCacheService); public states:States = this.injector.get(States); diff --git a/frontend/src/app/modules/work_packages/routing/wp-set/wp-set.component.ts b/frontend/src/app/modules/work_packages/routing/wp-view-base/work-packages-view.base.ts similarity index 56% rename from frontend/src/app/modules/work_packages/routing/wp-set/wp-set.component.ts rename to frontend/src/app/modules/work_packages/routing/wp-view-base/work-packages-view.base.ts index dc121ab2a2..51f8f15c5a 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-set/wp-set.component.ts +++ b/frontend/src/app/modules/work_packages/routing/wp-view-base/work-packages-view.base.ts @@ -53,65 +53,45 @@ import {WorkPackageQueryStateService} from "core-components/wp-fast-table/state/ import {debugLog} from "core-app/helpers/debug_output"; import {WorkPackageFiltersService} from "core-components/filters/wp-filters/wp-filters.service"; -export class WorkPackagesSetComponent implements OnInit, OnDestroy { - - projectIdentifier = this.$state.params['projectPath'] || null; - - tableInformationLoaded = false; - - private removeTransitionSubscription:Function; - - constructor(readonly injector:Injector, - readonly states:States, - readonly querySpace:IsolatedQuerySpace, - readonly authorisationService:AuthorisationService, - readonly wpTableRefresh:WorkPackageTableRefreshService, - readonly wpTableColumns:WorkPackageTableColumnsService, - readonly wpTableHighlighting:WorkPackageTableHighlightingService, - readonly wpTableSortBy:WorkPackageTableSortByService, - readonly wpTableGroupBy:WorkPackageTableGroupByService, - readonly wpTableFilters:WorkPackageTableFiltersService, - readonly wpTableSum:WorkPackageTableSumService, - readonly wpTableTimeline:WorkPackageTableTimelineService, - readonly wpTableHierarchies:WorkPackageTableHierarchiesService, - readonly wpTablePagination:WorkPackageTablePaginationService, - readonly wpListService:WorkPackagesListService, - readonly wpFilters:WorkPackageFiltersService, - readonly wpListChecksumService:WorkPackagesListChecksumService, - readonly loadingIndicator:LoadingIndicatorService, - readonly $transitions:TransitionService, - readonly $state:StateService, - readonly I18n:I18nService, - readonly wpStaticQueries:WorkPackageStaticQueriesService) { - +export abstract class WorkPackagesViewBase implements OnInit, OnDestroy { + + readonly $state:StateService = this.injector.get(StateService); + readonly states:States = this.injector.get(States); + readonly querySpace:IsolatedQuerySpace = this.injector.get(IsolatedQuerySpace); + readonly authorisationService:AuthorisationService = this.injector.get(AuthorisationService); + readonly wpTableRefresh:WorkPackageTableRefreshService = this.injector.get(WorkPackageTableRefreshService); + readonly wpTableColumns:WorkPackageTableColumnsService = this.injector.get(WorkPackageTableColumnsService); + readonly wpTableHighlighting:WorkPackageTableHighlightingService = this.injector.get(WorkPackageTableHighlightingService); + readonly wpTableSortBy:WorkPackageTableSortByService = this.injector.get(WorkPackageTableSortByService); + readonly wpTableGroupBy:WorkPackageTableGroupByService = this.injector.get(WorkPackageTableGroupByService); + readonly wpTableFilters:WorkPackageTableFiltersService = this.injector.get(WorkPackageTableFiltersService); + readonly wpTableSum:WorkPackageTableSumService = this.injector.get(WorkPackageTableSumService); + readonly wpTableTimeline:WorkPackageTableTimelineService = this.injector.get(WorkPackageTableTimelineService); + readonly wpTableHierarchies:WorkPackageTableHierarchiesService = this.injector.get(WorkPackageTableHierarchiesService); + readonly wpTablePagination:WorkPackageTablePaginationService = this.injector.get(WorkPackageTablePaginationService); + readonly wpListService:WorkPackagesListService = this.injector.get(WorkPackagesListService); + readonly wpListChecksumService:WorkPackagesListChecksumService = this.injector.get(WorkPackagesListChecksumService); + readonly loadingIndicatorService:LoadingIndicatorService = this.injector.get(LoadingIndicatorService); + readonly $transitions:TransitionService = this.injector.get(TransitionService); + readonly I18n:I18nService = this.injector.get(I18nService); + readonly wpStaticQueries:WorkPackageStaticQueriesService = this.injector.get(WorkPackageStaticQueriesService); + + constructor(protected injector:Injector) { } ngOnInit() { // Listen to changes on the query state objects this.setupQueryObservers(); - this.initialQueryLoading(); - // Listen for refresh changes this.setupRefreshObserver(); - - this.updateQueryOnParamsChanges(); } ngOnDestroy():void { - if (this.removeTransitionSubscription) { - this.removeTransitionSubscription(); - } this.wpTableRefresh.clear('Table controller scope destroyed.'); } private setupQueryObservers() { - this.querySpace.tableRendering.onQueryUpdated.values$() - .pipe( - take(1) - ) - .subscribe(() => this.tableInformationLoaded = true); - this.querySpace.ready.fireOnStateChange(this.wpTablePagination.state, 'Query loaded').values$().pipe( untilComponentDestroyed(this), @@ -119,7 +99,7 @@ export class WorkPackagesSetComponent implements OnInit, OnDestroy { ).subscribe(([pagination, query]) => { if (this.wpListChecksumService.isQueryOutdated(query, pagination)) { this.wpListChecksumService.update(query, pagination); - this.updateResultsVisibly(); + this.refresh(true, false); } }); @@ -133,13 +113,23 @@ export class WorkPackagesSetComponent implements OnInit, OnDestroy { this.setupChangeObserver(this.wpTableHighlighting); } - setupChangeObserver(service:WorkPackageQueryStateService, firstPage:boolean = false) { + /** + * Listen to changes in the given service and reload the query / results if + * the service requests that. + * + * @param service Work package query state service to listento + * @param firstPage If the service requests a change, load the first page + */ + protected setupChangeObserver(service:WorkPackageQueryStateService, firstPage:boolean = false) { const queryState = this.querySpace.query; - this.querySpace.ready.fireOnStateChange(service.state, 'Query loaded').values$().pipe( - untilComponentDestroyed(this), - filter(() => queryState.hasValue() && service.hasChanged(queryState.value!)) - ).subscribe(() => { + this.querySpace.ready + .fireOnStateChange(service.state, 'Query loaded') + .values$() + .pipe( + untilComponentDestroyed(this), + filter(() => queryState.hasValue() && service.hasChanged(queryState.value!)) + ).subscribe(() => { const newQuery = queryState.value!; const triggerUpdate = service.applyToQuery(newQuery); this.querySpace.query.putValue(newQuery); @@ -158,68 +148,30 @@ export class WorkPackagesSetComponent implements OnInit, OnDestroy { * Setup the listener for members of the table to request a refresh of the entire table * through the refresh service. */ - setupRefreshObserver() { - this.wpTableRefresh.state.values$('Refresh listener in wp-list.controller').pipe( + protected setupRefreshObserver() { + this.wpTableRefresh.state.values$('Refresh listener in wp-set.component').pipe( untilComponentDestroyed(this), auditTime(20) ).subscribe(([refreshVisibly, firstPage]) => { - if (refreshVisibly) { - debugLog('Refreshing work package results visibly.'); - this.updateResultsVisibly(firstPage); - } else { - debugLog('Refreshing work package results in the background.'); - this.updateResults(); - } + debugLog('Refreshing work package results.'); + this.refresh(refreshVisibly, firstPage); }); } - updateResults():Promise { - return this.wpListService.reloadCurrentResultsList(); - } - - updateResultsVisibly(firstPage:boolean = false) { - if (firstPage) { - this.loadingIndicator.table.promise = this.updateToFirstResultsPage(); - } else { - this.loadingIndicator.table.promise = this.updateResults(); - } - } - - updateToFirstResultsPage():Promise { - return this.wpListService.loadCurrentResultsListFirstPage(); - } - private updateQueryOnParamsChanges() { - // Listen for param changes - this.removeTransitionSubscription = this.$transitions.onSuccess({}, (transition):any => { - let options = transition.options(); - - // Avoid performing any changes when we're going to reload - if (options.reload || (options.custom && options.custom.notify === false)) { - return true; - } - - const params = transition.params('to'); - let newChecksum = this.wpListService.getCurrentQueryProps(params); - let newId = params.query_id && parseInt(params.query_id); - - this.wpListChecksumService - .executeIfOutdated(newId, - newChecksum, - () => this.loadCurrentQuery()); - }); - } - - protected initialQueryLoading() { - this.wpTableRefresh.clear('Impending query loading.'); - this.loadCurrentQuery(); - } - - protected loadCurrentQuery():Promise { - let loadingPromise = this.wpListService.loadCurrentQueryFromParams(this.projectIdentifier); + /** + * Refresh the set of results, + * showing the loading indicator if visibly is set. + * + * @param visibly Set true when desiring the loading indicator + * @param firstPage Reload the page to first page. + */ + public abstract refresh(visibly:boolean, firstPage:boolean):Promise; - this.loadingIndicator.table.promise = loadingPromise; - return loadingPromise; - } + /** + * Set the loading indicator for this set instance + * @param promise + */ + protected abstract set loadingIndicator(promise:Promise); } diff --git a/modules/grids/spec/features/my/my_page_assigned_to_me_spec.rb b/modules/grids/spec/features/my/my_page_assigned_to_me_spec.rb index 901556689c..03abb8aa68 100644 --- a/modules/grids/spec/features/my/my_page_assigned_to_me_spec.rb +++ b/modules/grids/spec/features/my/my_page_assigned_to_me_spec.rb @@ -36,6 +36,7 @@ describe 'Assigned to me embedded query on my page', type: :feature, js: true do let!(:assigned_work_package) do FactoryBot.create :work_package, project: project, + subject: 'Assigned to me', type: type, author: user, assigned_to: user @@ -43,6 +44,7 @@ describe 'Assigned to me embedded query on my page', type: :feature, js: true do let!(:assigned_to_other_work_package) do FactoryBot.create :work_package, project: project, + subject: 'Not assigend to me', type: type, author: user, assigned_to: other_user @@ -61,16 +63,61 @@ describe 'Assigned to me embedded query on my page', type: :feature, js: true do let(:my_page) do Pages::My::Page.new end + let(:assigned_area) { Components::Grids::GridArea.new('.grid--area', text: 'Work packages assigned to me') } + let(:embedded_table) { Pages::EmbeddedWorkPackagesTable.new(assigned_area.area) } + let(:hierarchies) { ::Components::WorkPackages::Hierarchies.new } before do login_as user + end - my_page.visit! + context 'with parent work package' do + let!(:assigned_work_package_child) do + FactoryBot.create :work_package, + subject: 'Child', + parent: assigned_work_package, + project: project, + type: type, + author: user, + assigned_to: user + end + + it 'can toggle hierarchy mode in embedded tables (Regression test #29578)' do + my_page.visit! + + # exists as default + assigned_area.expect_to_exist + + page.within(assigned_area.area) do + # expect hierarchy in child + hierarchies.expect_mode_enabled + + hierarchies.expect_hierarchy_at assigned_work_package + hierarchies.expect_leaf_at assigned_work_package_child + + # toggle parent + hierarchies.toggle_row assigned_work_package + hierarchies.expect_hierarchy_at assigned_work_package, collapsed: true + + # disable + hierarchies.disable_via_header + hierarchies.expect_no_hierarchies + + # re-enable + hierarchies.enable_via_header + + hierarchies.expect_mode_enabled + # Re-enabling resets collapsed state for now + hierarchies.expect_hierarchy_at assigned_work_package, collapsed: false + hierarchies.expect_leaf_at assigned_work_package_child + end + end end it 'can create a new ticket with correct me values (Regression test #28488)' do + my_page.visit! + # exists as default - assigned_area = Components::Grids::GridArea.new('.grid--area', text: 'Work packages assigned to me') assigned_area.expect_to_exist expect(assigned_area.area) @@ -79,7 +126,6 @@ describe 'Assigned to me embedded query on my page', type: :feature, js: true do expect(assigned_area.area) .to have_no_selector('.subject', text: assigned_to_other_work_package.subject) - embedded_table = Pages::EmbeddedWorkPackagesTable.new(assigned_area.area) embedded_table.click_inline_create subject_field = embedded_table.edit_field(nil, :subject) diff --git a/spec/support/components/work_packages/hierarchies.rb b/spec/support/components/work_packages/hierarchies.rb index a22a371467..c555efa494 100644 --- a/spec/support/components/work_packages/hierarchies.rb +++ b/spec/support/components/work_packages/hierarchies.rb @@ -44,6 +44,10 @@ module Components page.find('.wp-table--table-header .icon-no-hierarchy').click end + def disable_via_header + page.find('.wp-table--table-header .icon-hierarchy').click + end + def disable_hierarchy ::Components::WorkPackages::TableConfigurationModal.do_and_save do |modal| modal.open_and_set_display_mode :default