@entifix/react-controls 0.1.4 → 0.3.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.
Files changed (43) hide show
  1. package/dist/i18n/catalog/en.d.ts.map +1 -1
  2. package/dist/i18n/catalog/en.js +7 -1
  3. package/dist/i18n/catalog/en.js.map +1 -1
  4. package/dist/i18n/catalog/es.d.ts +6 -0
  5. package/dist/i18n/catalog/es.d.ts.map +1 -1
  6. package/dist/i18n/catalog/es.js +7 -1
  7. package/dist/i18n/catalog/es.js.map +1 -1
  8. package/dist/i18n/catalog/index.d.ts +12 -0
  9. package/dist/i18n/catalog/index.d.ts.map +1 -1
  10. package/dist/i18n/i18n-context.d.ts.map +1 -1
  11. package/dist/i18n/i18n-context.js +15 -7
  12. package/dist/i18n/i18n-context.js.map +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/preferences/ui-preferences-state.d.ts +1 -1
  18. package/dist/preferences/ui-preferences-state.d.ts.map +1 -1
  19. package/dist/preferences/ui-preferences-state.js +1 -1
  20. package/dist/preferences/ui-preferences-state.js.map +1 -1
  21. package/dist/ui/atoms/cell-value/cell-value.d.ts.map +1 -1
  22. package/dist/ui/atoms/cell-value/cell-value.js +20 -11
  23. package/dist/ui/atoms/cell-value/cell-value.js.map +1 -1
  24. package/dist/ui/atoms/localized-field-control/index.d.ts +2 -0
  25. package/dist/ui/atoms/localized-field-control/index.d.ts.map +1 -0
  26. package/dist/ui/atoms/localized-field-control/index.js +3 -0
  27. package/dist/ui/atoms/localized-field-control/index.js.map +1 -0
  28. package/dist/ui/atoms/localized-field-control/localized-field-control.d.ts +24 -0
  29. package/dist/ui/atoms/localized-field-control/localized-field-control.d.ts.map +1 -0
  30. package/dist/ui/atoms/localized-field-control/localized-field-control.js +47 -0
  31. package/dist/ui/atoms/localized-field-control/localized-field-control.js.map +1 -0
  32. package/dist/ui/molecules/filter-builder/filter-builder.d.ts +1 -1
  33. package/dist/ui/molecules/filter-builder/filter-builder.d.ts.map +1 -1
  34. package/dist/ui/molecules/filter-builder/filter-builder.js +15 -8
  35. package/dist/ui/molecules/filter-builder/filter-builder.js.map +1 -1
  36. package/dist/ui/molecules/sort-builder/sort-builder.d.ts +1 -1
  37. package/dist/ui/molecules/sort-builder/sort-builder.d.ts.map +1 -1
  38. package/dist/ui/molecules/sort-builder/sort-builder.js +8 -4
  39. package/dist/ui/molecules/sort-builder/sort-builder.js.map +1 -1
  40. package/dist/ui/organisms/entity-form/entity-form.d.ts.map +1 -1
  41. package/dist/ui/organisms/entity-form/entity-form.js +9 -1
  42. package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
  43. package/package.json +7 -10
