Merge pull request #5676 from opf/feature/25544/render-contents-of-additional-rows

[25544][25596] Render contents of additional relation rows
pull/5700/head
ulferts 7 years ago committed by GitHub
commit 8ead87f46d
  1. 2
      app/assets/stylesheets/content/work_packages/_table_relations.sass
  2. 4
      app/assets/stylesheets/content/work_packages/timelines/_timelines.sass
  3. 19
      frontend/app/components/api/api-v3/hal-request/hal-request.service.test.ts
  4. 39
      frontend/app/components/api/api-v3/hal-request/hal-request.service.ts
  5. 9
      frontend/app/components/api/api-work-packages/api-work-packages.service.ts
  6. 64
      frontend/app/components/states.service.ts
  7. 4
      frontend/app/components/states/switch-state.ts
  8. 8
      frontend/app/components/work-packages/work-package-cache.service.ts
  9. 4
      frontend/app/components/wp-edit-form/table-row-edit-context.ts
  10. 27
      frontend/app/components/wp-fast-table/builders/modes/grouped/grouped-render-pass.ts
  11. 6
      frontend/app/components/wp-fast-table/builders/modes/grouped/grouped-rows-builder.ts
  12. 26
      frontend/app/components/wp-fast-table/builders/modes/hierarchy/hierarchy-render-pass.ts
  13. 1
      frontend/app/components/wp-fast-table/builders/modes/hierarchy/hierarchy-rows-builder.ts
  14. 43
      frontend/app/components/wp-fast-table/builders/modes/hierarchy/single-hierarchy-row-builder.ts
  15. 15
      frontend/app/components/wp-fast-table/builders/modes/rows-builder.ts
  16. 98
      frontend/app/components/wp-fast-table/builders/primary-render-pass.ts
  17. 105
      frontend/app/components/wp-fast-table/builders/relations/relation-row-builder.ts
  18. 75
      frontend/app/components/wp-fast-table/builders/relations/relations-render-pass.ts
  19. 53
      frontend/app/components/wp-fast-table/builders/rows/row-refresh-builder.ts
  20. 88
      frontend/app/components/wp-fast-table/builders/rows/single-row-builder.ts
  21. 11
      frontend/app/components/wp-fast-table/builders/timeline/timeline-render-pass.ts
  22. 7
      frontend/app/components/wp-fast-table/builders/timeline/timeline-row-builder.ts
  23. 29
      frontend/app/components/wp-fast-table/handlers/cell/edit-cell-handler.ts
  24. 6
      frontend/app/components/wp-fast-table/handlers/cell/relations-cell-handler.ts
  25. 14
      frontend/app/components/wp-fast-table/handlers/row/click-handler.ts
  26. 4
      frontend/app/components/wp-fast-table/handlers/row/context-menu-handler.ts
  27. 4
      frontend/app/components/wp-fast-table/handlers/row/context-menu-keyboard-handler.ts
  28. 10
      frontend/app/components/wp-fast-table/handlers/row/double-click-handler.ts
  29. 6
      frontend/app/components/wp-fast-table/handlers/row/hierarchy-click-handler.ts
  30. 6
      frontend/app/components/wp-fast-table/handlers/state/hierarchy-transformer.ts
  31. 2
      frontend/app/components/wp-fast-table/handlers/state/relations-transformer.ts
  32. 19
      frontend/app/components/wp-fast-table/handlers/state/rows-transformer.ts
  33. 6
      frontend/app/components/wp-fast-table/handlers/state/selection-transformer.ts
  34. 4
      frontend/app/components/wp-fast-table/helpers/wp-table-hierarchy-helpers.ts
  35. 8
      frontend/app/components/wp-fast-table/helpers/wp-table-row-helpers.ts
  36. 5
      frontend/app/components/wp-fast-table/state/wp-table-relation-columns.service.ts
  37. 49
      frontend/app/components/wp-fast-table/wp-fast-table.ts
  38. 18
      frontend/app/components/wp-inline-create/inline-create-row-builder.ts
  39. 14
      frontend/app/components/wp-inline-create/wp-inline-create.directive.ts
  40. 2
      frontend/app/components/wp-list/wp-list.service.ts
  41. 6
      frontend/app/components/wp-query/query-column.ts
  42. 12
      frontend/app/components/wp-table/timeline/cells/wp-timeline-cell.ts
  43. 60
      frontend/app/components/wp-table/timeline/cells/wp-timeline-cells-renderer.ts
  44. 25
      frontend/app/components/wp-table/timeline/container/wp-timeline-container.directive.ts
  45. 48
      frontend/app/components/wp-table/timeline/global-elements/wp-timeline-relations.directive.ts
  46. 2
      frontend/app/vendors.js
  47. 5
      frontend/npm-shrinkwrap.json
  48. 1
      frontend/package.json
  49. 8
      spec/features/accessibility/work_packages/work_package_query_spec.rb
  50. 4
      spec/features/support/work_package_table.rb
  51. 2
      spec/features/work_packages/navigation_spec.rb
  52. 4
      spec/features/work_packages/table/hierarchy_spec.rb
  53. 4
      spec/features/work_packages/table/relations_spec.rb
  54. 2
      spec/support/components/work_packages/context_menu.rb
  55. 8
      spec/support/components/work_packages/hierarchies.rb
  56. 8
      spec/support/pages/work_packages_table.rb
  57. 4
      spec/support/pages/work_packages_timeline.rb

@ -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

@ -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

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

@ -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<CollectionResource[]>}
*/
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

@ -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 }

@ -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<WorkPackageTableHierarchies>();
// State to be updated when the table is up to date
rendered = input<TableRenderResult>();
rendered = input<RenderedRow[]>();
renderedWorkPackages: State<RenderedRow[]> = 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<WorkPackageTableTimelineState>();

