diff --git a/projects/natural/src/lib/modules/hierarchic-selector/classes/hierarchic-configuration.ts b/projects/natural/src/lib/modules/hierarchic-selector/classes/hierarchic-configuration.ts index 089e08d0..74bd9b57 100644 --- a/projects/natural/src/lib/modules/hierarchic-selector/classes/hierarchic-configuration.ts +++ b/projects/natural/src/lib/modules/hierarchic-selector/classes/hierarchic-configuration.ts @@ -1,43 +1,21 @@ import {Type} from '@angular/core'; -import {QueryVariables} from '../../../classes/query-variable-manager'; -import {UntypedModelService} from '../../../types/types'; +import {type ExtractTallOne, type ExtractVall, UntypedModelService} from '../../../types/types'; -export type NaturalHierarchicConfiguration = { +export type NodeConfig = { /** * An AbstractModelService to be used to fetch items */ service: Type; /** - * A list of FilterConditionField name to filter items - * - * Those will be used directly to build filter to fetch items, so they must be - * valid API FilterConditionField names for the given service. - * - * Eg: given the QuestionService, possible names would be: - * - * - "chapter" to filter by the question's chapter - * - "parent" to filter by the question's parent question - */ - parentsRelationNames?: string[]; - - /** - * A list of FilterConditionField name to declare hierarchy - * - * Those must be the `parentsRelationNames` name, that correspond to this service, - * of all children services. - * - * Eg: given the QuestionService, possible names would be: - * - * - "questions" coming from ChapterService - * - "questions" coming from QuestionService + * Whether this node is at the root of the tree (there can be multiple roots in one tree) */ - childrenRelationNames?: string[]; + root?: boolean; /** * Additional filters applied in the query sent by getList function */ - filter?: QueryVariables['filter']; + filter?: ExtractVall['filter']; /** * Key of the returned literal container models by config / service @@ -55,12 +33,60 @@ export type NaturalHierarchicConfiguration boolean; + isSelectableCallback?: (item: ExtractTallOne) => boolean; /** * Functions that receives a model and returns a string for display value * * If missing, fallback on global `NaturalHierarchicSelectorComponent.displayWith` */ - displayWith?: (item: any) => string; + displayWith?: (item: ExtractTallOne) => string; }; + +type RelationConfig = { + /** + * The parent node, eg: ChapterService + */ + parent: Nodes[number]; + + /** + * The child node, eg: QuestionService + */ + child: Nodes[number]; + + /** + * One of the keys of the `FilterGroupCondition` for the child service, to filter children by their parent(s) + * + * Those will be used directly to build filter to fetch children, so they must be + * valid API `FilterGroupCondition` keys for the given child service. + * + * Eg: given the `QuestionService`, possible names would be: + * + * - "chapter" to filter the questions by their chapter + * - "parent" to filter the questions by their parent question + */ + field: string; +}; + +export type NaturalHierarchicConfiguration = { + /** + * All possible nodes in the tree + */ + nodes: Nodes; + + /** + * All possible relations between nodes + */ + relations: RelationConfig[]; +}; + +export function nodeConfig(node: NodeConfig): NodeConfig { + return node; +} + +export function hierarchicConfig( + nodes: Nodes, + relations: RelationConfig[], +): NaturalHierarchicConfiguration { + return {nodes: nodes, relations: relations}; +} diff --git a/projects/natural/src/lib/modules/hierarchic-selector/classes/model-node.ts b/projects/natural/src/lib/modules/hierarchic-selector/classes/model-node.ts index 9b87a8d8..88a36ab9 100644 --- a/projects/natural/src/lib/modules/hierarchic-selector/classes/model-node.ts +++ b/projects/natural/src/lib/modules/hierarchic-selector/classes/model-node.ts @@ -1,5 +1,5 @@ import {BehaviorSubject, Observable} from 'rxjs'; -import {NaturalHierarchicConfiguration} from './hierarchic-configuration'; +import {NaturalHierarchicConfiguration, type NodeConfig} from './hierarchic-configuration'; import {NameOrFullName} from '../../../types/types'; export type HierarchicModel = {__typename: string} & NameOrFullName; @@ -16,7 +16,7 @@ export class ModelNode { public constructor( public readonly model: HierarchicModel, - public readonly config: NaturalHierarchicConfiguration, + public readonly config: NaturalHierarchicConfiguration, ) {} public get children(): Observable { diff --git a/projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector-dialog/hierarchic-selector-dialog.component.ts b/projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector-dialog/hierarchic-selector-dialog.component.ts index cac07720..7a8c6bff 100644 --- a/projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector-dialog/hierarchic-selector-dialog.component.ts +++ b/projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector-dialog/hierarchic-selector-dialog.component.ts @@ -4,7 +4,7 @@ import {clone} from 'es-toolkit'; import {defaults} from 'es-toolkit/compat'; import {NaturalSearchFacets} from '../../search/types/facet'; import {NaturalSearchSelections} from '../../search/types/values'; -import {NaturalHierarchicConfiguration} from '../classes/hierarchic-configuration'; +import {NaturalHierarchicConfiguration, type NodeConfig} from '../classes/hierarchic-configuration'; import {HierarchicFiltersConfiguration} from '../classes/hierarchic-filters-configuration'; import {OrganizedModelSelection} from '../hierarchic-selector/hierarchic-selector.service'; import {MatButton} from '@angular/material/button'; @@ -15,11 +15,11 @@ export type HierarchicDialogResult = { searchSelections?: NaturalSearchSelections | null; }; -export type HierarchicDialogConfig = { +export type HierarchicDialogConfig = { /** * Configuration to setup rules of hierarchy */ - hierarchicConfig: NaturalHierarchicConfiguration[]; + hierarchicConfig: NaturalHierarchicConfiguration; /** * Selected items when HierarchicComponent initializes @@ -57,14 +57,14 @@ export type HierarchicDialogConfig = { templateUrl: './hierarchic-selector-dialog.component.html', styleUrl: './hierarchic-selector-dialog.component.scss', }) -export class NaturalHierarchicSelectorDialogComponent { +export class NaturalHierarchicSelectorDialogComponent { private dialogRef = - inject>(MatDialogRef); + inject, HierarchicDialogResult>>(MatDialogRef); /** * Set of hierarchic configurations to pass as attribute to HierarchicComponent */ - public config: HierarchicDialogConfig; + public config: HierarchicDialogConfig; /** * Natural search selections after initialisation @@ -72,7 +72,7 @@ export class NaturalHierarchicSelectorDialogComponent { public searchSelectionsOutput: NaturalSearchSelections | undefined | null; public constructor() { - const data = inject(MAT_DIALOG_DATA); + const data = inject>(MAT_DIALOG_DATA); this.config = defaults(data, {multiple: true}); this.searchSelectionsOutput = this.config.searchSelections; diff --git a/projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector/hierarchic-selector.component.ts b/projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector/hierarchic-selector.component.ts index 3475bd49..c0e8e7f5 100644 --- a/projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector/hierarchic-selector.component.ts +++ b/projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector/hierarchic-selector.component.ts @@ -25,7 +25,7 @@ import {toGraphQLDoctrineFilter} from '../../search/classes/graphql-doctrine'; import {NaturalSearchComponent} from '../../search/search/search.component'; import {NaturalSearchFacets} from '../../search/types/facet'; import {NaturalSearchSelections} from '../../search/types/values'; -import {NaturalHierarchicConfiguration} from '../classes/hierarchic-configuration'; +import {NaturalHierarchicConfiguration, type NodeConfig} from '../classes/hierarchic-configuration'; import {HierarchicFiltersConfiguration} from '../classes/hierarchic-filters-configuration'; import {ModelNode} from '../classes/model-node'; import {NaturalHierarchicSelectorService, OrganizedModelSelection} from './hierarchic-selector.service'; @@ -59,7 +59,7 @@ import {NgTemplateOutlet} from '@angular/common'; styleUrl: './hierarchic-selector.component.scss', providers: [NaturalHierarchicSelectorService], }) -export class NaturalHierarchicSelectorComponent implements OnInit, OnChanges { +export class NaturalHierarchicSelectorComponent implements OnInit, OnChanges { protected readonly hierarchicSelectorService = inject(NaturalHierarchicSelectorService); /** @@ -70,7 +70,7 @@ export class NaturalHierarchicSelectorComponent implements OnInit, OnChanges { /** * Config for items and relations arrangement */ - public readonly config = input.required(); + public readonly config = input.required>(); /** * If multiple or single item selection diff --git a/projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector/hierarchic-selector.service.ts b/projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector/hierarchic-selector.service.ts index 270b3c73..3aa08623 100644 --- a/projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector/hierarchic-selector.service.ts +++ b/projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector/hierarchic-selector.service.ts @@ -5,7 +5,7 @@ import {map} from 'rxjs/operators'; import {NaturalQueryVariablesManager, QueryVariables} from '../../../classes/query-variable-manager'; import {Literal, UntypedModelService} from '../../../types/types'; import {FilterGroupCondition} from '../../search/classes/graphql-doctrine.types'; -import {NaturalHierarchicConfiguration} from '../classes/hierarchic-configuration'; +import {NaturalHierarchicConfiguration, type NodeConfig} from '../classes/hierarchic-configuration'; import { HierarchicFilterConfiguration, HierarchicFiltersConfiguration, @@ -179,22 +179,22 @@ export class NaturalHierarchicSelectorService { /** * Checks that each configuration.selectableAtKey attribute is unique */ - public validateConfiguration(configurations: NaturalHierarchicConfiguration[]): void { - const selectableAtKeyAttributes: string[] = []; - for (const config of configurations) { - if (config.selectableAtKey) { - const keyIndex = selectableAtKeyAttributes.indexOf(config.selectableAtKey); + public validateConfiguration(configurations: NaturalHierarchicConfiguration): void { + const selectableAtKeyAttributes = new Set(); + configurations.nodes.forEach(node => { + if (node.selectableAtKey) { + selectableAtKeyAttributes.add(node.selectableAtKey); + } + }); - if (keyIndex === -1 && config.selectableAtKey) { - selectableAtKeyAttributes.push(config.selectableAtKey); - } + if (selectableAtKeyAttributes.size !== 1) { + console.error( + 'Invalid hierarchic configuration: `selectableAtKey` attribute must exists on a least one node, and it must be unique across all nodes', + ); + } - // This behavior maybe dangerous in case we re-open hierarchical selector with the last returned config - // having non-unique keys - if (keyIndex < -1) { - console.warn('Invalid hierarchic configuration : selectableAtKey attribute should be unique'); - } - } + if (!configurations.nodes.find(node => node.root)) { + console.error('Invalid hierarchic configuration: `root` attribute must exists on a least one node'); } } diff --git a/projects/natural/src/lib/modules/select/select-hierarchic/select-hierarchic.component.ts b/projects/natural/src/lib/modules/select/select-hierarchic/select-hierarchic.component.ts index e9b0a1be..07efd65e 100644 --- a/projects/natural/src/lib/modules/select/select-hierarchic/select-hierarchic.component.ts +++ b/projects/natural/src/lib/modules/select/select-hierarchic/select-hierarchic.component.ts @@ -7,6 +7,7 @@ import { HierarchicDialogConfig, NaturalHierarchicConfiguration, NaturalHierarchicSelectorDialogService, + type NodeConfig, OrganizedModelSelection, } from '../../hierarchic-selector/public-api'; import {AbstractSelect} from '../abstract-select.component'; @@ -67,7 +68,7 @@ function defaultDisplayFn(item: Literal | null): string { templateUrl: './select-hierarchic.component.html', styleUrl: './select-hierarchic.component.scss', }) -export class NaturalSelectHierarchicComponent +export class NaturalSelectHierarchicComponent extends AbstractSelect implements OnInit, ControlValueAccessor { @@ -83,7 +84,7 @@ export class NaturalSelectHierarchicComponent * * It should be an array with at least one element with `selectableAtKey` configured, otherwise the selector will never open. */ - @Input() public config: NaturalHierarchicConfiguration[] | null = null; + @Input() public config: NaturalHierarchicConfiguration | null = null; /** * Filters formatted for hierarchic selector @@ -147,7 +148,7 @@ export class NaturalSelectHierarchicComponent selected[selectAtKey] = [this.value]; } - const hierarchicConfig: HierarchicDialogConfig = { + const hierarchicConfig: HierarchicDialogConfig = { hierarchicConfig: this.config, hierarchicSelection: selected, hierarchicFilters: this.filters(), @@ -178,6 +179,6 @@ export class NaturalSelectHierarchicComponent } private getSelectKey(): string | undefined { - return this.config?.find(c => !!c.selectableAtKey)?.selectableAtKey; + return this.config?.nodes.find(node => !!node.selectableAtKey)?.selectableAtKey; } } diff --git a/src/app/select-hierarchic/select-hierarchic.component.ts b/src/app/select-hierarchic/select-hierarchic.component.ts index 7ba24e3d..6012c714 100644 --- a/src/app/select-hierarchic/select-hierarchic.component.ts +++ b/src/app/select-hierarchic/select-hierarchic.component.ts @@ -2,13 +2,14 @@ import {JsonPipe} from '@angular/common'; import {Component} from '@angular/core'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {MatButton} from '@angular/material/button'; -import {MatFormField, MatLabel, MatHint} from '@angular/material/form-field'; +import {MatFormField, MatHint, MatLabel} from '@angular/material/form-field'; import {MatInput} from '@angular/material/input'; -import {NaturalHierarchicConfiguration} from '@ecodev/natural'; +import {hierarchicConfig, nodeConfig} from '@ecodev/natural'; import {NaturalSelectHierarchicComponent} from '../../../projects/natural/src/lib/modules/select/select-hierarchic/select-hierarchic.component'; import {ItemService} from '../../../projects/natural/src/lib/testing/item.service'; import {AbstractSelect} from '../AbstractSelect'; import {DebugControlComponent} from '../debug-form.component'; +import {FileService} from '../file/file.service'; @Component({ imports: [ @@ -27,12 +28,57 @@ import {DebugControlComponent} from '../debug-form.component'; styleUrl: './select-hierarchic.component.scss', }) export class SelectHierarchicComponent extends AbstractSelect { - public hierarchicConfig: NaturalHierarchicConfiguration[] = [ - { - service: ItemService, - parentsRelationNames: ['parent'], - childrenRelationNames: ['parent'], - selectableAtKey: 'any', + public readonly itemNode = nodeConfig({ + service: ItemService, + root: true, + selectableAtKey: 'any', + }); + + public readonly fileNode = nodeConfig({ + service: FileService, + selectableAtKey: 'any', + }); + + public hierarchicConfig = hierarchicConfig( + [this.itemNode], + [ + { + parent: this.itemNode, + child: this.itemNode, + field: 'parent', + }, + ], + ); + + public readonly itemForRootNode = nodeConfig({ + service: ItemService, + root: true, + selectableAtKey: 'any', + }); + + public readonly itemForChildNode = nodeConfig({ + service: ItemService, + selectableAtKey: 'any', + filter: { + conditions: [ + // ... + ], }, - ]; + }); + + public hierarchicConfig2 = hierarchicConfig( + [this.itemForRootNode, this.itemForChildNode], + [ + { + parent: this.itemForRootNode, + child: this.itemForChildNode, + field: 'parent', + }, + { + parent: this.itemForChildNode, + child: this.itemForChildNode, + field: 'parent', + }, + ], + ); }