@l3mpire/ui 2.12.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/USAGE.md CHANGED
@@ -941,7 +941,10 @@ import {
941
941
  | `InteractiveFilterChip` | FilterChip with per-segment popovers (property, operator, value, kebab) |
942
942
  | `PropertySelector` | 2-level popover: categories → properties with search |
943
943
  | `AdvancedChip` | Split button "Advanced filters (N)" + close (outlined neutral) |
944
- | `AdvancedPopover` | Popover with Where/And/Or rows, property/operator selects, inline value input |
944
+ | `AdvancedPopover` | Popover with Where/And/Or rows + nested groups, property/operator/value selects |
945
+ | `AdvancedRow` | Single condition row (property + operator + type-aware value popover + actions) |
946
+ | `AdvancedGroup` | Nested group container with shared connector and inline property selector |
947
+ | `FilterNodeActions` | Kebab menu shared by rows and groups (Duplicate / Turn into group↔filter / Delete) |
945
948
  | `SaveViewButton` | Split button (Save view + dropdown chevron) |
946
949
  | `SummaryChip` | Minimal mode: "Filters (N)" with interactive popover |
947
950
 
@@ -954,8 +957,35 @@ import {
954
957
 
955
958
  **And/Or toggle:** each row in the advanced popover has an independent And/Or toggle button. The choice is **persisted on each `FilterCondition` as `logicOperator: "and" | "or"`** (defaults to `"and"` when undefined), so it survives remounts and can be serialized.
956
959
 
960
+ **Nested groups:** the advanced popover supports arbitrarily-nested `FilterGroup` nodes via `FilterNode = FilterCondition | FilterGroup`. Each row's kebab menu (`FilterNodeActions`) lets users **Duplicate**, **Turn into group** / **Turn into filter**, or **Delete**. Groups share a single connector (`Where`/`And`/`Or`) for all their children and render an inline `PropertySelector` to add new conditions. Tree mutations go through the immutable `updateNodeInTree` / `removeNodeFromTree` / `insertAfterInTree` / `replaceNodeInTree` helpers in `utils`.
961
+
962
+ **Type-aware value inputs:** every `AdvancedRow` value slot renders the same `<ValueInput>` used by `InteractiveFilterChip` — text → text input, number → number / range, date → calendar, enum/tags → single/multi select with `dynamicOptions` support, etc. The trigger button shows the formatted current value (via `formatFilterValue`) and a `+N` badge for multi-selections (via `getBadgeCount`).
963
+
957
964
  **Advanced filter shortcut:** the PropertySelector footer shows an "Advanced filter · N rule(s)" item. Clicking it closes the property selector and opens the advanced popover directly in its initial empty state — where the user picks the first property inline via a "Where | [Select property ▾]" draft row. When no advanced filters exist yet, clicking this shortcut makes the AdvancedChip appear with the popover already open; closing without adding a filter removes the chip automatically.
958
965
 
966
+ **Dynamic options ("Me", "Unassigned", …):** enum/tags/relation properties accept a `dynamicOptions` array. Each entry is `{ value, label, description?, icon? }` and is rendered at the top of the SingleSelect / MultiSelect dropdown with a divider separating it from the regular options. The `value` is a sentinel string stored on `FilterCondition.value` — the DS only renders, the consuming app resolves it at query time (e.g. `"__me__"` → `currentUser.id`). This keeps session/business logic out of the DS while still getting a consistent visual treatment.
967
+
968
+ ```tsx
969
+ {
970
+ id: "contact_owner",
971
+ label: "Contact owner",
972
+ type: "enum",
973
+ icon: faUserOutline,
974
+ group: "contact",
975
+ groupLabel: "Contact",
976
+ options: ["Quentin", "Simon", "Philippe"],
977
+ dynamicOptions: [
978
+ {
979
+ value: "__me__",
980
+ label: "Me",
981
+ description: "This value is dynamically applied to the current user",
982
+ icon: faBoltOutline,
983
+ },
984
+ { value: "__deactivated__", label: "All deactivated and removed owners", icon: faBoltOutline },
985
+ ],
986
+ }
987
+ ```
988
+
959
989
  | Utility | Purpose |
960
990
  |---|---|
961
991
  | `OPERATORS_BY_TYPE` | Operators for each DataType |
@@ -963,6 +993,13 @@ import {
963
993
  | `createFilterWithDefaults(propertyId, type)` | Creates FilterCondition with default operator |
964
994
  | `isNoValueOperator(op)` | True for is empty / is not empty / is true / is false |
965
995
  | `getValueInputType(type, op)` | Returns the input component name for a type+operator combo |
996
+ | `formatFilterValue(value)` | Stringifies a `FilterValue` for display (handles Date, ranges, arrays, booleans) |
997
+ | `getBadgeCount(value)` | Returns `+N` count for multi-value selections, `undefined` otherwise |
998
+ | `createEmptyGroup()` / `isFilterGroup(node)` | Tree helpers for nested groups |
999
+ | `wrapInGroup(condition)` / `unwrapGroup(group)` | Convert between condition and single-child group |
1000
+ | `duplicateNode(node)` | Deep clone a node tree with fresh UUIDs |
1001
+ | `updateNodeInTree` / `removeNodeFromTree` / `insertAfterInTree` / `replaceNodeInTree` | Immutable tree mutations by id |
1002
+ | `countConditions(nodes)` | Count all leaf conditions in a tree |
966
1003
 
967
1004
  ---
968
1005
 
package/dist/index.d.mts CHANGED
@@ -676,12 +676,28 @@ declare const FilterChip: React.ForwardRefExoticComponent<FilterChipProps & Reac
676
676
  type DataType = "text" | "number" | "date" | "enum" | "tags" | "boolean" | "relation";
677
677
  type TextOperator = "contains" | "does not contain" | "is" | "is not" | "starts with" | "ends with" | "is empty" | "is not empty";
678
678
  type NumberOperator = "=" | "≠" | ">" | "<" | "≥" | "≤" | "is between" | "is empty" | "is not empty";
679
- type DateOperator = "is" | "is before" | "is after" | "is on or before" | "is on or after" | "is between" | "is relative" | "is empty" | "is not empty";
679
+ type DateOperator = "is" | "is before" | "is after" | "is on or before" | "is on or after" | "is between" | "is empty" | "is not empty";
680
680
  type EnumOperator = "is" | "is not" | "is any of" | "is none of" | "is empty" | "is not empty";
681
681
  type TagsOperator = "contains" | "does not contain" | "contains any of" | "contains all of" | "is empty" | "is not empty";
682
682
  type BooleanOperator = "is true" | "is false";
683
683
  type RelationOperator = "is" | "is not" | "is any of" | "is none of" | "is empty" | "is not empty";
684
684
  type OperatorType = TextOperator | NumberOperator | DateOperator | EnumOperator | TagsOperator | BooleanOperator | RelationOperator;
685
+ /**
686
+ * A "smart" / dynamic option shown at the top of a relation or enum selector
687
+ * (e.g. "Me", "My team", "Unassigned"). The value is a sentinel string that
688
+ * the consuming app resolves at query time (e.g. `__me__` → `currentUser.id`).
689
+ *
690
+ * The design system only handles the rendering (icon + label + description +
691
+ * divider above the regular options). Resolving the sentinels is an app
692
+ * concern.
693
+ */
694
+ interface DynamicOption {
695
+ /** Sentinel value stored on FilterCondition.value (e.g. "__me__"). */
696
+ value: string;
697
+ label: string;
698
+ description?: string;
699
+ icon?: _l3mpire_icons.IconDefinition;
700
+ }
685
701
  interface PropertyDefinition {
686
702
  id: string;
687
703
  label: string;
@@ -690,6 +706,12 @@ interface PropertyDefinition {
690
706
  group: string;
691
707
  groupLabel: string;
692
708
  options?: string[];
709
+ /**
710
+ * Dynamic/smart options rendered at the top of the value selector with a
711
+ * divider. Only used for enum, tags, and relation types. The app resolves
712
+ * the sentinel values at query time.
713
+ */
714
+ dynamicOptions?: DynamicOption[];
693
715
  relationConfig?: {
694
716
  entity: string;
695
717
  displayProperty: string;
@@ -701,15 +723,27 @@ type FilterValue = string | number | boolean | Date | [number, number] | [Date,
701
723
  type LogicOperator = "and" | "or";
702
724
  interface FilterCondition {
703
725
  id: string;
726
+ /** Optional discriminator. Defaults to "condition" when absent. */
727
+ type?: "condition";
704
728
  propertyId: string;
705
729
  operator: OperatorType | null;
706
730
  value: FilterValue;
707
- /** Logic connector to the previous filter. Defaults to "and". */
731
+ /** Logic connector to the previous sibling. Defaults to "and". */
732
+ logicOperator?: LogicOperator;
733
+ }
734
+ interface FilterGroup {
735
+ id: string;
736
+ type: "group";
737
+ /** Logic connector to the previous sibling. Defaults to "and". */
708
738
  logicOperator?: LogicOperator;
739
+ children: FilterNode[];
709
740
  }
741
+ /** A filter node is either a single condition or a group of nested nodes. */
742
+ type FilterNode = FilterCondition | FilterGroup;
710
743
  interface FilterState {
711
744
  basicFilters: FilterCondition[];
712
- advancedFilters: FilterCondition[];
745
+ /** Supports nested groups. Each node is either a condition or a group. */
746
+ advancedFilters: FilterNode[];
713
747
  sort: {
714
748
  field: string;
715
749
  direction: "asc" | "desc";
@@ -814,6 +848,8 @@ interface ValueInputProps {
814
848
  onChange: (value: FilterValue) => void;
815
849
  onSubmit?: () => void;
816
850
  options?: string[];
851
+ /** Dynamic/smart entries rendered at the top with a divider. */
852
+ dynamicOptions?: DynamicOption[];
817
853
  className?: string;
818
854
  }
819
855
  declare const ValueInput: React.FC<ValueInputProps>;
@@ -914,13 +950,15 @@ interface AdvancedRowProps {
914
950
  onUpdate: (condition: FilterCondition) => void;
915
951
  onPropertyChange: (property: PropertyDefinition) => void;
916
952
  onDelete: () => void;
953
+ onDuplicate?: () => void;
954
+ onTurnIntoGroup?: () => void;
917
955
  }
918
956
  declare const AdvancedRow: React.FC<AdvancedRowProps>;
919
957
 
920
958
  interface AdvancedPopoverProps {
921
- filters: FilterCondition[];
959
+ filters: FilterNode[];
922
960
  properties: PropertyDefinition[];
923
- onFiltersChange: (filters: FilterCondition[]) => void;
961
+ onFiltersChange: (filters: FilterNode[]) => void;
924
962
  open?: boolean;
925
963
  onOpenChange?: (open: boolean) => void;
926
964
  children?: React.ReactNode;
@@ -929,14 +967,12 @@ declare const AdvancedPopover: React.FC<AdvancedPopoverProps>;
929
967
 
930
968
  interface SummaryChipProps {
931
969
  count: number;
932
- filters: FilterCondition[];
970
+ filters: FilterNode[];
933
971
  properties: PropertyDefinition[];
934
- onFiltersChange: (filters: FilterCondition[]) => void;
972
+ onFiltersChange: (filters: FilterNode[]) => void;
935
973
  onClearAll: () => void;
936
- /** Custom trigger element. When omitted, renders the default "Filters (N)" button. */
937
974
  children?: React.ReactNode;
938
975
  className?: string;
939
- /** Controlled open state. */
940
976
  open?: boolean;
941
977
  onOpenChange?: (open: boolean) => void;
942
978
  }
package/dist/index.d.ts CHANGED
@@ -676,12 +676,28 @@ declare const FilterChip: React.ForwardRefExoticComponent<FilterChipProps & Reac
676
676
  type DataType = "text" | "number" | "date" | "enum" | "tags" | "boolean" | "relation";
677
677
  type TextOperator = "contains" | "does not contain" | "is" | "is not" | "starts with" | "ends with" | "is empty" | "is not empty";
678
678
  type NumberOperator = "=" | "≠" | ">" | "<" | "≥" | "≤" | "is between" | "is empty" | "is not empty";
679
- type DateOperator = "is" | "is before" | "is after" | "is on or before" | "is on or after" | "is between" | "is relative" | "is empty" | "is not empty";
679
+ type DateOperator = "is" | "is before" | "is after" | "is on or before" | "is on or after" | "is between" | "is empty" | "is not empty";
680
680
  type EnumOperator = "is" | "is not" | "is any of" | "is none of" | "is empty" | "is not empty";
681
681
  type TagsOperator = "contains" | "does not contain" | "contains any of" | "contains all of" | "is empty" | "is not empty";
682
682
  type BooleanOperator = "is true" | "is false";
683
683
  type RelationOperator = "is" | "is not" | "is any of" | "is none of" | "is empty" | "is not empty";
684
684
  type OperatorType = TextOperator | NumberOperator | DateOperator | EnumOperator | TagsOperator | BooleanOperator | RelationOperator;
685
+ /**
686
+ * A "smart" / dynamic option shown at the top of a relation or enum selector
687
+ * (e.g. "Me", "My team", "Unassigned"). The value is a sentinel string that
688
+ * the consuming app resolves at query time (e.g. `__me__` → `currentUser.id`).
689
+ *
690
+ * The design system only handles the rendering (icon + label + description +
691
+ * divider above the regular options). Resolving the sentinels is an app
692
+ * concern.
693
+ */
694
+ interface DynamicOption {
695
+ /** Sentinel value stored on FilterCondition.value (e.g. "__me__"). */
696
+ value: string;
697
+ label: string;
698
+ description?: string;
699
+ icon?: _l3mpire_icons.IconDefinition;
700
+ }
685
701
  interface PropertyDefinition {
686
702
  id: string;
687
703
  label: string;
@@ -690,6 +706,12 @@ interface PropertyDefinition {
690
706
  group: string;
691
707
  groupLabel: string;
692
708
  options?: string[];
709
+ /**
710
+ * Dynamic/smart options rendered at the top of the value selector with a
711
+ * divider. Only used for enum, tags, and relation types. The app resolves
712
+ * the sentinel values at query time.
713
+ */
714
+ dynamicOptions?: DynamicOption[];
693
715
  relationConfig?: {
694
716
  entity: string;
695
717
  displayProperty: string;
@@ -701,15 +723,27 @@ type FilterValue = string | number | boolean | Date | [number, number] | [Date,
701
723
  type LogicOperator = "and" | "or";
702
724
  interface FilterCondition {
703
725
  id: string;
726
+ /** Optional discriminator. Defaults to "condition" when absent. */
727
+ type?: "condition";
704
728
  propertyId: string;
705
729
  operator: OperatorType | null;
706
730
  value: FilterValue;
707
- /** Logic connector to the previous filter. Defaults to "and". */
731
+ /** Logic connector to the previous sibling. Defaults to "and". */
732
+ logicOperator?: LogicOperator;
733
+ }
734
+ interface FilterGroup {
735
+ id: string;
736
+ type: "group";
737
+ /** Logic connector to the previous sibling. Defaults to "and". */
708
738
  logicOperator?: LogicOperator;
739
+ children: FilterNode[];
709
740
  }
741
+ /** A filter node is either a single condition or a group of nested nodes. */
742
+ type FilterNode = FilterCondition | FilterGroup;
710
743
  interface FilterState {
711
744
  basicFilters: FilterCondition[];
712
- advancedFilters: FilterCondition[];
745
+ /** Supports nested groups. Each node is either a condition or a group. */
746
+ advancedFilters: FilterNode[];
713
747
  sort: {
714
748
  field: string;
715
749
  direction: "asc" | "desc";
@@ -814,6 +848,8 @@ interface ValueInputProps {
814
848
  onChange: (value: FilterValue) => void;
815
849
  onSubmit?: () => void;
816
850
  options?: string[];
851
+ /** Dynamic/smart entries rendered at the top with a divider. */
852
+ dynamicOptions?: DynamicOption[];
817
853
  className?: string;
818
854
  }
819
855
  declare const ValueInput: React.FC<ValueInputProps>;
@@ -914,13 +950,15 @@ interface AdvancedRowProps {
914
950
  onUpdate: (condition: FilterCondition) => void;
915
951
  onPropertyChange: (property: PropertyDefinition) => void;
916
952
  onDelete: () => void;
953
+ onDuplicate?: () => void;
954
+ onTurnIntoGroup?: () => void;
917
955
  }
918
956
  declare const AdvancedRow: React.FC<AdvancedRowProps>;
919
957
 
920
958
  interface AdvancedPopoverProps {
921
- filters: FilterCondition[];
959
+ filters: FilterNode[];
922
960
  properties: PropertyDefinition[];
923
- onFiltersChange: (filters: FilterCondition[]) => void;
961
+ onFiltersChange: (filters: FilterNode[]) => void;
924
962
  open?: boolean;
925
963
  onOpenChange?: (open: boolean) => void;
926
964
  children?: React.ReactNode;
@@ -929,14 +967,12 @@ declare const AdvancedPopover: React.FC<AdvancedPopoverProps>;
929
967
 
930
968
  interface SummaryChipProps {
931
969
  count: number;
932
- filters: FilterCondition[];
970
+ filters: FilterNode[];
933
971
  properties: PropertyDefinition[];
934
- onFiltersChange: (filters: FilterCondition[]) => void;
972
+ onFiltersChange: (filters: FilterNode[]) => void;
935
973
  onClearAll: () => void;
936
- /** Custom trigger element. When omitted, renders the default "Filters (N)" button. */
937
974
  children?: React.ReactNode;
938
975
  className?: string;
939
- /** Controlled open state. */
940
976
  open?: boolean;
941
977
  onOpenChange?: (open: boolean) => void;
942
978
  }