@open-mercato/ui 0.6.6-develop.6413.1.ae875fb682 → 0.6.6-develop.6416.1.888fffc007
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/detail/AttachmentMetadataDialog.js +78 -45
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterPanel.js +1 -0
- package/dist/backend/filters/AdvancedFilterPanel.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/__tests__/AttachmentMetadataDialog.test.tsx +145 -0
- package/src/backend/detail/AttachmentMetadataDialog.tsx +73 -39
- package/src/backend/filters/AdvancedFilterPanel.tsx +1 -1
|
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { Button } from "../../primitives/button.js";
|
|
6
|
+
import { IconButton } from "../../primitives/icon-button.js";
|
|
6
7
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@open-mercato/ui/primitives/dialog";
|
|
7
8
|
import { Input } from "@open-mercato/ui/primitives/input";
|
|
8
9
|
import { TagsInput } from "@open-mercato/ui/backend/inputs/TagsInput";
|
|
@@ -76,56 +77,88 @@ function AssignmentInputRow({
|
|
|
76
77
|
disabled,
|
|
77
78
|
onRemove
|
|
78
79
|
}) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium", children: labels.id }),
|
|
94
|
-
/* @__PURE__ */ jsx(
|
|
95
|
-
Input,
|
|
96
|
-
{
|
|
97
|
-
value: value.id,
|
|
98
|
-
onChange: (event) => onChange({ ...value, id: event.target.value }),
|
|
99
|
-
placeholder: "Record ID",
|
|
100
|
-
disabled
|
|
101
|
-
}
|
|
102
|
-
)
|
|
103
|
-
] }),
|
|
104
|
-
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
105
|
-
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium", children: labels.href }),
|
|
106
|
-
/* @__PURE__ */ jsx(
|
|
107
|
-
Input,
|
|
108
|
-
{
|
|
109
|
-
value: value.href ?? "",
|
|
110
|
-
onChange: (event) => onChange({ ...value, href: event.target.value }),
|
|
111
|
-
placeholder: "https://",
|
|
112
|
-
disabled
|
|
113
|
-
}
|
|
114
|
-
)
|
|
115
|
-
] }),
|
|
116
|
-
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
117
|
-
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium", children: labels.label }),
|
|
80
|
+
const trimmedLabel = value.label?.trim() ?? "";
|
|
81
|
+
const trimmedType = value.type?.trim() ?? "";
|
|
82
|
+
const trimmedId = value.id?.trim() ?? "";
|
|
83
|
+
const primaryLabel = trimmedLabel || trimmedType || trimmedId;
|
|
84
|
+
const secondaryLabel = [
|
|
85
|
+
trimmedLabel ? trimmedType : "",
|
|
86
|
+
trimmedId && trimmedId !== primaryLabel ? trimmedId : ""
|
|
87
|
+
].filter(Boolean).join(" - ");
|
|
88
|
+
return /* @__PURE__ */ jsxs("div", { "data-assignment-card": true, className: "min-w-0 rounded-md border border-border/70 bg-background p-3", children: [
|
|
89
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-3 flex min-w-0 items-start justify-between gap-3", children: [
|
|
90
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
91
|
+
primaryLabel ? /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-foreground", children: primaryLabel }) : null,
|
|
92
|
+
secondaryLabel ? /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: secondaryLabel }) : null
|
|
93
|
+
] }),
|
|
118
94
|
/* @__PURE__ */ jsx(
|
|
119
|
-
|
|
95
|
+
IconButton,
|
|
120
96
|
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
97
|
+
type: "button",
|
|
98
|
+
variant: "ghost",
|
|
99
|
+
size: "default",
|
|
100
|
+
"aria-label": labels.remove,
|
|
101
|
+
onClick: onRemove,
|
|
102
|
+
disabled,
|
|
103
|
+
className: "shrink-0",
|
|
104
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "size-4" })
|
|
125
105
|
}
|
|
126
106
|
)
|
|
127
107
|
] }),
|
|
128
|
-
/* @__PURE__ */
|
|
108
|
+
/* @__PURE__ */ jsxs("div", { className: "grid min-w-0 grid-cols-1 gap-3 md:grid-cols-2", children: [
|
|
109
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1", children: [
|
|
110
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium", children: labels.type }),
|
|
111
|
+
/* @__PURE__ */ jsx(
|
|
112
|
+
Input,
|
|
113
|
+
{
|
|
114
|
+
className: "w-full min-w-0",
|
|
115
|
+
value: value.type,
|
|
116
|
+
onChange: (event) => onChange({ ...value, type: event.target.value }),
|
|
117
|
+
placeholder: "catalog.product",
|
|
118
|
+
disabled
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
] }),
|
|
122
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1", children: [
|
|
123
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium", children: labels.id }),
|
|
124
|
+
/* @__PURE__ */ jsx(
|
|
125
|
+
Input,
|
|
126
|
+
{
|
|
127
|
+
className: "w-full min-w-0",
|
|
128
|
+
value: value.id,
|
|
129
|
+
onChange: (event) => onChange({ ...value, id: event.target.value }),
|
|
130
|
+
placeholder: "Record ID",
|
|
131
|
+
disabled
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
] }),
|
|
135
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1 md:col-span-2", children: [
|
|
136
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium", children: labels.href }),
|
|
137
|
+
/* @__PURE__ */ jsx(
|
|
138
|
+
Input,
|
|
139
|
+
{
|
|
140
|
+
className: "w-full min-w-0",
|
|
141
|
+
value: value.href ?? "",
|
|
142
|
+
onChange: (event) => onChange({ ...value, href: event.target.value }),
|
|
143
|
+
placeholder: "https://",
|
|
144
|
+
disabled
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
] }),
|
|
148
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1 md:col-span-2", children: [
|
|
149
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium", children: labels.label }),
|
|
150
|
+
/* @__PURE__ */ jsx(
|
|
151
|
+
Input,
|
|
152
|
+
{
|
|
153
|
+
className: "w-full min-w-0",
|
|
154
|
+
value: value.label ?? "",
|
|
155
|
+
onChange: (event) => onChange({ ...value, label: event.target.value }),
|
|
156
|
+
placeholder: "Optional label",
|
|
157
|
+
disabled
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
] })
|
|
161
|
+
] })
|
|
129
162
|
] });
|
|
130
163
|
}
|
|
131
164
|
function AttachmentAssignmentsEditor({
|
|
@@ -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 { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\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 return (\n <div className=\"grid grid-cols-1 gap-2 rounded-md border border-border/70 bg-background p-3 sm:grid-cols-2 lg:grid-cols-[1.2fr_1.2fr_1.6fr_1fr_auto]\">\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium\">{labels.type}</label>\n <Input\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=\"space-y-1\">\n <label className=\"text-xs font-medium\">{labels.id}</label>\n <Input\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=\"space-y-1\">\n <label className=\"text-xs font-medium\">{labels.href}</label>\n <Input\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=\"space-y-1\">\n <label className=\"text-xs font-medium\">{labels.label}</label>\n <Input\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 className=\"flex items-end\">\n <Button type=\"button\" variant=\"ghost\" size=\"icon\" onClick={onRemove} disabled={disabled}>\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n )\n}\n\nfunction 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={`${entry.type}-${entry.id}-${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 <div className=\"flex flex-wrap gap-4 border-b px-3 py-2 text-sm font-medium\" role=\"tablist\">\n {(['preview', 'resize'] as const).map((tab) => (\n <Button\n key={tab}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n role=\"tab\"\n aria-selected={imageTab === tab}\n onClick={() => setImageTab(tab)}\n className={cn(\n 'h-auto -mb-px rounded-none border-b-2 border-transparent px-0 py-1',\n imageTab === tab\n ? 'border-accent-indigo text-foreground'\n : 'text-muted-foreground hover:text-foreground',\n )}\n >\n {tab === 'preview'\n ? t('attachments.library.metadata.preview', 'Preview')\n : t('attachments.library.metadata.resizeTool.title', 'Generate resized URL')}\n </Button>\n ))}\n </div>\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
|
-
"mappings": "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",
|
|
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 { 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\nfunction 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={`${entry.type}-${entry.id}-${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 <div className=\"flex flex-wrap gap-4 border-b px-3 py-2 text-sm font-medium\" role=\"tablist\">\n {(['preview', 'resize'] as const).map((tab) => (\n <Button\n key={tab}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n role=\"tab\"\n aria-selected={imageTab === tab}\n onClick={() => setImageTab(tab)}\n className={cn(\n 'h-auto -mb-px rounded-none border-b-2 border-transparent px-0 py-1',\n imageTab === tab\n ? 'border-accent-indigo text-foreground'\n : 'text-muted-foreground hover:text-foreground',\n )}\n >\n {tab === 'preview'\n ? t('attachments.library.metadata.preview', 'Preview')\n : t('attachments.library.metadata.resizeTool.title', 'Generate resized URL')}\n </Button>\n ))}\n </div>\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
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -300,6 +300,7 @@ function AdvancedFilterPanel(props) {
|
|
|
300
300
|
align: "end",
|
|
301
301
|
sideOffset: 8,
|
|
302
302
|
"data-testid": "advanced-filter-panel",
|
|
303
|
+
ded: true,
|
|
303
304
|
onPointerDownOutside: ignoreAdvancedFilterPortalInteractions,
|
|
304
305
|
onFocusOutside: ignoreAdvancedFilterPortalInteractions,
|
|
305
306
|
onInteractOutside: ignoreAdvancedFilterPortalInteractions,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/filters/AdvancedFilterPanel.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Popover, PopoverAnchor, PopoverContent } from '../../primitives/popover'\nimport { VisuallyHidden } from '@radix-ui/react-visually-hidden'\nimport { Alert, AlertDescription } from '../../primitives/alert'\nimport { Button } from '../../primitives/button'\nimport { Input } from '../../primitives/input'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../../primitives/dialog'\nimport { Filter, Save, Trash2 } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n deserializeTreeFromPersist,\n isPersistedFilterTree,\n serializeTreeForPersist,\n type AdvancedFilterTree,\n type PersistedFilterTree,\n} from '@open-mercato/shared/lib/query/advanced-filter-tree'\nimport type { FilterFieldDef } from '@open-mercato/shared/lib/query/advanced-filter'\nimport { getDefaultOperator } from '@open-mercato/shared/lib/query/advanced-filter'\nimport type { ValidationError } from './filterValidation'\nimport { AdvancedFilterBuilder } from './AdvancedFilterBuilder'\nimport { FilterEmptyState } from './FilterEmptyState'\nimport { QuickFilters, type FilterPreset } from './QuickFilters'\nimport { FilterFieldPicker } from './FilterFieldPicker'\nimport { treeReducer } from './treeReducer'\nimport { flash } from '../FlashMessages'\n\nexport type AdvancedFilterPanelProps = {\n fields: FilterFieldDef[]\n value: AdvancedFilterTree\n onChange: (t: AdvancedFilterTree) => void\n onApply: () => void\n onClear: () => void\n onFlush?: () => void\n pendingErrors: ValidationError[]\n userId: string\n presets: FilterPreset[]\n open: boolean\n onOpenChange: (open: boolean) => void\n triggerRef?: React.RefObject<HTMLElement | null>\n aiSlot?: React.ReactNode\n savedFilterStorageKey?: string\n}\n\ntype SavedAdvancedFilter = {\n id: string\n name: string\n tree: PersistedFilterTree\n createdAt: string\n updatedAt: string\n}\n\n/**\n * Versioned envelope for saved-filter records in localStorage. The `v` discriminator\n * lets future schema changes (rename fields, retention policy, sharing flags) migrate\n * existing records instead of silently dropping them. Bump `v` when adding a breaking\n * change and add a read-old migration branch in `readSavedFilters`.\n */\ntype SavedFiltersEnvelopeV1 = {\n v: 1\n filters: SavedAdvancedFilter[]\n}\n\nconst SAVED_FILTER_STORAGE_PREFIX = 'open-mercato:advanced-filters:'\nconst MAX_SAVED_FILTERS = 20\nconst CURRENT_SAVED_FILTERS_VERSION = 1 as const\n\nfunction makeSavedFilterStorageKey(key?: string): string | null {\n const trimmed = key?.trim()\n return trimmed ? `${SAVED_FILTER_STORAGE_PREFIX}${trimmed}` : null\n}\n\nfunction isSavedAdvancedFilter(value: unknown): value is SavedAdvancedFilter {\n if (!value || typeof value !== 'object') return false\n const record = value as Record<string, unknown>\n return (\n typeof record.id === 'string'\n && typeof record.name === 'string'\n && typeof record.createdAt === 'string'\n && typeof record.updatedAt === 'string'\n && isPersistedFilterTree(record.tree)\n )\n}\n\nfunction readSavedFilters(storageKey: string): SavedAdvancedFilter[] {\n if (typeof window === 'undefined') return []\n try {\n const raw = window.localStorage.getItem(storageKey)\n if (!raw) return []\n const parsed = JSON.parse(raw)\n // v1 envelope (current shape).\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n const envelope = parsed as { v?: unknown; filters?: unknown }\n if (envelope.v === 1 && Array.isArray(envelope.filters)) {\n return envelope.filters.filter(isSavedAdvancedFilter)\n }\n }\n // Pre-envelope shape (bare array) \u2014 migrate forward on next write.\n if (Array.isArray(parsed)) {\n return parsed.filter(isSavedAdvancedFilter)\n }\n return []\n } catch {\n return []\n }\n}\n\nfunction writeSavedFilters(storageKey: string, filters: SavedAdvancedFilter[]) {\n if (typeof window === 'undefined') return\n const envelope: SavedFiltersEnvelopeV1 = { v: CURRENT_SAVED_FILTERS_VERSION, filters }\n window.localStorage.setItem(storageKey, JSON.stringify(envelope))\n}\n\nfunction SaveFilterDialog({\n open,\n onOpenChange,\n onSave,\n t,\n}: {\n open: boolean\n onOpenChange: (open: boolean) => void\n onSave: (name: string) => Promise<void>\n t: (key: string, fallback: string) => string\n}) {\n const [name, setName] = React.useState('')\n const [saving, setSaving] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const trimmed = name.trim()\n\n React.useEffect(() => {\n if (!open) {\n setName('')\n setSaving(false)\n setError(null)\n }\n }, [open])\n\n const submit = React.useCallback(async () => {\n if (!trimmed || saving) return\n setSaving(true)\n setError(null)\n try {\n await onSave(trimmed)\n onOpenChange(false)\n } catch (err: unknown) {\n setError(err instanceof Error ? err.message : t('ui.advancedFilter.saveFilter.error', 'Failed to save filter'))\n } finally {\n setSaving(false)\n }\n }, [onSave, onOpenChange, saving, t, trimmed])\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n {/* `elevated` lifts this dialog above the popover (z-popover=45) \u2014 at default\n z-modal=40 the dialog is occluded by the popover that opens it. */}\n <DialogContent elevated className=\"sm:max-w-md\" data-advanced-filter-portal>\n <form\n className=\"contents\"\n onSubmit={(event) => {\n event.preventDefault()\n void submit()\n }}\n >\n <DialogHeader>\n <DialogTitle>{t('ui.advancedFilter.saveFilter.title', 'Save filter')}</DialogTitle>\n <DialogDescription>\n {t('ui.advancedFilter.saveFilter.description', 'Name this filter so you can reuse the current conditions later.')}\n </DialogDescription>\n </DialogHeader>\n <div className=\"space-y-2\">\n <Input\n value={name}\n onChange={(event) => setName(event.target.value)}\n placeholder={t('ui.advancedFilter.saveFilter.placeholder', 'Filter name')}\n aria-label={t('ui.advancedFilter.saveFilter.nameAriaLabel', 'Filter name')}\n autoFocus\n />\n {error ? <p className=\"text-sm text-status-error-text\">{error}</p> : null}\n </div>\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => onOpenChange(false)}>\n {t('common.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={!trimmed || saving}>\n {saving ? t('ui.advancedFilter.saveFilter.saving', 'Saving...') : t('ui.advancedFilter.saveFilter.submit', 'Save filter')}\n </Button>\n </DialogFooter>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction SavedFiltersSection({\n filters,\n canSaveCurrent,\n onSaveClick,\n onApply,\n onDelete,\n t,\n}: {\n filters: SavedAdvancedFilter[]\n canSaveCurrent: boolean\n onSaveClick: () => void\n onApply: (filter: SavedAdvancedFilter) => void\n onDelete: (filterId: string) => void\n t: (key: string, fallback: string) => string\n}) {\n if (!canSaveCurrent && filters.length === 0) return null\n return (\n <div className=\"flex flex-col gap-2\" data-testid=\"advanced-filter-saved-filters\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"text-overline font-medium uppercase tracking-widest text-muted-foreground\">\n {t('ui.advancedFilter.savedFilters.heading', 'Saved filters')}\n </div>\n {canSaveCurrent ? (\n <Button\n data-testid=\"advanced-filter-save-trigger\"\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-8 rounded-full gap-1.5\"\n onClick={onSaveClick}\n >\n <Save className=\"size-3.5 text-muted-foreground\" />\n {t('ui.advancedFilter.saveFilter.button', 'Save filter')}\n </Button>\n ) : null}\n </div>\n {filters.length ? (\n <div className=\"flex flex-wrap gap-2\">\n {filters.map((filter) => (\n <div key={filter.id} className=\"inline-flex max-w-full items-center overflow-hidden rounded-full border border-border bg-background\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 min-w-0 rounded-r-none px-3 gap-1.5\"\n title={filter.name}\n onClick={() => onApply(filter)}\n >\n <Filter className=\"size-3.5 shrink-0 text-muted-foreground\" />\n <span className=\"truncate\">{filter.name}</span>\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 rounded-l-none px-0 text-muted-foreground hover:text-status-error-text\"\n aria-label={`${t('ui.advancedFilter.savedFilters.delete', 'Delete saved filter')}: ${filter.name}`}\n onClick={() => onDelete(filter.id)}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n </div>\n ))}\n </div>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t('ui.advancedFilter.savedFilters.empty', 'No saved filters yet')}\n </p>\n )}\n </div>\n )\n}\n\nexport function AdvancedFilterPanel(props: AdvancedFilterPanelProps) {\n const t = useT()\n const empty = props.value.root.children.length === 0\n const [pickerOpen, setPickerOpen] = React.useState(false)\n const [saveDialogOpen, setSaveDialogOpen] = React.useState(false)\n const [savedFilters, setSavedFilters] = React.useState<SavedAdvancedFilter[]>([])\n const addConditionRef = React.useRef<HTMLButtonElement>(null)\n const savedFilterStorageKey = React.useMemo(\n () => makeSavedFilterStorageKey(props.savedFilterStorageKey),\n [props.savedFilterStorageKey],\n )\n\n // Flush on close \u2014 the hook's useEffect on close also flushes; this is belt-and-suspenders.\n const wasOpenRef = React.useRef(props.open)\n React.useEffect(() => {\n if (wasOpenRef.current && !props.open && props.onFlush) props.onFlush()\n wasOpenRef.current = props.open\n }, [props.open, props.onFlush])\n\n const handlePresetApply = (tree: AdvancedFilterTree) => {\n props.onChange(tree)\n props.onOpenChange(false)\n }\n\n const { onFlush, onOpenChange, open } = props\n\n React.useEffect(() => {\n if (!open || pickerOpen || saveDialogOpen) return\n const closeOnEscape = (event: KeyboardEvent) => {\n if (event.key !== 'Escape') return\n // Do NOT gate on `event.defaultPrevented`. Radix keeps a just-closed\n // dropdown's dismissable layer mounted while its exit animation plays, and\n // that layer still calls preventDefault() on Escape. Picking a value in the\n // operator/value Select therefore leaves a layer that swallows the next\n // Escape, so the panel never closes and the active-filter chips never render\n // (regression seen on loaded standalone CI runners where the exit-animation\n // unmount is delayed). Instead, only defer Escape to an inner dropdown that\n // is *actually* open \u2014 a Radix Select trigger flips aria-expanded to \"false\"\n // synchronously on selection, so a closing-but-animating dropdown is no\n // longer treated as open here.\n if (typeof document !== 'undefined'\n && document.querySelector('[role=\"combobox\"][aria-expanded=\"true\"]')) {\n return\n }\n onOpenChange(false)\n onFlush?.()\n }\n window.addEventListener('keydown', closeOnEscape)\n return () => window.removeEventListener('keydown', closeOnEscape)\n }, [onFlush, onOpenChange, open, pickerOpen, saveDialogOpen])\n\n React.useEffect(() => {\n if (!savedFilterStorageKey) {\n setSavedFilters([])\n return\n }\n if (props.open) {\n setSavedFilters(readSavedFilters(savedFilterStorageKey))\n }\n }, [props.open, savedFilterStorageKey])\n\n const persistSavedFilters = React.useCallback((next: SavedAdvancedFilter[]) => {\n if (!savedFilterStorageKey) return\n setSavedFilters(next)\n writeSavedFilters(savedFilterStorageKey, next)\n }, [savedFilterStorageKey])\n\n const handleSaveFilter = React.useCallback(async (name: string) => {\n if (!savedFilterStorageKey) return\n const now = new Date().toISOString()\n const normalizedName = name.trim()\n const existing = savedFilters.find((item) => item.name.trim().toLowerCase() === normalizedName.toLowerCase())\n const record: SavedAdvancedFilter = {\n id: existing?.id ?? crypto.randomUUID(),\n name: normalizedName,\n tree: serializeTreeForPersist(props.value),\n createdAt: existing?.createdAt ?? now,\n updatedAt: now,\n }\n const next = [\n record,\n ...savedFilters.filter((item) => item.id !== record.id),\n ].slice(0, MAX_SAVED_FILTERS)\n persistSavedFilters(next)\n flash(t('ui.advancedFilter.saveFilter.success', 'Filter saved'), 'success')\n }, [persistSavedFilters, props.value, savedFilterStorageKey, savedFilters, t])\n\n const handleApplySavedFilter = React.useCallback((filter: SavedAdvancedFilter) => {\n const tree = deserializeTreeFromPersist(filter.tree)\n if (!tree) return\n props.onChange(tree)\n props.onOpenChange(false)\n }, [props.onChange, props.onOpenChange])\n\n const handleDeleteSavedFilter = React.useCallback((filterId: string) => {\n persistSavedFilters(savedFilters.filter((item) => item.id !== filterId))\n }, [persistSavedFilters, savedFilters])\n\n const shouldShowSavedFilters = Boolean(savedFilterStorageKey && (!empty || savedFilters.length > 0))\n const savedFiltersSection = shouldShowSavedFilters ? (\n <SavedFiltersSection\n filters={savedFilters}\n canSaveCurrent={!empty}\n onSaveClick={() => setSaveDialogOpen(true)}\n onApply={handleApplySavedFilter}\n onDelete={handleDeleteSavedFilter}\n t={t}\n />\n ) : null\n\n const emptyStateShortcuts = props.presets.length || savedFiltersSection ? (\n <div className=\"flex flex-col gap-4\">\n {props.presets.length ? <QuickFilters presets={props.presets} userId={props.userId} onApply={handlePresetApply} /> : null}\n {savedFiltersSection}\n </div>\n ) : null\n\n const builderErrors = React.useMemo(\n () => props.pendingErrors.map(e => ({ ruleId: e.ruleId, messageKey: e.messageKey, message: e.message })),\n [props.pendingErrors],\n )\n const ignoreAdvancedFilterPortalInteractions = React.useCallback((event: { target: EventTarget | null; preventDefault: () => void }) => {\n const target = event.target\n if (!(target instanceof Element)) return\n if (target.closest('[data-advanced-filter-portal]')) {\n event.preventDefault()\n }\n }, [])\n\n return (\n <Popover open={props.open} onOpenChange={props.onOpenChange}>\n {props.triggerRef ? (\n <PopoverAnchor virtualRef={props.triggerRef as React.RefObject<{ getBoundingClientRect(): DOMRect }>} />\n ) : null}\n <PopoverContent\n className=\"w-[min(calc(100vw-2rem),780px)] max-h-[min(calc(100vh-6rem),720px)] overflow-y-auto p-0 gap-0\"\n align=\"end\"\n sideOffset={8}\n data-testid=\"advanced-filter-panel\"\n onPointerDownOutside={ignoreAdvancedFilterPortalInteractions}\n onFocusOutside={ignoreAdvancedFilterPortalInteractions}\n onInteractOutside={ignoreAdvancedFilterPortalInteractions}\n >\n <VisuallyHidden>\n <h2>{t('ui.advancedFilter.dialog.title', 'Advanced filter')}</h2>\n </VisuallyHidden>\n <div className=\"flex flex-col\">\n {empty ? (\n <FilterEmptyState\n onAddCondition={() => setPickerOpen(true)}\n addConditionRef={addConditionRef}\n aiSlot={props.aiSlot}\n quickFilters={emptyStateShortcuts}\n />\n ) : (\n <>\n <AdvancedFilterBuilder\n fields={props.fields}\n value={props.value}\n onChange={props.onChange}\n onApply={props.onApply}\n onClear={props.onClear}\n pendingErrors={builderErrors}\n />\n {savedFiltersSection ? (\n <div className=\"border-t border-border p-3\">\n {savedFiltersSection}\n </div>\n ) : null}\n </>\n )}\n {props.pendingErrors.length > 0 ? (\n <div className=\"p-3 border-t border-border\" data-testid=\"filter-validation-banner\">\n <Alert variant=\"destructive\">\n <AlertDescription>\n {props.pendingErrors.length === 1\n ? t('ui.advancedFilter.banner.incomplete', '{count} filter is incomplete \u2014 filters won\\'t apply until all values are picked.', { count: 1 })\n : t('ui.advancedFilter.banner.incompletePlural', '{count} filters are incomplete \u2014 filters won\\'t apply until all values are picked.', { count: props.pendingErrors.length })}\n </AlertDescription>\n </Alert>\n </div>\n ) : null}\n </div>\n </PopoverContent>\n <FilterFieldPicker\n fields={props.fields}\n open={pickerOpen}\n onOpenChange={setPickerOpen}\n onSelect={(field) => {\n // Pass defaultOperator so the rule starts with a valid operator for the field's\n // type \u2014 otherwise emptyRule defaults to 'contains', which only matches text.\n const next = treeReducer(props.value, {\n type: 'addRule',\n groupId: props.value.root.id,\n defaultField: field.key,\n defaultOperator: getDefaultOperator(field.type),\n })\n props.onChange(next)\n }}\n triggerRef={addConditionRef}\n />\n <SaveFilterDialog\n open={saveDialogOpen}\n onOpenChange={setSaveDialogOpen}\n onSave={handleSaveFilter}\n t={t}\n />\n </Popover>\n )\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Popover, PopoverAnchor, PopoverContent } from '../../primitives/popover'\nimport { VisuallyHidden } from '@radix-ui/react-visually-hidden'\nimport { Alert, AlertDescription } from '../../primitives/alert'\nimport { Button } from '../../primitives/button'\nimport { Input } from '../../primitives/input'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../../primitives/dialog'\nimport { Filter, Save, Trash2 } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n deserializeTreeFromPersist,\n isPersistedFilterTree,\n serializeTreeForPersist,\n type AdvancedFilterTree,\n type PersistedFilterTree,\n} from '@open-mercato/shared/lib/query/advanced-filter-tree'\nimport type { FilterFieldDef } from '@open-mercato/shared/lib/query/advanced-filter'\nimport { getDefaultOperator } from '@open-mercato/shared/lib/query/advanced-filter'\nimport type { ValidationError } from './filterValidation'\nimport { AdvancedFilterBuilder } from './AdvancedFilterBuilder'\nimport { FilterEmptyState } from './FilterEmptyState'\nimport { QuickFilters, type FilterPreset } from './QuickFilters'\nimport { FilterFieldPicker } from './FilterFieldPicker'\nimport { treeReducer } from './treeReducer'\nimport { flash } from '../FlashMessages'\n\nexport type AdvancedFilterPanelProps = {\n fields: FilterFieldDef[]\n value: AdvancedFilterTree\n onChange: (t: AdvancedFilterTree) => void\n onApply: () => void\n onClear: () => void\n onFlush?: () => void\n pendingErrors: ValidationError[]\n userId: string\n presets: FilterPreset[]\n open: boolean\n onOpenChange: (open: boolean) => void\n triggerRef?: React.RefObject<HTMLElement | null>\n aiSlot?: React.ReactNode\n savedFilterStorageKey?: string\n}\n\ntype SavedAdvancedFilter = {\n id: string\n name: string\n tree: PersistedFilterTree\n createdAt: string\n updatedAt: string\n}\n\n/**\n * Versioned envelope for saved-filter records in localStorage. The `v` discriminator\n * lets future schema changes (rename fields, retention policy, sharing flags) migrate\n * existing records instead of silently dropping them. Bump `v` when adding a breaking\n * change and add a read-old migration branch in `readSavedFilters`.\n */\ntype SavedFiltersEnvelopeV1 = {\n v: 1\n filters: SavedAdvancedFilter[]\n}\n\nconst SAVED_FILTER_STORAGE_PREFIX = 'open-mercato:advanced-filters:'\nconst MAX_SAVED_FILTERS = 20\nconst CURRENT_SAVED_FILTERS_VERSION = 1 as const\n\nfunction makeSavedFilterStorageKey(key?: string): string | null {\n const trimmed = key?.trim()\n return trimmed ? `${SAVED_FILTER_STORAGE_PREFIX}${trimmed}` : null\n}\n\nfunction isSavedAdvancedFilter(value: unknown): value is SavedAdvancedFilter {\n if (!value || typeof value !== 'object') return false\n const record = value as Record<string, unknown>\n return (\n typeof record.id === 'string'\n && typeof record.name === 'string'\n && typeof record.createdAt === 'string'\n && typeof record.updatedAt === 'string'\n && isPersistedFilterTree(record.tree)\n )\n}\n\nfunction readSavedFilters(storageKey: string): SavedAdvancedFilter[] {\n if (typeof window === 'undefined') return []\n try {\n const raw = window.localStorage.getItem(storageKey)\n if (!raw) return []\n const parsed = JSON.parse(raw)\n // v1 envelope (current shape).\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n const envelope = parsed as { v?: unknown; filters?: unknown }\n if (envelope.v === 1 && Array.isArray(envelope.filters)) {\n return envelope.filters.filter(isSavedAdvancedFilter)\n }\n }\n // Pre-envelope shape (bare array) \u2014 migrate forward on next write.\n if (Array.isArray(parsed)) {\n return parsed.filter(isSavedAdvancedFilter)\n }\n return []\n } catch {\n return []\n }\n}\n\nfunction writeSavedFilters(storageKey: string, filters: SavedAdvancedFilter[]) {\n if (typeof window === 'undefined') return\n const envelope: SavedFiltersEnvelopeV1 = { v: CURRENT_SAVED_FILTERS_VERSION, filters }\n window.localStorage.setItem(storageKey, JSON.stringify(envelope))\n}\n\nfunction SaveFilterDialog({\n open,\n onOpenChange,\n onSave,\n t,\n}: {\n open: boolean\n onOpenChange: (open: boolean) => void\n onSave: (name: string) => Promise<void>\n t: (key: string, fallback: string) => string\n}) {\n const [name, setName] = React.useState('')\n const [saving, setSaving] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const trimmed = name.trim()\n\n React.useEffect(() => {\n if (!open) {\n setName('')\n setSaving(false)\n setError(null)\n }\n }, [open])\n\n const submit = React.useCallback(async () => {\n if (!trimmed || saving) return\n setSaving(true)\n setError(null)\n try {\n await onSave(trimmed)\n onOpenChange(false)\n } catch (err: unknown) {\n setError(err instanceof Error ? err.message : t('ui.advancedFilter.saveFilter.error', 'Failed to save filter'))\n } finally {\n setSaving(false)\n }\n }, [onSave, onOpenChange, saving, t, trimmed])\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n {/* `elevated` lifts this dialog above the popover (z-popover=45) \u2014 at default\n z-modal=40 the dialog is occluded by the popover that opens it. */}\n <DialogContent elevated className=\"sm:max-w-md\" data-advanced-filter-portal>\n <form\n className=\"contents\"\n onSubmit={(event) => {\n event.preventDefault()\n void submit()\n }}\n >\n <DialogHeader>\n <DialogTitle>{t('ui.advancedFilter.saveFilter.title', 'Save filter')}</DialogTitle>\n <DialogDescription>\n {t('ui.advancedFilter.saveFilter.description', 'Name this filter so you can reuse the current conditions later.')}\n </DialogDescription>\n </DialogHeader>\n <div className=\"space-y-2\">\n <Input\n value={name}\n onChange={(event) => setName(event.target.value)}\n placeholder={t('ui.advancedFilter.saveFilter.placeholder', 'Filter name')}\n aria-label={t('ui.advancedFilter.saveFilter.nameAriaLabel', 'Filter name')}\n autoFocus\n />\n {error ? <p className=\"text-sm text-status-error-text\">{error}</p> : null}\n </div>\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => onOpenChange(false)}>\n {t('common.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={!trimmed || saving}>\n {saving ? t('ui.advancedFilter.saveFilter.saving', 'Saving...') : t('ui.advancedFilter.saveFilter.submit', 'Save filter')}\n </Button>\n </DialogFooter>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction SavedFiltersSection({\n filters,\n canSaveCurrent,\n onSaveClick,\n onApply,\n onDelete,\n t,\n}: {\n filters: SavedAdvancedFilter[]\n canSaveCurrent: boolean\n onSaveClick: () => void\n onApply: (filter: SavedAdvancedFilter) => void\n onDelete: (filterId: string) => void\n t: (key: string, fallback: string) => string\n}) {\n if (!canSaveCurrent && filters.length === 0) return null\n return (\n <div className=\"flex flex-col gap-2\" data-testid=\"advanced-filter-saved-filters\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"text-overline font-medium uppercase tracking-widest text-muted-foreground\">\n {t('ui.advancedFilter.savedFilters.heading', 'Saved filters')}\n </div>\n {canSaveCurrent ? (\n <Button\n data-testid=\"advanced-filter-save-trigger\"\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-8 rounded-full gap-1.5\"\n onClick={onSaveClick}\n >\n <Save className=\"size-3.5 text-muted-foreground\" />\n {t('ui.advancedFilter.saveFilter.button', 'Save filter')}\n </Button>\n ) : null}\n </div>\n {filters.length ? (\n <div className=\"flex flex-wrap gap-2\">\n {filters.map((filter) => (\n <div key={filter.id} className=\"inline-flex max-w-full items-center overflow-hidden rounded-full border border-border bg-background\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 min-w-0 rounded-r-none px-3 gap-1.5\"\n title={filter.name}\n onClick={() => onApply(filter)}\n >\n <Filter className=\"size-3.5 shrink-0 text-muted-foreground\" />\n <span className=\"truncate\">{filter.name}</span>\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 rounded-l-none px-0 text-muted-foreground hover:text-status-error-text\"\n aria-label={`${t('ui.advancedFilter.savedFilters.delete', 'Delete saved filter')}: ${filter.name}`}\n onClick={() => onDelete(filter.id)}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n </div>\n ))}\n </div>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t('ui.advancedFilter.savedFilters.empty', 'No saved filters yet')}\n </p>\n )}\n </div>\n )\n}\n\nexport function AdvancedFilterPanel(props: AdvancedFilterPanelProps) {\n const t = useT()\n const empty = props.value.root.children.length === 0\n const [pickerOpen, setPickerOpen] = React.useState(false)\n const [saveDialogOpen, setSaveDialogOpen] = React.useState(false)\n const [savedFilters, setSavedFilters] = React.useState<SavedAdvancedFilter[]>([])\n const addConditionRef = React.useRef<HTMLButtonElement>(null)\n const savedFilterStorageKey = React.useMemo(\n () => makeSavedFilterStorageKey(props.savedFilterStorageKey),\n [props.savedFilterStorageKey],\n )\n\n // Flush on close \u2014 the hook's useEffect on close also flushes; this is belt-and-suspenders.\n const wasOpenRef = React.useRef(props.open)\n React.useEffect(() => {\n if (wasOpenRef.current && !props.open && props.onFlush) props.onFlush()\n wasOpenRef.current = props.open\n }, [props.open, props.onFlush])\n\n const handlePresetApply = (tree: AdvancedFilterTree) => {\n props.onChange(tree)\n props.onOpenChange(false)\n }\n\n const { onFlush, onOpenChange, open } = props\n\n React.useEffect(() => {\n if (!open || pickerOpen || saveDialogOpen) return\n const closeOnEscape = (event: KeyboardEvent) => {\n if (event.key !== 'Escape') return\n // Do NOT gate on `event.defaultPrevented`. Radix keeps a just-closed\n // dropdown's dismissable layer mounted while its exit animation plays, and\n // that layer still calls preventDefault() on Escape. Picking a value in the\n // operator/value Select therefore leaves a layer that swallows the next\n // Escape, so the panel never closes and the active-filter chips never render\n // (regression seen on loaded standalone CI runners where the exit-animation\n // unmount is delayed). Instead, only defer Escape to an inner dropdown that\n // is *actually* open \u2014 a Radix Select trigger flips aria-expanded to \"false\"\n // synchronously on selection, so a closing-but-animating dropdown is no\n // longer treated as open here.\n if (typeof document !== 'undefined'\n && document.querySelector('[role=\"combobox\"][aria-expanded=\"true\"]')) {\n return\n }\n onOpenChange(false)\n onFlush?.()\n }\n window.addEventListener('keydown', closeOnEscape)\n return () => window.removeEventListener('keydown', closeOnEscape)\n }, [onFlush, onOpenChange, open, pickerOpen, saveDialogOpen])\n\n React.useEffect(() => {\n if (!savedFilterStorageKey) {\n setSavedFilters([])\n return\n }\n if (props.open) {\n setSavedFilters(readSavedFilters(savedFilterStorageKey))\n }\n }, [props.open, savedFilterStorageKey])\n\n const persistSavedFilters = React.useCallback((next: SavedAdvancedFilter[]) => {\n if (!savedFilterStorageKey) return\n setSavedFilters(next)\n writeSavedFilters(savedFilterStorageKey, next)\n }, [savedFilterStorageKey])\n\n const handleSaveFilter = React.useCallback(async (name: string) => {\n if (!savedFilterStorageKey) return\n const now = new Date().toISOString()\n const normalizedName = name.trim()\n const existing = savedFilters.find((item) => item.name.trim().toLowerCase() === normalizedName.toLowerCase())\n const record: SavedAdvancedFilter = {\n id: existing?.id ?? crypto.randomUUID(),\n name: normalizedName,\n tree: serializeTreeForPersist(props.value),\n createdAt: existing?.createdAt ?? now,\n updatedAt: now,\n }\n const next = [\n record,\n ...savedFilters.filter((item) => item.id !== record.id),\n ].slice(0, MAX_SAVED_FILTERS)\n persistSavedFilters(next)\n flash(t('ui.advancedFilter.saveFilter.success', 'Filter saved'), 'success')\n }, [persistSavedFilters, props.value, savedFilterStorageKey, savedFilters, t])\n\n const handleApplySavedFilter = React.useCallback((filter: SavedAdvancedFilter) => {\n const tree = deserializeTreeFromPersist(filter.tree)\n if (!tree) return\n props.onChange(tree)\n props.onOpenChange(false)\n }, [props.onChange, props.onOpenChange])\n\n const handleDeleteSavedFilter = React.useCallback((filterId: string) => {\n persistSavedFilters(savedFilters.filter((item) => item.id !== filterId))\n }, [persistSavedFilters, savedFilters])\n\n const shouldShowSavedFilters = Boolean(savedFilterStorageKey && (!empty || savedFilters.length > 0))\n const savedFiltersSection = shouldShowSavedFilters ? (\n <SavedFiltersSection\n filters={savedFilters}\n canSaveCurrent={!empty}\n onSaveClick={() => setSaveDialogOpen(true)}\n onApply={handleApplySavedFilter}\n onDelete={handleDeleteSavedFilter}\n t={t}\n />\n ) : null\n\n const emptyStateShortcuts = props.presets.length || savedFiltersSection ? (\n <div className=\"flex flex-col gap-4\">\n {props.presets.length ? <QuickFilters presets={props.presets} userId={props.userId} onApply={handlePresetApply} /> : null}\n {savedFiltersSection}\n </div>\n ) : null\n\n const builderErrors = React.useMemo(\n () => props.pendingErrors.map(e => ({ ruleId: e.ruleId, messageKey: e.messageKey, message: e.message })),\n [props.pendingErrors],\n )\n const ignoreAdvancedFilterPortalInteractions = React.useCallback((event: { target: EventTarget | null; preventDefault: () => void }) => {\n const target = event.target\n if (!(target instanceof Element)) return\n if (target.closest('[data-advanced-filter-portal]')) {\n event.preventDefault()\n }\n }, [])\n\n return (\n <Popover open={props.open} onOpenChange={props.onOpenChange}>\n {props.triggerRef ? (\n <PopoverAnchor virtualRef={props.triggerRef as React.RefObject<{ getBoundingClientRect(): DOMRect }>} />\n ) : null}\n <PopoverContent\n className=\"w-[min(calc(100vw-2rem),780px)] max-h-[min(calc(100vh-6rem),720px)] overflow-y-auto p-0 gap-0\"\n align=\"end\"\n sideOffset={8}\n data-testid=\"advanced-filter-panel\"\nded onPointerDownOutside={ignoreAdvancedFilterPortalInteractions}\n onFocusOutside={ignoreAdvancedFilterPortalInteractions}\n onInteractOutside={ignoreAdvancedFilterPortalInteractions}\n >\n <VisuallyHidden>\n <h2>{t('ui.advancedFilter.dialog.title', 'Advanced filter')}</h2>\n </VisuallyHidden>\n <div className=\"flex flex-col\">\n {empty ? (\n <FilterEmptyState\n onAddCondition={() => setPickerOpen(true)}\n addConditionRef={addConditionRef}\n aiSlot={props.aiSlot}\n quickFilters={emptyStateShortcuts}\n />\n ) : (\n <>\n <AdvancedFilterBuilder\n fields={props.fields}\n value={props.value}\n onChange={props.onChange}\n onApply={props.onApply}\n onClear={props.onClear}\n pendingErrors={builderErrors}\n />\n {savedFiltersSection ? (\n <div className=\"border-t border-border p-3\">\n {savedFiltersSection}\n </div>\n ) : null}\n </>\n )}\n {props.pendingErrors.length > 0 ? (\n <div className=\"p-3 border-t border-border\" data-testid=\"filter-validation-banner\">\n <Alert variant=\"destructive\">\n <AlertDescription>\n {props.pendingErrors.length === 1\n ? t('ui.advancedFilter.banner.incomplete', '{count} filter is incomplete \u2014 filters won\\'t apply until all values are picked.', { count: 1 })\n : t('ui.advancedFilter.banner.incompletePlural', '{count} filters are incomplete \u2014 filters won\\'t apply until all values are picked.', { count: props.pendingErrors.length })}\n </AlertDescription>\n </Alert>\n </div>\n ) : null}\n </div>\n </PopoverContent>\n <FilterFieldPicker\n fields={props.fields}\n open={pickerOpen}\n onOpenChange={setPickerOpen}\n onSelect={(field) => {\n // Pass defaultOperator so the rule starts with a valid operator for the field's\n // type \u2014 otherwise emptyRule defaults to 'contains', which only matches text.\n const next = treeReducer(props.value, {\n type: 'addRule',\n groupId: props.value.root.id,\n defaultField: field.key,\n defaultOperator: getDefaultOperator(field.type),\n })\n props.onChange(next)\n }}\n triggerRef={addConditionRef}\n />\n <SaveFilterDialog\n open={saveDialogOpen}\n onOpenChange={setSaveDialogOpen}\n onSave={handleSaveFilter}\n t={t}\n />\n </Popover>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6416.1.888fffc007",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,13 +155,13 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
158
|
+
"@open-mercato/shared": "0.6.6-develop.6416.1.888fffc007",
|
|
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
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
164
|
+
"@open-mercato/shared": "0.6.6-develop.6416.1.888fffc007",
|
|
165
165
|
"@testing-library/dom": "^10.4.1",
|
|
166
166
|
"@testing-library/jest-dom": "^6.9.1",
|
|
167
167
|
"@testing-library/react": "^16.3.1",
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { screen, waitFor } from '@testing-library/react'
|
|
5
|
+
import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
|
|
6
|
+
import { AttachmentMetadataDialog, type AttachmentItem } from '../detail/AttachmentMetadataDialog'
|
|
7
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
8
|
+
|
|
9
|
+
jest.mock('@open-mercato/ui/backend/utils/apiCall', () => ({
|
|
10
|
+
apiCall: jest.fn(),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
jest.mock('@open-mercato/ui/backend/FlashMessages', () => ({
|
|
14
|
+
flash: jest.fn(),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
jest.mock('@open-mercato/ui/backend/inputs/TagsInput', () => ({
|
|
18
|
+
TagsInput: () => <div data-testid="tags-input" />,
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
jest.mock('@open-mercato/core/generated-shims/entities.ids.generated', () => ({
|
|
22
|
+
E: { attachments: { attachment: 'attachments.attachment' } },
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
jest.mock('@open-mercato/ui/backend/CrudForm', () => ({
|
|
26
|
+
CrudForm: ({ fields, initialValues }: { fields: any[]; initialValues?: Record<string, unknown> }) => (
|
|
27
|
+
<form data-testid="mock-crud-form">
|
|
28
|
+
{fields.map((field) => (
|
|
29
|
+
<div key={field.id} data-crud-field-id={field.id}>
|
|
30
|
+
{field.type === 'custom'
|
|
31
|
+
? field.component({
|
|
32
|
+
id: field.id,
|
|
33
|
+
value: initialValues?.[field.id],
|
|
34
|
+
disabled: false,
|
|
35
|
+
setValue: jest.fn(),
|
|
36
|
+
})
|
|
37
|
+
: null}
|
|
38
|
+
</div>
|
|
39
|
+
))}
|
|
40
|
+
</form>
|
|
41
|
+
),
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
jest.mock('@open-mercato/core/modules/attachments/components/AttachmentContentPreview', () => ({
|
|
45
|
+
AttachmentContentPreview: () => <div data-testid="attachment-content-preview" />,
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
describe('AttachmentMetadataDialog assignment layout', () => {
|
|
49
|
+
const longAssignment = {
|
|
50
|
+
type: 'production_operations:production_order_with_a_very_long_assignment_type',
|
|
51
|
+
id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479-f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
|
52
|
+
href:
|
|
53
|
+
'https://example.test/backend/production/operations/orders/f47ac10b-58cc-4372-a567-0e02b2c3d479/details/with/a/long/path',
|
|
54
|
+
label: 'Production order with a long label',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const secondAssignment = {
|
|
58
|
+
type: 'sales.invoice',
|
|
59
|
+
id: 'invoice-with-a-long-record-id-0001-0002-0003-0004',
|
|
60
|
+
href: 'https://example.test/backend/sales/invoices/invoice-with-a-long-record-id-0001-0002-0003-0004',
|
|
61
|
+
label: 'Invoice settlement attachment',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const assignments = [longAssignment, secondAssignment]
|
|
65
|
+
|
|
66
|
+
const item: AttachmentItem = {
|
|
67
|
+
id: 'attachment-1',
|
|
68
|
+
fileName: 'work-order-photo.png',
|
|
69
|
+
fileSize: 2048,
|
|
70
|
+
mimeType: 'application/pdf',
|
|
71
|
+
partitionCode: 'default',
|
|
72
|
+
partitionTitle: 'Default',
|
|
73
|
+
tags: [],
|
|
74
|
+
assignments,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
jest.resetAllMocks()
|
|
79
|
+
;(apiCall as jest.Mock).mockResolvedValue({
|
|
80
|
+
ok: true,
|
|
81
|
+
status: 200,
|
|
82
|
+
response: { status: 200 },
|
|
83
|
+
result: {
|
|
84
|
+
item: {
|
|
85
|
+
id: item.id,
|
|
86
|
+
tags: [],
|
|
87
|
+
assignments,
|
|
88
|
+
customFields: {},
|
|
89
|
+
content: null,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('renders each assignment as a distinct wide edit card', async () => {
|
|
96
|
+
renderWithProviders(
|
|
97
|
+
<AttachmentMetadataDialog
|
|
98
|
+
open
|
|
99
|
+
item={item}
|
|
100
|
+
availableTags={[]}
|
|
101
|
+
onOpenChange={jest.fn()}
|
|
102
|
+
onSave={jest.fn()}
|
|
103
|
+
/>,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const typeInput = await screen.findByDisplayValue(longAssignment.type)
|
|
107
|
+
const typeWrapper = typeInput.closest('[data-slot="input-wrapper"]')
|
|
108
|
+
expect(typeWrapper).not.toBeNull()
|
|
109
|
+
|
|
110
|
+
const fieldWrapper = typeWrapper?.parentElement
|
|
111
|
+
const fieldsGrid = fieldWrapper?.parentElement
|
|
112
|
+
const card = fieldsGrid?.parentElement
|
|
113
|
+
expect(card).not.toBeNull()
|
|
114
|
+
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
expect(screen.getAllByRole('button', { name: 'Remove' })).toHaveLength(2)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const cards = Array.from(document.querySelectorAll('[data-assignment-card]'))
|
|
120
|
+
expect(cards).toHaveLength(2)
|
|
121
|
+
expect(cards[0]).toHaveTextContent(longAssignment.label)
|
|
122
|
+
expect(cards[1]).toHaveTextContent(secondAssignment.label)
|
|
123
|
+
|
|
124
|
+
expect(card?.className).not.toContain('lg:grid-cols-[1.2fr_1.2fr_1.6fr_1fr_auto]')
|
|
125
|
+
expect(card?.className).not.toContain('lg:grid-cols-[minmax(0,1.2fr)_minmax(0,1.2fr)_minmax(0,1.6fr)_minmax(0,1fr)_auto]')
|
|
126
|
+
expect(fieldsGrid?.className).toContain('md:grid-cols-2')
|
|
127
|
+
expect(fieldsGrid?.className).toContain('min-w-0')
|
|
128
|
+
expect(fieldWrapper?.className).toContain('min-w-0')
|
|
129
|
+
|
|
130
|
+
const hrefInput = screen.getByDisplayValue(longAssignment.href)
|
|
131
|
+
const hrefFieldWrapper = hrefInput.closest('[data-slot="input-wrapper"]')?.parentElement
|
|
132
|
+
expect(hrefFieldWrapper?.className).toContain('md:col-span-2')
|
|
133
|
+
|
|
134
|
+
const labelInput = screen.getByDisplayValue(longAssignment.label)
|
|
135
|
+
const labelFieldWrapper = labelInput.closest('[data-slot="input-wrapper"]')?.parentElement
|
|
136
|
+
expect(labelFieldWrapper?.className).toContain('md:col-span-2')
|
|
137
|
+
|
|
138
|
+
const inputWrappers = Array.from(card?.querySelectorAll('[data-slot="input-wrapper"]') ?? [])
|
|
139
|
+
expect(inputWrappers).toHaveLength(4)
|
|
140
|
+
inputWrappers.forEach((wrapper) => {
|
|
141
|
+
expect((wrapper as HTMLElement).className).toContain('w-full')
|
|
142
|
+
expect((wrapper as HTMLElement).className).toContain('min-w-0')
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
})
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
import { Button } from '../../primitives/button'
|
|
6
|
+
import { IconButton } from '../../primitives/icon-button'
|
|
6
7
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
7
8
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
8
9
|
import { TagsInput } from '@open-mercato/ui/backend/inputs/TagsInput'
|
|
@@ -167,48 +168,81 @@ function AssignmentInputRow({
|
|
|
167
168
|
disabled?: boolean
|
|
168
169
|
onRemove: () => void
|
|
169
170
|
}) {
|
|
171
|
+
const trimmedLabel = value.label?.trim() ?? ''
|
|
172
|
+
const trimmedType = value.type?.trim() ?? ''
|
|
173
|
+
const trimmedId = value.id?.trim() ?? ''
|
|
174
|
+
const primaryLabel = trimmedLabel || trimmedType || trimmedId
|
|
175
|
+
const secondaryLabel = [
|
|
176
|
+
trimmedLabel ? trimmedType : '',
|
|
177
|
+
trimmedId && trimmedId !== primaryLabel ? trimmedId : '',
|
|
178
|
+
]
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
.join(' - ')
|
|
181
|
+
|
|
170
182
|
return (
|
|
171
|
-
<div className="
|
|
172
|
-
<div className="
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
<div data-assignment-card className="min-w-0 rounded-md border border-border/70 bg-background p-3">
|
|
184
|
+
<div className="mb-3 flex min-w-0 items-start justify-between gap-3">
|
|
185
|
+
<div className="min-w-0">
|
|
186
|
+
{primaryLabel ? (
|
|
187
|
+
<p className="truncate text-sm font-medium text-foreground">{primaryLabel}</p>
|
|
188
|
+
) : null}
|
|
189
|
+
{secondaryLabel ? (
|
|
190
|
+
<p className="truncate text-xs text-muted-foreground">{secondaryLabel}</p>
|
|
191
|
+
) : null}
|
|
192
|
+
</div>
|
|
193
|
+
<IconButton
|
|
194
|
+
type="button"
|
|
195
|
+
variant="ghost"
|
|
196
|
+
size="default"
|
|
197
|
+
aria-label={labels.remove}
|
|
198
|
+
onClick={onRemove}
|
|
187
199
|
disabled={disabled}
|
|
188
|
-
|
|
200
|
+
className="shrink-0"
|
|
201
|
+
>
|
|
202
|
+
<Trash2 className="size-4" />
|
|
203
|
+
</IconButton>
|
|
189
204
|
</div>
|
|
190
|
-
<div className="
|
|
191
|
-
<
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
205
|
+
<div className="grid min-w-0 grid-cols-1 gap-3 md:grid-cols-2">
|
|
206
|
+
<div className="min-w-0 space-y-1">
|
|
207
|
+
<label className="text-xs font-medium">{labels.type}</label>
|
|
208
|
+
<Input
|
|
209
|
+
className="w-full min-w-0"
|
|
210
|
+
value={value.type}
|
|
211
|
+
onChange={(event) => onChange({ ...value, type: event.target.value })}
|
|
212
|
+
placeholder="catalog.product"
|
|
213
|
+
disabled={disabled}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
<div className="min-w-0 space-y-1">
|
|
217
|
+
<label className="text-xs font-medium">{labels.id}</label>
|
|
218
|
+
<Input
|
|
219
|
+
className="w-full min-w-0"
|
|
220
|
+
value={value.id}
|
|
221
|
+
onChange={(event) => onChange({ ...value, id: event.target.value })}
|
|
222
|
+
placeholder="Record ID"
|
|
223
|
+
disabled={disabled}
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
<div className="min-w-0 space-y-1 md:col-span-2">
|
|
227
|
+
<label className="text-xs font-medium">{labels.href}</label>
|
|
228
|
+
<Input
|
|
229
|
+
className="w-full min-w-0"
|
|
230
|
+
value={value.href ?? ''}
|
|
231
|
+
onChange={(event) => onChange({ ...value, href: event.target.value })}
|
|
232
|
+
placeholder="https://"
|
|
233
|
+
disabled={disabled}
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
<div className="min-w-0 space-y-1 md:col-span-2">
|
|
237
|
+
<label className="text-xs font-medium">{labels.label}</label>
|
|
238
|
+
<Input
|
|
239
|
+
className="w-full min-w-0"
|
|
240
|
+
value={value.label ?? ''}
|
|
241
|
+
onChange={(event) => onChange({ ...value, label: event.target.value })}
|
|
242
|
+
placeholder="Optional label"
|
|
243
|
+
disabled={disabled}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
212
246
|
</div>
|
|
213
247
|
</div>
|
|
214
248
|
)
|
|
@@ -410,7 +410,7 @@ export function AdvancedFilterPanel(props: AdvancedFilterPanelProps) {
|
|
|
410
410
|
align="end"
|
|
411
411
|
sideOffset={8}
|
|
412
412
|
data-testid="advanced-filter-panel"
|
|
413
|
-
onPointerDownOutside={ignoreAdvancedFilterPortalInteractions}
|
|
413
|
+
ded onPointerDownOutside={ignoreAdvancedFilterPortalInteractions}
|
|
414
414
|
onFocusOutside={ignoreAdvancedFilterPortalInteractions}
|
|
415
415
|
onInteractOutside={ignoreAdvancedFilterPortalInteractions}
|
|
416
416
|
>
|