@ -16,6 +16,10 @@ export class SwitchState<StateName> {
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);

@ -107,7 +107,7 @@ export class WorkPackageCacheService {
*
* @param workPackageIds
*/
loadWorkPackages(workPackageIds:string[]):ng.IPromise<void> {
loadWorkPackages(workPackageIds:string[]):Promise<void> {
const needToLoad:string[] = [];
workPackageIds.forEach((id:string) => {
@ -121,9 +121,10 @@ export class WorkPackageCacheService {
}
return this.apiWorkPackages
.loadWorkPackagesCollectionFor(workPackageIds)
.then((results:WorkPackageCollectionResourceInterface) => {
.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);
@ -134,6 +135,7 @@ export class WorkPackageCacheService {
this.updateWorkPackageList(results.elements);
}
});
});
}
loadWorkPackage(workPackageId: string, forceUpdate = false): State<WorkPackageResource> {

@ -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);
}
@ -102,7 +102,7 @@ export class TableRowEditContext implements WorkPackageEditContext {
}
private get rowContainer() {
return jQuery(`#${rowId(this.workPackageId)}`);
return jQuery(`.${this.classIdentifier}-table`);
}
}

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

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

@ -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,
}
);

@ -21,7 +21,6 @@ export class HierarchyRowsBuilder extends RowsBuilder {
super(workPackageTable);
injectorBridge(this);
this.rowBuilder = new SingleHierarchyRowBuilder(this.workPackageTable);
this.refreshBuilder = this.rowBuilder;
}
/**

@ -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;
@ -33,8 +33,10 @@ export class SingleHierarchyRowBuilder extends RowRefreshBuilder {
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 }),
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,15 +70,14 @@ 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,
public buildAncestorRow(ancestor:WorkPackageResourceInterface,
ancestorGroups:string[],
index:number):[HTMLElement, boolean] {
@ -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,8 +134,7 @@ 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')
@ -166,8 +164,10 @@ export class SingleHierarchyRowBuilder extends RowRefreshBuilder {
hierarchyIndicator.innerHTML = `
<a href tabindex="0" role="button" class="wp-table--hierarchy-indicator ${className}">
<span class="wp-table--hierarchy-indicator-icon" aria-hidden="true"></span>
<span class="wp-table--hierarchy-indicator-expanded hidden-for-sighted">${this.text.expanded(level)}</span>
<span class="wp-table--hierarchy-indicator-collapsed hidden-for-sighted">${this.text.collapsed(level)}</span>
<span class="wp-table--hierarchy-indicator-expanded hidden-for-sighted">${this.text.expanded(
level)}</span>
<span class="wp-table--hierarchy-indicator-collapsed hidden-for-sighted">${this.text.collapsed(
level)}</span>
</a>
`;
}
@ -175,7 +175,6 @@ export class SingleHierarchyRowBuilder extends RowRefreshBuilder {
return hierarchyIndicator;
}
}
SingleHierarchyRowBuilder.$inject = ['states', 'I18n'];

@ -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'];

@ -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 <null>, 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 {
public get result():RenderedRow[] {
return this.renderedOrder.map((row) => {
return {
renderedOrder: this.renderedOrder
};
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
});
}

@ -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
);
td.appendChild(link);
td.classList.add('relation-row--id-cell');
return super.buildCell(workPackage, column);
}
tr.appendChild(td);
});
/**
* Build the columns on the given empty row
*/
public buildEmptyRelationRow(from:WorkPackageResourceInterface, relation:RelationResource, type:RelationColumnType):[HTMLElement, WorkPackageResourceInterface] {
const denormalized = relation.denormalized(from);
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 <WP Type>" 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 "<Relation Type> 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;
}
}

@ -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();
}

@ -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];
}
}

@ -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);
}
@ -77,6 +76,67 @@ export class SingleRowBuilder {
*/
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;
}
}

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

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

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

@ -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

@ -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;
}
}

@ -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) {

@ -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) {

@ -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;
}
}

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

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

@ -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';

@ -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'];

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

@ -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.

@ -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}`;
}
/**

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

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

@ -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;
}

@ -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() {

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

@ -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 {

@ -39,8 +39,6 @@ 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;
@ -56,6 +54,7 @@ export class WorkPackageTimelineCell {
constructor(public workPackageTimeline:WorkPackageTimelineTableController,
public renderers:{ milestone:TimelineMilestoneCellRenderer, generic:TimelineCellRenderer },
public latestRenderInfo:RenderInfo,
public classIdentifier:string,
public workPackageId:string) {
injectorBridge(this);
}
@ -80,7 +79,6 @@ export class WorkPackageTimelineCell {
return renderer.getPaddingRightForOutgoingRelationLines(this.latestRenderInfo);
}
canConnectRelations():boolean {
const wp = this.latestRenderInfo.workPackage;
if (wp.isMilestone) {
@ -100,7 +98,7 @@ 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 {
@ -115,9 +113,7 @@ export class WorkPackageTimelineCell {
}
// Remove the element first if we're redrawing
if (wasRendered) {
this.clear();
}
// Render the given element
this.wpElement = renderer.render(renderInfo);
@ -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();
}

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

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

@ -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,9 +285,9 @@ export class WorkPackageTableTimelineRelations {
this.container.append(newSegment(vp, e.classNames, idxTo, 19, targetX + 1, 1, 11, 'blue'));
}
}
}
}
}
openprojectModule.component('wpTimelineRelations', {
template: '<div class="wp-table-timeline--relations"></div>',

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

@ -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"
}
}
}

@ -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",

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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

Loading…
Cancel
Save