From 8113ed9d1983fa0a0f150e71fb487ef3ec322b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 20 Jun 2017 08:57:05 +0200 Subject: [PATCH] Show relation rows as regular members of the table Still WIP: - Relation column is rendered as all other rows (indicator for dropdown), which is confusing --- .../work_packages/_table_relations.sass | 2 +- .../work_packages/timelines/_timelines.sass | 4 + .../hal-request/hal-request.service.test.ts | 19 ++++ .../api-v3/hal-request/hal-request.service.ts | 39 +++++++ .../api-work-packages.service.ts | 9 +- frontend/app/components/states.service.ts | 64 ++++++----- .../app/components/states/switch-state.ts | 4 + .../work-package-cache.service.ts | 28 ++--- .../wp-edit-form/table-row-edit-context.ts | 10 +- .../modes/grouped/grouped-render-pass.ts | 27 +---- .../modes/grouped/grouped-rows-builder.ts | 6 +- .../modes/hierarchy/hierarchy-render-pass.ts | 26 ++--- .../modes/hierarchy/hierarchy-rows-builder.ts | 1 - .../hierarchy/single-hierarchy-row-builder.ts | 57 +++++----- .../modes/plain/plain-rows-builder.ts | 2 +- .../builders/modes/rows-builder.ts | 15 --- .../builders/primary-render-pass.ts | 100 ++++++++++------ .../relations/relation-row-builder.ts | 107 +++++++++++------- .../relations/relations-render-pass.ts | 77 ++++++++++--- .../builders/rows/row-refresh-builder.ts | 53 --------- .../builders/rows/single-row-builder.ts | 90 +++++++++++---- .../builders/timeline/timeline-render-pass.ts | 11 +- .../builders/timeline/timeline-row-builder.ts | 7 -- .../handlers/cell/edit-cell-handler.ts | 29 ++--- .../handlers/cell/relations-cell-handler.ts | 6 +- .../handlers/row/click-handler.ts | 14 ++- .../handlers/row/context-menu-handler.ts | 4 +- .../row/context-menu-keyboard-handler.ts | 4 +- .../handlers/row/double-click-handler.ts | 10 +- .../handlers/row/hierarchy-click-handler.ts | 6 +- .../handlers/state/hierarchy-transformer.ts | 6 +- .../handlers/state/relations-transformer.ts | 2 +- .../handlers/state/rows-transformer.ts | 19 +--- .../handlers/state/selection-transformer.ts | 6 +- .../helpers/wp-table-hierarchy-helpers.ts | 4 + .../helpers/wp-table-row-helpers.ts | 8 +- .../wp-table-relation-columns.service.ts | 5 +- .../components/wp-fast-table/wp-fast-table.ts | 49 ++++---- .../inline-create-row-builder.ts | 18 ++- .../wp-inline-create.directive.ts | 14 +-- .../app/components/wp-list/wp-list.service.ts | 2 +- .../app/components/wp-query/query-column.ts | 6 +- .../timeline/cells/wp-timeline-cell.ts | 52 ++++----- .../cells/wp-timeline-cells-renderer.ts | 60 +++++----- .../wp-timeline-container.directive.ts | 25 ++-- .../wp-timeline-relations.directive.ts | 48 +++++--- frontend/app/vendors.js | 2 + frontend/npm-shrinkwrap.json | 5 + frontend/package.json | 1 + .../work_packages/work_package_query_spec.rb | 8 +- spec/features/support/work_package_table.rb | 4 +- .../features/work_packages/navigation_spec.rb | 2 +- .../work_packages/table/hierarchy_spec.rb | 4 +- .../work_packages/table/relations_spec.rb | 4 +- .../components/work_packages/context_menu.rb | 2 +- .../components/work_packages/hierarchies.rb | 8 +- spec/support/pages/work_packages_table.rb | 8 +- spec/support/pages/work_packages_timeline.rb | 4 +- 58 files changed, 690 insertions(+), 517 deletions(-) delete mode 100644 frontend/app/components/wp-fast-table/builders/rows/row-refresh-builder.ts diff --git a/app/assets/stylesheets/content/work_packages/_table_relations.sass b/app/assets/stylesheets/content/work_packages/_table_relations.sass index 5857c9268f..be22b44da4 100644 --- a/app/assets/stylesheets/content/work_packages/_table_relations.sass +++ b/app/assets/stylesheets/content/work_packages/_table_relations.sass @@ -24,7 +24,7 @@ body:not(.accessibility-mode ) background: white // Override default left-align of cells -.wp-table--relation-cell-td +.wp-table--relation-cell-td, text-align: center !important // Fix padding on additional row/id field diff --git a/app/assets/stylesheets/content/work_packages/timelines/_timelines.sass b/app/assets/stylesheets/content/work_packages/timelines/_timelines.sass index 1cf429d8ab..91471694e9 100644 --- a/app/assets/stylesheets/content/work_packages/timelines/_timelines.sass +++ b/app/assets/stylesheets/content/work_packages/timelines/_timelines.sass @@ -36,6 +36,10 @@ // Position 45px below header top: $generic-table--header-height height: calc(100% - #{$generic-table--header-height}) + // Position above the cells container + z-index: 1 + // Disable pointer-events to allow cells to handle them + pointer-events: none // Left border for the timeline .work-packages-split-view--left-timeline diff --git a/frontend/app/components/api/api-v3/hal-request/hal-request.service.test.ts b/frontend/app/components/api/api-v3/hal-request/hal-request.service.test.ts index 197b560e38..afd78f28b3 100644 --- a/frontend/app/components/api/api-v3/hal-request/hal-request.service.test.ts +++ b/frontend/app/components/api/api-v3/hal-request/hal-request.service.test.ts @@ -218,5 +218,24 @@ describe('halRequest service', () => { $httpBackend.flush(); }); }); + + describe('#getAllPaginated', () => { + const params = {}; + let promise:any; + + beforeEach(() => { + promise = halRequest.getAllPaginated('href', 25, params); + + $httpBackend.expectGET('href?offset=1').respond(200, { count: 12, total: 25 }); + $httpBackend.expectGET('href?offset=2').respond(200, { count: 12, total: 25 }); + $httpBackend.expectGET('href?offset=3').respond(200, { count: 1, total: 25 }); + }); + + it('should resolve with three results', () => { + expect(promise).to.eventually.be.fulfilled.then(allResults => { + expect(allResults.length).to.eq(3); + }); + }); + }); }); }); diff --git a/frontend/app/components/api/api-v3/hal-request/hal-request.service.ts b/frontend/app/components/api/api-v3/hal-request/hal-request.service.ts index b194b3bce3..5df64502da 100644 --- a/frontend/app/components/api/api-v3/hal-request/hal-request.service.ts +++ b/frontend/app/components/api/api-v3/hal-request/hal-request.service.ts @@ -30,6 +30,7 @@ import {opApiModule} from '../../../../angular-modules'; import {HalResource} from '../hal-resources/hal-resource.service'; import {HalResourceFactoryService} from '../hal-resource-factory/hal-resource-factory.service'; import IPromise = angular.IPromise; +import {CollectionResource} from '../hal-resources/collection-resource.service'; export class HalRequestService { /** @@ -97,6 +98,44 @@ export class HalRequestService { return this.request('get', href, params, headers); } + /** + * Return all potential pages to the request, when the elements returned from API is smaller + * than the expected. + * + * @param href + * @param expected The expected number of elements + * @param params + * @param headers + * @return {Promise} + */ + public async getAllPaginated(href:string, expected:number, params:any = {}, headers:any = {}) { + // Total number retrieved + let retrieved = 0; + // Current offset page + let page = 1; + // Accumulated results + const allResults:CollectionResource[] = []; + // If possible, request all at once. + params.pageSize = expected; + + while (retrieved < expected) { + params.offset = page; + + const results = await this.request('get', href, params, headers); + + if (results.count === 0) { + throw 'No more results for this query, but expected more.'; + } + + allResults.push(results as CollectionResource); + + retrieved += results.count; + page += 1; + } + + return allResults; + } + /** * Perform a PUT request and return a resource promise. * @param href diff --git a/frontend/app/components/api/api-work-packages/api-work-packages.service.ts b/frontend/app/components/api/api-work-packages/api-work-packages.service.ts index cc5bce8136..3417953244 100644 --- a/frontend/app/components/api/api-work-packages/api-work-packages.service.ts +++ b/frontend/app/components/api/api-work-packages/api-work-packages.service.ts @@ -68,13 +68,14 @@ export class ApiWorkPackagesService { * Returns a WP Collection with schemas and results embedded. * * @param ids - * @return {WorkPackageCollectionResourceInterface} + * @return {WorkPackageCollectionResourceInterface[]} */ - public loadWorkPackagesCollectionFor(ids:string[]) { - return this.halRequest.get( + public loadWorkPackagesCollectionsFor(ids:string[]) { + return this.halRequest.getAllPaginated( this.v3Path.wps(), + ids.length, { - filters: buildApiV3Filter('id', '=', ids).toJson() + filters: buildApiV3Filter('id', '=', ids).toJson(), }, { caching: { enabled: false } diff --git a/frontend/app/components/states.service.ts b/frontend/app/components/states.service.ts index 643caf3ebe..bfadf7a700 100644 --- a/frontend/app/components/states.service.ts +++ b/frontend/app/components/states.service.ts @@ -1,31 +1,39 @@ -import {WorkPackageTableRelationColumns} from "./wp-fast-table/wp-table-relation-columns"; import { - combine, createNewContext, derive, input, multiInput, State, + combine, + createNewContext, + derive, + input, + multiInput, + State, StatesGroup -} from "reactivestates"; -import {Subject} from "rxjs"; -import {opServicesModule} from "../angular-modules"; -import {QueryFormResource} from "./api/api-v3/hal-resources/query-form-resource.service"; -import {QueryResource} from "./api/api-v3/hal-resources/query-resource.service"; -import {SchemaResource} from "./api/api-v3/hal-resources/schema-resource.service"; -import {TypeResource} from "./api/api-v3/hal-resources/type-resource.service"; -import {WorkPackageResource} from "./api/api-v3/hal-resources/work-package-resource.service"; -import {GroupObject, WorkPackageCollectionResource} from "./api/api-v3/hal-resources/wp-collection-resource.service"; -import {WorkPackageEditForm} from "./wp-edit-form/work-package-edit-form"; -import {RenderedRow, TableRenderResult} from "./wp-fast-table/builders/primary-render-pass"; -import {WorkPackageTableColumns} from "./wp-fast-table/wp-table-columns"; -import {WorkPackageTableFilters} from "./wp-fast-table/wp-table-filters"; -import {WorkPackageTableGroupBy} from "./wp-fast-table/wp-table-group-by"; -import {WorkPackageTableHierarchies} from "./wp-fast-table/wp-table-hierarchies"; -import {WorkPackageTablePagination} from "./wp-fast-table/wp-table-pagination"; -import {WorkPackageTableSortBy} from "./wp-fast-table/wp-table-sort-by"; -import {WorkPackageTableSum} from "./wp-fast-table/wp-table-sum"; -import {WorkPackageTableTimelineState} from "./wp-fast-table/wp-table-timeline"; -import {WPTableRowSelectionState} from "./wp-fast-table/wp-table.interfaces"; -import {SwitchState} from "./states/switch-state"; -import {QueryColumn} from "./wp-query/query-column"; -import {QuerySortByResource} from "./api/api-v3/hal-resources/query-sort-by-resource.service"; -import {QueryGroupByResource} from "./api/api-v3/hal-resources/query-group-by-resource.service"; +} from 'reactivestates'; +import {Subject} from 'rxjs'; +import {opServicesModule} from '../angular-modules'; +import {QueryFormResource} from './api/api-v3/hal-resources/query-form-resource.service'; +import {QueryResource} from './api/api-v3/hal-resources/query-resource.service'; +import {SchemaResource} from './api/api-v3/hal-resources/schema-resource.service'; +import {TypeResource} from './api/api-v3/hal-resources/type-resource.service'; +import {WorkPackageResource} from './api/api-v3/hal-resources/work-package-resource.service'; +import { + GroupObject, + WorkPackageCollectionResource +} from './api/api-v3/hal-resources/wp-collection-resource.service'; +import {WorkPackageEditForm} from './wp-edit-form/work-package-edit-form'; +import {WorkPackageTableColumns} from './wp-fast-table/wp-table-columns'; +import {WorkPackageTableFilters} from './wp-fast-table/wp-table-filters'; +import {WorkPackageTableGroupBy} from './wp-fast-table/wp-table-group-by'; +import {WorkPackageTableHierarchies} from './wp-fast-table/wp-table-hierarchies'; +import {WorkPackageTablePagination} from './wp-fast-table/wp-table-pagination'; +import {WorkPackageTableSortBy} from './wp-fast-table/wp-table-sort-by'; +import {WorkPackageTableSum} from './wp-fast-table/wp-table-sum'; +import {WorkPackageTableTimelineState} from './wp-fast-table/wp-table-timeline'; +import {RenderedRow} from './wp-fast-table/builders/primary-render-pass'; +import {SwitchState} from './states/switch-state'; +import {QueryColumn} from './wp-query/query-column'; +import {QuerySortByResource} from './api/api-v3/hal-resources/query-sort-by-resource.service'; +import {QueryGroupByResource} from './api/api-v3/hal-resources/query-group-by-resource.service'; +import {WPTableRowSelectionState} from './wp-fast-table/wp-table.interfaces'; +import {WorkPackageTableRelationColumns} from './wp-fast-table/wp-table-relation-columns'; export class States extends StatesGroup { @@ -88,10 +96,10 @@ export class TableState extends StatesGroup { // Hierarchies of table hierarchies = input(); // State to be updated when the table is up to date - rendered = input(); + rendered = input(); renderedWorkPackages: State = derive(this.rendered, $ => $ - .map(rows => rows.renderedOrder.filter(row => row.isWorkPackage))); + .map(rows => rows.filter(row => !!row.workPackageId))); // State to determine timeline visibility timelineVisible = input(); diff --git a/frontend/app/components/states/switch-state.ts b/frontend/app/components/states/switch-state.ts index 8fb8ffa27d..f02a2ce648 100644 --- a/frontend/app/components/states/switch-state.ts +++ b/frontend/app/components/states/switch-state.ts @@ -16,6 +16,10 @@ export class SwitchState { return (this.contextSwitch$.value !== to); } + public get current():StateName|undefined { + return this.contextSwitch$.value; + } + public reset(reason?: string) { debugLog('Resetting table context.'); this.contextSwitch$.clear(reason); diff --git a/frontend/app/components/work-packages/work-package-cache.service.ts b/frontend/app/components/work-packages/work-package-cache.service.ts index 7f5ea9f9ca..0117a22e3a 100644 --- a/frontend/app/components/work-packages/work-package-cache.service.ts +++ b/frontend/app/components/work-packages/work-package-cache.service.ts @@ -107,7 +107,7 @@ export class WorkPackageCacheService { * * @param workPackageIds */ - loadWorkPackages(workPackageIds:string[]):ng.IPromise { + loadWorkPackages(workPackageIds:string[]):Promise { const needToLoad:string[] = []; workPackageIds.forEach((id:string) => { @@ -121,18 +121,20 @@ export class WorkPackageCacheService { } return this.apiWorkPackages - .loadWorkPackagesCollectionFor(workPackageIds) - .then((results:WorkPackageCollectionResourceInterface) => { - - if (results.schemas) { - _.each(results.schemas.elements, (schema:SchemaResource) => { - this.states.schemas.get(schema.href as string).putValue(schema); - }); - } - - if (results.elements) { - this.updateWorkPackageList(results.elements); - } + .loadWorkPackagesCollectionsFor(workPackageIds) + .then((pagedResults:WorkPackageCollectionResourceInterface[]) => { + + _.each(pagedResults, (results) => { + if (results.schemas) { + _.each(results.schemas.elements, (schema:SchemaResource) => { + this.states.schemas.get(schema.href as string).putValue(schema); + }); + } + + if (results.elements) { + this.updateWorkPackageList(results.elements); + } + }); }); } diff --git a/frontend/app/components/wp-edit-form/table-row-edit-context.ts b/frontend/app/components/wp-edit-form/table-row-edit-context.ts index 944f20be3a..970e59d5e4 100644 --- a/frontend/app/components/wp-edit-form/table-row-edit-context.ts +++ b/frontend/app/components/wp-edit-form/table-row-edit-context.ts @@ -28,7 +28,7 @@ import {WorkPackageEditContext} from './work-package-edit-context'; import {WorkPackageTableRow} from '../wp-fast-table/wp-table.interfaces'; -import { CellBuilder, tdClassName, editCellContainer } from '../wp-fast-table/builders/cell-builder'; +import {CellBuilder, tdClassName, editCellContainer} from '../wp-fast-table/builders/cell-builder'; import {injectorBridge} from '../angular/angular-injector-bridge.functions'; import {WorkPackageResource} from '../api/api-v3/hal-resources/work-package-resource.service'; import {WorkPackageCacheService} from '../work-packages/work-package-cache.service'; @@ -51,7 +51,7 @@ export class TableRowEditContext implements WorkPackageEditContext { // Use cell builder to reset edit fields private cellBuilder = new CellBuilder(); - constructor(public workPackageId:string) { + constructor(public workPackageId:string, public classIdentifier:string) { injectorBridge(this); } @@ -71,7 +71,7 @@ export class TableRowEditContext implements WorkPackageEditContext { } } - public requireVisible(fieldName:string): PromiseLike { + public requireVisible(fieldName:string):PromiseLike { this.wpTableColumns.addColumn(fieldName); return this.waitForContainer(fieldName); } @@ -86,7 +86,7 @@ export class TableRowEditContext implements WorkPackageEditContext { // Ensure the given field is visible. // We may want to look into MutationObserver if we need this in several places. - private waitForContainer(fieldName:string): PromiseLike { + private waitForContainer(fieldName:string):PromiseLike { const deferred = this.$q.defer(); const interval = setInterval(() => { @@ -102,7 +102,7 @@ export class TableRowEditContext implements WorkPackageEditContext { } private get rowContainer() { - return jQuery(`#${rowId(this.workPackageId)}`); + return jQuery(`.${this.classIdentifier}-table`); } } diff --git a/frontend/app/components/wp-fast-table/builders/modes/grouped/grouped-render-pass.ts b/frontend/app/components/wp-fast-table/builders/modes/grouped/grouped-render-pass.ts index 7ed098215f..d7edf526e3 100644 --- a/frontend/app/components/wp-fast-table/builders/modes/grouped/grouped-render-pass.ts +++ b/frontend/app/components/wp-fast-table/builders/modes/grouped/grouped-render-pass.ts @@ -8,7 +8,6 @@ import {HalResource} from '../../../../api/api-v3/hal-resources/hal-resource.ser import {groupClassNameFor, GroupHeaderBuilder} from './group-header-builder'; import {groupByProperty, groupedRowClassName} from './grouped-rows-helpers'; import {PlainRenderPass} from '../plain/plain-render-pass'; -import {RenderedRow} from '../../primary-render-pass'; export class GroupedRenderPass extends PlainRenderPass { constructor(public workPackageTable:WorkPackageTable, @@ -85,23 +84,6 @@ export class GroupedRenderPass extends PlainRenderPass { ); } - public augmentSecondaryElement(row:HTMLElement, rendered:RenderedRow):HTMLElement { - if (!rendered.belongsTo) { - return row; - } - - const wpRow = this.workPackageTable.rowIndex[rendered.belongsTo.id]; - const group = wpRow.group; - - if (!group) { - return row; - } - - row.classList.add(groupedRowClassName(group.index as number)); - - return row; - } - /** * Enhance a row from the rowBuilder with group information. */ @@ -109,14 +91,17 @@ export class GroupedRenderPass extends PlainRenderPass { const group = row.group!; const hidden = group.collapsed; + let additionalClasses:string[] = []; + let [tr, _] = this.rowBuilder.buildEmpty(row.object); - tr.classList.add(groupedRowClassName(group.index as number)); + additionalClasses.push(groupedRowClassName(group.index as number)); if (hidden) { - tr.classList.add(collapsedRowClass); + additionalClasses.push(collapsedRowClass); } row.element = tr; - this.appendRow(row.object, tr, hidden); + tr.classList.add(...additionalClasses); + this.appendRow(row.object, tr, additionalClasses, hidden); } } diff --git a/frontend/app/components/wp-fast-table/builders/modes/grouped/grouped-rows-builder.ts b/frontend/app/components/wp-fast-table/builders/modes/grouped/grouped-rows-builder.ts index 04cd762e5b..74ca43a445 100644 --- a/frontend/app/components/wp-fast-table/builders/modes/grouped/grouped-rows-builder.ts +++ b/frontend/app/components/wp-fast-table/builders/modes/grouped/grouped-rows-builder.ts @@ -7,7 +7,7 @@ import {GroupObject} from '../../../../api/api-v3/hal-resources/wp-collection-re import {GroupedRenderPass} from './grouped-render-pass'; import {groupedRowClassName, groupIdentifier} from './grouped-rows-helpers'; import {GroupHeaderBuilder} from './group-header-builder'; -import {rowClassName} from '../../rows/single-row-builder'; +import {tableRowClassName} from '../../rows/single-row-builder'; export const rowGroupClassName = 'wp-table--group-header'; export const collapsedRowClass = '-collapsed'; @@ -85,12 +85,12 @@ export class GroupedRowsBuilder extends RowsBuilder { affected.toggleClass(collapsedRowClass, group.collapsed); // Update the hidden section of the rendered state - affected.filter(`.${rowClassName}`).each((i, el) => { + affected.filter(`.${tableRowClassName}`).each((i, el) => { // Get the index of this row const index = jQuery(el).index(); // Update the hidden state - rendered.renderedOrder[index].hidden = !!group.collapsed; + rendered[index].hidden = !!group.collapsed; }); }); diff --git a/frontend/app/components/wp-fast-table/builders/modes/hierarchy/hierarchy-render-pass.ts b/frontend/app/components/wp-fast-table/builders/modes/hierarchy/hierarchy-render-pass.ts index bfa3a2aad3..a97c05543f 100644 --- a/frontend/app/components/wp-fast-table/builders/modes/hierarchy/hierarchy-render-pass.ts +++ b/frontend/app/components/wp-fast-table/builders/modes/hierarchy/hierarchy-render-pass.ts @@ -3,15 +3,15 @@ import {WorkPackageResourceInterface} from '../../../../api/api-v3/hal-resources import {SingleHierarchyRowBuilder} from './single-hierarchy-row-builder'; import {WorkPackageTableRow} from '../../../wp-table.interfaces'; import { + ancestorClassIdentifier, collapsedGroupClass, hierarchyGroupClass, hierarchyRootClass } from '../../../helpers/wp-table-hierarchy-helpers'; -import {PrimaryRenderPass, RenderedRow} from '../../primary-render-pass'; +import {PrimaryRenderPass} from '../../primary-render-pass'; import {States} from '../../../../states.service'; import {$injectFields} from '../../../../angular/angular-injector-bridge.functions'; import {WorkPackageTableHierarchies} from '../../../wp-table-hierarchies'; -import {rowClass} from '../../../helpers/wp-table-row-helpers'; export class HierarchyRenderPass extends PrimaryRenderPass { public states:States; @@ -181,19 +181,16 @@ export class HierarchyRenderPass extends PrimaryRenderPass { private markRendered(workPackage:WorkPackageResourceInterface, hidden:boolean = false, isAncestor:boolean) { this.rendered[workPackage.id] = true; this.renderedOrder.push({ - isWorkPackage: !isAncestor, - belongsTo: workPackage, + classIdentifier: isAncestor ? ancestorClassIdentifier(workPackage.id) : this.rowBuilder.classIdentifier(workPackage), + additionalClasses: this.ancestorClasses(workPackage), + workPackage: isAncestor ? null : workPackage, + renderType: 'primary', hidden: hidden }); } - public augmentSecondaryElement(row:HTMLElement, rendered:RenderedRow):HTMLElement { - if (!rendered.belongsTo) { - return row; - } - - const workPackage = rendered.belongsTo; + public ancestorClasses(workPackage:WorkPackageResourceInterface) { const rowClasses = [hierarchyRootClass(workPackage.id)]; if (_.isArray(workPackage.ancestors)) { @@ -207,8 +204,7 @@ export class HierarchyRenderPass extends PrimaryRenderPass { }); } - row.classList.add(...rowClasses); - return row; + return rowClasses; } /** @@ -229,8 +225,10 @@ export class HierarchyRenderPass extends PrimaryRenderPass { el, `${hierarchyRoot},${hierarchyGroup}`, { - isWorkPackage: !isAncestor, - belongsTo: workPackage, + classIdentifier: isAncestor ? ancestorClassIdentifier(workPackage.id) : this.rowBuilder.classIdentifier(workPackage), + workPackage: isAncestor ? null : workPackage, + additionalClasses: this.ancestorClasses(workPackage), + renderType: 'primary', hidden: hidden, } ); diff --git a/frontend/app/components/wp-fast-table/builders/modes/hierarchy/hierarchy-rows-builder.ts b/frontend/app/components/wp-fast-table/builders/modes/hierarchy/hierarchy-rows-builder.ts index 335e386824..dba22d68c1 100644 --- a/frontend/app/components/wp-fast-table/builders/modes/hierarchy/hierarchy-rows-builder.ts +++ b/frontend/app/components/wp-fast-table/builders/modes/hierarchy/hierarchy-rows-builder.ts @@ -21,7 +21,6 @@ export class HierarchyRowsBuilder extends RowsBuilder { super(workPackageTable); injectorBridge(this); this.rowBuilder = new SingleHierarchyRowBuilder(this.workPackageTable); - this.refreshBuilder = this.rowBuilder; } /** diff --git a/frontend/app/components/wp-fast-table/builders/modes/hierarchy/single-hierarchy-row-builder.ts b/frontend/app/components/wp-fast-table/builders/modes/hierarchy/single-hierarchy-row-builder.ts index b5e5c2c349..575b16b901 100644 --- a/frontend/app/components/wp-fast-table/builders/modes/hierarchy/single-hierarchy-row-builder.ts +++ b/frontend/app/components/wp-fast-table/builders/modes/hierarchy/single-hierarchy-row-builder.ts @@ -3,7 +3,6 @@ import {WorkPackageTableRow} from '../../../wp-table.interfaces'; import {WorkPackageResourceInterface} from '../../../../api/api-v3/hal-resources/work-package-resource.service'; import {WorkPackageTableHierarchiesService} from '../../../state/wp-table-hierarchy.service'; import {$injectFields} from '../../../../angular/angular-injector-bridge.functions'; -import {RowRefreshBuilder} from '../../rows/row-refresh-builder'; import {WorkPackageEditForm} from '../../../../wp-edit-form/work-package-edit-form'; import { collapsedGroupClass, @@ -12,11 +11,12 @@ import { } from '../../../helpers/wp-table-hierarchy-helpers'; import {UiStateLinkBuilder} from '../../ui-state-link-builder'; import {QueryColumn} from '../../../../wp-query/query-column'; +import {SingleRowBuilder} from '../../rows/single-row-builder'; export const indicatorCollapsedClass = '-hierarchy-collapsed'; export const hierarchyCellClassName = 'wp-table--hierarchy-span'; -export class SingleHierarchyRowBuilder extends RowRefreshBuilder { +export class SingleHierarchyRowBuilder extends SingleRowBuilder { // Injected public wpTableHierarchies:WorkPackageTableHierarchiesService; @@ -27,14 +27,16 @@ export class SingleHierarchyRowBuilder extends RowRefreshBuilder { collapsed:(level:number) => string; }; - constructor(protected workPackageTable: WorkPackageTable) { + constructor(protected workPackageTable:WorkPackageTable) { super(workPackageTable); $injectFields(this, 'wpTableHierarchies'); this.text = { - leaf: (level:number) => this.I18n.t('js.work_packages.hierarchy.leaf', { level: level }), - expanded: (level:number) => this.I18n.t('js.work_packages.hierarchy.children_expanded', { level: level }), - collapsed: (level:number) => this.I18n.t('js.work_packages.hierarchy.children_collapsed', { level: level }), + leaf: (level:number) => this.I18n.t('js.work_packages.hierarchy.leaf', {level: level}), + expanded: (level:number) => this.I18n.t('js.work_packages.hierarchy.children_expanded', + {level: level}), + collapsed: (level:number) => this.I18n.t('js.work_packages.hierarchy.children_collapsed', + {level: level}), }; } @@ -42,17 +44,13 @@ export class SingleHierarchyRowBuilder extends RowRefreshBuilder { * Refresh a single row after structural changes. * Remembers and re-adds the hierarchy indicator if neccessary. */ - public refreshRow(row: WorkPackageTableRow, editForm: WorkPackageEditForm | undefined):[HTMLElement, boolean]|null { + public refreshRow(workPackage:WorkPackageResourceInterface, editForm:WorkPackageEditForm|undefined, jRow:JQuery):JQuery { // Remove any old hierarchy - const result = super.refreshRow(row, editForm); + const newRow = super.refreshRow(workPackage, editForm, jRow); + newRow.find(`.wp-table--hierarchy-span`).remove(); + this.appendHierarchyIndicator(workPackage, newRow); - if (result !== null) { - const [newRow, _hidden] = result; - jQuery(newRow).find(`.wp-table--hierarchy-span`).remove(); - this.appendHierarchyIndicator(row.object, newRow); - } - - return result; + return newRow; } /** @@ -72,17 +70,16 @@ export class SingleHierarchyRowBuilder extends RowRefreshBuilder { }); element.classList.add(`__hierarchy-root-${workPackage.id}`); - this.appendHierarchyIndicator(workPackage, element); + this.appendHierarchyIndicator(workPackage, jQuery(element)); return [element, hidden]; } /** * Append an additional ancestor row that is not yet loaded */ - public buildAncestorRow( - ancestor:WorkPackageResourceInterface, - ancestorGroups:string[], - index:number):[HTMLElement, boolean] { + public buildAncestorRow(ancestor:WorkPackageResourceInterface, + ancestorGroups:string[], + index:number):[HTMLElement, boolean] { const loadedRow = this.workPackageTable.rowIndex[ancestor.id]; @@ -95,7 +92,9 @@ export class SingleHierarchyRowBuilder extends RowRefreshBuilder { const tr = this.createEmptyRow(ancestor); const columns = this.wpTableColumns.getColumns(); - tr.classList.add(`wp-table--hierarchy-aditional-row`, hierarchyRootClass(ancestor.id), ...ancestorGroups); + tr.classList.add(`wp-table--hierarchy-aditional-row`, + hierarchyRootClass(ancestor.id), + ...ancestorGroups); // Set available information for ID and subject column // and print hierarchy indicator at subject field. @@ -135,19 +134,18 @@ export class SingleHierarchyRowBuilder extends RowRefreshBuilder { * @param row * @param level */ - private appendHierarchyIndicator(workPackage:WorkPackageResourceInterface, row:HTMLElement, level?:number):void { - const jRow = jQuery(row); + private appendHierarchyIndicator(workPackage:WorkPackageResourceInterface, jRow:JQuery, level?:number):void { const hierarchyElement = this.buildHierarchyIndicator(workPackage, jRow, level); jRow.find('td.subject') - .addClass('-with-hierarchy') - .prepend(hierarchyElement); + .addClass('-with-hierarchy') + .prepend(hierarchyElement); } /** * Build the hierarchy indicator at the given indentation level. */ - private buildHierarchyIndicator(workPackage:WorkPackageResourceInterface, jRow:JQuery|null, index:number|null = null):HTMLElement { + private buildHierarchyIndicator(workPackage:WorkPackageResourceInterface, jRow:JQuery | null, index:number | null = null):HTMLElement { const level = index === null ? workPackage.ancestors.length : index; const hierarchyIndicator = document.createElement('span'); const collapsed = this.wpTableHierarchies.collapsed(workPackage.id); @@ -166,8 +164,10 @@ export class SingleHierarchyRowBuilder extends RowRefreshBuilder { hierarchyIndicator.innerHTML = ` - ${this.text.expanded(level)} - ${this.text.collapsed(level)} + ${this.text.expanded( + level)} + ${this.text.collapsed( + level)} `; } @@ -175,7 +175,6 @@ export class SingleHierarchyRowBuilder extends RowRefreshBuilder { return hierarchyIndicator; } - } SingleHierarchyRowBuilder.$inject = ['states', 'I18n']; diff --git a/frontend/app/components/wp-fast-table/builders/modes/plain/plain-rows-builder.ts b/frontend/app/components/wp-fast-table/builders/modes/plain/plain-rows-builder.ts index de4659e2c0..45728a09b0 100644 --- a/frontend/app/components/wp-fast-table/builders/modes/plain/plain-rows-builder.ts +++ b/frontend/app/components/wp-fast-table/builders/modes/plain/plain-rows-builder.ts @@ -12,7 +12,7 @@ export class PlainRowsBuilder extends RowsBuilder { protected rowBuilder:SingleRowBuilder; // The group expansion state - constructor(workPackageTable: WorkPackageTable) { + constructor(workPackageTable:WorkPackageTable) { super(workPackageTable); injectorBridge(this); diff --git a/frontend/app/components/wp-fast-table/builders/modes/rows-builder.ts b/frontend/app/components/wp-fast-table/builders/modes/rows-builder.ts index 331a810ec7..269d1bc719 100644 --- a/frontend/app/components/wp-fast-table/builders/modes/rows-builder.ts +++ b/frontend/app/components/wp-fast-table/builders/modes/rows-builder.ts @@ -1,17 +1,11 @@ import {States} from '../../../states.service'; import {WorkPackageTable} from '../../wp-fast-table'; -import {WorkPackageTableRow} from '../../wp-table.interfaces'; -import {RowRefreshBuilder} from '../rows/row-refresh-builder'; import {PrimaryRenderPass} from '../primary-render-pass'; -import {Subject} from 'rxjs'; export abstract class RowsBuilder { public states:States; - protected refreshBuilder:RowRefreshBuilder; - constructor(public workPackageTable:WorkPackageTable) { - this.refreshBuilder = new RowRefreshBuilder(this.workPackageTable); } /** @@ -25,15 +19,6 @@ export abstract class RowsBuilder { public isApplicable(table:WorkPackageTable) { return true; } - - /** - * Refresh a single row after structural changes. - * Will perform dirty checking for when a work package is currently being edited. - */ - public refreshRow(row:WorkPackageTableRow):[HTMLElement, boolean]|null { - let editing = this.states.editing.get(row.workPackageId).value; - return this.refreshBuilder.refreshRow(row, editing); - } } RowsBuilder.$inject = ['states']; diff --git a/frontend/app/components/wp-fast-table/builders/primary-render-pass.ts b/frontend/app/components/wp-fast-table/builders/primary-render-pass.ts index 50c2d38274..632aaa9c64 100644 --- a/frontend/app/components/wp-fast-table/builders/primary-render-pass.ts +++ b/frontend/app/components/wp-fast-table/builders/primary-render-pass.ts @@ -2,32 +2,39 @@ import {States} from '../../states.service'; import {WorkPackageTable} from '../wp-fast-table'; import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service'; import {$injectFields} from '../../angular/angular-injector-bridge.functions'; -import {rowClass} from '../helpers/wp-table-row-helpers'; import {TimelineRenderPass} from './timeline/timeline-render-pass'; import {SingleRowBuilder} from './rows/single-row-builder'; -import {RelationsRenderPass} from './relations/relations-render-pass'; +import {RelationRenderInfo, RelationsRenderPass} from './relations/relations-render-pass'; import {timeOutput} from '../../../helpers/debug_output'; -export interface RenderedRow { - isWorkPackage:boolean; +export type RenderedRowType = 'primary' | 'relations'; + +export interface RowRenderInfo { + // Unique class name as an identifier to uniquely identify the row in both table and timeline + classIdentifier:string; + // Additional classes to be added by any secondary render passes + additionalClasses:string[]; + // If this row is a work package, contains a reference to the rendered WP + workPackage:WorkPackageResourceInterface|null; + // If this is an additional row not present, this contains a reference to the WP + // it originated from belongsTo?:WorkPackageResourceInterface; + // The type of row this was rendered from + renderType:RenderedRowType; + // Marks if the row is currently hidden to the user hidden:boolean; + // Additional data by the render passes + data?:any; } -export interface TableRenderResult { - renderedOrder:RenderedRow[]; -} - -export interface SecondaryRenderPass { - render():void; -} +export type RenderedRow = { classIdentifier:string, workPackageId:string|null, hidden:boolean }; export abstract class PrimaryRenderPass { public states:States; public I18n:op.I18n; /** The rendered order of rows of work package IDs or , if not a work package row */ - public renderedOrder:RenderedRow[]; + public renderedOrder:RowRenderInfo[]; /** Resulting table body */ public tableBody:DocumentFragment; @@ -36,13 +43,19 @@ export abstract class PrimaryRenderPass { public timeline:TimelineRenderPass; /** Additional render pass that handles table relation rendering */ - public relations:SecondaryRenderPass; + public relations:RelationsRenderPass; - constructor(public workPackageTable:WorkPackageTable, public rowBuilder:SingleRowBuilder) { + constructor(public workPackageTable:WorkPackageTable, + public rowBuilder:SingleRowBuilder) { $injectFields(this, 'states', 'I18n'); } + /** + * Execute the entire render pass, executing this pass and all subsequent registered passes + * for timeline and relations. + * @return {PrimaryRenderPass} + */ public render():this { timeOutput('Primary render pass', () => { @@ -69,23 +82,35 @@ export abstract class PrimaryRenderPass { } /** - * Augment a new row added by a secondary render pass with whatever information is needed - * by the current render mode. - * - * e.g., add a class name to demark which group this element belongs to. - * - * @param row The HTMLElement to be inserted by the secondary render pass. - * @param belongsTo The RenderedRow the element will be inserted for. - * @return {HTMLElement} The augmented row element. + * Refresh a single row using the render pass it was originally created from. + * @param row */ - public augmentSecondaryElement(row:HTMLElement, belongsTo:RenderedRow):HTMLElement { - return row; + public refresh(row:RowRenderInfo, workPackage:WorkPackageResourceInterface, body:HTMLElement) { + let oldRow = jQuery(body).find(`.${row.classIdentifier}`); + let replacement:JQuery|null = null; + let editing = this.states.editing.get(row.workPackage!.id).value; + + switch(row.renderType) { + case 'primary': + replacement = this.rowBuilder.refreshRow(workPackage, editing, oldRow); + break; + case 'relations': + replacement = this.relations.refreshRelationRow(row as RelationRenderInfo, workPackage, editing, oldRow); + } + + if (replacement !== null && oldRow.length) { + oldRow.replaceWith(replacement); + } } - public get result():TableRenderResult { - return { - renderedOrder: this.renderedOrder - }; + public get result():RenderedRow[] { + return this.renderedOrder.map((row) => { + return { + classIdentifier: row.classIdentifier, + workPackageId: row.workPackage ? row.workPackage.id : null, + hidden: row.hidden + } as RenderedRow; + }); } /** @@ -94,7 +119,7 @@ export abstract class PrimaryRenderPass { * 1. Insert into the document fragment after the last match of the selector * 2. Splice into the renderedOrder array. */ - public spliceRow(row:HTMLElement, selector:string, renderedInfo:RenderedRow) { + public spliceRow(row:HTMLElement, selector:string, renderedInfo:RowRenderInfo) { // Insert into table using the selector // If it matches multiple, select the last element const target = jQuery(this.tableBody) @@ -130,13 +155,16 @@ export abstract class PrimaryRenderPass { */ protected appendRow(workPackage:WorkPackageResourceInterface, row:HTMLElement, + additionalClasses:string[] = [], hidden:boolean = false) { this.tableBody.appendChild(row); this.renderedOrder.push({ - isWorkPackage: true, - belongsTo: workPackage, + classIdentifier: this.rowBuilder.classIdentifier(workPackage), + additionalClasses: additionalClasses, + workPackage: workPackage, + renderType: 'primary', hidden: hidden }); } @@ -147,12 +175,18 @@ export abstract class PrimaryRenderPass { * @param classIdentifer a unique identifier for the two rows (one each in table/timeline). * @param hidden whether the row was rendered hidden */ - protected appendNonWorkPackageRow(row:HTMLElement, classIdentifer:string, hidden:boolean = false) { + protected appendNonWorkPackageRow(row:HTMLElement, + classIdentifer:string, + additionalClasses:string[] = [], + hidden:boolean = false) { row.classList.add(classIdentifer); this.tableBody.appendChild(row); this.renderedOrder.push({ - isWorkPackage: false, + classIdentifier: classIdentifer, + additionalClasses: additionalClasses, + workPackage: null, + renderType: 'primary', hidden: hidden }); } diff --git a/frontend/app/components/wp-fast-table/builders/relations/relation-row-builder.ts b/frontend/app/components/wp-fast-table/builders/relations/relation-row-builder.ts index f6065267b2..8cf4605270 100644 --- a/frontend/app/components/wp-fast-table/builders/relations/relation-row-builder.ts +++ b/frontend/app/components/wp-fast-table/builders/relations/relation-row-builder.ts @@ -3,96 +3,107 @@ import { WorkPackageResourceInterface } from '../../../api/api-v3/hal-resources/work-package-resource.service'; import {WorkPackageTable} from '../../wp-fast-table'; -import {commonRowClassName, rowClassName, SingleRowBuilder} from '../rows/single-row-builder'; +import {tableRowClassName, SingleRowBuilder, commonRowClassName} from '../rows/single-row-builder'; import { DenormalizedRelationData, RelationResource } from '../../../api/api-v3/hal-resources/relation-resource.service'; import {UiStateLinkBuilder} from '../ui-state-link-builder'; -import {QueryColumn} from '../../../wp-query/query-column'; +import {isRelationColumn, QueryColumn, queryColumnTypes} from '../../../wp-query/query-column'; import {$injectFields} from '../../../angular/angular-injector-bridge.functions'; import {RelationColumnType} from '../../state/wp-table-relation-columns.service'; import {States} from '../../../states.service'; +import {wpCellTdClassName} from '../cell-builder'; export function relationGroupClass(workPackageId:string) { return `__relations-expanded-from-${workPackageId}`; } -export const internalDetailsColumn = { - id: '__internal-detailsLink' -} as QueryColumn; +export function relationIdentifier(targetId:string, workPackageId:string) { + return `wp-relation-row-${workPackageId}-to-${targetId}`; +} + +export const relationCellClassName = 'wp-table--relation-cell-td'; export class RelationRowBuilder extends SingleRowBuilder { - public uiStateBuilder:UiStateLinkBuilder; public states:States; public I18n:op.I18n; constructor(protected workPackageTable:WorkPackageTable) { super(workPackageTable); - this.uiStateBuilder = new UiStateLinkBuilder(); $injectFields(this, 'I18n', 'states'); } /** - * Build the columns on the given empty row + * For additional relation rows, we don't want to render an expandable relation cell, + * but instead we render the relation label. + * @param workPackage + * @param column + * @return {any} */ - public buildEmptyRelationRow(from:WorkPackageResourceInterface, relation:RelationResource, type:RelationColumnType):[HTMLElement, boolean] { - const denormalized = relation.denormalized(from); - const tr = this.createEmptyRelationRow(from, denormalized); - const columns = this.wpTableColumns.getColumns(); - - // Set available information for ID and subject column - // and print hierarchy indicator at subject field. - columns.forEach((column:QueryColumn) => { - const td = document.createElement('td'); + public buildCell(workPackage:WorkPackageResourceInterface, column:QueryColumn):HTMLElement { - if (column.id === 'subject') { - this.buildRelationLabel(td, from, denormalized, type); - } + // handle relation types + if (isRelationColumn(column)) { + return this.emptyRelationCell(column); + } - if (column.id === 'id') { - const link = this.uiStateBuilder.linkToShow( - denormalized.target.id, - denormalized.target.name, - denormalized.target.id - ); + return super.buildCell(workPackage, column); + } - td.appendChild(link); - td.classList.add('relation-row--id-cell'); - } + /** + * Build the columns on the given empty row + */ + public buildEmptyRelationRow(from:WorkPackageResourceInterface, relation:RelationResource, type:RelationColumnType):[HTMLElement, WorkPackageResourceInterface] { + const denormalized = relation.denormalized(from); - tr.appendChild(td); - }); + const to = this.states.workPackages.get(denormalized.targetId).value! as WorkPackageResourceInterface; - // Append details icon - const td = document.createElement('td'); - tr.appendChild(td); + // Let the primary row builder build the row + const row = this.createEmptyRelationRow(from, to); + const [tr, _] = super.buildEmptyRow(to, row); - return [tr, false]; + return [tr, to]; } - /** * Create an empty unattached row element for the given work package * @param workPackage * @returns {any} */ - public createEmptyRelationRow(from:WorkPackageResource, relation:DenormalizedRelationData) { + public createEmptyRelationRow(from:WorkPackageResource, to:WorkPackageResource) { + const identifier = this.relationClassIdentifier(from, to); let tr = document.createElement('tr'); - tr.dataset['relatedWorkPackageId'] = from.id; + tr.dataset['workPackageId'] = to.id; + tr.dataset['classIdentifier'] = identifier; + tr.classList.add( - rowClassName, commonRowClassName, 'issue', '-no-highlighting', - `wp-table--relations-aditional-row`, relationGroupClass(from.id) + commonRowClassName, tableRowClassName, 'issue', + `wp-table--relations-aditional-row`, + identifier, + `${identifier}-table`, + relationGroupClass(from.id) ); return tr; } - private buildRelationLabel(cell:HTMLElement, from:WorkPackageResource, denormalized:DenormalizedRelationData, type:RelationColumnType) { + public relationClassIdentifier(from:WorkPackageResource, to:WorkPackageResource) { + return relationIdentifier(to.id, from.id); + } + + /** + * + * @param from + * @param denormalized + * @param type + */ + public appendRelationLabel(jRow:JQuery, from:WorkPackageResourceInterface, relation:RelationResource, columnId:string, type:RelationColumnType) { + const denormalized = relation.denormalized(from); let typeLabel; // Add the relation label if this is a "Relations for " column if (type === 'toType') { - typeLabel = this.I18n.t(`js.relation_labels.${denormalized.relationType}`); + typeLabel = this.I18n.t(`js.relation_labels.${denormalized.reverseRelationType}`); } // Add the WP type label if this is a " Relations" column if (type === 'ofType') { @@ -105,7 +116,15 @@ export class RelationRowBuilder extends SingleRowBuilder { relationLabel.textContent = typeLabel; const textNode = document.createTextNode(denormalized.target.name); - cell.appendChild(relationLabel); - cell.appendChild(textNode); + + jRow.find(`.${relationCellClassName}`).empty(); + jRow.find(`.${relationCellClassName}.${columnId}`).append(relationLabel); + } + + protected emptyRelationCell(column:QueryColumn) { + const cell = document.createElement('td'); + cell.classList.add(relationCellClassName, wpCellTdClassName, column.id); + + return cell; } } diff --git a/frontend/app/components/wp-fast-table/builders/relations/relations-render-pass.ts b/frontend/app/components/wp-fast-table/builders/relations/relations-render-pass.ts index 4e45585d93..9b445efba3 100644 --- a/frontend/app/components/wp-fast-table/builders/relations/relations-render-pass.ts +++ b/frontend/app/components/wp-fast-table/builders/relations/relations-render-pass.ts @@ -1,13 +1,26 @@ -import {PrimaryRenderPass, RenderedRow, SecondaryRenderPass} from '../primary-render-pass'; +import {PrimaryRenderPass, RowRenderInfo} from '../primary-render-pass'; import {WorkPackageTable} from '../../wp-fast-table'; -import {WorkPackageTableRelationColumnsService} from '../../state/wp-table-relation-columns.service'; +import { + RelationColumnType, + WorkPackageTableRelationColumnsService +} from '../../state/wp-table-relation-columns.service'; import {$injectFields} from '../../../angular/angular-injector-bridge.functions'; import {WorkPackageTableColumnsService} from '../../state/wp-table-columns.service'; import {relationGroupClass, RelationRowBuilder} from './relation-row-builder'; -import {rowId} from '../../helpers/wp-table-row-helpers'; import {WorkPackageRelationsService} from '../../../wp-relations/wp-relations.service'; +import {WorkPackageEditForm} from '../../../wp-edit-form/work-package-edit-form'; +import {WorkPackageResourceInterface} from '../../../api/api-v3/hal-resources/work-package-resource.service'; +import {RelationResource} from '../../../api/api-v3/hal-resources/relation-resource.service'; -export class RelationsRenderPass implements SecondaryRenderPass { +export interface RelationRenderInfo extends RowRenderInfo { + data:{ + relation:RelationResource; + columnId:string; + relationType:RelationColumnType; + }; +} + +export class RelationsRenderPass { public wpRelations:WorkPackageRelationsService; public wpTableColumns:WorkPackageTableColumnsService; public wpTableRelationColumns:WorkPackageTableRelationColumnsService; @@ -28,31 +41,39 @@ export class RelationsRenderPass implements SecondaryRenderPass { // Render for each original row, clone it since we're modifying the tablepass const rendered = _.clone(this.tablePass.renderedOrder); - rendered.forEach((row:RenderedRow, position:number) => { + rendered.forEach((row:RowRenderInfo, position:number) => { // We only care for rows that are natural work packages - if (!(row.isWorkPackage && row.belongsTo)) { + if (!row.workPackage) { return; } // If the work package has no relations, ignore - const fromId = row.belongsTo.id; + const workPackage = row.workPackage; + const fromId = workPackage.id; const state = this.wpRelations.getRelationsForWorkPackage(fromId); if (!state.hasValue() || _.size(state.value!) === 0) { return; } - this.wpTableRelationColumns.relationsToExtendFor(row.belongsTo, + this.wpTableRelationColumns.relationsToExtendFor(workPackage, state.value!, - (relation, type) => { + (relation, column, type) => { // Build each relation row (currently sorted by order defined in API) - const [relationRow,] = this.relationRowBuilder.buildEmptyRelationRow(row.belongsTo!, + const [relationRow, target] = this.relationRowBuilder.buildEmptyRelationRow( + workPackage, relation, - type); + type + ); // Augment any data for the belonging work package row to it - this.tablePass.augmentSecondaryElement(relationRow, row); + relationRow.classList.add(...row.additionalClasses); + this.relationRowBuilder.appendRelationLabel(jQuery(relationRow), + workPackage, + relation, + column.id, + type); // Insert next to the work package row // If no relations exist until here, directly under the row @@ -60,17 +81,39 @@ export class RelationsRenderPass implements SecondaryRenderPass { // Insert into table this.tablePass.spliceRow( relationRow, - `#${rowId(fromId)},.${relationGroupClass(fromId)}`, + `.${this.relationRowBuilder.classIdentifier(workPackage)},.${relationGroupClass(fromId)}`, { - isWorkPackage: false, - belongsTo: row.belongsTo, - hidden: row.hidden - } + classIdentifier: this.relationRowBuilder.relationClassIdentifier(workPackage, target), + additionalClasses: row.additionalClasses.concat(['wp-table--relations-aditional-row']), + workPackage: target, + belongsTo: workPackage, + renderType: 'relations', + hidden: row.hidden, + data: { + relation: relation, + columnId: column.id, + relationType: type + } + } as RelationRenderInfo ); }); }); } + public refreshRelationRow(renderedRow:RelationRenderInfo, + workPackage:WorkPackageResourceInterface, + editing:WorkPackageEditForm | undefined, + oldRow:JQuery) { + const newRow = this.relationRowBuilder.refreshRow(workPackage, editing, oldRow); + this.relationRowBuilder.appendRelationLabel(newRow, + renderedRow.belongsTo!, + renderedRow.data.relation, + renderedRow.data.columnId, + renderedRow.data.relationType); + + return newRow; + } + private get isApplicable() { return this.wpTableColumns.hasRelationColumns(); } diff --git a/frontend/app/components/wp-fast-table/builders/rows/row-refresh-builder.ts b/frontend/app/components/wp-fast-table/builders/rows/row-refresh-builder.ts deleted file mode 100644 index 138532a408..0000000000 --- a/frontend/app/components/wp-fast-table/builders/rows/row-refresh-builder.ts +++ /dev/null @@ -1,53 +0,0 @@ -import {WorkPackageEditForm} from '../../../wp-edit-form/work-package-edit-form'; -import {locateRow} from '../../helpers/wp-table-row-helpers'; -import {WorkPackageTableRow} from '../../wp-table.interfaces'; -import {wpCellTdClassName} from '../cell-builder'; -import {SingleRowBuilder} from './single-row-builder'; -import {debugLog} from '../../../../helpers/debug_output'; -import {QueryColumn} from '../../../wp-query/query-column'; - -export class RowRefreshBuilder extends SingleRowBuilder { - - /** - * Refresh a row that is currently being edited, that is, some edit fields may be open - */ - public refreshRow(row: WorkPackageTableRow, editForm: WorkPackageEditForm | undefined):[HTMLElement, boolean] | null { - // Get the row for the WP if refreshing existing - const rowElement = row.element || locateRow(row.workPackageId); - - if (!rowElement) { - debugLog(`Trying to refresh row for ${row.workPackageId} that is not in the table`); - return null; - } - - // Iterate all columns, reattaching or rendering new columns - const jRow = jQuery(rowElement); - - // Detach all current edit cells - const cells = jRow.find(`.${wpCellTdClassName}`).detach(); - - // Remember the order of all new edit cells - const newCells:HTMLElement[] = []; - - this.columns.forEach((column:QueryColumn) => { - const oldTd = cells.filter(`td.${column.id}`); - - // Skip the replacement of the column if this is being edited. - if (this.isColumnBeingEdited(editForm, column)) { - newCells.push(oldTd[0]); - return; - } - - // Otherwise, refresh that cell and append it - const cell = this.buildCell(row.object, column); - newCells.push(cell); - }); - - jRow.prepend(newCells); - return [rowElement!, false]; - } - - private isColumnBeingEdited(editForm: WorkPackageEditForm | undefined, column: QueryColumn) { - return editForm && editForm.activeFields[column.id]; - } -} diff --git a/frontend/app/components/wp-fast-table/builders/rows/single-row-builder.ts b/frontend/app/components/wp-fast-table/builders/rows/single-row-builder.ts index 8e972e6c31..d832282a39 100644 --- a/frontend/app/components/wp-fast-table/builders/rows/single-row-builder.ts +++ b/frontend/app/components/wp-fast-table/builders/rows/single-row-builder.ts @@ -1,5 +1,5 @@ import {WorkPackageTableSelection} from '../../state/wp-table-selection.service'; -import {CellBuilder} from '../cell-builder'; +import {CellBuilder, wpCellTdClassName} from '../cell-builder'; import {DetailsLinkBuilder} from '../details-link-builder'; import {$injectFields} from '../../../angular/angular-injector-bridge.functions'; import { @@ -8,14 +8,14 @@ import { } from '../../../api/api-v3/hal-resources/work-package-resource.service'; import {WorkPackageTableColumnsService} from '../../state/wp-table-columns.service'; import {checkedClassName} from '../ui-state-link-builder'; -import {rowId} from '../../helpers/wp-table-row-helpers'; import {WorkPackageTable} from '../../wp-fast-table'; -import {QueryColumn, queryColumnTypes} from '../../../wp-query/query-column'; +import {isRelationColumn, QueryColumn} from '../../../wp-query/query-column'; import {RelationCellbuilder} from '../relation-cell-builder'; +import {WorkPackageEditForm} from '../../../wp-edit-form/work-package-edit-form'; // Work package table row entries -export const rowClassName = 'wp-table--row'; -// Class name for both table and timeline rows +export const tableRowClassName = 'wp-table--row'; +// Work package and timeline rows export const commonRowClassName = 'wp--row'; export const internalDetailsColumn = { @@ -58,8 +58,7 @@ export class SingleRowBuilder { public buildCell(workPackage:WorkPackageResourceInterface, column:QueryColumn):HTMLElement { // handle relation types - const relationTypes = [queryColumnTypes.RELATION_TO_TYPE, queryColumnTypes.RELATION_OF_TYPE]; - if (relationTypes.indexOf(column._type) >= 0) { + if (isRelationColumn(column)) { return this.relationCellBuilder.build(workPackage, column); } @@ -75,8 +74,69 @@ export class SingleRowBuilder { /** * Build the columns on the given empty row */ - public buildEmpty(workPackage:WorkPackageResourceInterface):[HTMLElement,boolean] { + public buildEmpty(workPackage:WorkPackageResourceInterface):[HTMLElement, boolean] { let row = this.createEmptyRow(workPackage); + return this.buildEmptyRow(workPackage, row); + } + + /** + * Create an empty unattached row element for the given work package + * @param workPackage + * @returns {any} + */ + public createEmptyRow(workPackage:WorkPackageResource) { + const identifier = this.classIdentifier(workPackage); + let tr = document.createElement('tr'); + tr.dataset['workPackageId'] = workPackage.id; + tr.dataset['classIdentifier'] = identifier; + tr.classList.add( + tableRowClassName, + commonRowClassName, + identifier, + `${identifier}-table`, + 'issue' + ); + + return tr; + } + + public classIdentifier(workPackage:WorkPackageResource) { + return `wp-row-${workPackage.id}`; + } + + /** + * Refresh a row that is currently being edited, that is, some edit fields may be open + */ + public refreshRow(workPackage:WorkPackageResourceInterface, editForm:WorkPackageEditForm|undefined, jRow:JQuery):JQuery { + // Detach all current edit cells + const cells = jRow.find(`.${wpCellTdClassName}`).detach(); + + // Remember the order of all new edit cells + const newCells:HTMLElement[] = []; + + this.columns.forEach((column:QueryColumn) => { + const oldTd = cells.filter(`td.${column.id}`); + + // Skip the replacement of the column if this is being edited. + if (this.isColumnBeingEdited(editForm, column)) { + newCells.push(oldTd[0]); + return; + } + + // Otherwise, refresh that cell and append it + const cell = this.buildCell(workPackage, column); + newCells.push(cell); + }); + + jRow.prepend(newCells); + return jRow; + } + + protected isColumnBeingEdited(editForm:WorkPackageEditForm | undefined, column:QueryColumn) { + return editForm && editForm.activeFields[column.id]; + } + + protected buildEmptyRow(workPackage:WorkPackageResourceInterface, row:HTMLElement):[HTMLElement, boolean] { let cell = null; this.augmentedColumns.forEach((column:QueryColumn) => { @@ -91,18 +151,4 @@ export class SingleRowBuilder { return [row, false]; } - - /** - * Create an empty unattached row element for the given work package - * @param workPackage - * @returns {any} - */ - public createEmptyRow(workPackage:WorkPackageResource) { - let tr = document.createElement('tr'); - tr.id = rowId(workPackage.id); - tr.dataset['workPackageId'] = workPackage.id; - tr.classList.add(rowClassName, commonRowClassName, `${commonRowClassName}-${workPackage.id}`, 'issue'); - - return tr; - } } diff --git a/frontend/app/components/wp-fast-table/builders/timeline/timeline-render-pass.ts b/frontend/app/components/wp-fast-table/builders/timeline/timeline-render-pass.ts index f22e406dac..3706a66d86 100644 --- a/frontend/app/components/wp-fast-table/builders/timeline/timeline-render-pass.ts +++ b/frontend/app/components/wp-fast-table/builders/timeline/timeline-render-pass.ts @@ -1,8 +1,8 @@ -import {PrimaryRenderPass, RenderedRow, SecondaryRenderPass} from '../primary-render-pass'; +import {PrimaryRenderPass, RowRenderInfo} from '../primary-render-pass'; import {TimelineRowBuilder} from './timeline-row-builder'; import {WorkPackageTable} from '../../wp-fast-table'; -export class TimelineRenderPass implements SecondaryRenderPass { +export class TimelineRenderPass { /** Row builders */ protected timelineBuilder:TimelineRowBuilder; @@ -18,12 +18,11 @@ export class TimelineRenderPass implements SecondaryRenderPass { this.timelineBuilder = new TimelineRowBuilder(this.table); // Render into timeline fragment - this.tablePass.renderedOrder.forEach((row:RenderedRow) => { - const wpId = row.isWorkPackage ? row.belongsTo!.id : null; + this.tablePass.renderedOrder.forEach((row:RowRenderInfo) => { + const wpId = row.workPackage ? row.workPackage.id : null; const secondary = this.timelineBuilder.build(wpId); - this.tablePass.augmentSecondaryElement(secondary, row); - + secondary.classList.add(row.classIdentifier, `${row.classIdentifier}-timeline`, ...row.additionalClasses); this.timelineBody.appendChild(secondary); }); } diff --git a/frontend/app/components/wp-fast-table/builders/timeline/timeline-row-builder.ts b/frontend/app/components/wp-fast-table/builders/timeline/timeline-row-builder.ts index 97b416f226..3a19f5ed0d 100644 --- a/frontend/app/components/wp-fast-table/builders/timeline/timeline-row-builder.ts +++ b/frontend/app/components/wp-fast-table/builders/timeline/timeline-row-builder.ts @@ -1,6 +1,5 @@ import {WorkPackageTable} from '../../wp-fast-table'; import {$injectFields} from '../../../angular/angular-injector-bridge.functions'; -import {WorkPackageResourceInterface} from '../../../api/api-v3/hal-resources/work-package-resource.service'; import {States} from '../../../states.service'; import {WorkPackageTableTimelineService} from '../../state/wp-table-timeline.service'; import {WorkPackageCacheService} from '../../../work-packages/work-package-cache.service'; @@ -8,10 +7,6 @@ import {commonRowClassName} from '../rows/single-row-builder'; export const timelineCellClassName = 'wp-timeline-cell'; -export function timelineRowId(id:string) { - return `wp-timeline-row-${id}`; -} - export class TimelineRowBuilder { public states:States; public wpTableTimeline:WorkPackageTableTimelineService; @@ -26,9 +21,7 @@ export class TimelineRowBuilder { cell.classList.add(timelineCellClassName, commonRowClassName); if (workPackageId) { - cell.id = timelineRowId(workPackageId); cell.dataset['workPackageId'] = workPackageId; - cell.classList.add(`${commonRowClassName}-${workPackageId}`); } return cell; diff --git a/frontend/app/components/wp-fast-table/handlers/cell/edit-cell-handler.ts b/frontend/app/components/wp-fast-table/handlers/cell/edit-cell-handler.ts index 53db52a00c..3a922d9f80 100644 --- a/frontend/app/components/wp-fast-table/handlers/cell/edit-cell-handler.ts +++ b/frontend/app/components/wp-fast-table/handlers/cell/edit-cell-handler.ts @@ -1,14 +1,14 @@ -import {InputState} from "reactivestates"; -import {debugLog} from "../../../../helpers/debug_output"; -import {injectorBridge} from "../../../angular/angular-injector-bridge.functions"; -import {States} from "../../../states.service"; -import {TableRowEditContext} from "../../../wp-edit-form/table-row-edit-context"; -import {WorkPackageEditForm} from "../../../wp-edit-form/work-package-edit-form"; -import {cellClassName, editableClassName, readOnlyClassName} from "../../builders/cell-builder"; -import {rowClassName} from "../../builders/rows/single-row-builder"; -import {WorkPackageTable} from "../../wp-fast-table"; -import {ClickOrEnterHandler} from "../click-or-enter-handler"; -import {TableEventHandler} from "../table-handler-registry"; +import {InputState} from 'reactivestates'; +import {debugLog} from '../../../../helpers/debug_output'; +import {injectorBridge} from '../../../angular/angular-injector-bridge.functions'; +import {States} from '../../../states.service'; +import {TableRowEditContext} from '../../../wp-edit-form/table-row-edit-context'; +import {WorkPackageEditForm} from '../../../wp-edit-form/work-package-edit-form'; +import {cellClassName, editableClassName, readOnlyClassName} from '../../builders/cell-builder'; +import {tableRowClassName} from '../../builders/rows/single-row-builder'; +import {WorkPackageTable} from '../../wp-fast-table'; +import {ClickOrEnterHandler} from '../click-or-enter-handler'; +import {TableEventHandler} from '../table-handler-registry'; export class EditCellHandler extends ClickOrEnterHandler implements TableEventHandler { // Injections @@ -46,8 +46,11 @@ export class EditCellHandler extends ClickOrEnterHandler implements TableEventHa } // Locate the row - const rowElement = target.closest(`.${rowClassName}`); + const rowElement = target.closest(`.${tableRowClassName}`); + // Get the work package we're editing const workPackageId = rowElement.data('workPackageId'); + // Get the row context + const classIdentifier = rowElement.data('classIdentifier'); // Get any existing edit state for this work package let state = this.editState(workPackageId); @@ -57,7 +60,7 @@ export class EditCellHandler extends ClickOrEnterHandler implements TableEventHa const positionOffset = this.getClickPosition(evt); // Set editing context to table - form.editContext = new TableRowEditContext(workPackageId); + form.editContext = new TableRowEditContext(workPackageId, classIdentifier); // Activate the field form.activate(fieldName) diff --git a/frontend/app/components/wp-fast-table/handlers/cell/relations-cell-handler.ts b/frontend/app/components/wp-fast-table/handlers/cell/relations-cell-handler.ts index 83b24261b6..2ac05e7a8a 100644 --- a/frontend/app/components/wp-fast-table/handlers/cell/relations-cell-handler.ts +++ b/frontend/app/components/wp-fast-table/handlers/cell/relations-cell-handler.ts @@ -5,7 +5,7 @@ import {States} from '../../../states.service'; import {TableRowEditContext} from '../../../wp-edit-form/table-row-edit-context'; import {WorkPackageEditForm} from '../../../wp-edit-form/work-package-edit-form'; import {cellClassName, readOnlyClassName} from '../../builders/cell-builder'; -import {rowClassName} from '../../builders/rows/single-row-builder'; +import {tableRowClassName} from '../../builders/rows/single-row-builder'; import {WorkPackageTable} from '../../wp-fast-table'; import {ClickOrEnterHandler} from '../click-or-enter-handler'; import {TableEventHandler} from '../table-handler-registry'; @@ -37,7 +37,7 @@ export class RelationsCellHandler extends ClickOrEnterHandler implements TableEv } protected processEvent(table:WorkPackageTable, evt:JQueryEventObject):boolean { - debugLog('Handled click on relation cell: ', evt.target); + debugLog('Handled click on relation cell %o', evt.target); evt.preventDefault(); // Locate the relation td @@ -45,7 +45,7 @@ export class RelationsCellHandler extends ClickOrEnterHandler implements TableEv const columnId = td.data('columnId'); // Locate the row - const rowElement = jQuery(evt.target).closest(`.${rowClassName}`); + const rowElement = jQuery(evt.target).closest(`.${tableRowClassName}`); const workPackageId = rowElement.data('workPackageId'); // Get any existing edit state for this work package diff --git a/frontend/app/components/wp-fast-table/handlers/row/click-handler.ts b/frontend/app/components/wp-fast-table/handlers/row/click-handler.ts index d83f7a1fe6..6396ba78c8 100644 --- a/frontend/app/components/wp-fast-table/handlers/row/click-handler.ts +++ b/frontend/app/components/wp-fast-table/handlers/row/click-handler.ts @@ -4,7 +4,7 @@ import {WorkPackageTable} from '../../wp-fast-table'; import {States} from '../../../states.service'; import {TableEventHandler} from '../table-handler-registry'; import {WorkPackageTableSelection} from '../../state/wp-table-selection.service'; -import {rowClassName} from '../../builders/rows/single-row-builder'; +import {tableRowClassName} from '../../builders/rows/single-row-builder'; import {tdClassName} from '../../builders/cell-builder'; export class RowClickHandler implements TableEventHandler { @@ -21,7 +21,7 @@ export class RowClickHandler implements TableEventHandler { } public get SELECTOR() { - return `.${rowClassName}`; + return `.${tableRowClassName}`; } public eventScope(table:WorkPackageTable) { @@ -35,7 +35,7 @@ export class RowClickHandler implements TableEventHandler { // We don't want to handle these. if (target.parents(`.${tdClassName}`).length) { debugLog('Skipping click on inner cell'); - return; + return true; } // Locate the row from event @@ -43,19 +43,19 @@ export class RowClickHandler implements TableEventHandler { let wpId = element.data('workPackageId'); if (!wpId) { - return; + return true; } // Ignore links if (target.is('a') || target.parent().is('a')) { - return; + return true; } // The current row is the last selected work package // not matter what other rows are (de-)selected below. // Thus save that row for the details view button. let row = table.rowObject(wpId); - this.states.focusedWorkPackage.putValue(row.workPackageId); + this.states.focusedWorkPackage.putValue(wpId); // Update single selection if no modifier present if (!(evt.ctrlKey || evt.metaKey || evt.shiftKey)) { @@ -71,6 +71,8 @@ export class RowClickHandler implements TableEventHandler { if (evt.ctrlKey || evt.metaKey) { this.wpTableSelection.toggleRow(row.workPackageId); } + + return false; } } diff --git a/frontend/app/components/wp-fast-table/handlers/row/context-menu-handler.ts b/frontend/app/components/wp-fast-table/handlers/row/context-menu-handler.ts index 97d03c7c88..0b2cd0ebaa 100644 --- a/frontend/app/components/wp-fast-table/handlers/row/context-menu-handler.ts +++ b/frontend/app/components/wp-fast-table/handlers/row/context-menu-handler.ts @@ -2,7 +2,7 @@ import {debugLog} from "../../../../helpers/debug_output"; import {injectorBridge} from "../../../angular/angular-injector-bridge.functions"; import {WorkPackageTable} from "../../wp-fast-table"; import {TableEventHandler} from "../table-handler-registry"; -import {rowClassName} from "../../builders/rows/single-row-builder"; +import {tableRowClassName} from "../../builders/rows/single-row-builder"; import {uiStateLinkClass} from "../../builders/ui-state-link-builder"; import {ContextMenuService} from "../../../context-menus/context-menu.service"; import {timelineCellClassName} from "../../builders/timeline/timeline-row-builder"; @@ -20,7 +20,7 @@ export class ContextMenuHandler implements TableEventHandler { } public get SELECTOR() { - return `.${rowClassName},.${timelineCellClassName}`; + return `.${tableRowClassName},.${timelineCellClassName}`; } public eventScope(table:WorkPackageTable) { diff --git a/frontend/app/components/wp-fast-table/handlers/row/context-menu-keyboard-handler.ts b/frontend/app/components/wp-fast-table/handlers/row/context-menu-keyboard-handler.ts index db66b5cc2b..976163a5ef 100644 --- a/frontend/app/components/wp-fast-table/handlers/row/context-menu-keyboard-handler.ts +++ b/frontend/app/components/wp-fast-table/handlers/row/context-menu-keyboard-handler.ts @@ -1,7 +1,7 @@ import {injectorBridge} from "../../../angular/angular-injector-bridge.functions"; import {WorkPackageTable} from "../../wp-fast-table"; import {TableEventHandler} from "../table-handler-registry"; -import {rowClassName} from "../../builders/rows/single-row-builder"; +import {tableRowClassName} from "../../builders/rows/single-row-builder"; import {ContextMenuService} from "../../../context-menus/context-menu.service"; import {keyCodes} from "../../../common/keyCodes.enum"; @@ -18,7 +18,7 @@ export class ContextMenuKeyboardHandler implements TableEventHandler { } public get SELECTOR() { - return `.${rowClassName}`; + return `.${tableRowClassName}`; } public eventScope(table:WorkPackageTable) { diff --git a/frontend/app/components/wp-fast-table/handlers/row/double-click-handler.ts b/frontend/app/components/wp-fast-table/handlers/row/double-click-handler.ts index 827d092a0d..2b35fcbdae 100644 --- a/frontend/app/components/wp-fast-table/handlers/row/double-click-handler.ts +++ b/frontend/app/components/wp-fast-table/handlers/row/double-click-handler.ts @@ -4,7 +4,7 @@ import {WorkPackageTable} from '../../wp-fast-table'; import {States} from '../../../states.service'; import {TableEventHandler} from '../table-handler-registry'; import {WorkPackageTableSelection} from '../../state/wp-table-selection.service'; -import {rowClassName} from '../../builders/rows/single-row-builder'; +import {tableRowClassName} from '../../builders/rows/single-row-builder'; import {tdClassName} from '../../builders/cell-builder'; export class RowDoubleClickHandler implements TableEventHandler { @@ -22,7 +22,7 @@ export class RowDoubleClickHandler implements TableEventHandler { } public get SELECTOR() { - return `.${rowClassName}`; + return `.${tableRowClassName}`; } public eventScope(table:WorkPackageTable) { @@ -36,7 +36,7 @@ export class RowDoubleClickHandler implements TableEventHandler { // We don't want to handle these. if (target.parents(`.${tdClassName}`).length) { debugLog('Skipping click on inner cell'); - return; + return true; } // Locate the row from event @@ -45,7 +45,7 @@ export class RowDoubleClickHandler implements TableEventHandler { // Ignore links if (target.is('a') || target.parent().is('a')) { - return; + return true; } // Save the currently focused work package @@ -55,6 +55,8 @@ export class RowDoubleClickHandler implements TableEventHandler { 'work-packages.show', { workPackageId: row.workPackageId } ); + + return false; } } diff --git a/frontend/app/components/wp-fast-table/handlers/row/hierarchy-click-handler.ts b/frontend/app/components/wp-fast-table/handlers/row/hierarchy-click-handler.ts index 639ada89e2..7c85038781 100644 --- a/frontend/app/components/wp-fast-table/handlers/row/hierarchy-click-handler.ts +++ b/frontend/app/components/wp-fast-table/handlers/row/hierarchy-click-handler.ts @@ -4,7 +4,7 @@ import {injectorBridge} from '../../../angular/angular-injector-bridge.functions import {WorkPackageTable} from '../../wp-fast-table'; import {States} from '../../../states.service'; import {TableEventHandler} from '../table-handler-registry'; -import {rowClassName} from '../../builders/rows/single-row-builder'; +import {tableRowClassName} from '../../builders/rows/single-row-builder'; export class HierarchyClickHandler extends ClickOrEnterHandler implements TableEventHandler { // Injections @@ -21,7 +21,7 @@ export class HierarchyClickHandler extends ClickOrEnterHandler implements TableE } public get SELECTOR() { - return `.${rowClassName} .wp-table--hierarchy-indicator `; + return `.${tableRowClassName} .wp-table--hierarchy-indicator `; } public eventScope(table:WorkPackageTable) { @@ -32,7 +32,7 @@ export class HierarchyClickHandler extends ClickOrEnterHandler implements TableE let target = jQuery(evt.target); // Locate the row from event - let element = target.closest(`.${rowClassName}`); + let element = target.closest(`.${tableRowClassName}`); let wpId = element.data('workPackageId'); this.wpTableHierarchies.toggle(wpId); diff --git a/frontend/app/components/wp-fast-table/handlers/state/hierarchy-transformer.ts b/frontend/app/components/wp-fast-table/handlers/state/hierarchy-transformer.ts index 7141350372..527d2ec874 100644 --- a/frontend/app/components/wp-fast-table/handlers/state/hierarchy-transformer.ts +++ b/frontend/app/components/wp-fast-table/handlers/state/hierarchy-transformer.ts @@ -5,7 +5,7 @@ import {WorkPackageTable} from "../../wp-fast-table"; import {WorkPackageTableHierarchiesService} from './../../state/wp-table-hierarchy.service'; import {WorkPackageTableHierarchies} from "../../wp-table-hierarchies"; import {indicatorCollapsedClass} from "../../builders/modes/hierarchy/single-hierarchy-row-builder"; -import {rowClassName} from '../../builders/rows/single-row-builder'; +import {tableRowClassName} from '../../builders/rows/single-row-builder'; import {debugLog} from '../../../../helpers/debug_output'; export class HierarchyTransformer { @@ -61,12 +61,12 @@ export class HierarchyTransformer { affected.toggleClass(collapsedGroupClass(wpId), isCollapsed); // Update the hidden section of the rendered state - affected.filter(`.${rowClassName}`).each((i, el) => { + affected.filter(`.${tableRowClassName}`).each((i, el) => { // Get the index of this row const index = jQuery(el).index(); // Update the hidden state - rendered.renderedOrder[index].hidden = isCollapsed; + rendered[index].hidden = isCollapsed; }); }); diff --git a/frontend/app/components/wp-fast-table/handlers/state/relations-transformer.ts b/frontend/app/components/wp-fast-table/handlers/state/relations-transformer.ts index 9e7f4222a6..7079f10943 100644 --- a/frontend/app/components/wp-fast-table/handlers/state/relations-transformer.ts +++ b/frontend/app/components/wp-fast-table/handlers/state/relations-transformer.ts @@ -5,7 +5,7 @@ import {WorkPackageTable} from "../../wp-fast-table"; import {WorkPackageTableHierarchiesService} from './../../state/wp-table-hierarchy.service'; import {WorkPackageTableHierarchies} from "../../wp-table-hierarchies"; import {indicatorCollapsedClass} from "../../builders/modes/hierarchy/single-hierarchy-row-builder"; -import {rowClassName} from '../../builders/rows/single-row-builder'; +import {tableRowClassName} from '../../builders/rows/single-row-builder'; import {debugLog} from '../../../../helpers/debug_output'; import {WorkPackageTableRelationColumnsService} from '../../state/wp-table-relation-columns.service'; import {WorkPackageTableRelationColumns} from '../../wp-table-relation-columns'; diff --git a/frontend/app/components/wp-fast-table/handlers/state/rows-transformer.ts b/frontend/app/components/wp-fast-table/handlers/state/rows-transformer.ts index 34aad2ed26..d1a3fc38e5 100644 --- a/frontend/app/components/wp-fast-table/handlers/state/rows-transformer.ts +++ b/frontend/app/components/wp-fast-table/handlers/state/rows-transformer.ts @@ -28,28 +28,15 @@ export class RowsTransformer { // Refresh a single row if it exists this.states.workPackages.observeChange() .takeUntil(this.states.table.stopAllSubscriptions.asObservable()) - .subscribe(([changedId, wp, state]) => { + .filter(() => this.states.query.context.current === 'Query loaded') + .subscribe(([changedId, wp]) => { if (wp === undefined) { return; } - // let [changedId, wp] = nextVal; - let row: WorkPackageTableRow = table.rowIndex[changedId]; - - if (wp && row) { - row.object = wp as any; - this.refreshWorkPackage(table, row); - } + this.table.refreshRows(wp as WorkPackageResourceInterface); }); } - - /** - * Refreshes a single entity from changes in the work package itself. - * Will skip rendering when dirty or fresh. Does not check for table changes. - */ - private refreshWorkPackage(table: WorkPackageTable, row: WorkPackageTableRow) { - table.refreshRow(row); - } } RowsTransformer.$inject = ['states']; diff --git a/frontend/app/components/wp-fast-table/handlers/state/selection-transformer.ts b/frontend/app/components/wp-fast-table/handlers/state/selection-transformer.ts index d8044f2362..0dab2624cd 100644 --- a/frontend/app/components/wp-fast-table/handlers/state/selection-transformer.ts +++ b/frontend/app/components/wp-fast-table/handlers/state/selection-transformer.ts @@ -1,6 +1,6 @@ import {injectorBridge} from "../../../angular/angular-injector-bridge.functions"; import {States} from "../../../states.service"; -import {rowClassName} from "../../builders/rows/single-row-builder"; +import {tableRowClassName} from "../../builders/rows/single-row-builder"; import {checkedClassName} from "../../builders/ui-state-link-builder"; import {rowId} from "../../helpers/wp-table-row-helpers"; import {WorkPackageTableSelection} from "../../state/wp-table-selection.service"; @@ -40,10 +40,10 @@ export class SelectionTransformer { * Update all currently visible rows to match the selection state. */ private renderSelectionState(state:WPTableRowSelectionState) { - jQuery(`.${rowClassName}.${checkedClassName}`).removeClass(checkedClassName); + jQuery(`.${tableRowClassName}.${checkedClassName}`).removeClass(checkedClassName); _.each(state.selected, (selected: boolean, workPackageId:any) => { - jQuery(`#${rowId(workPackageId)}`).toggleClass(checkedClassName, selected); + jQuery(`.${tableRowClassName}[data-work-package-id="${workPackageId}"]`).toggleClass(checkedClassName, selected); }); } } diff --git a/frontend/app/components/wp-fast-table/helpers/wp-table-hierarchy-helpers.ts b/frontend/app/components/wp-fast-table/helpers/wp-table-hierarchy-helpers.ts index 4f94e40722..e5a1d5f5b1 100644 --- a/frontend/app/components/wp-fast-table/helpers/wp-table-hierarchy-helpers.ts +++ b/frontend/app/components/wp-fast-table/helpers/wp-table-hierarchy-helpers.ts @@ -15,6 +15,10 @@ export function hierarchyRootClass(ancestorId:string):string { return `__hierarchy-root-${ancestorId}`; } +export function ancestorClassIdentifier(ancestorId:string) { + return `wp-ancestor-row-${ancestorId}`; +} + /** * Returns whether any of the children of this work package * are visible in the table results. diff --git a/frontend/app/components/wp-fast-table/helpers/wp-table-row-helpers.ts b/frontend/app/components/wp-fast-table/helpers/wp-table-row-helpers.ts index 9c5135fe06..8f0246d58b 100644 --- a/frontend/app/components/wp-fast-table/helpers/wp-table-row-helpers.ts +++ b/frontend/app/components/wp-fast-table/helpers/wp-table-row-helpers.ts @@ -1,13 +1,9 @@ -import {commonRowClassName} from '../builders/rows/single-row-builder'; + /** * Return the row html id attribute for the given work package ID. */ export function rowId(workPackageId:string):string { - return `wp-row-${workPackageId}`; -} - -export function rowClass(workPackageId:string):string { - return `${commonRowClassName}-${workPackageId}`; + return `wp-table-row-${workPackageId}`; } /** diff --git a/frontend/app/components/wp-fast-table/state/wp-table-relation-columns.service.ts b/frontend/app/components/wp-fast-table/state/wp-table-relation-columns.service.ts index 723e044b9d..33b07791f9 100644 --- a/frontend/app/components/wp-fast-table/state/wp-table-relation-columns.service.ts +++ b/frontend/app/components/wp-fast-table/state/wp-table-relation-columns.service.ts @@ -78,7 +78,7 @@ export class WorkPackageTableRelationColumnsService extends WorkPackageTableBase */ public relationsToExtendFor(workPackage:WorkPackageResourceInterface, relations:RelationsStateValue|undefined, - eachCallback:(relation:RelationResource, type:RelationColumnType) => void) { + eachCallback:(relation:RelationResource, column:QueryColumn, type:RelationColumnType) => void) { // Only if any relation columns or stored expansion state exist if (!this.wpTableColumns.hasRelationColumns() || this.state.isPristine()) { return; @@ -99,7 +99,8 @@ export class WorkPackageTableRelationColumnsService extends WorkPackageTableBase const type = this.relationColumnType(column); if (type !== null) { - _.each(relations, (relation) => eachCallback(relation as RelationResource, type)); + _.each(this.relationsForColumn(workPackage, relations, column), + (relation) => eachCallback(relation as RelationResource, column, type)); } } diff --git a/frontend/app/components/wp-fast-table/wp-fast-table.ts b/frontend/app/components/wp-fast-table/wp-fast-table.ts index 07403eeb23..b77f09acfa 100644 --- a/frontend/app/components/wp-fast-table/wp-fast-table.ts +++ b/frontend/app/components/wp-fast-table/wp-fast-table.ts @@ -1,5 +1,8 @@ import {WorkPackageCacheService} from '../work-packages/work-package-cache.service'; -import {WorkPackageResource} from '../api/api-v3/hal-resources/work-package-resource.service'; +import { + WorkPackageResource, + WorkPackageResourceInterface +} from '../api/api-v3/hal-resources/work-package-resource.service'; import {States} from '../states.service'; import {injectorBridge} from '../angular/angular-injector-bridge.functions'; @@ -7,13 +10,13 @@ import {injectorBridge} from '../angular/angular-injector-bridge.functions'; import {WorkPackageTableRow} from './wp-table.interfaces'; import {TableHandlerRegistry} from './handlers/table-handler-registry'; import {locateRow} from './helpers/wp-table-row-helpers'; -import {PlainRowsBuilder} from "./builders/modes/plain/plain-rows-builder"; -import {GroupedRowsBuilder} from "./builders/modes/grouped/grouped-rows-builder"; -import {HierarchyRowsBuilder} from "./builders/modes/hierarchy/hierarchy-rows-builder"; -import {RowsBuilder} from "./builders/modes/rows-builder"; -import {WorkPackageTimelineTableController} from "../wp-table/timeline/container/wp-timeline-container.directive"; +import {PlainRowsBuilder} from './builders/modes/plain/plain-rows-builder'; +import {GroupedRowsBuilder} from './builders/modes/grouped/grouped-rows-builder'; +import {HierarchyRowsBuilder} from './builders/modes/hierarchy/hierarchy-rows-builder'; +import {RowsBuilder} from './builders/modes/rows-builder'; +import {WorkPackageTimelineTableController} from '../wp-table/timeline/container/wp-timeline-container.directive'; import {PrimaryRenderPass} from './builders/primary-render-pass'; -import {Subject} from 'rxjs'; +import {debugLog} from '../../helpers/debug_output'; export class WorkPackageTable { public wpCacheService:WorkPackageCacheService; @@ -31,6 +34,9 @@ export class WorkPackageTable { new PlainRowsBuilder(this) ]; + // Last render pass used for refreshing single rows + private lastRenderPass:PrimaryRenderPass|null = null; + constructor(public container:HTMLElement, public tbody:HTMLElement, public timelineBody:HTMLElement, @@ -81,7 +87,7 @@ export class WorkPackageTable { * all elements. */ public redrawTableAndTimeline() { - const renderPass = this.rowBuilder.buildRows(); + const renderPass = this.lastRenderPass = this.rowBuilder.buildRows(); // Insert table body this.tbody.innerHTML = ''; @@ -98,7 +104,7 @@ export class WorkPackageTable { * Redraw all elements in the table section only */ public redrawTable() { - const renderPass = this.rowBuilder.buildRows(); + const renderPass = this.lastRenderPass = this.rowBuilder.buildRows(); this.tbody.innerHTML = ''; this.tbody.appendChild(renderPass.tableBody); @@ -107,19 +113,22 @@ export class WorkPackageTable { } /** - * Redraw a single row after structural changes + * Redraw single rows for a given work package being updated. */ - public refreshRow(row:WorkPackageTableRow) { - // Find the row we want to replace - let oldRow = row.element || locateRow(row.workPackageId); - let result = this.rowBuilder.refreshRow(row); - - if (result !== null && oldRow && oldRow.parentNode) { - let [newRow, _hidden] = result; - oldRow.parentNode.replaceChild(newRow, oldRow); - row.element = newRow; - this.rowIndex[row.workPackageId] = row; + public refreshRows(workPackage:WorkPackageResourceInterface) { + const pass = this.lastRenderPass; + if (!pass) { + debugLog('Trying to refresh a singular row without a previus render pass.'); + return; } + + _.each(pass.renderedOrder, (row) => { + if (row.workPackage && row.workPackage.id === workPackage.id) { + debugLog(`Refreshing rendered row ${row.classIdentifier}`); + row.workPackage = workPackage; + pass.refresh(row, workPackage, this.tbody); + } + }); } } diff --git a/frontend/app/components/wp-inline-create/inline-create-row-builder.ts b/frontend/app/components/wp-inline-create/inline-create-row-builder.ts index bc4256173b..b9405d37dd 100644 --- a/frontend/app/components/wp-inline-create/inline-create-row-builder.ts +++ b/frontend/app/components/wp-inline-create/inline-create-row-builder.ts @@ -11,17 +11,17 @@ import {WorkPackageTableSelection} from '../wp-fast-table/state/wp-table-selecti import {WorkPackageTableColumnsService} from '../wp-fast-table/state/wp-table-columns.service'; import { internalDetailsColumn, - rowClassName + tableRowClassName, + SingleRowBuilder, commonRowClassName } from '../wp-fast-table/builders/rows/single-row-builder'; import {WorkPackageTable} from '../wp-fast-table/wp-fast-table'; -import {RowRefreshBuilder} from '../wp-fast-table/builders/rows/row-refresh-builder'; -import IScope = angular.IScope; import {QueryColumn} from '../wp-query/query-column'; +import IScope = angular.IScope; export const inlineCreateRowClassName = 'wp-inline-create-row'; export const inlineCreateCancelClassName = 'wp-table--cancel-create-link'; -export class InlineCreateRowBuilder extends RowRefreshBuilder { +export class InlineCreateRowBuilder extends SingleRowBuilder { // Injections public states:States; public wpTableSelection:WorkPackageTableSelection; @@ -53,7 +53,7 @@ export class InlineCreateRowBuilder extends RowRefreshBuilder { const [row, hidden] = this.buildEmpty(workPackage); // Set editing context to table - form.editContext = new TableRowEditContext(workPackage.id); + form.editContext = new TableRowEditContext(workPackage.id, this.classIdentifier(workPackage)); this.states.editing.get(workPackage.id).putValue(form); return [row, hidden]; @@ -65,10 +65,16 @@ export class InlineCreateRowBuilder extends RowRefreshBuilder { * @returns {any} */ public createEmptyRow(workPackage:WorkPackageResource) { + const identifier = this.classIdentifier(workPackage); const tr = document.createElement('tr'); tr.id = rowId(workPackage.id); tr.dataset['workPackageId'] = workPackage.id; - tr.classList.add(inlineCreateRowClassName, rowClassName, 'wp--row', 'issue'); + tr.dataset['classIdentifier'] = identifier; + tr.classList.add( + inlineCreateRowClassName, commonRowClassName, tableRowClassName, 'issue', + identifier, + `${identifier}-table` + ); return tr; } diff --git a/frontend/app/components/wp-inline-create/wp-inline-create.directive.ts b/frontend/app/components/wp-inline-create/wp-inline-create.directive.ts index a42e084a44..b46eb216d8 100644 --- a/frontend/app/components/wp-inline-create/wp-inline-create.directive.ts +++ b/frontend/app/components/wp-inline-create/wp-inline-create.directive.ts @@ -111,14 +111,8 @@ export class WorkPackageInlineCreateController { .takeUntil(scopeDestroyed$($scope)).subscribe(() => { const rowElement = this.$element.find(`.${inlineCreateRowClassName}`); - if (rowElement.length) { - const data = { - element: rowElement[0], - object: this.currentWorkPackage, - workPackageId: 'new', - position: 0 - }; - this.rowBuilder.refreshRow(data as WorkPackageTableRow, this.workPackageEditForm); + if (rowElement.length && this.currentWorkPackage) { + this.rowBuilder.refreshRow(this.currentWorkPackage, this.workPackageEditForm, rowElement); } }); @@ -204,8 +198,8 @@ export class WorkPackageInlineCreateController { this.currentWorkPackage = null; this.states.editing.get('new').clear(); this.states.workPackages.get('new').clear(); - this.$element.find('#wp-row-new').remove(); - jQuery(this.table.timelineBody).find('#wp-timeline-row-new').remove(); + this.$element.find('.wp-row-new').remove(); + jQuery(this.table.timelineBody).find('.wp-row-new-timeline').remove(); } public showRow() { diff --git a/frontend/app/components/wp-list/wp-list.service.ts b/frontend/app/components/wp-list/wp-list.service.ts index 8f3b2962d9..22433746e7 100644 --- a/frontend/app/components/wp-list/wp-list.service.ts +++ b/frontend/app/components/wp-list/wp-list.service.ts @@ -245,7 +245,7 @@ export class WorkPackagesListService { let currentForm = this.states.query.form.value; if (!currentForm || query.$links.update.$href !== currentForm.$href) { - this.loadForm(query); + setTimeout(() => this.loadForm(query), 0); } return query; diff --git a/frontend/app/components/wp-query/query-column.ts b/frontend/app/components/wp-query/query-column.ts index 903ce79a3d..3410f66bfb 100644 --- a/frontend/app/components/wp-query/query-column.ts +++ b/frontend/app/components/wp-query/query-column.ts @@ -6,6 +6,10 @@ export const queryColumnTypes = { RELATION_TO_TYPE: 'QueryColumn::RelationToType', }; +export function isRelationColumn(column:QueryColumn) { + const relationTypes = [queryColumnTypes.RELATION_TO_TYPE, queryColumnTypes.RELATION_OF_TYPE]; + return relationTypes.indexOf(column._type) >= 0; +} /** * A reference to a query column object as returned from the API. @@ -15,7 +19,7 @@ export interface QueryColumn extends HalResource { name:string; _links?: { self:{ href:string, title:string }; - } + }; } export interface TypeRelationQueryColumn extends QueryColumn { diff --git a/frontend/app/components/wp-table/timeline/cells/wp-timeline-cell.ts b/frontend/app/components/wp-table/timeline/cells/wp-timeline-cell.ts index 45f943f1b7..6422214599 100644 --- a/frontend/app/components/wp-table/timeline/cells/wp-timeline-cell.ts +++ b/frontend/app/components/wp-table/timeline/cells/wp-timeline-cell.ts @@ -34,54 +34,52 @@ import {TimelineMilestoneCellRenderer} from "../cells/timeline-milestone-cell-re import {TimelineCellRenderer} from "../cells/timeline-cell-renderer"; import {WorkPackageResourceInterface} from "../../../api/api-v3/hal-resources/work-package-resource.service"; import * as moment from "moment"; -import { injectorBridge } from "../../../angular/angular-injector-bridge.functions"; +import {injectorBridge} from "../../../angular/angular-injector-bridge.functions"; import IScope = angular.IScope; import Moment = moment.Moment; import {WorkPackageTableRefreshService} from "../../wp-table-refresh-request.service"; import {LoadingIndicatorService} from '../../../common/loading-indicator/loading-indicator.service'; -import {timelineRowId} from "../../../wp-fast-table/builders/timeline/timeline-row-builder"; - export class WorkPackageTimelineCell { - public wpCacheService: WorkPackageCacheService; - public wpTableRefresh: WorkPackageTableRefreshService; - public states: States; - public loadingIndicator: LoadingIndicatorService; + public wpCacheService:WorkPackageCacheService; + public wpTableRefresh:WorkPackageTableRefreshService; + public states:States; + public loadingIndicator:LoadingIndicatorService; - private wpElement: HTMLDivElement|null = null; + private wpElement:HTMLDivElement | null = null; - private elementShape: string; + private elementShape:string; private timelineCell:JQuery; - constructor(public workPackageTimeline: WorkPackageTimelineTableController, - public renderers:{ milestone: TimelineMilestoneCellRenderer, generic: TimelineCellRenderer }, - public latestRenderInfo: RenderInfo, - public workPackageId: string) { + constructor(public workPackageTimeline:WorkPackageTimelineTableController, + public renderers:{ milestone:TimelineMilestoneCellRenderer, generic:TimelineCellRenderer }, + public latestRenderInfo:RenderInfo, + public classIdentifier:string, + public workPackageId:string) { injectorBridge(this); } - getMarginLeftOfLeftSide(): number { + getMarginLeftOfLeftSide():number { const renderer = this.cellRenderer(this.latestRenderInfo.workPackage); return renderer.getMarginLeftOfLeftSide(this.latestRenderInfo); } - getMarginLeftOfRightSide(): number { + getMarginLeftOfRightSide():number { const renderer = this.cellRenderer(this.latestRenderInfo.workPackage); return renderer.getMarginLeftOfRightSide(this.latestRenderInfo); } - getPaddingLeftForIncomingRelationLines(): number { + getPaddingLeftForIncomingRelationLines():number { const renderer = this.cellRenderer(this.latestRenderInfo.workPackage); return renderer.getPaddingLeftForIncomingRelationLines(this.latestRenderInfo); } - getPaddingRightForOutgoingRelationLines(): number { + getPaddingRightForOutgoingRelationLines():number { const renderer = this.cellRenderer(this.latestRenderInfo.workPackage); return renderer.getPaddingRightForOutgoingRelationLines(this.latestRenderInfo); } - - canConnectRelations(): boolean { + canConnectRelations():boolean { const wp = this.latestRenderInfo.workPackage; if (wp.isMilestone) { return !_.isNil(wp.date); @@ -100,10 +98,10 @@ export class WorkPackageTimelineCell { } private get cellElement() { - return this.cellContainer.find(`#${timelineRowId(this.workPackageId)}`); + return this.cellContainer.find(`.${this.classIdentifier}`); } - private lazyInit(renderer: TimelineCellRenderer, renderInfo: RenderInfo):JQuery { + private lazyInit(renderer:TimelineCellRenderer, renderInfo:RenderInfo):JQuery { const body = this.workPackageTimeline.timelineBody[0]; const cell = this.cellElement; @@ -115,9 +113,7 @@ export class WorkPackageTimelineCell { } // Remove the element first if we're redrawing - if (wasRendered) { - this.clear(); - } + this.clear(); // Render the given element this.wpElement = renderer.render(renderInfo); @@ -145,7 +141,7 @@ export class WorkPackageTimelineCell { return cell; } - private cellRenderer(workPackage: WorkPackageResourceInterface): TimelineCellRenderer { + private cellRenderer(workPackage:WorkPackageResourceInterface):TimelineCellRenderer { if (workPackage.isMilestone) { return this.renderers.milestone; } @@ -153,7 +149,7 @@ export class WorkPackageTimelineCell { return this.renderers.generic; } - public refreshView(renderInfo: RenderInfo) { + public refreshView(renderInfo:RenderInfo) { this.latestRenderInfo = renderInfo; const renderer = this.cellRenderer(renderInfo.workPackage); @@ -161,7 +157,9 @@ export class WorkPackageTimelineCell { const cell = this.lazyInit(renderer, renderInfo); // Render the upgrade from renderInfo - const shouldBeDisplayed = renderer.update(cell[0], this.wpElement as HTMLDivElement, renderInfo); + const shouldBeDisplayed = renderer.update(cell[0], + this.wpElement as HTMLDivElement, + renderInfo); if (!shouldBeDisplayed) { this.clear(); } diff --git a/frontend/app/components/wp-table/timeline/cells/wp-timeline-cells-renderer.ts b/frontend/app/components/wp-table/timeline/cells/wp-timeline-cells-renderer.ts index 937bc6fdd4..f0b4dc949f 100644 --- a/frontend/app/components/wp-table/timeline/cells/wp-timeline-cells-renderer.ts +++ b/frontend/app/components/wp-table/timeline/cells/wp-timeline-cells-renderer.ts @@ -25,20 +25,20 @@ // // See doc/COPYRIGHT.rdoc for more details. // ++ -import {States} from "../../../states.service"; -import {RenderInfo} from "../wp-timeline"; -import {TimelineMilestoneCellRenderer} from "./timeline-milestone-cell-renderer"; -import {TimelineCellRenderer} from "./timeline-cell-renderer"; -import {WorkPackageTimelineTableController} from "../container/wp-timeline-container.directive"; -import {$injectFields} from "../../../angular/angular-injector-bridge.functions"; -import {WorkPackageTimelineCell} from "./wp-timeline-cell"; -import {RenderedRow} from "../../../wp-fast-table/builders/primary-render-pass"; +import {States} from '../../../states.service'; +import {RenderInfo} from '../wp-timeline'; +import {TimelineMilestoneCellRenderer} from './timeline-milestone-cell-renderer'; +import {TimelineCellRenderer} from './timeline-cell-renderer'; +import {WorkPackageTimelineTableController} from '../container/wp-timeline-container.directive'; +import {$injectFields} from '../../../angular/angular-injector-bridge.functions'; +import {WorkPackageTimelineCell} from './wp-timeline-cell'; +import {RenderedRow} from '../../../wp-fast-table/builders/primary-render-pass'; export class WorkPackageTimelineCellsRenderer { // Injections public states:States; - public cells:{ [id:string]:WorkPackageTimelineCell } = {}; + public cells:{ [classIdentifier:string]:WorkPackageTimelineCell } = {}; private cellRenderers:{ milestone:TimelineMilestoneCellRenderer, generic:TimelineCellRenderer }; @@ -52,7 +52,11 @@ export class WorkPackageTimelineCellsRenderer { } public hasCell(wpId:string) { - return !!this.cells[wpId]; + return this.getCellsFor(wpId).length > 0; + } + + public getCellsFor(wpId:string):WorkPackageTimelineCell[] { + return _.filter(this.cells, (cell) => cell.workPackageId === wpId) || []; } /** @@ -66,10 +70,8 @@ export class WorkPackageTimelineCellsRenderer { _.each(this.cells, (cell) => this.refreshSingleCell(cell)); } - public refreshCellFor(wpId:string) { - if (this.hasCell(wpId)) { - this.refreshSingleCell(this.cells[wpId]); - } + public refreshCellsFor(wpId:string) { + _.each(this.getCellsFor(wpId), (cell) => this.refreshSingleCell(cell)); } public refreshSingleCell(cell:WorkPackageTimelineCell) { @@ -91,37 +93,43 @@ export class WorkPackageTimelineCellsRenderer { const newCells:string[] = []; _.each(this.wpTimeline.workPackageIdOrder, (renderedRow:RenderedRow) => { + const wpId = renderedRow.workPackageId; // Ignore extra rows not tied to a work package - if (!(renderedRow.isWorkPackage && renderedRow.belongsTo)) { + if (!wpId) { return; } - const wpId = renderedRow.belongsTo.id.toString(); - if (!wpId) { + const state = this.states.workPackages.get(wpId); + if (state.isPristine()) { return; } + // As work packages may occur several times, get the unique identifier + // to identify the cell + const identifier = renderedRow.classIdentifier; + // Create a cell unless we already have an active cell - if (!this.hasCell(wpId)) { - this.cells[wpId] = this.buildCell(wpId); + if (!this.cells[identifier]) { + this.cells[identifier] = this.buildCell(identifier, wpId.toString()); } - newCells.push(wpId); + newCells.push(identifier); }); - _.difference(currentlyActive, newCells).forEach((wpId:string) => { - this.cells[wpId].clear(); - delete this.cells[wpId]; + _.difference(currentlyActive, newCells).forEach((identifier:string) => { + this.cells[identifier].clear(); + delete this.cells[identifier]; }); } - private buildCell(wpId:string) { + private buildCell(classIdentifier:string, workPackageId:string) { return new WorkPackageTimelineCell( this.wpTimeline, this.cellRenderers, - this.renderInfoFor(wpId), - wpId + this.renderInfoFor(workPackageId), + classIdentifier, + workPackageId ); } diff --git a/frontend/app/components/wp-table/timeline/container/wp-timeline-container.directive.ts b/frontend/app/components/wp-table/timeline/container/wp-timeline-container.directive.ts index e37110c068..b04786ca80 100644 --- a/frontend/app/components/wp-table/timeline/container/wp-timeline-container.directive.ts +++ b/frontend/app/components/wp-table/timeline/container/wp-timeline-container.directive.ts @@ -116,7 +116,6 @@ export class WorkPackageTimelineTableController { this.states.table.rendered.values$() .takeUntil(this.states.table.stopAllSubscriptions) .filter(() => this.initialized) - .map(rendered => rendered.renderedOrder) .subscribe((orderedRows) => { this.workPackageIdOrder = orderedRows; this.refreshView(); @@ -147,8 +146,17 @@ export class WorkPackageTimelineTableController { return this.cellsRenderer.hasCell(wpId); } - workPackageCell(wpId:string):WorkPackageTimelineCell { - return this.cellsRenderer.cells[wpId]; + workPackageCells(wpId:string):WorkPackageTimelineCell[] { + return this.cellsRenderer.getCellsFor(wpId); + } + + /** + * Return the index of a given row by its class identifier + * @param cell + * @return {number} + */ + workPackageIndex(classIdentifier:string):number { + return this.workPackageIdOrder.findIndex((el) => el.classIdentifier === classIdentifier); } onRefreshRequested(name:string, callback:(vp:TimelineViewParameters) => void) { @@ -215,7 +223,7 @@ export class WorkPackageTimelineTableController { this.debouncedRefresh(); } else { // Refresh the single cell - this.cellsRenderer.refreshCellFor(wpId); + this.cellsRenderer.refreshCellsFor(wpId); } }); } @@ -291,18 +299,15 @@ export class WorkPackageTimelineTableController { // Calculate view parameters this.workPackageIdOrder.forEach((renderedRow) => { + const wpId = renderedRow.workPackageId; // Not all rendered rows are work packages - if (!renderedRow.isWorkPackage) { + if (!wpId || this.states.workPackages.get(wpId).isPristine()) { return; } // We may still have a reference to a row that, e.g., just got deleted - const workPackage = renderedRow.belongsTo; - if (!workPackage) { - return; - } - + const workPackage = this.states.workPackages.get(wpId).value!; const startDate = workPackage.startDate ? moment(workPackage.startDate) : currentParams.now; const dueDate = workPackage.dueDate ? moment(workPackage.dueDate) : currentParams.now; const date = workPackage.date ? moment(workPackage.date) : currentParams.now; diff --git a/frontend/app/components/wp-table/timeline/global-elements/wp-timeline-relations.directive.ts b/frontend/app/components/wp-table/timeline/global-elements/wp-timeline-relations.directive.ts index 578c834909..ec47e93861 100644 --- a/frontend/app/components/wp-table/timeline/global-elements/wp-timeline-relations.directive.ts +++ b/frontend/app/components/wp-table/timeline/global-elements/wp-timeline-relations.directive.ts @@ -31,11 +31,11 @@ import {Observable} from 'rxjs/Observable'; import {openprojectModule} from '../../../../angular-modules'; import {scopeDestroyed$} from '../../../../helpers/angular-rx-utils'; import {States} from '../../../states.service'; -import {RenderedRow} from '../../../wp-fast-table/builders/primary-render-pass'; import {RelationsStateValue, WorkPackageRelationsService} from '../../../wp-relations/wp-relations.service'; import {WorkPackageTimelineTableController} from '../container/wp-timeline-container.directive'; import {timelineElementCssClass, TimelineViewParameters} from '../wp-timeline'; import {TimelineRelationElement, workPackagePrefix} from './timeline-relation-element'; +import {WorkPackageTimelineCell} from '../cells/wp-timeline-cell'; const DEBUG_DRAW_RELATION_LINES_WITH_COLOR = false; @@ -114,7 +114,7 @@ export class WorkPackageTableTimelineRelations { .map(([rendered, visible]) => rendered) .subscribe(list => { // ... make sure that the corresponding relations are loaded ... - const wps = _.compact(list.map(row => row.isWorkPackage && row.belongsTo!.id) as string[]); + const wps = _.compact(list.map(row => row.workPackageId) as string[]); this.wpRelations.requireInvolved(wps); wps.forEach(wpId => { @@ -185,21 +185,43 @@ export class WorkPackageTableTimelineRelations { } + /** + * Render a single relation to all shown work packages. Since work packages may occur multiple + * times in the timeline, iterate all potential combinations and render them. + * @param vp + * @param e + */ private renderElement(vp:TimelineViewParameters, e:TimelineRelationElement) { const involved = e.relation.ids; - // Get the rendered rows - const visibleRows = this.workPackageIdOrder.filter(e => !e.hidden); - const idxFrom = _.findIndex(visibleRows, - (el:RenderedRow) => el.isWorkPackage && el.belongsTo!.id.toString() === involved.from); - const idxTo = _.findIndex(visibleRows, - (el:RenderedRow) => el.isWorkPackage && el.belongsTo!.id.toString() === involved.to); + const startCells = this.wpTimeline.workPackageCells(involved.from); + const endCells = this.wpTimeline.workPackageCells(involved.to); - const startCell = this.wpTimeline.workPackageCell(involved.from); - const endCell = this.wpTimeline.workPackageCell(involved.to); + // If either sources or targets are not rendered, ignore this relation + if (startCells.length === 0 || endCells.length === 0) { + return; + } - // If targets do not exist anywhere in the table, skip - if (idxFrom === -1 || idxTo === -1 || _.isNil(startCell) || _.isNil(endCell)) { + // Now, render all sources to all targets + startCells.forEach((startCell) => { + const idxFrom = this.wpTimeline.workPackageIndex(startCell.classIdentifier); + endCells.forEach((endCell) => { + const idxTo = this.wpTimeline.workPackageIndex(endCell.classIdentifier); + this.renderRelation(vp, e, idxFrom, idxTo, startCell, endCell); + }); + }); + } + + private renderRelation( + vp:TimelineViewParameters, + e:TimelineRelationElement, + idxFrom:number, + idxTo:number, + startCell:WorkPackageTimelineCell, + endCell:WorkPackageTimelineCell) { + + // If any of the targets are hidden in the table, skip + if (this.workPackageIdOrder[idxFrom].hidden || this.workPackageIdOrder[idxTo].hidden) { return; } @@ -263,8 +285,8 @@ export class WorkPackageTableTimelineRelations { this.container.append(newSegment(vp, e.classNames, idxTo, 19, targetX + 1, 1, 11, 'blue')); } } - } + } } openprojectModule.component('wpTimelineRelations', { diff --git a/frontend/app/vendors.js b/frontend/app/vendors.js index 5dc700810f..4c4d6f44fa 100644 --- a/frontend/app/vendors.js +++ b/frontend/app/vendors.js @@ -33,6 +33,8 @@ // NOTE: currently needed for PhantomJS to support Webpack's style-loader. // See: https://github.com/webpack/style-loader/issues/31 require('phantomjs-polyfill'); +// ES6 Promise polyfill +require('expose-loader?Promise!es6-promise'); // jQuery require('expose-loader?jQuery!jquery'); diff --git a/frontend/npm-shrinkwrap.json b/frontend/npm-shrinkwrap.json index fa89dc9383..1f64dbea86 100644 --- a/frontend/npm-shrinkwrap.json +++ b/frontend/npm-shrinkwrap.json @@ -3202,6 +3202,11 @@ "version": "0.1.2", "from": "yeast@0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz" + }, + "zone.js": { + "version": "0.8.12", + "from": "zone.js@>=0.8.0 <0.9.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.12.tgz" } } } diff --git a/frontend/package.json b/frontend/package.json index b8c593c80a..9f70afe6f9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,6 +6,7 @@ "body-parser": "^1.2.0", "chai": "^3.5.0", "chai-as-promised": "^5.3.0", + "es6-promise": "^4.1.0", "exec": "0.0.6", "jquery-mockjax": "~2.2.1", "json2htmlcov": "~0.1.1", diff --git a/spec/features/accessibility/work_packages/work_package_query_spec.rb b/spec/features/accessibility/work_packages/work_package_query_spec.rb index 9f4ab2cf3c..0d0cedcf1c 100644 --- a/spec/features/accessibility/work_packages/work_package_query_spec.rb +++ b/spec/features/accessibility/work_packages/work_package_query_spec.rb @@ -176,15 +176,15 @@ describe 'Work package index accessibility', type: :feature, selenium: true do context 'focus' do let(:first_link_selector) do - "#wp-row-#{work_package.id} td.id a" + ".wp-row-#{work_package.id} td.id a" end let(:second_link_selector) do - "#wp-row-#{another_work_package.id} td.id a" + ".wp-row-#{another_work_package.id} td.id a" end it 'navigates with J and K' do - expect(page).to have_selector("#wp-row-#{work_package.id}") - expect(page).to have_selector("#wp-row-#{another_work_package.id}") + expect(page).to have_selector(".wp-row-#{work_package.id}") + expect(page).to have_selector(".wp-row-#{another_work_package.id}") find('body').native.send_keys('j') expect(page).to have_focus_on(first_link_selector) diff --git a/spec/features/support/work_package_table.rb b/spec/features/support/work_package_table.rb index d3da7747df..f72a25a71a 100644 --- a/spec/features/support/work_package_table.rb +++ b/spec/features/support/work_package_table.rb @@ -78,8 +78,8 @@ shared_context 'work package table helpers' do preceeding_elements.each_with_index do |wp_1, i| wp_2 = following_elements[i] - expect(self).to have_selector("#wp-row-#{wp_1.id} + \ - #wp-row-#{wp_2.id}") + expect(self).to have_selector(".wp-row-#{wp_1.id} + \ + .wp-row-#{wp_2.id}") end end end diff --git a/spec/features/work_packages/navigation_spec.rb b/spec/features/work_packages/navigation_spec.rb index a2dd295907..8b2e76829a 100644 --- a/spec/features/work_packages/navigation_spec.rb +++ b/spec/features/work_packages/navigation_spec.rb @@ -64,7 +64,7 @@ RSpec.feature 'Work package navigation', js: true, selenium: true do split_work_package.visit! split_work_package.expect_subject # Should be checked in table - expect(page).to have_selector("#wp-row-#{work_package.id}.-checked") + expect(page).to have_selector(".wp-row-#{work_package.id}.-checked") # deep link work package show diff --git a/spec/features/work_packages/table/hierarchy_spec.rb b/spec/features/work_packages/table/hierarchy_spec.rb index e51dfccbdc..fd1e6fb4d7 100644 --- a/spec/features/work_packages/table/hierarchy_spec.rb +++ b/spec/features/work_packages/table/hierarchy_spec.rb @@ -83,8 +83,8 @@ describe 'Work Package table hierarchy', js: true do # Disabling hierarchy hides them again hierarchy.disable_hierarchy - expect(page).to have_no_selector("#wp-row-#{wp_root.id}") - expect(page).to have_no_selector("#wp-row-#{wp_inter.id}") + expect(page).to have_no_selector(".wp-row-#{wp_root.id}") + expect(page).to have_no_selector(".wp-row-#{wp_inter.id}") end end diff --git a/spec/features/work_packages/table/relations_spec.rb b/spec/features/work_packages/table/relations_spec.rb index 03542288e7..b2154cb93d 100644 --- a/spec/features/work_packages/table/relations_spec.rb +++ b/spec/features/work_packages/table/relations_spec.rb @@ -70,7 +70,7 @@ describe 'Work Package table relations', js: true do wp_from_row.find(".#{type_column_id} .wp-table--relation-indicator").click expect(page).to have_selector(".__relations-expanded-from-#{wp_from.id}", count: 2) related_row = page.first(".__relations-expanded-from-#{wp_from.id}") - expect(related_row).to have_selector('td', text: "Follows#{wp_to.subject}") + expect(related_row).to have_selector('td.wp-table--relation-cell-td', text: "Precedes") # Collapse wp_from_row.find(".#{type_column_id} .wp-table--relation-indicator").click @@ -80,7 +80,7 @@ describe 'Work Package table relations', js: true do wp_from_row.find(".#{type_column_follows} .wp-table--relation-indicator").click expect(page).to have_selector(".__relations-expanded-from-#{wp_from.id}", count: 2) related_row = page.first(".__relations-expanded-from-#{wp_from.id}") - expect(related_row).to have_selector('td', text: "#{wp_to.type}#{wp_to.subject}") + expect(related_row).to have_selector('.wp-table--relation-cell-td', text: wp_to.type) # Open Timeline # Should be initially closed diff --git a/spec/support/components/work_packages/context_menu.rb b/spec/support/components/work_packages/context_menu.rb index b9d4ecc13a..1c64d9fe7c 100644 --- a/spec/support/components/work_packages/context_menu.rb +++ b/spec/support/components/work_packages/context_menu.rb @@ -33,7 +33,7 @@ module Components include RSpec::Matchers def open_for(work_package) - find("#wp-row-#{work_package.id}").right_click + find(".wp-row-#{work_package.id}-table").right_click expect_open end diff --git a/spec/support/components/work_packages/hierarchies.rb b/spec/support/components/work_packages/hierarchies.rb index 4fdac3d818..6b365c4750 100644 --- a/spec/support/components/work_packages/hierarchies.rb +++ b/spec/support/components/work_packages/hierarchies.rb @@ -49,7 +49,7 @@ module Components def expect_leaf_at(*work_packages) work_packages.each do |wp| - expect(page).to have_selector("#wp-row-#{wp.id} .wp-table--leaf-indicator") + expect(page).to have_selector(".wp-row-#{wp.id} .wp-table--leaf-indicator") end end @@ -57,7 +57,7 @@ module Components collapsed_sel = ".-hierarchy-collapsed" work_packages.each do |wp| - selector = "#wp-row-#{wp.id} .wp-table--hierarchy-indicator" + selector = ".wp-row-#{wp.id} .wp-table--hierarchy-indicator" if collapsed expect(page).to have_selector("#{selector}#{collapsed_sel}") @@ -70,12 +70,12 @@ module Components def expect_hidden(*work_packages) work_packages.each do |wp| - expect(page).to have_selector("#wp-row-#{wp.id}", visible: :hidden) + expect(page).to have_selector(".wp-row-#{wp.id}", visible: :hidden) end end def toggle_row(work_package) - find("#wp-row-#{work_package.id} .wp-table--hierarchy-indicator").click + find(".wp-row-#{work_package.id} .wp-table--hierarchy-indicator").click end end end diff --git a/spec/support/pages/work_packages_table.rb b/spec/support/pages/work_packages_table.rb index c28db94fe6..df88d4dbb5 100644 --- a/spec/support/pages/work_packages_table.rb +++ b/spec/support/pages/work_packages_table.rb @@ -47,7 +47,7 @@ module Pages def expect_work_package_listed(*work_packages) within(table_container) do work_packages.each do |wp| - expect(page).to have_selector("#wp-row-#{wp.id} td.subject", + expect(page).to have_selector(".wp-row-#{wp.id} td.subject", text: wp.subject) end end @@ -56,7 +56,7 @@ module Pages def expect_work_package_not_listed(*work_packages) within(table_container) do work_packages.each do |wp| - expect(page).to have_no_selector("#wp-row-#{wp.id} td.subject", + expect(page).to have_no_selector(".wp-row-#{wp.id} td.subject", text: wp.subject) end end @@ -147,7 +147,7 @@ module Pages end def row(work_package) - table_container.find("#wp-row-#{work_package.id}") + table_container.find(".wp-row-#{work_package.id}") end def edit_field(work_package, attribute) @@ -200,7 +200,7 @@ module Pages end def work_package_row_selector(work_package) - "#wp-row-#{work_package.id}" + ".wp-row-#{work_package.id}" end private diff --git a/spec/support/pages/work_packages_timeline.rb b/spec/support/pages/work_packages_timeline.rb index df9feef87d..8bc75d17f9 100644 --- a/spec/support/pages/work_packages_timeline.rb +++ b/spec/support/pages/work_packages_timeline.rb @@ -95,7 +95,7 @@ module Pages def expect_timeline_element(work_package) type = work_package.milestone? ? :milestone : :bar - expect(page).to have_selector("#{timeline_row_selector(work_package.id)} .timeline-element.#{type}") + expect(page).to have_selector(".wp-row-#{work_package.id}-timeline .timeline-element.#{type}") end def expect_timeline_relation(from, to) @@ -117,7 +117,7 @@ module Pages end def expect_hidden_row(work_package) - expect(page).to have_selector("#wp-timeline-row-#{work_package.id}", visible: :hidden) + expect(page).to have_selector(".wp-row-#{work_package.id}-timeline", visible: :hidden) end end end