Skip to content

Commit 256bfd2

Browse files
committed
WIP alternative hierarchic config
1 parent 1f49f93 commit 256bfd2

7 files changed

Lines changed: 143 additions & 69 deletions

File tree

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,21 @@
11
import {Type} from '@angular/core';
2-
import {QueryVariables} from '../../../classes/query-variable-manager';
3-
import {UntypedModelService} from '../../../types/types';
2+
import {type ExtractTallOne, type ExtractVall, UntypedModelService} from '../../../types/types';
43

5-
export type NaturalHierarchicConfiguration<T extends UntypedModelService = UntypedModelService> = {
4+
export type NodeConfig<T extends UntypedModelService = UntypedModelService> = {
65
/**
76
* An AbstractModelService to be used to fetch items
87
*/
98
service: Type<T>;
109

1110
/**
12-
* A list of FilterConditionField name to filter items
13-
*
14-
* Those will be used directly to build filter to fetch items, so they must be
15-
* valid API FilterConditionField names for the given service.
16-
*
17-
* Eg: given the QuestionService, possible names would be:
18-
*
19-
* - "chapter" to filter by the question's chapter
20-
* - "parent" to filter by the question's parent question
21-
*/
22-
parentsRelationNames?: string[];
23-
24-
/**
25-
* A list of FilterConditionField name to declare hierarchy
26-
*
27-
* Those must be the `parentsRelationNames` name, that correspond to this service,
28-
* of all children services.
29-
*
30-
* Eg: given the QuestionService, possible names would be:
31-
*
32-
* - "questions" coming from ChapterService
33-
* - "questions" coming from QuestionService
11+
* Whether this node is at the root of the tree (there can be multiple roots in one tree)
3412
*/
35-
childrenRelationNames?: string[];
13+
root?: true | ExtractVall<T>['filter'];
3614

3715
/**
3816
* Additional filters applied in the query sent by getList function
3917
*/
40-
filter?: QueryVariables['filter'];
18+
filter?: ExtractVall<T>['filter'];
4119

4220
/**
4321
* Key of the returned literal container models by config / service
@@ -55,12 +33,60 @@ export type NaturalHierarchicConfiguration<T extends UntypedModelService = Untyp
5533
*
5634
* In fact, this means isDisabled. Also applies to unselect.
5735
*/
58-
isSelectableCallback?: (item: any) => boolean;
36+
isSelectableCallback?: (item: ExtractTallOne<T>) => boolean;
5937

6038
/**
6139
* Functions that receives a model and returns a string for display value
6240
*
6341
* If missing, fallback on global `NaturalHierarchicSelectorComponent.displayWith`
6442
*/
65-
displayWith?: (item: any) => string;
43+
displayWith?: (item: ExtractTallOne<T>) => string;
6644
};
45+
46+
type RelationConfig<Nodes extends NodeConfig[]> = {
47+
/**
48+
* The parent node, eg: ChapterService
49+
*/
50+
parent: Nodes[number];
51+
52+
/**
53+
* The child node, eg: QuestionService
54+
*/
55+
child: Nodes[number];
56+
57+
/**
58+
* One of the keys of the `FilterGroupCondition` for the child service, to filter children by their parent(s)
59+
*
60+
* Those will be used directly to build filter to fetch children, so they must be
61+
* valid API `FilterGroupCondition` keys for the given child service.
62+
*
63+
* Eg: given the `QuestionService`, possible names would be:
64+
*
65+
* - "chapter" to filter the questions by their chapter
66+
* - "parent" to filter the questions by their parent question
67+
*/
68+
field: string;
69+
};
70+
71+
export type NaturalHierarchicConfiguration<Nodes extends NodeConfig[]> = {
72+
/**
73+
* All possible nodes in the tree
74+
*/
75+
nodes: Nodes;
76+
77+
/**
78+
* All possible relations between nodes
79+
*/
80+
relations: RelationConfig<Nodes>[];
81+
};
82+
83+
export function nodeConfig<T extends UntypedModelService>(node: NodeConfig<T>): NodeConfig<T> {
84+
return node;
85+
}
86+
87+
export function hierarchicConfig<Nodes extends NodeConfig[]>(
88+
nodes: Nodes,
89+
relations: RelationConfig<Nodes>[],
90+
): NaturalHierarchicConfiguration<Nodes> {
91+
return {nodes: nodes, relations: relations};
92+
}

projects/natural/src/lib/modules/hierarchic-selector/classes/model-node.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {BehaviorSubject, Observable} from 'rxjs';
2-
import {NaturalHierarchicConfiguration} from './hierarchic-configuration';
2+
import {NaturalHierarchicConfiguration, type NodeConfig} from './hierarchic-configuration';
33
import {NameOrFullName} from '../../../types/types';
44

55
export type HierarchicModel = {__typename: string} & NameOrFullName;
@@ -16,7 +16,7 @@ export class ModelNode {
1616

1717
public constructor(
1818
public readonly model: HierarchicModel,
19-
public readonly config: NaturalHierarchicConfiguration,
19+
public readonly config: NaturalHierarchicConfiguration<NodeConfig[]>,
2020
) {}
2121

2222
public get children(): Observable<ModelNode[]> {

projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector-dialog/hierarchic-selector-dialog.component.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {clone} from 'es-toolkit';
44
import {defaults} from 'es-toolkit/compat';
55
import {NaturalSearchFacets} from '../../search/types/facet';
66
import {NaturalSearchSelections} from '../../search/types/values';
7-
import {NaturalHierarchicConfiguration} from '../classes/hierarchic-configuration';
7+
import {NaturalHierarchicConfiguration, type NodeConfig} from '../classes/hierarchic-configuration';
88
import {HierarchicFiltersConfiguration} from '../classes/hierarchic-filters-configuration';
99
import {OrganizedModelSelection} from '../hierarchic-selector/hierarchic-selector.service';
1010
import {MatButton} from '@angular/material/button';
@@ -15,11 +15,11 @@ export type HierarchicDialogResult = {
1515
searchSelections?: NaturalSearchSelections | null;
1616
};
1717

18-
export type HierarchicDialogConfig = {
18+
export type HierarchicDialogConfig<Nodes extends NodeConfig[]> = {
1919
/**
2020
* Configuration to setup rules of hierarchy
2121
*/
22-
hierarchicConfig: NaturalHierarchicConfiguration[];
22+
hierarchicConfig: NaturalHierarchicConfiguration<Nodes>;
2323

2424
/**
2525
* Selected items when HierarchicComponent initializes
@@ -57,22 +57,22 @@ export type HierarchicDialogConfig = {
5757
templateUrl: './hierarchic-selector-dialog.component.html',
5858
styleUrl: './hierarchic-selector-dialog.component.scss',
5959
})
60-
export class NaturalHierarchicSelectorDialogComponent {
60+
export class NaturalHierarchicSelectorDialogComponent<Nodes extends NodeConfig[]> {
6161
private dialogRef =
62-
inject<MatDialogRef<NaturalHierarchicSelectorDialogComponent, HierarchicDialogResult>>(MatDialogRef);
62+
inject<MatDialogRef<NaturalHierarchicSelectorDialogComponent<Nodes>, HierarchicDialogResult>>(MatDialogRef);
6363

6464
/**
6565
* Set of hierarchic configurations to pass as attribute to HierarchicComponent
6666
*/
67-
public config: HierarchicDialogConfig;
67+
public config: HierarchicDialogConfig<Nodes>;
6868

6969
/**
7070
* Natural search selections after initialisation
7171
*/
7272
public searchSelectionsOutput: NaturalSearchSelections | undefined | null;
7373

7474
public constructor() {
75-
const data = inject<HierarchicDialogConfig>(MAT_DIALOG_DATA);
75+
const data = inject<HierarchicDialogConfig<Nodes>>(MAT_DIALOG_DATA);
7676

7777
this.config = defaults(data, {multiple: true});
7878
this.searchSelectionsOutput = this.config.searchSelections;

projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector/hierarchic-selector.component.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {toGraphQLDoctrineFilter} from '../../search/classes/graphql-doctrine';
2525
import {NaturalSearchComponent} from '../../search/search/search.component';
2626
import {NaturalSearchFacets} from '../../search/types/facet';
2727
import {NaturalSearchSelections} from '../../search/types/values';
28-
import {NaturalHierarchicConfiguration} from '../classes/hierarchic-configuration';
28+
import {NaturalHierarchicConfiguration, type NodeConfig} from '../classes/hierarchic-configuration';
2929
import {HierarchicFiltersConfiguration} from '../classes/hierarchic-filters-configuration';
3030
import {ModelNode} from '../classes/model-node';
3131
import {NaturalHierarchicSelectorService, OrganizedModelSelection} from './hierarchic-selector.service';
@@ -59,7 +59,7 @@ import {NgTemplateOutlet} from '@angular/common';
5959
styleUrl: './hierarchic-selector.component.scss',
6060
providers: [NaturalHierarchicSelectorService],
6161
})
62-
export class NaturalHierarchicSelectorComponent implements OnInit, OnChanges {
62+
export class NaturalHierarchicSelectorComponent<Nodes extends NodeConfig[]> implements OnInit, OnChanges {
6363
protected readonly hierarchicSelectorService = inject(NaturalHierarchicSelectorService);
6464

6565
/**
@@ -70,7 +70,7 @@ export class NaturalHierarchicSelectorComponent implements OnInit, OnChanges {
7070
/**
7171
* Config for items and relations arrangement
7272
*/
73-
public readonly config = input.required<NaturalHierarchicConfiguration[]>();
73+
public readonly config = input.required<NaturalHierarchicConfiguration<Nodes>>();
7474

7575
/**
7676
* If multiple or single item selection

projects/natural/src/lib/modules/hierarchic-selector/hierarchic-selector/hierarchic-selector.service.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {map} from 'rxjs/operators';
55
import {NaturalQueryVariablesManager, QueryVariables} from '../../../classes/query-variable-manager';
66
import {Literal, UntypedModelService} from '../../../types/types';
77
import {FilterGroupCondition} from '../../search/classes/graphql-doctrine.types';
8-
import {NaturalHierarchicConfiguration} from '../classes/hierarchic-configuration';
8+
import {NaturalHierarchicConfiguration, type NodeConfig} from '../classes/hierarchic-configuration';
99
import {
1010
HierarchicFilterConfiguration,
1111
HierarchicFiltersConfiguration,
@@ -179,22 +179,22 @@ export class NaturalHierarchicSelectorService {
179179
/**
180180
* Checks that each configuration.selectableAtKey attribute is unique
181181
*/
182-
public validateConfiguration(configurations: NaturalHierarchicConfiguration[]): void {
183-
const selectableAtKeyAttributes: string[] = [];
184-
for (const config of configurations) {
185-
if (config.selectableAtKey) {
186-
const keyIndex = selectableAtKeyAttributes.indexOf(config.selectableAtKey);
182+
public validateConfiguration(configurations: NaturalHierarchicConfiguration<NodeConfig[]>): void {
183+
const selectableAtKeyAttributes = new Set<string>();
184+
configurations.nodes.forEach(node => {
185+
if (node.selectableAtKey) {
186+
selectableAtKeyAttributes.add(node.selectableAtKey);
187+
}
188+
});
187189

188-
if (keyIndex === -1 && config.selectableAtKey) {
189-
selectableAtKeyAttributes.push(config.selectableAtKey);
190-
}
190+
if (selectableAtKeyAttributes.size !== 1) {
191+
console.error(
192+
'Invalid hierarchic configuration: `selectableAtKey` attribute must exists on a least one node, and it must be unique across all nodes',
193+
);
194+
}
191195

192-
// This behavior maybe dangerous in case we re-open hierarchical selector with the last returned config
193-
// having non-unique keys
194-
if (keyIndex < -1) {
195-
console.warn('Invalid hierarchic configuration : selectableAtKey attribute should be unique');
196-
}
197-
}
196+
if (!configurations.nodes.find(node => node.root)) {
197+
console.error('Invalid hierarchic configuration: `root` attribute must exists on a least one node');
198198
}
199199
}
200200

projects/natural/src/lib/modules/select/select-hierarchic/select-hierarchic.component.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
HierarchicDialogConfig,
88
NaturalHierarchicConfiguration,
99
NaturalHierarchicSelectorDialogService,
10+
type NodeConfig,
1011
OrganizedModelSelection,
1112
} from '../../hierarchic-selector/public-api';
1213
import {AbstractSelect} from '../abstract-select.component';
@@ -67,7 +68,7 @@ function defaultDisplayFn(item: Literal | null): string {
6768
templateUrl: './select-hierarchic.component.html',
6869
styleUrl: './select-hierarchic.component.scss',
6970
})
70-
export class NaturalSelectHierarchicComponent
71+
export class NaturalSelectHierarchicComponent<Nodes extends NodeConfig[]>
7172
extends AbstractSelect<Literal, string>
7273
implements OnInit, ControlValueAccessor
7374
{
@@ -83,7 +84,7 @@ export class NaturalSelectHierarchicComponent
8384
*
8485
* It should be an array with at least one element with `selectableAtKey` configured, otherwise the selector will never open.
8586
*/
86-
@Input() public config: NaturalHierarchicConfiguration[] | null = null;
87+
@Input() public config: NaturalHierarchicConfiguration<Nodes> | null = null;
8788

8889
/**
8990
* Filters formatted for hierarchic selector
@@ -147,7 +148,7 @@ export class NaturalSelectHierarchicComponent
147148
selected[selectAtKey] = [this.value];
148149
}
149150

150-
const hierarchicConfig: HierarchicDialogConfig = {
151+
const hierarchicConfig: HierarchicDialogConfig<Nodes> = {
151152
hierarchicConfig: this.config,
152153
hierarchicSelection: selected,
153154
hierarchicFilters: this.filters(),
@@ -178,6 +179,6 @@ export class NaturalSelectHierarchicComponent
178179
}
179180

180181
private getSelectKey(): string | undefined {
181-
return this.config?.find(c => !!c.selectableAtKey)?.selectableAtKey;
182+
return this.config?.nodes.find(node => !!node.selectableAtKey)?.selectableAtKey;
182183
}
183184
}

src/app/select-hierarchic/select-hierarchic.component.ts

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import {JsonPipe} from '@angular/common';
22
import {Component} from '@angular/core';
33
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
44
import {MatButton} from '@angular/material/button';
5-
import {MatFormField, MatLabel, MatHint} from '@angular/material/form-field';
5+
import {MatFormField, MatHint, MatLabel} from '@angular/material/form-field';
66
import {MatInput} from '@angular/material/input';
7-
import {NaturalHierarchicConfiguration} from '@ecodev/natural';
7+
import {hierarchicConfig, nodeConfig} from '@ecodev/natural';
88
import {NaturalSelectHierarchicComponent} from '../../../projects/natural/src/lib/modules/select/select-hierarchic/select-hierarchic.component';
99
import {ItemService} from '../../../projects/natural/src/lib/testing/item.service';
1010
import {AbstractSelect} from '../AbstractSelect';
1111
import {DebugControlComponent} from '../debug-form.component';
12+
import {FileService} from '../file/file.service';
1213

1314
@Component({
1415
imports: [
@@ -27,12 +28,58 @@ import {DebugControlComponent} from '../debug-form.component';
2728
styleUrl: './select-hierarchic.component.scss',
2829
})
2930
export class SelectHierarchicComponent extends AbstractSelect {
30-
public hierarchicConfig: NaturalHierarchicConfiguration[] = [
31-
{
32-
service: ItemService,
33-
parentsRelationNames: ['parent'],
34-
childrenRelationNames: ['parent'],
35-
selectableAtKey: 'any',
31+
public readonly itemNode = nodeConfig({
32+
service: ItemService,
33+
root: true,
34+
selectableAtKey: 'any',
35+
});
36+
37+
public readonly fileNode = nodeConfig({
38+
service: FileService,
39+
selectableAtKey: 'any',
40+
});
41+
42+
public hierarchicConfig = hierarchicConfig(
43+
[this.itemNode],
44+
[
45+
{
46+
parent: this.itemNode,
47+
child: this.itemNode,
48+
field: 'parent',
49+
},
50+
],
51+
);
52+
53+
public readonly itemForRootNode = nodeConfig({
54+
service: ItemService,
55+
root: {
56+
filter: {
57+
conditions: [
58+
// ...
59+
],
60+
},
3661
},
37-
];
62+
selectableAtKey: 'any',
63+
});
64+
65+
public readonly itemForChildNode = nodeConfig({
66+
service: ItemService,
67+
selectableAtKey: 'any',
68+
});
69+
70+
public hierarchicConfig2 = hierarchicConfig(
71+
[this.itemForRootNode, this.itemForChildNode],
72+
[
73+
{
74+
parent: this.itemForRootNode,
75+
child: this.itemForChildNode,
76+
field: 'parent',
77+
},
78+
{
79+
parent: this.itemForChildNode,
80+
child: this.itemForChildNode,
81+
field: 'parent',
82+
},
83+
],
84+
);
3885
}

0 commit comments

Comments
 (0)