@@ -0,0 +1,24 @@
1
+ import { type EntityFieldDescriptor, type EntityLocalizedDraft } from '@entifix/core';
2
+ export interface LocalizedFieldControlProps {
3
+ descriptor: EntityFieldDescriptor;
4
+ /** The draft: one text per locale. A locale with no entry renders empty. */
5
+ value: EntityLocalizedDraft;
6
+ onChange: (value: EntityLocalizedDraft) => void;
7
+ /** The first input's id, so the row's `<label for>` lands on it. */
8
+ id?: string;
9
+ 'aria-invalid'?: boolean;
10
+ }
11
+ /**
12
+ * The editor for a `localized` member: one text input per locale the host
13
+ * declared, in declaration order, each marked with its locale code.
14
+ *
15
+ * Every locale is always on screen. A translation is part of the record, not an
16
+ * afterthought behind a switcher, and an input left empty is exactly what the
17
+ * form's completeness rule will point at.
18
+ *
19
+ * Its own atom rather than a branch of `FieldControl`, because its value is a
20
+ * map and not a string: `FieldControl` is the string-draft control, and keeping
21
+ * it so keeps every other member's contract unchanged.
22
+ */
23
+ export declare function LocalizedFieldControl({ descriptor, value, onChange, id, 'aria-invalid': invalid, }: LocalizedFieldControlProps): import("react").JSX.Element;
24
+ //# sourceMappingURL=localized-field-control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localized-field-control.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/localized-field-control/localized-field-control.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EAE1B,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,qBAAqB,CAAC;IAClC,4EAA4E;IAC5E,KAAK,EAAE,oBAAoB,CAAC;IAC5B,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChD,oEAAoE;IACpE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,UAAU,EACV,KAAK,EACL,QAAQ,EACR,EAAE,EACF,cAAc,EAAE,OAAO,GACxB,EAAE,0BAA0B,+BA0B5B"}
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+ import { _ as _extends } from "@swc/helpers/_/_extends";
3
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
+ import { locales } from '@entifix/core';
5
+ import { TextInput } from '../field/index.js';
6
+ /**
7
+ * The editor for a `localized` member: one text input per locale the host
8
+ * declared, in declaration order, each marked with its locale code.
9
+ *
10
+ * Every locale is always on screen. A translation is part of the record, not an
11
+ * afterthought behind a switcher, and an input left empty is exactly what the
12
+ * form's completeness rule will point at.
13
+ *
14
+ * Its own atom rather than a branch of `FieldControl`, because its value is a
15
+ * map and not a string: `FieldControl` is the string-draft control, and keeping
16
+ * it so keeps every other member's contract unchanged.
17
+ */ export function LocalizedFieldControl({ descriptor, value, onChange, id, 'aria-invalid': invalid }) {
18
+ return /*#__PURE__*/ _jsx("div", {
19
+ className: "flex flex-col gap-3xs",
20
+ children: locales().map((locale, index)=>{
21
+ var _value_locale;
22
+ return /*#__PURE__*/ _jsxs("div", {
23
+ className: "flex items-center gap-2xs",
24
+ children: [
25
+ /*#__PURE__*/ _jsx("span", {
26
+ "aria-hidden": true,
27
+ className: "w-8 shrink-0 text-step-sm text-content-muted uppercase",
28
+ children: locale
29
+ }),
30
+ /*#__PURE__*/ _jsx(TextInput, {
31
+ id: index === 0 ? id : id && `${id}-${locale}`,
32
+ "aria-label": `${descriptor.label} (${locale})`,
33
+ "aria-invalid": invalid,
34
+ lang: locale,
35
+ value: (_value_locale = value[locale]) != null ? _value_locale : '',
36
+ disabled: descriptor.readonly,
37
+ onChange: (event)=>onChange(_extends({}, value, {
38
+ [locale]: event.currentTarget.value
39
+ }))
40
+ })
41
+ ]
42
+ }, locale);
43
+ })
44
+ });
45
+ }
46
+
47
+ //# sourceMappingURL=localized-field-control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/ui/atoms/localized-field-control/localized-field-control.tsx"],"sourcesContent":["'use client';\n\nimport {\n type EntityFieldDescriptor,\n type EntityLocalizedDraft,\n locales,\n} from '@entifix/core';\n\nimport { TextInput } from '../field/index.js';\n\nexport interface LocalizedFieldControlProps {\n descriptor: EntityFieldDescriptor;\n /** The draft: one text per locale. A locale with no entry renders empty. */\n value: EntityLocalizedDraft;\n onChange: (value: EntityLocalizedDraft) => void;\n /** The first input's id, so the row's `<label for>` lands on it. */\n id?: string;\n 'aria-invalid'?: boolean;\n}\n\n/**\n * The editor for a `localized` member: one text input per locale the host\n * declared, in declaration order, each marked with its locale code.\n *\n * Every locale is always on screen. A translation is part of the record, not an\n * afterthought behind a switcher, and an input left empty is exactly what the\n * form's completeness rule will point at.\n *\n * Its own atom rather than a branch of `FieldControl`, because its value is a\n * map and not a string: `FieldControl` is the string-draft control, and keeping\n * it so keeps every other member's contract unchanged.\n */\nexport function LocalizedFieldControl({\n descriptor,\n value,\n onChange,\n id,\n 'aria-invalid': invalid,\n}: LocalizedFieldControlProps) {\n return (\n <div className=\"flex flex-col gap-3xs\">\n {locales().map((locale, index) => (\n <div key={locale} className=\"flex items-center gap-2xs\">\n <span\n aria-hidden\n className=\"w-8 shrink-0 text-step-sm text-content-muted uppercase\"\n >\n {locale}\n </span>\n <TextInput\n id={index === 0 ? id : id && `${id}-${locale}`}\n aria-label={`${descriptor.label} (${locale})`}\n aria-invalid={invalid}\n lang={locale}\n value={value[locale] ?? ''}\n disabled={descriptor.readonly}\n onChange={event =>\n onChange({ ...value, [locale]: event.currentTarget.value })\n }\n />\n </div>\n ))}\n </div>\n );\n}\n"],"names":["locales","TextInput","LocalizedFieldControl","descriptor","value","onChange","id","invalid","div","className","map","locale","index","span","aria-hidden","aria-label","label","aria-invalid","lang","disabled","readonly","event","currentTarget"],"mappings":"AAAA;;;AAEA,SAGEA,OAAO,QACF,gBAAgB;AAEvB,SAASC,SAAS,QAAQ,oBAAoB;AAY9C;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,sBAAsB,EACpCC,UAAU,EACVC,KAAK,EACLC,QAAQ,EACRC,EAAE,EACF,gBAAgBC,OAAO,EACI;IAC3B,qBACE,KAACC;QAAIC,WAAU;kBACZT,UAAUU,GAAG,CAAC,CAACC,QAAQC;gBAaXR;iCAZX,MAACI;gBAAiBC,WAAU;;kCAC1B,KAACI;wBACCC,aAAW;wBACXL,WAAU;kCAETE;;kCAEH,KAACV;wBACCK,IAAIM,UAAU,IAAIN,KAAKA,MAAM,GAAGA,GAAG,CAAC,EAAEK,QAAQ;wBAC9CI,cAAY,GAAGZ,WAAWa,KAAK,CAAC,EAAE,EAAEL,OAAO,CAAC,CAAC;wBAC7CM,gBAAcV;wBACdW,MAAMP;wBACNP,KAAK,GAAEA,gBAAAA,KAAK,CAACO,OAAO,YAAbP,gBAAiB;wBACxBe,UAAUhB,WAAWiB,QAAQ;wBAC7Bf,UAAUgB,CAAAA,QACRhB,SAAS,aAAKD;gCAAO,CAACO,OAAO,EAAEU,MAAMC,aAAa,CAAClB,KAAK;;;;eAfpDO;;;AAsBlB"}
@@ -1,4 +1,4 @@
1
- import type { Entity, EntityFieldDescriptor, FilterGroup } from '@entifix/core';
1
+ import { type Entity, type EntityFieldDescriptor, type FilterGroup } from '@entifix/core';
2
2
  export interface FilterBuilderProps<TEntity extends Entity> {
3
3
  /** Filterable members, already resolved from the entity metadata. */
4
4
  descriptors: readonly EntityFieldDescriptor[];
@@ -1 +1 @@
1
- {"version":3,"file":"filter-builder.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/filter-builder/filter-builder.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,MAAM,EACN,qBAAqB,EAErB,WAAW,EAGZ,MAAM,eAAe,CAAC;AAwIvB,MAAM,WAAW,kBAAkB,CAAC,OAAO,SAAS,MAAM;IACxD,qEAAqE;IACrE,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC9C,8DAA8D;IAC9D,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAAE,EACpD,WAAW,EACX,KAAK,EAAE,OAAO,EACd,QAAQ,GACT,EAAE,kBAAkB,CAAC,OAAO,CAAC,+BA4L7B"}
1
+ {"version":3,"file":"filter-builder.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/filter-builder/filter-builder.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,qBAAqB,EAE1B,KAAK,WAAW,EAMjB,MAAM,eAAe,CAAC;AA+IvB,MAAM,WAAW,kBAAkB,CAAC,OAAO,SAAS,MAAM;IACxD,qEAAqE;IACrE,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC9C,8DAA8D;IAC9D,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAAE,EACpD,WAAW,EACX,KAAK,EAAE,OAAO,EACd,QAAQ,GACT,EAAE,kBAAkB,CAAC,OAAO,CAAC,+BA+L7B"}
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
  import { _ as _extends } from "@swc/helpers/_/_extends";
3
3
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
4
+ import { findQueriedDescriptor, queryPropertyFor } from '@entifix/core';
4
5
  import { useCallback, useId, useState } from 'react';
5
- import { useEnumLabel, useT } from '../../../i18n/index.js';
6
+ import { useEnumLabel, useLocale, useT } from '../../../i18n/index.js';
6
7
  import { Button } from '../../atoms/button/index.js';
7
8
  import { Select, TextInput } from '../../atoms/field/index.js';
8
9
  import { Text } from '../../atoms/text/index.js';
@@ -23,8 +24,9 @@ function coerce(raw, type) {
23
24
  /**
24
25
  * Converts a draft row into the wire filter, or `undefined` while the row is
25
26
  * still incomplete — a half-typed row must not reach the load request.
26
- */ function toFilter(draft, descriptor) {
27
- const property = descriptor.key;
27
+ */ function toFilter(draft, descriptor, locale) {
28
+ // A localized member is filtered in the reader's own locale.
29
+ const property = queryPropertyFor(descriptor, locale);
28
30
  switch(operatorArity(draft.operator)){
29
31
  case 'none':
30
32
  return {
@@ -67,17 +69,20 @@ function coerce(raw, type) {
67
69
  * panel shows what is actually in effect rather than an empty form. Only the
68
70
  * flat rows this builder can express are restored — a group nested by some
69
71
  * other means is left to the caller to re-enter.
70
- */ function toDrafts(filtering, idPrefix) {
72
+ */ function toDrafts(filtering, idPrefix, descriptors) {
71
73
  if (!filtering) return [];
72
74
  return filtering.values.flatMap((node, index)=>{
73
- var _filter_start;
75
+ var _ref, _filter_start;
76
+ var _findQueriedDescriptor;
74
77
  if (!('property' in node)) return [];
75
78
  const filter = node;
76
79
  const text = (value)=>value instanceof Date ? value.toISOString().slice(0, 10) : value === undefined ? '' : String(value);
77
80
  return [
78
81
  {
79
82
  key: `${idPrefix}-applied-${index}`,
80
- property: String(filter.property),
83
+ // Rows address members by accessor name; an applied filter carries the
84
+ // wire key, or a locale path into a localized member.
85
+ property: (_ref = (_findQueriedDescriptor = findQueriedDescriptor(descriptors, String(filter.property))) == null ? void 0 : _findQueriedDescriptor.name) != null ? _ref : String(filter.property),
81
86
  operator: filter.operator,
82
87
  value: filter.values ? filter.values.map(text).join(', ') : text((_filter_start = filter.start) != null ? _filter_start : filter.value),
83
88
  end: text(filter.end)
@@ -98,14 +103,15 @@ function coerce(raw, type) {
98
103
  const t = useT();
99
104
  const idPrefix = useId();
100
105
  const [logic, setLogic] = useState((_ref = applied == null ? void 0 : applied.operator) != null ? _ref : 'and');
101
- const [drafts, setDrafts] = useState(()=>toDrafts(applied, idPrefix));
106
+ const [drafts, setDrafts] = useState(()=>toDrafts(applied, idPrefix, descriptors));
107
+ const locale = useLocale();
102
108
  const descriptorFor = useCallback((name)=>descriptors.find((entry)=>entry.name === name), [
103
109
  descriptors
104
110
  ]);
105
111
  const apply = useCallback((nextDrafts, nextLogic)=>{
106
112
  const values = nextDrafts.map((draft)=>{
107
113
  const descriptor = descriptorFor(draft.property);
108
- return descriptor ? toFilter(draft, descriptor) : undefined;
114
+ return descriptor ? toFilter(draft, descriptor, locale) : undefined;
109
115
  }).filter((filter)=>filter !== undefined);
110
116
  onChange({
111
117
  operator: nextLogic,
@@ -113,6 +119,7 @@ function coerce(raw, type) {
113
119
  });
114
120
  }, [
115
121
  descriptorFor,
122
+ locale,
116
123
  onChange
117
124
  ]);
118
125
  /** Edits the draft only — nothing reaches the caller until Apply. */ const update = (nextDrafts)=>setDrafts(nextDrafts);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/filter-builder/filter-builder.tsx"],"sourcesContent":["'use client';\n\nimport type {\n Entity,\n EntityFieldDescriptor,\n EntityFilter,\n FilterGroup,\n LogicOperator,\n MetaAccessorType,\n} from '@entifix/core';\nimport { useCallback, useId, useState } from 'react';\n\nimport { useEnumLabel, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { Select, TextInput } from '../../atoms/field/index.js';\nimport { Text } from '../../atoms/text/index.js';\nimport {\n type EntityFilterOperator,\n operatorArity,\n operatorLabelKey,\n operatorsForType,\n} from './filter-operators.js';\n\n/**\n * One in-progress filter row. Values are held as strings because that is what\n * the inputs produce; they are coerced to the member's type only when the row\n * is converted into an {@link EntityFilter}.\n */\ninterface FilterDraft {\n key: string;\n property: string;\n operator: EntityFilterOperator;\n value: string;\n end: string;\n}\n\nfunction coerce(raw: string, type: MetaAccessorType): unknown {\n if (raw === '') return undefined;\n switch (type) {\n case 'number':\n return Number(raw);\n case 'boolean':\n return raw === 'true';\n case 'date':\n return new Date(raw);\n default:\n return raw;\n }\n}\n\n/**\n * Converts a draft row into the wire filter, or `undefined` while the row is\n * still incomplete — a half-typed row must not reach the load request.\n */\nfunction toFilter<TEntity extends Entity>(\n draft: FilterDraft,\n descriptor: EntityFieldDescriptor,\n): EntityFilter<TEntity> | undefined {\n const property = descriptor.key as keyof TEntity;\n\n switch (operatorArity(draft.operator)) {\n case 'none':\n return { property, operator: draft.operator } as EntityFilter<TEntity>;\n case 'list': {\n const values = draft.value\n .split(',')\n .map(entry => entry.trim())\n .filter(entry => entry !== '')\n .map(entry => coerce(entry, descriptor.type));\n return values.length === 0\n ? undefined\n : ({\n property,\n operator: draft.operator,\n values,\n } as EntityFilter<TEntity>);\n }\n case 'range': {\n const start = coerce(draft.value, descriptor.type);\n const end = coerce(draft.end, descriptor.type);\n return start === undefined || end === undefined\n ? undefined\n : ({\n property,\n operator: draft.operator,\n start,\n end,\n } as EntityFilter<TEntity>);\n }\n default: {\n const value = coerce(draft.value, descriptor.type);\n return value === undefined\n ? undefined\n : ({\n property,\n operator: draft.operator,\n value,\n } as EntityFilter<TEntity>);\n }\n }\n}\n\n/**\n * Rebuilds the editable rows from an already-applied filter, so reopening the\n * panel shows what is actually in effect rather than an empty form. Only the\n * flat rows this builder can express are restored — a group nested by some\n * other means is left to the caller to re-enter.\n */\nfunction toDrafts<TEntity extends Entity>(\n filtering: FilterGroup<TEntity> | undefined,\n idPrefix: string,\n): FilterDraft[] {\n if (!filtering) return [];\n\n return filtering.values.flatMap((node, index) => {\n if (!('property' in node)) return [];\n const filter = node as {\n property: unknown;\n operator: EntityFilterOperator;\n value?: unknown;\n values?: unknown[];\n start?: unknown;\n end?: unknown;\n };\n const text = (value: unknown) =>\n value instanceof Date\n ? value.toISOString().slice(0, 10)\n : value === undefined\n ? ''\n : String(value);\n\n return [\n {\n key: `${idPrefix}-applied-${index}`,\n property: String(filter.property),\n operator: filter.operator,\n value: filter.values\n ? filter.values.map(text).join(', ')\n : text(filter.start ?? filter.value),\n end: text(filter.end),\n },\n ];\n });\n}\n\nexport interface FilterBuilderProps<TEntity extends Entity> {\n /** Filterable members, already resolved from the entity metadata. */\n descriptors: readonly EntityFieldDescriptor[];\n /** The filtering currently applied, used to seed the rows. */\n value?: FilterGroup<TEntity>;\n onChange: (filtering: FilterGroup<TEntity>) => void;\n}\n\n/**\n * Builds an `EntityFiltering` value from entity metadata: the member list, the\n * operators offered and the shape of the value control all come from each\n * member's descriptor, so no per-entity filter UI has to be written.\n *\n * Editing is a draft. `onChange` fires only on **Apply**, because the value\n * feeds a load request: emitting per keystroke would put one HTTP request on\n * the wire per character typed.\n */\nexport function FilterBuilder<TEntity extends Entity>({\n descriptors,\n value: applied,\n onChange,\n}: FilterBuilderProps<TEntity>) {\n const t = useT();\n const idPrefix = useId();\n const [logic, setLogic] = useState<LogicOperator>(applied?.operator ?? 'and');\n const [drafts, setDrafts] = useState<FilterDraft[]>(() =>\n toDrafts(applied, idPrefix),\n );\n\n const descriptorFor = useCallback(\n (name: string) => descriptors.find(entry => entry.name === name),\n [descriptors],\n );\n\n const apply = useCallback(\n (nextDrafts: FilterDraft[], nextLogic: LogicOperator) => {\n const values = nextDrafts\n .map(draft => {\n const descriptor = descriptorFor(draft.property);\n return descriptor ? toFilter<TEntity>(draft, descriptor) : undefined;\n })\n .filter(\n (filter): filter is EntityFilter<TEntity> => filter !== undefined,\n );\n\n onChange({ operator: nextLogic, values });\n },\n [descriptorFor, onChange],\n );\n\n /** Edits the draft only — nothing reaches the caller until Apply. */\n const update = (nextDrafts: FilterDraft[]) => setDrafts(nextDrafts);\n\n const addRow = () => {\n const first = descriptors[0];\n // Defence in depth: the component returns early for an empty descriptor\n // list, so this button does not exist without a first member.\n /* v8 ignore next */\n if (!first) return;\n update([\n ...drafts,\n {\n key: `${idPrefix}-${drafts.length}-${Date.now()}`,\n property: first.name,\n operator: operatorsForType(first.type)[0],\n value: '',\n end: '',\n },\n ]);\n };\n\n const patch = (key: string, changes: Partial<FilterDraft>) =>\n update(\n drafts.map(draft =>\n draft.key === key ? { ...draft, ...changes } : draft,\n ),\n );\n\n if (descriptors.length === 0) {\n return (\n <Text step={-1} tone=\"muted\">\n {t('filter.none')}\n </Text>\n );\n }\n\n return (\n <div className=\"flex flex-col gap-2xs\">\n <div className=\"flex items-center gap-2xs\">\n <Text step={-2} tone=\"muted\">\n {t('filter.match')}\n </Text>\n <Select\n aria-label={t('filter.matchAria')}\n value={logic}\n onChange={event => {\n setLogic(event.target.value as LogicOperator);\n }}\n >\n <option value=\"and\">{t('filter.all')}</option>\n <option value=\"or\">{t('filter.any')}</option>\n </Select>\n </div>\n\n {drafts.map(draft => {\n const descriptor = descriptorFor(draft.property);\n if (!descriptor) return null;\n const arity = operatorArity(draft.operator);\n\n return (\n <div key={draft.key} className=\"flex flex-wrap items-center gap-2xs\">\n <Select\n aria-label={t('filter.member')}\n value={draft.property}\n onChange={event => {\n const next = descriptorFor(event.target.value);\n // Defence in depth: the options are built from `descriptors`,\n // so a chosen value always resolves.\n /* v8 ignore next */\n if (!next) return;\n patch(draft.key, {\n property: next.name,\n // The previous operator may not exist for the new type.\n operator: operatorsForType(next.type)[0],\n value: '',\n end: '',\n });\n }}\n >\n {descriptors.map(entry => (\n <option key={entry.name} value={entry.name}>\n {entry.label}\n </option>\n ))}\n </Select>\n\n <Select\n aria-label={t('filter.operator')}\n value={draft.operator}\n onChange={event =>\n patch(draft.key, {\n operator: event.target.value as EntityFilterOperator,\n })\n }\n >\n {operatorsForType(descriptor.type).map(operator => (\n <option key={operator} value={operator}>\n {t(operatorLabelKey(operator))}\n </option>\n ))}\n </Select>\n\n {arity !== 'none' && (\n <FilterValueInput\n descriptor={descriptor}\n arity={arity}\n value={draft.value}\n end={draft.end}\n onValueChange={value => patch(draft.key, { value })}\n onEndChange={end => patch(draft.key, { end })}\n />\n )}\n\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('filter.remove')}\n onClick={() =>\n update(drafts.filter(entry => entry.key !== draft.key))\n }\n >\n ✕\n </Button>\n </div>\n );\n })}\n\n <div className=\"flex flex-wrap items-center gap-2xs\">\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={addRow}>\n {t('filter.add')}\n </Button>\n <Button\n type=\"button\"\n variant=\"primary\"\n size=\"sm\"\n aria-label={t('filter.applyAria')}\n onClick={() => apply(drafts, logic)}\n >\n {t('filter.apply')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('filter.clearAria')}\n onClick={() => {\n setDrafts([]);\n setLogic('and');\n // Applying the emptied form is what actually clears the listing;\n // dropping the rows alone would leave the old filter in effect.\n apply([], 'and');\n }}\n >\n {t('filter.clear')}\n </Button>\n </div>\n </div>\n );\n}\n\ninterface FilterValueInputProps {\n descriptor: EntityFieldDescriptor;\n arity: ReturnType<typeof operatorArity>;\n value: string;\n end: string;\n onValueChange: (value: string) => void;\n onEndChange: (end: string) => void;\n}\n\n/** The value control the member's type calls for. */\nfunction FilterValueInput({\n descriptor,\n arity,\n value,\n end,\n onValueChange,\n onEndChange,\n}: FilterValueInputProps) {\n const t = useT();\n const enumLabel = useEnumLabel();\n\n if (descriptor.type === 'boolean') {\n return (\n <Select\n aria-label={t('filter.value')}\n value={value}\n onChange={event => onValueChange(event.target.value)}\n >\n <option value=\"\">—</option>\n <option value=\"true\">{t('value.yes')}</option>\n <option value=\"false\">{t('value.no')}</option>\n </Select>\n );\n }\n\n if (descriptor.type === 'enum' && descriptor.enumValues && arity !== 'list') {\n return (\n <Select\n aria-label={t('filter.value')}\n value={value}\n onChange={event => onValueChange(event.target.value)}\n >\n <option value=\"\">—</option>\n {descriptor.enumValues.map(option => (\n <option key={option} value={option}>\n {enumLabel(descriptor, option)}\n </option>\n ))}\n </Select>\n );\n }\n\n const inputType =\n descriptor.type === 'number'\n ? 'number'\n : descriptor.type === 'date'\n ? 'date'\n : 'text';\n\n return (\n <>\n <TextInput\n aria-label={t('filter.value')}\n type={arity === 'list' ? 'text' : inputType}\n placeholder={arity === 'list' ? t('filter.listPlaceholder') : undefined}\n value={value}\n onChange={event => onValueChange(event.target.value)}\n />\n {arity === 'range' && (\n <TextInput\n aria-label={t('filter.rangeEnd')}\n type={inputType}\n value={end}\n onChange={event => onEndChange(event.target.value)}\n />\n )}\n </>\n );\n}\n"],"names":["useCallback","useId","useState","useEnumLabel","useT","Button","Select","TextInput","Text","operatorArity","operatorLabelKey","operatorsForType","coerce","raw","type","undefined","Number","Date","toFilter","draft","descriptor","property","key","operator","values","value","split","map","entry","trim","filter","length","start","end","toDrafts","filtering","idPrefix","flatMap","node","index","text","toISOString","slice","String","join","FilterBuilder","descriptors","applied","onChange","t","logic","setLogic","drafts","setDrafts","descriptorFor","name","find","apply","nextDrafts","nextLogic","update","addRow","first","now","patch","changes","step","tone","div","className","aria-label","event","target","option","arity","next","label","FilterValueInput","onValueChange","onEndChange","variant","size","onClick","enumLabel","enumValues","inputType","placeholder"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/filter-builder/filter-builder.tsx"],"sourcesContent":["'use client';\n\nimport {\n type Entity,\n type EntityFieldDescriptor,\n type EntityFilter,\n type FilterGroup,\n findQueriedDescriptor,\n type Locale,\n type LogicOperator,\n type MetaAccessorType,\n queryPropertyFor,\n} from '@entifix/core';\nimport { useCallback, useId, useState } from 'react';\n\nimport { useEnumLabel, useLocale, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { Select, TextInput } from '../../atoms/field/index.js';\nimport { Text } from '../../atoms/text/index.js';\nimport {\n type EntityFilterOperator,\n operatorArity,\n operatorLabelKey,\n operatorsForType,\n} from './filter-operators.js';\n\n/**\n * One in-progress filter row. Values are held as strings because that is what\n * the inputs produce; they are coerced to the member's type only when the row\n * is converted into an {@link EntityFilter}.\n */\ninterface FilterDraft {\n key: string;\n property: string;\n operator: EntityFilterOperator;\n value: string;\n end: string;\n}\n\nfunction coerce(raw: string, type: MetaAccessorType): unknown {\n if (raw === '') return undefined;\n switch (type) {\n case 'number':\n return Number(raw);\n case 'boolean':\n return raw === 'true';\n case 'date':\n return new Date(raw);\n default:\n return raw;\n }\n}\n\n/**\n * Converts a draft row into the wire filter, or `undefined` while the row is\n * still incomplete — a half-typed row must not reach the load request.\n */\nfunction toFilter<TEntity extends Entity>(\n draft: FilterDraft,\n descriptor: EntityFieldDescriptor,\n locale: Locale,\n): EntityFilter<TEntity> | undefined {\n // A localized member is filtered in the reader's own locale.\n const property = queryPropertyFor(descriptor, locale) as keyof TEntity;\n\n switch (operatorArity(draft.operator)) {\n case 'none':\n return { property, operator: draft.operator } as EntityFilter<TEntity>;\n case 'list': {\n const values = draft.value\n .split(',')\n .map(entry => entry.trim())\n .filter(entry => entry !== '')\n .map(entry => coerce(entry, descriptor.type));\n return values.length === 0\n ? undefined\n : ({\n property,\n operator: draft.operator,\n values,\n } as EntityFilter<TEntity>);\n }\n case 'range': {\n const start = coerce(draft.value, descriptor.type);\n const end = coerce(draft.end, descriptor.type);\n return start === undefined || end === undefined\n ? undefined\n : ({\n property,\n operator: draft.operator,\n start,\n end,\n } as EntityFilter<TEntity>);\n }\n default: {\n const value = coerce(draft.value, descriptor.type);\n return value === undefined\n ? undefined\n : ({\n property,\n operator: draft.operator,\n value,\n } as EntityFilter<TEntity>);\n }\n }\n}\n\n/**\n * Rebuilds the editable rows from an already-applied filter, so reopening the\n * panel shows what is actually in effect rather than an empty form. Only the\n * flat rows this builder can express are restored — a group nested by some\n * other means is left to the caller to re-enter.\n */\nfunction toDrafts<TEntity extends Entity>(\n filtering: FilterGroup<TEntity> | undefined,\n idPrefix: string,\n descriptors: readonly EntityFieldDescriptor[],\n): FilterDraft[] {\n if (!filtering) return [];\n\n return filtering.values.flatMap((node, index) => {\n if (!('property' in node)) return [];\n const filter = node as {\n property: unknown;\n operator: EntityFilterOperator;\n value?: unknown;\n values?: unknown[];\n start?: unknown;\n end?: unknown;\n };\n const text = (value: unknown) =>\n value instanceof Date\n ? value.toISOString().slice(0, 10)\n : value === undefined\n ? ''\n : String(value);\n\n return [\n {\n key: `${idPrefix}-applied-${index}`,\n // Rows address members by accessor name; an applied filter carries the\n // wire key, or a locale path into a localized member.\n property:\n findQueriedDescriptor(descriptors, String(filter.property))?.name ??\n String(filter.property),\n operator: filter.operator,\n value: filter.values\n ? filter.values.map(text).join(', ')\n : text(filter.start ?? filter.value),\n end: text(filter.end),\n },\n ];\n });\n}\n\nexport interface FilterBuilderProps<TEntity extends Entity> {\n /** Filterable members, already resolved from the entity metadata. */\n descriptors: readonly EntityFieldDescriptor[];\n /** The filtering currently applied, used to seed the rows. */\n value?: FilterGroup<TEntity>;\n onChange: (filtering: FilterGroup<TEntity>) => void;\n}\n\n/**\n * Builds an `EntityFiltering` value from entity metadata: the member list, the\n * operators offered and the shape of the value control all come from each\n * member's descriptor, so no per-entity filter UI has to be written.\n *\n * Editing is a draft. `onChange` fires only on **Apply**, because the value\n * feeds a load request: emitting per keystroke would put one HTTP request on\n * the wire per character typed.\n */\nexport function FilterBuilder<TEntity extends Entity>({\n descriptors,\n value: applied,\n onChange,\n}: FilterBuilderProps<TEntity>) {\n const t = useT();\n const idPrefix = useId();\n const [logic, setLogic] = useState<LogicOperator>(applied?.operator ?? 'and');\n const [drafts, setDrafts] = useState<FilterDraft[]>(() =>\n toDrafts(applied, idPrefix, descriptors),\n );\n const locale = useLocale();\n\n const descriptorFor = useCallback(\n (name: string) => descriptors.find(entry => entry.name === name),\n [descriptors],\n );\n\n const apply = useCallback(\n (nextDrafts: FilterDraft[], nextLogic: LogicOperator) => {\n const values = nextDrafts\n .map(draft => {\n const descriptor = descriptorFor(draft.property);\n return descriptor\n ? toFilter<TEntity>(draft, descriptor, locale)\n : undefined;\n })\n .filter(\n (filter): filter is EntityFilter<TEntity> => filter !== undefined,\n );\n\n onChange({ operator: nextLogic, values });\n },\n [descriptorFor, locale, onChange],\n );\n\n /** Edits the draft only — nothing reaches the caller until Apply. */\n const update = (nextDrafts: FilterDraft[]) => setDrafts(nextDrafts);\n\n const addRow = () => {\n const first = descriptors[0];\n // Defence in depth: the component returns early for an empty descriptor\n // list, so this button does not exist without a first member.\n /* v8 ignore next */\n if (!first) return;\n update([\n ...drafts,\n {\n key: `${idPrefix}-${drafts.length}-${Date.now()}`,\n property: first.name,\n operator: operatorsForType(first.type)[0],\n value: '',\n end: '',\n },\n ]);\n };\n\n const patch = (key: string, changes: Partial<FilterDraft>) =>\n update(\n drafts.map(draft =>\n draft.key === key ? { ...draft, ...changes } : draft,\n ),\n );\n\n if (descriptors.length === 0) {\n return (\n <Text step={-1} tone=\"muted\">\n {t('filter.none')}\n </Text>\n );\n }\n\n return (\n <div className=\"flex flex-col gap-2xs\">\n <div className=\"flex items-center gap-2xs\">\n <Text step={-2} tone=\"muted\">\n {t('filter.match')}\n </Text>\n <Select\n aria-label={t('filter.matchAria')}\n value={logic}\n onChange={event => {\n setLogic(event.target.value as LogicOperator);\n }}\n >\n <option value=\"and\">{t('filter.all')}</option>\n <option value=\"or\">{t('filter.any')}</option>\n </Select>\n </div>\n\n {drafts.map(draft => {\n const descriptor = descriptorFor(draft.property);\n if (!descriptor) return null;\n const arity = operatorArity(draft.operator);\n\n return (\n <div key={draft.key} className=\"flex flex-wrap items-center gap-2xs\">\n <Select\n aria-label={t('filter.member')}\n value={draft.property}\n onChange={event => {\n const next = descriptorFor(event.target.value);\n // Defence in depth: the options are built from `descriptors`,\n // so a chosen value always resolves.\n /* v8 ignore next */\n if (!next) return;\n patch(draft.key, {\n property: next.name,\n // The previous operator may not exist for the new type.\n operator: operatorsForType(next.type)[0],\n value: '',\n end: '',\n });\n }}\n >\n {descriptors.map(entry => (\n <option key={entry.name} value={entry.name}>\n {entry.label}\n </option>\n ))}\n </Select>\n\n <Select\n aria-label={t('filter.operator')}\n value={draft.operator}\n onChange={event =>\n patch(draft.key, {\n operator: event.target.value as EntityFilterOperator,\n })\n }\n >\n {operatorsForType(descriptor.type).map(operator => (\n <option key={operator} value={operator}>\n {t(operatorLabelKey(operator))}\n </option>\n ))}\n </Select>\n\n {arity !== 'none' && (\n <FilterValueInput\n descriptor={descriptor}\n arity={arity}\n value={draft.value}\n end={draft.end}\n onValueChange={value => patch(draft.key, { value })}\n onEndChange={end => patch(draft.key, { end })}\n />\n )}\n\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('filter.remove')}\n onClick={() =>\n update(drafts.filter(entry => entry.key !== draft.key))\n }\n >\n ✕\n </Button>\n </div>\n );\n })}\n\n <div className=\"flex flex-wrap items-center gap-2xs\">\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={addRow}>\n {t('filter.add')}\n </Button>\n <Button\n type=\"button\"\n variant=\"primary\"\n size=\"sm\"\n aria-label={t('filter.applyAria')}\n onClick={() => apply(drafts, logic)}\n >\n {t('filter.apply')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('filter.clearAria')}\n onClick={() => {\n setDrafts([]);\n setLogic('and');\n // Applying the emptied form is what actually clears the listing;\n // dropping the rows alone would leave the old filter in effect.\n apply([], 'and');\n }}\n >\n {t('filter.clear')}\n </Button>\n </div>\n </div>\n );\n}\n\ninterface FilterValueInputProps {\n descriptor: EntityFieldDescriptor;\n arity: ReturnType<typeof operatorArity>;\n value: string;\n end: string;\n onValueChange: (value: string) => void;\n onEndChange: (end: string) => void;\n}\n\n/** The value control the member's type calls for. */\nfunction FilterValueInput({\n descriptor,\n arity,\n value,\n end,\n onValueChange,\n onEndChange,\n}: FilterValueInputProps) {\n const t = useT();\n const enumLabel = useEnumLabel();\n\n if (descriptor.type === 'boolean') {\n return (\n <Select\n aria-label={t('filter.value')}\n value={value}\n onChange={event => onValueChange(event.target.value)}\n >\n <option value=\"\">—</option>\n <option value=\"true\">{t('value.yes')}</option>\n <option value=\"false\">{t('value.no')}</option>\n </Select>\n );\n }\n\n if (descriptor.type === 'enum' && descriptor.enumValues && arity !== 'list') {\n return (\n <Select\n aria-label={t('filter.value')}\n value={value}\n onChange={event => onValueChange(event.target.value)}\n >\n <option value=\"\">—</option>\n {descriptor.enumValues.map(option => (\n <option key={option} value={option}>\n {enumLabel(descriptor, option)}\n </option>\n ))}\n </Select>\n );\n }\n\n const inputType =\n descriptor.type === 'number'\n ? 'number'\n : descriptor.type === 'date'\n ? 'date'\n : 'text';\n\n return (\n <>\n <TextInput\n aria-label={t('filter.value')}\n type={arity === 'list' ? 'text' : inputType}\n placeholder={arity === 'list' ? t('filter.listPlaceholder') : undefined}\n value={value}\n onChange={event => onValueChange(event.target.value)}\n />\n {arity === 'range' && (\n <TextInput\n aria-label={t('filter.rangeEnd')}\n type={inputType}\n value={end}\n onChange={event => onEndChange(event.target.value)}\n />\n )}\n </>\n );\n}\n"],"names":["findQueriedDescriptor","queryPropertyFor","useCallback","useId","useState","useEnumLabel","useLocale","useT","Button","Select","TextInput","Text","operatorArity","operatorLabelKey","operatorsForType","coerce","raw","type","undefined","Number","Date","toFilter","draft","descriptor","locale","property","operator","values","value","split","map","entry","trim","filter","length","start","end","toDrafts","filtering","idPrefix","descriptors","flatMap","node","index","text","toISOString","slice","String","key","name","join","FilterBuilder","applied","onChange","t","logic","setLogic","drafts","setDrafts","descriptorFor","find","apply","nextDrafts","nextLogic","update","addRow","first","now","patch","changes","step","tone","div","className","aria-label","event","target","option","arity","next","label","FilterValueInput","onValueChange","onEndChange","variant","size","onClick","enumLabel","enumValues","inputType","placeholder"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { Entity, EntityFieldDescriptor, EntitySorting } from '@entifix/core';
1
+ import { type Entity, type EntityFieldDescriptor, type EntitySorting } from '@entifix/core';
2
2
  export interface SortBuilderProps<TEntity extends Entity> {
3
3
  /** Sortable members, already resolved from the entity metadata. */
4
4
  descriptors: readonly EntityFieldDescriptor[];
@@ -1 +1 @@
1
- {"version":3,"file":"sort-builder.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/sort-builder/sort-builder.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,MAAM,EACN,qBAAqB,EACrB,aAAa,EAEd,MAAM,eAAe,CAAC;AA2CvB,MAAM,WAAW,gBAAgB,CAAC,OAAO,SAAS,MAAM;IACtD,mEAAmE;IACnE,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC9C,4DAA4D;IAC5D,KAAK,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAC/B,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,EAClD,WAAW,EACX,KAAK,EAAE,OAAO,EACd,QAAQ,GACT,EAAE,gBAAgB,CAAC,OAAO,CAAC,+BAwK3B"}
1
+ {"version":3,"file":"sort-builder.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/sort-builder/sort-builder.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAInB,MAAM,eAAe,CAAC;AA0CvB,MAAM,WAAW,gBAAgB,CAAC,OAAO,SAAS,MAAM;IACtD,mEAAmE;IACnE,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC9C,4DAA4D;IAC5D,KAAK,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAC/B,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,EAClD,WAAW,EACX,KAAK,EAAE,OAAO,EACd,QAAQ,GACT,EAAE,gBAAgB,CAAC,OAAO,CAAC,+BA0K3B"}
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
  import { _ as _extends } from "@swc/helpers/_/_extends";
3
3
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
+ import { findQueriedDescriptor, queryPropertyFor } from '@entifix/core';
4
5
  import { useCallback, useState } from 'react';
5
- import { useT } from '../../../i18n/index.js';
6
+ import { useLocale, useT } from '../../../i18n/index.js';
6
7
  import { Button } from '../../atoms/button/index.js';
7
8
  import { Select } from '../../atoms/field/index.js';
8
9
  import { Text } from '../../atoms/text/index.js';
@@ -16,8 +17,8 @@ import { Text } from '../../atoms/text/index.js';
16
17
  const entry = sorting[priority];
17
18
  if (!entry) return [];
18
19
  // Rows address members by accessor name; an applied value carries the
19
- // wire key, which is the same string unless the member declared an alias.
20
- const descriptor = descriptors.find((candidate)=>candidate.key === String(entry.property) || candidate.name === String(entry.property));
20
+ // wire key, or a locale path into a localized member.
21
+ const descriptor = findQueriedDescriptor(descriptors, String(entry.property));
21
22
  return descriptor ? [
22
23
  {
23
24
  property: descriptor.name,
@@ -35,12 +36,14 @@ import { Text } from '../../atoms/text/index.js';
35
36
  * **Apply**: each emission is a round trip to the server.
36
37
  */ export function SortBuilder({ descriptors, value: applied, onChange }) {
37
38
  const [drafts, setDrafts] = useState(()=>toDrafts(applied, descriptors));
39
+ const locale = useLocale();
38
40
  const apply = useCallback((next)=>{
39
41
  const sorting = next.reduce((accumulator, draft, index)=>{
40
42
  const descriptor = descriptors.find((entry)=>entry.name === draft.property);
41
43
  if (descriptor) {
42
44
  accumulator[index] = {
43
- property: descriptor.key,
45
+ // A localized member sorts by the reader's own locale.
46
+ property: queryPropertyFor(descriptor, locale),
44
47
  type: draft.type
45
48
  };
46
49
  }
@@ -49,6 +52,7 @@ import { Text } from '../../atoms/text/index.js';
49
52
  onChange(sorting);
50
53
  }, [
51
54
  descriptors,
55
+ locale,
52
56
  onChange
53
57
  ]);
54
58
  const t = useT();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/sort-builder/sort-builder.tsx"],"sourcesContent":["'use client';\n\nimport type {\n Entity,\n EntityFieldDescriptor,\n EntitySorting,\n EntitySortType,\n} from '@entifix/core';\nimport { useCallback, useState } from 'react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { Select } from '../../atoms/field/index.js';\nimport { Text } from '../../atoms/text/index.js';\n\ninterface SortDraft {\n property: string;\n type: EntitySortType;\n}\n\n/**\n * Rebuilds the rows from an already-applied sorting, so reopening the panel\n * shows what is actually in effect. The numeric keys are the precedence, so\n * they are walked in order.\n */\nfunction toDrafts<TEntity extends Entity>(\n sorting: EntitySorting<TEntity> | undefined,\n descriptors: readonly EntityFieldDescriptor[],\n): SortDraft[] {\n if (!sorting) return [];\n\n return Object.keys(sorting)\n .map(Number)\n .sort((left, right) => left - right)\n .flatMap(priority => {\n const entry = sorting[priority];\n if (!entry) return [];\n // Rows address members by accessor name; an applied value carries the\n // wire key, which is the same string unless the member declared an alias.\n const descriptor = descriptors.find(\n candidate =>\n candidate.key === String(entry.property) ||\n candidate.name === String(entry.property),\n );\n return descriptor\n ? [{ property: descriptor.name, type: entry.type }]\n : [];\n });\n}\n\nexport interface SortBuilderProps<TEntity extends Entity> {\n /** Sortable members, already resolved from the entity metadata. */\n descriptors: readonly EntityFieldDescriptor[];\n /** The sorting currently applied, used to seed the rows. */\n value?: EntitySorting<TEntity>;\n onChange: (sorting: EntitySorting<TEntity>) => void;\n}\n\n/**\n * Builds an `EntitySorting` value — a precedence-ordered map, so the list order\n * here *is* the sort priority. Reordering uses explicit buttons for the same\n * reasons as the column settings.\n *\n * As in the filter builder, editing is a draft and `onChange` fires only on\n * **Apply**: each emission is a round trip to the server.\n */\nexport function SortBuilder<TEntity extends Entity>({\n descriptors,\n value: applied,\n onChange,\n}: SortBuilderProps<TEntity>) {\n const [drafts, setDrafts] = useState<SortDraft[]>(() =>\n toDrafts(applied, descriptors),\n );\n\n const apply = useCallback(\n (next: SortDraft[]) => {\n const sorting = next.reduce<EntitySorting<TEntity>>(\n (accumulator, draft, index) => {\n const descriptor = descriptors.find(\n entry => entry.name === draft.property,\n );\n if (descriptor) {\n accumulator[index] = {\n property: descriptor.key as keyof TEntity,\n type: draft.type,\n };\n }\n return accumulator;\n },\n {} as EntitySorting<TEntity>,\n );\n onChange(sorting);\n },\n [descriptors, onChange],\n );\n\n const t = useT();\n\n /** Edits the draft only — nothing reaches the caller until Apply. */\n const update = (next: SortDraft[]) => setDrafts(next);\n\n const shift = (index: number, delta: number) => {\n const target = index + delta;\n // Defence in depth: the buttons are already disabled at both ends, so this\n // is unreachable through the UI — but an out-of-range splice would silently\n // corrupt the list rather than no-op.\n /* v8 ignore next */\n if (target < 0 || target >= drafts.length) return;\n const next = [...drafts];\n const [moved] = next.splice(index, 1);\n next.splice(target, 0, moved);\n update(next);\n };\n\n if (descriptors.length === 0) {\n return (\n <Text step={-1} tone=\"muted\">\n {t('sort.none')}\n </Text>\n );\n }\n\n return (\n <div className=\"flex flex-col gap-2xs\">\n {drafts.map((draft, index) => (\n <div\n key={`${draft.property}-${index}`}\n className=\"flex flex-wrap items-center gap-2xs\"\n >\n <Text step={-2} tone=\"muted\">\n {index === 0 ? t('sort.by') : t('sort.then')}\n </Text>\n <Select\n aria-label={t('sort.member')}\n value={draft.property}\n onChange={event =>\n update(\n drafts.map((entry, position) =>\n position === index\n ? { ...entry, property: event.target.value }\n : entry,\n ),\n )\n }\n >\n {descriptors.map(descriptor => (\n <option key={descriptor.name} value={descriptor.name}>\n {descriptor.label}\n </option>\n ))}\n </Select>\n <Select\n aria-label={t('sort.direction')}\n value={draft.type}\n onChange={event =>\n update(\n drafts.map((entry, position) =>\n position === index\n ? { ...entry, type: event.target.value as EntitySortType }\n : entry,\n ),\n )\n }\n >\n <option value=\"asc\">{t('sort.ascending')}</option>\n <option value=\"desc\">{t('sort.descending')}</option>\n </Select>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('sort.raisePriority')}\n disabled={index === 0}\n onClick={() => shift(index, -1)}\n >\n ↑\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('sort.lowerPriority')}\n disabled={index === drafts.length - 1}\n onClick={() => shift(index, 1)}\n >\n ↓\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('sort.remove')}\n onClick={() =>\n update(drafts.filter((_, position) => position !== index))\n }\n >\n ✕\n </Button>\n </div>\n ))}\n\n <div className=\"flex flex-wrap items-center gap-2xs\">\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={() =>\n update([...drafts, { property: descriptors[0].name, type: 'asc' }])\n }\n >\n {t('sort.add')}\n </Button>\n <Button\n type=\"button\"\n variant=\"primary\"\n size=\"sm\"\n aria-label={t('sort.applyAria')}\n onClick={() => apply(drafts)}\n >\n {t('sort.apply')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('sort.clearAria')}\n onClick={() => {\n setDrafts([]);\n // Applying the emptied form is what restores the default order.\n apply([]);\n }}\n >\n {t('sort.clear')}\n </Button>\n </div>\n </div>\n );\n}\n"],"names":["useCallback","useState","useT","Button","Select","Text","toDrafts","sorting","descriptors","Object","keys","map","Number","sort","left","right","flatMap","priority","entry","descriptor","find","candidate","key","String","property","name","type","SortBuilder","value","applied","onChange","drafts","setDrafts","apply","next","reduce","accumulator","draft","index","t","update","shift","delta","target","length","moved","splice","step","tone","div","className","aria-label","event","position","option","label","variant","size","disabled","onClick","filter","_"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/sort-builder/sort-builder.tsx"],"sourcesContent":["'use client';\n\nimport {\n type Entity,\n type EntityFieldDescriptor,\n type EntitySorting,\n type EntitySortType,\n findQueriedDescriptor,\n queryPropertyFor,\n} from '@entifix/core';\nimport { useCallback, useState } from 'react';\n\nimport { useLocale, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { Select } from '../../atoms/field/index.js';\nimport { Text } from '../../atoms/text/index.js';\n\ninterface SortDraft {\n property: string;\n type: EntitySortType;\n}\n\n/**\n * Rebuilds the rows from an already-applied sorting, so reopening the panel\n * shows what is actually in effect. The numeric keys are the precedence, so\n * they are walked in order.\n */\nfunction toDrafts<TEntity extends Entity>(\n sorting: EntitySorting<TEntity> | undefined,\n descriptors: readonly EntityFieldDescriptor[],\n): SortDraft[] {\n if (!sorting) return [];\n\n return Object.keys(sorting)\n .map(Number)\n .sort((left, right) => left - right)\n .flatMap(priority => {\n const entry = sorting[priority];\n if (!entry) return [];\n // Rows address members by accessor name; an applied value carries the\n // wire key, or a locale path into a localized member.\n const descriptor = findQueriedDescriptor(\n descriptors,\n String(entry.property),\n );\n return descriptor\n ? [{ property: descriptor.name, type: entry.type }]\n : [];\n });\n}\n\nexport interface SortBuilderProps<TEntity extends Entity> {\n /** Sortable members, already resolved from the entity metadata. */\n descriptors: readonly EntityFieldDescriptor[];\n /** The sorting currently applied, used to seed the rows. */\n value?: EntitySorting<TEntity>;\n onChange: (sorting: EntitySorting<TEntity>) => void;\n}\n\n/**\n * Builds an `EntitySorting` value — a precedence-ordered map, so the list order\n * here *is* the sort priority. Reordering uses explicit buttons for the same\n * reasons as the column settings.\n *\n * As in the filter builder, editing is a draft and `onChange` fires only on\n * **Apply**: each emission is a round trip to the server.\n */\nexport function SortBuilder<TEntity extends Entity>({\n descriptors,\n value: applied,\n onChange,\n}: SortBuilderProps<TEntity>) {\n const [drafts, setDrafts] = useState<SortDraft[]>(() =>\n toDrafts(applied, descriptors),\n );\n const locale = useLocale();\n\n const apply = useCallback(\n (next: SortDraft[]) => {\n const sorting = next.reduce<EntitySorting<TEntity>>(\n (accumulator, draft, index) => {\n const descriptor = descriptors.find(\n entry => entry.name === draft.property,\n );\n if (descriptor) {\n accumulator[index] = {\n // A localized member sorts by the reader's own locale.\n property: queryPropertyFor(descriptor, locale) as keyof TEntity,\n type: draft.type,\n };\n }\n return accumulator;\n },\n {} as EntitySorting<TEntity>,\n );\n onChange(sorting);\n },\n [descriptors, locale, onChange],\n );\n\n const t = useT();\n\n /** Edits the draft only — nothing reaches the caller until Apply. */\n const update = (next: SortDraft[]) => setDrafts(next);\n\n const shift = (index: number, delta: number) => {\n const target = index + delta;\n // Defence in depth: the buttons are already disabled at both ends, so this\n // is unreachable through the UI — but an out-of-range splice would silently\n // corrupt the list rather than no-op.\n /* v8 ignore next */\n if (target < 0 || target >= drafts.length) return;\n const next = [...drafts];\n const [moved] = next.splice(index, 1);\n next.splice(target, 0, moved);\n update(next);\n };\n\n if (descriptors.length === 0) {\n return (\n <Text step={-1} tone=\"muted\">\n {t('sort.none')}\n </Text>\n );\n }\n\n return (\n <div className=\"flex flex-col gap-2xs\">\n {drafts.map((draft, index) => (\n <div\n key={`${draft.property}-${index}`}\n className=\"flex flex-wrap items-center gap-2xs\"\n >\n <Text step={-2} tone=\"muted\">\n {index === 0 ? t('sort.by') : t('sort.then')}\n </Text>\n <Select\n aria-label={t('sort.member')}\n value={draft.property}\n onChange={event =>\n update(\n drafts.map((entry, position) =>\n position === index\n ? { ...entry, property: event.target.value }\n : entry,\n ),\n )\n }\n >\n {descriptors.map(descriptor => (\n <option key={descriptor.name} value={descriptor.name}>\n {descriptor.label}\n </option>\n ))}\n </Select>\n <Select\n aria-label={t('sort.direction')}\n value={draft.type}\n onChange={event =>\n update(\n drafts.map((entry, position) =>\n position === index\n ? { ...entry, type: event.target.value as EntitySortType }\n : entry,\n ),\n )\n }\n >\n <option value=\"asc\">{t('sort.ascending')}</option>\n <option value=\"desc\">{t('sort.descending')}</option>\n </Select>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('sort.raisePriority')}\n disabled={index === 0}\n onClick={() => shift(index, -1)}\n >\n ↑\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('sort.lowerPriority')}\n disabled={index === drafts.length - 1}\n onClick={() => shift(index, 1)}\n >\n ↓\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('sort.remove')}\n onClick={() =>\n update(drafts.filter((_, position) => position !== index))\n }\n >\n ✕\n </Button>\n </div>\n ))}\n\n <div className=\"flex flex-wrap items-center gap-2xs\">\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={() =>\n update([...drafts, { property: descriptors[0].name, type: 'asc' }])\n }\n >\n {t('sort.add')}\n </Button>\n <Button\n type=\"button\"\n variant=\"primary\"\n size=\"sm\"\n aria-label={t('sort.applyAria')}\n onClick={() => apply(drafts)}\n >\n {t('sort.apply')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('sort.clearAria')}\n onClick={() => {\n setDrafts([]);\n // Applying the emptied form is what restores the default order.\n apply([]);\n }}\n >\n {t('sort.clear')}\n </Button>\n </div>\n </div>\n );\n}\n"],"names":["findQueriedDescriptor","queryPropertyFor","useCallback","useState","useLocale","useT","Button","Select","Text","toDrafts","sorting","descriptors","Object","keys","map","Number","sort","left","right","flatMap","priority","entry","descriptor","String","property","name","type","SortBuilder","value","applied","onChange","drafts","setDrafts","locale","apply","next","reduce","accumulator","draft","index","find","t","update","shift","delta","target","length","moved","splice","step","tone","div","className","aria-label","event","position","option","label","variant","size","disabled","onClick","filter","_"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"entity-form.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-form/entity-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,MAAM,EASZ,MAAM,eAAe,CAAC;AA2BvB,OAAO,KAAK,EAGV,eAAe,EAChB,MAAM,wBAAwB,CAAC;AA6DhC,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAAE,EACjD,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,aAAa,EACb,WAAW,EACX,YAAY,EACZ,IAAI,EAAE,QAAQ,EACd,WAAW,EACX,YAAY,EACZ,MAAM,EACN,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,QAAgB,EAChB,SAAiB,EACjB,QAAe,EACf,QAAgB,EAChB,UAAkB,EAClB,KAAK,EACL,QAAQ,EACR,UAAyB,EACzB,KAAK,EACL,QAAQ,EACR,iBAAyB,EACzB,SAAS,EACT,OAAO,EACP,QAAQ,GACT,EAAE,eAAe,CAAC,OAAO,CAAC,+BA8W1B"}
1
+ {"version":3,"file":"entity-form.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-form/entity-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,MAAM,EAUZ,MAAM,eAAe,CAAC;AA4BvB,OAAO,KAAK,EAGV,eAAe,EAChB,MAAM,wBAAwB,CAAC;AA6DhC,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAAE,EACjD,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,aAAa,EACb,WAAW,EACX,YAAY,EACZ,IAAI,EAAE,QAAQ,EACd,WAAW,EACX,YAAY,EACZ,MAAM,EACN,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,QAAgB,EAChB,SAAiB,EACjB,QAAe,EACf,QAAgB,EAChB,UAAkB,EAClB,KAAK,EACL,QAAQ,EACR,UAAyB,EACzB,KAAK,EACL,QAAQ,EACR,iBAAyB,EACzB,SAAS,EACT,OAAO,EACP,QAAQ,GACT,EAAE,eAAe,CAAC,OAAO,CAAC,+BA8W1B"}
@@ -1,12 +1,13 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { cloneEntityDraft, describeEntityColumns, EntifixLogicError, readDraftString, readRowDrafts } from '@entifix/core';
3
+ import { cloneEntityDraft, describeEntityColumns, EntifixLogicError, readDraftString, readLocalizedDraft, readRowDrafts } from '@entifix/core';
4
4
  import { useId, useState } from 'react';
5
5
  import { useErrorMessage, useLocalizedDescriptors, useT, useTranslateKey } from '../../../i18n/index.js';
6
6
  import { OVERFLOW_GLYPH, renderAnchor, useCasesForSurface } from '../../actions/index.js';
7
7
  import { Button, button } from '../../atoms/button/index.js';
8
8
  import { CellValue } from '../../atoms/cell-value/index.js';
9
9
  import { FieldControl } from '../../atoms/field-control/index.js';
10
+ import { LocalizedFieldControl } from '../../atoms/localized-field-control/index.js';
10
11
  import { Skeleton } from '../../atoms/skeleton/index.js';
11
12
  import { Text } from '../../atoms/text/index.js';
12
13
  import { Card } from '../../molecules/card/index.js';
@@ -461,6 +462,13 @@ linkSources) {
461
462
  onLinkChange == null ? void 0 : onLinkChange(field.name, undefined);
462
463
  }
463
464
  });
465
+ } else if (field.type === 'localized' && !field.virtual) {
466
+ control = /*#__PURE__*/ _jsx(LocalizedFieldControl, {
467
+ descriptor: field,
468
+ value: readLocalizedDraft(draft, field.name),
469
+ onChange: (next)=>setField(field.name, next),
470
+ id: id
471
+ });
464
472
  } else if (field.virtual || isRelation) {
465
473
  // A relation with no source has no input to offer, and a virtual field has
466
474
  // no member to edit unless a `render` was supplied — both fall back to the