@open-mercato/ui 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6824.1.21fff90682
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/AGENTS.md +4 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +1 -1
- package/dist/backend/utils/nav.js +5 -2
- package/dist/backend/utils/nav.js.map +2 -2
- package/figma/alert.figma.tsx +35 -0
- package/figma/badge.figma.tsx +33 -0
- package/figma/button.figma.tsx +33 -0
- package/figma/checkbox.figma.tsx +28 -0
- package/figma/drawer.figma.tsx +40 -0
- package/figma/input-variants.figma.tsx +58 -0
- package/figma/radio.figma.tsx +16 -0
- package/figma/select.figma.tsx +24 -0
- package/figma/status-badge.figma.tsx +24 -0
- package/figma/switch.figma.tsx +28 -0
- package/figma/tabs.figma.tsx +22 -0
- package/figma/tag.figma.tsx +27 -0
- package/figma.config.json +6 -0
- package/package.json +4 -3
- package/src/backend/__tests__/CrudForm.groupInjectionColumn.test.tsx +98 -0
- package/src/backend/__tests__/nav-utils.test.ts +139 -0
- package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
- package/src/backend/detail/__tests__/AttachmentAssignmentsEditor.test.tsx +21 -0
- package/src/backend/utils/nav.ts +22 -2
package/AGENTS.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# UI Package - Agent Guidelines
|
|
2
2
|
|
|
3
|
-
UI
|
|
3
|
+
UI defaults for `packages/ui` and its consumers.
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
> References: [DS rules](../../.ai/ds-rules.md), [component contracts](../../.ai/ui-components.md), and [backend component families](../../.ai/ui-backend-components.md).
|
|
6
6
|
|
|
7
7
|
## Always
|
|
8
8
|
|
|
@@ -14,6 +14,7 @@ UI usage patterns based on customers, sales, and staff modules. Use these defaul
|
|
|
14
14
|
- Use i18n keys and `useT()` for user-facing copy.
|
|
15
15
|
- Keep UMES spot IDs, replacement handles, field/group IDs, and portal page metadata stable.
|
|
16
16
|
- Follow `.ai/ds-rules.md` and `.ai/ui-components.md` for tokens, primitives, and component contracts.
|
|
17
|
+
- Keep documented primitive additions/renames and their `figma/*.figma.tsx` mappings in the same PR; run `yarn ds:code-connect:check`.
|
|
17
18
|
|
|
18
19
|
## Ask First
|
|
19
20
|
|
|
@@ -36,6 +37,7 @@ UI usage patterns based on customers, sales, and staff modules. Use these defaul
|
|
|
36
37
|
```bash
|
|
37
38
|
yarn workspace @open-mercato/ui test
|
|
38
39
|
yarn workspace @open-mercato/ui build
|
|
40
|
+
yarn ds:code-connect:check
|
|
39
41
|
yarn i18n:check
|
|
40
42
|
```
|
|
41
43
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/detail/AttachmentMetadataDialog.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'\nimport { TagsInput } from '@open-mercato/ui/backend/inputs/TagsInput'\nimport { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'\nimport { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Copy, Download, Trash2 } from 'lucide-react'\nimport { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'\nimport { AttachmentContentPreview } from '@open-mercato/core/modules/attachments/components/AttachmentContentPreview'\nimport { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'\nimport { E } from '@open-mercato/core/generated-shims/entities.ids.generated'\n\nexport type AttachmentAssignment = {\n type: string\n id: string\n href?: string | null\n label?: string | null\n}\n\nexport type AttachmentItem = {\n id: string\n fileName: string\n fileSize: number\n mimeType?: string | null\n partitionCode?: string | null\n partitionTitle?: string | null\n url?: string | null\n createdAt?: string | null\n tags?: string[]\n assignments?: AttachmentAssignment[]\n thumbnailUrl?: string\n content?: string | null\n}\n\nexport type AssignmentDraft = {\n type: string\n id: string\n href?: string\n label?: string\n}\n\nexport type AttachmentMetadataSavePayload = {\n tags: string[]\n assignments: AssignmentDraft[]\n customFields?: Record<string, unknown>\n}\n\ntype AttachmentMetadataResponse = {\n item: {\n id: string\n fileName?: string\n fileSize?: number\n mimeType?: string | null\n partitionCode?: string\n partitionTitle?: string | null\n tags?: string[]\n content?: string | null\n assignments?: AttachmentAssignment[]\n customFields?: Record<string, unknown>\n }\n error?: string\n}\n\ntype AttachmentMetadataFormValues = {\n id: string\n tags?: string[]\n assignments?: AssignmentDraft[]\n} & Record<string, unknown>\n\ntype AssignmentEditorLabels = {\n title: string\n description: string\n type: string\n id: string\n href: string\n label: string\n add: string\n remove: string\n}\n\ntype AttachmentMetadataDialogProps = {\n open: boolean\n onOpenChange: (next: boolean) => void\n item: AttachmentItem | null\n availableTags: string[]\n onSave: (id: string, payload: AttachmentMetadataSavePayload) => Promise<void>\n}\n\nfunction formatFileSize(value: number): string {\n if (!Number.isFinite(value)) return '\\u2014'\n if (value <= 0) return '0 B'\n const units = ['B', 'KB', 'MB', 'GB', 'TB']\n let idx = 0\n let current = value\n while (current >= 1024 && idx < units.length - 1) {\n current /= 1024\n idx += 1\n }\n return `${current.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`\n}\n\nconst ENV_APP_URL = (process.env.NEXT_PUBLIC_APP_URL || '').replace(/\\/$/, '')\n\nfunction resolveAbsoluteUrl(path: string): string {\n if (!path) return path\n if (/^https?:\\/\\//i.test(path)) return path\n const base =\n ENV_APP_URL ||\n (typeof window !== 'undefined' && window.location?.origin ? window.location.origin : '')\n if (!base) return path\n const normalizedBase = base.replace(/\\/$/, '')\n return `${normalizedBase}${path.startsWith('/') ? path : `/${path}`}`\n}\n\nfunction normalizeCustomFieldSubmitValue(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.filter((entry) => entry !== undefined)\n }\n if (value === undefined) return null\n return value\n}\n\nfunction prepareAssignmentsForForm(assignments?: AttachmentAssignment[] | null): AssignmentDraft[] {\n return (assignments ?? []).map((assignment) => ({\n type: assignment.type,\n id: assignment.id,\n href: assignment.href ?? '',\n label: assignment.label ?? '',\n }))\n}\n\nfunction prefixCustomFieldValues(values?: Record<string, unknown> | null): Record<string, unknown> {\n if (!values) return {}\n const prefixed: Record<string, unknown> = {}\n Object.entries(values).forEach(([key, value]) => {\n if (!key) return\n if (key.startsWith('cf_')) {\n prefixed[key] = value\n } else if (key.startsWith('cf:')) {\n const normalized = key.slice(3)\n if (normalized) prefixed[`cf_${normalized}`] = value\n } else {\n prefixed[`cf_${key}`] = value\n }\n })\n return prefixed\n}\n\nfunction AssignmentInputRow({\n value,\n onChange,\n labels,\n disabled,\n onRemove,\n}: {\n value: AssignmentDraft\n onChange: (next: AssignmentDraft) => void\n labels: AssignmentEditorLabels\n disabled?: boolean\n onRemove: () => void\n}) {\n const trimmedLabel = value.label?.trim() ?? ''\n const trimmedType = value.type?.trim() ?? ''\n const trimmedId = value.id?.trim() ?? ''\n const primaryLabel = trimmedLabel || trimmedType || trimmedId\n const secondaryLabel = [\n trimmedLabel ? trimmedType : '',\n trimmedId && trimmedId !== primaryLabel ? trimmedId : '',\n ]\n .filter(Boolean)\n .join(' - ')\n\n return (\n <div data-assignment-card className=\"min-w-0 rounded-md border border-border/70 bg-background p-3\">\n <div className=\"mb-3 flex min-w-0 items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n {primaryLabel ? (\n <p className=\"truncate text-sm font-medium text-foreground\">{primaryLabel}</p>\n ) : null}\n {secondaryLabel ? (\n <p className=\"truncate text-xs text-muted-foreground\">{secondaryLabel}</p>\n ) : null}\n </div>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"default\"\n aria-label={labels.remove}\n onClick={onRemove}\n disabled={disabled}\n className=\"shrink-0\"\n >\n <Trash2 className=\"size-4\" />\n </IconButton>\n </div>\n <div className=\"grid min-w-0 grid-cols-1 gap-3 md:grid-cols-2\">\n <div className=\"min-w-0 space-y-1\">\n <label className=\"text-xs font-medium\">{labels.type}</label>\n <Input\n className=\"w-full min-w-0\"\n value={value.type}\n onChange={(event) => onChange({ ...value, type: event.target.value })}\n placeholder=\"catalog.product\"\n disabled={disabled}\n />\n </div>\n <div className=\"min-w-0 space-y-1\">\n <label className=\"text-xs font-medium\">{labels.id}</label>\n <Input\n className=\"w-full min-w-0\"\n value={value.id}\n onChange={(event) => onChange({ ...value, id: event.target.value })}\n placeholder=\"Record ID\"\n disabled={disabled}\n />\n </div>\n <div className=\"min-w-0 space-y-1 md:col-span-2\">\n <label className=\"text-xs font-medium\">{labels.href}</label>\n <Input\n className=\"w-full min-w-0\"\n value={value.href ?? ''}\n onChange={(event) => onChange({ ...value, href: event.target.value })}\n placeholder=\"https://\"\n disabled={disabled}\n />\n </div>\n <div className=\"min-w-0 space-y-1 md:col-span-2\">\n <label className=\"text-xs font-medium\">{labels.label}</label>\n <Input\n className=\"w-full min-w-0\"\n value={value.label ?? ''}\n onChange={(event) => onChange({ ...value, label: event.target.value })}\n placeholder=\"Optional label\"\n disabled={disabled}\n />\n </div>\n </div>\n </div>\n )\n}\n\nexport function AttachmentAssignmentsEditor({\n value,\n onChange,\n labels,\n disabled,\n}: {\n value: AssignmentDraft[]\n onChange: (next: AssignmentDraft[]) => void\n labels: AssignmentEditorLabels\n disabled?: boolean\n}) {\n const handleAdd = React.useCallback(() => {\n onChange([...value, { type: '', id: '', href: '', label: '' }])\n }, [onChange, value])\n\n const handleChange = React.useCallback(\n (index: number, next: AssignmentDraft) => {\n const draft = [...value]\n draft[index] = next\n onChange(draft)\n },\n [onChange, value],\n )\n\n const handleRemove = React.useCallback(\n (index: number) => {\n const next = value.filter((_, idx) => idx !== index)\n onChange(next)\n },\n [onChange, value],\n )\n\n return (\n <div className=\"space-y-2\">\n <div className=\"space-y-1\">\n <div className=\"text-sm font-medium\">{labels.title}</div>\n <p className=\"text-xs text-muted-foreground\">{labels.description}</p>\n </div>\n <div className=\"space-y-2\">\n {value.length ? value.map((entry, idx) => (\n <AssignmentInputRow\n key={idx}\n value={entry}\n labels={labels}\n disabled={disabled}\n onChange={(next) => handleChange(idx, next)}\n onRemove={() => handleRemove(idx)}\n />\n )) : (\n <div className=\"rounded-md border border-dashed border-border/70 px-3 py-4 text-xs text-muted-foreground\">\n {labels.description}\n </div>\n )}\n </div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={handleAdd} disabled={disabled}>\n {labels.add}\n </Button>\n </div>\n )\n}\n\nexport function AttachmentMetadataDialog({ open, onOpenChange, item, availableTags, onSave }: AttachmentMetadataDialogProps) {\n const t = useT()\n const [sizeWidth, setSizeWidth] = React.useState<string>('')\n const [sizeHeight, setSizeHeight] = React.useState<string>('')\n const [imageTab, setImageTab] = React.useState<'preview' | 'resize'>('preview')\n const [initialValues, setInitialValues] = React.useState<Partial<AttachmentMetadataFormValues> | null>(null)\n const [loading, setLoading] = React.useState(false)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const [extractedContent, setExtractedContent] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n if (!open || !item) {\n setInitialValues(null)\n setLoadError(null)\n setLoading(false)\n setImageTab('preview')\n setExtractedContent(null)\n return\n }\n let cancelled = false\n setLoading(true)\n setLoadError(null)\n setSizeWidth('')\n setSizeHeight('')\n setImageTab('preview')\n setInitialValues({\n id: item.id,\n tags: item.tags ?? [],\n assignments: prepareAssignmentsForForm(item.assignments),\n })\n setExtractedContent(item.content && item.content.trim() ? item.content : null)\n const loadDetails = async () => {\n try {\n const call = await apiCall<AttachmentMetadataResponse>(`/api/attachments/library/${encodeURIComponent(item.id)}`)\n if (!call.ok || !call.result?.item) {\n const message = call.result?.error || t('attachments.library.metadata.error', 'Failed to update metadata.')\n throw new Error(message)\n }\n const payload = call.result.item\n const prefixedCustom = prefixCustomFieldValues(payload.customFields)\n if (!cancelled) {\n setInitialValues({\n id: payload.id,\n tags: Array.isArray(payload.tags) ? payload.tags : [],\n assignments: prepareAssignmentsForForm(payload.assignments ?? item.assignments),\n ...prefixedCustom,\n })\n const nextContent = typeof payload.content === 'string' && payload.content.trim() ? payload.content : null\n setExtractedContent(nextContent)\n }\n } catch (err: any) {\n if (!cancelled) {\n const message =\n err?.message || t('attachments.library.metadata.loadError', 'Failed to load attachment metadata.')\n setLoadError(message)\n }\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n void loadDetails()\n return () => {\n cancelled = true\n }\n }, [item, open, t])\n\n const isImage = React.useMemo(() => Boolean(item?.mimeType?.toLowerCase().startsWith('image/')), [item])\n const previewUrl = React.useMemo(() => {\n if (!item) return null\n return (\n item.thumbnailUrl ??\n buildAttachmentImageUrl(item.id, {\n width: 320,\n height: 320,\n slug: slugifyAttachmentFileName(item.fileName),\n })\n )\n }, [item])\n const downloadUrl = React.useMemo(() => {\n if (!item) return null\n const original = buildAttachmentFileUrl(item.id, { download: true })\n return resolveAbsoluteUrl(original)\n }, [item])\n\n const assignmentLabels = React.useMemo(\n () => ({\n title: t('attachments.library.metadata.assignments.title', 'Assignments'),\n description: t(\n 'attachments.library.metadata.assignments.description',\n 'Add the records this attachment belongs to with optional links.',\n ),\n type: t('attachments.library.metadata.assignments.type', 'Type'),\n id: t('attachments.library.metadata.assignments.id', 'Record ID'),\n href: t('attachments.library.metadata.assignments.href', 'Link'),\n label: t('attachments.library.metadata.assignments.label', 'Label'),\n add: t('attachments.library.metadata.assignments.add', 'Add assignment'),\n remove: t('attachments.library.metadata.assignments.remove', 'Remove'),\n }),\n [t],\n )\n\n const metadataFields = React.useMemo<CrudField[]>(() => {\n return [\n {\n id: 'tags',\n label: t('attachments.library.table.tags', 'Tags'),\n type: 'custom',\n component: ({ value, setValue, disabled }) => (\n <TagsInput\n value={Array.isArray(value) ? (value as string[]) : []}\n onChange={(next) => setValue(next)}\n suggestions={availableTags}\n placeholder={t('attachments.library.metadata.tagsPlaceholder', 'Add tags')}\n disabled={Boolean(disabled) || loading}\n />\n ),\n },\n {\n id: 'assignments',\n label: '',\n type: 'custom',\n component: ({ value, setValue, disabled }) => (\n <AttachmentAssignmentsEditor\n value={Array.isArray(value) ? (value as AssignmentDraft[]) : []}\n onChange={(next) => setValue(next)}\n labels={assignmentLabels}\n disabled={Boolean(disabled) || loading}\n />\n ),\n },\n ]\n }, [assignmentLabels, availableTags, loading, t])\n\n const metadataGroups = React.useMemo<CrudFormGroup[]>(() => {\n return [\n {\n id: 'details',\n title: t('attachments.library.metadata.details', 'Details'),\n column: 1,\n fields: ['tags', 'assignments'],\n },\n {\n id: 'customFields',\n title: t('entities.customFields.title', 'Custom attributes'),\n column: 2,\n kind: 'customFields',\n },\n ]\n }, [t])\n\n const metadataSchema = React.useMemo(\n () =>\n z\n .object({\n id: z.string().min(1),\n tags: z.array(z.string()).optional(),\n assignments: z\n .array(\n z.object({\n type: z.string().min(1),\n id: z.string().min(1),\n href: z.string().optional(),\n label: z.string().optional(),\n }),\n )\n .optional(),\n })\n .passthrough(),\n [],\n )\n\n const handleSubmit = React.useCallback(\n async (values: AttachmentMetadataFormValues) => {\n if (!item) return\n const tags = Array.isArray(values.tags)\n ? values.tags.map((tag) => (typeof tag === 'string' ? tag.trim() : '')).filter((tag) => tag.length > 0)\n : []\n const assignments = Array.isArray(values.assignments)\n ? values.assignments\n .map((assignment) => ({\n type: assignment.type?.trim() ?? '',\n id: assignment.id?.trim() ?? '',\n href: assignment.href?.trim() || undefined,\n label: assignment.label?.trim() || undefined,\n }))\n .filter((assignment) => assignment.type && assignment.id)\n : []\n const customFields = collectCustomFieldValues(values, {\n transform: (value) => normalizeCustomFieldSubmitValue(value),\n })\n const payload: AttachmentMetadataSavePayload = {\n tags,\n assignments,\n }\n if (Object.keys(customFields).length) {\n payload.customFields = customFields\n }\n await onSave(item.id, payload)\n },\n [item, onSave],\n )\n\n const handleKeyDown = useDialogKeyHandler({\n onCancel: () => onOpenChange(false),\n })\n\n const handleCopyResizedUrl = React.useCallback(async () => {\n if (!item) return\n const width = sizeWidth ? Number(sizeWidth) : undefined\n const height = sizeHeight ? Number(sizeHeight) : undefined\n if (!width && !height) {\n flash(\n t('attachments.library.metadata.resizeTool.missing', 'Enter width or height to generate the URL.'),\n 'error',\n )\n return\n }\n const url = buildAttachmentImageUrl(item.id, {\n width: width && width > 0 ? width : undefined,\n height: height && height > 0 ? height : undefined,\n slug: slugifyAttachmentFileName(item.fileName),\n })\n const absolute = resolveAbsoluteUrl(url)\n try {\n await navigator.clipboard.writeText(absolute)\n flash(\n t('attachments.library.metadata.resizeTool.copied', 'Image URL copied.'),\n 'success',\n )\n } catch {\n flash(\n t('attachments.library.metadata.resizeTool.copyError', 'Unable to copy URL.'),\n 'error',\n )\n }\n }, [item, sizeHeight, sizeWidth, t])\n\n const loadMessage = t('attachments.library.metadata.loading', 'Loading attachment details\\u2026')\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"sm:max-w-2xl\" onKeyDown={handleKeyDown}>\n <DialogHeader>\n <DialogTitle>{t('attachments.library.metadata.title', 'Edit attachment metadata')}</DialogTitle>\n </DialogHeader>\n {item ? (\n <div className=\"space-y-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"space-y-1 min-w-0\">\n <div className=\"truncate text-sm font-medium\" title={item.fileName}>\n {item.fileName}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {formatFileSize(item.fileSize)} {'\\u2022'} {item.partitionTitle ?? item.partitionCode}\n </div>\n </div>\n {downloadUrl ? (\n <Button variant=\"outline\" size=\"sm\" asChild className=\"shrink-0\">\n <a href={downloadUrl} download>\n <Download className=\"mr-2 h-4 w-4\" />\n {t('attachments.library.metadata.download', 'Download')}\n </a>\n </Button>\n ) : null}\n </div>\n {isImage ? (\n <div className=\"rounded border\">\n <Tabs\n value={imageTab}\n onValueChange={(value) => setImageTab(value as 'preview' | 'resize')}\n variant=\"underline\"\n >\n <TabsList className=\"w-full flex-wrap px-3\">\n {(['preview', 'resize'] as const).map((tab) => (\n <TabsTrigger key={tab} value={tab}>\n {tab === 'preview'\n ? t('attachments.library.metadata.preview', 'Preview')\n : t('attachments.library.metadata.resizeTool.title', 'Generate resized URL')}\n </TabsTrigger>\n ))}\n </TabsList>\n </Tabs>\n <div className=\"space-y-3 p-3\">\n {imageTab === 'preview' ? (\n previewUrl ? (\n <img\n src={previewUrl}\n alt={item.fileName}\n className=\"h-48 w-full rounded-md bg-muted object-contain\"\n />\n ) : (\n <div className=\"text-sm text-muted-foreground\">\n {t('attachments.library.metadata.previewUnavailable', 'Preview unavailable.')}\n </div>\n )\n ) : (\n <div className=\"space-y-2\">\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium\" htmlFor=\"resize-width\">\n {t('attachments.library.metadata.resizeTool.width', 'Width (px)')}\n </label>\n <Input\n id=\"resize-width\"\n type=\"number\"\n min={0}\n value={sizeWidth}\n onChange={(event) => setSizeWidth(event.target.value)}\n disabled={loading}\n />\n </div>\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium\" htmlFor=\"resize-height\">\n {t('attachments.library.metadata.resizeTool.height', 'Height (px)')}\n </label>\n <Input\n id=\"resize-height\"\n type=\"number\"\n min={0}\n value={sizeHeight}\n onChange={(event) => setSizeHeight(event.target.value)}\n disabled={loading}\n />\n </div>\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"inline-flex items-center gap-2\"\n onClick={() => void handleCopyResizedUrl()}\n disabled={loading}\n >\n <Copy className=\"h-4 w-4\" />\n {t('attachments.library.metadata.resizeTool.copy', 'Copy URL')}\n </Button>\n </div>\n )}\n </div>\n </div>\n ) : null}\n {loadError ? (\n <div className=\"rounded border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive\">\n {loadError}\n </div>\n ) : null}\n <div className=\"rounded border border-border/70 bg-muted/30 px-3 py-2\">\n <div className=\"text-xs font-semibold text-muted-foreground\">\n {t('attachments.library.metadata.extractedTitle', 'Extracted text')}\n </div>\n <AttachmentContentPreview\n content={extractedContent}\n emptyLabel={t('attachments.library.metadata.noContent', 'No text extracted')}\n showMoreLabel={t('attachments.library.metadata.showMore', 'Show more')}\n showLessLabel={t('attachments.library.metadata.showLess', 'Show less')}\n />\n </div>\n <CrudForm<AttachmentMetadataFormValues>\n embedded\n schema={metadataSchema}\n entityId={E.attachments.attachment}\n fields={metadataFields}\n groups={metadataGroups}\n initialValues={initialValues ?? undefined}\n isLoading={!initialValues || loading}\n loadingMessage={loadMessage}\n submitLabel={t('attachments.library.metadata.save', 'Save')}\n extraActions={\n <Button type=\"button\" variant=\"outline\" onClick={() => onOpenChange(false)}>\n {t('attachments.library.metadata.cancel', 'Cancel')}\n </Button>\n }\n onSubmit={handleSubmit}\n />\n </div>\n ) : (\n <div className=\"py-8 text-center text-sm text-muted-foreground\">\n {t('attachments.library.metadata.noSelection', 'Select an attachment to edit.')}\n </div>\n )}\n </DialogContent>\n </Dialog>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'\nimport { TagsInput } from '@open-mercato/ui/backend/inputs/TagsInput'\nimport { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'\nimport { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Copy, Download, Trash2 } from 'lucide-react'\nimport { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'\nimport { AttachmentContentPreview } from '@open-mercato/core/modules/attachments/components/AttachmentContentPreview'\nimport { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'\nimport { E } from '@open-mercato/core/generated-shims/entities.ids.generated'\n\nexport type AttachmentAssignment = {\n type: string\n id: string\n href?: string | null\n label?: string | null\n}\n\nexport type AttachmentItem = {\n id: string\n fileName: string\n fileSize: number\n mimeType?: string | null\n partitionCode?: string | null\n partitionTitle?: string | null\n url?: string | null\n createdAt?: string | null\n tags?: string[]\n assignments?: AttachmentAssignment[]\n thumbnailUrl?: string\n content?: string | null\n}\n\nexport type AssignmentDraft = {\n type: string\n id: string\n href?: string\n label?: string\n}\n\nexport type AttachmentMetadataSavePayload = {\n tags: string[]\n assignments: AssignmentDraft[]\n customFields?: Record<string, unknown>\n}\n\ntype AttachmentMetadataResponse = {\n item: {\n id: string\n fileName?: string\n fileSize?: number\n mimeType?: string | null\n partitionCode?: string\n partitionTitle?: string | null\n tags?: string[]\n content?: string | null\n assignments?: AttachmentAssignment[]\n customFields?: Record<string, unknown>\n }\n error?: string\n}\n\ntype AttachmentMetadataFormValues = {\n id: string\n tags?: string[]\n assignments?: AssignmentDraft[]\n} & Record<string, unknown>\n\nexport type AssignmentEditorLabels = {\n title: string\n description: string\n type: string\n id: string\n href: string\n label: string\n add: string\n remove: string\n}\n\ntype AttachmentMetadataDialogProps = {\n open: boolean\n onOpenChange: (next: boolean) => void\n item: AttachmentItem | null\n availableTags: string[]\n onSave: (id: string, payload: AttachmentMetadataSavePayload) => Promise<void>\n}\n\nfunction formatFileSize(value: number): string {\n if (!Number.isFinite(value)) return '\\u2014'\n if (value <= 0) return '0 B'\n const units = ['B', 'KB', 'MB', 'GB', 'TB']\n let idx = 0\n let current = value\n while (current >= 1024 && idx < units.length - 1) {\n current /= 1024\n idx += 1\n }\n return `${current.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`\n}\n\nconst ENV_APP_URL = (process.env.NEXT_PUBLIC_APP_URL || '').replace(/\\/$/, '')\n\nfunction resolveAbsoluteUrl(path: string): string {\n if (!path) return path\n if (/^https?:\\/\\//i.test(path)) return path\n const base =\n ENV_APP_URL ||\n (typeof window !== 'undefined' && window.location?.origin ? window.location.origin : '')\n if (!base) return path\n const normalizedBase = base.replace(/\\/$/, '')\n return `${normalizedBase}${path.startsWith('/') ? path : `/${path}`}`\n}\n\nfunction normalizeCustomFieldSubmitValue(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.filter((entry) => entry !== undefined)\n }\n if (value === undefined) return null\n return value\n}\n\nfunction prepareAssignmentsForForm(assignments?: AttachmentAssignment[] | null): AssignmentDraft[] {\n return (assignments ?? []).map((assignment) => ({\n type: assignment.type,\n id: assignment.id,\n href: assignment.href ?? '',\n label: assignment.label ?? '',\n }))\n}\n\nfunction prefixCustomFieldValues(values?: Record<string, unknown> | null): Record<string, unknown> {\n if (!values) return {}\n const prefixed: Record<string, unknown> = {}\n Object.entries(values).forEach(([key, value]) => {\n if (!key) return\n if (key.startsWith('cf_')) {\n prefixed[key] = value\n } else if (key.startsWith('cf:')) {\n const normalized = key.slice(3)\n if (normalized) prefixed[`cf_${normalized}`] = value\n } else {\n prefixed[`cf_${key}`] = value\n }\n })\n return prefixed\n}\n\nfunction AssignmentInputRow({\n value,\n onChange,\n labels,\n disabled,\n onRemove,\n}: {\n value: AssignmentDraft\n onChange: (next: AssignmentDraft) => void\n labels: AssignmentEditorLabels\n disabled?: boolean\n onRemove: () => void\n}) {\n const trimmedLabel = value.label?.trim() ?? ''\n const trimmedType = value.type?.trim() ?? ''\n const trimmedId = value.id?.trim() ?? ''\n const primaryLabel = trimmedLabel || trimmedType || trimmedId\n const secondaryLabel = [\n trimmedLabel ? trimmedType : '',\n trimmedId && trimmedId !== primaryLabel ? trimmedId : '',\n ]\n .filter(Boolean)\n .join(' - ')\n\n return (\n <div data-assignment-card className=\"min-w-0 rounded-md border border-border/70 bg-background p-3\">\n <div className=\"mb-3 flex min-w-0 items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n {primaryLabel ? (\n <p className=\"truncate text-sm font-medium text-foreground\">{primaryLabel}</p>\n ) : null}\n {secondaryLabel ? (\n <p className=\"truncate text-xs text-muted-foreground\">{secondaryLabel}</p>\n ) : null}\n </div>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"default\"\n aria-label={labels.remove}\n onClick={onRemove}\n disabled={disabled}\n className=\"shrink-0\"\n >\n <Trash2 className=\"size-4\" />\n </IconButton>\n </div>\n <div className=\"grid min-w-0 grid-cols-1 gap-3 md:grid-cols-2\">\n <div className=\"min-w-0 space-y-1\">\n <label className=\"text-xs font-medium\">{labels.type}</label>\n <Input\n className=\"w-full min-w-0\"\n value={value.type}\n onChange={(event) => onChange({ ...value, type: event.target.value })}\n placeholder=\"catalog.product\"\n disabled={disabled}\n />\n </div>\n <div className=\"min-w-0 space-y-1\">\n <label className=\"text-xs font-medium\">{labels.id}</label>\n <Input\n className=\"w-full min-w-0\"\n value={value.id}\n onChange={(event) => onChange({ ...value, id: event.target.value })}\n placeholder=\"Record ID\"\n disabled={disabled}\n />\n </div>\n <div className=\"min-w-0 space-y-1 md:col-span-2\">\n <label className=\"text-xs font-medium\">{labels.href}</label>\n <Input\n className=\"w-full min-w-0\"\n value={value.href ?? ''}\n onChange={(event) => onChange({ ...value, href: event.target.value })}\n placeholder=\"https://\"\n disabled={disabled}\n />\n </div>\n <div className=\"min-w-0 space-y-1 md:col-span-2\">\n <label className=\"text-xs font-medium\">{labels.label}</label>\n <Input\n className=\"w-full min-w-0\"\n value={value.label ?? ''}\n onChange={(event) => onChange({ ...value, label: event.target.value })}\n placeholder=\"Optional label\"\n disabled={disabled}\n />\n </div>\n </div>\n </div>\n )\n}\n\nexport function AttachmentAssignmentsEditor({\n value,\n onChange,\n labels,\n disabled,\n}: {\n value: AssignmentDraft[]\n onChange: (next: AssignmentDraft[]) => void\n labels: AssignmentEditorLabels\n disabled?: boolean\n}) {\n const handleAdd = React.useCallback(() => {\n onChange([...value, { type: '', id: '', href: '', label: '' }])\n }, [onChange, value])\n\n const handleChange = React.useCallback(\n (index: number, next: AssignmentDraft) => {\n const draft = [...value]\n draft[index] = next\n onChange(draft)\n },\n [onChange, value],\n )\n\n const handleRemove = React.useCallback(\n (index: number) => {\n const next = value.filter((_, idx) => idx !== index)\n onChange(next)\n },\n [onChange, value],\n )\n\n return (\n <div className=\"space-y-2\">\n <div className=\"space-y-1\">\n <div className=\"text-sm font-medium\">{labels.title}</div>\n <p className=\"text-xs text-muted-foreground\">{labels.description}</p>\n </div>\n <div className=\"space-y-2\">\n {value.length ? value.map((entry, idx) => (\n <AssignmentInputRow\n key={idx}\n value={entry}\n labels={labels}\n disabled={disabled}\n onChange={(next) => handleChange(idx, next)}\n onRemove={() => handleRemove(idx)}\n />\n )) : (\n <div className=\"rounded-md border border-dashed border-border/70 px-3 py-4 text-xs text-muted-foreground\">\n {labels.description}\n </div>\n )}\n </div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={handleAdd} disabled={disabled}>\n {labels.add}\n </Button>\n </div>\n )\n}\n\nexport function AttachmentMetadataDialog({ open, onOpenChange, item, availableTags, onSave }: AttachmentMetadataDialogProps) {\n const t = useT()\n const [sizeWidth, setSizeWidth] = React.useState<string>('')\n const [sizeHeight, setSizeHeight] = React.useState<string>('')\n const [imageTab, setImageTab] = React.useState<'preview' | 'resize'>('preview')\n const [initialValues, setInitialValues] = React.useState<Partial<AttachmentMetadataFormValues> | null>(null)\n const [loading, setLoading] = React.useState(false)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const [extractedContent, setExtractedContent] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n if (!open || !item) {\n setInitialValues(null)\n setLoadError(null)\n setLoading(false)\n setImageTab('preview')\n setExtractedContent(null)\n return\n }\n let cancelled = false\n setLoading(true)\n setLoadError(null)\n setSizeWidth('')\n setSizeHeight('')\n setImageTab('preview')\n setInitialValues({\n id: item.id,\n tags: item.tags ?? [],\n assignments: prepareAssignmentsForForm(item.assignments),\n })\n setExtractedContent(item.content && item.content.trim() ? item.content : null)\n const loadDetails = async () => {\n try {\n const call = await apiCall<AttachmentMetadataResponse>(`/api/attachments/library/${encodeURIComponent(item.id)}`)\n if (!call.ok || !call.result?.item) {\n const message = call.result?.error || t('attachments.library.metadata.error', 'Failed to update metadata.')\n throw new Error(message)\n }\n const payload = call.result.item\n const prefixedCustom = prefixCustomFieldValues(payload.customFields)\n if (!cancelled) {\n setInitialValues({\n id: payload.id,\n tags: Array.isArray(payload.tags) ? payload.tags : [],\n assignments: prepareAssignmentsForForm(payload.assignments ?? item.assignments),\n ...prefixedCustom,\n })\n const nextContent = typeof payload.content === 'string' && payload.content.trim() ? payload.content : null\n setExtractedContent(nextContent)\n }\n } catch (err: any) {\n if (!cancelled) {\n const message =\n err?.message || t('attachments.library.metadata.loadError', 'Failed to load attachment metadata.')\n setLoadError(message)\n }\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n void loadDetails()\n return () => {\n cancelled = true\n }\n }, [item, open, t])\n\n const isImage = React.useMemo(() => Boolean(item?.mimeType?.toLowerCase().startsWith('image/')), [item])\n const previewUrl = React.useMemo(() => {\n if (!item) return null\n return (\n item.thumbnailUrl ??\n buildAttachmentImageUrl(item.id, {\n width: 320,\n height: 320,\n slug: slugifyAttachmentFileName(item.fileName),\n })\n )\n }, [item])\n const downloadUrl = React.useMemo(() => {\n if (!item) return null\n const original = buildAttachmentFileUrl(item.id, { download: true })\n return resolveAbsoluteUrl(original)\n }, [item])\n\n const assignmentLabels = React.useMemo(\n () => ({\n title: t('attachments.library.metadata.assignments.title', 'Assignments'),\n description: t(\n 'attachments.library.metadata.assignments.description',\n 'Add the records this attachment belongs to with optional links.',\n ),\n type: t('attachments.library.metadata.assignments.type', 'Type'),\n id: t('attachments.library.metadata.assignments.id', 'Record ID'),\n href: t('attachments.library.metadata.assignments.href', 'Link'),\n label: t('attachments.library.metadata.assignments.label', 'Label'),\n add: t('attachments.library.metadata.assignments.add', 'Add assignment'),\n remove: t('attachments.library.metadata.assignments.remove', 'Remove'),\n }),\n [t],\n )\n\n const metadataFields = React.useMemo<CrudField[]>(() => {\n return [\n {\n id: 'tags',\n label: t('attachments.library.table.tags', 'Tags'),\n type: 'custom',\n component: ({ value, setValue, disabled }) => (\n <TagsInput\n value={Array.isArray(value) ? (value as string[]) : []}\n onChange={(next) => setValue(next)}\n suggestions={availableTags}\n placeholder={t('attachments.library.metadata.tagsPlaceholder', 'Add tags')}\n disabled={Boolean(disabled) || loading}\n />\n ),\n },\n {\n id: 'assignments',\n label: '',\n type: 'custom',\n component: ({ value, setValue, disabled }) => (\n <AttachmentAssignmentsEditor\n value={Array.isArray(value) ? (value as AssignmentDraft[]) : []}\n onChange={(next) => setValue(next)}\n labels={assignmentLabels}\n disabled={Boolean(disabled) || loading}\n />\n ),\n },\n ]\n }, [assignmentLabels, availableTags, loading, t])\n\n const metadataGroups = React.useMemo<CrudFormGroup[]>(() => {\n return [\n {\n id: 'details',\n title: t('attachments.library.metadata.details', 'Details'),\n column: 1,\n fields: ['tags', 'assignments'],\n },\n {\n id: 'customFields',\n title: t('entities.customFields.title', 'Custom attributes'),\n column: 2,\n kind: 'customFields',\n },\n ]\n }, [t])\n\n const metadataSchema = React.useMemo(\n () =>\n z\n .object({\n id: z.string().min(1),\n tags: z.array(z.string()).optional(),\n assignments: z\n .array(\n z.object({\n type: z.string().min(1),\n id: z.string().min(1),\n href: z.string().optional(),\n label: z.string().optional(),\n }),\n )\n .optional(),\n })\n .passthrough(),\n [],\n )\n\n const handleSubmit = React.useCallback(\n async (values: AttachmentMetadataFormValues) => {\n if (!item) return\n const tags = Array.isArray(values.tags)\n ? values.tags.map((tag) => (typeof tag === 'string' ? tag.trim() : '')).filter((tag) => tag.length > 0)\n : []\n const assignments = Array.isArray(values.assignments)\n ? values.assignments\n .map((assignment) => ({\n type: assignment.type?.trim() ?? '',\n id: assignment.id?.trim() ?? '',\n href: assignment.href?.trim() || undefined,\n label: assignment.label?.trim() || undefined,\n }))\n .filter((assignment) => assignment.type && assignment.id)\n : []\n const customFields = collectCustomFieldValues(values, {\n transform: (value) => normalizeCustomFieldSubmitValue(value),\n })\n const payload: AttachmentMetadataSavePayload = {\n tags,\n assignments,\n }\n if (Object.keys(customFields).length) {\n payload.customFields = customFields\n }\n await onSave(item.id, payload)\n },\n [item, onSave],\n )\n\n const handleKeyDown = useDialogKeyHandler({\n onCancel: () => onOpenChange(false),\n })\n\n const handleCopyResizedUrl = React.useCallback(async () => {\n if (!item) return\n const width = sizeWidth ? Number(sizeWidth) : undefined\n const height = sizeHeight ? Number(sizeHeight) : undefined\n if (!width && !height) {\n flash(\n t('attachments.library.metadata.resizeTool.missing', 'Enter width or height to generate the URL.'),\n 'error',\n )\n return\n }\n const url = buildAttachmentImageUrl(item.id, {\n width: width && width > 0 ? width : undefined,\n height: height && height > 0 ? height : undefined,\n slug: slugifyAttachmentFileName(item.fileName),\n })\n const absolute = resolveAbsoluteUrl(url)\n try {\n await navigator.clipboard.writeText(absolute)\n flash(\n t('attachments.library.metadata.resizeTool.copied', 'Image URL copied.'),\n 'success',\n )\n } catch {\n flash(\n t('attachments.library.metadata.resizeTool.copyError', 'Unable to copy URL.'),\n 'error',\n )\n }\n }, [item, sizeHeight, sizeWidth, t])\n\n const loadMessage = t('attachments.library.metadata.loading', 'Loading attachment details\\u2026')\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"sm:max-w-2xl\" onKeyDown={handleKeyDown}>\n <DialogHeader>\n <DialogTitle>{t('attachments.library.metadata.title', 'Edit attachment metadata')}</DialogTitle>\n </DialogHeader>\n {item ? (\n <div className=\"space-y-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"space-y-1 min-w-0\">\n <div className=\"truncate text-sm font-medium\" title={item.fileName}>\n {item.fileName}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {formatFileSize(item.fileSize)} {'\\u2022'} {item.partitionTitle ?? item.partitionCode}\n </div>\n </div>\n {downloadUrl ? (\n <Button variant=\"outline\" size=\"sm\" asChild className=\"shrink-0\">\n <a href={downloadUrl} download>\n <Download className=\"mr-2 h-4 w-4\" />\n {t('attachments.library.metadata.download', 'Download')}\n </a>\n </Button>\n ) : null}\n </div>\n {isImage ? (\n <div className=\"rounded border\">\n <Tabs\n value={imageTab}\n onValueChange={(value) => setImageTab(value as 'preview' | 'resize')}\n variant=\"underline\"\n >\n <TabsList className=\"w-full flex-wrap px-3\">\n {(['preview', 'resize'] as const).map((tab) => (\n <TabsTrigger key={tab} value={tab}>\n {tab === 'preview'\n ? t('attachments.library.metadata.preview', 'Preview')\n : t('attachments.library.metadata.resizeTool.title', 'Generate resized URL')}\n </TabsTrigger>\n ))}\n </TabsList>\n </Tabs>\n <div className=\"space-y-3 p-3\">\n {imageTab === 'preview' ? (\n previewUrl ? (\n <img\n src={previewUrl}\n alt={item.fileName}\n className=\"h-48 w-full rounded-md bg-muted object-contain\"\n />\n ) : (\n <div className=\"text-sm text-muted-foreground\">\n {t('attachments.library.metadata.previewUnavailable', 'Preview unavailable.')}\n </div>\n )\n ) : (\n <div className=\"space-y-2\">\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium\" htmlFor=\"resize-width\">\n {t('attachments.library.metadata.resizeTool.width', 'Width (px)')}\n </label>\n <Input\n id=\"resize-width\"\n type=\"number\"\n min={0}\n value={sizeWidth}\n onChange={(event) => setSizeWidth(event.target.value)}\n disabled={loading}\n />\n </div>\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium\" htmlFor=\"resize-height\">\n {t('attachments.library.metadata.resizeTool.height', 'Height (px)')}\n </label>\n <Input\n id=\"resize-height\"\n type=\"number\"\n min={0}\n value={sizeHeight}\n onChange={(event) => setSizeHeight(event.target.value)}\n disabled={loading}\n />\n </div>\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"inline-flex items-center gap-2\"\n onClick={() => void handleCopyResizedUrl()}\n disabled={loading}\n >\n <Copy className=\"h-4 w-4\" />\n {t('attachments.library.metadata.resizeTool.copy', 'Copy URL')}\n </Button>\n </div>\n )}\n </div>\n </div>\n ) : null}\n {loadError ? (\n <div className=\"rounded border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive\">\n {loadError}\n </div>\n ) : null}\n <div className=\"rounded border border-border/70 bg-muted/30 px-3 py-2\">\n <div className=\"text-xs font-semibold text-muted-foreground\">\n {t('attachments.library.metadata.extractedTitle', 'Extracted text')}\n </div>\n <AttachmentContentPreview\n content={extractedContent}\n emptyLabel={t('attachments.library.metadata.noContent', 'No text extracted')}\n showMoreLabel={t('attachments.library.metadata.showMore', 'Show more')}\n showLessLabel={t('attachments.library.metadata.showLess', 'Show less')}\n />\n </div>\n <CrudForm<AttachmentMetadataFormValues>\n embedded\n schema={metadataSchema}\n entityId={E.attachments.attachment}\n fields={metadataFields}\n groups={metadataGroups}\n initialValues={initialValues ?? undefined}\n isLoading={!initialValues || loading}\n loadingMessage={loadMessage}\n submitLabel={t('attachments.library.metadata.save', 'Save')}\n extraActions={\n <Button type=\"button\" variant=\"outline\" onClick={() => onOpenChange(false)}>\n {t('attachments.library.metadata.cancel', 'Cancel')}\n </Button>\n }\n onSubmit={handleSubmit}\n />\n </div>\n ) : (\n <div className=\"py-8 text-center text-sm text-muted-foreground\">\n {t('attachments.library.metadata.noSelection', 'Select an attachment to edit.')}\n </div>\n )}\n </DialogContent>\n </Dialog>\n )\n}\n"],
|
|
5
5
|
"mappings": ";AAyLQ,SAEI,KAFJ;AAvLR,YAAY,WAAW;AACvB,SAAS,SAAS;AAClB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,QAAQ,eAAe,cAAc,mBAAmB;AACjE,SAAS,aAAa;AACtB,SAAS,MAAM,UAAU,mBAAmB;AAC5C,SAAS,iBAAiB;AAC1B,SAAS,gBAAoD;AAC7D,SAAS,gCAAgC;AACzC,SAAS,eAAe;AACxB,SAAS,aAAa;AACtB,SAAS,YAAY;AAErB,SAAS,MAAM,UAAU,cAAc;AACvC,SAAS,2BAA2B;AACpC,SAAS,gCAAgC;AACzC,SAAS,wBAAwB,yBAAyB,iCAAiC;AAC3F,SAAS,SAAS;AA8ElB,SAAS,eAAe,OAAuB;AAC7C,MAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,MAAI,SAAS,EAAG,QAAO;AACvB,QAAM,QAAQ,CAAC,KAAK,MAAM,MAAM,MAAM,IAAI;AAC1C,MAAI,MAAM;AACV,MAAI,UAAU;AACd,SAAO,WAAW,QAAQ,MAAM,MAAM,SAAS,GAAG;AAChD,eAAW;AACX,WAAO;AAAA,EACT;AACA,SAAO,GAAG,QAAQ,QAAQ,QAAQ,IAAI,IAAI,CAAC,CAAC,IAAI,MAAM,GAAG,CAAC;AAC5D;AAEA,MAAM,eAAe,QAAQ,IAAI,uBAAuB,IAAI,QAAQ,OAAO,EAAE;AAE7E,SAAS,mBAAmB,MAAsB;AAChD,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,gBAAgB,KAAK,IAAI,EAAG,QAAO;AACvC,QAAM,OACJ,gBACC,OAAO,WAAW,eAAe,OAAO,UAAU,SAAS,OAAO,SAAS,SAAS;AACvF,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,iBAAiB,KAAK,QAAQ,OAAO,EAAE;AAC7C,SAAO,GAAG,cAAc,GAAG,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI,EAAE;AACrE;AAEA,SAAS,gCAAgC,OAAyB;AAChE,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,OAAO,CAAC,UAAU,UAAU,MAAS;AAAA,EACpD;AACA,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO;AACT;AAEA,SAAS,0BAA0B,aAAgE;AACjG,UAAQ,eAAe,CAAC,GAAG,IAAI,CAAC,gBAAgB;AAAA,IAC9C,MAAM,WAAW;AAAA,IACjB,IAAI,WAAW;AAAA,IACf,MAAM,WAAW,QAAQ;AAAA,IACzB,OAAO,WAAW,SAAS;AAAA,EAC7B,EAAE;AACJ;AAEA,SAAS,wBAAwB,QAAkE;AACjG,MAAI,CAAC,OAAQ,QAAO,CAAC;AACrB,QAAM,WAAoC,CAAC;AAC3C,SAAO,QAAQ,MAAM,EAAE,QAAQ,CAAC,CAAC,KAAK,KAAK,MAAM;AAC/C,QAAI,CAAC,IAAK;AACV,QAAI,IAAI,WAAW,KAAK,GAAG;AACzB,eAAS,GAAG,IAAI;AAAA,IAClB,WAAW,IAAI,WAAW,KAAK,GAAG;AAChC,YAAM,aAAa,IAAI,MAAM,CAAC;AAC9B,UAAI,WAAY,UAAS,MAAM,UAAU,EAAE,IAAI;AAAA,IACjD,OAAO;AACL,eAAS,MAAM,GAAG,EAAE,IAAI;AAAA,IAC1B;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAEA,SAAS,mBAAmB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,eAAe,MAAM,OAAO,KAAK,KAAK;AAC5C,QAAM,cAAc,MAAM,MAAM,KAAK,KAAK;AAC1C,QAAM,YAAY,MAAM,IAAI,KAAK,KAAK;AACtC,QAAM,eAAe,gBAAgB,eAAe;AACpD,QAAM,iBAAiB;AAAA,IACrB,eAAe,cAAc;AAAA,IAC7B,aAAa,cAAc,eAAe,YAAY;AAAA,EACxD,EACG,OAAO,OAAO,EACd,KAAK,KAAK;AAEb,SACE,qBAAC,SAAI,wBAAoB,MAAC,WAAU,gEAClC;AAAA,yBAAC,SAAI,WAAU,uDACb;AAAA,2BAAC,SAAI,WAAU,WACZ;AAAA,uBACC,oBAAC,OAAE,WAAU,gDAAgD,wBAAa,IACxE;AAAA,QACH,iBACC,oBAAC,OAAE,WAAU,0CAA0C,0BAAe,IACpE;AAAA,SACN;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,cAAY,OAAO;AAAA,UACnB,SAAS;AAAA,UACT;AAAA,UACA,WAAU;AAAA,UAEV,8BAAC,UAAO,WAAU,UAAS;AAAA;AAAA,MAC7B;AAAA,OACF;AAAA,IACA,qBAAC,SAAI,WAAU,iDACb;AAAA,2BAAC,SAAI,WAAU,qBACb;AAAA,4BAAC,WAAM,WAAU,uBAAuB,iBAAO,MAAK;AAAA,QACpD;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO,MAAM;AAAA,YACb,UAAU,CAAC,UAAU,SAAS,EAAE,GAAG,OAAO,MAAM,MAAM,OAAO,MAAM,CAAC;AAAA,YACpE,aAAY;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,qBACb;AAAA,4BAAC,WAAM,WAAU,uBAAuB,iBAAO,IAAG;AAAA,QAClD;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO,MAAM;AAAA,YACb,UAAU,CAAC,UAAU,SAAS,EAAE,GAAG,OAAO,IAAI,MAAM,OAAO,MAAM,CAAC;AAAA,YAClE,aAAY;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,mCACb;AAAA,4BAAC,WAAM,WAAU,uBAAuB,iBAAO,MAAK;AAAA,QACpD;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO,MAAM,QAAQ;AAAA,YACrB,UAAU,CAAC,UAAU,SAAS,EAAE,GAAG,OAAO,MAAM,MAAM,OAAO,MAAM,CAAC;AAAA,YACpE,aAAY;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,mCACb;AAAA,4BAAC,WAAM,WAAU,uBAAuB,iBAAO,OAAM;AAAA,QACrD;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO,MAAM,SAAS;AAAA,YACtB,UAAU,CAAC,UAAU,SAAS,EAAE,GAAG,OAAO,OAAO,MAAM,OAAO,MAAM,CAAC;AAAA,YACrE,aAAY;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;AAEO,SAAS,4BAA4B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,YAAY,MAAM,YAAY,MAAM;AACxC,aAAS,CAAC,GAAG,OAAO,EAAE,MAAM,IAAI,IAAI,IAAI,MAAM,IAAI,OAAO,GAAG,CAAC,CAAC;AAAA,EAChE,GAAG,CAAC,UAAU,KAAK,CAAC;AAEpB,QAAM,eAAe,MAAM;AAAA,IACzB,CAAC,OAAe,SAA0B;AACxC,YAAM,QAAQ,CAAC,GAAG,KAAK;AACvB,YAAM,KAAK,IAAI;AACf,eAAS,KAAK;AAAA,IAChB;AAAA,IACA,CAAC,UAAU,KAAK;AAAA,EAClB;AAEA,QAAM,eAAe,MAAM;AAAA,IACzB,CAAC,UAAkB;AACjB,YAAM,OAAO,MAAM,OAAO,CAAC,GAAG,QAAQ,QAAQ,KAAK;AACnD,eAAS,IAAI;AAAA,IACf;AAAA,IACA,CAAC,UAAU,KAAK;AAAA,EAClB;AAEA,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,yBAAC,SAAI,WAAU,aACb;AAAA,0BAAC,SAAI,WAAU,uBAAuB,iBAAO,OAAM;AAAA,MACnD,oBAAC,OAAE,WAAU,iCAAiC,iBAAO,aAAY;AAAA,OACnE;AAAA,IACA,oBAAC,SAAI,WAAU,aACZ,gBAAM,SAAS,MAAM,IAAI,CAAC,OAAO,QAChC;AAAA,MAAC;AAAA;AAAA,QAEC,OAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA,UAAU,CAAC,SAAS,aAAa,KAAK,IAAI;AAAA,QAC1C,UAAU,MAAM,aAAa,GAAG;AAAA;AAAA,MAL3B;AAAA,IAMP,CACD,IACC,oBAAC,SAAI,WAAU,4FACZ,iBAAO,aACV,GAEJ;AAAA,IACA,oBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,WAAW,UACnE,iBAAO,KACV;AAAA,KACF;AAEJ;AAEO,SAAS,yBAAyB,EAAE,MAAM,cAAc,MAAM,eAAe,OAAO,GAAkC;AAC3H,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAiB,EAAE;AAC3D,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAiB,EAAE;AAC7D,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAA+B,SAAS;AAC9E,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAuD,IAAI;AAC3G,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,IAAI;AACpE,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAwB,IAAI;AAElF,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,QAAQ,CAAC,MAAM;AAClB,uBAAiB,IAAI;AACrB,mBAAa,IAAI;AACjB,iBAAW,KAAK;AAChB,kBAAY,SAAS;AACrB,0BAAoB,IAAI;AACxB;AAAA,IACF;AACA,QAAI,YAAY;AAChB,eAAW,IAAI;AACf,iBAAa,IAAI;AACjB,iBAAa,EAAE;AACf,kBAAc,EAAE;AAChB,gBAAY,SAAS;AACrB,qBAAiB;AAAA,MACf,IAAI,KAAK;AAAA,MACT,MAAM,KAAK,QAAQ,CAAC;AAAA,MACpB,aAAa,0BAA0B,KAAK,WAAW;AAAA,IACzD,CAAC;AACD,wBAAoB,KAAK,WAAW,KAAK,QAAQ,KAAK,IAAI,KAAK,UAAU,IAAI;AAC7E,UAAM,cAAc,YAAY;AAC9B,UAAI;AACF,cAAM,OAAO,MAAM,QAAoC,4BAA4B,mBAAmB,KAAK,EAAE,CAAC,EAAE;AAChH,YAAI,CAAC,KAAK,MAAM,CAAC,KAAK,QAAQ,MAAM;AAClC,gBAAM,UAAU,KAAK,QAAQ,SAAS,EAAE,sCAAsC,4BAA4B;AAC1G,gBAAM,IAAI,MAAM,OAAO;AAAA,QACzB;AACA,cAAM,UAAU,KAAK,OAAO;AAC5B,cAAM,iBAAiB,wBAAwB,QAAQ,YAAY;AACnE,YAAI,CAAC,WAAW;AACd,2BAAiB;AAAA,YACf,IAAI,QAAQ;AAAA,YACZ,MAAM,MAAM,QAAQ,QAAQ,IAAI,IAAI,QAAQ,OAAO,CAAC;AAAA,YACpD,aAAa,0BAA0B,QAAQ,eAAe,KAAK,WAAW;AAAA,YAC9E,GAAG;AAAA,UACL,CAAC;AACD,gBAAM,cAAc,OAAO,QAAQ,YAAY,YAAY,QAAQ,QAAQ,KAAK,IAAI,QAAQ,UAAU;AACtG,8BAAoB,WAAW;AAAA,QACjC;AAAA,MACF,SAAS,KAAU;AACjB,YAAI,CAAC,WAAW;AACd,gBAAM,UACJ,KAAK,WAAW,EAAE,0CAA0C,qCAAqC;AACnG,uBAAa,OAAO;AAAA,QACtB;AAAA,MACF,UAAE;AACA,YAAI,CAAC,UAAW,YAAW,KAAK;AAAA,MAClC;AAAA,IACF;AACA,SAAK,YAAY;AACjB,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,MAAM,MAAM,CAAC,CAAC;AAElB,QAAM,UAAU,MAAM,QAAQ,MAAM,QAAQ,MAAM,UAAU,YAAY,EAAE,WAAW,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;AACvG,QAAM,aAAa,MAAM,QAAQ,MAAM;AACrC,QAAI,CAAC,KAAM,QAAO;AAClB,WACE,KAAK,gBACL,wBAAwB,KAAK,IAAI;AAAA,MAC/B,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM,0BAA0B,KAAK,QAAQ;AAAA,IAC/C,CAAC;AAAA,EAEL,GAAG,CAAC,IAAI,CAAC;AACT,QAAM,cAAc,MAAM,QAAQ,MAAM;AACtC,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,WAAW,uBAAuB,KAAK,IAAI,EAAE,UAAU,KAAK,CAAC;AACnE,WAAO,mBAAmB,QAAQ;AAAA,EACpC,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,mBAAmB,MAAM;AAAA,IAC7B,OAAO;AAAA,MACL,OAAO,EAAE,kDAAkD,aAAa;AAAA,MACxE,aAAa;AAAA,QACX;AAAA,QACA;AAAA,MACF;AAAA,MACA,MAAM,EAAE,iDAAiD,MAAM;AAAA,MAC/D,IAAI,EAAE,+CAA+C,WAAW;AAAA,MAChE,MAAM,EAAE,iDAAiD,MAAM;AAAA,MAC/D,OAAO,EAAE,kDAAkD,OAAO;AAAA,MAClE,KAAK,EAAE,gDAAgD,gBAAgB;AAAA,MACvE,QAAQ,EAAE,mDAAmD,QAAQ;AAAA,IACvE;AAAA,IACA,CAAC,CAAC;AAAA,EACJ;AAEA,QAAM,iBAAiB,MAAM,QAAqB,MAAM;AACtD,WAAO;AAAA,MACL;AAAA,QACE,IAAI;AAAA,QACJ,OAAO,EAAE,kCAAkC,MAAM;AAAA,QACjD,MAAM;AAAA,QACN,WAAW,CAAC,EAAE,OAAO,UAAU,SAAS,MACtC;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,MAAM,QAAQ,KAAK,IAAK,QAAqB,CAAC;AAAA,YACrD,UAAU,CAAC,SAAS,SAAS,IAAI;AAAA,YACjC,aAAa;AAAA,YACb,aAAa,EAAE,gDAAgD,UAAU;AAAA,YACzE,UAAU,QAAQ,QAAQ,KAAK;AAAA;AAAA,QACjC;AAAA,MAEJ;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,WAAW,CAAC,EAAE,OAAO,UAAU,SAAS,MACtC;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,MAAM,QAAQ,KAAK,IAAK,QAA8B,CAAC;AAAA,YAC9D,UAAU,CAAC,SAAS,SAAS,IAAI;AAAA,YACjC,QAAQ;AAAA,YACR,UAAU,QAAQ,QAAQ,KAAK;AAAA;AAAA,QACjC;AAAA,MAEJ;AAAA,IACF;AAAA,EACF,GAAG,CAAC,kBAAkB,eAAe,SAAS,CAAC,CAAC;AAEhD,QAAM,iBAAiB,MAAM,QAAyB,MAAM;AAC1D,WAAO;AAAA,MACL;AAAA,QACE,IAAI;AAAA,QACJ,OAAO,EAAE,wCAAwC,SAAS;AAAA,QAC1D,QAAQ;AAAA,QACR,QAAQ,CAAC,QAAQ,aAAa;AAAA,MAChC;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,OAAO,EAAE,+BAA+B,mBAAmB;AAAA,QAC3D,QAAQ;AAAA,QACR,MAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC,CAAC;AAEN,QAAM,iBAAiB,MAAM;AAAA,IAC3B,MACE,EACG,OAAO;AAAA,MACN,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MACpB,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,MACnC,aAAa,EACV;AAAA,QACC,EAAE,OAAO;AAAA,UACP,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,UACtB,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,UACpB,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,UAC1B,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,QAC7B,CAAC;AAAA,MACH,EACC,SAAS;AAAA,IACd,CAAC,EACA,YAAY;AAAA,IACjB,CAAC;AAAA,EACH;AAEA,QAAM,eAAe,MAAM;AAAA,IACzB,OAAO,WAAyC;AAC9C,UAAI,CAAC,KAAM;AACX,YAAM,OAAO,MAAM,QAAQ,OAAO,IAAI,IAClC,OAAO,KAAK,IAAI,CAAC,QAAS,OAAO,QAAQ,WAAW,IAAI,KAAK,IAAI,EAAG,EAAE,OAAO,CAAC,QAAQ,IAAI,SAAS,CAAC,IACpG,CAAC;AACL,YAAM,cAAc,MAAM,QAAQ,OAAO,WAAW,IAChD,OAAO,YACJ,IAAI,CAAC,gBAAgB;AAAA,QACpB,MAAM,WAAW,MAAM,KAAK,KAAK;AAAA,QACjC,IAAI,WAAW,IAAI,KAAK,KAAK;AAAA,QAC7B,MAAM,WAAW,MAAM,KAAK,KAAK;AAAA,QACjC,OAAO,WAAW,OAAO,KAAK,KAAK;AAAA,MACrC,EAAE,EACD,OAAO,CAAC,eAAe,WAAW,QAAQ,WAAW,EAAE,IAC1D,CAAC;AACL,YAAM,eAAe,yBAAyB,QAAQ;AAAA,QACpD,WAAW,CAAC,UAAU,gCAAgC,KAAK;AAAA,MAC7D,CAAC;AACD,YAAM,UAAyC;AAAA,QAC7C;AAAA,QACA;AAAA,MACF;AACA,UAAI,OAAO,KAAK,YAAY,EAAE,QAAQ;AACpC,gBAAQ,eAAe;AAAA,MACzB;AACA,YAAM,OAAO,KAAK,IAAI,OAAO;AAAA,IAC/B;AAAA,IACA,CAAC,MAAM,MAAM;AAAA,EACf;AAEA,QAAM,gBAAgB,oBAAoB;AAAA,IACxC,UAAU,MAAM,aAAa,KAAK;AAAA,EACpC,CAAC;AAED,QAAM,uBAAuB,MAAM,YAAY,YAAY;AACzD,QAAI,CAAC,KAAM;AACX,UAAM,QAAQ,YAAY,OAAO,SAAS,IAAI;AAC9C,UAAM,SAAS,aAAa,OAAO,UAAU,IAAI;AACjD,QAAI,CAAC,SAAS,CAAC,QAAQ;AACrB;AAAA,QACE,EAAE,mDAAmD,4CAA4C;AAAA,QACjG;AAAA,MACF;AACA;AAAA,IACF;AACA,UAAM,MAAM,wBAAwB,KAAK,IAAI;AAAA,MAC3C,OAAO,SAAS,QAAQ,IAAI,QAAQ;AAAA,MACpC,QAAQ,UAAU,SAAS,IAAI,SAAS;AAAA,MACxC,MAAM,0BAA0B,KAAK,QAAQ;AAAA,IAC/C,CAAC;AACD,UAAM,WAAW,mBAAmB,GAAG;AACvC,QAAI;AACF,YAAM,UAAU,UAAU,UAAU,QAAQ;AAC5C;AAAA,QACE,EAAE,kDAAkD,mBAAmB;AAAA,QACvE;AAAA,MACF;AAAA,IACF,QAAQ;AACN;AAAA,QACE,EAAE,qDAAqD,qBAAqB;AAAA,QAC5E;AAAA,MACF;AAAA,IACF;AAAA,EACF,GAAG,CAAC,MAAM,YAAY,WAAW,CAAC,CAAC;AAEnC,QAAM,cAAc,EAAE,wCAAwC,kCAAkC;AAEhG,SACE,oBAAC,UAAO,MAAY,cAClB,+BAAC,iBAAc,WAAU,gBAAe,WAAW,eACjD;AAAA,wBAAC,gBACC,8BAAC,eAAa,YAAE,sCAAsC,0BAA0B,GAAE,GACpF;AAAA,IACC,OACC,qBAAC,SAAI,WAAU,aACb;AAAA,2BAAC,SAAI,WAAU,0CACb;AAAA,6BAAC,SAAI,WAAU,qBACb;AAAA,8BAAC,SAAI,WAAU,gCAA+B,OAAO,KAAK,UACvD,eAAK,UACR;AAAA,UACA,qBAAC,SAAI,WAAU,iCACZ;AAAA,2BAAe,KAAK,QAAQ;AAAA,YAAE;AAAA,YAAE;AAAA,YAAS;AAAA,YAAE,KAAK,kBAAkB,KAAK;AAAA,aAC1E;AAAA,WACF;AAAA,QACC,cACC,oBAAC,UAAO,SAAQ,WAAU,MAAK,MAAK,SAAO,MAAC,WAAU,YACpD,+BAAC,OAAE,MAAM,aAAa,UAAQ,MAC5B;AAAA,8BAAC,YAAS,WAAU,gBAAe;AAAA,UAClC,EAAE,yCAAyC,UAAU;AAAA,WACxD,GACF,IACE;AAAA,SACN;AAAA,MACC,UACC,qBAAC,SAAI,WAAU,kBACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,eAAe,CAAC,UAAU,YAAY,KAA6B;AAAA,YACnE,SAAQ;AAAA,YAER,8BAAC,YAAS,WAAU,yBAChB,WAAC,WAAW,QAAQ,EAAY,IAAI,CAAC,QACrC,oBAAC,eAAsB,OAAO,KAC3B,kBAAQ,YACL,EAAE,wCAAwC,SAAS,IACnD,EAAE,iDAAiD,sBAAsB,KAH7D,GAIlB,CACD,GACH;AAAA;AAAA,QACF;AAAA,QACA,oBAAC,SAAI,WAAU,iBACZ,uBAAa,YACZ,aACE;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,KAAK,KAAK;AAAA,YACV,WAAU;AAAA;AAAA,QACZ,IAEA,oBAAC,SAAI,WAAU,iCACZ,YAAE,mDAAmD,sBAAsB,GAC9E,IAGF,qBAAC,SAAI,WAAU,aACb;AAAA,+BAAC,SAAI,WAAU,6BACb;AAAA,iCAAC,SAAI,WAAU,aACb;AAAA,kCAAC,WAAM,WAAU,uBAAsB,SAAQ,gBAC5C,YAAE,iDAAiD,YAAY,GAClE;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,IAAG;AAAA,kBACH,MAAK;AAAA,kBACL,KAAK;AAAA,kBACL,OAAO;AAAA,kBACP,UAAU,CAAC,UAAU,aAAa,MAAM,OAAO,KAAK;AAAA,kBACpD,UAAU;AAAA;AAAA,cACZ;AAAA,eACF;AAAA,YACA,qBAAC,SAAI,WAAU,aACb;AAAA,kCAAC,WAAM,WAAU,uBAAsB,SAAQ,iBAC5C,YAAE,kDAAkD,aAAa,GACpE;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,IAAG;AAAA,kBACH,MAAK;AAAA,kBACL,KAAK;AAAA,kBACL,OAAO;AAAA,kBACP,UAAU,CAAC,UAAU,cAAc,MAAM,OAAO,KAAK;AAAA,kBACrD,UAAU;AAAA;AAAA,cACZ;AAAA,eACF;AAAA,aACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,SAAS,MAAM,KAAK,qBAAqB;AAAA,cACzC,UAAU;AAAA,cAEV;AAAA,oCAAC,QAAK,WAAU,WAAU;AAAA,gBACzB,EAAE,gDAAgD,UAAU;AAAA;AAAA;AAAA,UAC/D;AAAA,WACF,GAEJ;AAAA,SACF,IACE;AAAA,MACH,YACC,oBAAC,SAAI,WAAU,6FACZ,qBACH,IACE;AAAA,MACJ,qBAAC,SAAI,WAAU,yDACb;AAAA,4BAAC,SAAI,WAAU,+CACZ,YAAE,+CAA+C,gBAAgB,GACpE;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,SAAS;AAAA,YACT,YAAY,EAAE,0CAA0C,mBAAmB;AAAA,YAC3E,eAAe,EAAE,yCAAyC,WAAW;AAAA,YACrE,eAAe,EAAE,yCAAyC,WAAW;AAAA;AAAA,QACvE;AAAA,SACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,UAAQ;AAAA,UACR,QAAQ;AAAA,UACR,UAAU,EAAE,YAAY;AAAA,UACxB,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,eAAe,iBAAiB;AAAA,UAChC,WAAW,CAAC,iBAAiB;AAAA,UAC7B,gBAAgB;AAAA,UAChB,aAAa,EAAE,qCAAqC,MAAM;AAAA,UAC1D,cACE,oBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,SAAS,MAAM,aAAa,KAAK,GACtE,YAAE,uCAAuC,QAAQ,GACpD;AAAA,UAEF,UAAU;AAAA;AAAA,MACZ;AAAA,OACF,IAEA,oBAAC,SAAI,WAAU,kDACZ,YAAE,4CAA4C,+BAA+B,GAChF;AAAA,KAEJ,GACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -34,6 +34,9 @@ async function fetchFeatureGrants(requestFeatures) {
|
|
|
34
34
|
}
|
|
35
35
|
return granted;
|
|
36
36
|
}
|
|
37
|
+
function legacySettingsSectionSlug(groupLabel) {
|
|
38
|
+
return groupLabel.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
39
|
+
}
|
|
37
40
|
function buildSettingsSections(entries, sectionOrder) {
|
|
38
41
|
const settingsItems = entries.filter((e) => e.pageContext === "settings");
|
|
39
42
|
const sectionMap = /* @__PURE__ */ new Map();
|
|
@@ -51,8 +54,8 @@ function buildSettingsSections(entries, sectionOrder) {
|
|
|
51
54
|
};
|
|
52
55
|
};
|
|
53
56
|
for (const item of settingsItems) {
|
|
54
|
-
const sectionId = item.
|
|
55
|
-
const order = sectionOrder[sectionId] ?? 999;
|
|
57
|
+
const sectionId = item.groupId;
|
|
58
|
+
const order = sectionOrder[sectionId] ?? sectionOrder[legacySettingsSectionSlug(item.group)] ?? 999;
|
|
56
59
|
if (!sectionMap.has(sectionId)) {
|
|
57
60
|
sectionMap.set(sectionId, {
|
|
58
61
|
id: sectionId,
|
|
@@ -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\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.group.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '')\n const order = sectionOrder[sectionId] ?? 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;
|
|
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": "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",
|
|
6
6
|
"names": ["byHref"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import figma from '@figma/code-connect'
|
|
3
|
+
import { Alert, AlertDescription, AlertTitle } from '../src/primitives/alert'
|
|
4
|
+
|
|
5
|
+
figma.connect(Alert, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=169-2358', {
|
|
6
|
+
imports: ["import { Alert, AlertTitle, AlertDescription } from '@open-mercato/ui/primitives/alert'"],
|
|
7
|
+
props: {
|
|
8
|
+
status: figma.enum('Status', {
|
|
9
|
+
Error: 'error',
|
|
10
|
+
Warning: 'warning',
|
|
11
|
+
Success: 'success',
|
|
12
|
+
Information: 'information',
|
|
13
|
+
Feature: 'feature',
|
|
14
|
+
}),
|
|
15
|
+
style: figma.enum('Style', {
|
|
16
|
+
Filled: 'filled',
|
|
17
|
+
Light: 'light',
|
|
18
|
+
Lighter: 'lighter',
|
|
19
|
+
Stroke: 'stroke',
|
|
20
|
+
}),
|
|
21
|
+
size: figma.enum('Size', {
|
|
22
|
+
XS: 'xs',
|
|
23
|
+
Small: 'sm',
|
|
24
|
+
Default: 'default',
|
|
25
|
+
}),
|
|
26
|
+
title: figma.string('Title'),
|
|
27
|
+
description: figma.string('Description'),
|
|
28
|
+
},
|
|
29
|
+
example: ({ status, style, size, title, description }) => (
|
|
30
|
+
<Alert status={status} style={style} size={size}>
|
|
31
|
+
<AlertTitle>{title}</AlertTitle>
|
|
32
|
+
<AlertDescription>{description}</AlertDescription>
|
|
33
|
+
</Alert>
|
|
34
|
+
),
|
|
35
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import figma from '@figma/code-connect'
|
|
3
|
+
import { Badge } from '../src/primitives/badge'
|
|
4
|
+
|
|
5
|
+
// TODO(figma): resolve the real node id in the DS file before ds:code-connect:publish.
|
|
6
|
+
figma.connect(Badge, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
7
|
+
imports: ["import { Badge } from '@open-mercato/ui/primitives/badge'"],
|
|
8
|
+
props: {
|
|
9
|
+
variant: figma.enum('Variant', {
|
|
10
|
+
Default: 'default',
|
|
11
|
+
Secondary: 'secondary',
|
|
12
|
+
Outline: 'outline',
|
|
13
|
+
Muted: 'muted',
|
|
14
|
+
Success: 'success',
|
|
15
|
+
Warning: 'warning',
|
|
16
|
+
Info: 'info',
|
|
17
|
+
Neutral: 'neutral',
|
|
18
|
+
Error: 'error',
|
|
19
|
+
Brand: 'brand',
|
|
20
|
+
}),
|
|
21
|
+
size: figma.enum('Size', {
|
|
22
|
+
Small: 'sm',
|
|
23
|
+
Default: 'default',
|
|
24
|
+
Large: 'lg',
|
|
25
|
+
}),
|
|
26
|
+
label: figma.string('Label'),
|
|
27
|
+
},
|
|
28
|
+
example: ({ variant, size, label }) => (
|
|
29
|
+
<Badge variant={variant} size={size}>
|
|
30
|
+
{label}
|
|
31
|
+
</Badge>
|
|
32
|
+
),
|
|
33
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import figma from '@figma/code-connect'
|
|
3
|
+
import { Button } from '../src/primitives/button'
|
|
4
|
+
|
|
5
|
+
// TODO(figma): resolve the real node id in the DS file before ds:code-connect:publish.
|
|
6
|
+
figma.connect(Button, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
7
|
+
imports: ["import { Button } from '@open-mercato/ui/primitives/button'"],
|
|
8
|
+
props: {
|
|
9
|
+
variant: figma.enum('Variant', {
|
|
10
|
+
Primary: 'default',
|
|
11
|
+
Destructive: 'destructive',
|
|
12
|
+
Outline: 'outline',
|
|
13
|
+
Secondary: 'secondary',
|
|
14
|
+
Ghost: 'ghost',
|
|
15
|
+
Muted: 'muted',
|
|
16
|
+
Link: 'link',
|
|
17
|
+
}),
|
|
18
|
+
size: figma.enum('Size', {
|
|
19
|
+
Default: 'default',
|
|
20
|
+
Small: 'sm',
|
|
21
|
+
Large: 'lg',
|
|
22
|
+
'2XS': '2xs',
|
|
23
|
+
Icon: 'icon',
|
|
24
|
+
}),
|
|
25
|
+
disabled: figma.boolean('Disabled'),
|
|
26
|
+
label: figma.string('Label'),
|
|
27
|
+
},
|
|
28
|
+
example: ({ variant, size, disabled, label }) => (
|
|
29
|
+
<Button variant={variant} size={size} disabled={disabled}>
|
|
30
|
+
{label}
|
|
31
|
+
</Button>
|
|
32
|
+
),
|
|
33
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import figma from '@figma/code-connect'
|
|
3
|
+
import { Checkbox } from '../src/primitives/checkbox'
|
|
4
|
+
import { CheckboxField } from '../src/primitives/checkbox-field'
|
|
5
|
+
|
|
6
|
+
// TODO(figma): resolve the real node id in the DS file before ds:code-connect:publish.
|
|
7
|
+
figma.connect(Checkbox, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
8
|
+
imports: ["import { Checkbox } from '@open-mercato/ui/primitives/checkbox'"],
|
|
9
|
+
variant: { Label: 'False' },
|
|
10
|
+
props: {
|
|
11
|
+
checked: figma.boolean('Checked'),
|
|
12
|
+
disabled: figma.boolean('Disabled'),
|
|
13
|
+
},
|
|
14
|
+
example: ({ checked, disabled }) => <Checkbox checked={checked} disabled={disabled} />,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
figma.connect(CheckboxField, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
18
|
+
imports: ["import { CheckboxField } from '@open-mercato/ui/primitives/checkbox-field'"],
|
|
19
|
+
variant: { Label: 'True' },
|
|
20
|
+
props: {
|
|
21
|
+
checked: figma.boolean('Checked'),
|
|
22
|
+
disabled: figma.boolean('Disabled'),
|
|
23
|
+
label: figma.string('Label text'),
|
|
24
|
+
},
|
|
25
|
+
example: ({ checked, disabled, label }) => (
|
|
26
|
+
<CheckboxField checked={checked} disabled={disabled} label={label} />
|
|
27
|
+
),
|
|
28
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import figma from '@figma/code-connect'
|
|
3
|
+
import {
|
|
4
|
+
Drawer,
|
|
5
|
+
DrawerBody,
|
|
6
|
+
DrawerClose,
|
|
7
|
+
DrawerContent,
|
|
8
|
+
DrawerDescription,
|
|
9
|
+
DrawerFooter,
|
|
10
|
+
DrawerHeader,
|
|
11
|
+
DrawerTitle,
|
|
12
|
+
} from '../src/primitives/drawer'
|
|
13
|
+
import { Button } from '../src/primitives/button'
|
|
14
|
+
|
|
15
|
+
figma.connect(Drawer, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=486-7366', {
|
|
16
|
+
imports: [
|
|
17
|
+
"import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerBody, DrawerFooter, DrawerClose } from '@open-mercato/ui/primitives/drawer'",
|
|
18
|
+
],
|
|
19
|
+
props: {
|
|
20
|
+
title: figma.string('Title'),
|
|
21
|
+
description: figma.string('Description'),
|
|
22
|
+
},
|
|
23
|
+
example: ({ title, description }) => (
|
|
24
|
+
<Drawer>
|
|
25
|
+
<DrawerContent>
|
|
26
|
+
<DrawerHeader>
|
|
27
|
+
<DrawerTitle>{title}</DrawerTitle>
|
|
28
|
+
<DrawerDescription>{description}</DrawerDescription>
|
|
29
|
+
</DrawerHeader>
|
|
30
|
+
<DrawerBody>Content</DrawerBody>
|
|
31
|
+
<DrawerFooter layout="equal">
|
|
32
|
+
<DrawerClose asChild>
|
|
33
|
+
<Button variant="outline">Cancel</Button>
|
|
34
|
+
</DrawerClose>
|
|
35
|
+
<Button>Save</Button>
|
|
36
|
+
</DrawerFooter>
|
|
37
|
+
</DrawerContent>
|
|
38
|
+
</Drawer>
|
|
39
|
+
),
|
|
40
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import figma from '@figma/code-connect'
|
|
3
|
+
import { Input } from '../src/primitives/input'
|
|
4
|
+
import { EmailInput } from '../src/primitives/email-input'
|
|
5
|
+
import { SearchInput } from '../src/primitives/search-input'
|
|
6
|
+
import { PasswordInput } from '../src/primitives/password-input'
|
|
7
|
+
import { WebsiteInput } from '../src/primitives/website-input'
|
|
8
|
+
|
|
9
|
+
// TODO(figma): resolve the real node id in the DS file before ds:code-connect:publish.
|
|
10
|
+
figma.connect(Input, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
11
|
+
imports: ["import { Input } from '@open-mercato/ui/primitives/input'"],
|
|
12
|
+
variant: { Type: 'Text' },
|
|
13
|
+
props: {
|
|
14
|
+
placeholder: figma.string('Placeholder'),
|
|
15
|
+
disabled: figma.boolean('Disabled'),
|
|
16
|
+
},
|
|
17
|
+
example: ({ placeholder, disabled }) => <Input placeholder={placeholder} disabled={disabled} />,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
figma.connect(EmailInput, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
21
|
+
imports: ["import { EmailInput } from '@open-mercato/ui/primitives/email-input'"],
|
|
22
|
+
variant: { Type: 'Email' },
|
|
23
|
+
props: {
|
|
24
|
+
placeholder: figma.string('Placeholder'),
|
|
25
|
+
disabled: figma.boolean('Disabled'),
|
|
26
|
+
},
|
|
27
|
+
example: ({ placeholder, disabled }) => <EmailInput placeholder={placeholder} disabled={disabled} />,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
figma.connect(SearchInput, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
31
|
+
imports: ["import { SearchInput } from '@open-mercato/ui/primitives/search-input'"],
|
|
32
|
+
variant: { Type: 'Search' },
|
|
33
|
+
props: {
|
|
34
|
+
placeholder: figma.string('Placeholder'),
|
|
35
|
+
disabled: figma.boolean('Disabled'),
|
|
36
|
+
},
|
|
37
|
+
example: ({ placeholder, disabled }) => <SearchInput placeholder={placeholder} disabled={disabled} />,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
figma.connect(PasswordInput, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
41
|
+
imports: ["import { PasswordInput } from '@open-mercato/ui/primitives/password-input'"],
|
|
42
|
+
variant: { Type: 'Password' },
|
|
43
|
+
props: {
|
|
44
|
+
placeholder: figma.string('Placeholder'),
|
|
45
|
+
disabled: figma.boolean('Disabled'),
|
|
46
|
+
},
|
|
47
|
+
example: ({ placeholder, disabled }) => <PasswordInput placeholder={placeholder} disabled={disabled} />,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
figma.connect(WebsiteInput, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
51
|
+
imports: ["import { WebsiteInput } from '@open-mercato/ui/primitives/website-input'"],
|
|
52
|
+
variant: { Type: 'Website' },
|
|
53
|
+
props: {
|
|
54
|
+
placeholder: figma.string('Placeholder'),
|
|
55
|
+
disabled: figma.boolean('Disabled'),
|
|
56
|
+
},
|
|
57
|
+
example: ({ placeholder, disabled }) => <WebsiteInput placeholder={placeholder} disabled={disabled} />,
|
|
58
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import figma from '@figma/code-connect'
|
|
3
|
+
import { Radio, RadioGroup } from '../src/primitives/radio'
|
|
4
|
+
|
|
5
|
+
// TODO(figma): resolve the real node id in the DS file before ds:code-connect:publish.
|
|
6
|
+
figma.connect(Radio, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
7
|
+
imports: ["import { Radio, RadioGroup } from '@open-mercato/ui/primitives/radio'"],
|
|
8
|
+
props: {
|
|
9
|
+
disabled: figma.boolean('Disabled'),
|
|
10
|
+
},
|
|
11
|
+
example: ({ disabled }) => (
|
|
12
|
+
<RadioGroup defaultValue="first">
|
|
13
|
+
<Radio value="first" disabled={disabled} />
|
|
14
|
+
</RadioGroup>
|
|
15
|
+
),
|
|
16
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import figma from '@figma/code-connect'
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../src/primitives/select'
|
|
4
|
+
|
|
5
|
+
// TODO(figma): resolve the real node id in the DS file before ds:code-connect:publish.
|
|
6
|
+
figma.connect(Select, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
7
|
+
imports: [
|
|
8
|
+
"import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@open-mercato/ui/primitives/select'",
|
|
9
|
+
],
|
|
10
|
+
props: {
|
|
11
|
+
placeholder: figma.string('Placeholder'),
|
|
12
|
+
disabled: figma.boolean('Disabled'),
|
|
13
|
+
},
|
|
14
|
+
example: ({ placeholder, disabled }) => (
|
|
15
|
+
<Select>
|
|
16
|
+
<SelectTrigger disabled={disabled}>
|
|
17
|
+
<SelectValue placeholder={placeholder} />
|
|
18
|
+
</SelectTrigger>
|
|
19
|
+
<SelectContent>
|
|
20
|
+
<SelectItem value="first">First option</SelectItem>
|
|
21
|
+
</SelectContent>
|
|
22
|
+
</Select>
|
|
23
|
+
),
|
|
24
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import figma from '@figma/code-connect'
|
|
3
|
+
import { StatusBadge } from '../src/primitives/status-badge'
|
|
4
|
+
|
|
5
|
+
// TODO(figma): resolve the real node id in the DS file before ds:code-connect:publish.
|
|
6
|
+
figma.connect(StatusBadge, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
7
|
+
imports: ["import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'"],
|
|
8
|
+
props: {
|
|
9
|
+
variant: figma.enum('Variant', {
|
|
10
|
+
Success: 'success',
|
|
11
|
+
Warning: 'warning',
|
|
12
|
+
Error: 'error',
|
|
13
|
+
Info: 'info',
|
|
14
|
+
Neutral: 'neutral',
|
|
15
|
+
}),
|
|
16
|
+
dot: figma.boolean('Dot'),
|
|
17
|
+
label: figma.string('Label'),
|
|
18
|
+
},
|
|
19
|
+
example: ({ variant, dot, label }) => (
|
|
20
|
+
<StatusBadge variant={variant} dot={dot}>
|
|
21
|
+
{label}
|
|
22
|
+
</StatusBadge>
|
|
23
|
+
),
|
|
24
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import figma from '@figma/code-connect'
|
|
3
|
+
import { Switch } from '../src/primitives/switch'
|
|
4
|
+
import { SwitchField } from '../src/primitives/switch-field'
|
|
5
|
+
|
|
6
|
+
// TODO(figma): resolve the real node id in the DS file before ds:code-connect:publish.
|
|
7
|
+
figma.connect(Switch, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
8
|
+
imports: ["import { Switch } from '@open-mercato/ui/primitives/switch'"],
|
|
9
|
+
variant: { Label: 'False' },
|
|
10
|
+
props: {
|
|
11
|
+
checked: figma.boolean('Checked'),
|
|
12
|
+
disabled: figma.boolean('Disabled'),
|
|
13
|
+
},
|
|
14
|
+
example: ({ checked, disabled }) => <Switch checked={checked} disabled={disabled} />,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
figma.connect(SwitchField, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
18
|
+
imports: ["import { SwitchField } from '@open-mercato/ui/primitives/switch-field'"],
|
|
19
|
+
variant: { Label: 'True' },
|
|
20
|
+
props: {
|
|
21
|
+
checked: figma.boolean('Checked'),
|
|
22
|
+
disabled: figma.boolean('Disabled'),
|
|
23
|
+
label: figma.string('Label text'),
|
|
24
|
+
},
|
|
25
|
+
example: ({ checked, disabled, label }) => (
|
|
26
|
+
<SwitchField checked={checked} disabled={disabled} label={label} />
|
|
27
|
+
),
|
|
28
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import figma from '@figma/code-connect'
|
|
3
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../src/primitives/tabs'
|
|
4
|
+
|
|
5
|
+
// TODO(figma): resolve the real node id in the DS file before ds:code-connect:publish.
|
|
6
|
+
figma.connect(Tabs, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
7
|
+
imports: ["import { Tabs, TabsList, TabsTrigger, TabsContent } from '@open-mercato/ui/primitives/tabs'"],
|
|
8
|
+
props: {
|
|
9
|
+
label: figma.string('Label'),
|
|
10
|
+
count: figma.string('Count'),
|
|
11
|
+
},
|
|
12
|
+
example: ({ label, count }) => (
|
|
13
|
+
<Tabs defaultValue="first" variant="underline">
|
|
14
|
+
<TabsList aria-label="Sections">
|
|
15
|
+
<TabsTrigger value="first" count={count}>
|
|
16
|
+
{label}
|
|
17
|
+
</TabsTrigger>
|
|
18
|
+
</TabsList>
|
|
19
|
+
<TabsContent value="first">Content</TabsContent>
|
|
20
|
+
</Tabs>
|
|
21
|
+
),
|
|
22
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import figma from '@figma/code-connect'
|
|
3
|
+
import { Tag } from '../src/primitives/tag'
|
|
4
|
+
|
|
5
|
+
// TODO(figma): resolve the real node id in the DS file before ds:code-connect:publish.
|
|
6
|
+
figma.connect(Tag, 'https://www.figma.com/design/qCq9z6q1if0mpoRstV5OEA/Design-System?node-id=0-1', {
|
|
7
|
+
imports: ["import { Tag } from '@open-mercato/ui/primitives/tag'"],
|
|
8
|
+
props: {
|
|
9
|
+
variant: figma.enum('Variant', {
|
|
10
|
+
Default: 'default',
|
|
11
|
+
Success: 'success',
|
|
12
|
+
Warning: 'warning',
|
|
13
|
+
Error: 'error',
|
|
14
|
+
Info: 'info',
|
|
15
|
+
Neutral: 'neutral',
|
|
16
|
+
Brand: 'brand',
|
|
17
|
+
Pink: 'pink',
|
|
18
|
+
}),
|
|
19
|
+
dot: figma.boolean('Dot'),
|
|
20
|
+
label: figma.string('Label'),
|
|
21
|
+
},
|
|
22
|
+
example: ({ variant, dot, label }) => (
|
|
23
|
+
<Tag variant={variant} dot={dot}>
|
|
24
|
+
{label}
|
|
25
|
+
</Tag>
|
|
26
|
+
),
|
|
27
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6824.1.21fff90682",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,13 +155,14 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
158
|
+
"@open-mercato/shared": "0.6.7-develop.6824.1.21fff90682",
|
|
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.7-develop.6824.1.21fff90682",
|
|
165
166
|
"@testing-library/dom": "^10.4.1",
|
|
166
167
|
"@testing-library/jest-dom": "^6.9.1",
|
|
167
168
|
"@testing-library/react": "^16.3.1",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
jest.setTimeout(15000)
|
|
3
|
+
|
|
4
|
+
// Regression guard for #4400: an injected group widget placed in column 1 must
|
|
5
|
+
// render as a full-width row in the main stack — CrudForm must NOT switch to
|
|
6
|
+
// the narrow 7fr/3fr secondary-column layout unless a widget explicitly asks
|
|
7
|
+
// for column 2. The hook is mocked so the test drives the injected widgets
|
|
8
|
+
// directly, following the CrudForm.fieldInjection.test.tsx pattern.
|
|
9
|
+
let injectedGroupWidgets: unknown[] = []
|
|
10
|
+
|
|
11
|
+
jest.mock('next/navigation', () => ({
|
|
12
|
+
useRouter: () => ({ push: () => {} }),
|
|
13
|
+
usePathname: () => '/',
|
|
14
|
+
useSearchParams: () => new URLSearchParams(),
|
|
15
|
+
}))
|
|
16
|
+
jest.mock('remark-gfm', () => ({ __esModule: true, default: {} }))
|
|
17
|
+
jest.mock('../injection/InjectionSpot', () => ({
|
|
18
|
+
__esModule: true,
|
|
19
|
+
InjectionSpot: () => null,
|
|
20
|
+
useInjectionWidgets: () => ({ widgets: injectedGroupWidgets, loading: false, error: null }),
|
|
21
|
+
useInjectionSpotEvents: () => ({ triggerEvent: jest.fn(async () => ({ ok: true, data: {} })) }),
|
|
22
|
+
}))
|
|
23
|
+
jest.mock('../injection/useInjectionDataWidgets', () => ({
|
|
24
|
+
__esModule: true,
|
|
25
|
+
useInjectionDataWidgets: () => ({ widgets: [], isLoading: false, error: null }),
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
import * as React from 'react'
|
|
29
|
+
import { waitFor } from '@testing-library/react'
|
|
30
|
+
import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
|
|
31
|
+
import { CrudForm, type CrudField, type CrudFormGroup } from '../CrudForm'
|
|
32
|
+
|
|
33
|
+
const fields: CrudField[] = [
|
|
34
|
+
{ id: 'name', label: 'Name', type: 'text' },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const groups: CrudFormGroup[] = [
|
|
38
|
+
{ id: 'contact', label: 'Contact', fields: ['name'] },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
function makeGroupWidget(column: 1 | 2) {
|
|
42
|
+
return {
|
|
43
|
+
widgetId: 'customer_accounts.injection.company-users',
|
|
44
|
+
placement: { kind: 'group', column, groupLabel: 'Portal users', priority: 200 },
|
|
45
|
+
module: {
|
|
46
|
+
metadata: { title: 'Portal users', description: '' },
|
|
47
|
+
Widget: () => <div data-testid="portal-users-widget">portal users</div>,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderForm() {
|
|
53
|
+
return renderWithProviders(
|
|
54
|
+
React.createElement(CrudForm as never, {
|
|
55
|
+
title: 'Company',
|
|
56
|
+
entityId: 'customers:company',
|
|
57
|
+
fields,
|
|
58
|
+
groups,
|
|
59
|
+
onSubmit: () => {},
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('CrudForm injected group column placement (#4400)', () => {
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
injectedGroupWidgets = []
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('renders a column-1 group widget as a full-width row without the secondary column', async () => {
|
|
70
|
+
injectedGroupWidgets = [makeGroupWidget(1)]
|
|
71
|
+
|
|
72
|
+
const { container } = renderForm()
|
|
73
|
+
|
|
74
|
+
await waitFor(() => {
|
|
75
|
+
expect(container.querySelector('[data-testid="portal-users-widget"]')).toBeTruthy()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(container.querySelector('[data-crud-injection-region]')).toBeNull()
|
|
79
|
+
const twoColumnGrid = Array.from(container.querySelectorAll('div')).find((node) =>
|
|
80
|
+
node.className.includes('7fr_3fr'),
|
|
81
|
+
)
|
|
82
|
+
expect(twoColumnGrid).toBeUndefined()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('still supports an explicit column-2 group widget via the secondary column (sanity)', async () => {
|
|
86
|
+
injectedGroupWidgets = [makeGroupWidget(2)]
|
|
87
|
+
|
|
88
|
+
const { container } = renderForm()
|
|
89
|
+
|
|
90
|
+
await waitFor(() => {
|
|
91
|
+
expect(container.querySelector('[data-testid="portal-users-widget"]')).toBeTruthy()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const secondaryRegion = container.querySelector('[data-crud-injection-region]')
|
|
95
|
+
expect(secondaryRegion).toBeTruthy()
|
|
96
|
+
expect(secondaryRegion?.querySelector('[data-testid="portal-users-widget"]')).toBeTruthy()
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -202,4 +202,143 @@ describe('settings navigation helpers', () => {
|
|
|
202
202
|
)
|
|
203
203
|
expect(allSettingsHrefs).not.toContain('/backend/config/system-status')
|
|
204
204
|
})
|
|
205
|
+
|
|
206
|
+
it('orders settings sections by the untranslated group id, not the rendered label', () => {
|
|
207
|
+
const polishEntry = (
|
|
208
|
+
group: string,
|
|
209
|
+
groupKey: string,
|
|
210
|
+
href: string,
|
|
211
|
+
): AdminNavItem => ({
|
|
212
|
+
group,
|
|
213
|
+
groupId: groupKey,
|
|
214
|
+
groupKey,
|
|
215
|
+
groupDefaultName: group,
|
|
216
|
+
title: href,
|
|
217
|
+
defaultTitle: href,
|
|
218
|
+
href,
|
|
219
|
+
enabled: true,
|
|
220
|
+
order: 10,
|
|
221
|
+
pageContext: 'settings',
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const entries: AdminNavItem[] = [
|
|
225
|
+
polishEntry('System', 'settings.sections.system', '/backend/config/system-status'),
|
|
226
|
+
polishEntry('Konfiguracja modułów', 'settings.sections.moduleConfigs', '/backend/config/wms'),
|
|
227
|
+
polishEntry('Autoryzacja', 'settings.sections.auth', '/backend/users'),
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
const sections = buildSettingsSections(entries, {
|
|
231
|
+
'settings.sections.system': 1,
|
|
232
|
+
'settings.sections.auth': 2,
|
|
233
|
+
'settings.sections.moduleConfigs': 5,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
expect(sections.map((section) => section.id)).toEqual([
|
|
237
|
+
'settings.sections.system',
|
|
238
|
+
'settings.sections.auth',
|
|
239
|
+
'settings.sections.moduleConfigs',
|
|
240
|
+
])
|
|
241
|
+
expect(sections.map((section) => section.order)).toEqual([1, 2, 5])
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('keeps the section label and labelKey of the rendered group', () => {
|
|
245
|
+
const entries: AdminNavItem[] = [
|
|
246
|
+
{
|
|
247
|
+
group: 'Konfiguracja modułów',
|
|
248
|
+
groupId: 'settings.sections.moduleConfigs',
|
|
249
|
+
groupKey: 'settings.sections.moduleConfigs',
|
|
250
|
+
groupDefaultName: 'Konfiguracja modułów',
|
|
251
|
+
title: 'WMS',
|
|
252
|
+
defaultTitle: 'WMS',
|
|
253
|
+
href: '/backend/config/wms',
|
|
254
|
+
enabled: true,
|
|
255
|
+
pageContext: 'settings',
|
|
256
|
+
},
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
const [section] = buildSettingsSections(entries, { 'settings.sections.moduleConfigs': 5 })
|
|
260
|
+
|
|
261
|
+
expect(section.label).toBe('Konfiguracja modułów')
|
|
262
|
+
expect(section.labelKey).toBe('settings.sections.moduleConfigs')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('still honors legacy sectionOrder maps keyed by the English label slug', () => {
|
|
266
|
+
const entries: AdminNavItem[] = [
|
|
267
|
+
{
|
|
268
|
+
group: 'Module Configs',
|
|
269
|
+
groupId: 'settings.sections.moduleConfigs',
|
|
270
|
+
groupKey: 'settings.sections.moduleConfigs',
|
|
271
|
+
groupDefaultName: 'Module Configs',
|
|
272
|
+
title: 'WMS',
|
|
273
|
+
defaultTitle: 'WMS',
|
|
274
|
+
href: '/backend/config/wms',
|
|
275
|
+
enabled: true,
|
|
276
|
+
pageContext: 'settings',
|
|
277
|
+
},
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
const [section] = buildSettingsSections(entries, { 'module-configs': 5 })
|
|
281
|
+
|
|
282
|
+
expect(section.order).toBe(5)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('falls back to the catch-all weight for groups the order map does not name', () => {
|
|
286
|
+
const entries: AdminNavItem[] = [
|
|
287
|
+
{
|
|
288
|
+
group: 'Bezpieczeństwo',
|
|
289
|
+
groupId: 'settings.sections.security',
|
|
290
|
+
groupKey: 'settings.sections.security',
|
|
291
|
+
groupDefaultName: 'Bezpieczeństwo',
|
|
292
|
+
title: 'Audit Logs',
|
|
293
|
+
defaultTitle: 'Audit Logs',
|
|
294
|
+
href: '/backend/audit-logs',
|
|
295
|
+
enabled: true,
|
|
296
|
+
pageContext: 'settings',
|
|
297
|
+
},
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
const [section] = buildSettingsSections(entries, { 'settings.sections.system': 1 })
|
|
301
|
+
|
|
302
|
+
expect(section.order).toBe(999)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
// A partially translated locale renders `translate(groupKey, group)` as the localized label for
|
|
306
|
+
// pages whose key is covered and as the raw English fallback for the rest, so one group can reach
|
|
307
|
+
// buildSettingsSections under two different labels.
|
|
308
|
+
it('groups entries that share a group id even when labels differ', () => {
|
|
309
|
+
const entries: AdminNavItem[] = [
|
|
310
|
+
{
|
|
311
|
+
group: 'Konfiguracja modułów',
|
|
312
|
+
groupId: 'settings.sections.moduleConfigs',
|
|
313
|
+
groupKey: 'settings.sections.moduleConfigs',
|
|
314
|
+
groupDefaultName: 'Konfiguracja modułów',
|
|
315
|
+
title: 'WMS',
|
|
316
|
+
defaultTitle: 'WMS',
|
|
317
|
+
href: '/backend/config/wms',
|
|
318
|
+
enabled: true,
|
|
319
|
+
order: 20,
|
|
320
|
+
pageContext: 'settings',
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
group: 'Module Configs',
|
|
324
|
+
groupId: 'settings.sections.moduleConfigs',
|
|
325
|
+
groupKey: 'settings.sections.moduleConfigs',
|
|
326
|
+
groupDefaultName: 'Module Configs',
|
|
327
|
+
title: 'Sales',
|
|
328
|
+
defaultTitle: 'Sales',
|
|
329
|
+
href: '/backend/config/sales',
|
|
330
|
+
enabled: true,
|
|
331
|
+
order: 10,
|
|
332
|
+
pageContext: 'settings',
|
|
333
|
+
},
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
const sections = buildSettingsSections(entries, { 'settings.sections.moduleConfigs': 5 })
|
|
337
|
+
|
|
338
|
+
expect(sections).toHaveLength(1)
|
|
339
|
+
expect(sections[0].items.map((item) => item.href)).toEqual([
|
|
340
|
+
'/backend/config/sales',
|
|
341
|
+
'/backend/config/wms',
|
|
342
|
+
])
|
|
343
|
+
})
|
|
205
344
|
})
|
|
@@ -75,4 +75,25 @@ describe('AttachmentAssignmentsEditor (metadata dialog)', () => {
|
|
|
75
75
|
fireEvent.click(screen.getByRole('button', { name: 'Remove assignment' }))
|
|
76
76
|
expect(screen.queryByPlaceholderText('catalog.product')).toBeNull()
|
|
77
77
|
})
|
|
78
|
+
|
|
79
|
+
it('keeps the surviving assignment values after removing a middle row', () => {
|
|
80
|
+
render(
|
|
81
|
+
<EditorHarness
|
|
82
|
+
initial={[
|
|
83
|
+
{ type: 'catalog.product', id: 'p-1', href: '', label: '' },
|
|
84
|
+
{ type: 'sales.order', id: 'o-2', href: '', label: '' },
|
|
85
|
+
{ type: 'customers.person', id: 'c-3', href: '', label: '' },
|
|
86
|
+
]}
|
|
87
|
+
/>,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
fireEvent.click(screen.getAllByRole('button', { name: labels.remove })[1])
|
|
91
|
+
|
|
92
|
+
const typeInputs = screen.getAllByPlaceholderText('catalog.product') as HTMLInputElement[]
|
|
93
|
+
const idInputs = screen.getAllByPlaceholderText('Record ID') as HTMLInputElement[]
|
|
94
|
+
expect(typeInputs[0]).toHaveValue('catalog.product')
|
|
95
|
+
expect(idInputs[0]).toHaveValue('p-1')
|
|
96
|
+
expect(typeInputs[1]).toHaveValue('customers.person')
|
|
97
|
+
expect(idInputs[1]).toHaveValue('c-3')
|
|
98
|
+
})
|
|
78
99
|
})
|
package/src/backend/utils/nav.ts
CHANGED
|
@@ -100,6 +100,26 @@ export type SettingsSectionItem = {
|
|
|
100
100
|
children?: SettingsSectionItem[]
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Slug of a rendered group label, used as the pre-#4843 settings section id.
|
|
105
|
+
*
|
|
106
|
+
* `sectionOrder` used to be keyed by these slugs, which made ordering depend on the active locale.
|
|
107
|
+
* Kept only as a lookup fallback so third-party callers passing a legacy map keep their weights.
|
|
108
|
+
*
|
|
109
|
+
* @deprecated Key `sectionOrder` by the untranslated group id (`AdminNavItem.groupId`) instead.
|
|
110
|
+
*/
|
|
111
|
+
function legacySettingsSectionSlug(groupLabel: string): string {
|
|
112
|
+
return groupLabel.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Groups the settings-context nav entries into ordered sections.
|
|
117
|
+
*
|
|
118
|
+
* `sectionOrder` is keyed by the untranslated group id (`AdminNavItem.groupId`, i.e. the page's
|
|
119
|
+
* `pageGroupKey` when it declares one) — the same convention the main sidebar's `defaultGroupOrder`
|
|
120
|
+
* follows. Keying off the rendered label instead made every non-English deployment miss its weights
|
|
121
|
+
* and fall back to the catch-all bucket (#4843).
|
|
122
|
+
*/
|
|
103
123
|
export function buildSettingsSections(
|
|
104
124
|
entries: AdminNavItem[],
|
|
105
125
|
sectionOrder: Record<string, number>
|
|
@@ -123,8 +143,8 @@ export function buildSettingsSections(
|
|
|
123
143
|
}
|
|
124
144
|
|
|
125
145
|
for (const item of settingsItems) {
|
|
126
|
-
const sectionId = item.
|
|
127
|
-
const order = sectionOrder[sectionId] ?? 999
|
|
146
|
+
const sectionId = item.groupId
|
|
147
|
+
const order = sectionOrder[sectionId] ?? sectionOrder[legacySettingsSectionSlug(item.group)] ?? 999
|
|
128
148
|
|
|
129
149
|
if (!sectionMap.has(sectionId)) {
|
|
130
150
|
sectionMap.set(sectionId, {
|