@open-mercato/ui 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6814.1.0627c7e9f1
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/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/detail/AttachmentMetadataDialog.tsx +1 -1
- package/src/backend/detail/__tests__/AttachmentAssignmentsEditor.test.tsx +21 -0
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": "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",
|
|
6
6
|
"names": []
|
|
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.6814.1.0627c7e9f1",
|
|
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.6814.1.0627c7e9f1",
|
|
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.6814.1.0627c7e9f1",
|
|
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
|
+
})
|
|
@@ -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
|
})
|