@open-mercato/ui 0.6.6 → 0.6.7-develop.6553.1.2ec7246e04

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.
Files changed (29) hide show
  1. package/AGENTS.md +1 -1
  2. package/dist/ai/ChatPaneTabs.js +2 -2
  3. package/dist/ai/ChatPaneTabs.js.map +2 -2
  4. package/dist/backend/AppShell.js +2 -2
  5. package/dist/backend/AppShell.js.map +1 -1
  6. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +37 -20
  7. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +2 -2
  8. package/dist/backend/detail/AttachmentMetadataDialog.js +9 -17
  9. package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
  10. package/dist/backend/filters/AdvancedFilterBuilder.js +1 -1
  11. package/dist/backend/filters/AdvancedFilterBuilder.js.map +1 -1
  12. package/dist/backend/messages/useMessageCompose.js +1 -0
  13. package/dist/backend/messages/useMessageCompose.js.map +2 -2
  14. package/dist/primitives/drawer.js +6 -3
  15. package/dist/primitives/drawer.js.map +2 -2
  16. package/dist/primitives/tabs.js +2 -1
  17. package/dist/primitives/tabs.js.map +2 -2
  18. package/package.json +5 -4
  19. package/src/ai/ChatPaneTabs.tsx +2 -2
  20. package/src/backend/AppShell.tsx +2 -2
  21. package/src/backend/__tests__/DataTable.extensions.test.tsx +26 -10
  22. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +52 -24
  23. package/src/backend/detail/AttachmentMetadataDialog.tsx +16 -23
  24. package/src/backend/filters/AdvancedFilterBuilder.tsx +1 -1
  25. package/src/backend/messages/__tests__/useMessageCompose.test.tsx +60 -0
  26. package/src/backend/messages/useMessageCompose.ts +4 -0
  27. package/src/primitives/__tests__/drawer.test.tsx +2 -1
  28. package/src/primitives/drawer.tsx +4 -3
  29. package/src/primitives/tabs.tsx +6 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/detail/AttachmentMetadataDialog.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { 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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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'\nimport { TagsInput } from '@open-mercato/ui/backend/inputs/TagsInput'\nimport { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'\nimport { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Copy, Download, Trash2 } from 'lucide-react'\nimport { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'\nimport { AttachmentContentPreview } from '@open-mercato/core/modules/attachments/components/AttachmentContentPreview'\nimport { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'\nimport { E } from '@open-mercato/core/generated-shims/entities.ids.generated'\n\nexport type AttachmentAssignment = {\n type: string\n id: string\n href?: string | null\n label?: string | null\n}\n\nexport type AttachmentItem = {\n id: string\n fileName: string\n fileSize: number\n mimeType?: string | null\n partitionCode?: string | null\n partitionTitle?: string | null\n url?: string | null\n createdAt?: string | null\n tags?: string[]\n assignments?: AttachmentAssignment[]\n thumbnailUrl?: string\n content?: string | null\n}\n\nexport type AssignmentDraft = {\n type: string\n id: string\n href?: string\n label?: string\n}\n\nexport type AttachmentMetadataSavePayload = {\n tags: string[]\n assignments: AssignmentDraft[]\n customFields?: Record<string, unknown>\n}\n\ntype AttachmentMetadataResponse = {\n item: {\n id: string\n fileName?: string\n fileSize?: number\n mimeType?: string | null\n partitionCode?: string\n partitionTitle?: string | null\n tags?: string[]\n content?: string | null\n assignments?: AttachmentAssignment[]\n customFields?: Record<string, unknown>\n }\n error?: string\n}\n\ntype AttachmentMetadataFormValues = {\n id: string\n tags?: string[]\n assignments?: AssignmentDraft[]\n} & Record<string, unknown>\n\ntype AssignmentEditorLabels = {\n title: string\n description: string\n type: string\n id: string\n href: string\n label: string\n add: string\n remove: string\n}\n\ntype AttachmentMetadataDialogProps = {\n open: boolean\n onOpenChange: (next: boolean) => void\n item: AttachmentItem | null\n availableTags: string[]\n onSave: (id: string, payload: AttachmentMetadataSavePayload) => Promise<void>\n}\n\nfunction formatFileSize(value: number): string {\n if (!Number.isFinite(value)) return '\\u2014'\n if (value <= 0) return '0 B'\n const units = ['B', 'KB', 'MB', 'GB', 'TB']\n let idx = 0\n let current = value\n while (current >= 1024 && idx < units.length - 1) {\n current /= 1024\n idx += 1\n }\n return `${current.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`\n}\n\nconst ENV_APP_URL = (process.env.NEXT_PUBLIC_APP_URL || '').replace(/\\/$/, '')\n\nfunction resolveAbsoluteUrl(path: string): string {\n if (!path) return path\n if (/^https?:\\/\\//i.test(path)) return path\n const base =\n ENV_APP_URL ||\n (typeof window !== 'undefined' && window.location?.origin ? window.location.origin : '')\n if (!base) return path\n const normalizedBase = base.replace(/\\/$/, '')\n return `${normalizedBase}${path.startsWith('/') ? path : `/${path}`}`\n}\n\nfunction normalizeCustomFieldSubmitValue(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.filter((entry) => entry !== undefined)\n }\n if (value === undefined) return null\n return value\n}\n\nfunction prepareAssignmentsForForm(assignments?: AttachmentAssignment[] | null): AssignmentDraft[] {\n return (assignments ?? []).map((assignment) => ({\n type: assignment.type,\n id: assignment.id,\n href: assignment.href ?? '',\n label: assignment.label ?? '',\n }))\n}\n\nfunction prefixCustomFieldValues(values?: Record<string, unknown> | null): Record<string, unknown> {\n if (!values) return {}\n const prefixed: Record<string, unknown> = {}\n Object.entries(values).forEach(([key, value]) => {\n if (!key) return\n if (key.startsWith('cf_')) {\n prefixed[key] = value\n } else if (key.startsWith('cf:')) {\n const normalized = key.slice(3)\n if (normalized) prefixed[`cf_${normalized}`] = value\n } else {\n prefixed[`cf_${key}`] = value\n }\n })\n return prefixed\n}\n\nfunction AssignmentInputRow({\n value,\n onChange,\n labels,\n disabled,\n onRemove,\n}: {\n value: AssignmentDraft\n onChange: (next: AssignmentDraft) => void\n labels: AssignmentEditorLabels\n disabled?: boolean\n onRemove: () => void\n}) {\n const trimmedLabel = value.label?.trim() ?? ''\n const trimmedType = value.type?.trim() ?? ''\n const trimmedId = value.id?.trim() ?? ''\n const primaryLabel = trimmedLabel || trimmedType || trimmedId\n const secondaryLabel = [\n trimmedLabel ? trimmedType : '',\n trimmedId && trimmedId !== primaryLabel ? trimmedId : '',\n ]\n .filter(Boolean)\n .join(' - ')\n\n return (\n <div data-assignment-card className=\"min-w-0 rounded-md border border-border/70 bg-background p-3\">\n <div className=\"mb-3 flex min-w-0 items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n {primaryLabel ? (\n <p className=\"truncate text-sm font-medium text-foreground\">{primaryLabel}</p>\n ) : null}\n {secondaryLabel ? (\n <p className=\"truncate text-xs text-muted-foreground\">{secondaryLabel}</p>\n ) : null}\n </div>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"default\"\n aria-label={labels.remove}\n onClick={onRemove}\n disabled={disabled}\n className=\"shrink-0\"\n >\n <Trash2 className=\"size-4\" />\n </IconButton>\n </div>\n <div className=\"grid min-w-0 grid-cols-1 gap-3 md:grid-cols-2\">\n <div className=\"min-w-0 space-y-1\">\n <label className=\"text-xs font-medium\">{labels.type}</label>\n <Input\n className=\"w-full min-w-0\"\n value={value.type}\n onChange={(event) => onChange({ ...value, type: event.target.value })}\n placeholder=\"catalog.product\"\n disabled={disabled}\n />\n </div>\n <div className=\"min-w-0 space-y-1\">\n <label className=\"text-xs font-medium\">{labels.id}</label>\n <Input\n className=\"w-full min-w-0\"\n value={value.id}\n onChange={(event) => onChange({ ...value, id: event.target.value })}\n placeholder=\"Record ID\"\n disabled={disabled}\n />\n </div>\n <div className=\"min-w-0 space-y-1 md:col-span-2\">\n <label className=\"text-xs font-medium\">{labels.href}</label>\n <Input\n className=\"w-full min-w-0\"\n value={value.href ?? ''}\n onChange={(event) => onChange({ ...value, href: event.target.value })}\n placeholder=\"https://\"\n disabled={disabled}\n />\n </div>\n <div className=\"min-w-0 space-y-1 md:col-span-2\">\n <label className=\"text-xs font-medium\">{labels.label}</label>\n <Input\n className=\"w-full min-w-0\"\n value={value.label ?? ''}\n onChange={(event) => onChange({ ...value, label: event.target.value })}\n placeholder=\"Optional label\"\n disabled={disabled}\n />\n </div>\n </div>\n </div>\n )\n}\n\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 <Tabs\n value={imageTab}\n onValueChange={(value) => setImageTab(value as 'preview' | 'resize')}\n variant=\"underline\"\n >\n <TabsList className=\"w-full flex-wrap px-3\">\n {(['preview', 'resize'] as const).map((tab) => (\n <TabsTrigger key={tab} value={tab}>\n {tab === 'preview'\n ? t('attachments.library.metadata.preview', 'Preview')\n : t('attachments.library.metadata.resizeTool.title', 'Generate resized URL')}\n </TabsTrigger>\n ))}\n </TabsList>\n </Tabs>\n <div className=\"space-y-3 p-3\">\n {imageTab === 'preview' ? (\n previewUrl ? (\n <img\n src={previewUrl}\n alt={item.fileName}\n className=\"h-48 w-full rounded-md bg-muted object-contain\"\n />\n ) : (\n <div className=\"text-sm text-muted-foreground\">\n {t('attachments.library.metadata.previewUnavailable', 'Preview unavailable.')}\n </div>\n )\n ) : (\n <div className=\"space-y-2\">\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium\" htmlFor=\"resize-width\">\n {t('attachments.library.metadata.resizeTool.width', 'Width (px)')}\n </label>\n <Input\n id=\"resize-width\"\n type=\"number\"\n min={0}\n value={sizeWidth}\n onChange={(event) => setSizeWidth(event.target.value)}\n disabled={loading}\n />\n </div>\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium\" htmlFor=\"resize-height\">\n {t('attachments.library.metadata.resizeTool.height', 'Height (px)')}\n </label>\n <Input\n id=\"resize-height\"\n type=\"number\"\n min={0}\n value={sizeHeight}\n onChange={(event) => setSizeHeight(event.target.value)}\n disabled={loading}\n />\n </div>\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"inline-flex items-center gap-2\"\n onClick={() => void handleCopyResizedUrl()}\n disabled={loading}\n >\n <Copy className=\"h-4 w-4\" />\n {t('attachments.library.metadata.resizeTool.copy', 'Copy URL')}\n </Button>\n </div>\n )}\n </div>\n </div>\n ) : null}\n {loadError ? (\n <div className=\"rounded border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive\">\n {loadError}\n </div>\n ) : null}\n <div className=\"rounded border border-border/70 bg-muted/30 px-3 py-2\">\n <div className=\"text-xs font-semibold text-muted-foreground\">\n {t('attachments.library.metadata.extractedTitle', 'Extracted text')}\n </div>\n <AttachmentContentPreview\n content={extractedContent}\n emptyLabel={t('attachments.library.metadata.noContent', 'No text extracted')}\n showMoreLabel={t('attachments.library.metadata.showMore', 'Show more')}\n showLessLabel={t('attachments.library.metadata.showLess', 'Show less')}\n />\n </div>\n <CrudForm<AttachmentMetadataFormValues>\n embedded\n schema={metadataSchema}\n entityId={E.attachments.attachment}\n fields={metadataFields}\n groups={metadataGroups}\n initialValues={initialValues ?? undefined}\n isLoading={!initialValues || loading}\n loadingMessage={loadMessage}\n submitLabel={t('attachments.library.metadata.save', 'Save')}\n extraActions={\n <Button type=\"button\" variant=\"outline\" onClick={() => onOpenChange(false)}>\n {t('attachments.library.metadata.cancel', 'Cancel')}\n </Button>\n }\n onSubmit={handleSubmit}\n />\n </div>\n ) : (\n <div className=\"py-8 text-center text-sm text-muted-foreground\">\n {t('attachments.library.metadata.noSelection', 'Select an attachment to edit.')}\n </div>\n )}\n </DialogContent>\n </Dialog>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -276,7 +276,7 @@ function AndOrToggle({
276
276
  const andLabel = t("ui.advancedFilter.combinator.and", "And");
277
277
  const orLabel = t("ui.advancedFilter.combinator.or", "Or");
278
278
  const baseBtn = "h-8 px-3 text-sm font-medium outline-none focus-visible:shadow-focus disabled:cursor-not-allowed";
279
- const selBtn = "bg-brand-violet text-white hover:bg-brand-violet/90";
279
+ const selBtn = "bg-brand-violet text-brand-violet-foreground hover:bg-brand-violet/90";
280
280
  const unselBtn = "bg-background text-foreground hover:bg-accent";
281
281
  return /* @__PURE__ */ jsxs(
282
282
  "div",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/filters/AdvancedFilterBuilder.tsx"],
4
- "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { createPortal } from 'react-dom'\nimport { Plus, Trash2, X, GripVertical, ChevronDown } from 'lucide-react'\nimport {\n DndContext,\n DragOverlay,\n PointerSensor,\n KeyboardSensor,\n useSensor,\n useSensors,\n closestCenter,\n type DragEndEvent,\n type DragStartEvent,\n} from '@dnd-kit/core'\nimport {\n SortableContext,\n sortableKeyboardCoordinates,\n useSortable,\n verticalListSortingStrategy,\n} from '@dnd-kit/sortable'\nimport { CSS } from '@dnd-kit/utilities'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Input } from '../../primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../../primitives/select'\nimport { Popover, PopoverContent, PopoverTrigger } from '../../primitives/popover'\nimport { Checkbox } from '../../primitives/checkbox'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type {\n FilterFieldDef,\n FilterFieldType,\n FilterOperator,\n} from '@open-mercato/shared/lib/query/advanced-filter'\nimport {\n OPERATORS_BY_FIELD_TYPE,\n getDefaultOperator,\n isValuelessOperator,\n} from '@open-mercato/shared/lib/query/advanced-filter'\nimport {\n type AdvancedFilterTree,\n type FilterRule,\n type FilterGroup,\n type FilterCombinator,\n TREE_LIMITS,\n} from '@open-mercato/shared/lib/query/advanced-filter-tree'\nimport { treeReducer, canAddRule, canAddGroup, type TreeAction } from './treeReducer'\nimport { FilterFieldPicker } from './FilterFieldPicker'\n\nexport type AdvancedFilterBuilderProps = {\n fields: FilterFieldDef[]\n value: AdvancedFilterTree\n onChange: (state: AdvancedFilterTree) => void\n onApply: () => void\n onClear: () => void\n pendingErrors?: Array<{ ruleId: string; messageKey: string; message: string }>\n}\n\nconst OPERATOR_LABELS: Record<FilterOperator, string> = {\n is: 'is', is_not: 'is not', contains: 'contains', does_not_contain: 'does not contain',\n starts_with: 'starts with', ends_with: 'ends with', is_empty: 'is empty', is_not_empty: 'is not empty',\n equals: 'equals', not_equals: 'not equals', greater_than: 'greater than', less_than: 'less than',\n greater_or_equal: 'greater or equal', less_or_equal: 'less or equal', between: 'between',\n is_before: 'is before', is_after: 'is after', is_any_of: 'is any of', is_none_of: 'is none of',\n is_true: 'is true', is_false: 'is false', has_any_of: 'has any of', has_all_of: 'has all of', has_none_of: 'has none of',\n}\n\nfunction getFieldType(fields: FilterFieldDef[], fieldKey: string): FilterFieldType {\n return fields.find((f) => f.key === fieldKey)?.type ?? 'text'\n}\n\n// Operator value shapes. Used to decide whether changing operator preserves the\n// current value: same shape preserves (`is` \u2192 `is not` keeps \"Active\"); different\n// shape resets (`is` \u2192 `between` clears, because value goes from scalar to pair).\ntype OperatorShape = 'valueless' | 'multi' | 'range' | 'single'\n\nconst MULTI_VALUE_OPS = new Set<FilterOperator>([\n 'is_any_of', 'is_none_of', 'has_any_of', 'has_all_of', 'has_none_of',\n])\n\nfunction getOperatorShape(op: FilterOperator): OperatorShape {\n if (isValuelessOperator(op)) return 'valueless'\n if (MULTI_VALUE_OPS.has(op)) return 'multi'\n if (op === 'between') return 'range'\n return 'single'\n}\n\ntype DragHandleProps = React.HTMLAttributes<HTMLButtonElement> & {\n ref?: React.Ref<HTMLButtonElement>\n}\n\ntype Translator = ReturnType<typeof useT>\n\nexport function AdvancedFilterBuilder({\n fields, value, onChange, onApply, onClear, pendingErrors,\n}: AdvancedFilterBuilderProps) {\n const t = useT()\n\n const dispatch = React.useCallback((action: TreeAction) => {\n onChange(treeReducer(value, action))\n }, [onChange, value])\n\n React.useEffect(() => {\n function onKey(e: KeyboardEvent) {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault()\n onApply()\n }\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [onApply])\n\n const errorByRuleId = React.useMemo(() => {\n const m = new Map<string, string>()\n for (const e of pendingErrors ?? []) m.set(e.ruleId, t(e.messageKey, e.message))\n return m\n }, [pendingErrors, t])\n\n const empty = value.root.children.length === 0\n const defaultField = fields.length > 0 ? fields[0].key : undefined\n\n // Per-group SortableContexts scope drag-and-drop to within-group ordering.\n // The DragOverlay below renders a stable preview that\n // matches the original row's footprint \u2014 without it, dnd-kit applies\n // `transform` to the source row in place, which the wrapping flex layout\n // distorted into a \"tall\" / morphed ghost.\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),\n useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n )\n const [activeDrag, setActiveDrag] = React.useState<{ node: FilterRule | FilterGroup; parent: FilterGroup } | null>(null)\n const findNode = React.useCallback((id: string) => locateInTree(value.root, id), [value])\n\n const handleDragStart = React.useCallback((e: DragStartEvent) => {\n const found = findNode(String(e.active.id))\n if (found) setActiveDrag(found)\n }, [findNode])\n\n const handleDragEnd = React.useCallback((e: DragEndEvent) => {\n setActiveDrag(null)\n const { active, over } = e\n if (!over || active.id === over.id) return\n const from = findNode(String(active.id))\n const to = findNode(String(over.id))\n if (!from || !to) return\n if (from.parent.id !== to.parent.id) return\n const fromIdx = from.parent.children.findIndex((c) => c.id === active.id)\n const toIdx = to.parent.children.findIndex((c) => c.id === over.id)\n if (fromIdx < 0 || toIdx < 0) return\n dispatch({ type: 'reorderChildren', groupId: from.parent.id, fromIdx, toIdx })\n }, [dispatch, findNode])\n\n const handleDragCancel = React.useCallback(() => setActiveDrag(null), [])\n\n // Wrapper has no `min-w-[640px]`: it forced horizontal scroll on a 375px viewport.\n // Rules wrap onto multiple lines on mobile; on desktop (`sm:max-w-[720px]`) they fit\n // on one line.\n return (\n <DndContext\n sensors={sensors}\n collisionDetection={closestCenter}\n onDragStart={handleDragStart}\n onDragEnd={handleDragEnd}\n onDragCancel={handleDragCancel}\n >\n <div className=\"flex flex-col gap-3 p-3\">\n {!empty ? (\n <GroupView\n group={value.root}\n level={1}\n fields={fields}\n tree={value}\n dispatch={dispatch}\n defaultField={defaultField}\n errorByRuleId={errorByRuleId}\n t={t}\n />\n ) : null}\n {!empty ? (\n <div className=\"flex items-center gap-3 pt-2 border-t border-border\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-auto px-0 py-0 text-base text-muted-foreground hover:bg-transparent hover:text-foreground\"\n onClick={onClear}\n >\n <X className=\"size-4\" />\n {t('ui.advancedFilter.clearAll', 'Clear all')}\n </Button>\n </div>\n ) : null}\n </div>\n {/* Portal DragOverlay to document.body. Radix DialogContent uses CSS `transform`\n for centering, which becomes the containing block for `position: fixed`\n descendants (dnd-kit's overlay) and shifts the ghost off-screen. Portaling\n restores viewport-relative coordinates. */}\n {typeof document !== 'undefined'\n ? createPortal(\n // dnd-kit's DragOverlay sets its own inline z-index, so a Tailwind\n // utility class would be overridden \u2014 read the DS token via CSS var.\n <DragOverlay dropAnimation={null} style={{ zIndex: 'var(--z-index-modal-elevated, 55)' }}>\n {activeDrag ? (\n <DragGhost node={activeDrag.node} parent={activeDrag.parent} fields={fields} t={t} />\n ) : null}\n </DragOverlay>,\n document.body,\n )\n : null}\n </DndContext>\n )\n}\n\n/** Find a node in the tree along with its parent group. Used by drag handlers. */\nfunction locateInTree(root: FilterGroup, id: string): { node: FilterRule | FilterGroup; parent: FilterGroup } | null {\n for (const child of root.children) {\n if (child.id === id) return { node: child, parent: root }\n if (child.type === 'group') {\n const found = locateInTree(child, id)\n if (found) return found\n }\n }\n return null\n}\n\n// Compact drag preview rendered inside DragOverlay. Cannot reuse RuleRow/GroupView \u2014\n// their popovers and form inputs conflict with the overlay.\nfunction DragGhost({\n node, parent, fields, t,\n}: {\n node: FilterRule | FilterGroup\n parent: FilterGroup\n fields: FilterFieldDef[]\n t: Translator\n}) {\n const isGroup = node.type === 'group'\n const labelParts: string[] = []\n if (isGroup) {\n const ruleCount = countRulesIn(node)\n labelParts.push(t('ui.advancedFilter.dragGhost.group', 'Group'))\n labelParts.push(`(${ruleCount})`)\n } else {\n const def = fields.find((f) => f.key === node.field)\n labelParts.push(def?.label ?? (node.field || t('ui.advancedFilter.dragGhost.unsetField', 'Unset field')))\n labelParts.push(OPERATOR_LABELS[node.operator] ?? node.operator)\n if (typeof node.value === 'string' && node.value) labelParts.push(`\"${node.value}\"`)\n }\n return (\n <div className=\"inline-flex items-center gap-2 rounded-md border border-border bg-popover px-3 py-1.5 text-sm shadow-md cursor-grabbing\">\n <GripVertical className=\"size-4 text-muted-foreground\" />\n <span className=\"text-muted-foreground\">{parent.combinator === 'and' ? t('ui.advancedFilter.connector.and', 'and') : t('ui.advancedFilter.connector.or', 'or')}</span>\n <span className=\"font-medium\">{labelParts.join(' ')}</span>\n </div>\n )\n}\n\nfunction countRulesIn(group: FilterGroup): number {\n let n = 0\n for (const c of group.children) {\n if (c.type === 'rule') n += 1\n else n += countRulesIn(c)\n }\n return n\n}\n\nfunction GroupView({\n group, level, fields, tree, dispatch, defaultField, errorByRuleId, t,\n}: {\n group: FilterGroup\n level: number\n fields: FilterFieldDef[]\n tree: AdvancedFilterTree\n dispatch: (a: TreeAction) => void\n defaultField?: string\n errorByRuleId: Map<string, string>\n t: Translator\n}) {\n const isRoot = level === 1\n // Sub-groups render as a card so the nesting boundary is visible; root group stays\n // flush with the popover so it doesn't double-up the dialog's own card chrome.\n const containerClass = isRoot\n ? 'space-y-3'\n : 'space-y-3 rounded-lg border border-border bg-muted/30 p-3'\n\n // Toolbar-first layout (Kendo React Filter pattern): one [And]/[Or] toggle per group\n // is the only combinator UI. Mixing AND with OR requires `Add subgroup`. Matches\n // `CompositeFilterDescriptor` semantics (one logic per group).\n return (\n <div className={containerClass}>\n <GroupToolbar\n group={group}\n level={level}\n tree={tree}\n dispatch={dispatch}\n defaultField={defaultField}\n fields={fields}\n isRoot={isRoot}\n t={t}\n />\n\n <SortableChildrenList\n group={group}\n level={level}\n fields={fields}\n tree={tree}\n dispatch={dispatch}\n defaultField={defaultField}\n errorByRuleId={errorByRuleId}\n t={t}\n />\n </div>\n )\n}\n\n// Kendo-style segmented `[And] [Or]` toggle. Clicking the unselected side flips\n// the group's combinator. Selected side is brand-violet filled so the affordance\n// is unambiguous.\nfunction AndOrToggle({\n combinator, onChange, t,\n}: {\n combinator: FilterCombinator\n onChange: (c: FilterCombinator) => void\n t: Translator\n}) {\n const andLabel = t('ui.advancedFilter.combinator.and', 'And')\n const orLabel = t('ui.advancedFilter.combinator.or', 'Or')\n const baseBtn = 'h-8 px-3 text-sm font-medium outline-none focus-visible:shadow-focus disabled:cursor-not-allowed'\n const selBtn = 'bg-brand-violet text-white hover:bg-brand-violet/90'\n const unselBtn = 'bg-background text-foreground hover:bg-accent'\n return (\n <div\n role=\"group\"\n aria-label={t('ui.advancedFilter.combinator.label', 'Group combinator')}\n className=\"inline-flex rounded-md border border-input overflow-hidden shrink-0\"\n data-testid=\"advanced-filter-combinator-toggle\"\n data-combinator={combinator}\n >\n <button\n type=\"button\"\n aria-pressed={combinator === 'and'}\n onClick={() => combinator !== 'and' && onChange('and')}\n className={`${baseBtn} ${combinator === 'and' ? selBtn : unselBtn}`}\n >\n {andLabel}\n </button>\n <button\n type=\"button\"\n aria-pressed={combinator === 'or'}\n onClick={() => combinator !== 'or' && onChange('or')}\n className={`${baseBtn} border-l border-input ${combinator === 'or' ? selBtn : unselBtn}`}\n >\n {orLabel}\n </button>\n </div>\n )\n}\n\nfunction SortableChildrenList({\n group, level, fields, tree, dispatch, defaultField, errorByRuleId, t,\n}: {\n group: FilterGroup\n level: number\n fields: FilterFieldDef[]\n tree: AdvancedFilterTree\n dispatch: (a: TreeAction) => void\n defaultField?: string\n errorByRuleId: Map<string, string>\n t: Translator\n}) {\n // DndContext + handlers live one level up so a drag can move children across\n // groups. Each group owns its own SortableContext to scope sort ordering.\n return (\n <SortableContext items={group.children.map((c) => c.id)} strategy={verticalListSortingStrategy}>\n {group.children.map((child, idx) => (\n <SortableRow\n key={child.id}\n child={child}\n idx={idx}\n parent={group}\n level={level}\n fields={fields}\n tree={tree}\n dispatch={dispatch}\n defaultField={defaultField}\n errorByRuleId={errorByRuleId}\n t={t}\n />\n ))}\n </SortableContext>\n )\n}\n\nfunction SortableRow({\n child, idx, parent, level, fields, tree, dispatch, defaultField, errorByRuleId, t,\n}: {\n child: FilterRule | FilterGroup\n idx: number\n parent: FilterGroup\n level: number\n fields: FilterFieldDef[]\n tree: AdvancedFilterTree\n dispatch: (a: TreeAction) => void\n defaultField?: string\n errorByRuleId: Map<string, string>\n t: Translator\n}) {\n const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({\n id: child.id,\n })\n // Hide the source while dragging (DragOverlay renders the snapshot); keep it in\n // layout via `visibility: hidden` so siblings don't jump. Without this, the flex\n // wrapper applies both `transform` and its own resizing to the source row.\n const style: React.CSSProperties = isDragging\n ? { visibility: 'hidden' as const }\n : { transform: CSS.Transform.toString(transform), transition }\n const dragHandleProps: DragHandleProps = { ...attributes, ...listeners }\n\n return (\n <div ref={setNodeRef} style={style} data-testid=\"filter-rule-row\" data-filter-node-id={child.id}>\n {child.type === 'rule' ? (\n <RuleRow\n rule={child}\n fields={fields}\n dispatch={dispatch}\n errorByRuleId={errorByRuleId}\n dragHandleProps={dragHandleProps}\n t={t}\n />\n ) : (\n <div className=\"flex items-start gap-2\">\n {/* Raw <button> required: dnd-kit spreads `attributes`+`listeners` onto the\n activator; <Button>'s own props/ref handling breaks the keyboard sensor. */}\n <button\n type=\"button\"\n {...dragHandleProps}\n data-testid=\"filter-drag-handle\"\n className=\"mt-1 text-muted-foreground/60 hover:text-muted-foreground cursor-grab outline-none focus-visible:shadow-focus rounded-sm\"\n aria-label={t('ui.advancedFilter.dragHandle', 'Drag to reorder')}\n >\n <GripVertical className=\"size-4\" />\n </button>\n <div className=\"flex-1\">\n <GroupView\n group={child}\n level={level + 1}\n fields={fields}\n tree={tree}\n dispatch={dispatch}\n defaultField={defaultField}\n errorByRuleId={errorByRuleId}\n t={t}\n />\n </div>\n </div>\n )}\n </div>\n )\n}\n\nfunction RuleRow({\n rule, fields, dispatch, errorByRuleId, dragHandleProps, t,\n}: {\n rule: FilterRule\n fields: FilterFieldDef[]\n dispatch: (a: TreeAction) => void\n errorByRuleId: Map<string, string>\n dragHandleProps: DragHandleProps\n t: Translator\n}) {\n const fieldType = getFieldType(fields, rule.field)\n const operators = OPERATORS_BY_FIELD_TYPE[fieldType] ?? OPERATORS_BY_FIELD_TYPE.text\n const valueless = isValuelessOperator(rule.operator)\n const errorMsg = errorByRuleId.get(rule.id)\n const fieldDef = fields.find((f) => f.key === rule.field)\n const [pickerOpen, setPickerOpen] = React.useState(false)\n const fieldBtnRef = React.useRef<HTMLButtonElement>(null)\n\n return (\n <div className=\"flex flex-col gap-1\" data-testid=\"filter-rule\" data-filter-rule-id={rule.id}>\n <div className=\"flex flex-wrap items-start gap-2\">\n {/* Raw <button> required for dnd-kit activator: see SortableRow drag-handle comment. */}\n <button\n type=\"button\"\n {...dragHandleProps}\n data-testid=\"filter-drag-handle\"\n className=\"mt-1 text-muted-foreground/60 hover:text-muted-foreground cursor-grab outline-none focus-visible:shadow-focus rounded-sm\"\n aria-label={t('ui.advancedFilter.dragHandle', 'Drag to reorder')}\n >\n <GripVertical className=\"size-4\" />\n </button>\n <Button\n ref={fieldBtnRef}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setPickerOpen(true)}\n className=\"min-w-[140px] justify-start\"\n >\n {fieldDef?.label ?? t('ui.advancedFilter.selectField', 'Select field')}\n </Button>\n <FilterFieldPicker\n fields={fields}\n open={pickerOpen}\n onOpenChange={setPickerOpen}\n onSelect={(f) => {\n const nextType = getFieldType(fields, f.key)\n dispatch({\n type: 'updateRule',\n ruleId: rule.id,\n updates: { field: f.key, operator: getDefaultOperator(nextType), value: '' },\n })\n }}\n triggerRef={fieldBtnRef as React.RefObject<HTMLElement | null>}\n />\n <Select\n value={rule.operator}\n onValueChange={(next) => {\n // Preserve the value when the operator shape doesn't change; reset\n // only when shape changes (e.g. `is` \u2192 `between` clears scalar to pair).\n const nextOp = next as FilterOperator\n const sameShape = getOperatorShape(rule.operator) === getOperatorShape(nextOp)\n const updates: Partial<Pick<FilterRule, 'operator' | 'value'>> = sameShape\n ? { operator: nextOp }\n : { operator: nextOp, value: getOperatorShape(nextOp) === 'multi' || getOperatorShape(nextOp) === 'range' ? [] : '' }\n dispatch({ type: 'updateRule', ruleId: rule.id, updates })\n }}\n >\n {/* `w-auto` overrides SelectTrigger's default `w-full` so it sits\n inline with the field/value inputs instead of breaking onto a new row. */}\n <SelectTrigger\n size=\"sm\"\n className=\"w-auto min-w-[140px]\"\n aria-label={t('ui.advancedFilter.selectOperator', 'Select operator')}\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent data-advanced-filter-portal=\"\">\n {operators.map((op) => (\n <SelectItem key={op} value={op}>\n {t(`ui.advancedFilter.operator.${op}`, OPERATOR_LABELS[op])}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {!valueless ? (\n <ValueInput\n rule={rule}\n fields={fields}\n fieldType={fieldType}\n dispatch={dispatch}\n hasError={!!errorMsg}\n t={t}\n />\n ) : null}\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => dispatch({ type: 'removeNode', nodeId: rule.id })}\n aria-label={t('ui.advancedFilter.removeCondition', 'Remove condition')}\n title={t('ui.advancedFilter.removeCondition', 'Remove condition')}\n >\n <Trash2 className=\"size-4 text-muted-foreground\" />\n </IconButton>\n </div>\n {errorMsg ? (\n <div className=\"ml-6 pl-12 text-xs text-status-error-text\">{errorMsg}</div>\n ) : null}\n </div>\n )\n}\n\nfunction ValueInput({\n rule, fields, fieldType, dispatch, hasError, t,\n}: {\n rule: FilterRule\n fields: FilterFieldDef[]\n fieldType: FilterFieldType\n dispatch: (a: TreeAction) => void\n hasError: boolean\n t: Translator\n}) {\n const fieldDef = fields.find((f) => f.key === rule.field)\n const value = rule.value\n const errorClass = hasError\n ? 'border-status-error-border ring-1 ring-status-error-border focus-visible:border-status-error-border focus-within:border-status-error-border'\n : ''\n\n if (fieldType === 'select' && fieldDef?.options) {\n // Multi-value operators (`is_any_of`, `is_none_of`, `has_any_of`, etc.)\n // need an array value with multiple selectable options. Radix `Select` is\n // single-select only, so the previous code silently overwrote each\n // selection (and validation kept failing because the value never became an\n // array). Render a checkbox-list popover when the operator is multi-shape.\n if (getOperatorShape(rule.operator) === 'multi') {\n return (\n <MultiValuePicker\n rule={rule}\n options={fieldDef.options}\n dispatch={dispatch}\n errorClass={errorClass}\n t={t}\n />\n )\n }\n return (\n <Select\n value={typeof value === 'string' && value.length ? value : undefined}\n onValueChange={(next) =>\n dispatch({ type: 'updateRule', ruleId: rule.id, updates: { value: next ?? '' } })\n }\n >\n <SelectTrigger\n size=\"sm\"\n className={`w-auto min-w-[160px] ${errorClass}`}\n aria-label={t('ui.advancedFilter.selectValue', 'Select value')}\n >\n <SelectValue placeholder={t('ui.advancedFilter.selectValuePlaceholder', 'Select...')} />\n </SelectTrigger>\n <SelectContent data-advanced-filter-portal=\"\">\n {fieldDef.options.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>\n {opt.tone ? (\n <span className=\"inline-flex items-center gap-2\">\n <span className={toneDotClass(opt.tone)} aria-hidden=\"true\" />\n {opt.label}\n </span>\n ) : (\n opt.label\n )}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n }\n\n // `Input` ships `w-full` by default \u2014 use `w-auto` so inputs don't stretch\n // inside the wrapping flex row. `between` needs a `[start, end]` pair, so it\n // gets its own component below.\n if (rule.operator === 'between' && (fieldType === 'number' || fieldType === 'date')) {\n return (\n <BetweenInput\n rule={rule}\n fieldType={fieldType}\n dispatch={dispatch}\n errorClass={errorClass}\n t={t}\n />\n )\n }\n\n if (fieldType === 'date') {\n return (\n <Input\n type=\"date\"\n size=\"sm\"\n className={`w-auto min-w-[160px] ${errorClass}`}\n value={typeof value === 'string' ? value : ''}\n onChange={(e) =>\n dispatch({ type: 'updateRule', ruleId: rule.id, updates: { value: e.target.value } })\n }\n aria-label={t('ui.advancedFilter.dateValue', 'Date value')}\n />\n )\n }\n\n if (fieldType === 'number') {\n return (\n <Input\n type=\"number\"\n size=\"sm\"\n className={`w-[120px] ${errorClass}`}\n value={typeof value === 'number' ? value : typeof value === 'string' ? value : ''}\n onChange={(e) =>\n dispatch({ type: 'updateRule', ruleId: rule.id, updates: { value: e.target.value } })\n }\n placeholder={t('ui.advancedFilter.numberPlaceholder', 'Value')}\n aria-label={t('ui.advancedFilter.numberValue', 'Number value')}\n />\n )\n }\n\n return (\n <Input\n type=\"text\"\n size=\"sm\"\n className={`w-auto min-w-[160px] ${errorClass}`}\n value={typeof value === 'string' ? value : ''}\n onChange={(e) =>\n dispatch({ type: 'updateRule', ruleId: rule.id, updates: { value: e.target.value } })\n }\n placeholder={t('ui.advancedFilter.textPlaceholder', 'Value...')}\n aria-label={t('ui.advancedFilter.textValue', 'Text value')}\n />\n )\n}\n\n/**\n * Two inputs (start / end) for the `between` operator on date and number\n * fields. Stores the value as `[start, end]`; either side can be empty while\n * the user types, but validation requires both to be set before apply.\n */\nfunction BetweenInput({\n rule, fieldType, dispatch, errorClass, t,\n}: {\n rule: FilterRule\n fieldType: FilterFieldType\n dispatch: (a: TreeAction) => void\n errorClass: string\n t: Translator\n}) {\n const pair = Array.isArray(rule.value) ? rule.value : []\n const start = typeof pair[0] === 'string' || typeof pair[0] === 'number' ? String(pair[0] ?? '') : ''\n const end = typeof pair[1] === 'string' || typeof pair[1] === 'number' ? String(pair[1] ?? '') : ''\n const update = (which: 'start' | 'end', next: string) => {\n const value = which === 'start' ? [next, end] : [start, next]\n dispatch({ type: 'updateRule', ruleId: rule.id, updates: { value } })\n }\n const inputType = fieldType === 'date' ? 'date' : 'number'\n const inputWidth = fieldType === 'date' ? 'w-auto min-w-[140px]' : 'w-[110px]'\n return (\n <div className=\"inline-flex items-center gap-1\">\n <Input\n type={inputType}\n size=\"sm\"\n className={`${inputWidth} ${errorClass}`}\n value={start}\n onChange={(e) => update('start', e.target.value)}\n placeholder={fieldType === 'number' ? t('ui.advancedFilter.numberPlaceholder', 'Value') : undefined}\n aria-label={t('ui.advancedFilter.betweenStart', 'Start')}\n />\n <span className=\"text-xs text-muted-foreground\">{t('ui.advancedFilter.and', 'and')}</span>\n <Input\n type={inputType}\n size=\"sm\"\n className={`${inputWidth} ${errorClass}`}\n value={end}\n onChange={(e) => update('end', e.target.value)}\n placeholder={fieldType === 'number' ? t('ui.advancedFilter.numberPlaceholder', 'Value') : undefined}\n aria-label={t('ui.advancedFilter.betweenEnd', 'End')}\n />\n </div>\n )\n}\n\n/**\n * Trigger + popover with a checkbox list. Used when a select-typed rule has a\n * multi-shape operator (`is_any_of`, `has_all_of`, etc.). The trigger displays\n * a \"{first} +N\" summary; the popover lets the user toggle each option.\n */\nfunction MultiValuePicker({\n rule, options, dispatch, errorClass, t,\n}: {\n rule: FilterRule\n options: NonNullable<FilterFieldDef['options']>\n dispatch: (a: TreeAction) => void\n errorClass: string\n t: Translator\n}) {\n const selected: string[] = Array.isArray(rule.value)\n ? rule.value.map((v) => String(v)).filter((v) => v.length > 0)\n : []\n const selectedLabel = (() => {\n if (selected.length === 0) return t('ui.advancedFilter.selectValuePlaceholder', 'Select...')\n const firstOpt = options.find((o) => o.value === selected[0])\n const head = firstOpt?.label ?? selected[0]\n const more = selected.length - 1\n return more > 0 ? `${head} +${more}` : head\n })()\n const toggle = (val: string) => {\n const next = selected.includes(val) ? selected.filter((v) => v !== val) : [...selected, val]\n dispatch({ type: 'updateRule', ruleId: rule.id, updates: { value: next } })\n }\n return (\n <Popover>\n <PopoverTrigger asChild>\n {/* Visually mirrors `SelectTrigger size=\"sm\"` (h-8, rounded-md, border)\n so the operator + value pair stays visually consistent. */}\n <button\n type=\"button\"\n className={`inline-flex h-8 items-center justify-between gap-2 rounded-md border border-input bg-background px-2.5 text-xs shadow-xs transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:shadow-focus focus-visible:border-foreground w-auto min-w-[160px] ${errorClass}`}\n aria-label={t('ui.advancedFilter.selectValue', 'Select value')}\n data-testid=\"multi-value-trigger\"\n >\n <span className={selected.length === 0 ? 'text-muted-foreground' : ''}>{selectedLabel}</span>\n <ChevronDown className=\"size-3 text-muted-foreground\" />\n </button>\n </PopoverTrigger>\n <PopoverContent className=\"w-64 p-1\" align=\"start\" data-advanced-filter-portal=\"\">\n <div className=\"max-h-64 overflow-y-auto\" role=\"listbox\" aria-multiselectable=\"true\">\n {options.map((opt) => {\n const isOn = selected.includes(opt.value)\n return (\n // Raw <button>: needs `role=\"option\"` + `aria-selected`; <Button> would override role.\n <button\n key={opt.value}\n type=\"button\"\n role=\"option\"\n aria-selected={isOn}\n onClick={() => toggle(opt.value)}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-accent focus-visible:bg-accent focus-visible:outline-none\"\n >\n <Checkbox checked={isOn} aria-hidden=\"true\" tabIndex={-1} className=\"pointer-events-none\" />\n {opt.tone ? (\n <span className=\"inline-flex items-center gap-2\">\n <span className={toneDotClass(opt.tone)} aria-hidden=\"true\" />\n {opt.label}\n </span>\n ) : (\n <span>{opt.label}</span>\n )}\n </button>\n )\n })}\n </div>\n </PopoverContent>\n </Popover>\n )\n}\n\nfunction toneDotClass(tone: NonNullable<FilterFieldDef['options']>[number]['tone']): string {\n // Solid filled dot using the saturated `*-icon` token (matches Tag's `dotColorMap`).\n const base = 'inline-block size-2 rounded-full shrink-0'\n switch (tone) {\n case 'success': return `${base} bg-status-success-icon`\n case 'error': return `${base} bg-status-error-icon`\n case 'warning': return `${base} bg-status-warning-icon`\n case 'info': return `${base} bg-status-info-icon`\n case 'neutral': return `${base} bg-status-neutral-icon`\n case 'pink': return `${base} bg-status-pink-icon`\n case 'brand': return `${base} bg-brand-violet`\n default: return `${base} bg-muted-foreground/40`\n }\n}\n\n// Single Kendo-style toolbar per group: combinator toggle, `Add condition`,\n// `Add subgroup` / `Add group`, plus a trailing delete for non-root groups.\nfunction GroupToolbar({\n group, level, tree, dispatch, defaultField, fields, isRoot, t,\n}: {\n group: FilterGroup\n level: number\n tree: AdvancedFilterTree\n dispatch: (a: TreeAction) => void\n defaultField?: string\n fields: FilterFieldDef[]\n isRoot: boolean\n t: Translator\n}) {\n const [pickerOpen, setPickerOpen] = React.useState(false)\n const addBtnRef = React.useRef<HTMLButtonElement>(null)\n const addRuleEnabled = canAddRule(tree, group.id)\n const addGroupEnabled = canAddGroup(tree, group.id)\n\n const widthCapHit = group.children.length >= TREE_LIMITS.maxChildrenPerGroup\n const depthCapHit = level >= TREE_LIMITS.maxGroupLevel\n\n const addRuleTitle = !addRuleEnabled\n ? widthCapHit\n ? t('ui.advancedFilter.limitWidthReached', 'Maximum {max} conditions per group', {\n max: TREE_LIMITS.maxChildrenPerGroup,\n })\n : t('ui.advancedFilter.limitTotalReached', 'Maximum {max} conditions reached', {\n max: TREE_LIMITS.maxTotalRules,\n })\n : undefined\n\n const addGroupTitle = !addGroupEnabled\n ? depthCapHit\n ? t('ui.advancedFilter.limitDepthReached', 'Maximum nesting depth reached')\n : widthCapHit\n ? t('ui.advancedFilter.limitWidthReached', 'Maximum {max} conditions per group', {\n max: TREE_LIMITS.maxChildrenPerGroup,\n })\n : t('ui.advancedFilter.limitTotalReached', 'Maximum {max} conditions reached', {\n max: TREE_LIMITS.maxTotalRules,\n })\n : undefined\n\n const addGroupLabel = level === 1\n ? t('ui.advancedFilter.addGroup', 'Add group')\n : t('ui.advancedFilter.addSubgroup', 'Add subgroup')\n\n // `text-brand-violet` overrides Button's default disabled token, so we have to\n // re-state muted disabled colors here \u2014 otherwise disabled CTAs still look clickable.\n const ctaClass = 'text-brand-violet hover:text-brand-violet/80 hover:bg-transparent px-0 disabled:text-muted-foreground/50 disabled:hover:text-muted-foreground/50'\n\n return (\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <AndOrToggle\n combinator={group.combinator}\n onChange={(c) => dispatch({ type: 'updateGroupCombinator', groupId: group.id, combinator: c })}\n t={t}\n />\n <Button\n ref={addBtnRef}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={!addRuleEnabled}\n title={addRuleTitle}\n onClick={() => setPickerOpen(true)}\n className={ctaClass}\n >\n <Plus className=\"size-4\" />\n {t('ui.advancedFilter.addCondition', 'Add condition')}\n </Button>\n <FilterFieldPicker\n fields={fields}\n open={pickerOpen}\n onOpenChange={setPickerOpen}\n onSelect={(f) =>\n dispatch({\n type: 'addRule',\n groupId: group.id,\n defaultField: f.key,\n defaultOperator: getDefaultOperator(f.type),\n })\n }\n triggerRef={addBtnRef as React.RefObject<HTMLElement | null>}\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={!addGroupEnabled}\n title={addGroupTitle}\n onClick={() =>\n dispatch({ type: 'addGroup', groupId: group.id, defaultField })\n }\n className={ctaClass}\n >\n <Plus className=\"size-4\" />\n {addGroupLabel}\n </Button>\n {!isRoot ? (\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => dispatch({ type: 'removeNode', nodeId: group.id })}\n aria-label={t('ui.advancedFilter.deleteGroup', 'Delete group')}\n title={t('ui.advancedFilter.deleteGroup', 'Delete group')}\n className=\"ml-auto\"\n >\n <X className=\"size-4 text-muted-foreground\" />\n </IconButton>\n ) : null}\n </div>\n {!addGroupEnabled && depthCapHit ? (\n <p className=\"text-xs text-muted-foreground\" data-testid=\"filter-depth-limit-hint\">\n {t('ui.advancedFilter.limitDepthHint', 'You\\'ve reached the maximum nesting depth ({max} levels). Switch this group\\'s combinator with the toggle above instead of nesting deeper.', {\n max: TREE_LIMITS.maxGroupLevel,\n })}\n </p>\n ) : null}\n </div>\n )\n}\n"],
4
+ "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { createPortal } from 'react-dom'\nimport { Plus, Trash2, X, GripVertical, ChevronDown } from 'lucide-react'\nimport {\n DndContext,\n DragOverlay,\n PointerSensor,\n KeyboardSensor,\n useSensor,\n useSensors,\n closestCenter,\n type DragEndEvent,\n type DragStartEvent,\n} from '@dnd-kit/core'\nimport {\n SortableContext,\n sortableKeyboardCoordinates,\n useSortable,\n verticalListSortingStrategy,\n} from '@dnd-kit/sortable'\nimport { CSS } from '@dnd-kit/utilities'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Input } from '../../primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../../primitives/select'\nimport { Popover, PopoverContent, PopoverTrigger } from '../../primitives/popover'\nimport { Checkbox } from '../../primitives/checkbox'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type {\n FilterFieldDef,\n FilterFieldType,\n FilterOperator,\n} from '@open-mercato/shared/lib/query/advanced-filter'\nimport {\n OPERATORS_BY_FIELD_TYPE,\n getDefaultOperator,\n isValuelessOperator,\n} from '@open-mercato/shared/lib/query/advanced-filter'\nimport {\n type AdvancedFilterTree,\n type FilterRule,\n type FilterGroup,\n type FilterCombinator,\n TREE_LIMITS,\n} from '@open-mercato/shared/lib/query/advanced-filter-tree'\nimport { treeReducer, canAddRule, canAddGroup, type TreeAction } from './treeReducer'\nimport { FilterFieldPicker } from './FilterFieldPicker'\n\nexport type AdvancedFilterBuilderProps = {\n fields: FilterFieldDef[]\n value: AdvancedFilterTree\n onChange: (state: AdvancedFilterTree) => void\n onApply: () => void\n onClear: () => void\n pendingErrors?: Array<{ ruleId: string; messageKey: string; message: string }>\n}\n\nconst OPERATOR_LABELS: Record<FilterOperator, string> = {\n is: 'is', is_not: 'is not', contains: 'contains', does_not_contain: 'does not contain',\n starts_with: 'starts with', ends_with: 'ends with', is_empty: 'is empty', is_not_empty: 'is not empty',\n equals: 'equals', not_equals: 'not equals', greater_than: 'greater than', less_than: 'less than',\n greater_or_equal: 'greater or equal', less_or_equal: 'less or equal', between: 'between',\n is_before: 'is before', is_after: 'is after', is_any_of: 'is any of', is_none_of: 'is none of',\n is_true: 'is true', is_false: 'is false', has_any_of: 'has any of', has_all_of: 'has all of', has_none_of: 'has none of',\n}\n\nfunction getFieldType(fields: FilterFieldDef[], fieldKey: string): FilterFieldType {\n return fields.find((f) => f.key === fieldKey)?.type ?? 'text'\n}\n\n// Operator value shapes. Used to decide whether changing operator preserves the\n// current value: same shape preserves (`is` \u2192 `is not` keeps \"Active\"); different\n// shape resets (`is` \u2192 `between` clears, because value goes from scalar to pair).\ntype OperatorShape = 'valueless' | 'multi' | 'range' | 'single'\n\nconst MULTI_VALUE_OPS = new Set<FilterOperator>([\n 'is_any_of', 'is_none_of', 'has_any_of', 'has_all_of', 'has_none_of',\n])\n\nfunction getOperatorShape(op: FilterOperator): OperatorShape {\n if (isValuelessOperator(op)) return 'valueless'\n if (MULTI_VALUE_OPS.has(op)) return 'multi'\n if (op === 'between') return 'range'\n return 'single'\n}\n\ntype DragHandleProps = React.HTMLAttributes<HTMLButtonElement> & {\n ref?: React.Ref<HTMLButtonElement>\n}\n\ntype Translator = ReturnType<typeof useT>\n\nexport function AdvancedFilterBuilder({\n fields, value, onChange, onApply, onClear, pendingErrors,\n}: AdvancedFilterBuilderProps) {\n const t = useT()\n\n const dispatch = React.useCallback((action: TreeAction) => {\n onChange(treeReducer(value, action))\n }, [onChange, value])\n\n React.useEffect(() => {\n function onKey(e: KeyboardEvent) {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault()\n onApply()\n }\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [onApply])\n\n const errorByRuleId = React.useMemo(() => {\n const m = new Map<string, string>()\n for (const e of pendingErrors ?? []) m.set(e.ruleId, t(e.messageKey, e.message))\n return m\n }, [pendingErrors, t])\n\n const empty = value.root.children.length === 0\n const defaultField = fields.length > 0 ? fields[0].key : undefined\n\n // Per-group SortableContexts scope drag-and-drop to within-group ordering.\n // The DragOverlay below renders a stable preview that\n // matches the original row's footprint \u2014 without it, dnd-kit applies\n // `transform` to the source row in place, which the wrapping flex layout\n // distorted into a \"tall\" / morphed ghost.\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),\n useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n )\n const [activeDrag, setActiveDrag] = React.useState<{ node: FilterRule | FilterGroup; parent: FilterGroup } | null>(null)\n const findNode = React.useCallback((id: string) => locateInTree(value.root, id), [value])\n\n const handleDragStart = React.useCallback((e: DragStartEvent) => {\n const found = findNode(String(e.active.id))\n if (found) setActiveDrag(found)\n }, [findNode])\n\n const handleDragEnd = React.useCallback((e: DragEndEvent) => {\n setActiveDrag(null)\n const { active, over } = e\n if (!over || active.id === over.id) return\n const from = findNode(String(active.id))\n const to = findNode(String(over.id))\n if (!from || !to) return\n if (from.parent.id !== to.parent.id) return\n const fromIdx = from.parent.children.findIndex((c) => c.id === active.id)\n const toIdx = to.parent.children.findIndex((c) => c.id === over.id)\n if (fromIdx < 0 || toIdx < 0) return\n dispatch({ type: 'reorderChildren', groupId: from.parent.id, fromIdx, toIdx })\n }, [dispatch, findNode])\n\n const handleDragCancel = React.useCallback(() => setActiveDrag(null), [])\n\n // Wrapper has no `min-w-[640px]`: it forced horizontal scroll on a 375px viewport.\n // Rules wrap onto multiple lines on mobile; on desktop (`sm:max-w-[720px]`) they fit\n // on one line.\n return (\n <DndContext\n sensors={sensors}\n collisionDetection={closestCenter}\n onDragStart={handleDragStart}\n onDragEnd={handleDragEnd}\n onDragCancel={handleDragCancel}\n >\n <div className=\"flex flex-col gap-3 p-3\">\n {!empty ? (\n <GroupView\n group={value.root}\n level={1}\n fields={fields}\n tree={value}\n dispatch={dispatch}\n defaultField={defaultField}\n errorByRuleId={errorByRuleId}\n t={t}\n />\n ) : null}\n {!empty ? (\n <div className=\"flex items-center gap-3 pt-2 border-t border-border\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-auto px-0 py-0 text-base text-muted-foreground hover:bg-transparent hover:text-foreground\"\n onClick={onClear}\n >\n <X className=\"size-4\" />\n {t('ui.advancedFilter.clearAll', 'Clear all')}\n </Button>\n </div>\n ) : null}\n </div>\n {/* Portal DragOverlay to document.body. Radix DialogContent uses CSS `transform`\n for centering, which becomes the containing block for `position: fixed`\n descendants (dnd-kit's overlay) and shifts the ghost off-screen. Portaling\n restores viewport-relative coordinates. */}\n {typeof document !== 'undefined'\n ? createPortal(\n // dnd-kit's DragOverlay sets its own inline z-index, so a Tailwind\n // utility class would be overridden \u2014 read the DS token via CSS var.\n <DragOverlay dropAnimation={null} style={{ zIndex: 'var(--z-index-modal-elevated, 55)' }}>\n {activeDrag ? (\n <DragGhost node={activeDrag.node} parent={activeDrag.parent} fields={fields} t={t} />\n ) : null}\n </DragOverlay>,\n document.body,\n )\n : null}\n </DndContext>\n )\n}\n\n/** Find a node in the tree along with its parent group. Used by drag handlers. */\nfunction locateInTree(root: FilterGroup, id: string): { node: FilterRule | FilterGroup; parent: FilterGroup } | null {\n for (const child of root.children) {\n if (child.id === id) return { node: child, parent: root }\n if (child.type === 'group') {\n const found = locateInTree(child, id)\n if (found) return found\n }\n }\n return null\n}\n\n// Compact drag preview rendered inside DragOverlay. Cannot reuse RuleRow/GroupView \u2014\n// their popovers and form inputs conflict with the overlay.\nfunction DragGhost({\n node, parent, fields, t,\n}: {\n node: FilterRule | FilterGroup\n parent: FilterGroup\n fields: FilterFieldDef[]\n t: Translator\n}) {\n const isGroup = node.type === 'group'\n const labelParts: string[] = []\n if (isGroup) {\n const ruleCount = countRulesIn(node)\n labelParts.push(t('ui.advancedFilter.dragGhost.group', 'Group'))\n labelParts.push(`(${ruleCount})`)\n } else {\n const def = fields.find((f) => f.key === node.field)\n labelParts.push(def?.label ?? (node.field || t('ui.advancedFilter.dragGhost.unsetField', 'Unset field')))\n labelParts.push(OPERATOR_LABELS[node.operator] ?? node.operator)\n if (typeof node.value === 'string' && node.value) labelParts.push(`\"${node.value}\"`)\n }\n return (\n <div className=\"inline-flex items-center gap-2 rounded-md border border-border bg-popover px-3 py-1.5 text-sm shadow-md cursor-grabbing\">\n <GripVertical className=\"size-4 text-muted-foreground\" />\n <span className=\"text-muted-foreground\">{parent.combinator === 'and' ? t('ui.advancedFilter.connector.and', 'and') : t('ui.advancedFilter.connector.or', 'or')}</span>\n <span className=\"font-medium\">{labelParts.join(' ')}</span>\n </div>\n )\n}\n\nfunction countRulesIn(group: FilterGroup): number {\n let n = 0\n for (const c of group.children) {\n if (c.type === 'rule') n += 1\n else n += countRulesIn(c)\n }\n return n\n}\n\nfunction GroupView({\n group, level, fields, tree, dispatch, defaultField, errorByRuleId, t,\n}: {\n group: FilterGroup\n level: number\n fields: FilterFieldDef[]\n tree: AdvancedFilterTree\n dispatch: (a: TreeAction) => void\n defaultField?: string\n errorByRuleId: Map<string, string>\n t: Translator\n}) {\n const isRoot = level === 1\n // Sub-groups render as a card so the nesting boundary is visible; root group stays\n // flush with the popover so it doesn't double-up the dialog's own card chrome.\n const containerClass = isRoot\n ? 'space-y-3'\n : 'space-y-3 rounded-lg border border-border bg-muted/30 p-3'\n\n // Toolbar-first layout (Kendo React Filter pattern): one [And]/[Or] toggle per group\n // is the only combinator UI. Mixing AND with OR requires `Add subgroup`. Matches\n // `CompositeFilterDescriptor` semantics (one logic per group).\n return (\n <div className={containerClass}>\n <GroupToolbar\n group={group}\n level={level}\n tree={tree}\n dispatch={dispatch}\n defaultField={defaultField}\n fields={fields}\n isRoot={isRoot}\n t={t}\n />\n\n <SortableChildrenList\n group={group}\n level={level}\n fields={fields}\n tree={tree}\n dispatch={dispatch}\n defaultField={defaultField}\n errorByRuleId={errorByRuleId}\n t={t}\n />\n </div>\n )\n}\n\n// Kendo-style segmented `[And] [Or]` toggle. Clicking the unselected side flips\n// the group's combinator. Selected side is brand-violet filled so the affordance\n// is unambiguous.\nfunction AndOrToggle({\n combinator, onChange, t,\n}: {\n combinator: FilterCombinator\n onChange: (c: FilterCombinator) => void\n t: Translator\n}) {\n const andLabel = t('ui.advancedFilter.combinator.and', 'And')\n const orLabel = t('ui.advancedFilter.combinator.or', 'Or')\n const baseBtn = 'h-8 px-3 text-sm font-medium outline-none focus-visible:shadow-focus disabled:cursor-not-allowed'\n const selBtn = 'bg-brand-violet text-brand-violet-foreground hover:bg-brand-violet/90'\n const unselBtn = 'bg-background text-foreground hover:bg-accent'\n return (\n <div\n role=\"group\"\n aria-label={t('ui.advancedFilter.combinator.label', 'Group combinator')}\n className=\"inline-flex rounded-md border border-input overflow-hidden shrink-0\"\n data-testid=\"advanced-filter-combinator-toggle\"\n data-combinator={combinator}\n >\n <button\n type=\"button\"\n aria-pressed={combinator === 'and'}\n onClick={() => combinator !== 'and' && onChange('and')}\n className={`${baseBtn} ${combinator === 'and' ? selBtn : unselBtn}`}\n >\n {andLabel}\n </button>\n <button\n type=\"button\"\n aria-pressed={combinator === 'or'}\n onClick={() => combinator !== 'or' && onChange('or')}\n className={`${baseBtn} border-l border-input ${combinator === 'or' ? selBtn : unselBtn}`}\n >\n {orLabel}\n </button>\n </div>\n )\n}\n\nfunction SortableChildrenList({\n group, level, fields, tree, dispatch, defaultField, errorByRuleId, t,\n}: {\n group: FilterGroup\n level: number\n fields: FilterFieldDef[]\n tree: AdvancedFilterTree\n dispatch: (a: TreeAction) => void\n defaultField?: string\n errorByRuleId: Map<string, string>\n t: Translator\n}) {\n // DndContext + handlers live one level up so a drag can move children across\n // groups. Each group owns its own SortableContext to scope sort ordering.\n return (\n <SortableContext items={group.children.map((c) => c.id)} strategy={verticalListSortingStrategy}>\n {group.children.map((child, idx) => (\n <SortableRow\n key={child.id}\n child={child}\n idx={idx}\n parent={group}\n level={level}\n fields={fields}\n tree={tree}\n dispatch={dispatch}\n defaultField={defaultField}\n errorByRuleId={errorByRuleId}\n t={t}\n />\n ))}\n </SortableContext>\n )\n}\n\nfunction SortableRow({\n child, idx, parent, level, fields, tree, dispatch, defaultField, errorByRuleId, t,\n}: {\n child: FilterRule | FilterGroup\n idx: number\n parent: FilterGroup\n level: number\n fields: FilterFieldDef[]\n tree: AdvancedFilterTree\n dispatch: (a: TreeAction) => void\n defaultField?: string\n errorByRuleId: Map<string, string>\n t: Translator\n}) {\n const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({\n id: child.id,\n })\n // Hide the source while dragging (DragOverlay renders the snapshot); keep it in\n // layout via `visibility: hidden` so siblings don't jump. Without this, the flex\n // wrapper applies both `transform` and its own resizing to the source row.\n const style: React.CSSProperties = isDragging\n ? { visibility: 'hidden' as const }\n : { transform: CSS.Transform.toString(transform), transition }\n const dragHandleProps: DragHandleProps = { ...attributes, ...listeners }\n\n return (\n <div ref={setNodeRef} style={style} data-testid=\"filter-rule-row\" data-filter-node-id={child.id}>\n {child.type === 'rule' ? (\n <RuleRow\n rule={child}\n fields={fields}\n dispatch={dispatch}\n errorByRuleId={errorByRuleId}\n dragHandleProps={dragHandleProps}\n t={t}\n />\n ) : (\n <div className=\"flex items-start gap-2\">\n {/* Raw <button> required: dnd-kit spreads `attributes`+`listeners` onto the\n activator; <Button>'s own props/ref handling breaks the keyboard sensor. */}\n <button\n type=\"button\"\n {...dragHandleProps}\n data-testid=\"filter-drag-handle\"\n className=\"mt-1 text-muted-foreground/60 hover:text-muted-foreground cursor-grab outline-none focus-visible:shadow-focus rounded-sm\"\n aria-label={t('ui.advancedFilter.dragHandle', 'Drag to reorder')}\n >\n <GripVertical className=\"size-4\" />\n </button>\n <div className=\"flex-1\">\n <GroupView\n group={child}\n level={level + 1}\n fields={fields}\n tree={tree}\n dispatch={dispatch}\n defaultField={defaultField}\n errorByRuleId={errorByRuleId}\n t={t}\n />\n </div>\n </div>\n )}\n </div>\n )\n}\n\nfunction RuleRow({\n rule, fields, dispatch, errorByRuleId, dragHandleProps, t,\n}: {\n rule: FilterRule\n fields: FilterFieldDef[]\n dispatch: (a: TreeAction) => void\n errorByRuleId: Map<string, string>\n dragHandleProps: DragHandleProps\n t: Translator\n}) {\n const fieldType = getFieldType(fields, rule.field)\n const operators = OPERATORS_BY_FIELD_TYPE[fieldType] ?? OPERATORS_BY_FIELD_TYPE.text\n const valueless = isValuelessOperator(rule.operator)\n const errorMsg = errorByRuleId.get(rule.id)\n const fieldDef = fields.find((f) => f.key === rule.field)\n const [pickerOpen, setPickerOpen] = React.useState(false)\n const fieldBtnRef = React.useRef<HTMLButtonElement>(null)\n\n return (\n <div className=\"flex flex-col gap-1\" data-testid=\"filter-rule\" data-filter-rule-id={rule.id}>\n <div className=\"flex flex-wrap items-start gap-2\">\n {/* Raw <button> required for dnd-kit activator: see SortableRow drag-handle comment. */}\n <button\n type=\"button\"\n {...dragHandleProps}\n data-testid=\"filter-drag-handle\"\n className=\"mt-1 text-muted-foreground/60 hover:text-muted-foreground cursor-grab outline-none focus-visible:shadow-focus rounded-sm\"\n aria-label={t('ui.advancedFilter.dragHandle', 'Drag to reorder')}\n >\n <GripVertical className=\"size-4\" />\n </button>\n <Button\n ref={fieldBtnRef}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setPickerOpen(true)}\n className=\"min-w-[140px] justify-start\"\n >\n {fieldDef?.label ?? t('ui.advancedFilter.selectField', 'Select field')}\n </Button>\n <FilterFieldPicker\n fields={fields}\n open={pickerOpen}\n onOpenChange={setPickerOpen}\n onSelect={(f) => {\n const nextType = getFieldType(fields, f.key)\n dispatch({\n type: 'updateRule',\n ruleId: rule.id,\n updates: { field: f.key, operator: getDefaultOperator(nextType), value: '' },\n })\n }}\n triggerRef={fieldBtnRef as React.RefObject<HTMLElement | null>}\n />\n <Select\n value={rule.operator}\n onValueChange={(next) => {\n // Preserve the value when the operator shape doesn't change; reset\n // only when shape changes (e.g. `is` \u2192 `between` clears scalar to pair).\n const nextOp = next as FilterOperator\n const sameShape = getOperatorShape(rule.operator) === getOperatorShape(nextOp)\n const updates: Partial<Pick<FilterRule, 'operator' | 'value'>> = sameShape\n ? { operator: nextOp }\n : { operator: nextOp, value: getOperatorShape(nextOp) === 'multi' || getOperatorShape(nextOp) === 'range' ? [] : '' }\n dispatch({ type: 'updateRule', ruleId: rule.id, updates })\n }}\n >\n {/* `w-auto` overrides SelectTrigger's default `w-full` so it sits\n inline with the field/value inputs instead of breaking onto a new row. */}\n <SelectTrigger\n size=\"sm\"\n className=\"w-auto min-w-[140px]\"\n aria-label={t('ui.advancedFilter.selectOperator', 'Select operator')}\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent data-advanced-filter-portal=\"\">\n {operators.map((op) => (\n <SelectItem key={op} value={op}>\n {t(`ui.advancedFilter.operator.${op}`, OPERATOR_LABELS[op])}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {!valueless ? (\n <ValueInput\n rule={rule}\n fields={fields}\n fieldType={fieldType}\n dispatch={dispatch}\n hasError={!!errorMsg}\n t={t}\n />\n ) : null}\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => dispatch({ type: 'removeNode', nodeId: rule.id })}\n aria-label={t('ui.advancedFilter.removeCondition', 'Remove condition')}\n title={t('ui.advancedFilter.removeCondition', 'Remove condition')}\n >\n <Trash2 className=\"size-4 text-muted-foreground\" />\n </IconButton>\n </div>\n {errorMsg ? (\n <div className=\"ml-6 pl-12 text-xs text-status-error-text\">{errorMsg}</div>\n ) : null}\n </div>\n )\n}\n\nfunction ValueInput({\n rule, fields, fieldType, dispatch, hasError, t,\n}: {\n rule: FilterRule\n fields: FilterFieldDef[]\n fieldType: FilterFieldType\n dispatch: (a: TreeAction) => void\n hasError: boolean\n t: Translator\n}) {\n const fieldDef = fields.find((f) => f.key === rule.field)\n const value = rule.value\n const errorClass = hasError\n ? 'border-status-error-border ring-1 ring-status-error-border focus-visible:border-status-error-border focus-within:border-status-error-border'\n : ''\n\n if (fieldType === 'select' && fieldDef?.options) {\n // Multi-value operators (`is_any_of`, `is_none_of`, `has_any_of`, etc.)\n // need an array value with multiple selectable options. Radix `Select` is\n // single-select only, so the previous code silently overwrote each\n // selection (and validation kept failing because the value never became an\n // array). Render a checkbox-list popover when the operator is multi-shape.\n if (getOperatorShape(rule.operator) === 'multi') {\n return (\n <MultiValuePicker\n rule={rule}\n options={fieldDef.options}\n dispatch={dispatch}\n errorClass={errorClass}\n t={t}\n />\n )\n }\n return (\n <Select\n value={typeof value === 'string' && value.length ? value : undefined}\n onValueChange={(next) =>\n dispatch({ type: 'updateRule', ruleId: rule.id, updates: { value: next ?? '' } })\n }\n >\n <SelectTrigger\n size=\"sm\"\n className={`w-auto min-w-[160px] ${errorClass}`}\n aria-label={t('ui.advancedFilter.selectValue', 'Select value')}\n >\n <SelectValue placeholder={t('ui.advancedFilter.selectValuePlaceholder', 'Select...')} />\n </SelectTrigger>\n <SelectContent data-advanced-filter-portal=\"\">\n {fieldDef.options.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>\n {opt.tone ? (\n <span className=\"inline-flex items-center gap-2\">\n <span className={toneDotClass(opt.tone)} aria-hidden=\"true\" />\n {opt.label}\n </span>\n ) : (\n opt.label\n )}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n }\n\n // `Input` ships `w-full` by default \u2014 use `w-auto` so inputs don't stretch\n // inside the wrapping flex row. `between` needs a `[start, end]` pair, so it\n // gets its own component below.\n if (rule.operator === 'between' && (fieldType === 'number' || fieldType === 'date')) {\n return (\n <BetweenInput\n rule={rule}\n fieldType={fieldType}\n dispatch={dispatch}\n errorClass={errorClass}\n t={t}\n />\n )\n }\n\n if (fieldType === 'date') {\n return (\n <Input\n type=\"date\"\n size=\"sm\"\n className={`w-auto min-w-[160px] ${errorClass}`}\n value={typeof value === 'string' ? value : ''}\n onChange={(e) =>\n dispatch({ type: 'updateRule', ruleId: rule.id, updates: { value: e.target.value } })\n }\n aria-label={t('ui.advancedFilter.dateValue', 'Date value')}\n />\n )\n }\n\n if (fieldType === 'number') {\n return (\n <Input\n type=\"number\"\n size=\"sm\"\n className={`w-[120px] ${errorClass}`}\n value={typeof value === 'number' ? value : typeof value === 'string' ? value : ''}\n onChange={(e) =>\n dispatch({ type: 'updateRule', ruleId: rule.id, updates: { value: e.target.value } })\n }\n placeholder={t('ui.advancedFilter.numberPlaceholder', 'Value')}\n aria-label={t('ui.advancedFilter.numberValue', 'Number value')}\n />\n )\n }\n\n return (\n <Input\n type=\"text\"\n size=\"sm\"\n className={`w-auto min-w-[160px] ${errorClass}`}\n value={typeof value === 'string' ? value : ''}\n onChange={(e) =>\n dispatch({ type: 'updateRule', ruleId: rule.id, updates: { value: e.target.value } })\n }\n placeholder={t('ui.advancedFilter.textPlaceholder', 'Value...')}\n aria-label={t('ui.advancedFilter.textValue', 'Text value')}\n />\n )\n}\n\n/**\n * Two inputs (start / end) for the `between` operator on date and number\n * fields. Stores the value as `[start, end]`; either side can be empty while\n * the user types, but validation requires both to be set before apply.\n */\nfunction BetweenInput({\n rule, fieldType, dispatch, errorClass, t,\n}: {\n rule: FilterRule\n fieldType: FilterFieldType\n dispatch: (a: TreeAction) => void\n errorClass: string\n t: Translator\n}) {\n const pair = Array.isArray(rule.value) ? rule.value : []\n const start = typeof pair[0] === 'string' || typeof pair[0] === 'number' ? String(pair[0] ?? '') : ''\n const end = typeof pair[1] === 'string' || typeof pair[1] === 'number' ? String(pair[1] ?? '') : ''\n const update = (which: 'start' | 'end', next: string) => {\n const value = which === 'start' ? [next, end] : [start, next]\n dispatch({ type: 'updateRule', ruleId: rule.id, updates: { value } })\n }\n const inputType = fieldType === 'date' ? 'date' : 'number'\n const inputWidth = fieldType === 'date' ? 'w-auto min-w-[140px]' : 'w-[110px]'\n return (\n <div className=\"inline-flex items-center gap-1\">\n <Input\n type={inputType}\n size=\"sm\"\n className={`${inputWidth} ${errorClass}`}\n value={start}\n onChange={(e) => update('start', e.target.value)}\n placeholder={fieldType === 'number' ? t('ui.advancedFilter.numberPlaceholder', 'Value') : undefined}\n aria-label={t('ui.advancedFilter.betweenStart', 'Start')}\n />\n <span className=\"text-xs text-muted-foreground\">{t('ui.advancedFilter.and', 'and')}</span>\n <Input\n type={inputType}\n size=\"sm\"\n className={`${inputWidth} ${errorClass}`}\n value={end}\n onChange={(e) => update('end', e.target.value)}\n placeholder={fieldType === 'number' ? t('ui.advancedFilter.numberPlaceholder', 'Value') : undefined}\n aria-label={t('ui.advancedFilter.betweenEnd', 'End')}\n />\n </div>\n )\n}\n\n/**\n * Trigger + popover with a checkbox list. Used when a select-typed rule has a\n * multi-shape operator (`is_any_of`, `has_all_of`, etc.). The trigger displays\n * a \"{first} +N\" summary; the popover lets the user toggle each option.\n */\nfunction MultiValuePicker({\n rule, options, dispatch, errorClass, t,\n}: {\n rule: FilterRule\n options: NonNullable<FilterFieldDef['options']>\n dispatch: (a: TreeAction) => void\n errorClass: string\n t: Translator\n}) {\n const selected: string[] = Array.isArray(rule.value)\n ? rule.value.map((v) => String(v)).filter((v) => v.length > 0)\n : []\n const selectedLabel = (() => {\n if (selected.length === 0) return t('ui.advancedFilter.selectValuePlaceholder', 'Select...')\n const firstOpt = options.find((o) => o.value === selected[0])\n const head = firstOpt?.label ?? selected[0]\n const more = selected.length - 1\n return more > 0 ? `${head} +${more}` : head\n })()\n const toggle = (val: string) => {\n const next = selected.includes(val) ? selected.filter((v) => v !== val) : [...selected, val]\n dispatch({ type: 'updateRule', ruleId: rule.id, updates: { value: next } })\n }\n return (\n <Popover>\n <PopoverTrigger asChild>\n {/* Visually mirrors `SelectTrigger size=\"sm\"` (h-8, rounded-md, border)\n so the operator + value pair stays visually consistent. */}\n <button\n type=\"button\"\n className={`inline-flex h-8 items-center justify-between gap-2 rounded-md border border-input bg-background px-2.5 text-xs shadow-xs transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:shadow-focus focus-visible:border-foreground w-auto min-w-[160px] ${errorClass}`}\n aria-label={t('ui.advancedFilter.selectValue', 'Select value')}\n data-testid=\"multi-value-trigger\"\n >\n <span className={selected.length === 0 ? 'text-muted-foreground' : ''}>{selectedLabel}</span>\n <ChevronDown className=\"size-3 text-muted-foreground\" />\n </button>\n </PopoverTrigger>\n <PopoverContent className=\"w-64 p-1\" align=\"start\" data-advanced-filter-portal=\"\">\n <div className=\"max-h-64 overflow-y-auto\" role=\"listbox\" aria-multiselectable=\"true\">\n {options.map((opt) => {\n const isOn = selected.includes(opt.value)\n return (\n // Raw <button>: needs `role=\"option\"` + `aria-selected`; <Button> would override role.\n <button\n key={opt.value}\n type=\"button\"\n role=\"option\"\n aria-selected={isOn}\n onClick={() => toggle(opt.value)}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-accent focus-visible:bg-accent focus-visible:outline-none\"\n >\n <Checkbox checked={isOn} aria-hidden=\"true\" tabIndex={-1} className=\"pointer-events-none\" />\n {opt.tone ? (\n <span className=\"inline-flex items-center gap-2\">\n <span className={toneDotClass(opt.tone)} aria-hidden=\"true\" />\n {opt.label}\n </span>\n ) : (\n <span>{opt.label}</span>\n )}\n </button>\n )\n })}\n </div>\n </PopoverContent>\n </Popover>\n )\n}\n\nfunction toneDotClass(tone: NonNullable<FilterFieldDef['options']>[number]['tone']): string {\n // Solid filled dot using the saturated `*-icon` token (matches Tag's `dotColorMap`).\n const base = 'inline-block size-2 rounded-full shrink-0'\n switch (tone) {\n case 'success': return `${base} bg-status-success-icon`\n case 'error': return `${base} bg-status-error-icon`\n case 'warning': return `${base} bg-status-warning-icon`\n case 'info': return `${base} bg-status-info-icon`\n case 'neutral': return `${base} bg-status-neutral-icon`\n case 'pink': return `${base} bg-status-pink-icon`\n case 'brand': return `${base} bg-brand-violet`\n default: return `${base} bg-muted-foreground/40`\n }\n}\n\n// Single Kendo-style toolbar per group: combinator toggle, `Add condition`,\n// `Add subgroup` / `Add group`, plus a trailing delete for non-root groups.\nfunction GroupToolbar({\n group, level, tree, dispatch, defaultField, fields, isRoot, t,\n}: {\n group: FilterGroup\n level: number\n tree: AdvancedFilterTree\n dispatch: (a: TreeAction) => void\n defaultField?: string\n fields: FilterFieldDef[]\n isRoot: boolean\n t: Translator\n}) {\n const [pickerOpen, setPickerOpen] = React.useState(false)\n const addBtnRef = React.useRef<HTMLButtonElement>(null)\n const addRuleEnabled = canAddRule(tree, group.id)\n const addGroupEnabled = canAddGroup(tree, group.id)\n\n const widthCapHit = group.children.length >= TREE_LIMITS.maxChildrenPerGroup\n const depthCapHit = level >= TREE_LIMITS.maxGroupLevel\n\n const addRuleTitle = !addRuleEnabled\n ? widthCapHit\n ? t('ui.advancedFilter.limitWidthReached', 'Maximum {max} conditions per group', {\n max: TREE_LIMITS.maxChildrenPerGroup,\n })\n : t('ui.advancedFilter.limitTotalReached', 'Maximum {max} conditions reached', {\n max: TREE_LIMITS.maxTotalRules,\n })\n : undefined\n\n const addGroupTitle = !addGroupEnabled\n ? depthCapHit\n ? t('ui.advancedFilter.limitDepthReached', 'Maximum nesting depth reached')\n : widthCapHit\n ? t('ui.advancedFilter.limitWidthReached', 'Maximum {max} conditions per group', {\n max: TREE_LIMITS.maxChildrenPerGroup,\n })\n : t('ui.advancedFilter.limitTotalReached', 'Maximum {max} conditions reached', {\n max: TREE_LIMITS.maxTotalRules,\n })\n : undefined\n\n const addGroupLabel = level === 1\n ? t('ui.advancedFilter.addGroup', 'Add group')\n : t('ui.advancedFilter.addSubgroup', 'Add subgroup')\n\n // `text-brand-violet` overrides Button's default disabled token, so we have to\n // re-state muted disabled colors here \u2014 otherwise disabled CTAs still look clickable.\n const ctaClass = 'text-brand-violet hover:text-brand-violet/80 hover:bg-transparent px-0 disabled:text-muted-foreground/50 disabled:hover:text-muted-foreground/50'\n\n return (\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <AndOrToggle\n combinator={group.combinator}\n onChange={(c) => dispatch({ type: 'updateGroupCombinator', groupId: group.id, combinator: c })}\n t={t}\n />\n <Button\n ref={addBtnRef}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={!addRuleEnabled}\n title={addRuleTitle}\n onClick={() => setPickerOpen(true)}\n className={ctaClass}\n >\n <Plus className=\"size-4\" />\n {t('ui.advancedFilter.addCondition', 'Add condition')}\n </Button>\n <FilterFieldPicker\n fields={fields}\n open={pickerOpen}\n onOpenChange={setPickerOpen}\n onSelect={(f) =>\n dispatch({\n type: 'addRule',\n groupId: group.id,\n defaultField: f.key,\n defaultOperator: getDefaultOperator(f.type),\n })\n }\n triggerRef={addBtnRef as React.RefObject<HTMLElement | null>}\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={!addGroupEnabled}\n title={addGroupTitle}\n onClick={() =>\n dispatch({ type: 'addGroup', groupId: group.id, defaultField })\n }\n className={ctaClass}\n >\n <Plus className=\"size-4\" />\n {addGroupLabel}\n </Button>\n {!isRoot ? (\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={() => dispatch({ type: 'removeNode', nodeId: group.id })}\n aria-label={t('ui.advancedFilter.deleteGroup', 'Delete group')}\n title={t('ui.advancedFilter.deleteGroup', 'Delete group')}\n className=\"ml-auto\"\n >\n <X className=\"size-4 text-muted-foreground\" />\n </IconButton>\n ) : null}\n </div>\n {!addGroupEnabled && depthCapHit ? (\n <p className=\"text-xs text-muted-foreground\" data-testid=\"filter-depth-limit-hint\">\n {t('ui.advancedFilter.limitDepthHint', 'You\\'ve reached the maximum nesting depth ({max} levels). Switch this group\\'s combinator with the toggle above instead of nesting deeper.', {\n max: TREE_LIMITS.maxGroupLevel,\n })}\n </p>\n ) : null}\n </div>\n )\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -269,6 +269,7 @@ function useMessageCompose({
269
269
  const params = new URLSearchParams();
270
270
  params.set("page", "1");
271
271
  params.set("pageSize", "100");
272
+ params.set("scopeToActiveOrganization", "1");
272
273
  const call = await apiCall(
273
274
  `/api/auth/users?${params.toString()}`,
274
275
  {