@open-mercato/ui 0.6.8-develop.6918.1.7b3f32b0c8 → 0.6.8-develop.6925.1.cefa200fa4

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.
@@ -60,6 +60,7 @@ function PhoneFieldDefEditor({
60
60
  }
61
61
  FieldRegistry.register("phone", {
62
62
  input: (props) => /* @__PURE__ */ jsx(PhoneFieldInput, { ...props }),
63
+ inputRendersOwnError: true,
63
64
  defEditor: (props) => /* @__PURE__ */ jsx(PhoneFieldDefEditor, { ...props })
64
65
  });
65
66
  //# sourceMappingURL=phone.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/fields/phone.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { CrudCustomFieldRenderProps } from '../CrudForm'\nimport { FieldRegistry } from './registry'\nimport { PhoneNumberField, PHONE_COUNTRIES } from '../inputs/PhoneNumberField'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectItemLeading,\n SelectTrigger,\n SelectValue,\n} from '../../primitives/select'\n\ntype PhoneFieldConfig = {\n defaultCountryIso2?: string\n}\n\ntype PhoneFieldDef = PhoneFieldConfig & {\n configJson?: PhoneFieldConfig\n}\n\ntype PhoneFieldInputProps = CrudCustomFieldRenderProps & { def?: PhoneFieldDef }\n\n// Sentinel used by the definition editor to represent \"no fixed default\"\n// because Radix Select cannot use an empty string as an item value.\nconst AUTO_COUNTRY_VALUE = '__om_phone_auto__'\n\nfunction PhoneFieldInput({ id, value, setValue, disabled, error, def }: PhoneFieldInputProps) {\n const stringValue = typeof value === 'string' ? value : value == null ? '' : String(value)\n return (\n <PhoneNumberField\n id={id}\n value={stringValue}\n onValueChange={(next) => setValue(next ?? undefined)}\n disabled={disabled}\n externalError={error ?? null}\n defaultCountryIso2={def?.defaultCountryIso2}\n />\n )\n}\n\nfunction PhoneFieldDefEditor({\n def,\n onChange,\n}: {\n def: { configJson?: PhoneFieldConfig } | undefined\n onChange: (patch: Partial<PhoneFieldConfig>) => void\n}) {\n const t = useT()\n const selected = typeof def?.configJson?.defaultCountryIso2 === 'string' ? def.configJson.defaultCountryIso2 : ''\n return (\n <div className=\"mt-3 space-y-3 rounded border border-dashed border-muted-foreground/40 bg-muted/30 p-3\">\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium text-muted-foreground\">\n {t('ui.customFields.phone.defaultCountry', 'Default country')}\n </label>\n <Select\n value={selected || AUTO_COUNTRY_VALUE}\n onValueChange={(next) => onChange({ defaultCountryIso2: next === AUTO_COUNTRY_VALUE ? undefined : next })}\n >\n <SelectTrigger size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent align=\"start\">\n <SelectItem value={AUTO_COUNTRY_VALUE}>\n {t('ui.customFields.phone.defaultCountryAuto', 'Auto-detect from value')}\n </SelectItem>\n {PHONE_COUNTRIES.map((country) => (\n <SelectItem key={`${country.iso2}-${country.dialCode}`} value={country.iso2}>\n <SelectItemLeading>\n <span className=\"text-base leading-none\">{country.flag}</span>\n </SelectItemLeading>\n <span className=\"flex-1 truncate\">{country.label}</span>\n <span className=\"ml-2 text-xs text-muted-foreground tabular-nums\">{country.dialCode}</span>\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'ui.customFields.phone.defaultCountryHint',\n 'Pre-selects a country in the phone editor when the field is empty.',\n )}\n </p>\n </div>\n </div>\n )\n}\n\nFieldRegistry.register('phone', {\n input: (props) => <PhoneFieldInput {...props} />,\n defEditor: (props) => <PhoneFieldDefEditor {...props} />,\n})\n\nexport {}\n"],
5
- "mappings": ";AAiCI,cAsCU,YAtCV;AA7BJ,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB,uBAAuB;AAClD,SAAS,YAAY;AACrB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAcP,MAAM,qBAAqB;AAE3B,SAAS,gBAAgB,EAAE,IAAI,OAAO,UAAU,UAAU,OAAO,IAAI,GAAyB;AAC5F,QAAM,cAAc,OAAO,UAAU,WAAW,QAAQ,SAAS,OAAO,KAAK,OAAO,KAAK;AACzF,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO;AAAA,MACP,eAAe,CAAC,SAAS,SAAS,QAAQ,MAAS;AAAA,MACnD;AAAA,MACA,eAAe,SAAS;AAAA,MACxB,oBAAoB,KAAK;AAAA;AAAA,EAC3B;AAEJ;AAEA,SAAS,oBAAoB;AAAA,EAC3B;AAAA,EACA;AACF,GAGG;AACD,QAAM,IAAI,KAAK;AACf,QAAM,WAAW,OAAO,KAAK,YAAY,uBAAuB,WAAW,IAAI,WAAW,qBAAqB;AAC/G,SACE,oBAAC,SAAI,WAAU,0FACb,+BAAC,SAAI,WAAU,aACb;AAAA,wBAAC,WAAM,WAAU,6CACd,YAAE,wCAAwC,iBAAiB,GAC9D;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,YAAY;AAAA,QACnB,eAAe,CAAC,SAAS,SAAS,EAAE,oBAAoB,SAAS,qBAAqB,SAAY,KAAK,CAAC;AAAA,QAExG;AAAA,8BAAC,iBAAc,MAAK,MAClB,8BAAC,eAAY,GACf;AAAA,UACA,qBAAC,iBAAc,OAAM,SACnB;AAAA,gCAAC,cAAW,OAAO,oBAChB,YAAE,4CAA4C,wBAAwB,GACzE;AAAA,YACC,gBAAgB,IAAI,CAAC,YACpB,qBAAC,cAAuD,OAAO,QAAQ,MACrE;AAAA,kCAAC,qBACC,8BAAC,UAAK,WAAU,0BAA0B,kBAAQ,MAAK,GACzD;AAAA,cACA,oBAAC,UAAK,WAAU,mBAAmB,kBAAQ,OAAM;AAAA,cACjD,oBAAC,UAAK,WAAU,mDAAmD,kBAAQ,UAAS;AAAA,iBALrE,GAAG,QAAQ,IAAI,IAAI,QAAQ,QAAQ,EAMpD,CACD;AAAA,aACH;AAAA;AAAA;AAAA,IACF;AAAA,IACA,oBAAC,OAAE,WAAU,iCACV;AAAA,MACC;AAAA,MACA;AAAA,IACF,GACF;AAAA,KACF,GACF;AAEJ;AAEA,cAAc,SAAS,SAAS;AAAA,EAC9B,OAAO,CAAC,UAAU,oBAAC,mBAAiB,GAAG,OAAO;AAAA,EAC9C,WAAW,CAAC,UAAU,oBAAC,uBAAqB,GAAG,OAAO;AACxD,CAAC;",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { CrudCustomFieldRenderProps } from '../CrudForm'\nimport { FieldRegistry } from './registry'\nimport { PhoneNumberField, PHONE_COUNTRIES } from '../inputs/PhoneNumberField'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectItemLeading,\n SelectTrigger,\n SelectValue,\n} from '../../primitives/select'\n\ntype PhoneFieldConfig = {\n defaultCountryIso2?: string\n}\n\ntype PhoneFieldDef = PhoneFieldConfig & {\n configJson?: PhoneFieldConfig\n}\n\ntype PhoneFieldInputProps = CrudCustomFieldRenderProps & { def?: PhoneFieldDef }\n\n// Sentinel used by the definition editor to represent \"no fixed default\"\n// because Radix Select cannot use an empty string as an item value.\nconst AUTO_COUNTRY_VALUE = '__om_phone_auto__'\n\nfunction PhoneFieldInput({ id, value, setValue, disabled, error, def }: PhoneFieldInputProps) {\n const stringValue = typeof value === 'string' ? value : value == null ? '' : String(value)\n return (\n <PhoneNumberField\n id={id}\n value={stringValue}\n onValueChange={(next) => setValue(next ?? undefined)}\n disabled={disabled}\n externalError={error ?? null}\n defaultCountryIso2={def?.defaultCountryIso2}\n />\n )\n}\n\nfunction PhoneFieldDefEditor({\n def,\n onChange,\n}: {\n def: { configJson?: PhoneFieldConfig } | undefined\n onChange: (patch: Partial<PhoneFieldConfig>) => void\n}) {\n const t = useT()\n const selected = typeof def?.configJson?.defaultCountryIso2 === 'string' ? def.configJson.defaultCountryIso2 : ''\n return (\n <div className=\"mt-3 space-y-3 rounded border border-dashed border-muted-foreground/40 bg-muted/30 p-3\">\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium text-muted-foreground\">\n {t('ui.customFields.phone.defaultCountry', 'Default country')}\n </label>\n <Select\n value={selected || AUTO_COUNTRY_VALUE}\n onValueChange={(next) => onChange({ defaultCountryIso2: next === AUTO_COUNTRY_VALUE ? undefined : next })}\n >\n <SelectTrigger size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent align=\"start\">\n <SelectItem value={AUTO_COUNTRY_VALUE}>\n {t('ui.customFields.phone.defaultCountryAuto', 'Auto-detect from value')}\n </SelectItem>\n {PHONE_COUNTRIES.map((country) => (\n <SelectItem key={`${country.iso2}-${country.dialCode}`} value={country.iso2}>\n <SelectItemLeading>\n <span className=\"text-base leading-none\">{country.flag}</span>\n </SelectItemLeading>\n <span className=\"flex-1 truncate\">{country.label}</span>\n <span className=\"ml-2 text-xs text-muted-foreground tabular-nums\">{country.dialCode}</span>\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'ui.customFields.phone.defaultCountryHint',\n 'Pre-selects a country in the phone editor when the field is empty.',\n )}\n </p>\n </div>\n </div>\n )\n}\n\nFieldRegistry.register('phone', {\n input: (props) => <PhoneFieldInput {...props} />,\n inputRendersOwnError: true,\n defEditor: (props) => <PhoneFieldDefEditor {...props} />,\n})\n\nexport {}\n"],
5
+ "mappings": ";AAiCI,cAsCU,YAtCV;AA7BJ,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB,uBAAuB;AAClD,SAAS,YAAY;AACrB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAcP,MAAM,qBAAqB;AAE3B,SAAS,gBAAgB,EAAE,IAAI,OAAO,UAAU,UAAU,OAAO,IAAI,GAAyB;AAC5F,QAAM,cAAc,OAAO,UAAU,WAAW,QAAQ,SAAS,OAAO,KAAK,OAAO,KAAK;AACzF,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO;AAAA,MACP,eAAe,CAAC,SAAS,SAAS,QAAQ,MAAS;AAAA,MACnD;AAAA,MACA,eAAe,SAAS;AAAA,MACxB,oBAAoB,KAAK;AAAA;AAAA,EAC3B;AAEJ;AAEA,SAAS,oBAAoB;AAAA,EAC3B;AAAA,EACA;AACF,GAGG;AACD,QAAM,IAAI,KAAK;AACf,QAAM,WAAW,OAAO,KAAK,YAAY,uBAAuB,WAAW,IAAI,WAAW,qBAAqB;AAC/G,SACE,oBAAC,SAAI,WAAU,0FACb,+BAAC,SAAI,WAAU,aACb;AAAA,wBAAC,WAAM,WAAU,6CACd,YAAE,wCAAwC,iBAAiB,GAC9D;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,YAAY;AAAA,QACnB,eAAe,CAAC,SAAS,SAAS,EAAE,oBAAoB,SAAS,qBAAqB,SAAY,KAAK,CAAC;AAAA,QAExG;AAAA,8BAAC,iBAAc,MAAK,MAClB,8BAAC,eAAY,GACf;AAAA,UACA,qBAAC,iBAAc,OAAM,SACnB;AAAA,gCAAC,cAAW,OAAO,oBAChB,YAAE,4CAA4C,wBAAwB,GACzE;AAAA,YACC,gBAAgB,IAAI,CAAC,YACpB,qBAAC,cAAuD,OAAO,QAAQ,MACrE;AAAA,kCAAC,qBACC,8BAAC,UAAK,WAAU,0BAA0B,kBAAQ,MAAK,GACzD;AAAA,cACA,oBAAC,UAAK,WAAU,mBAAmB,kBAAQ,OAAM;AAAA,cACjD,oBAAC,UAAK,WAAU,mDAAmD,kBAAQ,UAAS;AAAA,iBALrE,GAAG,QAAQ,IAAI,IAAI,QAAQ,QAAQ,EAMpD,CACD;AAAA,aACH;AAAA;AAAA;AAAA,IACF;AAAA,IACA,oBAAC,OAAE,WAAU,iCACV;AAAA,MACC;AAAA,MACA;AAAA,IACF,GACF;AAAA,KACF,GACF;AAEJ;AAEA,cAAc,SAAS,SAAS;AAAA,EAC9B,OAAO,CAAC,UAAU,oBAAC,mBAAiB,GAAG,OAAO;AAAA,EAC9C,sBAAsB;AAAA,EACtB,WAAW,CAAC,UAAU,oBAAC,uBAAqB,GAAG,OAAO;AACxD,CAAC;",
6
6
  "names": []
