Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<T extends UntypedModelService = UntypedModelService> = {
// More globally in types.ts ?
export type ListableModelService = NaturalAbstractModelService<
any,
any,
PaginatedData<Literal>,
QueryVariables,
any,
any,
any,
any,
any,
any
>;

// Remove = ListableModelService ? keep the extends part only ?
export type NodeConfig<T extends ListableModelService = ListableModelService> = {
/**
* An AbstractModelService to be used to fetch items
*/
service: Type<T>;

/**
* 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<T>['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<T>) => 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<T>) => string;
};

type RelationConfig<Nodes extends NodeConfig[]> = {
/**
* 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<Node> = OrganizedSelectableConfigs<Node>,
> = {
/**
* 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<Node>[];

/**
* 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<T extends ListableModelService>(node: NodeConfig<T>): NodeConfig<T> {
return node;
}

export function hierarchicConfig<
const Nodes extends NodeConfig[],
const Selectables extends OrganizedSelectableConfigs<Nodes>,
>(
nodes: Nodes,
roots: Nodes[number][],
selectables: Selectables,
relations: RelationConfig<Nodes>[],
): NaturalHierarchicConfiguration<Nodes, Selectables> {
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<Nodes extends NodeConfig[]> = Record<string, Nodes[number][]>;

/**
* The model type of a single node config, eg: NodeConfig<QuestionService> → Question
*/
type Model<SingleNodeConfig extends NodeConfig> = SingleNodeConfig extends NodeConfig<infer T>
? ExtractTallOne<T>
: never;

/**
* The models selected for a single selectable slot (one key of `selectables`)
*/
export type Selections<Nodes extends NodeConfig[]> = Model<Nodes[number]>[];

/**
* All selections, organized by key, typed from the configuration's `selectables`.
*
* Replaces the old `OrganizedModelSelection = Record<string, any[]>`.
*/
export type OrganizedSelections<Selectables extends OrganizedSelectableConfigs<any> = OrganizedSelectableConfigs<any>> = {
[Key in keyof Selectables]: Selections<Selectables[Key]>;
};
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,7 +16,7 @@ export class ModelNode {

public constructor(
public readonly model: HierarchicModel,
public readonly config: NaturalHierarchicConfiguration,
public readonly config: NaturalHierarchicConfiguration<NodeConfig[]>,
) {}

public get children(): Observable<ModelNode[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -15,11 +15,11 @@ export type HierarchicDialogResult = {
searchSelections?: NaturalSearchSelections | null;
};

export type HierarchicDialogConfig = {
export type HierarchicDialogConfig<Nodes extends NodeConfig[]> = {
/**
* Configuration to setup rules of hierarchy
*/
hierarchicConfig: NaturalHierarchicConfiguration[];
hierarchicConfig: NaturalHierarchicConfiguration<Nodes>;

/**
* Selected items when HierarchicComponent initializes
Expand Down Expand Up @@ -57,22 +57,22 @@ export type HierarchicDialogConfig = {
templateUrl: './hierarchic-selector-dialog.component.html',
styleUrl: './hierarchic-selector-dialog.component.scss',
})
export class NaturalHierarchicSelectorDialogComponent {
export class NaturalHierarchicSelectorDialogComponent<Nodes extends NodeConfig[]> {
private dialogRef =
inject<MatDialogRef<NaturalHierarchicSelectorDialogComponent, HierarchicDialogResult>>(MatDialogRef);
inject<MatDialogRef<NaturalHierarchicSelectorDialogComponent<Nodes>, HierarchicDialogResult>>(MatDialogRef);

/**
* Set of hierarchic configurations to pass as attribute to HierarchicComponent
*/
public config: HierarchicDialogConfig;
public config: HierarchicDialogConfig<Nodes>;

/**
* Natural search selections after initialisation
*/
public searchSelectionsOutput: NaturalSearchSelections | undefined | null;

public constructor() {
const data = inject<HierarchicDialogConfig>(MAT_DIALOG_DATA);
const data = inject<HierarchicDialogConfig<Nodes>>(MAT_DIALOG_DATA);

this.config = defaults(data, {multiple: true});
this.searchSelectionsOutput = this.config.searchSelections;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Nodes extends NodeConfig[]> implements OnInit, OnChanges {
protected readonly hierarchicSelectorService = inject(NaturalHierarchicSelectorService);

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

/**
* If multiple or single item selection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<NodeConfig[]>): void {
const selectableAtKeyAttributes = new Set<string>();
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');
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
HierarchicDialogConfig,
NaturalHierarchicConfiguration,
NaturalHierarchicSelectorDialogService,
type NodeConfig,
OrganizedModelSelection,
} from '../../hierarchic-selector/public-api';
import {AbstractSelect} from '../abstract-select.component';
Expand Down Expand Up @@ -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<Nodes extends NodeConfig[]>
extends AbstractSelect<Literal, string>
implements OnInit, ControlValueAccessor
{
Expand All @@ -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<Nodes> | null = null;

/**
* Filters formatted for hierarchic selector
Expand Down Expand Up @@ -147,7 +148,7 @@ export class NaturalSelectHierarchicComponent
selected[selectAtKey] = [this.value];
}

const hierarchicConfig: HierarchicDialogConfig = {
const hierarchicConfig: HierarchicDialogConfig<Nodes> = {
hierarchicConfig: this.config,
hierarchicSelection: selected,
hierarchicFilters: this.filters(),
Expand Down Expand Up @@ -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;
}
}
Loading
Loading