@open-mercato/ui 0.7.1-develop.7153.1.7145d295e6 → 0.7.1-develop.7170.1.d95074d7ba
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/CrudForm.js +45 -21
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +7 -6
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/SectionHeader.js +17 -8
- package/dist/backend/SectionHeader.js.map +2 -2
- package/dist/backend/injection/InjectedField.js +55 -9
- package/dist/backend/injection/InjectedField.js.map +2 -2
- package/dist/backend/utils/nav.js +34 -4
- package/dist/backend/utils/nav.js.map +2 -2
- package/dist/primitives/date-format.js +68 -1
- package/dist/primitives/date-format.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/CrudForm.tsx +40 -9
- package/src/backend/DataTable.tsx +11 -15
- package/src/backend/SectionHeader.tsx +28 -5
- package/src/backend/__tests__/CrudForm.localeNumber.test.tsx +159 -0
- package/src/backend/__tests__/CrudForm.validation.test.tsx +1 -1
- package/src/backend/__tests__/DataTable.dateCells.test.tsx +87 -0
- package/src/backend/__tests__/SectionHeader.test.tsx +105 -0
- package/src/backend/__tests__/nav-utils.test.ts +180 -0
- package/src/backend/injection/InjectedField.tsx +69 -9
- package/src/backend/injection/__tests__/InjectedField.localeNumber.test.tsx +94 -0
- package/src/backend/utils/nav.ts +75 -5
- package/src/primitives/__tests__/date-format.test.ts +185 -0
- package/src/primitives/date-format.ts +145 -0
|
@@ -4,15 +4,18 @@ import * as React from "react";
|
|
|
4
4
|
import { ChevronDown } from "lucide-react";
|
|
5
5
|
import { Badge } from "../primitives/badge.js";
|
|
6
6
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
7
|
+
import { useOptionalT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
8
|
function SectionHeader({
|
|
8
9
|
title,
|
|
9
10
|
count,
|
|
10
11
|
action,
|
|
11
|
-
className
|
|
12
|
+
className,
|
|
13
|
+
titleClassName
|
|
12
14
|
}) {
|
|
15
|
+
const allowsTitleShrink = Boolean(titleClassName);
|
|
13
16
|
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center justify-between", className), children: [
|
|
14
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
15
|
-
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: title }),
|
|
17
|
+
/* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2", allowsTitleShrink && "min-w-0"), children: [
|
|
18
|
+
/* @__PURE__ */ jsx("h3", { className: cn("text-sm font-semibold", allowsTitleShrink && "min-w-0", titleClassName), children: title }),
|
|
16
19
|
count != null && /* @__PURE__ */ jsx(Badge, { variant: "muted", className: "text-xs tabular-nums", children: count })
|
|
17
20
|
] }),
|
|
18
21
|
action ? /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: action }) : null
|
|
@@ -27,7 +30,8 @@ function CollapsibleSection({
|
|
|
27
30
|
onCollapsedChange,
|
|
28
31
|
children,
|
|
29
32
|
className,
|
|
30
|
-
contentClassName
|
|
33
|
+
contentClassName,
|
|
34
|
+
titleClassName
|
|
31
35
|
}) {
|
|
32
36
|
const [internalCollapsed, setInternalCollapsed] = React.useState(defaultCollapsed);
|
|
33
37
|
const isControlled = controlledCollapsed !== void 0;
|
|
@@ -37,6 +41,10 @@ function CollapsibleSection({
|
|
|
37
41
|
if (!isControlled) setInternalCollapsed(next);
|
|
38
42
|
onCollapsedChange?.(next);
|
|
39
43
|
}, [isCollapsed, isControlled, onCollapsedChange]);
|
|
44
|
+
const contextT = useOptionalT();
|
|
45
|
+
const t = (key, fallback) => contextT ? contextT(key, fallback, { title }) : fallback.replace("{title}", title);
|
|
46
|
+
const toggleAriaLabel = isCollapsed ? t("ui.sectionHeader.expand", "Expand {title} section") : t("ui.sectionHeader.collapse", "Collapse {title} section");
|
|
47
|
+
const allowsTitleShrink = Boolean(titleClassName);
|
|
40
48
|
return /* @__PURE__ */ jsxs("div", { className: cn("space-y-3", className), children: [
|
|
41
49
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
42
50
|
/* @__PURE__ */ jsxs(
|
|
@@ -44,20 +52,21 @@ function CollapsibleSection({
|
|
|
44
52
|
{
|
|
45
53
|
type: "button",
|
|
46
54
|
onClick: toggle,
|
|
47
|
-
className: "flex items-center gap-2 group",
|
|
55
|
+
className: cn("flex items-center gap-2 group", allowsTitleShrink && "min-w-0"),
|
|
48
56
|
"aria-expanded": !isCollapsed,
|
|
49
|
-
"aria-label":
|
|
57
|
+
"aria-label": toggleAriaLabel,
|
|
50
58
|
children: [
|
|
51
59
|
/* @__PURE__ */ jsx(
|
|
52
60
|
ChevronDown,
|
|
53
61
|
{
|
|
54
62
|
className: cn(
|
|
55
63
|
"h-4 w-4 text-muted-foreground transition-transform duration-200",
|
|
56
|
-
isCollapsed && "-rotate-90"
|
|
64
|
+
isCollapsed && "-rotate-90",
|
|
65
|
+
allowsTitleShrink && "shrink-0"
|
|
57
66
|
)
|
|
58
67
|
}
|
|
59
68
|
),
|
|
60
|
-
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: title }),
|
|
69
|
+
/* @__PURE__ */ jsx("h3", { className: cn("text-sm font-semibold", allowsTitleShrink && "min-w-0", titleClassName), children: title }),
|
|
61
70
|
count != null && /* @__PURE__ */ jsx(Badge, { variant: "muted", className: "text-xs tabular-nums", children: count })
|
|
62
71
|
]
|
|
63
72
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/backend/SectionHeader.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ChevronDown } from 'lucide-react'\nimport { Badge } from '../primitives/badge'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type SectionHeaderProps = {\n /** Section title */\n title: string\n /** Optional item count \u2014 displayed as muted badge */\n count?: number\n /** Action element(s) on the right \u2014 typically Button or IconButton */\n action?: React.ReactNode\n /** Additional className */\n className?: string\n}\n\nexport function SectionHeader({\n title,\n count,\n action,\n className,\n}: SectionHeaderProps) {\n return (\n <div className={cn('flex items-center justify-between', className)}>\n <div className
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ChevronDown } from 'lucide-react'\nimport { Badge } from '../primitives/badge'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useOptionalT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type SectionHeaderProps = {\n /** Section title */\n title: string\n /** Optional item count \u2014 displayed as muted badge */\n count?: number\n /** Action element(s) on the right \u2014 typically Button or IconButton */\n action?: React.ReactNode\n /** Additional className */\n className?: string\n /**\n * Additional className on the title element. Supplying it also lets the title row\n * shrink below its content width, so `truncate` takes effect in narrow containers.\n */\n titleClassName?: string\n}\n\nexport function SectionHeader({\n title,\n count,\n action,\n className,\n titleClassName,\n}: SectionHeaderProps) {\n const allowsTitleShrink = Boolean(titleClassName)\n return (\n <div className={cn('flex items-center justify-between', className)}>\n <div className={cn('flex items-center gap-2', allowsTitleShrink && 'min-w-0')}>\n <h3 className={cn('text-sm font-semibold', allowsTitleShrink && 'min-w-0', titleClassName)}>{title}</h3>\n {count != null && (\n <Badge variant=\"muted\" className=\"text-xs tabular-nums\">\n {count}\n </Badge>\n )}\n </div>\n {action ? (\n <div className=\"flex items-center gap-1\">\n {action}\n </div>\n ) : null}\n </div>\n )\n}\n\nexport type CollapsibleSectionProps = {\n /** Section title */\n title: string\n count?: number\n action?: React.ReactNode\n /** Collapse behavior */\n defaultCollapsed?: boolean\n collapsed?: boolean\n onCollapsedChange?: (collapsed: boolean) => void\n /** Content */\n children?: React.ReactNode\n /** Additional className on root */\n className?: string\n /** Additional className on content wrapper */\n contentClassName?: string\n /**\n * Additional className on the title element. Supplying it also lets the header row\n * shrink below its content width, so `truncate` takes effect in narrow containers.\n */\n titleClassName?: string\n}\n\nexport function CollapsibleSection({\n title,\n count,\n action,\n defaultCollapsed = false,\n collapsed: controlledCollapsed,\n onCollapsedChange,\n children,\n className,\n contentClassName,\n titleClassName,\n}: CollapsibleSectionProps) {\n const [internalCollapsed, setInternalCollapsed] = React.useState(defaultCollapsed)\n const isControlled = controlledCollapsed !== undefined\n const isCollapsed = isControlled ? controlledCollapsed : internalCollapsed\n\n const toggle = React.useCallback(() => {\n const next = !isCollapsed\n if (!isControlled) setInternalCollapsed(next)\n onCollapsedChange?.(next)\n }, [isCollapsed, isControlled, onCollapsedChange])\n\n const contextT = useOptionalT()\n const t = (key: string, fallback: string) =>\n contextT ? contextT(key, fallback, { title }) : fallback.replace('{title}', title)\n const toggleAriaLabel = isCollapsed\n ? t('ui.sectionHeader.expand', 'Expand {title} section')\n : t('ui.sectionHeader.collapse', 'Collapse {title} section')\n const allowsTitleShrink = Boolean(titleClassName)\n\n return (\n <div className={cn('space-y-3', className)}>\n <div className=\"flex items-center justify-between\">\n <button\n type=\"button\"\n onClick={toggle}\n className={cn('flex items-center gap-2 group', allowsTitleShrink && 'min-w-0')}\n aria-expanded={!isCollapsed}\n aria-label={toggleAriaLabel}\n >\n <ChevronDown\n className={cn(\n 'h-4 w-4 text-muted-foreground transition-transform duration-200',\n isCollapsed && '-rotate-90',\n allowsTitleShrink && 'shrink-0',\n )}\n />\n <h3 className={cn('text-sm font-semibold', allowsTitleShrink && 'min-w-0', titleClassName)}>{title}</h3>\n {count != null && (\n <Badge variant=\"muted\" className=\"text-xs tabular-nums\">\n {count}\n </Badge>\n )}\n </button>\n {action ? (\n <div className=\"flex items-center gap-1\">\n {action}\n </div>\n ) : null}\n </div>\n\n {!isCollapsed && (\n <div className={contentClassName}>\n {children}\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAkCM,SACE,KADF;AAhCN,YAAY,WAAW;AACvB,SAAS,mBAAmB;AAC5B,SAAS,aAAa;AACtB,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAkBtB,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,oBAAoB,QAAQ,cAAc;AAChD,SACE,qBAAC,SAAI,WAAW,GAAG,qCAAqC,SAAS,GAC/D;AAAA,yBAAC,SAAI,WAAW,GAAG,2BAA2B,qBAAqB,SAAS,GAC1E;AAAA,0BAAC,QAAG,WAAW,GAAG,yBAAyB,qBAAqB,WAAW,cAAc,GAAI,iBAAM;AAAA,MAClG,SAAS,QACR,oBAAC,SAAM,SAAQ,SAAQ,WAAU,wBAC9B,iBACH;AAAA,OAEJ;AAAA,IACC,SACC,oBAAC,SAAI,WAAU,2BACZ,kBACH,IACE;AAAA,KACN;AAEJ;AAwBO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,MAAM,SAAS,gBAAgB;AACjF,QAAM,eAAe,wBAAwB;AAC7C,QAAM,cAAc,eAAe,sBAAsB;AAEzD,QAAM,SAAS,MAAM,YAAY,MAAM;AACrC,UAAM,OAAO,CAAC;AACd,QAAI,CAAC,aAAc,sBAAqB,IAAI;AAC5C,wBAAoB,IAAI;AAAA,EAC1B,GAAG,CAAC,aAAa,cAAc,iBAAiB,CAAC;AAEjD,QAAM,WAAW,aAAa;AAC9B,QAAM,IAAI,CAAC,KAAa,aACtB,WAAW,SAAS,KAAK,UAAU,EAAE,MAAM,CAAC,IAAI,SAAS,QAAQ,WAAW,KAAK;AACnF,QAAM,kBAAkB,cACpB,EAAE,2BAA2B,wBAAwB,IACrD,EAAE,6BAA6B,0BAA0B;AAC7D,QAAM,oBAAoB,QAAQ,cAAc;AAEhD,SACE,qBAAC,SAAI,WAAW,GAAG,aAAa,SAAS,GACvC;AAAA,yBAAC,SAAI,WAAU,qCACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS;AAAA,UACT,WAAW,GAAG,iCAAiC,qBAAqB,SAAS;AAAA,UAC7E,iBAAe,CAAC;AAAA,UAChB,cAAY;AAAA,UAEZ;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,WAAW;AAAA,kBACT;AAAA,kBACA,eAAe;AAAA,kBACf,qBAAqB;AAAA,gBACvB;AAAA;AAAA,YACF;AAAA,YACA,oBAAC,QAAG,WAAW,GAAG,yBAAyB,qBAAqB,WAAW,cAAc,GAAI,iBAAM;AAAA,YAClG,SAAS,QACR,oBAAC,SAAM,SAAQ,SAAQ,WAAU,wBAC9B,iBACH;AAAA;AAAA;AAAA,MAEJ;AAAA,MACC,SACC,oBAAC,SAAI,WAAU,2BACZ,kBACH,IACE;AAAA,OACN;AAAA,IAEC,CAAC,eACA,oBAAC,SAAI,WAAW,kBACb,UACH;AAAA,KAEJ;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { evaluateInjectedVisibility } from "./visibility-utils.js";
|
|
5
|
-
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
|
+
import { useT, useOptionalLocale } from "@open-mercato/shared/lib/i18n/context";
|
|
6
|
+
import { parseLocaleNumber } from "@open-mercato/shared/lib/number";
|
|
6
7
|
import { Input } from "../../primitives/input.js";
|
|
7
8
|
import { Checkbox } from "../../primitives/checkbox.js";
|
|
8
9
|
import { Textarea } from "../../primitives/textarea.js";
|
|
@@ -143,26 +144,71 @@ function InjectedField({ field, value, onChange, context, formData, readOnly = f
|
|
|
143
144
|
/* @__PURE__ */ jsx("span", { children: label })
|
|
144
145
|
] });
|
|
145
146
|
}
|
|
147
|
+
if (field.type === "number") {
|
|
148
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-crud-field-id": field.id, children: [
|
|
149
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: field.id, children: label }),
|
|
150
|
+
/* @__PURE__ */ jsx(
|
|
151
|
+
NumberFieldInput,
|
|
152
|
+
{
|
|
153
|
+
id: field.id,
|
|
154
|
+
value,
|
|
155
|
+
disabled,
|
|
156
|
+
onChange: (next) => onChange(field.id, next)
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
] });
|
|
160
|
+
}
|
|
146
161
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-crud-field-id": field.id, children: [
|
|
147
162
|
/* @__PURE__ */ jsx(Label, { htmlFor: field.id, children: label }),
|
|
148
163
|
/* @__PURE__ */ jsx(
|
|
149
164
|
Input,
|
|
150
165
|
{
|
|
151
166
|
id: field.id,
|
|
152
|
-
type: field.type === "
|
|
167
|
+
type: field.type === "date" ? "date" : "text",
|
|
153
168
|
value: typeof value === "string" || typeof value === "number" ? String(value) : "",
|
|
154
169
|
disabled,
|
|
155
|
-
onChange: (event) =>
|
|
156
|
-
if (field.type === "number") {
|
|
157
|
-
onChange(field.id, event.target.value === "" ? void 0 : Number(event.target.value));
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
onChange(field.id, event.target.value);
|
|
161
|
-
}
|
|
170
|
+
onChange: (event) => onChange(field.id, event.target.value)
|
|
162
171
|
}
|
|
163
172
|
)
|
|
164
173
|
] });
|
|
165
174
|
}
|
|
175
|
+
function NumberFieldInput({
|
|
176
|
+
id,
|
|
177
|
+
value,
|
|
178
|
+
disabled,
|
|
179
|
+
onChange
|
|
180
|
+
}) {
|
|
181
|
+
const locale = useOptionalLocale();
|
|
182
|
+
const serializedValue = typeof value === "string" || typeof value === "number" ? String(value) : "";
|
|
183
|
+
const [local, setLocal] = React.useState(serializedValue);
|
|
184
|
+
const isFocusedRef = React.useRef(false);
|
|
185
|
+
React.useEffect(() => {
|
|
186
|
+
if (!isFocusedRef.current) setLocal(serializedValue);
|
|
187
|
+
}, [serializedValue]);
|
|
188
|
+
return /* @__PURE__ */ jsx(
|
|
189
|
+
Input,
|
|
190
|
+
{
|
|
191
|
+
id,
|
|
192
|
+
type: "text",
|
|
193
|
+
inputMode: "decimal",
|
|
194
|
+
autoComplete: "off",
|
|
195
|
+
spellCheck: false,
|
|
196
|
+
value: local,
|
|
197
|
+
disabled,
|
|
198
|
+
onFocus: () => {
|
|
199
|
+
isFocusedRef.current = true;
|
|
200
|
+
},
|
|
201
|
+
onBlur: () => {
|
|
202
|
+
isFocusedRef.current = false;
|
|
203
|
+
},
|
|
204
|
+
onChange: (event) => {
|
|
205
|
+
const raw = event.target.value;
|
|
206
|
+
setLocal(raw);
|
|
207
|
+
onChange(raw === "" ? void 0 : parseLocaleNumber(raw, locale) ?? void 0);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
}
|
|
166
212
|
var InjectedField_default = InjectedField;
|
|
167
213
|
export {
|
|
168
214
|
InjectedField,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/injection/InjectedField.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { InjectionFieldDefinition, FieldContext } from '@open-mercato/shared/modules/widgets/injection'\nimport { evaluateInjectedVisibility } from './visibility-utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Input } from '../../primitives/input'\nimport { Checkbox } from '../../primitives/checkbox'\nimport { Textarea } from '../../primitives/textarea'\nimport { Label } from '../../primitives/label'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../../primitives/select'\nimport { Spinner } from '../../primitives/spinner'\n\ntype InjectedFieldProps = {\n field: InjectionFieldDefinition\n value: unknown\n onChange: (fieldId: string, value: unknown) => void\n context: FieldContext\n formData: Record<string, unknown>\n readOnly?: boolean\n}\n\ntype Option = { value: string; label: string; labelKey?: string }\n\nconst MAX_CACHE_ENTRIES = 100\nconst optionsCache = new Map<string, { expiresAt: number; options: Option[] }>()\n\nfunction evictExpiredCacheEntries() {\n if (optionsCache.size <= MAX_CACHE_ENTRIES) return\n const now = Date.now()\n for (const [key, entry] of optionsCache) {\n if (entry.expiresAt < now) optionsCache.delete(key)\n }\n}\n\nfunction SelectField({\n field,\n value,\n onChange,\n disabled,\n options,\n optionsError,\n label,\n}: {\n field: InjectionFieldDefinition\n value: unknown\n onChange: (fieldId: string, value: unknown) => void\n disabled?: boolean\n options: Option[]\n optionsError: boolean\n label: string\n}) {\n const t = useT()\n return (\n <div className=\"space-y-2\" data-crud-field-id={field.id}>\n <Label htmlFor={field.id}>{label}</Label>\n <Select\n value={typeof value === 'string' && value ? value : undefined}\n onValueChange={(next) => onChange(field.id, next || undefined)}\n disabled={disabled || (options.length === 0 && !field.options?.length)}\n >\n <SelectTrigger id={field.id}>\n <SelectValue placeholder={t('ui.filters.select.placeholder', 'Select...')} />\n </SelectTrigger>\n <SelectContent>\n {options.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.labelKey ? t(option.labelKey, option.label) : option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {optionsError ? (\n <div className=\"text-xs text-muted-foreground\">{t('ui.forms.optionsUnavailable', 'Options unavailable')}</div>\n ) : null}\n </div>\n )\n}\n\n\nexport function InjectedField({ field, value, onChange, context, formData, readOnly = false }: InjectedFieldProps) {\n const t = useT()\n const [dynamicOptions, setDynamicOptions] = React.useState<Option[] | null>(null)\n const [optionsError, setOptionsError] = React.useState(false)\n\n React.useEffect(() => {\n let cancelled = false\n const loadOptions = async () => {\n if (typeof field.optionsLoader !== 'function') return\n const ttl = Math.max(1, field.optionsCacheTtl ?? 60)\n const cacheKey = `${field.id}:${context.organizationId ?? ''}:${context.tenantId ?? ''}`\n const cached = optionsCache.get(cacheKey)\n if (cached && cached.expiresAt > Date.now()) {\n setDynamicOptions(cached.options)\n return\n }\n try {\n const loaded = await field.optionsLoader(context)\n if (cancelled) return\n const normalized = Array.isArray(loaded) ? loaded : []\n setDynamicOptions(normalized)\n setOptionsError(false)\n evictExpiredCacheEntries()\n optionsCache.set(cacheKey, { options: normalized, expiresAt: Date.now() + ttl * 1000 })\n } catch {\n if (cancelled) return\n setDynamicOptions(Array.isArray(field.options) ? field.options : null)\n setOptionsError(true)\n }\n }\n void loadOptions()\n return () => {\n cancelled = true\n }\n }, [context, field.id, field.options, field.optionsCacheTtl, field.optionsLoader])\n\n if (!evaluateInjectedVisibility(field.visibleWhen, formData, context)) return null\n\n const label = field.labelKey ? t(field.labelKey, field.label) : t(field.label, field.label)\n const disabled = readOnly || field.readOnly\n const options = dynamicOptions ?? field.options ?? []\n\n if (field.type === 'custom' && field.customComponent) {\n const CustomComponent = field.customComponent\n return (\n <React.Suspense fallback={<Spinner size=\"sm\" />}>\n <CustomComponent\n value={value}\n onChange={(next) => onChange(field.id, next)}\n context={context}\n disabled={disabled}\n />\n </React.Suspense>\n )\n }\n\n if (field.type === 'textarea') {\n return (\n <div className=\"space-y-2\" data-crud-field-id={field.id}>\n <Label htmlFor={field.id}>{label}</Label>\n <Textarea\n id={field.id}\n className=\"min-h-[96px]\"\n value={typeof value === 'string' ? value : ''}\n disabled={disabled}\n onChange={(event) => onChange(field.id, event.target.value)}\n />\n </div>\n )\n }\n\n if (field.type === 'select') {\n return (\n <SelectField\n field={field}\n value={value}\n onChange={onChange}\n disabled={disabled}\n options={options}\n optionsError={optionsError}\n label={label}\n />\n )\n }\n\n if (field.type === 'boolean') {\n return (\n <label className=\"flex items-center gap-2 text-sm\" data-crud-field-id={field.id}>\n <Checkbox\n checked={value === true}\n disabled={disabled}\n onCheckedChange={(checked) => onChange(field.id, checked === true)}\n />\n <span>{label}</span>\n </label>\n )\n }\n\n return (\n <div className=\"space-y-2\" data-crud-field-id={field.id}>\n <Label htmlFor={field.id}>{label}</Label>\n <Input\n id={field.id}\n type={field.type === '
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { InjectionFieldDefinition, FieldContext } from '@open-mercato/shared/modules/widgets/injection'\nimport { evaluateInjectedVisibility } from './visibility-utils'\nimport { useT, useOptionalLocale } from '@open-mercato/shared/lib/i18n/context'\nimport { parseLocaleNumber } from '@open-mercato/shared/lib/number'\nimport { Input } from '../../primitives/input'\nimport { Checkbox } from '../../primitives/checkbox'\nimport { Textarea } from '../../primitives/textarea'\nimport { Label } from '../../primitives/label'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../../primitives/select'\nimport { Spinner } from '../../primitives/spinner'\n\ntype InjectedFieldProps = {\n field: InjectionFieldDefinition\n value: unknown\n onChange: (fieldId: string, value: unknown) => void\n context: FieldContext\n formData: Record<string, unknown>\n readOnly?: boolean\n}\n\ntype Option = { value: string; label: string; labelKey?: string }\n\nconst MAX_CACHE_ENTRIES = 100\nconst optionsCache = new Map<string, { expiresAt: number; options: Option[] }>()\n\nfunction evictExpiredCacheEntries() {\n if (optionsCache.size <= MAX_CACHE_ENTRIES) return\n const now = Date.now()\n for (const [key, entry] of optionsCache) {\n if (entry.expiresAt < now) optionsCache.delete(key)\n }\n}\n\nfunction SelectField({\n field,\n value,\n onChange,\n disabled,\n options,\n optionsError,\n label,\n}: {\n field: InjectionFieldDefinition\n value: unknown\n onChange: (fieldId: string, value: unknown) => void\n disabled?: boolean\n options: Option[]\n optionsError: boolean\n label: string\n}) {\n const t = useT()\n return (\n <div className=\"space-y-2\" data-crud-field-id={field.id}>\n <Label htmlFor={field.id}>{label}</Label>\n <Select\n value={typeof value === 'string' && value ? value : undefined}\n onValueChange={(next) => onChange(field.id, next || undefined)}\n disabled={disabled || (options.length === 0 && !field.options?.length)}\n >\n <SelectTrigger id={field.id}>\n <SelectValue placeholder={t('ui.filters.select.placeholder', 'Select...')} />\n </SelectTrigger>\n <SelectContent>\n {options.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.labelKey ? t(option.labelKey, option.label) : option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {optionsError ? (\n <div className=\"text-xs text-muted-foreground\">{t('ui.forms.optionsUnavailable', 'Options unavailable')}</div>\n ) : null}\n </div>\n )\n}\n\n\nexport function InjectedField({ field, value, onChange, context, formData, readOnly = false }: InjectedFieldProps) {\n const t = useT()\n const [dynamicOptions, setDynamicOptions] = React.useState<Option[] | null>(null)\n const [optionsError, setOptionsError] = React.useState(false)\n\n React.useEffect(() => {\n let cancelled = false\n const loadOptions = async () => {\n if (typeof field.optionsLoader !== 'function') return\n const ttl = Math.max(1, field.optionsCacheTtl ?? 60)\n const cacheKey = `${field.id}:${context.organizationId ?? ''}:${context.tenantId ?? ''}`\n const cached = optionsCache.get(cacheKey)\n if (cached && cached.expiresAt > Date.now()) {\n setDynamicOptions(cached.options)\n return\n }\n try {\n const loaded = await field.optionsLoader(context)\n if (cancelled) return\n const normalized = Array.isArray(loaded) ? loaded : []\n setDynamicOptions(normalized)\n setOptionsError(false)\n evictExpiredCacheEntries()\n optionsCache.set(cacheKey, { options: normalized, expiresAt: Date.now() + ttl * 1000 })\n } catch {\n if (cancelled) return\n setDynamicOptions(Array.isArray(field.options) ? field.options : null)\n setOptionsError(true)\n }\n }\n void loadOptions()\n return () => {\n cancelled = true\n }\n }, [context, field.id, field.options, field.optionsCacheTtl, field.optionsLoader])\n\n if (!evaluateInjectedVisibility(field.visibleWhen, formData, context)) return null\n\n const label = field.labelKey ? t(field.labelKey, field.label) : t(field.label, field.label)\n const disabled = readOnly || field.readOnly\n const options = dynamicOptions ?? field.options ?? []\n\n if (field.type === 'custom' && field.customComponent) {\n const CustomComponent = field.customComponent\n return (\n <React.Suspense fallback={<Spinner size=\"sm\" />}>\n <CustomComponent\n value={value}\n onChange={(next) => onChange(field.id, next)}\n context={context}\n disabled={disabled}\n />\n </React.Suspense>\n )\n }\n\n if (field.type === 'textarea') {\n return (\n <div className=\"space-y-2\" data-crud-field-id={field.id}>\n <Label htmlFor={field.id}>{label}</Label>\n <Textarea\n id={field.id}\n className=\"min-h-[96px]\"\n value={typeof value === 'string' ? value : ''}\n disabled={disabled}\n onChange={(event) => onChange(field.id, event.target.value)}\n />\n </div>\n )\n }\n\n if (field.type === 'select') {\n return (\n <SelectField\n field={field}\n value={value}\n onChange={onChange}\n disabled={disabled}\n options={options}\n optionsError={optionsError}\n label={label}\n />\n )\n }\n\n if (field.type === 'boolean') {\n return (\n <label className=\"flex items-center gap-2 text-sm\" data-crud-field-id={field.id}>\n <Checkbox\n checked={value === true}\n disabled={disabled}\n onCheckedChange={(checked) => onChange(field.id, checked === true)}\n />\n <span>{label}</span>\n </label>\n )\n }\n\n if (field.type === 'number') {\n return (\n <div className=\"space-y-2\" data-crud-field-id={field.id}>\n <Label htmlFor={field.id}>{label}</Label>\n <NumberFieldInput\n id={field.id}\n value={value}\n disabled={disabled}\n onChange={(next) => onChange(field.id, next)}\n />\n </div>\n )\n }\n\n return (\n <div className=\"space-y-2\" data-crud-field-id={field.id}>\n <Label htmlFor={field.id}>{label}</Label>\n <Input\n id={field.id}\n type={field.type === 'date' ? 'date' : 'text'}\n value={typeof value === 'string' || typeof value === 'number' ? String(value) : ''}\n disabled={disabled}\n onChange={(event) => onChange(field.id, event.target.value)}\n />\n </div>\n )\n}\n\n/**\n * Buffers the raw string the user is typing so a value that is not YET a number \u2014\n * `110,` on the way to `110,70` \u2014 does not clear the box. Renders as text rather than\n * type=\"number\" because a type=\"number\" value is sanitized against the BROWSER locale,\n * which discards the separator the application locale displays (issue #5552).\n */\nfunction NumberFieldInput({\n id,\n value,\n disabled,\n onChange,\n}: {\n id: string\n value: unknown\n disabled?: boolean\n onChange: (value: number | undefined) => void\n}) {\n const locale = useOptionalLocale()\n const serializedValue = typeof value === 'string' || typeof value === 'number' ? String(value) : ''\n const [local, setLocal] = React.useState<string>(serializedValue)\n const isFocusedRef = React.useRef(false)\n\n React.useEffect(() => {\n if (!isFocusedRef.current) setLocal(serializedValue)\n }, [serializedValue])\n\n return (\n <Input\n id={id}\n type=\"text\"\n inputMode=\"decimal\"\n // A text input is eligible for autofill and spellcheck; a number field is not.\n autoComplete=\"off\"\n spellCheck={false}\n value={local}\n disabled={disabled}\n onFocus={() => {\n isFocusedRef.current = true\n }}\n onBlur={() => {\n isFocusedRef.current = false\n }}\n onChange={(event) => {\n const raw = event.target.value\n setLocal(raw)\n onChange(raw === '' ? undefined : (parseLocaleNumber(raw, locale) ?? undefined))\n }}\n />\n )\n}\n\nexport default InjectedField\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -37,8 +37,8 @@ async function fetchFeatureGrants(requestFeatures) {
|
|
|
37
37
|
function legacySettingsSectionSlug(groupLabel) {
|
|
38
38
|
return groupLabel.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
39
39
|
}
|
|
40
|
-
function
|
|
41
|
-
const
|
|
40
|
+
function buildContextSections(entries, sectionOrder, pageContext) {
|
|
41
|
+
const contextItems = entries.filter((e) => e.pageContext === pageContext);
|
|
42
42
|
const sectionMap = /* @__PURE__ */ new Map();
|
|
43
43
|
const mapSectionItem = (item) => {
|
|
44
44
|
const itemId = item.href.replace(/\//g, "-").slice(1);
|
|
@@ -53,7 +53,7 @@ function buildSettingsSections(entries, sectionOrder) {
|
|
|
53
53
|
children: item.children?.map(mapSectionItem)
|
|
54
54
|
};
|
|
55
55
|
};
|
|
56
|
-
for (const item of
|
|
56
|
+
for (const item of contextItems) {
|
|
57
57
|
const sectionId = item.groupId;
|
|
58
58
|
const order = sectionOrder[sectionId] ?? sectionOrder[legacySettingsSectionSlug(item.group)] ?? 999;
|
|
59
59
|
if (!sectionMap.has(sectionId)) {
|
|
@@ -81,6 +81,34 @@ function buildSettingsSections(entries, sectionOrder) {
|
|
|
81
81
|
}
|
|
82
82
|
return sections;
|
|
83
83
|
}
|
|
84
|
+
function buildSettingsSections(entries, sectionOrder) {
|
|
85
|
+
return buildContextSections(entries, sectionOrder, "settings");
|
|
86
|
+
}
|
|
87
|
+
function buildProfileSections(entries, sectionOrder) {
|
|
88
|
+
return buildContextSections(entries, sectionOrder, "profile");
|
|
89
|
+
}
|
|
90
|
+
function mergeSectionsWithDiscovered(baseline, discovered) {
|
|
91
|
+
const merged = baseline.map((section) => ({ ...section, items: [...section.items] }));
|
|
92
|
+
const byId = new Map(merged.map((section) => [section.id, section]));
|
|
93
|
+
const knownHrefs = new Set(merged.flatMap((section) => section.items.map((item) => item.href)));
|
|
94
|
+
for (const section of discovered) {
|
|
95
|
+
const items = section.items.filter((item) => !knownHrefs.has(item.href));
|
|
96
|
+
items.forEach((item) => knownHrefs.add(item.href));
|
|
97
|
+
if (items.length === 0) continue;
|
|
98
|
+
const existing = byId.get(section.id);
|
|
99
|
+
if (existing) {
|
|
100
|
+
existing.items.push(...items);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const added = { ...section, items };
|
|
104
|
+
byId.set(added.id, added);
|
|
105
|
+
merged.push(added);
|
|
106
|
+
}
|
|
107
|
+
const sortItems = (items) => [...items].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).map((item) => item.children?.length ? { ...item, children: sortItems(item.children) } : item);
|
|
108
|
+
merged.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
109
|
+
for (const section of merged) section.items = sortItems(section.items);
|
|
110
|
+
return merged;
|
|
111
|
+
}
|
|
84
112
|
function computeSettingsPathPrefixes(sections) {
|
|
85
113
|
const prefixes = /* @__PURE__ */ new Set();
|
|
86
114
|
const visitItem = (item) => {
|
|
@@ -276,8 +304,10 @@ async function buildAdminNav(modules, ctx, userEntities, translate, options) {
|
|
|
276
304
|
}
|
|
277
305
|
export {
|
|
278
306
|
buildAdminNav,
|
|
307
|
+
buildProfileSections,
|
|
279
308
|
buildSettingsSections,
|
|
280
309
|
computeSettingsPathPrefixes,
|
|
281
|
-
convertToSectionNavGroups
|
|
310
|
+
convertToSectionNavGroups,
|
|
311
|
+
mergeSectionsWithDiscovered
|
|
282
312
|
};
|
|
283
313
|
//# sourceMappingURL=nav.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/utils/nav.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ReactNode } from 'react'\nimport React from 'react'\nimport type { Module, ModuleRoute, PageMetadata } from '@open-mercato/shared/modules/registry'\nimport { hasAllFeatures as checkFeatures } from '@open-mercato/shared/security/features'\n\n/** Route with optional page-metadata aliases that may be merged during generation. */\ntype NavRoute = ModuleRoute & Partial<Pick<PageMetadata, 'pageTitleKey' | 'pageGroupKey'>>\n\nexport type AdminNavItem = {\n group: string\n groupId: string\n groupKey?: string\n groupDefaultName: string\n title: string\n defaultTitle: string\n titleKey?: string\n href: string\n enabled: boolean\n hidden?: boolean\n order?: number\n priority?: number\n icon?: ReactNode\n children?: AdminNavItem[]\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n}\n\nexport type AdminNavFeatureChecker = (features: string[]) => Promise<Iterable<string> | null | undefined>\n\nexport type BuildAdminNavOptions = {\n checkFeatures?: AdminNavFeatureChecker\n}\n\n/**\n * @deprecated The internal fetch-based feature check will be removed.\n * Provide `options.checkFeatures` so buildAdminNav can reuse your RBAC context.\n */\nasync function fetchFeatureGrants(requestFeatures: string[]): Promise<Set<string>> { // NOSONAR \u2014 mutable accumulator pattern; Set is populated between early return and final return\n const granted = new Set<string>()\n if (!requestFeatures.length) return granted\n let url = '/api/auth/feature-check'\n let headersInit: Record<string, string> | undefined\n if (typeof window === 'undefined') {\n // On the server, build absolute URL and forward cookies so auth is available\n try {\n const { headers: getHeaders } = await import('next/headers')\n const h = await getHeaders()\n const host = h.get('x-forwarded-host') || h.get('host') || ''\n const proto = h.get('x-forwarded-proto') || 'http'\n const cookie = h.get('cookie') || ''\n if (host) url = `${proto}://${host}/api/auth/feature-check`\n headersInit = { cookie }\n } catch {\n // ignore; fall back to relative URL without forwarded cookies\n }\n }\n try {\n const res = await fetch(url, {\n method: 'POST',\n credentials: 'include' as any,\n headers: { 'content-type': 'application/json', ...(headersInit || {}) },\n body: JSON.stringify({ features: requestFeatures }),\n } as any)\n if (res.ok) {\n const data = await res.json().catch(() => ({ granted: [] }))\n if (Array.isArray(data?.granted)) {\n data.granted.forEach((f: string) => granted.add(f))\n }\n }\n } catch {\n // ignore fetch failures and keep feature set empty\n }\n return granted\n}\n\n/**\n * @deprecated Use number directly in sectionOrder config instead\n */\nexport type SettingsSectionConfig = {\n label: string\n labelKey?: string\n order: number\n}\n\nexport type SettingsSection = {\n id: string\n label: string\n labelKey?: string\n order: number\n items: SettingsSectionItem[]\n}\n\nexport type SettingsSectionItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: ReactNode\n requireFeatures?: string[]\n order: number\n children?: SettingsSectionItem[]\n}\n\n/**\n * Slug of a rendered group label, used as the pre-#4843 settings section id.\n *\n * `sectionOrder` used to be keyed by these slugs, which made ordering depend on the active locale.\n * Kept only as a lookup fallback so third-party callers passing a legacy map keep their weights.\n *\n * @deprecated Key `sectionOrder` by the untranslated group id (`AdminNavItem.groupId`) instead.\n */\nfunction legacySettingsSectionSlug(groupLabel: string): string {\n return groupLabel.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n}\n\n/**\n * Groups the settings-context nav entries into ordered sections.\n *\n * `sectionOrder` is keyed by the untranslated group id (`AdminNavItem.groupId`, i.e. the page's\n * `pageGroupKey` when it declares one) \u2014 the same convention the main sidebar's `defaultGroupOrder`\n * follows. Keying off the rendered label instead made every non-English deployment miss its weights\n * and fall back to the catch-all bucket (#4843).\n */\nexport function buildSettingsSections(\n entries: AdminNavItem[],\n sectionOrder: Record<string, number>\n): SettingsSection[] {\n const settingsItems = entries.filter(e => e.pageContext === 'settings')\n\n const sectionMap = new Map<string, SettingsSection>()\n\n const mapSectionItem = (item: AdminNavItem): SettingsSectionItem => {\n const itemId = item.href.replace(/\\//g, '-').slice(1)\n return {\n id: itemId,\n label: item.title,\n labelKey: item.titleKey,\n href: item.href,\n icon: item.icon,\n requireFeatures: undefined,\n order: item.order ?? item.priority ?? 100,\n children: item.children?.map(mapSectionItem),\n }\n }\n\n for (const item of settingsItems) {\n const sectionId = item.groupId\n const order = sectionOrder[sectionId] ?? sectionOrder[legacySettingsSectionSlug(item.group)] ?? 999\n\n if (!sectionMap.has(sectionId)) {\n sectionMap.set(sectionId, {\n id: sectionId,\n label: item.group,\n labelKey: item.groupKey,\n order,\n items: []\n })\n }\n\n const section = sectionMap.get(sectionId)!\n section.items.push(mapSectionItem(item))\n }\n\n const sections = Array.from(sectionMap.values())\n const sortItems = (items: SettingsSectionItem[]) => {\n items.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))\n for (const item of items) {\n if (item.children?.length) sortItems(item.children)\n }\n }\n sections.sort((a, b) => a.order - b.order)\n for (const section of sections) {\n sortItems(section.items)\n }\n\n return sections\n}\n\nexport function computeSettingsPathPrefixes(sections: SettingsSection[]): string[] {\n const prefixes = new Set<string>()\n const visitItem = (item: SettingsSectionItem) => {\n const parts = item.href.split('/')\n const lastSegment = parts[parts.length - 1]\n if (parts.length > 3 && lastSegment !== 'settings') {\n prefixes.add(parts.slice(0, -1).join('/'))\n }\n prefixes.add(item.href)\n if (item.children?.length) {\n for (const child of item.children) visitItem(child)\n }\n }\n for (const section of sections) {\n for (const item of section.items) {\n visitItem(item)\n }\n }\n return Array.from(prefixes)\n}\n\nexport function convertToSectionNavGroups(\n sections: SettingsSection[],\n translate?: (key: string | undefined, fallback: string) => string\n): Array<{\n id: string\n label: string\n labelKey?: string\n order?: number\n items: ConvertedSectionNavItem[]\n}> {\n const t = translate || ((key, fallback) => fallback)\n const mapSectionItem = (item: SettingsSectionItem): ConvertedSectionNavItem => ({\n id: item.id,\n label: t(item.labelKey, item.label),\n labelKey: item.labelKey,\n href: item.href,\n icon: item.icon,\n order: item.order,\n children: item.children?.map(mapSectionItem),\n })\n\n return sections.map(section => ({\n id: section.id,\n label: t(section.labelKey, section.label),\n labelKey: section.labelKey,\n order: section.order,\n items: section.items.map(mapSectionItem),\n }))\n}\n\ntype ConvertedSectionNavItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: ReactNode\n requireFeatures?: string[]\n order?: number\n children?: ConvertedSectionNavItem[]\n}\n\ntype BuildAdminNavModule = Pick<Module, 'id'> & { backendRoutes?: (ModuleRoute | Omit<ModuleRoute, 'Component'>)[] }\n\nexport async function buildAdminNav(\n modules: BuildAdminNavModule[],\n ctx: { auth?: { roles?: string[]; sub?: string; orgId?: string | null; tenantId?: string | null }; path?: string },\n userEntities?: Array<{ entityId: string; label: string; href: string }>,\n translate?: (key: string | undefined, fallback: string) => string,\n options?: BuildAdminNavOptions\n): Promise<AdminNavItem[]> {\n function capitalize(s: string) {\n return s.charAt(0).toUpperCase() + s.slice(1)\n }\n function deriveTitleFromPath(p: string) {\n const seg = p.split('/').filter(Boolean).pop() || ''\n return seg ? seg.split('-').map(capitalize).join(' ') : 'Home'\n }\n const entries: AdminNavItem[] = []\n\n // Collect all unique features needed across all routes first\n const allRequiredFeatures = new Set<string>()\n for (const m of modules) {\n for (const r of (m.backendRoutes ?? []) as NavRoute[]) {\n const features = r.requireFeatures\n if (features && features.length) {\n features.forEach(f => allRequiredFeatures.add(f))\n }\n }\n }\n\n // Batch check all features in a single API call\n let userFeatures: string[] = []\n if (allRequiredFeatures.size > 0) {\n const requestFeatures = Array.from(allRequiredFeatures)\n if (options?.checkFeatures) {\n try {\n const resolved = await options.checkFeatures(requestFeatures)\n if (resolved) {\n userFeatures = Array.from(resolved).filter((feature): feature is string => typeof feature === 'string' && feature.length > 0)\n }\n } catch {\n // ignore and fall back to empty feature set\n }\n } else {\n userFeatures = Array.from(await fetchFeatureGrants(requestFeatures))\n }\n }\n\n // Helper: check if user has all required features (from cache)\n function hasAllFeatures(required: string[]): boolean {\n if (!required || required.length === 0) return true\n return checkFeatures(userFeatures, required)\n }\n\n // Icons are defined per-page in metadata; no heuristic derivation here.\n for (const m of modules) {\n const groupDefault = capitalize(m.id)\n for (const r of (m.backendRoutes ?? []) as NavRoute[]) {\n const href = r.pattern ?? r.path ?? ''\n if (!href || href.includes('[')) continue\n if (r.navHidden) continue\n const title = r.title || deriveTitleFromPath(href)\n const titleKey = r.pageTitleKey ?? r.titleKey\n const group = r.group || groupDefault\n const groupKey = r.pageGroupKey ?? r.groupKey\n const groupId = groupKey ?? group\n const displayGroup = translate ? translate(groupKey, group) : group\n const displayTitle = translate ? translate(titleKey, title) : title\n const visible = r.visible ? await Promise.resolve(r.visible(ctx)) : true\n if (!visible) continue\n const enabled = r.enabled ? await Promise.resolve(r.enabled(ctx)) : true\n // If roles are required, check; otherwise include\n const required = r.requireRoles || []\n if (required.length) {\n const roles = ctx.auth?.roles || []\n const ok = required.some((role) => roles.includes(role))\n if (!ok) continue\n }\n // If features are required, check from cached batch result\n const features = r.requireFeatures\n if (features && features.length) {\n const ok = hasAllFeatures(features)\n if (!ok) continue\n }\n const order = r.order\n const priority = r.priority ?? order\n const icon = r.icon\n const pageContext = r.pageContext\n entries.push({\n group: displayGroup,\n groupId,\n groupKey,\n groupDefaultName: displayGroup,\n title: displayTitle,\n defaultTitle: displayTitle,\n titleKey,\n href,\n enabled,\n order,\n priority,\n icon,\n pageContext,\n })\n }\n }\n // Build hierarchy: treat routes whose href starts with a parent href + '/'\n // Sort by href length (shortest first) so potential parents are processed before children\n const sorted = [...entries].sort((a, b) => a.href.length - b.href.length)\n const byHref = new Map<string, AdminNavItem>()\n const roots: AdminNavItem[] = []\n for (const e of sorted) {\n // Walk up the href segments to find the longest matching parent in the same group\n let parent: AdminNavItem | undefined\n const segments = e.href.split('/')\n for (let i = segments.length - 1; i >= 2; i--) {\n const candidate = byHref.get(segments.slice(0, i).join('/'))\n if (candidate && candidate !== e && candidate.groupId === e.groupId) {\n parent = candidate\n break\n }\n }\n byHref.set(e.href, e)\n if (parent) {\n parent.children = parent.children || []\n parent.children.push(e)\n } else {\n roots.push(e)\n }\n }\n\n // Add dynamic user entities to the navigation\n if (userEntities && userEntities.length > 0) {\n const tableIcon = React.createElement(\n 'svg',\n { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 },\n React.createElement('rect', { x: 3, y: 4, width: 18, height: 16, rx: 2 }),\n React.createElement('path', { d: 'M3 10h18M9 4v16M15 4v16' }),\n )\n const userEntitiesLegacyGroupKeys = new Set(['settings.sections.dataDesigner', 'entities.nav.group'])\n const userEntitiesItem = entries.find((entry) => entry.href === '/backend/entities/user')\n ?? entries.find((entry) =>\n entry.titleKey === 'entities.nav.userEntities' &&\n typeof entry.groupKey === 'string' &&\n userEntitiesLegacyGroupKeys.has(entry.groupKey),\n )\n if (userEntitiesItem) {\n const existingChildren = userEntitiesItem.children || []\n const dynamicUserEntities = userEntities.map((entity) => ({\n group: userEntitiesItem.group,\n groupId: userEntitiesItem.groupId,\n groupKey: userEntitiesItem.groupKey,\n groupDefaultName: userEntitiesItem.groupDefaultName,\n title: entity.label,\n defaultTitle: entity.label,\n href: entity.href,\n enabled: true,\n order: 1000, // High order to appear at the end\n priority: 1000,\n icon: tableIcon,\n }))\n // Merge and deduplicate by href to avoid duplicates coming from server or generator\n const merged = [...existingChildren, ...dynamicUserEntities]\n const byHref = new Map<string, AdminNavItem>()\n for (const it of merged) {\n if (!byHref.has(it.href)) byHref.set(it.href, it)\n }\n userEntitiesItem.children = Array.from(byHref.values())\n }\n }\n\n // Sorting: group, then priority/order, then title. Apply within children too.\n const sortItems = (arr: AdminNavItem[]) => {\n arr.sort((a, b) => {\n if (a.groupId !== b.groupId) return a.groupId.localeCompare(b.groupId)\n const ap = a.priority ?? a.order ?? 10_000\n const bp = b.priority ?? b.order ?? 10_000\n if (ap !== bp) return ap - bp\n return a.title.localeCompare(b.title)\n })\n for (const it of arr) if (it.children?.length) sortItems(it.children)\n }\n sortItems(roots)\n return roots\n}\n"],
|
|
5
|
-
"mappings": "AACA,OAAO,WAAW;AAElB,SAAS,kBAAkB,qBAAqB;AAiChD,eAAe,mBAAmB,iBAAiD;AACjF,QAAM,UAAU,oBAAI,IAAY;AAChC,MAAI,CAAC,gBAAgB,OAAQ,QAAO;AACpC,MAAI,MAAM;AACV,MAAI;AACJ,MAAI,OAAO,WAAW,aAAa;AAEjC,QAAI;AACF,YAAM,EAAE,SAAS,WAAW,IAAI,MAAM,OAAO,cAAc;AAC3D,YAAM,IAAI,MAAM,WAAW;AAC3B,YAAM,OAAO,EAAE,IAAI,kBAAkB,KAAK,EAAE,IAAI,MAAM,KAAK;AAC3D,YAAM,QAAQ,EAAE,IAAI,mBAAmB,KAAK;AAC5C,YAAM,SAAS,EAAE,IAAI,QAAQ,KAAK;AAClC,UAAI,KAAM,OAAM,GAAG,KAAK,MAAM,IAAI;AAClC,oBAAc,EAAE,OAAO;AAAA,IACzB,QAAQ;AAAA,IAER;AAAA,EACF;AACA,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS,EAAE,gBAAgB,oBAAoB,GAAI,eAAe,CAAC,EAAG;AAAA,MACtE,MAAM,KAAK,UAAU,EAAE,UAAU,gBAAgB,CAAC;AAAA,IACpD,CAAQ;AACR,QAAI,IAAI,IAAI;AACV,YAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,EAAE,SAAS,CAAC,EAAE,EAAE;AAC3D,UAAI,MAAM,QAAQ,MAAM,OAAO,GAAG;AAChC,aAAK,QAAQ,QAAQ,CAAC,MAAc,QAAQ,IAAI,CAAC,CAAC;AAAA,MACpD;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAsCA,SAAS,0BAA0B,YAA4B;AAC7D,SAAO,WAAW,YAAY,EAAE,QAAQ,QAAQ,GAAG,EAAE,QAAQ,eAAe,EAAE;AAChF;
|
|
4
|
+
"sourcesContent": ["import type { ReactNode } from 'react'\nimport React from 'react'\nimport type { Module, ModuleRoute, PageMetadata } from '@open-mercato/shared/modules/registry'\nimport { hasAllFeatures as checkFeatures } from '@open-mercato/shared/security/features'\n\n/** Route with optional page-metadata aliases that may be merged during generation. */\ntype NavRoute = ModuleRoute & Partial<Pick<PageMetadata, 'pageTitleKey' | 'pageGroupKey'>>\n\nexport type AdminNavItem = {\n group: string\n groupId: string\n groupKey?: string\n groupDefaultName: string\n title: string\n defaultTitle: string\n titleKey?: string\n href: string\n enabled: boolean\n hidden?: boolean\n order?: number\n priority?: number\n icon?: ReactNode\n children?: AdminNavItem[]\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n}\n\nexport type AdminNavFeatureChecker = (features: string[]) => Promise<Iterable<string> | null | undefined>\n\nexport type BuildAdminNavOptions = {\n checkFeatures?: AdminNavFeatureChecker\n}\n\n/**\n * @deprecated The internal fetch-based feature check will be removed.\n * Provide `options.checkFeatures` so buildAdminNav can reuse your RBAC context.\n */\nasync function fetchFeatureGrants(requestFeatures: string[]): Promise<Set<string>> { // NOSONAR \u2014 mutable accumulator pattern; Set is populated between early return and final return\n const granted = new Set<string>()\n if (!requestFeatures.length) return granted\n let url = '/api/auth/feature-check'\n let headersInit: Record<string, string> | undefined\n if (typeof window === 'undefined') {\n // On the server, build absolute URL and forward cookies so auth is available\n try {\n const { headers: getHeaders } = await import('next/headers')\n const h = await getHeaders()\n const host = h.get('x-forwarded-host') || h.get('host') || ''\n const proto = h.get('x-forwarded-proto') || 'http'\n const cookie = h.get('cookie') || ''\n if (host) url = `${proto}://${host}/api/auth/feature-check`\n headersInit = { cookie }\n } catch {\n // ignore; fall back to relative URL without forwarded cookies\n }\n }\n try {\n const res = await fetch(url, {\n method: 'POST',\n credentials: 'include' as any,\n headers: { 'content-type': 'application/json', ...(headersInit || {}) },\n body: JSON.stringify({ features: requestFeatures }),\n } as any)\n if (res.ok) {\n const data = await res.json().catch(() => ({ granted: [] }))\n if (Array.isArray(data?.granted)) {\n data.granted.forEach((f: string) => granted.add(f))\n }\n }\n } catch {\n // ignore fetch failures and keep feature set empty\n }\n return granted\n}\n\n/**\n * @deprecated Use number directly in sectionOrder config instead\n */\nexport type SettingsSectionConfig = {\n label: string\n labelKey?: string\n order: number\n}\n\nexport type SettingsSection = {\n id: string\n label: string\n labelKey?: string\n order: number\n items: SettingsSectionItem[]\n}\n\nexport type SettingsSectionItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: ReactNode\n requireFeatures?: string[]\n order: number\n children?: SettingsSectionItem[]\n}\n\n/**\n * Slug of a rendered group label, used as the pre-#4843 settings section id.\n *\n * `sectionOrder` used to be keyed by these slugs, which made ordering depend on the active locale.\n * Kept only as a lookup fallback so third-party callers passing a legacy map keep their weights.\n *\n * @deprecated Key `sectionOrder` by the untranslated group id (`AdminNavItem.groupId`) instead.\n */\nfunction legacySettingsSectionSlug(groupLabel: string): string {\n return groupLabel.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n}\n\n/**\n * Groups the nav entries declaring `pageContext` into ordered sections for that context's sidebar.\n *\n * `sectionOrder` is keyed by the untranslated group id (`AdminNavItem.groupId`, i.e. the page's\n * `pageGroupKey` when it declares one) \u2014 the same convention the main sidebar's `defaultGroupOrder`\n * follows. Keying off the rendered label instead made every non-English deployment miss its weights\n * and fall back to the catch-all bucket (#4843).\n */\nfunction buildContextSections(\n entries: AdminNavItem[],\n sectionOrder: Record<string, number>,\n pageContext: 'settings' | 'profile'\n): SettingsSection[] {\n const contextItems = entries.filter(e => e.pageContext === pageContext)\n\n const sectionMap = new Map<string, SettingsSection>()\n\n const mapSectionItem = (item: AdminNavItem): SettingsSectionItem => {\n const itemId = item.href.replace(/\\//g, '-').slice(1)\n return {\n id: itemId,\n label: item.title,\n labelKey: item.titleKey,\n href: item.href,\n icon: item.icon,\n requireFeatures: undefined,\n order: item.order ?? item.priority ?? 100,\n children: item.children?.map(mapSectionItem),\n }\n }\n\n for (const item of contextItems) {\n const sectionId = item.groupId\n const order = sectionOrder[sectionId] ?? sectionOrder[legacySettingsSectionSlug(item.group)] ?? 999\n\n if (!sectionMap.has(sectionId)) {\n sectionMap.set(sectionId, {\n id: sectionId,\n label: item.group,\n labelKey: item.groupKey,\n order,\n items: []\n })\n }\n\n const section = sectionMap.get(sectionId)!\n section.items.push(mapSectionItem(item))\n }\n\n const sections = Array.from(sectionMap.values())\n const sortItems = (items: SettingsSectionItem[]) => {\n items.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))\n for (const item of items) {\n if (item.children?.length) sortItems(item.children)\n }\n }\n sections.sort((a, b) => a.order - b.order)\n for (const section of sections) {\n sortItems(section.items)\n }\n\n return sections\n}\n\n/**\n * Groups the settings-context nav entries into ordered sections for the settings sidebar.\n */\nexport function buildSettingsSections(\n entries: AdminNavItem[],\n sectionOrder: Record<string, number>\n): SettingsSection[] {\n return buildContextSections(entries, sectionOrder, 'settings')\n}\n\n/**\n * Groups the profile-context nav entries into ordered sections for the profile sidebar.\n *\n * The profile sidebar used to render a hard-coded list, so per-user pages contributed by other\n * modules (notification preferences, communication channels) were reachable from the profile\n * dropdown but missing from the sidebar (#5594). Deriving them from `pageContext: 'profile'` keeps\n * both surfaces in sync, including for third-party modules.\n */\nexport function buildProfileSections(\n entries: AdminNavItem[],\n sectionOrder: Record<string, number>\n): SettingsSection[] {\n return buildContextSections(entries, sectionOrder, 'profile')\n}\n\n/**\n * Folds discovered sections into a statically declared baseline, keyed by section id and item href.\n *\n * The baseline carries entries that route discovery cannot see \u2014 a `navHidden` page such as\n * `/backend/profile/change-password` never reaches `buildAdminNav`'s output \u2014 so it wins on\n * conflicts and keeps its declared position, while discovered items extend the matching section or\n * append a new one.\n *\n * The baseline is never mutated: sections, their item arrays and any nested `children` are copied\n * before sorting, because callers pass module-level constants that outlive a single request.\n */\nexport function mergeSectionsWithDiscovered(\n baseline: SettingsSection[],\n discovered: SettingsSection[]\n): SettingsSection[] {\n const merged = baseline.map((section) => ({ ...section, items: [...section.items] }))\n const byId = new Map(merged.map((section) => [section.id, section]))\n const knownHrefs = new Set(merged.flatMap((section) => section.items.map((item) => item.href)))\n\n for (const section of discovered) {\n const items = section.items.filter((item) => !knownHrefs.has(item.href))\n items.forEach((item) => knownHrefs.add(item.href))\n if (items.length === 0) continue\n const existing = byId.get(section.id)\n if (existing) {\n existing.items.push(...items)\n continue\n }\n const added = { ...section, items }\n byId.set(added.id, added)\n merged.push(added)\n }\n\n const sortItems = (items: SettingsSectionItem[]): SettingsSectionItem[] =>\n [...items]\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))\n .map((item) => (item.children?.length ? { ...item, children: sortItems(item.children) } : item))\n\n merged.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))\n for (const section of merged) section.items = sortItems(section.items)\n\n return merged\n}\n\nexport function computeSettingsPathPrefixes(sections: SettingsSection[]): string[] {\n const prefixes = new Set<string>()\n const visitItem = (item: SettingsSectionItem) => {\n const parts = item.href.split('/')\n const lastSegment = parts[parts.length - 1]\n if (parts.length > 3 && lastSegment !== 'settings') {\n prefixes.add(parts.slice(0, -1).join('/'))\n }\n prefixes.add(item.href)\n if (item.children?.length) {\n for (const child of item.children) visitItem(child)\n }\n }\n for (const section of sections) {\n for (const item of section.items) {\n visitItem(item)\n }\n }\n return Array.from(prefixes)\n}\n\nexport function convertToSectionNavGroups(\n sections: SettingsSection[],\n translate?: (key: string | undefined, fallback: string) => string\n): Array<{\n id: string\n label: string\n labelKey?: string\n order?: number\n items: ConvertedSectionNavItem[]\n}> {\n const t = translate || ((key, fallback) => fallback)\n const mapSectionItem = (item: SettingsSectionItem): ConvertedSectionNavItem => ({\n id: item.id,\n label: t(item.labelKey, item.label),\n labelKey: item.labelKey,\n href: item.href,\n icon: item.icon,\n order: item.order,\n children: item.children?.map(mapSectionItem),\n })\n\n return sections.map(section => ({\n id: section.id,\n label: t(section.labelKey, section.label),\n labelKey: section.labelKey,\n order: section.order,\n items: section.items.map(mapSectionItem),\n }))\n}\n\ntype ConvertedSectionNavItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: ReactNode\n requireFeatures?: string[]\n order?: number\n children?: ConvertedSectionNavItem[]\n}\n\ntype BuildAdminNavModule = Pick<Module, 'id'> & { backendRoutes?: (ModuleRoute | Omit<ModuleRoute, 'Component'>)[] }\n\nexport async function buildAdminNav(\n modules: BuildAdminNavModule[],\n ctx: { auth?: { roles?: string[]; sub?: string; orgId?: string | null; tenantId?: string | null }; path?: string },\n userEntities?: Array<{ entityId: string; label: string; href: string }>,\n translate?: (key: string | undefined, fallback: string) => string,\n options?: BuildAdminNavOptions\n): Promise<AdminNavItem[]> {\n function capitalize(s: string) {\n return s.charAt(0).toUpperCase() + s.slice(1)\n }\n function deriveTitleFromPath(p: string) {\n const seg = p.split('/').filter(Boolean).pop() || ''\n return seg ? seg.split('-').map(capitalize).join(' ') : 'Home'\n }\n const entries: AdminNavItem[] = []\n\n // Collect all unique features needed across all routes first\n const allRequiredFeatures = new Set<string>()\n for (const m of modules) {\n for (const r of (m.backendRoutes ?? []) as NavRoute[]) {\n const features = r.requireFeatures\n if (features && features.length) {\n features.forEach(f => allRequiredFeatures.add(f))\n }\n }\n }\n\n // Batch check all features in a single API call\n let userFeatures: string[] = []\n if (allRequiredFeatures.size > 0) {\n const requestFeatures = Array.from(allRequiredFeatures)\n if (options?.checkFeatures) {\n try {\n const resolved = await options.checkFeatures(requestFeatures)\n if (resolved) {\n userFeatures = Array.from(resolved).filter((feature): feature is string => typeof feature === 'string' && feature.length > 0)\n }\n } catch {\n // ignore and fall back to empty feature set\n }\n } else {\n userFeatures = Array.from(await fetchFeatureGrants(requestFeatures))\n }\n }\n\n // Helper: check if user has all required features (from cache)\n function hasAllFeatures(required: string[]): boolean {\n if (!required || required.length === 0) return true\n return checkFeatures(userFeatures, required)\n }\n\n // Icons are defined per-page in metadata; no heuristic derivation here.\n for (const m of modules) {\n const groupDefault = capitalize(m.id)\n for (const r of (m.backendRoutes ?? []) as NavRoute[]) {\n const href = r.pattern ?? r.path ?? ''\n if (!href || href.includes('[')) continue\n if (r.navHidden) continue\n const title = r.title || deriveTitleFromPath(href)\n const titleKey = r.pageTitleKey ?? r.titleKey\n const group = r.group || groupDefault\n const groupKey = r.pageGroupKey ?? r.groupKey\n const groupId = groupKey ?? group\n const displayGroup = translate ? translate(groupKey, group) : group\n const displayTitle = translate ? translate(titleKey, title) : title\n const visible = r.visible ? await Promise.resolve(r.visible(ctx)) : true\n if (!visible) continue\n const enabled = r.enabled ? await Promise.resolve(r.enabled(ctx)) : true\n // If roles are required, check; otherwise include\n const required = r.requireRoles || []\n if (required.length) {\n const roles = ctx.auth?.roles || []\n const ok = required.some((role) => roles.includes(role))\n if (!ok) continue\n }\n // If features are required, check from cached batch result\n const features = r.requireFeatures\n if (features && features.length) {\n const ok = hasAllFeatures(features)\n if (!ok) continue\n }\n const order = r.order\n const priority = r.priority ?? order\n const icon = r.icon\n const pageContext = r.pageContext\n entries.push({\n group: displayGroup,\n groupId,\n groupKey,\n groupDefaultName: displayGroup,\n title: displayTitle,\n defaultTitle: displayTitle,\n titleKey,\n href,\n enabled,\n order,\n priority,\n icon,\n pageContext,\n })\n }\n }\n // Build hierarchy: treat routes whose href starts with a parent href + '/'\n // Sort by href length (shortest first) so potential parents are processed before children\n const sorted = [...entries].sort((a, b) => a.href.length - b.href.length)\n const byHref = new Map<string, AdminNavItem>()\n const roots: AdminNavItem[] = []\n for (const e of sorted) {\n // Walk up the href segments to find the longest matching parent in the same group\n let parent: AdminNavItem | undefined\n const segments = e.href.split('/')\n for (let i = segments.length - 1; i >= 2; i--) {\n const candidate = byHref.get(segments.slice(0, i).join('/'))\n if (candidate && candidate !== e && candidate.groupId === e.groupId) {\n parent = candidate\n break\n }\n }\n byHref.set(e.href, e)\n if (parent) {\n parent.children = parent.children || []\n parent.children.push(e)\n } else {\n roots.push(e)\n }\n }\n\n // Add dynamic user entities to the navigation\n if (userEntities && userEntities.length > 0) {\n const tableIcon = React.createElement(\n 'svg',\n { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 },\n React.createElement('rect', { x: 3, y: 4, width: 18, height: 16, rx: 2 }),\n React.createElement('path', { d: 'M3 10h18M9 4v16M15 4v16' }),\n )\n const userEntitiesLegacyGroupKeys = new Set(['settings.sections.dataDesigner', 'entities.nav.group'])\n const userEntitiesItem = entries.find((entry) => entry.href === '/backend/entities/user')\n ?? entries.find((entry) =>\n entry.titleKey === 'entities.nav.userEntities' &&\n typeof entry.groupKey === 'string' &&\n userEntitiesLegacyGroupKeys.has(entry.groupKey),\n )\n if (userEntitiesItem) {\n const existingChildren = userEntitiesItem.children || []\n const dynamicUserEntities = userEntities.map((entity) => ({\n group: userEntitiesItem.group,\n groupId: userEntitiesItem.groupId,\n groupKey: userEntitiesItem.groupKey,\n groupDefaultName: userEntitiesItem.groupDefaultName,\n title: entity.label,\n defaultTitle: entity.label,\n href: entity.href,\n enabled: true,\n order: 1000, // High order to appear at the end\n priority: 1000,\n icon: tableIcon,\n }))\n // Merge and deduplicate by href to avoid duplicates coming from server or generator\n const merged = [...existingChildren, ...dynamicUserEntities]\n const byHref = new Map<string, AdminNavItem>()\n for (const it of merged) {\n if (!byHref.has(it.href)) byHref.set(it.href, it)\n }\n userEntitiesItem.children = Array.from(byHref.values())\n }\n }\n\n // Sorting: group, then priority/order, then title. Apply within children too.\n const sortItems = (arr: AdminNavItem[]) => {\n arr.sort((a, b) => {\n if (a.groupId !== b.groupId) return a.groupId.localeCompare(b.groupId)\n const ap = a.priority ?? a.order ?? 10_000\n const bp = b.priority ?? b.order ?? 10_000\n if (ap !== bp) return ap - bp\n return a.title.localeCompare(b.title)\n })\n for (const it of arr) if (it.children?.length) sortItems(it.children)\n }\n sortItems(roots)\n return roots\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["byHref"]
|
|
7
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { format as formatDateFns } from "date-fns/format";
|
|
2
|
+
import { parseISO } from "date-fns/parseISO";
|
|
2
3
|
const DAY_FIRST_LOCALE_CODES = /* @__PURE__ */ new Set([
|
|
3
4
|
"pl",
|
|
4
5
|
"de",
|
|
@@ -43,11 +44,77 @@ function formatWithPublicDateFormat(date, format, locale) {
|
|
|
43
44
|
return null;
|
|
44
45
|
}
|
|
45
46
|
}
|
|
47
|
+
function resolveDisplayDateFormat() {
|
|
48
|
+
return normalizeDateFormatPattern(process.env.NEXT_PUBLIC_OM_DATE_FORMAT) ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_DATE_FORMAT);
|
|
49
|
+
}
|
|
50
|
+
function resolveDisplayDateTimeFormat() {
|
|
51
|
+
return normalizeDateFormatPattern(process.env.NEXT_PUBLIC_OM_DATE_TIME_FORMAT) ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_DATE_TIME_FORMAT) ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_OM_DATE_FORMAT) ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_DATE_FORMAT);
|
|
52
|
+
}
|
|
53
|
+
function parseDisplayValue(value) {
|
|
54
|
+
if (value == null) return null;
|
|
55
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
|
|
56
|
+
const trimmed = value.trim();
|
|
57
|
+
if (!trimmed) return null;
|
|
58
|
+
const parsed = parseISO(trimmed);
|
|
59
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
60
|
+
}
|
|
61
|
+
function intlLocale(locale) {
|
|
62
|
+
if (!locale) return void 0;
|
|
63
|
+
return typeof locale === "string" ? locale : locale.code;
|
|
64
|
+
}
|
|
65
|
+
const DISPLAY_STYLES = {
|
|
66
|
+
date: { dateStyle: "medium" },
|
|
67
|
+
datetime: { dateStyle: "medium", timeStyle: "short" }
|
|
68
|
+
};
|
|
69
|
+
const formatterCache = /* @__PURE__ */ new Map();
|
|
70
|
+
function getFormatter(locale, style) {
|
|
71
|
+
const key = `${locale ?? ""}|${style}`;
|
|
72
|
+
const cached = formatterCache.get(key);
|
|
73
|
+
if (cached) return cached;
|
|
74
|
+
const created = new Intl.DateTimeFormat(locale, DISPLAY_STYLES[style]);
|
|
75
|
+
formatterCache.set(key, created);
|
|
76
|
+
return created;
|
|
77
|
+
}
|
|
78
|
+
function formatDisplayDate(value, locale) {
|
|
79
|
+
const parsed = parseDisplayValue(value);
|
|
80
|
+
if (!parsed) return null;
|
|
81
|
+
const pattern = resolveDisplayDateFormat();
|
|
82
|
+
if (pattern) return formatWithPublicDateFormat(parsed, pattern);
|
|
83
|
+
return getFormatter(intlLocale(locale), "date").format(parsed);
|
|
84
|
+
}
|
|
85
|
+
function formatDisplayDateTime(value, locale) {
|
|
86
|
+
const parsed = parseDisplayValue(value);
|
|
87
|
+
if (!parsed) return null;
|
|
88
|
+
const pattern = resolveDisplayDateTimeFormat();
|
|
89
|
+
if (pattern) return formatWithPublicDateFormat(parsed, pattern);
|
|
90
|
+
return getFormatter(intlLocale(locale), "datetime").format(parsed);
|
|
91
|
+
}
|
|
92
|
+
function toDateInputValue(value) {
|
|
93
|
+
const parsed = parseDisplayValue(value);
|
|
94
|
+
return parsed ? formatWithPublicDateFormat(parsed, "yyyy-MM-dd") : null;
|
|
95
|
+
}
|
|
96
|
+
function toUtcDateInputValue(value) {
|
|
97
|
+
if (value == null) return null;
|
|
98
|
+
if (typeof value === "string") {
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
if (!trimmed) return null;
|
|
101
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) return trimmed;
|
|
102
|
+
const parsed = new Date(trimmed);
|
|
103
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString().slice(0, 10);
|
|
104
|
+
}
|
|
105
|
+
return Number.isNaN(value.getTime()) ? null : value.toISOString().slice(0, 10);
|
|
106
|
+
}
|
|
46
107
|
export {
|
|
47
108
|
deriveDateDisplayFormat,
|
|
109
|
+
formatDisplayDate,
|
|
110
|
+
formatDisplayDateTime,
|
|
48
111
|
formatWithPublicDateFormat,
|
|
49
112
|
normalizeDateFormatPattern,
|
|
113
|
+
resolveDisplayDateFormat,
|
|
114
|
+
resolveDisplayDateTimeFormat,
|
|
50
115
|
resolvePublicDateFormat,
|
|
51
|
-
resolvePublicDateTimeFormat
|
|
116
|
+
resolvePublicDateTimeFormat,
|
|
117
|
+
toDateInputValue,
|
|
118
|
+
toUtcDateInputValue
|
|
52
119
|
};
|
|
53
120
|
//# sourceMappingURL=date-format.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/date-format.ts"],
|
|
4
|
-
"sourcesContent": ["import { format as formatDateFns } from 'date-fns/format'\nimport type { Locale } from 'date-fns/locale'\n\ntype LocaleLike = Locale | string | null | undefined\n\nconst DAY_FIRST_LOCALE_CODES = new Set([\n 'pl', 'de', 'fr', 'es', 'it', 'pt', 'nl', 'ru', 'cs', 'sk', 'hu', 'ro',\n])\n\nconst SYSTEM_FORMAT_VALUES = new Set(['auto', 'default', 'locale', 'system'])\n\nfunction getLocaleCode(locale?: LocaleLike): string {\n if (!locale) return ''\n if (typeof locale === 'string') return locale.split('-')[0]?.toLowerCase() ?? ''\n return locale.code?.split('-')[0]?.toLowerCase() ?? ''\n}\n\nexport function normalizeDateFormatPattern(value: string | null | undefined): string | null {\n const trimmed = value?.trim()\n if (!trimmed) return null\n if (SYSTEM_FORMAT_VALUES.has(trimmed.toLowerCase())) return null\n return trimmed\n .replace(/YYYY/g, 'yyyy')\n .replace(/YY/g, 'yy')\n .replace(/DD/g, 'dd')\n}\n\nexport function deriveDateDisplayFormat(locale?: LocaleLike): string {\n const code = getLocaleCode(locale)\n return code && DAY_FIRST_LOCALE_CODES.has(code) ? 'd MMM yyyy' : 'MMM d, yyyy'\n}\n\nexport function resolvePublicDateFormat(locale?: LocaleLike, explicitFormat?: string | null): string {\n return (\n normalizeDateFormatPattern(explicitFormat)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_OM_DATE_FORMAT)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_DATE_FORMAT)\n ?? deriveDateDisplayFormat(locale)\n )\n}\n\nexport function resolvePublicDateTimeFormat(locale?: LocaleLike, explicitFormat?: string | null): string {\n const dateFormat = deriveDateDisplayFormat(locale)\n return (\n normalizeDateFormatPattern(explicitFormat)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_OM_DATE_TIME_FORMAT)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_DATE_TIME_FORMAT)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_OM_DATE_FORMAT)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_DATE_FORMAT)\n ?? `${dateFormat} HH:mm`\n )\n}\n\nexport function formatWithPublicDateFormat(date: Date, format: string, locale?: Locale): string | null {\n try {\n return formatDateFns(date, format, locale ? { locale } : undefined)\n } catch {\n return null\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,UAAU,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import { format as formatDateFns } from 'date-fns/format'\nimport { parseISO } from 'date-fns/parseISO'\nimport type { Locale } from 'date-fns/locale'\n\ntype LocaleLike = Locale | string | null | undefined\n\nconst DAY_FIRST_LOCALE_CODES = new Set([\n 'pl', 'de', 'fr', 'es', 'it', 'pt', 'nl', 'ru', 'cs', 'sk', 'hu', 'ro',\n])\n\nconst SYSTEM_FORMAT_VALUES = new Set(['auto', 'default', 'locale', 'system'])\n\nfunction getLocaleCode(locale?: LocaleLike): string {\n if (!locale) return ''\n if (typeof locale === 'string') return locale.split('-')[0]?.toLowerCase() ?? ''\n return locale.code?.split('-')[0]?.toLowerCase() ?? ''\n}\n\nexport function normalizeDateFormatPattern(value: string | null | undefined): string | null {\n const trimmed = value?.trim()\n if (!trimmed) return null\n if (SYSTEM_FORMAT_VALUES.has(trimmed.toLowerCase())) return null\n return trimmed\n .replace(/YYYY/g, 'yyyy')\n .replace(/YY/g, 'yy')\n .replace(/DD/g, 'dd')\n}\n\nexport function deriveDateDisplayFormat(locale?: LocaleLike): string {\n const code = getLocaleCode(locale)\n return code && DAY_FIRST_LOCALE_CODES.has(code) ? 'd MMM yyyy' : 'MMM d, yyyy'\n}\n\nexport function resolvePublicDateFormat(locale?: LocaleLike, explicitFormat?: string | null): string {\n return (\n normalizeDateFormatPattern(explicitFormat)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_OM_DATE_FORMAT)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_DATE_FORMAT)\n ?? deriveDateDisplayFormat(locale)\n )\n}\n\nexport function resolvePublicDateTimeFormat(locale?: LocaleLike, explicitFormat?: string | null): string {\n const dateFormat = deriveDateDisplayFormat(locale)\n return (\n normalizeDateFormatPattern(explicitFormat)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_OM_DATE_TIME_FORMAT)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_DATE_TIME_FORMAT)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_OM_DATE_FORMAT)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_DATE_FORMAT)\n ?? `${dateFormat} HH:mm`\n )\n}\n\nexport function formatWithPublicDateFormat(date: Date, format: string, locale?: Locale): string | null {\n try {\n return formatDateFns(date, format, locale ? { locale } : undefined)\n } catch {\n return null\n }\n}\n\n/**\n * The configured pattern for a date rendered as static text, or `null` when none is set.\n *\n * `null` is the interesting case: with no override the format comes from `Intl`, which knows the\n * ordering and month naming of every locale. A pattern heuristic does not \u2014 it can only ask\n * \"day first or not\", which is wrong for `ko` (year first) and for `de` (numeric, dotted). Those\n * are two of the five locales this repo ships, and this resolver feeds every backend table.\n *\n * ISO stays the contract for machine-facing values \u2014 `toDateInputValue`, storage, serialization \u2014\n * never for a label.\n *\n * A pattern is a deliberate operator pin: it wins over the locale entirely, and month tokens render\n * in English on that path \u2014 `NEXT_PUBLIC_OM_DATE_FORMAT=d MMM yyyy` gives `1 Jul 2026` to a `pl`\n * tenant, not `1 lip 2026`. Set a numeric pattern, or leave it unset and let `Intl` localize.\n */\nexport function resolveDisplayDateFormat(): string | null {\n return (\n normalizeDateFormatPattern(process.env.NEXT_PUBLIC_OM_DATE_FORMAT)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_DATE_FORMAT)\n )\n}\n\n/**\n * As `resolveDisplayDateFormat`, for a value that carries a real time of day.\n *\n * The date vars are in the chain, and used bare, because a caller that sets only a date pattern has\n * said how a date should look and said nothing about time \u2014 appending `HH:mm` would invent a\n * requirement. This is the precedence `resolvePublicDateTimeFormat` already uses.\n */\nexport function resolveDisplayDateTimeFormat(): string | null {\n return (\n normalizeDateFormatPattern(process.env.NEXT_PUBLIC_OM_DATE_TIME_FORMAT)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_DATE_TIME_FORMAT)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_OM_DATE_FORMAT)\n ?? normalizeDateFormatPattern(process.env.NEXT_PUBLIC_DATE_FORMAT)\n )\n}\n\n/**\n * **ISO-8601 input only.** `parseISO` rejects shapes `new Date` would have accepted \u2014 `'July 1, 2026'`,\n * `'2026/07/01'` \u2014 so those now return `null` and the caller renders its empty label. Every in-repo\n * caller passes ISO; `DataTable.tryParseDate` keeps a `new Date` fallback because its input is\n * arbitrary column data, while these helpers' is not.\n *\n * `parseISO`, not `new Date`: a bare `yyyy-MM-dd` names a calendar day, and `new Date` reads it as\n * UTC midnight \u2014 which `format` then renders as the PREVIOUS day in every zone west of UTC. On a\n * date-only string `parseISO` returns local midnight of the stored day, so the day survives; a value\n * carrying a time or an offset keeps its instant either way.\n */\nfunction parseDisplayValue(value: string | Date | null | undefined): Date | null {\n if (value == null) return null\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value\n const trimmed = value.trim()\n if (!trimmed) return null\n const parsed = parseISO(trimmed)\n return Number.isNaN(parsed.getTime()) ? null : parsed\n}\n\nfunction intlLocale(locale?: LocaleLike): string | undefined {\n if (!locale) return undefined\n return typeof locale === 'string' ? locale : locale.code\n}\n\nconst DISPLAY_STYLES = {\n date: { dateStyle: 'medium' },\n datetime: { dateStyle: 'medium', timeStyle: 'short' },\n} as const satisfies Record<string, Intl.DateTimeFormatOptions>\n\n// Constructing an `Intl.DateTimeFormat` costs ~40x using one (Node 24: ~31 \u00B5s vs ~0.8 \u00B5s), and these\n// helpers render every date cell AND its tooltip in `DataTable` \u2014 two constructions per cell, so a\n// 100-row page with two date columns would pay 400 of them on every sort, filter and keystroke.\n// A formatter is immutable and the key space is the locales a deployment ships, so one instance per\n// (locale, style) is kept for the process. The style name is the key, so the two cannot desync.\nconst formatterCache = new Map<string, Intl.DateTimeFormat>()\n\nfunction getFormatter(locale: string | undefined, style: keyof typeof DISPLAY_STYLES): Intl.DateTimeFormat {\n const key = `${locale ?? ''}|${style}`\n const cached = formatterCache.get(key)\n if (cached) return cached\n const created = new Intl.DateTimeFormat(locale, DISPLAY_STYLES[style])\n formatterCache.set(key, created)\n return created\n}\n\n/** Render a date for display, or `null` when there is nothing valid to show. */\nexport function formatDisplayDate(value: string | Date | null | undefined, locale?: LocaleLike): string | null {\n const parsed = parseDisplayValue(value)\n if (!parsed) return null\n const pattern = resolveDisplayDateFormat()\n if (pattern) return formatWithPublicDateFormat(parsed, pattern)\n return getFormatter(intlLocale(locale), 'date').format(parsed)\n}\n\n/** Render a timestamp for display, or `null` when there is nothing valid to show. */\nexport function formatDisplayDateTime(value: string | Date | null | undefined, locale?: LocaleLike): string | null {\n const parsed = parseDisplayValue(value)\n if (!parsed) return null\n const pattern = resolveDisplayDateTimeFormat()\n if (pattern) return formatWithPublicDateFormat(parsed, pattern)\n return getFormatter(intlLocale(locale), 'datetime').format(parsed)\n}\n\n/**\n * The **local** calendar day of a real instant, as the `yyyy-MM-dd` an `<input type=\"date\">` requires.\n *\n * Not `new Date(value).toISOString().slice(0, 10)`: `toISOString` converts to UTC first, so east of\n * UTC an evening timestamp yields the previous day. Rendering that through `formatDisplayDate` is\n * faithful to a day that was already wrong.\n *\n * **Precondition: `value` must be a real instant** \u2014 a moment that happened, whose local day is the\n * one a human would name. It must NOT be a date-only value that some write path stored as UTC\n * midnight: reading that back locally names the PREVIOUS day west of UTC, which is the mirror image\n * of the bug above. Use `toUtcDateInputValue` for those; see its note for how to tell them apart.\n */\nexport function toDateInputValue(value: string | Date | null | undefined): string | null {\n const parsed = parseDisplayValue(value)\n return parsed ? formatWithPublicDateFormat(parsed, 'yyyy-MM-dd') : null\n}\n\n/**\n * The **UTC** calendar day of a value, as the `yyyy-MM-dd` an `<input type=\"date\">` requires.\n *\n * For a column that stores a date-only value as UTC midnight \u2014 a bare `yyyy-MM-dd` submitted by a\n * date input, coerced with `z.coerce.date()` and returned as `\u2026T00:00:00.000Z`. The stored instant\n * carries no local meaning, so the round trip only closes if it is read back in the same frame it\n * was written in: UTC.\n *\n * Which helper a field needs is decided by its WRITE path, not by its type. If the value can be set\n * from a date input, it is a UTC day; if it is only ever stamped from a clock, it is an instant.\n */\nexport function toUtcDateInputValue(value: string | Date | null | undefined): string | null {\n if (value == null) return null\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed) return null\n // A bare calendar day is already the answer. Parsing it into an instant first \u2014 by either\n // reading \u2014 can only move it, since there is no zone in which it was meant.\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(trimmed)) return trimmed\n const parsed = new Date(trimmed)\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString().slice(0, 10)\n }\n return Number.isNaN(value.getTime()) ? null : value.toISOString().slice(0, 10)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7170.1.d95074d7ba",
|
|
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.7.1-develop.
|
|
158
|
+
"@open-mercato/shared": "0.7.1-develop.7170.1.d95074d7ba",
|
|
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.7.1-develop.
|
|
165
|
+
"@open-mercato/shared": "0.7.1-develop.7170.1.d95074d7ba",
|
|
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",
|