7
7
  }
@@ -10,6 +10,9 @@ class FieldRegistryImpl {
10
10
  getInput(kind) {
11
11
  return this.map.get(kind.toLowerCase())?.input;
12
12
  }
13
+ inputRendersOwnError(kind) {
14
+ return this.map.get(kind.toLowerCase())?.inputRendersOwnError === true;
15
+ }
13
16
  getDefEditor(kind) {
14
17
  return this.map.get(kind.toLowerCase())?.defEditor;
15
18
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/fields/registry.ts"],
4
- "sourcesContent": ["import * as React from 'react'\nimport type { CrudCustomFieldRenderProps } from '../CrudForm'\n\nexport type FieldInputComponent = (props: CrudCustomFieldRenderProps & { def?: any }) => React.ReactNode\nexport type FieldDefEditorComponent = (props: { def: any; onChange: (patch: any) => void }) => React.ReactNode\n\ntype Entry = { input?: FieldInputComponent; defEditor?: FieldDefEditorComponent }\n\nclass FieldRegistryImpl {\n private map = new Map<string, Entry>()\n\n register(kind: string, entry: Entry) {\n const k = kind.toLowerCase()\n const prev = this.map.get(k) || {}\n this.map.set(k, { ...prev, ...entry })\n }\n\n getInput(kind: string): FieldInputComponent | undefined {\n return this.map.get(kind.toLowerCase())?.input\n }\n\n getDefEditor(kind: string): FieldDefEditorComponent | undefined {\n return this.map.get(kind.toLowerCase())?.defEditor\n }\n}\n\nexport const FieldRegistry = new FieldRegistryImpl()\n\n// Placeholder for generator to auto-register fields from modules\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nexport async function loadGeneratedFieldRegistrations(): Promise<void> {\n try {\n const gen = await import('./registry.generated')\n if (typeof gen.loadAll === 'function') gen.loadAll()\n } catch {\n // ignore when not present\n }\n}\n"],
5
- "mappings": "AAQA,MAAM,kBAAkB;AAAA,EAAxB;AACE,SAAQ,MAAM,oBAAI,IAAmB;AAAA;AAAA,EAErC,SAAS,MAAc,OAAc;AACnC,UAAM,IAAI,KAAK,YAAY;AAC3B,UAAM,OAAO,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC;AACjC,SAAK,IAAI,IAAI,GAAG,EAAE,GAAG,MAAM,GAAG,MAAM,CAAC;AAAA,EACvC;AAAA,EAEA,SAAS,MAA+C;AACtD,WAAO,KAAK,IAAI,IAAI,KAAK,YAAY,CAAC,GAAG;AAAA,EAC3C;AAAA,EAEA,aAAa,MAAmD;AAC9D,WAAO,KAAK,IAAI,IAAI,KAAK,YAAY,CAAC,GAAG;AAAA,EAC3C;AACF;AAEO,MAAM,gBAAgB,IAAI,kBAAkB;AAInD,eAAsB,kCAAiD;AACrE,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,sBAAsB;AAC/C,QAAI,OAAO,IAAI,YAAY,WAAY,KAAI,QAAQ;AAAA,EACrD,QAAQ;AAAA,EAER;AACF;",
4
+ "sourcesContent": ["import * as React from 'react'\nimport type { CrudCustomFieldRenderProps } from '../CrudForm'\n\nexport type FieldInputComponent = (props: CrudCustomFieldRenderProps & { def?: any }) => React.ReactNode\nexport type FieldDefEditorComponent = (props: { def: any; onChange: (patch: any) => void }) => React.ReactNode\n\ntype Entry = {\n input?: FieldInputComponent\n defEditor?: FieldDefEditorComponent\n inputRendersOwnError?: boolean\n}\n\nclass FieldRegistryImpl {\n private map = new Map<string, Entry>()\n\n register(kind: string, entry: Entry) {\n const k = kind.toLowerCase()\n const prev = this.map.get(k) || {}\n this.map.set(k, { ...prev, ...entry })\n }\n\n getInput(kind: string): FieldInputComponent | undefined {\n return this.map.get(kind.toLowerCase())?.input\n }\n\n inputRendersOwnError(kind: string): boolean {\n return this.map.get(kind.toLowerCase())?.inputRendersOwnError === true\n }\n\n getDefEditor(kind: string): FieldDefEditorComponent | undefined {\n return this.map.get(kind.toLowerCase())?.defEditor\n }\n}\n\nexport const FieldRegistry = new FieldRegistryImpl()\n\n// Placeholder for generator to auto-register fields from modules\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nexport async function loadGeneratedFieldRegistrations(): Promise<void> {\n try {\n const gen = await import('./registry.generated')\n if (typeof gen.loadAll === 'function') gen.loadAll()\n } catch {\n // ignore when not present\n }\n}\n"],
5
+ "mappings": "AAYA,MAAM,kBAAkB;AAAA,EAAxB;AACE,SAAQ,MAAM,oBAAI,IAAmB;AAAA;AAAA,EAErC,SAAS,MAAc,OAAc;AACnC,UAAM,IAAI,KAAK,YAAY;AAC3B,UAAM,OAAO,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC;AACjC,SAAK,IAAI,IAAI,GAAG,EAAE,GAAG,MAAM,GAAG,MAAM,CAAC;AAAA,EACvC;AAAA,EAEA,SAAS,MAA+C;AACtD,WAAO,KAAK,IAAI,IAAI,KAAK,YAAY,CAAC,GAAG;AAAA,EAC3C;AAAA,EAEA,qBAAqB,MAAuB;AAC1C,WAAO,KAAK,IAAI,IAAI,KAAK,YAAY,CAAC,GAAG,yBAAyB;AAAA,EACpE;AAAA,EAEA,aAAa,MAAmD;AAC9D,WAAO,KAAK,IAAI,IAAI,KAAK,YAAY,CAAC,GAAG;AAAA,EAC3C;AACF;AAEO,MAAM,gBAAgB,IAAI,kBAAkB;AAInD,eAAsB,kCAAiD;AACrE,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,sBAAsB;AAC/C,QAAI,OAAO,IAAI,YAAY,WAAY,KAAI,QAAQ;AAAA,EACrD,QAAQ;AAAA,EAER;AACF;",
6
6
  "names": []
7
7
  }
@@ -110,6 +110,7 @@ function buildFormFieldFromCustomFieldDef(def, opts) {
110
110
  label,
111
111
  type: "custom",
112
112
  ...baseProps,
113
+ ...FieldRegistry.inputRendersOwnError(def.kind) ? { rendersOwnError: true } : {},
113
114
  component: (props) => input({ ...props, def })
114
115
  };
115
116
  }
