Skip to content

Commit

Permalink
Merge pull request #17154 from opf/implementation/58524-render-full-b…
Browse files Browse the repository at this point in the history
…ranch-of-hierarchy-item-value-assignment

[#58524] render hierarchy items path in work package view
  • Loading branch information
Kharonus authored Nov 12, 2024
2 parents 849e280 + 0857271 commit b3bb0cb
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,27 @@
//++

import { Injectable, Injector } from '@angular/core';

import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { AbstractFieldService, IFieldType } from 'core-app/shared/components/fields/field.service';
import { DisplayField } from 'core-app/shared/components/fields/display/display-field.module';
import { IFieldSchema } from 'core-app/shared/components/fields/field.base';
import { MultipleLinesCustomOptionsDisplayField } from 'core-app/shared/components/fields/display/field-types/multiple-lines-custom-options-display-field.module';
import { MultipleLinesUserFieldModule } from 'core-app/shared/components/fields/display/field-types/multiple-lines-user-display-field.module';
import { ProgressTextDisplayField } from 'core-app/shared/components/fields/display/field-types/progress-text-display-field.module';
import { DisplayField } from 'core-app/shared/components/fields/display/display-field.module';
import { AbstractFieldService, IFieldType } from 'core-app/shared/components/fields/field.service';
import {
MultipleLinesCustomOptionsDisplayField,
} from 'core-app/shared/components/fields/display/field-types/multiple-lines-custom-options-display-field.module';
import {
MultipleLinesUserFieldModule,
} from 'core-app/shared/components/fields/display/field-types/multiple-lines-user-display-field.module';
import {
ProgressTextDisplayField,
} from 'core-app/shared/components/fields/display/field-types/progress-text-display-field.module';
import { DateDisplayField } from 'core-app/shared/components/fields/display/field-types/date-display-field.module';

export interface IDisplayFieldType extends IFieldType<DisplayField> {
new(resource:HalResource, attributeType:string, schema:IFieldSchema, context:DisplayFieldContext):DisplayField;
}
import {
HierarchyItemDisplayField,
} from 'core-app/shared/components/fields/display/field-types/hierarchy-item-display-field.module';
import {
MultipleLinesHierarchyItemDisplayField,
} from 'core-app/shared/components/fields/display/field-types/multiple-lines-hierarchy-item-display-field.module';

export interface DisplayFieldContext {
/** The injector to use for the context of this field. Relevant for embedded service injection */
Expand All @@ -51,6 +60,10 @@ export interface DisplayFieldContext {
options:{ [key:string]:any };
}

export interface IDisplayFieldType extends IFieldType<DisplayField> {
new(resource:HalResource, attributeType:string, schema:IFieldSchema, context:DisplayFieldContext):DisplayField;
}

@Injectable({ providedIn: 'root' })
export class DisplayFieldService extends AbstractFieldService<DisplayField, IDisplayFieldType> {
/**
Expand Down Expand Up @@ -80,6 +93,17 @@ export class DisplayFieldService extends AbstractFieldService<DisplayField, IDis
if (context.container === 'single-view' && isCustomMultiLinesField) {
return new MultipleLinesCustomOptionsDisplayField(fieldName, context) as DisplayField;
}

const isHierarchyItemsField = ['CustomField::Hierarchy::Item'].indexOf(schema.type) >= 0;
if (context.container === 'single-view' && isHierarchyItemsField) {
return new HierarchyItemDisplayField(fieldName, context) as DisplayField;
}

const isMultilineHierarchyItemsField = ['[]CustomField::Hierarchy::Item'].indexOf(schema.type) >= 0;
if (context.container === 'single-view' && isMultilineHierarchyItemsField) {
return new MultipleLinesHierarchyItemDisplayField(fieldName, context) as DisplayField;
}

// Separate class seems not needed (merge with []CustomOption above?)
const isVersionMultiLinesField = ['[]Version'].indexOf(schema.type) >= 0;
if (context.container === 'single-view' && isVersionMultiLinesField) {
Expand All @@ -104,6 +128,6 @@ export class DisplayFieldService extends AbstractFieldService<DisplayField, IDis
const cls = this.getSpecificClassFor(resource._type, fieldName, schema.type);

// eslint-disable-next-line new-cap
return new cls(fieldName, context) as DisplayField;
return new cls(fieldName, context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See COPYRIGHT and LICENSE files for more details.
//++

import { from, Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';

import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { HalLink } from 'core-app/features/hal/hal-link/hal-link';
import {
ResourceDisplayField,
} from 'core-app/shared/components/fields/display/field-types/resource-display-field.module';
import { renderHierarchyItem } from 'core-app/shared/components/fields/display/field-types/render-hierarchy-item';

export class HierarchyItemDisplayField extends ResourceDisplayField {
public render(element:HTMLElement, _displayText:string) {
const item = this.attribute as HalResource;
if (item === null || item.name === this.texts.placeholder) {
this.renderEmpty(element);
return;
}

element.innerHTML = '';
element.classList.add('hierarchy-items');

this.branch(item).subscribe((path) => {
element.appendChild(path);
});
}

private branch(item:HalResource):Observable<HTMLDivElement> {
const itemLink = item.$link as HalLink;

return from(itemLink.$fetch())
.pipe(
switchMap((resource:HalResource) => renderHierarchyItem(resource)),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export class MultipleLinesCustomOptionsDisplayField extends ResourcesDisplayFiel
}
}

public get valueString():string {
return this.stringValue.join(', ');
}

protected renderValues(values:string[], element:HTMLElement) {
values.forEach((value) => {
const div = document.createElement('div');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See COPYRIGHT and LICENSE files for more details.
//++

import { combineLatest, from, Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';

import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { HalLink } from 'core-app/features/hal/hal-link/hal-link';
import {
ResourcesDisplayField,
} from 'core-app/shared/components/fields/display/field-types/resources-display-field.module';
import { renderHierarchyItem } from 'core-app/shared/components/fields/display/field-types/render-hierarchy-item';

export class MultipleLinesHierarchyItemDisplayField extends ResourcesDisplayField {
public render(element:HTMLElement, _displayText:string) {
const items = this.attribute as HalResource[];
if (items.length === 0) {
this.renderEmpty(element);
return;
}

element.innerHTML = '';
element.classList.add('hierarchy-items');
this.branches(items).subscribe((elements) => {
elements.forEach((el) => {
element.appendChild(el);
});
});
}

public get valueString():string {
return this.stringValue.join(', ');
}

private branches(items:HalResource[]):Observable<HTMLDivElement[]> {
return combineLatest(items.map((value:HalResource) => {
const itemLink = value.$link as HalLink;

return from(itemLink.$fetch())
.pipe(
switchMap((resource:HalResource) => renderHierarchyItem(resource)),
);
}));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See COPYRIGHT and LICENSE files for more details.
//++

import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';

export function renderHierarchyItem(item:HalResource):Observable<HTMLDivElement> {
const customFieldItemLinks = item.$links as { branch:() => HalResource[] };
return from(customFieldItemLinks.branch())
.pipe(
map((ancestors:CollectionResource) => spansFromAncestors(ancestors)),
map((spans) => {
const div = document.createElement('div');
div.className = 'path';
spans.forEach((span) => div.appendChild(span));
return div;
}),
);
}

function spansFromAncestors(ancestors:CollectionResource):HTMLSpanElement[] {
const spans:HTMLSpanElement[] = [];

ancestors.elements
.filter((el) => !!el.label)
.forEach((el, idx, all) => {
const span = document.createElement('span');
span.textContent = el.label as string;
spans.push(span);

if (idx < all.length - 1) {
const separator = document.createElement('span');
separator.textContent = '/';
spans.push(separator);
} else if (el.short !== null) {
const short = document.createElement('span');
short.textContent = `(${el.short})`;
short.className = 'color-fg-subtle';
spans.push(short);
}
});

return spans;
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ display-field
vertical-align: middle
// Inherit from Parent, e.g. strikethrough for baseline comparison
text-decoration: inherit

&:first-of-type
padding-right: 5px

Expand All @@ -93,6 +94,7 @@ display-field
// Table specific styles
.wp-table--cell-container &
width: 100%

.-actual-value,
.-derived-value
@include wp-table--time-values
Expand All @@ -110,6 +112,15 @@ display-field
&.spentTime .time-logging--value
padding: 0 2px

&.hierarchy-items
.path
& > span
margin-right: 0.25rem

& > span:last-child
margin-right: 0


.wp-table--cell-container
.inline-edit--display-field.-placeholder,
&.estimatedTime,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ def _type
}
end
end

link :branch do
{ href: api_v3_paths.custom_field_item_branch(represented.id) }
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/api/v3/custom_fields/hierarchy/item_branch_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class ItemBranchAPI < ::API::OpenProjectAPI
.fmap { |items| items.map { |item| HierarchicalItemAggregate.new(item:, depth: item.depth - 1) } }
.either(
->(items) do
self_link = api_v3_paths.custom_field_item(@custom_field_item.id)
self_link = api_v3_paths.custom_field_item_branch(@custom_field_item.id)
HierarchyItemCollectionRepresenter.new(items, self_link:, current_user:)
end,
->(error) do
Expand Down
4 changes: 4 additions & 0 deletions lib/api/v3/utilities/path_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ def self.custom_field_item(id)
"#{root}/custom_field_items/#{id}"
end

def self.custom_field_item_branch(id)
"#{custom_field_item(id)}/branch"
end

def self.custom_field_items(id, parent = nil, depth = nil)
query = { parent:, depth: }.compact_blank.to_query

Expand Down

0 comments on commit b3bb0cb

Please sign in to comment.