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..ff800569 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,66 +1,147 @@ import {Type} from '@angular/core'; +import {PaginatedData} from '../../../classes/data-source'; import {QueryVariables} from '../../../classes/query-variable-manager'; -import {UntypedModelService} from '../../../types/types'; +import {NaturalAbstractModelService} from '../../../services/abstract-model.service'; +import {type ExtractTallOne, type ExtractVall, Literal} from '../../../types/types'; -export type NaturalHierarchicConfiguration = { +// More globally in types.ts ? +export type ListableModelService = NaturalAbstractModelService< + any, + any, + PaginatedData, + QueryVariables, + any, + any, + any, + any, + any, + any +>; + +// Remove = ListableModelService ? keep the extends part only ? +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. + * Additional filters applied in the query sent by getList function + */ + filter?: ExtractVall['filter']; + + /** + * Displayed icon for items retrieved for that config + */ + icon?: string; + + /** + * Callback function that returns boolean. If true the item is selectable, if false, it's not. + * If missing, item is selectable. * - * Eg: given the QuestionService, possible names would be: + * In fact, this means isDisabled. Also applies to unselect. + */ + isSelectableCallback?: (item: ExtractTallOne) => boolean; + + /** + * Functions that receives a model and returns a string for display value * - * - "chapter" to filter by the question's chapter - * - "parent" to filter by the question's parent question + * If missing, fallback on global `NaturalHierarchicSelectorComponent.displayWith` + */ + displayWith?: (item: ExtractTallOne) => string; +}; + +type RelationConfig = { + /** + * The parent node, eg: ChapterService */ - parentsRelationNames?: string[]; + parent: Nodes[number]; /** - * A list of FilterConditionField name to declare hierarchy + * 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 must be the `parentsRelationNames` name, that correspond to this service, - * of all children services. + * 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: + * Eg: given the `QuestionService`, possible names would be: * - * - "questions" coming from ChapterService - * - "questions" coming from QuestionService + * - "chapter" to filter the questions by their chapter + * - "parent" to filter the questions by their parent question */ - childrenRelationNames?: string[]; + field: string; +}; +export type NaturalHierarchicConfiguration< + Node extends NodeConfig[] = NodeConfig[], + Selectables extends OrganizedSelectableConfigs = OrganizedSelectableConfigs, +> = { /** - * Additional filters applied in the query sent by getList function + * All possible nodes in the tree */ - filter?: QueryVariables['filter']; + nodes: Node; /** - * Key of the returned literal container models by config / service + * All possible relations between nodes */ - selectableAtKey?: string; + relations: RelationConfig[]; /** - * Displayed icon for items retrieved for that config + * List of nodes used for the root of the tree */ - icon?: string; + roots: Node[number][]; /** - * Callback function that returns boolean. If true the item is selectable, if false, it's not. - * If missing, item is selectable. - * - * In fact, this means isDisabled. Also applies to unselect. + * List of nodes for selectable elements, organized by key. */ - isSelectableCallback?: (item: any) => boolean; + selectables: Selectables; +}; - /** - * Functions that receives a model and returns a string for display value - * - * If missing, fallback on global `NaturalHierarchicSelectorComponent.displayWith` - */ - displayWith?: (item: any) => string; +export function nodeConfig(node: NodeConfig): NodeConfig { + return node; +} + +export function hierarchicConfig< + const Nodes extends NodeConfig[], + const Selectables extends OrganizedSelectableConfigs, +>( + nodes: Nodes, + roots: Nodes[number][], + selectables: Selectables, + relations: RelationConfig[], +): NaturalHierarchicConfiguration { + return {nodes, roots, selectables, relations}; +} + +// Outputs / Resulting selections + +/** + * Equivalent to `OrganizedSelections` but for the config (input) side. + * List of selectable nodes (services) organized by key. + */ +export type OrganizedSelectableConfigs = Record; + +/** + * The model type of a single node config, eg: NodeConfig → Question + */ +type Model = SingleNodeConfig extends NodeConfig + ? ExtractTallOne + : never; + +/** + * The models selected for a single selectable slot (one key of `selectables`) + */ +export type Selections = Model[]; + +/** + * All selections, organized by key, typed from the configuration's `selectables`. + * + * Replaces the old `OrganizedModelSelection = Record`. + */ +export type OrganizedSelections = OrganizedSelectableConfigs> = { + [Key in keyof Selectables]: Selections; }; 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', + }, + ], + ); }