@@ -164,6 +165,7 @@ function buildFormFieldFromCustomFieldDef(def, opts) {
164
165
  label,
165
166
  type: "custom",
166
167
  ...baseProps,
168
+ ...FieldRegistry.inputRendersOwnError(def.kind) ? { rendersOwnError: true } : {},
167
169
  component: (props) => input({ ...props, def })
168
170
  };
169
171
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/utils/customFieldForms.ts"],
4
- "sourcesContent": ["import type { CrudField } from '../CrudForm'\nimport type {\n CustomFieldDefDto,\n CustomFieldDefinitionsPayload,\n} from './customFieldDefs'\nimport {\n filterCustomFieldDefs,\n fetchCustomFieldDefs,\n fetchCustomFieldDefinitionsPayload,\n} from './customFieldDefs'\nimport { FieldRegistry, loadGeneratedFieldRegistrations } from '../fields/registry'\nimport { fetchAllOffsetPages } from './fetchAllPages'\nimport { normalizeCustomFieldOptions } from '@open-mercato/shared/modules/entities/options'\nimport { CURRENCY_OPTIONS_URL } from '@open-mercato/shared/modules/entities/kinds'\n\nlet registryReady: Promise<void> | null = null\n\nasync function ensureFieldRegistryReady() {\n if (!registryReady) {\n registryReady = loadGeneratedFieldRegistrations().catch((err) => {\n registryReady = null\n throw err\n })\n }\n await registryReady\n}\n\nfunction buildOptionsUrl(base: string, query?: string): string {\n if (!query) return base\n try {\n const isAbsolute = /^([a-z][a-z\\d+\\-.]*:)?\\/\\//i.test(base)\n const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'\n const url = isAbsolute ? new URL(base) : new URL(base, origin)\n if (!url.searchParams.has('query')) url.searchParams.append('query', query)\n if (!url.searchParams.has('q')) url.searchParams.append('q', query)\n if (isAbsolute) return url.toString()\n return `${url.pathname}${url.search}`\n } catch {\n const sep = base.includes('?') ? '&' : '?'\n if (base.includes('query=')) return `${base}${sep}q=${encodeURIComponent(query)}`\n return `${base}${sep}query=${encodeURIComponent(query)}`\n }\n}\n\nasync function loadRemoteOptions(url: string): Promise<Array<{ value: string; label: string }>> {\n try {\n // Options routes bound how many rows one response carries (dictionary\n // entries at 500), so walk every page instead of reading a single\n // response \u2014 a stored value past the first page must stay selectable and\n // resolvable to its label. Routes without `hasMore` still resolve in one\n // request.\n const pages = await fetchAllOffsetPages(url)\n if (!pages.ok) return []\n return pages.items.map((item) => {\n const record = item && typeof item === 'object' ? (item as Record<string, unknown>) : null\n return {\n value: String(record?.value ?? item),\n label: String(record?.label ?? record?.value ?? item),\n }\n })\n } catch {\n return []\n }\n}\n\nexport function buildFormFieldFromCustomFieldDef(\n def: CustomFieldDefDto,\n opts?: { bareIds?: boolean }\n): CrudField | null {\n const id = opts?.bareIds ? def.key : `cf_${def.key}`\n const label = def.label || def.key\n const required = Array.isArray((def as any).validation)\n ? ((def as any).validation as any[]).some((rule) => rule && rule.rule === 'required')\n : false\n const baseProps = {\n description: def.description,\n required,\n ...(def.defaultValue !== undefined && def.defaultValue !== null ? { defaultValue: def.defaultValue } : {}),\n }\n\n switch (def.kind) {\n case 'boolean':\n return { id, label, type: 'checkbox', ...baseProps }\n case 'integer':\n case 'float':\n return { id, label, type: 'number', ...baseProps }\n case 'date':\n return { id, label, type: 'datepicker', ...baseProps }\n case 'datetime':\n return { id, label, type: 'datetime', ...baseProps }\n case 'multiline': {\n if (def.editor === 'plain') {\n return { id, label, type: 'textarea', ...baseProps }\n }\n let editor: 'simple' | 'uiw' | 'html' = 'html'\n if (def.editor === 'simpleMarkdown') editor = 'simple'\n else if (def.editor === 'htmlRichText') editor = 'html'\n return { id, label, type: 'richtext', ...baseProps, editor }\n }\n case 'select':\n case 'currency':\n case 'relation':\n return {\n id,\n label,\n type: 'select',\n ...baseProps,\n options: normalizeCustomFieldOptions(def.options || []).map((option) => ({\n value: option.value,\n label: option.label,\n })),\n multiple: !!def.multi,\n ...(def.kind === 'currency'\n ? {\n loadOptions: async (query?: string) => {\n const url = buildOptionsUrl(CURRENCY_OPTIONS_URL, query)\n return loadRemoteOptions(url)\n },\n }\n : def.optionsUrl\n ? {\n loadOptions: async (query?: string) => {\n const url = buildOptionsUrl(def.optionsUrl!, query)\n return loadRemoteOptions(url)\n },\n }\n : {}),\n ...(def.multi && def.input === 'listbox' ? ({ listbox: true } as any) : {}),\n }\n case 'dictionary': {\n if (!def.multi) {\n const input = FieldRegistry.getInput(def.kind)\n if (input) {\n return {\n id,\n label,\n type: 'custom',\n ...baseProps,\n component: (props) => input({ ...props, def }),\n }\n }\n return { id, label, type: 'text', ...baseProps }\n }\n const optionsUrl = def.optionsUrl || (def.dictionaryId ? `/api/dictionaries/${def.dictionaryId}/entries` : undefined)\n return {\n id,\n label,\n type: 'select',\n description: def.description,\n required,\n options: [],\n multiple: true,\n listbox: true,\n ...(optionsUrl\n ? {\n loadOptions: async (query?: string) => {\n const url = buildOptionsUrl(optionsUrl, query)\n return loadRemoteOptions(url)\n },\n }\n : {}),\n }\n }\n default: {\n if (def.kind === 'text' && def.multi) {\n const base: any = { id, label, type: 'tags', ...baseProps }\n const resolvedOptions = normalizeCustomFieldOptions(def.options || [])\n if (resolvedOptions.length > 0) {\n base.options = resolvedOptions.map((option) => ({ value: option.value, label: option.label }))\n }\n if (def.optionsUrl) {\n base.loadOptions = async (query?: string) => {\n const url = buildOptionsUrl(def.optionsUrl!, query)\n return loadRemoteOptions(url)\n }\n }\n return base\n }\n if (def.kind === 'text' && typeof def.editor === 'string' && def.editor) {\n if (def.editor === 'plain') {\n return { id, label, type: 'textarea', ...baseProps }\n }\n let editor: 'simple' | 'uiw' | 'html' = 'html'\n if (def.editor === 'simpleMarkdown') editor = 'simple'\n else if (def.editor === 'htmlRichText') editor = 'html'\n return { id, label, type: 'richtext', ...baseProps, editor }\n }\n const input = FieldRegistry.getInput(def.kind)\n if (input) {\n return {\n id,\n label,\n type: 'custom',\n ...baseProps,\n component: (props) => input({ ...props, def }),\n }\n }\n return { id, label, type: 'text', ...baseProps }\n }\n }\n}\n\nexport function buildFormFieldsFromCustomFields(\n defs: CustomFieldDefDto[],\n opts?: { bareIds?: boolean }\n): CrudField[] {\n const fields: CrudField[] = []\n const visible = filterCustomFieldDefs(defs, 'form')\n const seenKeys = new Set<string>()\n for (const def of visible) {\n const keyLower = String(def.key).toLowerCase()\n if (seenKeys.has(keyLower)) continue\n seenKeys.add(keyLower)\n const field = buildFormFieldFromCustomFieldDef(def, opts)\n if (field) fields.push(field)\n }\n return fields\n}\n\nexport async function fetchCustomFieldFormStructure(\n entityIds: string | string[],\n fetchImpl?: typeof fetch,\n options?: { bareIds?: boolean },\n): Promise<{ fields: CrudField[]; definitions: CustomFieldDefDto[]; metadata: CustomFieldDefinitionsPayload }> {\n await ensureFieldRegistryReady()\n const metadata = await fetchCustomFieldDefinitionsPayload(entityIds, fetchImpl)\n const definitions = Array.isArray(metadata.items) ? metadata.items : []\n const fields = buildFormFieldsFromCustomFields(definitions, options)\n return { fields, definitions, metadata }\n}\n\nexport async function fetchCustomFieldFormFields(\n entityIds: string | string[],\n fetchImpl?: typeof fetch,\n options?: { bareIds?: boolean },\n): Promise<CrudField[]> {\n const { fields } = await fetchCustomFieldFormStructure(entityIds, fetchImpl, options)\n return fields\n}\n\nexport async function fetchCustomFieldFormFieldsWithDefinitions(\n entityIds: string | string[],\n fetchImpl?: typeof fetch,\n options?: { bareIds?: boolean },\n): Promise<{ fields: CrudField[]; definitions: CustomFieldDefDto[]; metadata: CustomFieldDefinitionsPayload }> {\n return fetchCustomFieldFormStructure(entityIds, fetchImpl, options)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { CrudField } from '../CrudForm'\nimport type {\n CustomFieldDefDto,\n CustomFieldDefinitionsPayload,\n} from './customFieldDefs'\nimport {\n filterCustomFieldDefs,\n fetchCustomFieldDefs,\n fetchCustomFieldDefinitionsPayload,\n} from './customFieldDefs'\nimport { FieldRegistry, loadGeneratedFieldRegistrations } from '../fields/registry'\nimport { fetchAllOffsetPages } from './fetchAllPages'\nimport { normalizeCustomFieldOptions } from '@open-mercato/shared/modules/entities/options'\nimport { CURRENCY_OPTIONS_URL } from '@open-mercato/shared/modules/entities/kinds'\n\nlet registryReady: Promise<void> | null = null\n\nasync function ensureFieldRegistryReady() {\n if (!registryReady) {\n registryReady = loadGeneratedFieldRegistrations().catch((err) => {\n registryReady = null\n throw err\n })\n }\n await registryReady\n}\n\nfunction buildOptionsUrl(base: string, query?: string): string {\n if (!query) return base\n try {\n const isAbsolute = /^([a-z][a-z\\d+\\-.]*:)?\\/\\//i.test(base)\n const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'\n const url = isAbsolute ? new URL(base) : new URL(base, origin)\n if (!url.searchParams.has('query')) url.searchParams.append('query', query)\n if (!url.searchParams.has('q')) url.searchParams.append('q', query)\n if (isAbsolute) return url.toString()\n return `${url.pathname}${url.search}`\n } catch {\n const sep = base.includes('?') ? '&' : '?'\n if (base.includes('query=')) return `${base}${sep}q=${encodeURIComponent(query)}`\n return `${base}${sep}query=${encodeURIComponent(query)}`\n }\n}\n\nasync function loadRemoteOptions(url: string): Promise<Array<{ value: string; label: string }>> {\n try {\n // Options routes bound how many rows one response carries (dictionary\n // entries at 500), so walk every page instead of reading a single\n // response \u2014 a stored value past the first page must stay selectable and\n // resolvable to its label. Routes without `hasMore` still resolve in one\n // request.\n const pages = await fetchAllOffsetPages(url)\n if (!pages.ok) return []\n return pages.items.map((item) => {\n const record = item && typeof item === 'object' ? (item as Record<string, unknown>) : null\n return {\n value: String(record?.value ?? item),\n label: String(record?.label ?? record?.value ?? item),\n }\n })\n } catch {\n return []\n }\n}\n\nexport function buildFormFieldFromCustomFieldDef(\n def: CustomFieldDefDto,\n opts?: { bareIds?: boolean }\n): CrudField | null {\n const id = opts?.bareIds ? def.key : `cf_${def.key}`\n const label = def.label || def.key\n const required = Array.isArray((def as any).validation)\n ? ((def as any).validation as any[]).some((rule) => rule && rule.rule === 'required')\n : false\n const baseProps = {\n description: def.description,\n required,\n ...(def.defaultValue !== undefined && def.defaultValue !== null ? { defaultValue: def.defaultValue } : {}),\n }\n\n switch (def.kind) {\n case 'boolean':\n return { id, label, type: 'checkbox', ...baseProps }\n case 'integer':\n case 'float':\n return { id, label, type: 'number', ...baseProps }\n case 'date':\n return { id, label, type: 'datepicker', ...baseProps }\n case 'datetime':\n return { id, label, type: 'datetime', ...baseProps }\n case 'multiline': {\n if (def.editor === 'plain') {\n return { id, label, type: 'textarea', ...baseProps }\n }\n let editor: 'simple' | 'uiw' | 'html' = 'html'\n if (def.editor === 'simpleMarkdown') editor = 'simple'\n else if (def.editor === 'htmlRichText') editor = 'html'\n return { id, label, type: 'richtext', ...baseProps, editor }\n }\n case 'select':\n case 'currency':\n case 'relation':\n return {\n id,\n label,\n type: 'select',\n ...baseProps,\n options: normalizeCustomFieldOptions(def.options || []).map((option) => ({\n value: option.value,\n label: option.label,\n })),\n multiple: !!def.multi,\n ...(def.kind === 'currency'\n ? {\n loadOptions: async (query?: string) => {\n const url = buildOptionsUrl(CURRENCY_OPTIONS_URL, query)\n return loadRemoteOptions(url)\n },\n }\n : def.optionsUrl\n ? {\n loadOptions: async (query?: string) => {\n const url = buildOptionsUrl(def.optionsUrl!, query)\n return loadRemoteOptions(url)\n },\n }\n : {}),\n ...(def.multi && def.input === 'listbox' ? ({ listbox: true } as any) : {}),\n }\n case 'dictionary': {\n if (!def.multi) {\n const input = FieldRegistry.getInput(def.kind)\n if (input) {\n return {\n id,\n label,\n type: 'custom',\n ...baseProps,\n ...(FieldRegistry.inputRendersOwnError(def.kind) ? { rendersOwnError: true } : {}),\n component: (props) => input({ ...props, def }),\n }\n }\n return { id, label, type: 'text', ...baseProps }\n }\n const optionsUrl = def.optionsUrl || (def.dictionaryId ? `/api/dictionaries/${def.dictionaryId}/entries` : undefined)\n return {\n id,\n label,\n type: 'select',\n description: def.description,\n required,\n options: [],\n multiple: true,\n listbox: true,\n ...(optionsUrl\n ? {\n loadOptions: async (query?: string) => {\n const url = buildOptionsUrl(optionsUrl, query)\n return loadRemoteOptions(url)\n },\n }\n : {}),\n }\n }\n default: {\n if (def.kind === 'text' && def.multi) {\n const base: any = { id, label, type: 'tags', ...baseProps }\n const resolvedOptions = normalizeCustomFieldOptions(def.options || [])\n if (resolvedOptions.length > 0) {\n base.options = resolvedOptions.map((option) => ({ value: option.value, label: option.label }))\n }\n if (def.optionsUrl) {\n base.loadOptions = async (query?: string) => {\n const url = buildOptionsUrl(def.optionsUrl!, query)\n return loadRemoteOptions(url)\n }\n }\n return base\n }\n if (def.kind === 'text' && typeof def.editor === 'string' && def.editor) {\n if (def.editor === 'plain') {\n return { id, label, type: 'textarea', ...baseProps }\n }\n let editor: 'simple' | 'uiw' | 'html' = 'html'\n if (def.editor === 'simpleMarkdown') editor = 'simple'\n else if (def.editor === 'htmlRichText') editor = 'html'\n return { id, label, type: 'richtext', ...baseProps, editor }\n }\n const input = FieldRegistry.getInput(def.kind)\n if (input) {\n return {\n id,\n label,\n type: 'custom',\n ...baseProps,\n ...(FieldRegistry.inputRendersOwnError(def.kind) ? { rendersOwnError: true } : {}),\n component: (props) => input({ ...props, def }),\n }\n }\n return { id, label, type: 'text', ...baseProps }\n }\n }\n}\n\nexport function buildFormFieldsFromCustomFields(\n defs: CustomFieldDefDto[],\n opts?: { bareIds?: boolean }\n): CrudField[] {\n const fields: CrudField[] = []\n const visible = filterCustomFieldDefs(defs, 'form')\n const seenKeys = new Set<string>()\n for (const def of visible) {\n const keyLower = String(def.key).toLowerCase()\n if (seenKeys.has(keyLower)) continue\n seenKeys.add(keyLower)\n const field = buildFormFieldFromCustomFieldDef(def, opts)\n if (field) fields.push(field)\n }\n return fields\n}\n\nexport async function fetchCustomFieldFormStructure(\n entityIds: string | string[],\n fetchImpl?: typeof fetch,\n options?: { bareIds?: boolean },\n): Promise<{ fields: CrudField[]; definitions: CustomFieldDefDto[]; metadata: CustomFieldDefinitionsPayload }> {\n await ensureFieldRegistryReady()\n const metadata = await fetchCustomFieldDefinitionsPayload(entityIds, fetchImpl)\n const definitions = Array.isArray(metadata.items) ? metadata.items : []\n const fields = buildFormFieldsFromCustomFields(definitions, options)\n return { fields, definitions, metadata }\n}\n\nexport async function fetchCustomFieldFormFields(\n entityIds: string | string[],\n fetchImpl?: typeof fetch,\n options?: { bareIds?: boolean },\n): Promise<CrudField[]> {\n const { fields } = await fetchCustomFieldFormStructure(entityIds, fetchImpl, options)\n return fields\n}\n\nexport async function fetchCustomFieldFormFieldsWithDefinitions(\n entityIds: string | string[],\n fetchImpl?: typeof fetch,\n options?: { bareIds?: boolean },\n): Promise<{ fields: CrudField[]; definitions: CustomFieldDefDto[]; metadata: CustomFieldDefinitionsPayload }> {\n return fetchCustomFieldFormStructure(entityIds, fetchImpl, options)\n}\n"],
5
+ "mappings": "AAKA;AAAA,EACE;AAAA,EAEA;AAAA,OACK;AACP,SAAS,eAAe,uCAAuC;AAC/D,SAAS,2BAA2B;AACpC,SAAS,mCAAmC;AAC5C,SAAS,4BAA4B;AAErC,IAAI,gBAAsC;AAE1C,eAAe,2BAA2B;AACxC,MAAI,CAAC,eAAe;AAClB,oBAAgB,gCAAgC,EAAE,MAAM,CAAC,QAAQ;AAC/D,sBAAgB;AAChB,YAAM;AAAA,IACR,CAAC;AAAA,EACH;AACA,QAAM;AACR;AAEA,SAAS,gBAAgB,MAAc,OAAwB;AAC7D,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,aAAa,8BAA8B,KAAK,IAAI;AAC1D,UAAM,SAAS,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS;AACxE,UAAM,MAAM,aAAa,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,MAAM,MAAM;AAC7D,QAAI,CAAC,IAAI,aAAa,IAAI,OAAO,EAAG,KAAI,aAAa,OAAO,SAAS,KAAK;AAC1E,QAAI,CAAC,IAAI,aAAa,IAAI,GAAG,EAAG,KAAI,aAAa,OAAO,KAAK,KAAK;AAClE,QAAI,WAAY,QAAO,IAAI,SAAS;AACpC,WAAO,GAAG,IAAI,QAAQ,GAAG,IAAI,MAAM;AAAA,EACrC,QAAQ;AACN,UAAM,MAAM,KAAK,SAAS,GAAG,IAAI,MAAM;AACvC,QAAI,KAAK,SAAS,QAAQ,EAAG,QAAO,GAAG,IAAI,GAAG,GAAG,KAAK,mBAAmB,KAAK,CAAC;AAC/E,WAAO,GAAG,IAAI,GAAG,GAAG,SAAS,mBAAmB,KAAK,CAAC;AAAA,EACxD;AACF;AAEA,eAAe,kBAAkB,KAA+D;AAC9F,MAAI;AAMF,UAAM,QAAQ,MAAM,oBAAoB,GAAG;AAC3C,QAAI,CAAC,MAAM,GAAI,QAAO,CAAC;AACvB,WAAO,MAAM,MAAM,IAAI,CAAC,SAAS;AAC/B,YAAM,SAAS,QAAQ,OAAO,SAAS,WAAY,OAAmC;AACtF,aAAO;AAAA,QACL,OAAO,OAAO,QAAQ,SAAS,IAAI;AAAA,QACnC,OAAO,OAAO,QAAQ,SAAS,QAAQ,SAAS,IAAI;AAAA,MACtD;AAAA,IACF,CAAC;AAAA,EACH,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEO,SAAS,iCACd,KACA,MACkB;AAClB,QAAM,KAAK,MAAM,UAAU,IAAI,MAAM,MAAM,IAAI,GAAG;AAClD,QAAM,QAAQ,IAAI,SAAS,IAAI;AAC/B,QAAM,WAAW,MAAM,QAAS,IAAY,UAAU,IAChD,IAAY,WAAqB,KAAK,CAAC,SAAS,QAAQ,KAAK,SAAS,UAAU,IAClF;AACJ,QAAM,YAAY;AAAA,IAChB,aAAa,IAAI;AAAA,IACjB;AAAA,IACA,GAAI,IAAI,iBAAiB,UAAa,IAAI,iBAAiB,OAAO,EAAE,cAAc,IAAI,aAAa,IAAI,CAAC;AAAA,EAC1G;AAEA,UAAQ,IAAI,MAAM;AAAA,IAChB,KAAK;AACH,aAAO,EAAE,IAAI,OAAO,MAAM,YAAY,GAAG,UAAU;AAAA,IACrD,KAAK;AAAA,IACL,KAAK;AACH,aAAO,EAAE,IAAI,OAAO,MAAM,UAAU,GAAG,UAAU;AAAA,IACnD,KAAK;AACH,aAAO,EAAE,IAAI,OAAO,MAAM,cAAc,GAAG,UAAU;AAAA,IACvD,KAAK;AACH,aAAO,EAAE,IAAI,OAAO,MAAM,YAAY,GAAG,UAAU;AAAA,IACrD,KAAK,aAAa;AAChB,UAAI,IAAI,WAAW,SAAS;AAC1B,eAAO,EAAE,IAAI,OAAO,MAAM,YAAY,GAAG,UAAU;AAAA,MACrD;AACA,UAAI,SAAoC;AACxC,UAAI,IAAI,WAAW,iBAAkB,UAAS;AAAA,eACrC,IAAI,WAAW,eAAgB,UAAS;AACjD,aAAO,EAAE,IAAI,OAAO,MAAM,YAAY,GAAG,WAAW,OAAO;AAAA,IAC7D;AAAA,IACA,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA,MAAM;AAAA,QACN,GAAG;AAAA,QACH,SAAS,4BAA4B,IAAI,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,YAAY;AAAA,UACvE,OAAO,OAAO;AAAA,UACd,OAAO,OAAO;AAAA,QAChB,EAAE;AAAA,QACF,UAAU,CAAC,CAAC,IAAI;AAAA,QAChB,GAAI,IAAI,SAAS,aACb;AAAA,UACE,aAAa,OAAO,UAAmB;AACrC,kBAAM,MAAM,gBAAgB,sBAAsB,KAAK;AACvD,mBAAO,kBAAkB,GAAG;AAAA,UAC9B;AAAA,QACF,IACA,IAAI,aACJ;AAAA,UACE,aAAa,OAAO,UAAmB;AACrC,kBAAM,MAAM,gBAAgB,IAAI,YAAa,KAAK;AAClD,mBAAO,kBAAkB,GAAG;AAAA,UAC9B;AAAA,QACF,IACA,CAAC;AAAA,QACL,GAAI,IAAI,SAAS,IAAI,UAAU,YAAa,EAAE,SAAS,KAAK,IAAY,CAAC;AAAA,MAC3E;AAAA,IACF,KAAK,cAAc;AACjB,UAAI,CAAC,IAAI,OAAO;AACd,cAAM,QAAQ,cAAc,SAAS,IAAI,IAAI;AAC7C,YAAI,OAAO;AACT,iBAAO;AAAA,YACL;AAAA,YACA;AAAA,YACA,MAAM;AAAA,YACN,GAAG;AAAA,YACH,GAAI,cAAc,qBAAqB,IAAI,IAAI,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;AAAA,YAChF,WAAW,CAAC,UAAU,MAAM,EAAE,GAAG,OAAO,IAAI,CAAC;AAAA,UAC/C;AAAA,QACF;AACA,eAAO,EAAE,IAAI,OAAO,MAAM,QAAQ,GAAG,UAAU;AAAA,MACjD;AACA,YAAM,aAAa,IAAI,eAAe,IAAI,eAAe,qBAAqB,IAAI,YAAY,aAAa;AAC3G,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA,MAAM;AAAA,QACN,aAAa,IAAI;AAAA,QACjB;AAAA,QACA,SAAS,CAAC;AAAA,QACV,UAAU;AAAA,QACV,SAAS;AAAA,QACT,GAAI,aACA;AAAA,UACE,aAAa,OAAO,UAAmB;AACrC,kBAAM,MAAM,gBAAgB,YAAY,KAAK;AAC7C,mBAAO,kBAAkB,GAAG;AAAA,UAC9B;AAAA,QACF,IACA,CAAC;AAAA,MACP;AAAA,IACF;AAAA,IACA,SAAS;AACP,UAAI,IAAI,SAAS,UAAU,IAAI,OAAO;AACpC,cAAM,OAAY,EAAE,IAAI,OAAO,MAAM,QAAQ,GAAG,UAAU;AAC1D,cAAM,kBAAkB,4BAA4B,IAAI,WAAW,CAAC,CAAC;AACrE,YAAI,gBAAgB,SAAS,GAAG;AAC9B,eAAK,UAAU,gBAAgB,IAAI,CAAC,YAAY,EAAE,OAAO,OAAO,OAAO,OAAO,OAAO,MAAM,EAAE;AAAA,QAC/F;AACA,YAAI,IAAI,YAAY;AAClB,eAAK,cAAc,OAAO,UAAmB;AAC3C,kBAAM,MAAM,gBAAgB,IAAI,YAAa,KAAK;AAClD,mBAAO,kBAAkB,GAAG;AAAA,UAC9B;AAAA,QACF;AACA,eAAO;AAAA,MACT;AACA,UAAI,IAAI,SAAS,UAAU,OAAO,IAAI,WAAW,YAAY,IAAI,QAAQ;AACvE,YAAI,IAAI,WAAW,SAAS;AAC1B,iBAAO,EAAE,IAAI,OAAO,MAAM,YAAY,GAAG,UAAU;AAAA,QACrD;AACA,YAAI,SAAoC;AACxC,YAAI,IAAI,WAAW,iBAAkB,UAAS;AAAA,iBACrC,IAAI,WAAW,eAAgB,UAAS;AACjD,eAAO,EAAE,IAAI,OAAO,MAAM,YAAY,GAAG,WAAW,OAAO;AAAA,MAC7D;AACA,YAAM,QAAQ,cAAc,SAAS,IAAI,IAAI;AAC7C,UAAI,OAAO;AACT,eAAO;AAAA,UACL;AAAA,UACA;AAAA,UACA,MAAM;AAAA,UACN,GAAG;AAAA,UACH,GAAI,cAAc,qBAAqB,IAAI,IAAI,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;AAAA,UAChF,WAAW,CAAC,UAAU,MAAM,EAAE,GAAG,OAAO,IAAI,CAAC;AAAA,QAC/C;AAAA,MACF;AACA,aAAO,EAAE,IAAI,OAAO,MAAM,QAAQ,GAAG,UAAU;AAAA,IACjD;AAAA,EACF;AACF;AAEO,SAAS,gCACd,MACA,MACa;AACb,QAAM,SAAsB,CAAC;AAC7B,QAAM,UAAU,sBAAsB,MAAM,MAAM;AAClD,QAAM,WAAW,oBAAI,IAAY;AACjC,aAAW,OAAO,SAAS;AACzB,UAAM,WAAW,OAAO,IAAI,GAAG,EAAE,YAAY;AAC7C,QAAI,SAAS,IAAI,QAAQ,EAAG;AAC5B,aAAS,IAAI,QAAQ;AACrB,UAAM,QAAQ,iCAAiC,KAAK,IAAI;AACxD,QAAI,MAAO,QAAO,KAAK,KAAK;AAAA,EAC9B;AACA,SAAO;AACT;AAEA,eAAsB,8BACpB,WACA,WACA,SAC6G;AAC7G,QAAM,yBAAyB;AAC/B,QAAM,WAAW,MAAM,mCAAmC,WAAW,SAAS;AAC9E,QAAM,cAAc,MAAM,QAAQ,SAAS,KAAK,IAAI,SAAS,QAAQ,CAAC;AACtE,QAAM,SAAS,gCAAgC,aAAa,OAAO;AACnE,SAAO,EAAE,QAAQ,aAAa,SAAS;AACzC;AAEA,eAAsB,2BACpB,WACA,WACA,SACsB;AACtB,QAAM,EAAE,OAAO,IAAI,MAAM,8BAA8B,WAAW,WAAW,OAAO;AACpF,SAAO;AACT;AAEA,eAAsB,0CACpB,WACA,WACA,SAC6G;AAC7G,SAAO,8BAA8B,WAAW,WAAW,OAAO;AACpE;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.8-develop.6918.1.7b3f32b0c8",
3
+ "version": "0.6.8-develop.6925.1.cefa200fa4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -155,14 +155,14 @@
155
155
  "remark-gfm": "^4.0.1"
156
156
  },
157
157
  "peerDependencies": {
158
- "@open-mercato/shared": "0.6.8-develop.6918.1.7b3f32b0c8",
158
+ "@open-mercato/shared": "0.6.8-develop.6925.1.cefa200fa4",
159
159
  "react": ">=18.0.0",
160
160
  "react-dom": ">=18.0.0",
161
161
  "react-is": ">=18.0.0"
162
162
  },
163
163
  "devDependencies": {
164
164
  "@figma/code-connect": "^1.3.4",
165
- "@open-mercato/shared": "0.6.8-develop.6918.1.7b3f32b0c8",
165
+ "@open-mercato/shared": "0.6.8-develop.6925.1.cefa200fa4",
166
166
  "@testing-library/dom": "^10.4.1",
167
167
  "@testing-library/jest-dom": "^7.0.0",
168
168
  "@testing-library/react": "^16.3.1",
@@ -279,6 +279,7 @@ export type CrudCustomFieldRenderProps = {
279
279
  export type CrudCustomField = CrudFieldBase & {
280
280
  type: 'custom'
281
281
  component: (props: CrudCustomFieldRenderProps) => React.ReactNode
282
+ rendersOwnError?: boolean
282
283
  }
283
284
 
284
285
  export type CrudField = CrudBuiltinField | CrudCustomField
@@ -4592,7 +4593,9 @@ const FieldControl = React.memo(function FieldControlImpl({
4592
4593
  <div>{field.description}</div>
4593
4594
  </div>
4594
4595
  ) : null}
4595
- {error ? <div className="text-xs text-status-error-text">{error}</div> : null}
4596
+ {error && !(field.type === 'custom' && field.rendersOwnError) ? (
4597
+ <div className="text-xs text-status-error-text">{error}</div>
4598
+ ) : null}
4596
4599
  </div>
4597
4600
  )
4598
4601
  },
@@ -4616,5 +4619,6 @@ const FieldControl = React.memo(function FieldControlImpl({
4616
4619
  prev.recordId === next.recordId &&
4617
4620
  (prev.field.type !== 'custom' ||
4618
4621
  (prev.values === next.values &&
4619
- prev.field.component === (next.field as CrudCustomField).component))
4622
+ prev.field.component === (next.field as CrudCustomField).component &&
4623
+ prev.field.rendersOwnError === (next.field as CrudCustomField).rendersOwnError))
4620
4624
  )
@@ -0,0 +1,97 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ jest.mock('next/navigation', () => ({
4
+ useRouter: () => ({ push: jest.fn() }),
5
+ usePathname: () => '/',
6
+ useSearchParams: () => new URLSearchParams(),
7
+ }))
8
+ jest.mock('remark-gfm', () => ({ __esModule: true, default: {} }))
9
+ jest.mock('../confirm-dialog', () => ({
10
+ useConfirmDialog: () => ({ confirm: jest.fn().mockResolvedValue(true), ConfirmDialogElement: null }),
11
+ }))
12
+ jest.mock('../custom-fields/FieldDefinitionsManager', () => {
13
+ const React = require('react')
14
+ return { __esModule: true, FieldDefinitionsManager: React.forwardRef(() => <div>field definitions</div>) }
15
+ })
16
+ jest.mock('../utils/customFieldForms', () => ({
17
+ __esModule: true,
18
+ buildFormFieldFromCustomFieldDef: jest.fn(() => null),
19
+ buildFormFieldsFromCustomFields: jest.fn(() => []),
20
+ fetchCustomFieldFormStructure: jest.fn(async () => ({
21
+ fields: [],
22
+ definitions: [],
23
+ metadata: { items: [], fieldsetsByEntity: {}, entitySettings: {} },
24
+ })),
25
+ }))
26
+
27
+ import * as React from 'react'
28
+ import { fireEvent, screen, waitFor } from '@testing-library/react'
29
+ import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
30
+ import { CrudForm, type CrudField } from '../CrudForm'
31
+
32
+ const dict = { 'ui.forms.actions.save': 'Save' }
33
+
34
+ const fields: CrudField[] = [
35
+ { id: 'name', label: 'Name', type: 'text' },
36
+ { id: 'isActive', label: 'Active', type: 'checkbox' },
37
+ ]
38
+
39
+ /**
40
+ * A checkbox seeded `false` must be able to reach `true`. The users edit form uses this
41
+ * to reactivate a deactivated account, so a stuck-off checkbox would let an operator
42
+ * deactivate someone with no way back.
43
+ */
44
+ describe('CrudForm checkbox toggling', () => {
45
+ it('submits true after toggling a checkbox that started false', async () => {
46
+ const onSubmit = jest.fn().mockResolvedValue(undefined)
47
+
48
+ renderWithProviders(
49
+ <CrudForm
50
+ title="Test"
51
+ fields={fields}
52
+ initialValues={{ name: 'thing', isActive: false }}
53
+ onSubmit={onSubmit}
54
+ submitLabel="Save"
55
+ />,
56
+ { dict },
57
+ )
58
+
59
+ const checkbox = await screen.findByRole('checkbox')
60
+ expect(checkbox).toHaveAttribute('data-state', 'unchecked')
61
+
62
+ fireEvent.click(checkbox)
63
+
64
+ await waitFor(() => expect(checkbox).toHaveAttribute('data-state', 'checked'))
65
+
66
+ fireEvent.click(screen.getAllByRole('button', { name: 'Save' })[0])
67
+
68
+ await waitFor(() => expect(onSubmit).toHaveBeenCalled())
69
+ expect(onSubmit.mock.calls[0][0]).toMatchObject({ isActive: true })
70
+ })
71
+
72
+ it('submits false after clearing a checkbox that started true', async () => {
73
+ const onSubmit = jest.fn().mockResolvedValue(undefined)
74
+
75
+ renderWithProviders(
76
+ <CrudForm
77
+ title="Test"
78
+ fields={fields}
79
+ initialValues={{ name: 'thing', isActive: true }}
80
+ onSubmit={onSubmit}
81
+ submitLabel="Save"
82
+ />,
83
+ { dict },
84
+ )
85
+
86
+ const checkbox = await screen.findByRole('checkbox')
87
+ expect(checkbox).toHaveAttribute('data-state', 'checked')
88
+
89
+ fireEvent.click(checkbox)
90
+ await waitFor(() => expect(checkbox).toHaveAttribute('data-state', 'unchecked'))
91
+
92
+ fireEvent.click(screen.getAllByRole('button', { name: 'Save' })[0])
93
+
94
+ await waitFor(() => expect(onSubmit).toHaveBeenCalled())
95
+ expect(onSubmit.mock.calls[0][0]).toMatchObject({ isActive: false })
96
+ })
97
+ })
@@ -25,6 +25,58 @@ import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWith
25
25
  import { CrudForm, type CrudField } from '../CrudForm'
26
26
 
27
27
  describe('CrudForm validation state', () => {
28
+ it('renders each custom field error through exactly one declared owner', async () => {
29
+ const fields: CrudField[] = [
30
+ {
31
+ id: 'wrapperOwned',
32
+ label: 'Wrapper owned',
33
+ type: 'custom',
34
+ required: true,
35
+ component: () => <input aria-label="Wrapper owned" />,
36
+ },
37
+ {
38
+ id: 'componentOwned',
39
+ label: 'Component owned',
40
+ type: 'custom',
41
+ required: true,
42
+ rendersOwnError: true,
43
+ component: ({ error }) => (
44
+ <div>{error ? <span data-testid="component-owned-error">{error}</span> : null}</div>
45
+ ),
46
+ },
47
+ ]
48
+
49
+ const { container, getByTestId } = renderWithProviders(
50
+ <CrudForm title="Form" fields={fields} onSubmit={() => {}} />,
51
+ {
52
+ dict: {
53
+ 'ui.forms.actions.save': 'Save',
54
+ 'ui.forms.errors.highlighted': 'Please fix the highlighted errors.',
55
+ 'ui.forms.errors.required': 'This field is required',
56
+ },
57
+ },
58
+ )
59
+
60
+ const form = container.querySelector('form')
61
+ const wrapperOwnedField = container.querySelector('[data-crud-field-id="wrapperOwned"]')
62
+ const componentOwnedField = container.querySelector('[data-crud-field-id="componentOwned"]')
63
+
64
+ expect(form).not.toBeNull()
65
+ expect(wrapperOwnedField).not.toBeNull()
66
+ expect(componentOwnedField).not.toBeNull()
67
+
68
+ await act(async () => {
69
+ fireEvent.submit(form as HTMLFormElement)
70
+ })
71
+
72
+ await waitFor(() => {
73
+ expect(wrapperOwnedField?.querySelector('.text-xs.text-status-error-text')).toHaveTextContent('This field is required')
74
+ expect(getByTestId('component-owned-error')).toHaveTextContent('This field is required')
75
+ })
76
+
77
+ expect(componentOwnedField?.querySelector('.text-xs.text-status-error-text')).toBeNull()
78
+ })
79
+
28
80
  it('clears corrected field errors immediately without dropping unrelated errors', async () => {
29
81
  const fields: CrudField[] = [
30
82
  { id: 'name', label: 'Name', type: 'text', required: true },
@@ -5,7 +5,7 @@ import { FieldRegistry } from '../fields/registry'
5
5
  describe('buildFormFieldsFromCustomFields', () => {
6
6
  beforeAll(() => {
7
7
  FieldRegistry.register('dictionary', { input: () => null })
8
- FieldRegistry.register('phone', { input: () => null })
8
+ FieldRegistry.register('phone', { input: () => null, inputRendersOwnError: true })
9
9
  })
10
10
 
11
11
  it('maps kinds to CrudField and filters by formEditable', () => {
@@ -69,6 +69,8 @@ describe('buildFormFieldsFromCustomFields', () => {
69
69
  }
70
70
  // Phone resolves to a custom input via the field registry
71
71
  expect(byId['cf_work_phone']?.type).toBe('custom')
72
+ expect(byId['cf_work_phone']?.rendersOwnError).toBe(true)
73
+ expect(byId['cf_region']?.rendersOwnError).toBeUndefined()
72
74
  // Multiline now defaults to richtext (markdown editor)
73
75
  expect(byId['cf_notes']?.type).toBe('richtext')
74
76
  expect(byId['cf_desc']?.type).toBe('richtext')
@@ -54,6 +54,7 @@ describe('phone custom field', () => {
54
54
  it('registers an input and a definition editor under the "phone" kind', () => {
55
55
  expect(FieldRegistry.getInput('phone')).toBeDefined()
56
56
  expect(FieldRegistry.getDefEditor('phone')).toBeDefined()
57
+ expect(FieldRegistry.inputRendersOwnError('phone')).toBe(true)
57
58
  })
58
59
 
59
60
  it('wraps PhoneNumberField and forwards the configured default country', () => {
@@ -92,6 +92,7 @@ function PhoneFieldDefEditor({
92
92
 
93
93
  FieldRegistry.register('phone', {
94
94
  input: (props) => <PhoneFieldInput {...props} />,
95
+ inputRendersOwnError: true,
95
96
  defEditor: (props) => <PhoneFieldDefEditor {...props} />,
96
97
  })
97
98
 
@@ -4,7 +4,11 @@ import type { CrudCustomFieldRenderProps } from '../CrudForm'
4
4
  export type FieldInputComponent = (props: CrudCustomFieldRenderProps & { def?: any }) => React.ReactNode
5
5
  export type FieldDefEditorComponent = (props: { def: any; onChange: (patch: any) => void }) => React.ReactNode
6
6
 
7
- type Entry = { input?: FieldInputComponent; defEditor?: FieldDefEditorComponent }
7
+ type Entry = {
8
+ input?: FieldInputComponent
9
+ defEditor?: FieldDefEditorComponent
10
+ inputRendersOwnError?: boolean
11
+ }
8
12
 
9
13
  class FieldRegistryImpl {
10
14
  private map = new Map<string, Entry>()
@@ -19,6 +23,10 @@ class FieldRegistryImpl {
19
23
  return this.map.get(kind.toLowerCase())?.input
20
24
  }
21
25
 
26
+ inputRendersOwnError(kind: string): boolean {
27
+ return this.map.get(kind.toLowerCase())?.inputRendersOwnError === true
28
+ }
29
+
22
30
  getDefEditor(kind: string): FieldDefEditorComponent | undefined {
23
31
  return this.map.get(kind.toLowerCase())?.defEditor
24
32
  }
@@ -136,6 +136,7 @@ export function buildFormFieldFromCustomFieldDef(
136
136
  label,
137
137
  type: 'custom',
138
138
  ...baseProps,
139
+ ...(FieldRegistry.inputRendersOwnError(def.kind) ? { rendersOwnError: true } : {}),
139
140
  component: (props) => input({ ...props, def }),
140
141
  }
141
142
  }
@@ -192,6 +193,7 @@ export function buildFormFieldFromCustomFieldDef(
192
193
  label,
193
194
  type: 'custom',
194
195
  ...baseProps,
196
+ ...(FieldRegistry.inputRendersOwnError(def.kind) ? { rendersOwnError: true } : {}),
195
197
  component: (props) => input({ ...props, def }),
196
198
  }
197
199
  }