@open-mercato/core 0.4.6-develop-d0884fe560 → 0.4.6-develop-c2b70de148
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/attachments/components/AttachmentLibrary.js +1 -1
- package/dist/modules/attachments/components/AttachmentLibrary.js.map +1 -1
- package/dist/modules/catalog/components/categories/CategoriesDataTable.js +1 -1
- package/dist/modules/catalog/components/categories/CategoriesDataTable.js.map +1 -1
- package/dist/modules/customers/components/detail/hooks/usePersonTasks.js +1 -1
- package/dist/modules/customers/components/detail/hooks/usePersonTasks.js.map +1 -1
- package/dist/modules/directory/backend/directory/organizations/page.js +1 -1
- package/dist/modules/directory/backend/directory/organizations/page.js.map +1 -1
- package/dist/modules/directory/backend/directory/tenants/page.js +1 -1
- package/dist/modules/directory/backend/directory/tenants/page.js.map +1 -1
- package/dist/modules/feature_toggles/components/FeatureTogglesTable.js +1 -1
- package/dist/modules/feature_toggles/components/FeatureTogglesTable.js.map +1 -1
- package/dist/modules/feature_toggles/components/OverridesTable.js +1 -1
- package/dist/modules/feature_toggles/components/OverridesTable.js.map +1 -1
- package/dist/modules/messages/components/MessagesInboxPageClient.js +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +1 -1
- package/package.json +2 -2
- package/src/modules/attachments/components/AttachmentLibrary.tsx +1 -1
- package/src/modules/catalog/components/categories/CategoriesDataTable.tsx +1 -1
- package/src/modules/customers/components/detail/hooks/usePersonTasks.ts +1 -1
- package/src/modules/directory/backend/directory/organizations/page.tsx +1 -1
- package/src/modules/directory/backend/directory/tenants/page.tsx +1 -1
- package/src/modules/feature_toggles/components/FeatureTogglesTable.tsx +1 -1
- package/src/modules/feature_toggles/components/OverridesTable.tsx +1 -1
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +2 -2
|
@@ -838,7 +838,7 @@ function AttachmentLibrary() {
|
|
|
838
838
|
}
|
|
839
839
|
}, [deleteTarget, queryClient, selectedRow, t]);
|
|
840
840
|
const total = data?.total ?? 0;
|
|
841
|
-
const totalPages = data?.totalPages ??
|
|
841
|
+
const totalPages = data?.totalPages ?? 0;
|
|
842
842
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
843
843
|
/* @__PURE__ */ jsx(
|
|
844
844
|
DataTable,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/components/AttachmentLibrary.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\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 { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { CrudForm, type CrudField, type CrudFormGroup, type CrudCustomFieldRenderProps } 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 { z } from 'zod'\nimport { E } from '#generated/entities.ids.generated'\nimport type { LucideIcon } from 'lucide-react'\nimport { Download, Plus, Upload, Trash2, File, FileText, FileSpreadsheet, FileArchive, FileAudio, FileVideo, FileCode } from 'lucide-react'\nimport { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { AttachmentDeleteDialog, AttachmentMetadataDialog, type AttachmentItem, type AttachmentMetadataSavePayload, type AssignmentDraft } from '@open-mercato/ui/backend/detail'\n\ntype AttachmentAssignment = {\n type: string\n id: string\n href?: string | null\n label?: string | null\n}\n\ntype AttachmentRow = AttachmentItem\n\ntype AttachmentLibraryResponse = {\n items: AttachmentRow[]\n page: number\n pageSize: number\n total: number\n totalPages: number\n availableTags: string[]\n partitions: Array<{ code: string; title: string; description?: string | null; isPublic?: boolean }>\n error?: string\n}\n\nconst PAGE_SIZE = 25\nconst ENV_APP_URL = (process.env.NEXT_PUBLIC_APP_URL || '').replace(/\\/$/, '')\nconst LIBRARY_ENTITY_ID = 'attachments:library'\n\nfunction filterLibraryAssignments(assignments?: AttachmentAssignment[] | null): AttachmentAssignment[] {\n return (assignments ?? []).filter((assignment) => assignment.type !== LIBRARY_ENTITY_ID)\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\nfunction humanDate(value: string, locale?: string): string {\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return value\n return date.toLocaleString(locale ?? undefined)\n}\n\nfunction buildFilterSignature(values: FilterValues): string {\n return JSON.stringify(values, Object.keys(values).sort((a, b) => a.localeCompare(b)))\n}\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 resolveFileExtension(fileName?: string | null): string {\n if (!fileName) return ''\n const normalized = fileName.trim()\n if (!normalized) return ''\n const lastDot = normalized.lastIndexOf('.')\n if (lastDot === -1 || lastDot === normalized.length - 1) return ''\n return normalized.slice(lastDot + 1).toLowerCase()\n}\n\nconst EXTENSION_ICON_MAP: Record<string, LucideIcon> = {\n pdf: FileText,\n doc: FileText,\n docx: FileText,\n txt: FileText,\n md: FileText,\n rtf: FileText,\n xls: FileSpreadsheet,\n xlsx: FileSpreadsheet,\n csv: FileSpreadsheet,\n ods: FileSpreadsheet,\n ppt: FileText,\n pptx: FileText,\n zip: FileArchive,\n gz: FileArchive,\n rar: FileArchive,\n tgz: FileArchive,\n '7z': FileArchive,\n tar: FileArchive,\n json: FileCode,\n js: FileCode,\n ts: FileCode,\n jsx: FileCode,\n tsx: FileCode,\n html: FileCode,\n css: FileCode,\n xml: FileCode,\n yaml: FileCode,\n yml: FileCode,\n mp3: FileAudio,\n wav: FileAudio,\n flac: FileAudio,\n ogg: FileAudio,\n mp4: FileVideo,\n mov: FileVideo,\n avi: FileVideo,\n webm: FileVideo,\n}\n\nconst MIME_FALLBACK_ICONS: Record<string, LucideIcon> = {\n audio: FileAudio,\n video: FileVideo,\n text: FileText,\n application: FileText,\n}\n\nfunction resolveAttachmentPlaceholder(mimeType?: string | null, fileName?: string | null): { icon: LucideIcon; label: string } {\n const extension = resolveFileExtension(fileName)\n const normalizedMime = typeof mimeType === 'string' ? mimeType.toLowerCase() : ''\n if (extension && EXTENSION_ICON_MAP[extension]) {\n return { icon: EXTENSION_ICON_MAP[extension], label: extension.toUpperCase() }\n }\n if (!extension && normalizedMime.includes('pdf')) {\n return { icon: FileText, label: 'PDF' }\n }\n if (!extension && normalizedMime.includes('zip')) {\n return { icon: FileArchive, label: 'ZIP' }\n }\n if (!extension && normalizedMime.includes('json')) {\n return { icon: FileCode, label: 'JSON' }\n }\n const mimeRoot = normalizedMime.split('/')[0] || ''\n if (mimeRoot && MIME_FALLBACK_ICONS[mimeRoot]) {\n return { icon: MIME_FALLBACK_ICONS[mimeRoot], label: mimeRoot.toUpperCase() }\n }\n const fallbackSource = extension || mimeRoot || 'file'\n const fallbackLabel = fallbackSource.slice(0, 6).toUpperCase()\n return { icon: File, label: fallbackLabel }\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\n\ntype AttachmentUploadFormValues = {\n files: File[]\n partitionCode?: string\n tags?: string[]\n assignments?: AssignmentDraft[]\n} & Record<string, unknown>\n\ntype AssignmentsEditorProps = {\n value: AssignmentDraft[]\n onChange: (next: AssignmentDraft[]) => void\n labels: {\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 disabled?: boolean\n}\n\nfunction AttachmentAssignmentsEditor({ value, onChange, labels, disabled }: AssignmentsEditorProps) {\n const handleChange = React.useCallback(\n (index: number, patch: Partial<AssignmentDraft>) => {\n onChange(value.map((entry, idx) => (idx === index ? { ...entry, ...patch } : entry)))\n },\n [onChange, value],\n )\n\n const handleRemove = React.useCallback(\n (index: number) => {\n onChange(value.filter((_, idx) => idx !== index))\n },\n [onChange, value],\n )\n\n const handleAdd = React.useCallback(() => {\n onChange([...value, { type: '', id: '', href: '', label: '' }])\n }, [onChange, value])\n\n return (\n <div className=\"space-y-2\">\n <div>\n <div className=\"text-sm font-medium\">{labels.title}</div>\n <div className=\"text-xs text-muted-foreground\">{labels.description}</div>\n </div>\n <div className=\"space-y-3\">\n {value.length === 0 ? (\n <div className=\"text-xs text-muted-foreground\">No assignments yet.</div>\n ) : (\n value.map((entry, index) => (\n <div key={`${index}-${entry.type}-${entry.id}`} className=\"rounded border p-3 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\">{labels.type}</label>\n <input\n className=\"w-full rounded border px-2 py-1 text-sm\"\n value={entry.type}\n disabled={disabled}\n onChange={(event) => handleChange(index, { type: event.target.value })}\n />\n </div>\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium\">{labels.id}</label>\n <input\n className=\"w-full rounded border px-2 py-1 text-sm\"\n value={entry.id}\n disabled={disabled}\n onChange={(event) => handleChange(index, { id: event.target.value })}\n />\n </div>\n </div>\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium\">{labels.href}</label>\n <input\n className=\"w-full rounded border px-2 py-1 text-sm\"\n value={entry.href ?? ''}\n disabled={disabled}\n onChange={(event) => handleChange(index, { href: event.target.value })}\n />\n </div>\n {labels.label ? (\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium\">{labels.label}</label>\n <input\n className=\"w-full rounded border px-2 py-1 text-sm\"\n value={entry.label ?? ''}\n disabled={disabled}\n onChange={(event) => handleChange(index, { label: event.target.value })}\n />\n </div>\n ) : null}\n </div>\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n disabled={disabled}\n onClick={() => handleRemove(index)}\n className=\"inline-flex items-center gap-1 text-muted-foreground\"\n >\n <Trash2 className=\"h-4 w-4\" />\n {labels.remove}\n </Button>\n </div>\n </div>\n ))\n )}\n </div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={disabled} onClick={handleAdd} className=\"inline-flex items-center gap-1\">\n <Plus className=\"h-4 w-4\" />\n {labels.add}\n </Button>\n </div>\n )\n}\n\ntype AttachmentFilesFieldProps = CrudCustomFieldRenderProps & {\n labels: {\n dropHint: string\n choose: string\n uploading: string\n empty: string\n }\n uploading: boolean\n}\n\nfunction AttachmentFilesField({\n value,\n setValue,\n disabled,\n error,\n labels,\n uploading,\n}: AttachmentFilesFieldProps) {\n const files = React.useMemo(() => (Array.isArray(value) ? (value as File[]) : []), [value])\n const [isDragOver, setDragOver] = React.useState(false)\n const fileInputRef = React.useRef<HTMLInputElement | null>(null)\n\n const acceptFiles = React.useCallback(\n (list: FileList | null) => {\n if (!list?.length) return\n const dedupe = new Map<string, File>(files.map((file) => [`${file.name}:${file.size}`, file]))\n Array.from(list).forEach((file) => {\n dedupe.set(`${file.name}:${file.size}`, file)\n })\n setValue(Array.from(dedupe.values()))\n },\n [files, setValue],\n )\n\n const handleDrop = React.useCallback(\n (event: React.DragEvent<HTMLDivElement>) => {\n if (disabled || uploading) return\n event.preventDefault()\n event.stopPropagation()\n setDragOver(false)\n acceptFiles(event.dataTransfer?.files ?? null)\n },\n [acceptFiles, disabled, uploading],\n )\n\n const handleDragOver = React.useCallback(\n (event: React.DragEvent<HTMLDivElement>) => {\n if (disabled || uploading) return\n event.preventDefault()\n event.stopPropagation()\n setDragOver(true)\n },\n [disabled, uploading],\n )\n\n const handleDragLeave = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(false)\n }, [])\n\n const removeFile = React.useCallback(\n (name: string, size: number) => {\n if (disabled || uploading) return\n setValue(files.filter((file) => !(file.name === name && file.size === size)))\n },\n [disabled, files, setValue, uploading],\n )\n\n const pickFiles = React.useCallback(() => {\n if (disabled || uploading) return\n fileInputRef.current?.click()\n }, [disabled, uploading])\n\n const renderFileList = () => {\n if (!files.length) {\n return <p className=\"text-xs text-muted-foreground\">{labels.empty}</p>\n }\n return (\n <div className=\"space-y-2\">\n {files.map((candidate) => (\n <div key={`${candidate.name}-${candidate.size}`} className=\"flex items-center justify-between rounded border px-3 py-2 text-sm\">\n <div>\n <div className=\"font-medium\">{candidate.name}</div>\n <div className=\"text-xs text-muted-foreground\">{formatFileSize(candidate.size)}</div>\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => removeFile(candidate.name, candidate.size)}\n disabled={disabled || uploading}\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n ))}\n </div>\n )\n }\n\n return (\n <div className=\"space-y-2\">\n <div\n className={cn(\n 'flex flex-col items-center justify-center rounded-lg border border-dashed p-6 text-center transition-colors',\n isDragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/30',\n disabled || uploading ? 'opacity-70' : '',\n )}\n onDrop={handleDrop}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n role=\"presentation\"\n >\n <Upload className=\"mx-auto h-6 w-6 text-muted-foreground\" />\n <p className=\"mt-2 text-sm text-muted-foreground\">{labels.dropHint}</p>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={pickFiles} disabled={disabled || uploading}>\n {uploading ? labels.uploading : labels.choose}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n className=\"hidden\"\n multiple\n onChange={(event) => {\n acceptFiles(event.target.files)\n event.currentTarget.value = ''\n }}\n disabled={disabled || uploading}\n />\n </div>\n {renderFileList()}\n {error ? <p className=\"text-xs font-medium text-red-600\">{error}</p> : null}\n </div>\n )\n}\n\ntype AttachmentUploadFormProps = {\n partitions: Array<{ code: string; title: string }>\n availableTags: string[]\n onUploaded: () => void\n onCancel: () => void\n}\n\nfunction AttachmentUploadForm({ partitions, availableTags, onUploaded, onCancel }: AttachmentUploadFormProps) {\n const t = useT()\n const [isUploading, setIsUploading] = React.useState(false)\n const [uploadProgress, setUploadProgress] = React.useState<{ completed: number; total: number }>({ completed: 0, total: 0 })\n\n const partitionOptions = React.useMemo(\n () =>\n partitions.map((entry) => ({\n value: entry.code,\n label: entry.title || entry.code,\n })),\n [partitions],\n )\n\n const assignmentLabels = React.useMemo(\n () => ({\n title: t('attachments.library.upload.assignments.title', 'Assignments'),\n description: t(\n 'attachments.library.upload.assignments.description',\n 'Optionally link this file to existing records now or add them later.',\n ),\n type: t('attachments.library.upload.assignments.type', 'Type'),\n id: t('attachments.library.upload.assignments.id', 'Record ID'),\n href: t('attachments.library.upload.assignments.href', 'Link'),\n label: t('attachments.library.upload.assignments.label', 'Label'),\n add: t('attachments.library.upload.assignments.add', 'Add assignment'),\n remove: t('attachments.library.upload.assignments.remove', 'Remove'),\n }),\n [t],\n )\n\n const formSchema = React.useMemo(\n () =>\n z\n .object({\n files: z.array(z.any()).min(1, { message: t('attachments.library.upload.fileRequired', 'Select at least one file to upload.') }),\n partitionCode: z.string().optional(),\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 [t],\n )\n\n const fields = React.useMemo<CrudField[]>(() => {\n return [\n {\n id: 'files',\n label: t('attachments.library.upload.file', 'Files'),\n type: 'custom',\n component: (props) => (\n <AttachmentFilesField\n {...props}\n uploading={isUploading}\n labels={{\n dropHint: t('attachments.library.upload.dropHint', 'Drag and drop files here or click to upload.'),\n choose: t('attachments.library.upload.choose', 'Choose files'),\n uploading: t('attachments.library.upload.submitting', 'Uploading\u2026'),\n empty: t('attachments.library.upload.noFiles', 'No files selected yet.'),\n }}\n />\n ),\n },\n {\n id: 'partitionCode',\n label: t('attachments.library.upload.partition', 'Partition'),\n type: 'select',\n options: [\n { value: '', label: t('attachments.library.upload.partitionDefault', 'Default (private)') },\n ...partitionOptions,\n ],\n },\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.upload.tagsPlaceholder', 'Add tags')}\n disabled={Boolean(disabled) || isUploading}\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) || isUploading}\n />\n ),\n },\n ]\n }, [assignmentLabels, availableTags, isUploading, partitionOptions, t])\n\n const groups = React.useMemo<CrudFormGroup[]>(() => {\n return [\n {\n id: 'details',\n title: t('attachments.library.upload.title', 'Upload attachment'),\n column: 1,\n fields: ['files', 'partitionCode', '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 uploadPercentage = uploadProgress.total\n ? Math.min(100, Math.round((uploadProgress.completed / uploadProgress.total) * 100))\n : 0\n\n const handleSubmit = React.useCallback(\n async (values: AttachmentUploadFormValues) => {\n const files = Array.isArray(values.files) ? values.files : []\n if (!files.length) {\n throw new Error(t('attachments.library.upload.fileRequired', 'Select at least one file to upload.'))\n }\n setUploadProgress({ completed: 0, total: files.length })\n setIsUploading(true)\n try {\n const tags = Array.isArray(values.tags)\n ? values.tags\n .map((tag) => (typeof tag === 'string' ? tag.trim() : ''))\n .filter((tag) => tag.length > 0)\n : []\n const cleanedAssignments =\n Array.isArray(values.assignments) && values.assignments.length\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 let completed = 0\n for (const file of files) {\n const fd = new FormData()\n fd.set('entityId', LIBRARY_ENTITY_ID)\n fd.set(\n 'recordId',\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : String(Date.now()),\n )\n fd.set('file', file)\n if (typeof values.partitionCode === 'string' && values.partitionCode.trim().length) {\n fd.set('partitionCode', values.partitionCode.trim())\n }\n if (tags.length) fd.set('tags', JSON.stringify(tags))\n if (cleanedAssignments.length) fd.set('assignments', JSON.stringify(cleanedAssignments))\n if (Object.keys(customFields).length) fd.set('customFields', JSON.stringify(customFields))\n const call = await apiCall<{ error?: string }>('/api/attachments', {\n method: 'POST',\n body: fd,\n })\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.upload.failed', 'Upload failed.')\n throw new Error(message)\n }\n completed += 1\n setUploadProgress({ completed, total: files.length })\n }\n flash(t('attachments.library.upload.success', 'Attachment uploaded.'), 'success')\n onUploaded()\n onCancel()\n } catch (err: any) {\n const message = err?.message || t('attachments.library.upload.failed', 'Upload failed.')\n flash(message, 'error')\n throw new Error(message)\n } finally {\n setIsUploading(false)\n }\n },\n [onCancel, onUploaded, t],\n )\n\n return (\n <div className=\"relative\">\n <CrudForm<AttachmentUploadFormValues>\n embedded\n schema={formSchema}\n entityId={E.attachments.attachment}\n fields={fields}\n groups={groups}\n initialValues={{ files: [], tags: [], assignments: [], partitionCode: '' }}\n submitLabel={\n isUploading\n ? t('attachments.library.upload.submitting', 'Uploading\u2026')\n : t('attachments.library.upload.submit', 'Upload')\n }\n extraActions={\n <Button type=\"button\" variant=\"outline\" onClick={onCancel} disabled={isUploading}>\n {t('attachments.library.upload.cancel', 'Cancel')}\n </Button>\n }\n onSubmit={handleSubmit}\n />\n {isUploading ? (\n <div className=\"pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-background/90 px-6 text-center backdrop-blur\">\n <div className=\"flex w-full max-w-sm flex-col items-center gap-4 rounded-xl border border-border/50 bg-card/95 px-6 py-8 shadow-2xl\">\n <Spinner size=\"lg\" className=\"border-primary/50 border-t-primary\" />\n <div className=\"w-full space-y-3\">\n <p className=\"text-base font-semibold\">\n {t('attachments.library.upload.progressLabel', 'Uploading files')}\n </p>\n {uploadProgress.total > 0 ? (\n <>\n <p className=\"text-sm text-muted-foreground\">\n {uploadProgress.completed}/{uploadProgress.total}\n </p>\n <div className=\"h-2 w-full rounded bg-muted\">\n <div\n className=\"h-2 rounded bg-primary transition-all\"\n style={{\n width: `${uploadPercentage}%`,\n }}\n />\n </div>\n </>\n ) : null}\n </div>\n </div>\n </div>\n ) : null}\n </div>\n )\n}\ntype UploadDialogProps = {\n open: boolean\n onOpenChange: (next: boolean) => void\n partitions: Array<{ code: string; title: string }>\n availableTags: string[]\n onUploaded: () => void\n}\n\nfunction AttachmentUploadDialog({ open, onOpenChange, partitions, availableTags, onUploaded }: UploadDialogProps) {\n const t = useT()\n const [formResetKey, setFormResetKey] = React.useState(0)\n const previousOpen = React.useRef(open)\n\n React.useEffect(() => {\n if (previousOpen.current && !open) {\n setFormResetKey((prev) => prev + 1)\n }\n previousOpen.current = open\n }, [open])\n\n const handleDialogChange = React.useCallback(\n (next: boolean) => {\n onOpenChange(next)\n },\n [onOpenChange],\n )\n\n const handleUploaded = React.useCallback(() => {\n onUploaded()\n }, [onUploaded])\n\n return (\n <Dialog open={open} onOpenChange={handleDialogChange}>\n <DialogContent className=\"sm:max-w-[54.6rem]\">\n <DialogHeader>\n <DialogTitle>{t('attachments.library.upload.title', 'Upload attachment')}</DialogTitle>\n </DialogHeader>\n <AttachmentUploadForm\n key={formResetKey}\n partitions={partitions}\n availableTags={availableTags}\n onUploaded={handleUploaded}\n onCancel={() => handleDialogChange(false)}\n />\n </DialogContent>\n </Dialog>\n )\n}\n\nexport function AttachmentLibrary() {\n const t = useT()\n const queryClient = useQueryClient()\n const [page, setPage] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'createdAt', desc: true }])\n const [metadataDialogOpen, setMetadataDialogOpen] = React.useState(false)\n const [selectedRow, setSelectedRow] = React.useState<AttachmentRow | null>(null)\n const [uploadDialogOpen, setUploadDialogOpen] = React.useState(false)\n const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)\n const [deleteTarget, setDeleteTarget] = React.useState<AttachmentRow | null>(null)\n const [deletePending, setDeletePending] = React.useState(false)\n const filterSignature = React.useMemo(() => buildFilterSignature(filterValues), [filterValues])\n const sortingSignature = React.useMemo(() => JSON.stringify(sorting), [sorting])\n\n const { data, isLoading, error, refetch } = useQuery({\n queryKey: ['attachments-library', page, search, filterSignature, sortingSignature],\n queryFn: async () => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(PAGE_SIZE))\n if (search.trim().length > 0) params.set('search', search.trim())\n const partition = typeof filterValues.partition === 'string' ? filterValues.partition : ''\n if (partition) params.set('partition', partition)\n const tags = Array.isArray(filterValues.tags) ? filterValues.tags : []\n if (tags.length > 0) params.set('tags', tags.join(','))\n if (sorting.length > 0) {\n const primary = sorting[0]\n params.set('sortField', primary.id)\n params.set('sortDir', primary.desc ? 'desc' : 'asc')\n }\n const call = await apiCall<AttachmentLibraryResponse>(`/api/attachments/library?${params.toString()}`)\n if (!call.ok || !call.result) {\n const message = call.result?.error || t('attachments.library.errors.load', 'Failed to load attachments.')\n throw new Error(message)\n }\n return call.result\n },\n })\n\n const partitions = data?.partitions ?? []\n const availableTags = data?.availableTags ?? []\n\n const filters = React.useMemo<FilterDef[]>(() => {\n const partitionOptions = partitions.map((entry) => ({\n value: entry.code,\n label: entry.title || entry.code,\n }))\n return [\n {\n id: 'partition',\n label: t('attachments.library.filters.partition', 'Partition'),\n type: 'select',\n options: partitionOptions,\n },\n {\n id: 'tags',\n label: t('attachments.library.filters.tags', 'Tags'),\n type: 'tags',\n placeholder: t('attachments.library.filters.tagsPlaceholder', 'Filter by tag'),\n options: availableTags.map((tag) => ({ value: tag, label: tag })),\n },\n ]\n }, [availableTags, partitions, t])\n\n const items = data?.items ?? []\n\n const columns = React.useMemo<ColumnDef<AttachmentRow>[]>(() => {\n return [\n {\n id: 'preview',\n header: '',\n enableSorting: false,\n cell: ({ row }) => {\n const value = row.original\n if (value.thumbnailUrl) {\n return (\n <div className=\"h-16 w-16 overflow-hidden rounded border bg-muted\">\n <img\n src={value.thumbnailUrl}\n alt={value.fileName}\n className=\"h-full w-full object-cover\"\n loading=\"lazy\"\n />\n </div>\n )\n }\n const placeholder = resolveAttachmentPlaceholder(value.mimeType, value.fileName)\n const PlaceholderIcon = placeholder.icon\n return (\n <div className=\"flex h-16 w-16 flex-col items-center justify-center rounded border bg-muted text-[10px] font-semibold uppercase text-muted-foreground\">\n <PlaceholderIcon className=\"mb-1 h-5 w-5 text-muted-foreground\" aria-hidden />\n {placeholder.label}\n </div>\n )\n },\n },\n {\n id: 'fileName',\n accessorKey: 'fileName',\n header: t('attachments.library.table.file', 'File'),\n cell: ({ row }) => {\n const value = row.original\n return (\n <div className=\"space-y-1 min-w-0 max-w-[280px]\">\n <div className=\"font-medium truncate\" title={value.fileName}>\n {value.fileName}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {formatFileSize(value.fileSize)} \u2022 {value.mimeType || 'application/octet-stream'}\n </div>\n <div className=\"text-xs text-muted-foreground line-clamp-2\">\n {value.content?.trim()\n ? value.content\n : t('attachments.library.metadata.noContent', 'No text extracted')}\n </div>\n </div>\n )\n },\n },\n {\n id: 'tags',\n accessorKey: 'tags',\n header: t('attachments.library.table.tags', 'Tags'),\n enableSorting: false,\n cell: ({ row }) => {\n const tags = row.original.tags ?? []\n if (!tags.length) return <span className=\"text-xs text-muted-foreground\">\u2014</span>\n return (\n <div className=\"flex flex-wrap gap-1\">\n {tags.map((tag) => (\n <Badge key={tag} variant=\"outline\">\n {tag}\n </Badge>\n ))}\n </div>\n )\n },\n },\n {\n id: 'assignments',\n accessorKey: 'assignments',\n header: t('attachments.library.table.assignments', 'Assignments'),\n enableSorting: false,\n cell: ({ row }) => {\n const assignments = filterLibraryAssignments(row.original.assignments)\n if (!assignments.length) return <span className=\"text-xs text-muted-foreground\">\u2014</span>\n return (\n <div className=\"flex flex-col gap-1\">\n {assignments.map((assignment) => {\n const label = assignment.label?.trim() || assignment.id\n const hideType =\n assignment.type === (E as any).catalog?.catalog_product ||\n assignment.type === (E as any).catalog?.catalog_product_variant\n const content = hideType ? label : `${assignment.type}: ${label}`\n return assignment.href ? (\n <a\n key={`${assignment.type}-${assignment.id}-${assignment.href}`}\n href={assignment.href}\n className=\"text-sm text-blue-600 underline\"\n target=\"_blank\"\n rel=\"noreferrer\"\n >\n {content}\n </a>\n ) : (\n <div key={`${assignment.type}-${assignment.id}`} className=\"text-sm\">\n {content}\n </div>\n )\n })}\n </div>\n )\n },\n },\n {\n id: 'partitionCode',\n accessorKey: 'partitionCode',\n header: t('attachments.library.table.partition', 'Partition'),\n cell: ({ row }) => (\n <div className=\"text-sm text-muted-foreground\">\n {row.original.partitionTitle ?? row.original.partitionCode}\n </div>\n ),\n },\n {\n id: 'createdAt',\n accessorKey: 'createdAt',\n header: t('attachments.library.table.created', 'Created'),\n cell: ({ row }) => {\n const createdAt = row.original.createdAt\n return (\n <div className=\"text-sm text-muted-foreground\">\n {createdAt ? humanDate(createdAt) : '\u2014'}\n </div>\n )\n },\n },\n {\n id: 'download',\n header: t('attachments.library.table.download', 'Download'),\n enableSorting: false,\n cell: ({ row }) => {\n const downloadPath = buildAttachmentFileUrl(row.original.id, { download: true })\n const absolute = resolveAbsoluteUrl(downloadPath)\n return (\n <Button variant=\"ghost\" size=\"icon\" asChild>\n <a href={absolute} download aria-label={t('attachments.library.table.download', 'Download')}>\n <Download className=\"h-4 w-4\" />\n </a>\n </Button>\n )\n },\n },\n ]\n }, [t])\n\n const openMetadataDialog = React.useCallback((row: AttachmentRow) => {\n setSelectedRow(row)\n setMetadataDialogOpen(true)\n }, [])\n\n const openDeleteDialog = React.useCallback((row: AttachmentRow) => {\n setDeleteTarget(row)\n setDeleteDialogOpen(true)\n }, [])\n\n const handleMetadataSave = React.useCallback(\n async (id: string, payload: AttachmentMetadataSavePayload) => {\n try {\n const body: Record<string, unknown> = {\n tags: payload.tags,\n assignments: payload.assignments,\n }\n if (payload.customFields && Object.keys(payload.customFields).length) {\n body.customFields = payload.customFields\n }\n const call = await apiCall<{ error?: string }>(`/api/attachments/library/${encodeURIComponent(id)}`, {\n method: 'PATCH',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!call.ok) {\n const message =\n call.result?.error || t('attachments.library.metadata.error', 'Failed to update metadata.')\n flash(message, 'error')\n return\n }\n flash(t('attachments.library.metadata.success', 'Attachment updated.'), 'success')\n await queryClient.invalidateQueries({ queryKey: ['attachments-library'], exact: false })\n setMetadataDialogOpen(false)\n } catch (err: any) {\n flash(err?.message || t('attachments.library.metadata.error', 'Failed to update metadata.'), 'error')\n }\n },\n [queryClient, t],\n )\n\n const handleUploadCompleted = React.useCallback(async () => {\n await queryClient.invalidateQueries({ queryKey: ['attachments-library'], exact: false })\n }, [queryClient])\n\n const handleDelete = React.useCallback(async () => {\n if (!deleteTarget) return\n try {\n setDeletePending(true)\n const call = await apiCall<{ error?: string }>(\n `/api/attachments/library/${encodeURIComponent(deleteTarget.id)}`,\n { method: 'DELETE' },\n )\n if (!call.ok) {\n const message =\n call.result?.error || t('attachments.library.errors.delete', 'Failed to delete attachment.')\n flash(message, 'error')\n return\n }\n flash(t('attachments.library.messages.deleted', 'Attachment removed.'), 'success')\n if (selectedRow?.id === deleteTarget.id) {\n setSelectedRow(null)\n setMetadataDialogOpen(false)\n }\n setDeleteDialogOpen(false)\n setDeleteTarget(null)\n await queryClient.invalidateQueries({ queryKey: ['attachments-library'], exact: false })\n } catch (err: any) {\n flash(err?.message || t('attachments.library.errors.delete', 'Failed to delete attachment.'), 'error')\n } finally {\n setDeletePending(false)\n }\n }, [deleteTarget, queryClient, selectedRow, t])\n\n const total = data?.total ?? 0\n const totalPages = data?.totalPages ?? 1\n return (\n <>\n <DataTable<AttachmentRow>\n title={t('attachments.library.title', 'Attachments')}\n refreshButton={{\n label: t('attachments.library.actions.refresh', 'Refresh'),\n onRefresh: () => { void refetch() },\n isRefreshing: isLoading,\n }}\n actions={(\n <Button onClick={() => setUploadDialogOpen(true)}>\n {t('attachments.library.actions.upload', 'Upload')}\n </Button>\n )}\n columns={columns}\n data={items}\n sorting={sorting}\n onSortingChange={setSorting}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'open',\n label: t('attachments.library.actions.open', 'Open'),\n onSelect: () => {\n if (!row.url) return\n window.open(row.url, '_blank', 'noopener,noreferrer')\n },\n },\n {\n id: 'edit',\n label: t('attachments.library.actions.edit', 'Edit metadata'),\n onSelect: () => openMetadataDialog(row),\n },\n {\n id: 'copy-url',\n label: t('attachments.library.actions.copyUrl', 'Copy URL'),\n onSelect: () => {\n if (!row.url) {\n flash(t('attachments.library.actions.copyError', 'Unable to copy link.'), 'error')\n return\n }\n const absolute = resolveAbsoluteUrl(row.url)\n navigator.clipboard\n .writeText(absolute)\n .then(() =>\n flash(\n t('attachments.library.actions.copied', 'Link copied.'),\n 'success',\n ),\n )\n .catch(() =>\n flash(\n t('attachments.library.actions.copyError', 'Unable to copy link.'),\n 'error',\n ),\n )\n },\n },\n {\n id: 'delete',\n label: t('attachments.library.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => openDeleteDialog(row),\n },\n ]}\n />\n )}\n onRowClick={(row) => openMetadataDialog(row)}\n isLoading={isLoading}\n error={error?.message}\n emptyState={\n <div className=\"py-10 text-center text-sm text-muted-foreground\">\n {t('attachments.library.table.empty', 'No attachments found.')}\n </div>\n }\n searchValue={search}\n onSearchChange={(value) => {\n setPage(1)\n setSearch(value)\n }}\n searchPlaceholder={t('attachments.library.table.search', 'Search files\u2026')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(values) => {\n setFilterValues(values)\n setPage(1)\n }}\n onFiltersClear={() => {\n setFilterValues({})\n setPage(1)\n }}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages,\n onPageChange: (next) => setPage(next),\n }}\n />\n <AttachmentMetadataDialog\n open={metadataDialogOpen}\n onOpenChange={setMetadataDialogOpen}\n item={selectedRow}\n availableTags={availableTags}\n onSave={handleMetadataSave}\n />\n <AttachmentDeleteDialog\n open={deleteDialogOpen}\n onOpenChange={setDeleteDialogOpen}\n fileName={deleteTarget?.fileName}\n onConfirm={handleDelete}\n isDeleting={deletePending}\n />\n <AttachmentUploadDialog\n open={uploadDialogOpen}\n onOpenChange={setUploadDialogOpen}\n partitions={partitions}\n availableTags={availableTags}\n onUploaded={handleUploadCompleted}\n />\n </>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\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 { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { CrudForm, type CrudField, type CrudFormGroup, type CrudCustomFieldRenderProps } 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 { z } from 'zod'\nimport { E } from '#generated/entities.ids.generated'\nimport type { LucideIcon } from 'lucide-react'\nimport { Download, Plus, Upload, Trash2, File, FileText, FileSpreadsheet, FileArchive, FileAudio, FileVideo, FileCode } from 'lucide-react'\nimport { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { AttachmentDeleteDialog, AttachmentMetadataDialog, type AttachmentItem, type AttachmentMetadataSavePayload, type AssignmentDraft } from '@open-mercato/ui/backend/detail'\n\ntype AttachmentAssignment = {\n type: string\n id: string\n href?: string | null\n label?: string | null\n}\n\ntype AttachmentRow = AttachmentItem\n\ntype AttachmentLibraryResponse = {\n items: AttachmentRow[]\n page: number\n pageSize: number\n total: number\n totalPages: number\n availableTags: string[]\n partitions: Array<{ code: string; title: string; description?: string | null; isPublic?: boolean }>\n error?: string\n}\n\nconst PAGE_SIZE = 25\nconst ENV_APP_URL = (process.env.NEXT_PUBLIC_APP_URL || '').replace(/\\/$/, '')\nconst LIBRARY_ENTITY_ID = 'attachments:library'\n\nfunction filterLibraryAssignments(assignments?: AttachmentAssignment[] | null): AttachmentAssignment[] {\n return (assignments ?? []).filter((assignment) => assignment.type !== LIBRARY_ENTITY_ID)\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\nfunction humanDate(value: string, locale?: string): string {\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return value\n return date.toLocaleString(locale ?? undefined)\n}\n\nfunction buildFilterSignature(values: FilterValues): string {\n return JSON.stringify(values, Object.keys(values).sort((a, b) => a.localeCompare(b)))\n}\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 resolveFileExtension(fileName?: string | null): string {\n if (!fileName) return ''\n const normalized = fileName.trim()\n if (!normalized) return ''\n const lastDot = normalized.lastIndexOf('.')\n if (lastDot === -1 || lastDot === normalized.length - 1) return ''\n return normalized.slice(lastDot + 1).toLowerCase()\n}\n\nconst EXTENSION_ICON_MAP: Record<string, LucideIcon> = {\n pdf: FileText,\n doc: FileText,\n docx: FileText,\n txt: FileText,\n md: FileText,\n rtf: FileText,\n xls: FileSpreadsheet,\n xlsx: FileSpreadsheet,\n csv: FileSpreadsheet,\n ods: FileSpreadsheet,\n ppt: FileText,\n pptx: FileText,\n zip: FileArchive,\n gz: FileArchive,\n rar: FileArchive,\n tgz: FileArchive,\n '7z': FileArchive,\n tar: FileArchive,\n json: FileCode,\n js: FileCode,\n ts: FileCode,\n jsx: FileCode,\n tsx: FileCode,\n html: FileCode,\n css: FileCode,\n xml: FileCode,\n yaml: FileCode,\n yml: FileCode,\n mp3: FileAudio,\n wav: FileAudio,\n flac: FileAudio,\n ogg: FileAudio,\n mp4: FileVideo,\n mov: FileVideo,\n avi: FileVideo,\n webm: FileVideo,\n}\n\nconst MIME_FALLBACK_ICONS: Record<string, LucideIcon> = {\n audio: FileAudio,\n video: FileVideo,\n text: FileText,\n application: FileText,\n}\n\nfunction resolveAttachmentPlaceholder(mimeType?: string | null, fileName?: string | null): { icon: LucideIcon; label: string } {\n const extension = resolveFileExtension(fileName)\n const normalizedMime = typeof mimeType === 'string' ? mimeType.toLowerCase() : ''\n if (extension && EXTENSION_ICON_MAP[extension]) {\n return { icon: EXTENSION_ICON_MAP[extension], label: extension.toUpperCase() }\n }\n if (!extension && normalizedMime.includes('pdf')) {\n return { icon: FileText, label: 'PDF' }\n }\n if (!extension && normalizedMime.includes('zip')) {\n return { icon: FileArchive, label: 'ZIP' }\n }\n if (!extension && normalizedMime.includes('json')) {\n return { icon: FileCode, label: 'JSON' }\n }\n const mimeRoot = normalizedMime.split('/')[0] || ''\n if (mimeRoot && MIME_FALLBACK_ICONS[mimeRoot]) {\n return { icon: MIME_FALLBACK_ICONS[mimeRoot], label: mimeRoot.toUpperCase() }\n }\n const fallbackSource = extension || mimeRoot || 'file'\n const fallbackLabel = fallbackSource.slice(0, 6).toUpperCase()\n return { icon: File, label: fallbackLabel }\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\n\ntype AttachmentUploadFormValues = {\n files: File[]\n partitionCode?: string\n tags?: string[]\n assignments?: AssignmentDraft[]\n} & Record<string, unknown>\n\ntype AssignmentsEditorProps = {\n value: AssignmentDraft[]\n onChange: (next: AssignmentDraft[]) => void\n labels: {\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 disabled?: boolean\n}\n\nfunction AttachmentAssignmentsEditor({ value, onChange, labels, disabled }: AssignmentsEditorProps) {\n const handleChange = React.useCallback(\n (index: number, patch: Partial<AssignmentDraft>) => {\n onChange(value.map((entry, idx) => (idx === index ? { ...entry, ...patch } : entry)))\n },\n [onChange, value],\n )\n\n const handleRemove = React.useCallback(\n (index: number) => {\n onChange(value.filter((_, idx) => idx !== index))\n },\n [onChange, value],\n )\n\n const handleAdd = React.useCallback(() => {\n onChange([...value, { type: '', id: '', href: '', label: '' }])\n }, [onChange, value])\n\n return (\n <div className=\"space-y-2\">\n <div>\n <div className=\"text-sm font-medium\">{labels.title}</div>\n <div className=\"text-xs text-muted-foreground\">{labels.description}</div>\n </div>\n <div className=\"space-y-3\">\n {value.length === 0 ? (\n <div className=\"text-xs text-muted-foreground\">No assignments yet.</div>\n ) : (\n value.map((entry, index) => (\n <div key={`${index}-${entry.type}-${entry.id}`} className=\"rounded border p-3 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\">{labels.type}</label>\n <input\n className=\"w-full rounded border px-2 py-1 text-sm\"\n value={entry.type}\n disabled={disabled}\n onChange={(event) => handleChange(index, { type: event.target.value })}\n />\n </div>\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium\">{labels.id}</label>\n <input\n className=\"w-full rounded border px-2 py-1 text-sm\"\n value={entry.id}\n disabled={disabled}\n onChange={(event) => handleChange(index, { id: event.target.value })}\n />\n </div>\n </div>\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium\">{labels.href}</label>\n <input\n className=\"w-full rounded border px-2 py-1 text-sm\"\n value={entry.href ?? ''}\n disabled={disabled}\n onChange={(event) => handleChange(index, { href: event.target.value })}\n />\n </div>\n {labels.label ? (\n <div className=\"space-y-1\">\n <label className=\"text-xs font-medium\">{labels.label}</label>\n <input\n className=\"w-full rounded border px-2 py-1 text-sm\"\n value={entry.label ?? ''}\n disabled={disabled}\n onChange={(event) => handleChange(index, { label: event.target.value })}\n />\n </div>\n ) : null}\n </div>\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n disabled={disabled}\n onClick={() => handleRemove(index)}\n className=\"inline-flex items-center gap-1 text-muted-foreground\"\n >\n <Trash2 className=\"h-4 w-4\" />\n {labels.remove}\n </Button>\n </div>\n </div>\n ))\n )}\n </div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={disabled} onClick={handleAdd} className=\"inline-flex items-center gap-1\">\n <Plus className=\"h-4 w-4\" />\n {labels.add}\n </Button>\n </div>\n )\n}\n\ntype AttachmentFilesFieldProps = CrudCustomFieldRenderProps & {\n labels: {\n dropHint: string\n choose: string\n uploading: string\n empty: string\n }\n uploading: boolean\n}\n\nfunction AttachmentFilesField({\n value,\n setValue,\n disabled,\n error,\n labels,\n uploading,\n}: AttachmentFilesFieldProps) {\n const files = React.useMemo(() => (Array.isArray(value) ? (value as File[]) : []), [value])\n const [isDragOver, setDragOver] = React.useState(false)\n const fileInputRef = React.useRef<HTMLInputElement | null>(null)\n\n const acceptFiles = React.useCallback(\n (list: FileList | null) => {\n if (!list?.length) return\n const dedupe = new Map<string, File>(files.map((file) => [`${file.name}:${file.size}`, file]))\n Array.from(list).forEach((file) => {\n dedupe.set(`${file.name}:${file.size}`, file)\n })\n setValue(Array.from(dedupe.values()))\n },\n [files, setValue],\n )\n\n const handleDrop = React.useCallback(\n (event: React.DragEvent<HTMLDivElement>) => {\n if (disabled || uploading) return\n event.preventDefault()\n event.stopPropagation()\n setDragOver(false)\n acceptFiles(event.dataTransfer?.files ?? null)\n },\n [acceptFiles, disabled, uploading],\n )\n\n const handleDragOver = React.useCallback(\n (event: React.DragEvent<HTMLDivElement>) => {\n if (disabled || uploading) return\n event.preventDefault()\n event.stopPropagation()\n setDragOver(true)\n },\n [disabled, uploading],\n )\n\n const handleDragLeave = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(false)\n }, [])\n\n const removeFile = React.useCallback(\n (name: string, size: number) => {\n if (disabled || uploading) return\n setValue(files.filter((file) => !(file.name === name && file.size === size)))\n },\n [disabled, files, setValue, uploading],\n )\n\n const pickFiles = React.useCallback(() => {\n if (disabled || uploading) return\n fileInputRef.current?.click()\n }, [disabled, uploading])\n\n const renderFileList = () => {\n if (!files.length) {\n return <p className=\"text-xs text-muted-foreground\">{labels.empty}</p>\n }\n return (\n <div className=\"space-y-2\">\n {files.map((candidate) => (\n <div key={`${candidate.name}-${candidate.size}`} className=\"flex items-center justify-between rounded border px-3 py-2 text-sm\">\n <div>\n <div className=\"font-medium\">{candidate.name}</div>\n <div className=\"text-xs text-muted-foreground\">{formatFileSize(candidate.size)}</div>\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => removeFile(candidate.name, candidate.size)}\n disabled={disabled || uploading}\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n ))}\n </div>\n )\n }\n\n return (\n <div className=\"space-y-2\">\n <div\n className={cn(\n 'flex flex-col items-center justify-center rounded-lg border border-dashed p-6 text-center transition-colors',\n isDragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/30',\n disabled || uploading ? 'opacity-70' : '',\n )}\n onDrop={handleDrop}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n role=\"presentation\"\n >\n <Upload className=\"mx-auto h-6 w-6 text-muted-foreground\" />\n <p className=\"mt-2 text-sm text-muted-foreground\">{labels.dropHint}</p>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={pickFiles} disabled={disabled || uploading}>\n {uploading ? labels.uploading : labels.choose}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n className=\"hidden\"\n multiple\n onChange={(event) => {\n acceptFiles(event.target.files)\n event.currentTarget.value = ''\n }}\n disabled={disabled || uploading}\n />\n </div>\n {renderFileList()}\n {error ? <p className=\"text-xs font-medium text-red-600\">{error}</p> : null}\n </div>\n )\n}\n\ntype AttachmentUploadFormProps = {\n partitions: Array<{ code: string; title: string }>\n availableTags: string[]\n onUploaded: () => void\n onCancel: () => void\n}\n\nfunction AttachmentUploadForm({ partitions, availableTags, onUploaded, onCancel }: AttachmentUploadFormProps) {\n const t = useT()\n const [isUploading, setIsUploading] = React.useState(false)\n const [uploadProgress, setUploadProgress] = React.useState<{ completed: number; total: number }>({ completed: 0, total: 0 })\n\n const partitionOptions = React.useMemo(\n () =>\n partitions.map((entry) => ({\n value: entry.code,\n label: entry.title || entry.code,\n })),\n [partitions],\n )\n\n const assignmentLabels = React.useMemo(\n () => ({\n title: t('attachments.library.upload.assignments.title', 'Assignments'),\n description: t(\n 'attachments.library.upload.assignments.description',\n 'Optionally link this file to existing records now or add them later.',\n ),\n type: t('attachments.library.upload.assignments.type', 'Type'),\n id: t('attachments.library.upload.assignments.id', 'Record ID'),\n href: t('attachments.library.upload.assignments.href', 'Link'),\n label: t('attachments.library.upload.assignments.label', 'Label'),\n add: t('attachments.library.upload.assignments.add', 'Add assignment'),\n remove: t('attachments.library.upload.assignments.remove', 'Remove'),\n }),\n [t],\n )\n\n const formSchema = React.useMemo(\n () =>\n z\n .object({\n files: z.array(z.any()).min(1, { message: t('attachments.library.upload.fileRequired', 'Select at least one file to upload.') }),\n partitionCode: z.string().optional(),\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 [t],\n )\n\n const fields = React.useMemo<CrudField[]>(() => {\n return [\n {\n id: 'files',\n label: t('attachments.library.upload.file', 'Files'),\n type: 'custom',\n component: (props) => (\n <AttachmentFilesField\n {...props}\n uploading={isUploading}\n labels={{\n dropHint: t('attachments.library.upload.dropHint', 'Drag and drop files here or click to upload.'),\n choose: t('attachments.library.upload.choose', 'Choose files'),\n uploading: t('attachments.library.upload.submitting', 'Uploading\u2026'),\n empty: t('attachments.library.upload.noFiles', 'No files selected yet.'),\n }}\n />\n ),\n },\n {\n id: 'partitionCode',\n label: t('attachments.library.upload.partition', 'Partition'),\n type: 'select',\n options: [\n { value: '', label: t('attachments.library.upload.partitionDefault', 'Default (private)') },\n ...partitionOptions,\n ],\n },\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.upload.tagsPlaceholder', 'Add tags')}\n disabled={Boolean(disabled) || isUploading}\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) || isUploading}\n />\n ),\n },\n ]\n }, [assignmentLabels, availableTags, isUploading, partitionOptions, t])\n\n const groups = React.useMemo<CrudFormGroup[]>(() => {\n return [\n {\n id: 'details',\n title: t('attachments.library.upload.title', 'Upload attachment'),\n column: 1,\n fields: ['files', 'partitionCode', '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 uploadPercentage = uploadProgress.total\n ? Math.min(100, Math.round((uploadProgress.completed / uploadProgress.total) * 100))\n : 0\n\n const handleSubmit = React.useCallback(\n async (values: AttachmentUploadFormValues) => {\n const files = Array.isArray(values.files) ? values.files : []\n if (!files.length) {\n throw new Error(t('attachments.library.upload.fileRequired', 'Select at least one file to upload.'))\n }\n setUploadProgress({ completed: 0, total: files.length })\n setIsUploading(true)\n try {\n const tags = Array.isArray(values.tags)\n ? values.tags\n .map((tag) => (typeof tag === 'string' ? tag.trim() : ''))\n .filter((tag) => tag.length > 0)\n : []\n const cleanedAssignments =\n Array.isArray(values.assignments) && values.assignments.length\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 let completed = 0\n for (const file of files) {\n const fd = new FormData()\n fd.set('entityId', LIBRARY_ENTITY_ID)\n fd.set(\n 'recordId',\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : String(Date.now()),\n )\n fd.set('file', file)\n if (typeof values.partitionCode === 'string' && values.partitionCode.trim().length) {\n fd.set('partitionCode', values.partitionCode.trim())\n }\n if (tags.length) fd.set('tags', JSON.stringify(tags))\n if (cleanedAssignments.length) fd.set('assignments', JSON.stringify(cleanedAssignments))\n if (Object.keys(customFields).length) fd.set('customFields', JSON.stringify(customFields))\n const call = await apiCall<{ error?: string }>('/api/attachments', {\n method: 'POST',\n body: fd,\n })\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.upload.failed', 'Upload failed.')\n throw new Error(message)\n }\n completed += 1\n setUploadProgress({ completed, total: files.length })\n }\n flash(t('attachments.library.upload.success', 'Attachment uploaded.'), 'success')\n onUploaded()\n onCancel()\n } catch (err: any) {\n const message = err?.message || t('attachments.library.upload.failed', 'Upload failed.')\n flash(message, 'error')\n throw new Error(message)\n } finally {\n setIsUploading(false)\n }\n },\n [onCancel, onUploaded, t],\n )\n\n return (\n <div className=\"relative\">\n <CrudForm<AttachmentUploadFormValues>\n embedded\n schema={formSchema}\n entityId={E.attachments.attachment}\n fields={fields}\n groups={groups}\n initialValues={{ files: [], tags: [], assignments: [], partitionCode: '' }}\n submitLabel={\n isUploading\n ? t('attachments.library.upload.submitting', 'Uploading\u2026')\n : t('attachments.library.upload.submit', 'Upload')\n }\n extraActions={\n <Button type=\"button\" variant=\"outline\" onClick={onCancel} disabled={isUploading}>\n {t('attachments.library.upload.cancel', 'Cancel')}\n </Button>\n }\n onSubmit={handleSubmit}\n />\n {isUploading ? (\n <div className=\"pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-background/90 px-6 text-center backdrop-blur\">\n <div className=\"flex w-full max-w-sm flex-col items-center gap-4 rounded-xl border border-border/50 bg-card/95 px-6 py-8 shadow-2xl\">\n <Spinner size=\"lg\" className=\"border-primary/50 border-t-primary\" />\n <div className=\"w-full space-y-3\">\n <p className=\"text-base font-semibold\">\n {t('attachments.library.upload.progressLabel', 'Uploading files')}\n </p>\n {uploadProgress.total > 0 ? (\n <>\n <p className=\"text-sm text-muted-foreground\">\n {uploadProgress.completed}/{uploadProgress.total}\n </p>\n <div className=\"h-2 w-full rounded bg-muted\">\n <div\n className=\"h-2 rounded bg-primary transition-all\"\n style={{\n width: `${uploadPercentage}%`,\n }}\n />\n </div>\n </>\n ) : null}\n </div>\n </div>\n </div>\n ) : null}\n </div>\n )\n}\ntype UploadDialogProps = {\n open: boolean\n onOpenChange: (next: boolean) => void\n partitions: Array<{ code: string; title: string }>\n availableTags: string[]\n onUploaded: () => void\n}\n\nfunction AttachmentUploadDialog({ open, onOpenChange, partitions, availableTags, onUploaded }: UploadDialogProps) {\n const t = useT()\n const [formResetKey, setFormResetKey] = React.useState(0)\n const previousOpen = React.useRef(open)\n\n React.useEffect(() => {\n if (previousOpen.current && !open) {\n setFormResetKey((prev) => prev + 1)\n }\n previousOpen.current = open\n }, [open])\n\n const handleDialogChange = React.useCallback(\n (next: boolean) => {\n onOpenChange(next)\n },\n [onOpenChange],\n )\n\n const handleUploaded = React.useCallback(() => {\n onUploaded()\n }, [onUploaded])\n\n return (\n <Dialog open={open} onOpenChange={handleDialogChange}>\n <DialogContent className=\"sm:max-w-[54.6rem]\">\n <DialogHeader>\n <DialogTitle>{t('attachments.library.upload.title', 'Upload attachment')}</DialogTitle>\n </DialogHeader>\n <AttachmentUploadForm\n key={formResetKey}\n partitions={partitions}\n availableTags={availableTags}\n onUploaded={handleUploaded}\n onCancel={() => handleDialogChange(false)}\n />\n </DialogContent>\n </Dialog>\n )\n}\n\nexport function AttachmentLibrary() {\n const t = useT()\n const queryClient = useQueryClient()\n const [page, setPage] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'createdAt', desc: true }])\n const [metadataDialogOpen, setMetadataDialogOpen] = React.useState(false)\n const [selectedRow, setSelectedRow] = React.useState<AttachmentRow | null>(null)\n const [uploadDialogOpen, setUploadDialogOpen] = React.useState(false)\n const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)\n const [deleteTarget, setDeleteTarget] = React.useState<AttachmentRow | null>(null)\n const [deletePending, setDeletePending] = React.useState(false)\n const filterSignature = React.useMemo(() => buildFilterSignature(filterValues), [filterValues])\n const sortingSignature = React.useMemo(() => JSON.stringify(sorting), [sorting])\n\n const { data, isLoading, error, refetch } = useQuery({\n queryKey: ['attachments-library', page, search, filterSignature, sortingSignature],\n queryFn: async () => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(PAGE_SIZE))\n if (search.trim().length > 0) params.set('search', search.trim())\n const partition = typeof filterValues.partition === 'string' ? filterValues.partition : ''\n if (partition) params.set('partition', partition)\n const tags = Array.isArray(filterValues.tags) ? filterValues.tags : []\n if (tags.length > 0) params.set('tags', tags.join(','))\n if (sorting.length > 0) {\n const primary = sorting[0]\n params.set('sortField', primary.id)\n params.set('sortDir', primary.desc ? 'desc' : 'asc')\n }\n const call = await apiCall<AttachmentLibraryResponse>(`/api/attachments/library?${params.toString()}`)\n if (!call.ok || !call.result) {\n const message = call.result?.error || t('attachments.library.errors.load', 'Failed to load attachments.')\n throw new Error(message)\n }\n return call.result\n },\n })\n\n const partitions = data?.partitions ?? []\n const availableTags = data?.availableTags ?? []\n\n const filters = React.useMemo<FilterDef[]>(() => {\n const partitionOptions = partitions.map((entry) => ({\n value: entry.code,\n label: entry.title || entry.code,\n }))\n return [\n {\n id: 'partition',\n label: t('attachments.library.filters.partition', 'Partition'),\n type: 'select',\n options: partitionOptions,\n },\n {\n id: 'tags',\n label: t('attachments.library.filters.tags', 'Tags'),\n type: 'tags',\n placeholder: t('attachments.library.filters.tagsPlaceholder', 'Filter by tag'),\n options: availableTags.map((tag) => ({ value: tag, label: tag })),\n },\n ]\n }, [availableTags, partitions, t])\n\n const items = data?.items ?? []\n\n const columns = React.useMemo<ColumnDef<AttachmentRow>[]>(() => {\n return [\n {\n id: 'preview',\n header: '',\n enableSorting: false,\n cell: ({ row }) => {\n const value = row.original\n if (value.thumbnailUrl) {\n return (\n <div className=\"h-16 w-16 overflow-hidden rounded border bg-muted\">\n <img\n src={value.thumbnailUrl}\n alt={value.fileName}\n className=\"h-full w-full object-cover\"\n loading=\"lazy\"\n />\n </div>\n )\n }\n const placeholder = resolveAttachmentPlaceholder(value.mimeType, value.fileName)\n const PlaceholderIcon = placeholder.icon\n return (\n <div className=\"flex h-16 w-16 flex-col items-center justify-center rounded border bg-muted text-[10px] font-semibold uppercase text-muted-foreground\">\n <PlaceholderIcon className=\"mb-1 h-5 w-5 text-muted-foreground\" aria-hidden />\n {placeholder.label}\n </div>\n )\n },\n },\n {\n id: 'fileName',\n accessorKey: 'fileName',\n header: t('attachments.library.table.file', 'File'),\n cell: ({ row }) => {\n const value = row.original\n return (\n <div className=\"space-y-1 min-w-0 max-w-[280px]\">\n <div className=\"font-medium truncate\" title={value.fileName}>\n {value.fileName}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {formatFileSize(value.fileSize)} \u2022 {value.mimeType || 'application/octet-stream'}\n </div>\n <div className=\"text-xs text-muted-foreground line-clamp-2\">\n {value.content?.trim()\n ? value.content\n : t('attachments.library.metadata.noContent', 'No text extracted')}\n </div>\n </div>\n )\n },\n },\n {\n id: 'tags',\n accessorKey: 'tags',\n header: t('attachments.library.table.tags', 'Tags'),\n enableSorting: false,\n cell: ({ row }) => {\n const tags = row.original.tags ?? []\n if (!tags.length) return <span className=\"text-xs text-muted-foreground\">\u2014</span>\n return (\n <div className=\"flex flex-wrap gap-1\">\n {tags.map((tag) => (\n <Badge key={tag} variant=\"outline\">\n {tag}\n </Badge>\n ))}\n </div>\n )\n },\n },\n {\n id: 'assignments',\n accessorKey: 'assignments',\n header: t('attachments.library.table.assignments', 'Assignments'),\n enableSorting: false,\n cell: ({ row }) => {\n const assignments = filterLibraryAssignments(row.original.assignments)\n if (!assignments.length) return <span className=\"text-xs text-muted-foreground\">\u2014</span>\n return (\n <div className=\"flex flex-col gap-1\">\n {assignments.map((assignment) => {\n const label = assignment.label?.trim() || assignment.id\n const hideType =\n assignment.type === (E as any).catalog?.catalog_product ||\n assignment.type === (E as any).catalog?.catalog_product_variant\n const content = hideType ? label : `${assignment.type}: ${label}`\n return assignment.href ? (\n <a\n key={`${assignment.type}-${assignment.id}-${assignment.href}`}\n href={assignment.href}\n className=\"text-sm text-blue-600 underline\"\n target=\"_blank\"\n rel=\"noreferrer\"\n >\n {content}\n </a>\n ) : (\n <div key={`${assignment.type}-${assignment.id}`} className=\"text-sm\">\n {content}\n </div>\n )\n })}\n </div>\n )\n },\n },\n {\n id: 'partitionCode',\n accessorKey: 'partitionCode',\n header: t('attachments.library.table.partition', 'Partition'),\n cell: ({ row }) => (\n <div className=\"text-sm text-muted-foreground\">\n {row.original.partitionTitle ?? row.original.partitionCode}\n </div>\n ),\n },\n {\n id: 'createdAt',\n accessorKey: 'createdAt',\n header: t('attachments.library.table.created', 'Created'),\n cell: ({ row }) => {\n const createdAt = row.original.createdAt\n return (\n <div className=\"text-sm text-muted-foreground\">\n {createdAt ? humanDate(createdAt) : '\u2014'}\n </div>\n )\n },\n },\n {\n id: 'download',\n header: t('attachments.library.table.download', 'Download'),\n enableSorting: false,\n cell: ({ row }) => {\n const downloadPath = buildAttachmentFileUrl(row.original.id, { download: true })\n const absolute = resolveAbsoluteUrl(downloadPath)\n return (\n <Button variant=\"ghost\" size=\"icon\" asChild>\n <a href={absolute} download aria-label={t('attachments.library.table.download', 'Download')}>\n <Download className=\"h-4 w-4\" />\n </a>\n </Button>\n )\n },\n },\n ]\n }, [t])\n\n const openMetadataDialog = React.useCallback((row: AttachmentRow) => {\n setSelectedRow(row)\n setMetadataDialogOpen(true)\n }, [])\n\n const openDeleteDialog = React.useCallback((row: AttachmentRow) => {\n setDeleteTarget(row)\n setDeleteDialogOpen(true)\n }, [])\n\n const handleMetadataSave = React.useCallback(\n async (id: string, payload: AttachmentMetadataSavePayload) => {\n try {\n const body: Record<string, unknown> = {\n tags: payload.tags,\n assignments: payload.assignments,\n }\n if (payload.customFields && Object.keys(payload.customFields).length) {\n body.customFields = payload.customFields\n }\n const call = await apiCall<{ error?: string }>(`/api/attachments/library/${encodeURIComponent(id)}`, {\n method: 'PATCH',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!call.ok) {\n const message =\n call.result?.error || t('attachments.library.metadata.error', 'Failed to update metadata.')\n flash(message, 'error')\n return\n }\n flash(t('attachments.library.metadata.success', 'Attachment updated.'), 'success')\n await queryClient.invalidateQueries({ queryKey: ['attachments-library'], exact: false })\n setMetadataDialogOpen(false)\n } catch (err: any) {\n flash(err?.message || t('attachments.library.metadata.error', 'Failed to update metadata.'), 'error')\n }\n },\n [queryClient, t],\n )\n\n const handleUploadCompleted = React.useCallback(async () => {\n await queryClient.invalidateQueries({ queryKey: ['attachments-library'], exact: false })\n }, [queryClient])\n\n const handleDelete = React.useCallback(async () => {\n if (!deleteTarget) return\n try {\n setDeletePending(true)\n const call = await apiCall<{ error?: string }>(\n `/api/attachments/library/${encodeURIComponent(deleteTarget.id)}`,\n { method: 'DELETE' },\n )\n if (!call.ok) {\n const message =\n call.result?.error || t('attachments.library.errors.delete', 'Failed to delete attachment.')\n flash(message, 'error')\n return\n }\n flash(t('attachments.library.messages.deleted', 'Attachment removed.'), 'success')\n if (selectedRow?.id === deleteTarget.id) {\n setSelectedRow(null)\n setMetadataDialogOpen(false)\n }\n setDeleteDialogOpen(false)\n setDeleteTarget(null)\n await queryClient.invalidateQueries({ queryKey: ['attachments-library'], exact: false })\n } catch (err: any) {\n flash(err?.message || t('attachments.library.errors.delete', 'Failed to delete attachment.'), 'error')\n } finally {\n setDeletePending(false)\n }\n }, [deleteTarget, queryClient, selectedRow, t])\n\n const total = data?.total ?? 0\n const totalPages = data?.totalPages ?? 0\n return (\n <>\n <DataTable<AttachmentRow>\n title={t('attachments.library.title', 'Attachments')}\n refreshButton={{\n label: t('attachments.library.actions.refresh', 'Refresh'),\n onRefresh: () => { void refetch() },\n isRefreshing: isLoading,\n }}\n actions={(\n <Button onClick={() => setUploadDialogOpen(true)}>\n {t('attachments.library.actions.upload', 'Upload')}\n </Button>\n )}\n columns={columns}\n data={items}\n sorting={sorting}\n onSortingChange={setSorting}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'open',\n label: t('attachments.library.actions.open', 'Open'),\n onSelect: () => {\n if (!row.url) return\n window.open(row.url, '_blank', 'noopener,noreferrer')\n },\n },\n {\n id: 'edit',\n label: t('attachments.library.actions.edit', 'Edit metadata'),\n onSelect: () => openMetadataDialog(row),\n },\n {\n id: 'copy-url',\n label: t('attachments.library.actions.copyUrl', 'Copy URL'),\n onSelect: () => {\n if (!row.url) {\n flash(t('attachments.library.actions.copyError', 'Unable to copy link.'), 'error')\n return\n }\n const absolute = resolveAbsoluteUrl(row.url)\n navigator.clipboard\n .writeText(absolute)\n .then(() =>\n flash(\n t('attachments.library.actions.copied', 'Link copied.'),\n 'success',\n ),\n )\n .catch(() =>\n flash(\n t('attachments.library.actions.copyError', 'Unable to copy link.'),\n 'error',\n ),\n )\n },\n },\n {\n id: 'delete',\n label: t('attachments.library.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => openDeleteDialog(row),\n },\n ]}\n />\n )}\n onRowClick={(row) => openMetadataDialog(row)}\n isLoading={isLoading}\n error={error?.message}\n emptyState={\n <div className=\"py-10 text-center text-sm text-muted-foreground\">\n {t('attachments.library.table.empty', 'No attachments found.')}\n </div>\n }\n searchValue={search}\n onSearchChange={(value) => {\n setPage(1)\n setSearch(value)\n }}\n searchPlaceholder={t('attachments.library.table.search', 'Search files\u2026')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(values) => {\n setFilterValues(values)\n setPage(1)\n }}\n onFiltersClear={() => {\n setFilterValues({})\n setPage(1)\n }}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages,\n onPageChange: (next) => setPage(next),\n }}\n />\n <AttachmentMetadataDialog\n open={metadataDialogOpen}\n onOpenChange={setMetadataDialogOpen}\n item={selectedRow}\n availableTags={availableTags}\n onSave={handleMetadataSave}\n />\n <AttachmentDeleteDialog\n open={deleteDialogOpen}\n onOpenChange={setDeleteDialogOpen}\n fileName={deleteTarget?.fileName}\n onConfirm={handleDelete}\n isDeleting={deletePending}\n />\n <AttachmentUploadDialog\n open={uploadDialogOpen}\n onOpenChange={setUploadDialogOpen}\n partitions={partitions}\n availableTags={availableTags}\n onUploaded={handleUploadCompleted}\n />\n </>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -79,7 +79,7 @@ function CategoriesDataTable() {
|
|
|
79
79
|
});
|
|
80
80
|
const rows = data?.items ?? [];
|
|
81
81
|
const total = data?.total ?? 0;
|
|
82
|
-
const totalPages = data?.totalPages ??
|
|
82
|
+
const totalPages = data?.totalPages ?? 0;
|
|
83
83
|
const columns = React.useMemo(() => [
|
|
84
84
|
{
|
|
85
85
|
accessorKey: "name",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/catalog/components/categories/CategoriesDataTable.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { apiCall, apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { formatCategoryTreeLabel } from '../../lib/categoryTree'\n\ntype CategoryRow = {\n id: string\n name: string\n slug: string | null\n description: string | null\n parentId: string | null\n parentName: string | null\n depth: number\n treePath: string\n pathLabel: string\n childCount: number\n descendantCount: number\n isActive: boolean\n}\n\ntype CategoriesResponse = {\n items: CategoryRow[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\nconst PAGE_SIZE = 50\nconst TREE_BASE_INDENT = 18\nconst TREE_STEP_INDENT = 14\n\nfunction computeIndent(depth: number): number {\n if (depth <= 0) return 0\n return TREE_BASE_INDENT + (depth - 1) * TREE_STEP_INDENT\n}\n\nexport default function CategoriesDataTable() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const queryClient = useQueryClient()\n const scopeVersion = useOrganizationScopeVersion()\n const [page, setPage] = React.useState(1)\n const [status, setStatus] = React.useState<'all' | 'active' | 'inactive'>('all')\n const [search, setSearch] = React.useState('')\n const [canManage, setCanManage] = React.useState(false)\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n try {\n const call = await apiCall<{ granted?: string[]; ok?: boolean }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['catalog.categories.manage'] }),\n })\n if (!cancelled) {\n const granted = Array.isArray(call.result?.granted) ? call.result.granted : []\n setCanManage(call.result?.ok === true || granted.includes('catalog.categories.manage'))\n }\n } catch {\n if (!cancelled) setCanManage(false)\n }\n }\n load()\n return () => {\n cancelled = true\n }\n }, [])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('view', 'manage')\n params.set('page', String(page))\n params.set('pageSize', String(PAGE_SIZE))\n params.set('status', status)\n if (search) params.set('search', search)\n return params.toString()\n }, [page, status, search])\n\n const { data, isLoading } = useQuery<CategoriesResponse>({\n queryKey: ['catalog-categories', queryParams, scopeVersion],\n queryFn: async () => {\n const payload = await readApiResultOrThrow<CategoriesResponse>(\n `/api/catalog/categories?${queryParams}`,\n undefined,\n { errorMessage: t('catalog.categories.list.error.load', 'Failed to load categories') },\n )\n return {\n items: Array.isArray(payload.items) ? payload.items : [],\n total: typeof payload.total === 'number' ? payload.total : 0,\n page: typeof payload.page === 'number' ? payload.page : 1,\n pageSize: typeof payload.pageSize === 'number' ? payload.pageSize : PAGE_SIZE,\n totalPages: typeof payload.totalPages === 'number' ? payload.totalPages : 1,\n }\n },\n })\n\n const rows = data?.items ?? []\n const total = data?.total ?? 0\n const totalPages = data?.totalPages ??
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { apiCall, apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { formatCategoryTreeLabel } from '../../lib/categoryTree'\n\ntype CategoryRow = {\n id: string\n name: string\n slug: string | null\n description: string | null\n parentId: string | null\n parentName: string | null\n depth: number\n treePath: string\n pathLabel: string\n childCount: number\n descendantCount: number\n isActive: boolean\n}\n\ntype CategoriesResponse = {\n items: CategoryRow[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\nconst PAGE_SIZE = 50\nconst TREE_BASE_INDENT = 18\nconst TREE_STEP_INDENT = 14\n\nfunction computeIndent(depth: number): number {\n if (depth <= 0) return 0\n return TREE_BASE_INDENT + (depth - 1) * TREE_STEP_INDENT\n}\n\nexport default function CategoriesDataTable() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const queryClient = useQueryClient()\n const scopeVersion = useOrganizationScopeVersion()\n const [page, setPage] = React.useState(1)\n const [status, setStatus] = React.useState<'all' | 'active' | 'inactive'>('all')\n const [search, setSearch] = React.useState('')\n const [canManage, setCanManage] = React.useState(false)\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n try {\n const call = await apiCall<{ granted?: string[]; ok?: boolean }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['catalog.categories.manage'] }),\n })\n if (!cancelled) {\n const granted = Array.isArray(call.result?.granted) ? call.result.granted : []\n setCanManage(call.result?.ok === true || granted.includes('catalog.categories.manage'))\n }\n } catch {\n if (!cancelled) setCanManage(false)\n }\n }\n load()\n return () => {\n cancelled = true\n }\n }, [])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('view', 'manage')\n params.set('page', String(page))\n params.set('pageSize', String(PAGE_SIZE))\n params.set('status', status)\n if (search) params.set('search', search)\n return params.toString()\n }, [page, status, search])\n\n const { data, isLoading } = useQuery<CategoriesResponse>({\n queryKey: ['catalog-categories', queryParams, scopeVersion],\n queryFn: async () => {\n const payload = await readApiResultOrThrow<CategoriesResponse>(\n `/api/catalog/categories?${queryParams}`,\n undefined,\n { errorMessage: t('catalog.categories.list.error.load', 'Failed to load categories') },\n )\n return {\n items: Array.isArray(payload.items) ? payload.items : [],\n total: typeof payload.total === 'number' ? payload.total : 0,\n page: typeof payload.page === 'number' ? payload.page : 1,\n pageSize: typeof payload.pageSize === 'number' ? payload.pageSize : PAGE_SIZE,\n totalPages: typeof payload.totalPages === 'number' ? payload.totalPages : 1,\n }\n },\n })\n\n const rows = data?.items ?? []\n const total = data?.total ?? 0\n const totalPages = data?.totalPages ?? 0\n\n const columns = React.useMemo<ColumnDef<CategoryRow>[]>(() => [\n {\n accessorKey: 'name',\n header: t('catalog.categories.list.columns.category', 'Category'),\n meta: { priority: 1 },\n cell: ({ row }) => {\n const depth = row.original.depth ?? 0\n return (\n <div className=\"flex items-center text-sm font-medium leading-none text-foreground\">\n <span style={{ marginLeft: computeIndent(depth), whiteSpace: 'pre' }}>\n {formatCategoryTreeLabel(row.original.name, depth)}\n </span>\n </div>\n )\n },\n },\n {\n accessorKey: 'pathLabel',\n header: t('catalog.categories.list.columns.path', 'Path'),\n meta: { priority: 3 },\n cell: ({ getValue }) => {\n const value = getValue<string>()\n return <span className=\"text-xs text-muted-foreground\">{value || '\u2014'}</span>\n },\n },\n {\n accessorKey: 'parentName',\n header: t('catalog.categories.list.columns.parent', 'Parent'),\n meta: { priority: 4 },\n cell: ({ getValue }) => getValue<string>() || t('catalog.categories.list.none', '\u2014'),\n },\n {\n accessorKey: 'childCount',\n header: t('catalog.categories.list.columns.children', 'Children'),\n meta: { priority: 5 },\n },\n {\n accessorKey: 'isActive',\n header: t('catalog.categories.list.columns.active', 'Active'),\n enableSorting: false,\n meta: { priority: 2 },\n cell: ({ getValue }) => <BooleanIcon value={Boolean(getValue())} />,\n },\n ], [t])\n\n const handleDelete = React.useCallback(async (category: CategoryRow) => {\n const confirmLabel = t(\n 'catalog.categories.list.confirmDelete',\n 'Archive category \"{{name}}\"?',\n { name: category.name },\n )\n const confirmed = await confirm({\n title: confirmLabel,\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await apiCallOrThrow(\n `/api/catalog/categories?id=${encodeURIComponent(category.id)}`,\n { method: 'DELETE' },\n { errorMessage: t('catalog.categories.list.error.delete', 'Failed to delete category') },\n )\n await queryClient.invalidateQueries({ queryKey: ['catalog-categories'] })\n flash(t('catalog.categories.flash.deleted', 'Category archived'), 'success')\n } catch (err: unknown) {\n const fallback = t('catalog.categories.list.error.delete', 'Failed to delete category')\n const message = err instanceof Error ? err.message : fallback\n flash(message, 'error')\n }\n }, [confirm, queryClient, t])\n\n return (\n <>\n <DataTable\n title={t('catalog.categories.list.title', 'Categories')}\n actions={canManage ? (\n <Button asChild>\n <Link href=\"/backend/catalog/categories/create\">\n {t('catalog.categories.list.actions.create', 'Create')}\n </Link>\n </Button>\n ) : undefined}\n columns={columns}\n data={rows}\n searchValue={search}\n searchPlaceholder={t('catalog.categories.list.searchPlaceholder', 'Search categories')}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n filters={[\n {\n id: 'status',\n label: t('catalog.categories.list.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'all', label: t('catalog.categories.list.filters.all', 'All') },\n { value: 'active', label: t('catalog.categories.list.filters.active', 'Active') },\n { value: 'inactive', label: t('catalog.categories.list.filters.inactive', 'Inactive') },\n ],\n },\n ]}\n filterValues={status === 'all' ? {} : { status }}\n onFiltersApply={(values: FilterValues) => {\n const nextStatus = (values.status as 'all' | 'active' | 'inactive' | undefined) ?? 'all'\n setStatus(nextStatus)\n setPage(1)\n }}\n onFiltersClear={() => {\n setStatus('all')\n setPage(1)\n }}\n sortable={false}\n perspective={{ tableId: 'catalog.categories.list' }}\n rowActions={(row) => (\n canManage ? (\n <RowActions\n items={[\n { id: 'edit', label: t('catalog.categories.list.actions.edit', 'Edit'), href: `/backend/catalog/categories/${row.id}/edit` },\n { id: 'delete', label: t('catalog.categories.list.actions.delete', 'Delete'), destructive: true, onSelect: () => handleDelete(row) },\n ]}\n />\n ) : null\n )}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n isLoading={isLoading}\n />\n {ConfirmDialogElement}\n </>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -89,7 +89,7 @@ function usePersonTasks({
|
|
|
89
89
|
const mapped = Array.isArray(payload.items) ? payload.items.map(mapRowToSummary) : [];
|
|
90
90
|
setPageInfo({
|
|
91
91
|
page: payload.page ?? 1,
|
|
92
|
-
totalPages: payload.totalPages ??
|
|
92
|
+
totalPages: payload.totalPages ?? 0,
|
|
93
93
|
total: payload.total ?? mapped.length
|
|
94
94
|
});
|
|
95
95
|
setError(null);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customers/components/detail/hooks/usePersonTasks.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { resolveTodoApiPath } from '../utils'\nimport type { TodoLinkSummary } from '../types'\nimport { generateTempId } from '@open-mercato/core/modules/customers/lib/detailHelpers'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\nconst DEFAULT_TODO_SOURCE = 'example:todo'\n\ntype CustomerTodoRow = {\n id: string\n todoId: string\n todoSource: string\n todoTitle: string | null\n todoIsDone: boolean | null\n todoPriority: number | null\n todoSeverity: string | null\n todoDescription: string | null\n todoDueAt: string | null\n todoCustomValues: Record<string, unknown> | null\n todoOrganizationId: string | null\n organizationId: string\n tenantId: string\n createdAt: string\n}\n\ntype CustomerTodosResponse = {\n items: CustomerTodoRow[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\nexport type TaskFormPayload = {\n base: {\n title: string\n is_done?: boolean\n }\n custom: Record<string, unknown>\n}\n\nexport type UsePersonTasksOptions = {\n entityId: string | null\n initialTasks?: TodoLinkSummary[]\n pageSize?: number\n}\n\nexport type UsePersonTasksResult = {\n tasks: TodoLinkSummary[]\n isInitialLoading: boolean\n isLoadingMore: boolean\n isMutating: boolean\n hasMore: boolean\n loadMore: () => Promise<void>\n refresh: () => Promise<void>\n createTask: (payload: TaskFormPayload) => Promise<void>\n updateTask: (task: TodoLinkSummary, payload: TaskFormPayload) => Promise<void>\n toggleTask: (task: TodoLinkSummary, nextIsDone: boolean) => Promise<void>\n unlinkTask: (task: TodoLinkSummary) => Promise<void>\n pendingTaskId: string | null\n totalCount: number\n error: string | null\n}\n\nfunction mapRowToSummary(row: CustomerTodoRow): TodoLinkSummary {\n return {\n id: row.id,\n todoId: row.todoId,\n todoSource: row.todoSource || DEFAULT_TODO_SOURCE,\n createdAt: row.createdAt,\n title: row.todoTitle ?? null,\n isDone: row.todoIsDone ?? null,\n priority: row.todoPriority ?? null,\n severity: row.todoSeverity ?? null,\n description: row.todoDescription ?? null,\n dueAt: row.todoDueAt ?? null,\n todoOrganizationId: row.todoOrganizationId ?? null,\n customValues: row.todoCustomValues ?? null,\n }\n}\n\nfunction mergeUnique(existing: TodoLinkSummary[], incoming: TodoLinkSummary[]): TodoLinkSummary[] {\n if (!existing.length) return incoming\n if (!incoming.length) return existing\n const byId = new Map<string, TodoLinkSummary>()\n const result: TodoLinkSummary[] = []\n for (const item of existing) {\n byId.set(item.id, item)\n result.push(item)\n }\n for (const item of incoming) {\n if (byId.has(item.id)) {\n const index = result.findIndex((entry) => entry.id === item.id)\n if (index !== -1) result[index] = item\n } else {\n byId.set(item.id, item)\n result.push(item)\n }\n }\n return result\n}\n\nfunction normalizeBoolean(value: unknown): boolean | undefined {\n if (value === null || value === undefined) return undefined\n if (typeof value === 'boolean') return value\n if (typeof value === 'string') {\n const parsed = parseBooleanToken(value)\n return parsed === null ? undefined : parsed\n }\n return undefined\n}\n\nfunction normalizeNumber(value: unknown): number | null | undefined {\n if (value === null || value === undefined || value === '') return undefined\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed.length) return undefined\n const parsed = Number(trimmed)\n if (!Number.isNaN(parsed)) return parsed\n }\n return null\n}\n\nfunction normalizeString(value: unknown): string | null | undefined {\n if (value === null || value === undefined) return undefined\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n }\n return String(value)\n}\n\nexport function usePersonTasks({\n entityId,\n initialTasks = [],\n pageSize = 20,\n}: UsePersonTasksOptions): UsePersonTasksResult {\n const [tasks, setTasks] = React.useState<TodoLinkSummary[]>(initialTasks)\n const [pageInfo, setPageInfo] = React.useState<{ page: number; totalPages: number; total: number }>({\n page: 1,\n totalPages: 1,\n total: initialTasks.length,\n })\n const [isInitialLoading, setIsInitialLoading] = React.useState<boolean>(() => Boolean(entityId))\n const [isLoadingMore, setIsLoadingMore] = React.useState(false)\n const [isMutating, setIsMutating] = React.useState(false)\n const [pendingTaskId, setPendingTaskId] = React.useState<string | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n\n const mapResponse = React.useCallback((payload: CustomerTodosResponse) => {\n const mapped = Array.isArray(payload.items) ? payload.items.map(mapRowToSummary) : []\n setPageInfo({\n page: payload.page ?? 1,\n totalPages: payload.totalPages ?? 1,\n total: payload.total ?? mapped.length,\n })\n setError(null)\n return mapped\n }, [])\n\n const fetchPage = React.useCallback(\n async (page: number): Promise<CustomerTodosResponse> => {\n if (!entityId) {\n return {\n items: [],\n total: 0,\n page: 1,\n pageSize,\n totalPages: 1,\n }\n }\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n entityId,\n })\n return readApiResultOrThrow<CustomerTodosResponse>(\n `/api/customers/todos?${params.toString()}`,\n undefined,\n { errorMessage: 'Failed to load tasks.' },\n )\n },\n [entityId, pageSize],\n )\n\n const refresh = React.useCallback(async () => {\n if (!entityId) {\n setTasks([])\n setPageInfo({ page: 1, totalPages: 1, total: 0 })\n return\n }\n setIsInitialLoading(true)\n try {\n const payload = await fetchPage(1)\n const mapped = mapResponse(payload)\n setTasks(mapped)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n throw err\n } finally {\n setIsInitialLoading(false)\n }\n }, [entityId, fetchPage, mapResponse])\n\n const loadMore = React.useCallback(async () => {\n if (!entityId) return\n if (isLoadingMore) return\n if (pageInfo.page >= pageInfo.totalPages) return\n setIsLoadingMore(true)\n try {\n const payload = await fetchPage(pageInfo.page + 1)\n const mapped = mapResponse(payload)\n setTasks((prev) => mergeUnique(prev, mapped))\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n throw err\n } finally {\n setIsLoadingMore(false)\n }\n }, [entityId, fetchPage, isLoadingMore, mapResponse, pageInfo.page, pageInfo.totalPages])\n\n React.useEffect(() => {\n if (!entityId) {\n setTasks([])\n setPageInfo({ page: 1, totalPages: 1, total: 0 })\n setError(null)\n setIsInitialLoading(false)\n return\n }\n setTasks(initialTasks)\n setPageInfo({\n page: 1,\n totalPages: 1,\n total: initialTasks.length,\n })\n setError(null)\n let cancelled = false\n setIsInitialLoading(true)\n fetchPage(1)\n .then((payload) => {\n if (cancelled) return\n const mapped = mapResponse(payload)\n setTasks(mapped)\n })\n .catch((err) => {\n if (cancelled) return\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n })\n .finally(() => {\n if (!cancelled) setIsInitialLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [entityId, initialTasks, fetchPage, mapResponse])\n\n const createTask = React.useCallback(\n async ({ base, custom }: TaskFormPayload) => {\n if (!entityId) throw new Error('Task creation requires an entity id')\n setIsMutating(true)\n try {\n const payload: Record<string, unknown> = {\n entityId,\n title: base.title,\n }\n const normalizedDone = normalizeBoolean(base.is_done)\n if (normalizedDone !== undefined) payload.isDone = normalizedDone\n if (Object.keys(custom).length) payload.todoCustom = custom\n\n const response = await apiCallOrThrow<{ linkId?: string; todoId?: string }>(\n '/api/customers/todos',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: 'Failed to create task.' },\n )\n const body = response.result ?? {}\n const linkId = typeof body.linkId === 'string' && body.linkId.length ? body.linkId : generateTempId()\n const todoId = typeof body.todoId === 'string' && body.todoId.length ? body.todoId : generateTempId()\n const createdAt = new Date().toISOString()\n const customValues = Object.keys(custom).length ? { ...custom } : null\n const priority = normalizeNumber(custom.priority)\n const severity = normalizeString(custom.severity) ?? null\n const description = normalizeString(custom.description) ?? null\n const newTask: TodoLinkSummary = {\n id: linkId,\n todoId,\n todoSource: DEFAULT_TODO_SOURCE,\n createdAt,\n title: base.title,\n isDone: normalizedDone ?? false,\n priority: priority === undefined ? null : priority,\n severity,\n description,\n dueAt: normalizeString(custom.due_at) ?? normalizeString(custom.dueAt) ?? null,\n todoOrganizationId: null,\n customValues,\n }\n setTasks((prev) => [newTask, ...prev])\n setPageInfo((prev) => ({\n page: 1,\n totalPages: prev.totalPages,\n total: prev.total + 1,\n }))\n await refresh()\n } finally {\n setIsMutating(false)\n }\n },\n [entityId, refresh],\n )\n\n const updateTask = React.useCallback(\n async (task: TodoLinkSummary, { base, custom }: TaskFormPayload) => {\n if (!task.todoId) throw new Error('Task is missing todo id')\n const apiPath = resolveTodoApiPath(task.todoSource || DEFAULT_TODO_SOURCE)\n if (!apiPath) throw new Error('Unsupported task source')\n setIsMutating(true)\n try {\n const body: Record<string, unknown> = {\n id: task.todoId,\n }\n if (typeof base.title === 'string' && base.title.trim().length) {\n body.title = base.title.trim()\n }\n const normalizedDone = normalizeBoolean(base.is_done)\n if (normalizedDone !== undefined) body.is_done = normalizedDone\n if (Object.keys(custom).length) {\n body.customFields = custom\n }\n await apiCallOrThrow(\n apiPath,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n },\n { errorMessage: 'Failed to update task.' },\n )\n setTasks((prev) =>\n prev.map((item) => {\n if (item.id !== task.id) return item\n const nextCustomValues = { ...(item.customValues ?? {}) }\n for (const [key, value] of Object.entries(custom)) {\n nextCustomValues[key] = value === undefined ? null : value\n }\n return {\n ...item,\n title: typeof base.title === 'string' && base.title.trim().length ? base.title.trim() : item.title,\n isDone: normalizedDone !== undefined ? normalizedDone : item.isDone,\n priority: normalizeNumber(custom.priority) ?? (custom.priority === undefined ? item.priority ?? null : null),\n severity: normalizeString(custom.severity) ?? (custom.severity === undefined ? item.severity ?? null : null),\n description:\n normalizeString(custom.description) ?? (custom.description === undefined ? item.description ?? null : null),\n dueAt:\n normalizeString(custom.due_at) ??\n normalizeString(custom.dueAt) ??\n (custom.due_at === undefined && custom.dueAt === undefined ? item.dueAt ?? null : null),\n customValues: Object.keys(nextCustomValues).length ? nextCustomValues : null,\n }\n }),\n )\n } finally {\n setIsMutating(false)\n }\n },\n [],\n )\n\n const toggleTask = React.useCallback(\n async (task: TodoLinkSummary, nextIsDone: boolean) => {\n if (!task.todoId) {\n throw new Error('Task is missing todo id')\n }\n const apiPath = resolveTodoApiPath(task.todoSource || DEFAULT_TODO_SOURCE)\n if (!apiPath) {\n throw new Error('Unsupported task source')\n }\n setPendingTaskId(task.todoId)\n try {\n await updateTask(task, { base: { title: task.title ?? '', is_done: nextIsDone }, custom: {} })\n } finally {\n setPendingTaskId(null)\n }\n },\n [updateTask],\n )\n\n const unlinkTask = React.useCallback(\n async (task: TodoLinkSummary) => {\n if (!task.id) throw new Error('Task link id missing')\n setIsMutating(true)\n try {\n await apiCallOrThrow(\n '/api/customers/todos',\n {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: task.id }),\n },\n { errorMessage: 'Failed to remove task.' },\n )\n setTasks((prev) => prev.filter((item) => item.id !== task.id))\n setPageInfo((prev) => ({\n page: prev.page,\n totalPages: prev.totalPages,\n total: Math.max(0, prev.total - 1),\n }))\n } finally {\n setIsMutating(false)\n }\n },\n [],\n )\n\n const hasMore = entityId != null && pageInfo.page < pageInfo.totalPages\n\n return {\n tasks,\n isInitialLoading,\n isLoadingMore,\n isMutating,\n hasMore,\n loadMore,\n refresh,\n createTask,\n updateTask,\n toggleTask,\n unlinkTask,\n pendingTaskId,\n totalCount: pageInfo.total,\n error,\n }\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { resolveTodoApiPath } from '../utils'\nimport type { TodoLinkSummary } from '../types'\nimport { generateTempId } from '@open-mercato/core/modules/customers/lib/detailHelpers'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\nconst DEFAULT_TODO_SOURCE = 'example:todo'\n\ntype CustomerTodoRow = {\n id: string\n todoId: string\n todoSource: string\n todoTitle: string | null\n todoIsDone: boolean | null\n todoPriority: number | null\n todoSeverity: string | null\n todoDescription: string | null\n todoDueAt: string | null\n todoCustomValues: Record<string, unknown> | null\n todoOrganizationId: string | null\n organizationId: string\n tenantId: string\n createdAt: string\n}\n\ntype CustomerTodosResponse = {\n items: CustomerTodoRow[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\nexport type TaskFormPayload = {\n base: {\n title: string\n is_done?: boolean\n }\n custom: Record<string, unknown>\n}\n\nexport type UsePersonTasksOptions = {\n entityId: string | null\n initialTasks?: TodoLinkSummary[]\n pageSize?: number\n}\n\nexport type UsePersonTasksResult = {\n tasks: TodoLinkSummary[]\n isInitialLoading: boolean\n isLoadingMore: boolean\n isMutating: boolean\n hasMore: boolean\n loadMore: () => Promise<void>\n refresh: () => Promise<void>\n createTask: (payload: TaskFormPayload) => Promise<void>\n updateTask: (task: TodoLinkSummary, payload: TaskFormPayload) => Promise<void>\n toggleTask: (task: TodoLinkSummary, nextIsDone: boolean) => Promise<void>\n unlinkTask: (task: TodoLinkSummary) => Promise<void>\n pendingTaskId: string | null\n totalCount: number\n error: string | null\n}\n\nfunction mapRowToSummary(row: CustomerTodoRow): TodoLinkSummary {\n return {\n id: row.id,\n todoId: row.todoId,\n todoSource: row.todoSource || DEFAULT_TODO_SOURCE,\n createdAt: row.createdAt,\n title: row.todoTitle ?? null,\n isDone: row.todoIsDone ?? null,\n priority: row.todoPriority ?? null,\n severity: row.todoSeverity ?? null,\n description: row.todoDescription ?? null,\n dueAt: row.todoDueAt ?? null,\n todoOrganizationId: row.todoOrganizationId ?? null,\n customValues: row.todoCustomValues ?? null,\n }\n}\n\nfunction mergeUnique(existing: TodoLinkSummary[], incoming: TodoLinkSummary[]): TodoLinkSummary[] {\n if (!existing.length) return incoming\n if (!incoming.length) return existing\n const byId = new Map<string, TodoLinkSummary>()\n const result: TodoLinkSummary[] = []\n for (const item of existing) {\n byId.set(item.id, item)\n result.push(item)\n }\n for (const item of incoming) {\n if (byId.has(item.id)) {\n const index = result.findIndex((entry) => entry.id === item.id)\n if (index !== -1) result[index] = item\n } else {\n byId.set(item.id, item)\n result.push(item)\n }\n }\n return result\n}\n\nfunction normalizeBoolean(value: unknown): boolean | undefined {\n if (value === null || value === undefined) return undefined\n if (typeof value === 'boolean') return value\n if (typeof value === 'string') {\n const parsed = parseBooleanToken(value)\n return parsed === null ? undefined : parsed\n }\n return undefined\n}\n\nfunction normalizeNumber(value: unknown): number | null | undefined {\n if (value === null || value === undefined || value === '') return undefined\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed.length) return undefined\n const parsed = Number(trimmed)\n if (!Number.isNaN(parsed)) return parsed\n }\n return null\n}\n\nfunction normalizeString(value: unknown): string | null | undefined {\n if (value === null || value === undefined) return undefined\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n }\n return String(value)\n}\n\nexport function usePersonTasks({\n entityId,\n initialTasks = [],\n pageSize = 20,\n}: UsePersonTasksOptions): UsePersonTasksResult {\n const [tasks, setTasks] = React.useState<TodoLinkSummary[]>(initialTasks)\n const [pageInfo, setPageInfo] = React.useState<{ page: number; totalPages: number; total: number }>({\n page: 1,\n totalPages: 1,\n total: initialTasks.length,\n })\n const [isInitialLoading, setIsInitialLoading] = React.useState<boolean>(() => Boolean(entityId))\n const [isLoadingMore, setIsLoadingMore] = React.useState(false)\n const [isMutating, setIsMutating] = React.useState(false)\n const [pendingTaskId, setPendingTaskId] = React.useState<string | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n\n const mapResponse = React.useCallback((payload: CustomerTodosResponse) => {\n const mapped = Array.isArray(payload.items) ? payload.items.map(mapRowToSummary) : []\n setPageInfo({\n page: payload.page ?? 1,\n totalPages: payload.totalPages ?? 0,\n total: payload.total ?? mapped.length,\n })\n setError(null)\n return mapped\n }, [])\n\n const fetchPage = React.useCallback(\n async (page: number): Promise<CustomerTodosResponse> => {\n if (!entityId) {\n return {\n items: [],\n total: 0,\n page: 1,\n pageSize,\n totalPages: 1,\n }\n }\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n entityId,\n })\n return readApiResultOrThrow<CustomerTodosResponse>(\n `/api/customers/todos?${params.toString()}`,\n undefined,\n { errorMessage: 'Failed to load tasks.' },\n )\n },\n [entityId, pageSize],\n )\n\n const refresh = React.useCallback(async () => {\n if (!entityId) {\n setTasks([])\n setPageInfo({ page: 1, totalPages: 1, total: 0 })\n return\n }\n setIsInitialLoading(true)\n try {\n const payload = await fetchPage(1)\n const mapped = mapResponse(payload)\n setTasks(mapped)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n throw err\n } finally {\n setIsInitialLoading(false)\n }\n }, [entityId, fetchPage, mapResponse])\n\n const loadMore = React.useCallback(async () => {\n if (!entityId) return\n if (isLoadingMore) return\n if (pageInfo.page >= pageInfo.totalPages) return\n setIsLoadingMore(true)\n try {\n const payload = await fetchPage(pageInfo.page + 1)\n const mapped = mapResponse(payload)\n setTasks((prev) => mergeUnique(prev, mapped))\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n throw err\n } finally {\n setIsLoadingMore(false)\n }\n }, [entityId, fetchPage, isLoadingMore, mapResponse, pageInfo.page, pageInfo.totalPages])\n\n React.useEffect(() => {\n if (!entityId) {\n setTasks([])\n setPageInfo({ page: 1, totalPages: 1, total: 0 })\n setError(null)\n setIsInitialLoading(false)\n return\n }\n setTasks(initialTasks)\n setPageInfo({\n page: 1,\n totalPages: 1,\n total: initialTasks.length,\n })\n setError(null)\n let cancelled = false\n setIsInitialLoading(true)\n fetchPage(1)\n .then((payload) => {\n if (cancelled) return\n const mapped = mapResponse(payload)\n setTasks(mapped)\n })\n .catch((err) => {\n if (cancelled) return\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n })\n .finally(() => {\n if (!cancelled) setIsInitialLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [entityId, initialTasks, fetchPage, mapResponse])\n\n const createTask = React.useCallback(\n async ({ base, custom }: TaskFormPayload) => {\n if (!entityId) throw new Error('Task creation requires an entity id')\n setIsMutating(true)\n try {\n const payload: Record<string, unknown> = {\n entityId,\n title: base.title,\n }\n const normalizedDone = normalizeBoolean(base.is_done)\n if (normalizedDone !== undefined) payload.isDone = normalizedDone\n if (Object.keys(custom).length) payload.todoCustom = custom\n\n const response = await apiCallOrThrow<{ linkId?: string; todoId?: string }>(\n '/api/customers/todos',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: 'Failed to create task.' },\n )\n const body = response.result ?? {}\n const linkId = typeof body.linkId === 'string' && body.linkId.length ? body.linkId : generateTempId()\n const todoId = typeof body.todoId === 'string' && body.todoId.length ? body.todoId : generateTempId()\n const createdAt = new Date().toISOString()\n const customValues = Object.keys(custom).length ? { ...custom } : null\n const priority = normalizeNumber(custom.priority)\n const severity = normalizeString(custom.severity) ?? null\n const description = normalizeString(custom.description) ?? null\n const newTask: TodoLinkSummary = {\n id: linkId,\n todoId,\n todoSource: DEFAULT_TODO_SOURCE,\n createdAt,\n title: base.title,\n isDone: normalizedDone ?? false,\n priority: priority === undefined ? null : priority,\n severity,\n description,\n dueAt: normalizeString(custom.due_at) ?? normalizeString(custom.dueAt) ?? null,\n todoOrganizationId: null,\n customValues,\n }\n setTasks((prev) => [newTask, ...prev])\n setPageInfo((prev) => ({\n page: 1,\n totalPages: prev.totalPages,\n total: prev.total + 1,\n }))\n await refresh()\n } finally {\n setIsMutating(false)\n }\n },\n [entityId, refresh],\n )\n\n const updateTask = React.useCallback(\n async (task: TodoLinkSummary, { base, custom }: TaskFormPayload) => {\n if (!task.todoId) throw new Error('Task is missing todo id')\n const apiPath = resolveTodoApiPath(task.todoSource || DEFAULT_TODO_SOURCE)\n if (!apiPath) throw new Error('Unsupported task source')\n setIsMutating(true)\n try {\n const body: Record<string, unknown> = {\n id: task.todoId,\n }\n if (typeof base.title === 'string' && base.title.trim().length) {\n body.title = base.title.trim()\n }\n const normalizedDone = normalizeBoolean(base.is_done)\n if (normalizedDone !== undefined) body.is_done = normalizedDone\n if (Object.keys(custom).length) {\n body.customFields = custom\n }\n await apiCallOrThrow(\n apiPath,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n },\n { errorMessage: 'Failed to update task.' },\n )\n setTasks((prev) =>\n prev.map((item) => {\n if (item.id !== task.id) return item\n const nextCustomValues = { ...(item.customValues ?? {}) }\n for (const [key, value] of Object.entries(custom)) {\n nextCustomValues[key] = value === undefined ? null : value\n }\n return {\n ...item,\n title: typeof base.title === 'string' && base.title.trim().length ? base.title.trim() : item.title,\n isDone: normalizedDone !== undefined ? normalizedDone : item.isDone,\n priority: normalizeNumber(custom.priority) ?? (custom.priority === undefined ? item.priority ?? null : null),\n severity: normalizeString(custom.severity) ?? (custom.severity === undefined ? item.severity ?? null : null),\n description:\n normalizeString(custom.description) ?? (custom.description === undefined ? item.description ?? null : null),\n dueAt:\n normalizeString(custom.due_at) ??\n normalizeString(custom.dueAt) ??\n (custom.due_at === undefined && custom.dueAt === undefined ? item.dueAt ?? null : null),\n customValues: Object.keys(nextCustomValues).length ? nextCustomValues : null,\n }\n }),\n )\n } finally {\n setIsMutating(false)\n }\n },\n [],\n )\n\n const toggleTask = React.useCallback(\n async (task: TodoLinkSummary, nextIsDone: boolean) => {\n if (!task.todoId) {\n throw new Error('Task is missing todo id')\n }\n const apiPath = resolveTodoApiPath(task.todoSource || DEFAULT_TODO_SOURCE)\n if (!apiPath) {\n throw new Error('Unsupported task source')\n }\n setPendingTaskId(task.todoId)\n try {\n await updateTask(task, { base: { title: task.title ?? '', is_done: nextIsDone }, custom: {} })\n } finally {\n setPendingTaskId(null)\n }\n },\n [updateTask],\n )\n\n const unlinkTask = React.useCallback(\n async (task: TodoLinkSummary) => {\n if (!task.id) throw new Error('Task link id missing')\n setIsMutating(true)\n try {\n await apiCallOrThrow(\n '/api/customers/todos',\n {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: task.id }),\n },\n { errorMessage: 'Failed to remove task.' },\n )\n setTasks((prev) => prev.filter((item) => item.id !== task.id))\n setPageInfo((prev) => ({\n page: prev.page,\n totalPages: prev.totalPages,\n total: Math.max(0, prev.total - 1),\n }))\n } finally {\n setIsMutating(false)\n }\n },\n [],\n )\n\n const hasMore = entityId != null && pageInfo.page < pageInfo.totalPages\n\n return {\n tasks,\n isInitialLoading,\n isLoadingMore,\n isMutating,\n hasMore,\n loadMore,\n refresh,\n createTask,\n updateTask,\n toggleTask,\n unlinkTask,\n pendingTaskId,\n totalCount: pageInfo.total,\n error,\n }\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -135,7 +135,7 @@ function DirectoryOrganizationsPage() {
|
|
|
135
135
|
return base;
|
|
136
136
|
}, [isSuperAdmin, t]);
|
|
137
137
|
const total = data?.total ?? 0;
|
|
138
|
-
const totalPages = data?.totalPages ??
|
|
138
|
+
const totalPages = data?.totalPages ?? 0;
|
|
139
139
|
const handleDelete = React.useCallback(async (org) => {
|
|
140
140
|
const confirmLabel = t("directory.organizations.list.confirmDelete", 'Archive organization "{{name}}"?', { name: org.name });
|
|
141
141
|
const confirmed = await confirm({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/directory/backend/directory/organizations/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall, apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype OrganizationRow = {\n id: string\n name: string\n tenantId: string\n tenantName?: string | null\n parentId: string | null\n parentName: string | null\n depth: number\n rootId: string\n treePath: string\n pathLabel: string\n ancestorIds: string[]\n childIds: string[]\n descendantIds: string[]\n childrenCount: number\n descendantsCount: number\n isActive: boolean\n}\n\ntype OrganizationsResponse = {\n items: OrganizationRow[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n isSuperAdmin?: boolean\n}\n\nconst TREE_BASE_INDENT = 18\nconst TREE_STEP_INDENT = 14\n\nfunction formatTreeLabel(name: string, depth: number): string {\n if (depth <= 0) return name\n return `${'\\u00A0'.repeat(Math.max(0, (depth - 1) * 2))}\u21B3 ${name}`\n}\n\nfunction computeIndent(depth: number): number {\n if (depth <= 0) return 0\n return TREE_BASE_INDENT + (depth - 1) * TREE_STEP_INDENT\n}\n\nexport default function DirectoryOrganizationsPage() {\n const queryClient = useQueryClient()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [page, setPage] = React.useState(1)\n const [status, setStatus] = React.useState<string>('all')\n const [search, setSearch] = React.useState('')\n const [canManage, setCanManage] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n const t = useT()\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n try {\n const call = await apiCall<{ granted?: string[]; ok?: boolean }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['directory.organizations.manage'] }),\n })\n if (!cancelled) {\n const granted = Array.isArray(call.result?.granted) ? call.result?.granted : []\n setCanManage(call.result?.ok === true || granted.includes('directory.organizations.manage'))\n }\n } catch {\n if (!cancelled) setCanManage(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('view', 'manage')\n params.set('page', String(page))\n params.set('pageSize', '50')\n params.set('status', status)\n if (status !== 'active') params.set('includeInactive', 'true')\n if (search) params.set('search', search)\n return params.toString()\n }, [page, status, search])\n\n const { data, isLoading } = useQuery<OrganizationsResponse>({\n queryKey: ['directory-organizations', queryParams, scopeVersion],\n queryFn: async () => {\n return readApiResultOrThrow<OrganizationsResponse>(\n `/api/directory/organizations?${queryParams}`,\n undefined,\n { errorMessage: t('directory.organizations.list.error.load', 'Failed to load organizations') },\n )\n },\n })\n\n const rows = data?.items ?? []\n const isSuperAdmin = data?.isSuperAdmin ?? false\n const columns = React.useMemo<ColumnDef<OrganizationRow>[]>(() => {\n const base: ColumnDef<OrganizationRow>[] = [\n {\n accessorKey: 'name',\n header: t('directory.organizations.list.columns.organization', 'Organization'),\n cell: ({ row }) => {\n const depth = row.original.depth ?? 0\n return (\n <div className=\"flex items-center text-sm font-medium leading-none text-foreground\">\n <span\n style={{ marginLeft: computeIndent(depth), whiteSpace: 'pre' }}\n >\n {formatTreeLabel(row.original.name, depth)}\n </span>\n </div>\n )\n },\n meta: { priority: 1 },\n },\n {\n accessorKey: 'pathLabel',\n header: t('directory.organizations.list.columns.path', 'Path'),\n meta: { priority: 3 },\n cell: ({ getValue }) => {\n const value = getValue<string>()\n return <span className=\"text-xs text-muted-foreground\">{value}</span>\n },\n },\n {\n accessorKey: 'parentName',\n header: t('directory.organizations.list.columns.parent', 'Parent'),\n meta: { priority: 4 },\n cell: ({ getValue }) => getValue<string>() || t('directory.organizations.common.none', '\u2014'),\n },\n {\n accessorKey: 'childrenCount',\n header: t('directory.organizations.list.columns.children', 'Children'),\n meta: { priority: 5 },\n },\n {\n accessorKey: 'isActive',\n header: t('directory.organizations.list.columns.active', 'Active'),\n enableSorting: false,\n meta: { priority: 2 },\n cell: ({ getValue }) => <BooleanIcon value={Boolean(getValue())} />, \n },\n ]\n if (isSuperAdmin) {\n base.splice(1, 0, {\n accessorKey: 'tenantName',\n header: t('directory.organizations.list.columns.tenant', 'Tenant'),\n meta: { priority: 2 },\n cell: ({ row }) => {\n const value = row.original.tenantName ?? row.original.tenantId\n return <span className=\"text-xs text-muted-foreground\">{value}</span>\n },\n })\n }\n return base\n }, [isSuperAdmin, t])\n const total = data?.total ?? 0\n const totalPages = data?.totalPages ??
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall, apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype OrganizationRow = {\n id: string\n name: string\n tenantId: string\n tenantName?: string | null\n parentId: string | null\n parentName: string | null\n depth: number\n rootId: string\n treePath: string\n pathLabel: string\n ancestorIds: string[]\n childIds: string[]\n descendantIds: string[]\n childrenCount: number\n descendantsCount: number\n isActive: boolean\n}\n\ntype OrganizationsResponse = {\n items: OrganizationRow[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n isSuperAdmin?: boolean\n}\n\nconst TREE_BASE_INDENT = 18\nconst TREE_STEP_INDENT = 14\n\nfunction formatTreeLabel(name: string, depth: number): string {\n if (depth <= 0) return name\n return `${'\\u00A0'.repeat(Math.max(0, (depth - 1) * 2))}\u21B3 ${name}`\n}\n\nfunction computeIndent(depth: number): number {\n if (depth <= 0) return 0\n return TREE_BASE_INDENT + (depth - 1) * TREE_STEP_INDENT\n}\n\nexport default function DirectoryOrganizationsPage() {\n const queryClient = useQueryClient()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [page, setPage] = React.useState(1)\n const [status, setStatus] = React.useState<string>('all')\n const [search, setSearch] = React.useState('')\n const [canManage, setCanManage] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n const t = useT()\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n try {\n const call = await apiCall<{ granted?: string[]; ok?: boolean }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['directory.organizations.manage'] }),\n })\n if (!cancelled) {\n const granted = Array.isArray(call.result?.granted) ? call.result?.granted : []\n setCanManage(call.result?.ok === true || granted.includes('directory.organizations.manage'))\n }\n } catch {\n if (!cancelled) setCanManage(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('view', 'manage')\n params.set('page', String(page))\n params.set('pageSize', '50')\n params.set('status', status)\n if (status !== 'active') params.set('includeInactive', 'true')\n if (search) params.set('search', search)\n return params.toString()\n }, [page, status, search])\n\n const { data, isLoading } = useQuery<OrganizationsResponse>({\n queryKey: ['directory-organizations', queryParams, scopeVersion],\n queryFn: async () => {\n return readApiResultOrThrow<OrganizationsResponse>(\n `/api/directory/organizations?${queryParams}`,\n undefined,\n { errorMessage: t('directory.organizations.list.error.load', 'Failed to load organizations') },\n )\n },\n })\n\n const rows = data?.items ?? []\n const isSuperAdmin = data?.isSuperAdmin ?? false\n const columns = React.useMemo<ColumnDef<OrganizationRow>[]>(() => {\n const base: ColumnDef<OrganizationRow>[] = [\n {\n accessorKey: 'name',\n header: t('directory.organizations.list.columns.organization', 'Organization'),\n cell: ({ row }) => {\n const depth = row.original.depth ?? 0\n return (\n <div className=\"flex items-center text-sm font-medium leading-none text-foreground\">\n <span\n style={{ marginLeft: computeIndent(depth), whiteSpace: 'pre' }}\n >\n {formatTreeLabel(row.original.name, depth)}\n </span>\n </div>\n )\n },\n meta: { priority: 1 },\n },\n {\n accessorKey: 'pathLabel',\n header: t('directory.organizations.list.columns.path', 'Path'),\n meta: { priority: 3 },\n cell: ({ getValue }) => {\n const value = getValue<string>()\n return <span className=\"text-xs text-muted-foreground\">{value}</span>\n },\n },\n {\n accessorKey: 'parentName',\n header: t('directory.organizations.list.columns.parent', 'Parent'),\n meta: { priority: 4 },\n cell: ({ getValue }) => getValue<string>() || t('directory.organizations.common.none', '\u2014'),\n },\n {\n accessorKey: 'childrenCount',\n header: t('directory.organizations.list.columns.children', 'Children'),\n meta: { priority: 5 },\n },\n {\n accessorKey: 'isActive',\n header: t('directory.organizations.list.columns.active', 'Active'),\n enableSorting: false,\n meta: { priority: 2 },\n cell: ({ getValue }) => <BooleanIcon value={Boolean(getValue())} />, \n },\n ]\n if (isSuperAdmin) {\n base.splice(1, 0, {\n accessorKey: 'tenantName',\n header: t('directory.organizations.list.columns.tenant', 'Tenant'),\n meta: { priority: 2 },\n cell: ({ row }) => {\n const value = row.original.tenantName ?? row.original.tenantId\n return <span className=\"text-xs text-muted-foreground\">{value}</span>\n },\n })\n }\n return base\n }, [isSuperAdmin, t])\n const total = data?.total ?? 0\n const totalPages = data?.totalPages ?? 0\n\n const handleDelete = React.useCallback(async (org: OrganizationRow) => {\n const confirmLabel = t('directory.organizations.list.confirmDelete', 'Archive organization \"{{name}}\"?', { name: org.name })\n const confirmed = await confirm({\n title: t('directory.organizations.list.actions.delete', 'Delete'),\n text: confirmLabel,\n variant: 'destructive',\n })\n if (!confirmed) return\n\n try {\n await apiCallOrThrow(\n `/api/directory/organizations?id=${encodeURIComponent(org.id)}`,\n { method: 'DELETE' },\n { errorMessage: t('directory.organizations.list.error.delete', 'Failed to delete organization') },\n )\n await queryClient.invalidateQueries({ queryKey: ['directory-organizations'] })\n flash(t('directory.organizations.flash.deleted', 'Organization deleted'), 'success')\n } catch (err: unknown) {\n const fallback = t('directory.organizations.list.error.delete', 'Failed to delete organization')\n const message = err instanceof Error ? err.message : fallback\n flash(message, 'error')\n }\n }, [confirm, queryClient, t])\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('directory.organizations.list.title', 'Organizations')}\n actions={canManage ? (\n <Button asChild>\n <Link href=\"/backend/directory/organizations/create\">\n {t('directory.organizations.list.actions.create', 'Create')}\n </Link>\n </Button>\n ) : undefined}\n columns={columns}\n data={rows}\n searchValue={search}\n searchPlaceholder={t('directory.organizations.list.searchPlaceholder', 'Search organizations')}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n filters={[\n {\n id: 'status',\n label: t('directory.organizations.list.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'all', label: t('directory.organizations.list.filters.all', 'All') },\n { value: 'active', label: t('directory.organizations.list.filters.active', 'Active') },\n { value: 'inactive', label: t('directory.organizations.list.filters.inactive', 'Inactive') },\n ],\n },\n ]}\n filterValues={status === 'all' ? {} : { status }}\n onFiltersApply={(vals: FilterValues) => {\n const nextStatus = (vals.status as string) || 'all'\n setStatus(nextStatus)\n setPage(1)\n }}\n onFiltersClear={() => {\n setStatus('all')\n setPage(1)\n }}\n sortable={false}\n perspective={{ tableId: 'directory.organizations.list' }}\n rowActions={(row) => (\n canManage ? (\n <RowActions\n items={[\n { id: 'edit', label: t('directory.organizations.list.actions.edit', 'Edit'), href: `/backend/directory/organizations/${row.id}/edit` },\n { id: 'delete', label: t('directory.organizations.list.actions.delete', 'Delete'), destructive: true, onSelect: () => handleDelete(row) },\n ]}\n />\n ) : null\n )}\n pagination={{ page, pageSize: 50, total, totalPages, onPageChange: setPage }}\n isLoading={isLoading}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -92,7 +92,7 @@ function DirectoryTenantsPage() {
|
|
|
92
92
|
});
|
|
93
93
|
const rows = data?.items ?? [];
|
|
94
94
|
const total = data?.total ?? 0;
|
|
95
|
-
const totalPages = data?.totalPages ??
|
|
95
|
+
const totalPages = data?.totalPages ?? 0;
|
|
96
96
|
const handleDelete = React.useCallback(async (tenant) => {
|
|
97
97
|
const confirmMessage = t("directory.tenants.list.confirmDelete", 'Delete tenant "{{name}}"? This will archive it.').replace("{{name}}", tenant.name);
|
|
98
98
|
const confirmed = await confirm({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/directory/backend/directory/tenants/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport type { FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype TenantRow = {\n id: string\n name: string\n isActive: boolean\n createdAt: string | null\n updatedAt: string | null\n}\n\ntype TenantsResponse = {\n items: TenantRow[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\n\nexport default function DirectoryTenantsPage() {\n const queryClient = useQueryClient()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'name', desc: false }])\n const [page, setPage] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [canManage, setCanManage] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n const t = useT()\n const columns = React.useMemo<ColumnDef<TenantRow>[]>(() => [\n { accessorKey: 'name', header: t('directory.tenants.list.columns.tenant', 'Tenant'), meta: { priority: 1 } },\n {\n accessorKey: 'isActive',\n header: t('directory.tenants.list.columns.active', 'Active'),\n enableSorting: false,\n meta: { priority: 2 },\n cell: ({ getValue }) => <BooleanIcon value={Boolean(getValue())} />,\n },\n {\n accessorKey: 'createdAt',\n header: t('directory.tenants.list.columns.created', 'Created'),\n meta: { priority: 3 },\n cell: ({ getValue }) => {\n const timestamp = getValue() as string | null\n if (!timestamp) return <span className=\"text-xs text-muted-foreground\">\u2014</span>\n const date = new Date(timestamp)\n if (Number.isNaN(date.getTime())) return <span className=\"text-xs text-muted-foreground\">\u2014</span>\n return <span>{date.toLocaleString()}</span>\n },\n },\n ], [t])\n\n React.useEffect(() => {\n let cancelled = false\n async function loadFeature() {\n try {\n const call = await apiCall<{ ok?: boolean; granted?: string[] }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['directory.tenants.manage'] }),\n })\n if (!cancelled) {\n const granted = Array.isArray(call.result?.granted) ? call.result!.granted! : []\n setCanManage(call.result?.ok === true || granted.includes('directory.tenants.manage'))\n }\n } catch {\n if (!cancelled) setCanManage(false)\n }\n }\n loadFeature()\n return () => { cancelled = true }\n }, [])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', '20')\n if (sorting.length > 0) {\n params.set('sortField', sorting[0]?.id || 'name')\n params.set('sortDir', sorting[0]?.desc ? 'desc' : 'asc')\n }\n if (search) params.set('search', search)\n if (filters.active !== undefined && filters.active !== '') params.set('isActive', String(filters.active))\n return params.toString()\n }, [page, sorting, search, filters])\n\n const { data, isLoading } = useQuery({\n queryKey: ['directory-tenants', queryParams, scopeVersion],\n queryFn: async (): Promise<TenantsResponse> => {\n return readApiResultOrThrow<TenantsResponse>(\n `/api/directory/tenants?${queryParams}`,\n undefined,\n { errorMessage: t('directory.tenants.list.error.load', 'Failed to load tenants') },\n )\n },\n })\n\n const rows = data?.items ?? []\n const total = data?.total ?? 0\n const totalPages = data?.totalPages ??
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport type { FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype TenantRow = {\n id: string\n name: string\n isActive: boolean\n createdAt: string | null\n updatedAt: string | null\n}\n\ntype TenantsResponse = {\n items: TenantRow[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\n\nexport default function DirectoryTenantsPage() {\n const queryClient = useQueryClient()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'name', desc: false }])\n const [page, setPage] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [canManage, setCanManage] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n const t = useT()\n const columns = React.useMemo<ColumnDef<TenantRow>[]>(() => [\n { accessorKey: 'name', header: t('directory.tenants.list.columns.tenant', 'Tenant'), meta: { priority: 1 } },\n {\n accessorKey: 'isActive',\n header: t('directory.tenants.list.columns.active', 'Active'),\n enableSorting: false,\n meta: { priority: 2 },\n cell: ({ getValue }) => <BooleanIcon value={Boolean(getValue())} />,\n },\n {\n accessorKey: 'createdAt',\n header: t('directory.tenants.list.columns.created', 'Created'),\n meta: { priority: 3 },\n cell: ({ getValue }) => {\n const timestamp = getValue() as string | null\n if (!timestamp) return <span className=\"text-xs text-muted-foreground\">\u2014</span>\n const date = new Date(timestamp)\n if (Number.isNaN(date.getTime())) return <span className=\"text-xs text-muted-foreground\">\u2014</span>\n return <span>{date.toLocaleString()}</span>\n },\n },\n ], [t])\n\n React.useEffect(() => {\n let cancelled = false\n async function loadFeature() {\n try {\n const call = await apiCall<{ ok?: boolean; granted?: string[] }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['directory.tenants.manage'] }),\n })\n if (!cancelled) {\n const granted = Array.isArray(call.result?.granted) ? call.result!.granted! : []\n setCanManage(call.result?.ok === true || granted.includes('directory.tenants.manage'))\n }\n } catch {\n if (!cancelled) setCanManage(false)\n }\n }\n loadFeature()\n return () => { cancelled = true }\n }, [])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', '20')\n if (sorting.length > 0) {\n params.set('sortField', sorting[0]?.id || 'name')\n params.set('sortDir', sorting[0]?.desc ? 'desc' : 'asc')\n }\n if (search) params.set('search', search)\n if (filters.active !== undefined && filters.active !== '') params.set('isActive', String(filters.active))\n return params.toString()\n }, [page, sorting, search, filters])\n\n const { data, isLoading } = useQuery({\n queryKey: ['directory-tenants', queryParams, scopeVersion],\n queryFn: async (): Promise<TenantsResponse> => {\n return readApiResultOrThrow<TenantsResponse>(\n `/api/directory/tenants?${queryParams}`,\n undefined,\n { errorMessage: t('directory.tenants.list.error.load', 'Failed to load tenants') },\n )\n },\n })\n\n const rows = data?.items ?? []\n const total = data?.total ?? 0\n const totalPages = data?.totalPages ?? 0\n\n const handleDelete = React.useCallback(async (tenant: TenantRow) => {\n const confirmMessage = t('directory.tenants.list.confirmDelete', 'Delete tenant \"{{name}}\"? This will archive it.').replace('{{name}}', tenant.name)\n const confirmed = await confirm({\n title: t('common.delete', 'Delete'),\n text: confirmMessage,\n variant: 'destructive',\n })\n if (!confirmed) return\n\n try {\n const call = await apiCall(\n `/api/directory/tenants?id=${encodeURIComponent(tenant.id)}`,\n { method: 'DELETE' },\n )\n if (!call.ok) {\n await raiseCrudError(call.response, t('directory.tenants.list.error.delete', 'Failed to delete tenant'))\n }\n await queryClient.invalidateQueries({ queryKey: ['directory-tenants'] })\n flash(t('directory.tenants.list.success.delete', 'Tenant deleted'), 'success')\n } catch (err: any) {\n const message = err instanceof Error ? err.message : t('directory.tenants.list.error.delete', 'Failed to delete tenant')\n flash(message, 'error')\n }\n }, [confirm, queryClient, t])\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('directory.tenants.list.title', 'Tenants')}\n actions={canManage ? (\n <Button asChild>\n <Link href=\"/backend/directory/tenants/create\">{t('directory.tenants.list.actions.create', 'Create')}</Link>\n </Button>\n ) : undefined}\n columns={columns}\n data={rows}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n filters={[{ id: 'active', label: t('directory.tenants.list.filters.status', 'Status'), type: 'select', options: [\n { value: 'true', label: t('directory.tenants.list.filters.active', 'Active') },\n { value: 'false', label: t('directory.tenants.list.filters.inactive', 'Inactive') },\n ] }]}\n filterValues={filters}\n onFiltersApply={(vals) => { setFilters(vals); setPage(1) }}\n onFiltersClear={() => { setFilters({}); setPage(1) }}\n sortable\n sorting={sorting}\n onSortingChange={(state) => { setSorting(state); setPage(1) }}\n perspective={{ tableId: 'directory.tenants.list' }}\n rowActions={(row) => (\n canManage ? (\n <RowActions\n items={[\n { id: 'edit', label: t('common.edit', 'Edit'), href: `/backend/directory/tenants/${row.id}/edit` },\n { id: 'delete', label: t('common.delete', 'Delete'), destructive: true, onSelect: () => handleDelete(row) },\n ]}\n />\n ) : null\n )}\n pagination={{ page, pageSize: 20, total, totalPages, onPageChange: setPage }}\n isLoading={isLoading}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -166,7 +166,7 @@ function FeatureTogglesTable() {
|
|
|
166
166
|
page: featureTogglesData?.page ?? 1,
|
|
167
167
|
pageSize: featureTogglesData?.pageSize ?? 25,
|
|
168
168
|
total: featureTogglesData?.total ?? 0,
|
|
169
|
-
totalPages: featureTogglesData?.totalPages ??
|
|
169
|
+
totalPages: featureTogglesData?.totalPages ?? 0,
|
|
170
170
|
onPageChange: handlePageChange
|
|
171
171
|
},
|
|
172
172
|
rowActions: (row) => /* @__PURE__ */ jsx(RowActions, { items: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/feature_toggles/components/FeatureTogglesTable.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { DataTable } from \"@open-mercato/ui/backend/DataTable\";\nimport { RowActions } from \"@open-mercato/ui/backend/RowActions\";\nimport { useT } from \"@open-mercato/shared/lib/i18n/context\";\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport * as React from 'react'\nimport type { FilterDef, FilterValues } from \"@open-mercato/ui/backend/FilterBar\"\nimport { useMutation } from '@tanstack/react-query'\nimport { deleteCrud, updateCrud } from \"@open-mercato/ui/backend/utils/crud\";\nimport { Button } from \"@open-mercato/ui/primitives/button\";\nimport { Badge } from \"@open-mercato/ui/primitives/badge\";\nimport Link from \"next/link\";\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog';\nimport { FeatureToggleType } from \"../data/entities\";\n\ntype Row = {\n id: string\n identifier: string\n name: string\n description: string\n category?: string\n type: FeatureToggleType\n}\n\ntype BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'muted'\n\nexport function FeatureTogglesTable() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const queryClient = useQueryClient()\n\n const featureToggleTypeLabelMap = React.useMemo(() => new Map<FeatureToggleType, { label: string; variant: BadgeVariant }>([\n ['boolean', { label: t('feature_toggles.types.boolean', 'Boolean'), variant: 'default' }],\n ['string', { label: t('feature_toggles.types.string', 'String'), variant: 'secondary' }],\n ['number', { label: t('feature_toggles.types.number', 'Number'), variant: 'outline' }],\n ['json', { label: t('feature_toggles.types.json', 'JSON'), variant: 'destructive' }],\n ]), [t])\n\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [sorting, setSorting] = React.useState<SortingState>([])\n const [pagination, setPagination] = React.useState({ page: 1, pageSize: 25 })\n\n const categoryFilterValue = typeof filterValues.category === 'string' ? filterValues.category.trim() : ''\n const nameFilterValue = typeof filterValues.name === 'string' ? filterValues.name.trim() : ''\n const identifierFilterValue = typeof filterValues.identifier === 'string' ? filterValues.identifier.trim() : ''\n const typeFilterValue = typeof filterValues.type === 'string' ? filterValues.type.trim() : ''\n const sortField = sorting.length > 0 ? sorting[0].id : 'category'\n const sortDir = sorting.length > 0 && sorting[0].desc ? 'desc' : 'asc'\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n if (categoryFilterValue) params.set('category', categoryFilterValue)\n if (nameFilterValue) params.set('name', nameFilterValue)\n if (identifierFilterValue) params.set('identifier', identifierFilterValue)\n if (typeFilterValue) params.set('type', typeFilterValue)\n\n params.set('sortField', sortField)\n params.set('sortDir', sortDir)\n\n params.set('page', pagination.page.toString())\n params.set('pageSize', pagination.pageSize.toString())\n\n return params.toString()\n }, [categoryFilterValue, identifierFilterValue, nameFilterValue, sortField, sortDir, typeFilterValue, pagination])\n\n const handleSortingChange = React.useCallback((newSorting: SortingState) => {\n setSorting(newSorting)\n }, [])\n\n const handlePageChange = React.useCallback((page: number) => {\n setPagination(prev => ({ ...prev, page }))\n }, [])\n\n const { data: featureTogglesData, isLoading } = useQuery({\n queryKey: ['feature_toggles', queryParams],\n queryFn: async () => {\n const call = await apiCall<{ items: Row[]; total: number; totalPages: number; page: number; pageSize: number; isSuperAdmin?: boolean }>(\n `/api/feature_toggles/global${queryParams ? `?${queryParams}` : ''}`,\n )\n if (!call.ok) {\n await raiseCrudError(call.response, t('feature_toggles.list.error.load', 'Failed to load feature toggles'))\n }\n return call.result ?? { items: [], total: 0, totalPages: 1, page: 1, pageSize: 25 }\n },\n })\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'identifier',\n label: t('feature_toggles.list.filters.identifier', 'Identifier'),\n type: 'text',\n },\n {\n id: 'name',\n label: t('feature_toggles.list.filters.name', 'Name'),\n type: 'text',\n },\n {\n id: 'category',\n label: t('feature_toggles.list.filters.category', 'Category'),\n type: 'text',\n },\n {\n id: 'type',\n label: t('feature_toggles.list.filters.type', 'Type'),\n type: 'select',\n options: [\n { label: t('feature_toggles.types.boolean', 'Boolean'), value: 'boolean' },\n { label: t('feature_toggles.types.string', 'String'), value: 'string' },\n { label: t('feature_toggles.types.number', 'Number'), value: 'number' },\n { label: t('feature_toggles.types.json', 'JSON'), value: 'json' },\n ],\n },\n ], [t])\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n setFilterValues(values)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n }, [])\n\n const deleteFeatureToggleMutation = useMutation({\n mutationFn: async (row: Row) => {\n await deleteCrud('feature_toggles/global', row.id)\n },\n onSuccess: async () => {\n await queryClient.invalidateQueries({ queryKey: ['feature_toggles'] })\n },\n })\n\n const handleDelete = React.useCallback(async (row: Row) => {\n const confirmed = await confirm({\n title: t('feature_toggles.list.confirmDelete', 'Delete feature toggle \"{identifier}\"?', { identifier: row.identifier }),\n variant: 'destructive',\n })\n if (!confirmed) return\n await deleteFeatureToggleMutation.mutateAsync(row)\n }, [confirm, deleteFeatureToggleMutation, t])\n\n const columns = React.useMemo<ColumnDef<Row>[]>(() => {\n const base: ColumnDef<Row>[] = [\n {\n accessorKey: 'category',\n header: t('feature_toggles.list.headers.category', 'Category'),\n enableSorting: true,\n cell: ({ row }) => {\n return row.original.category || '-'\n },\n },\n {\n accessorKey: 'identifier',\n header: t('feature_toggles.list.headers.identifier', 'Identifier'),\n enableSorting: true\n },\n {\n accessorKey: 'name',\n header: t('feature_toggles.list.headers.name', 'Name'),\n enableSorting: true\n },\n {\n accessorKey: 'type',\n header: t('feature_toggles.list.headers.type', 'Type'),\n enableSorting: true,\n cell: ({ row }) => {\n const typeInfo = featureToggleTypeLabelMap.get(row.original.type)\n if (!typeInfo) return '-'\n\n return (\n <Badge variant={typeInfo.variant}>\n {typeInfo.label}\n </Badge>\n )\n },\n },\n ]\n return base\n }, [t, featureToggleTypeLabelMap])\n\n return (\n <>\n <DataTable\n title={t('feature_toggles.global.help.title', 'Feature Toggles')}\n disableRowClick\n actions={\n <Button asChild>\n <Link href=\"/backend/feature-toggles/global/create\">\n {t('common.create', 'Create')}\n </Link>\n </Button>\n }\n columns={columns}\n data={featureTogglesData?.items ?? []}\n isLoading={isLoading}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n sorting={sorting}\n onSortingChange={handleSortingChange}\n sortable={true}\n pagination={{\n page: featureTogglesData?.page ?? 1,\n pageSize: featureTogglesData?.pageSize ?? 25,\n total: featureTogglesData?.total ?? 0,\n totalPages: featureTogglesData?.totalPages ??
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { DataTable } from \"@open-mercato/ui/backend/DataTable\";\nimport { RowActions } from \"@open-mercato/ui/backend/RowActions\";\nimport { useT } from \"@open-mercato/shared/lib/i18n/context\";\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport * as React from 'react'\nimport type { FilterDef, FilterValues } from \"@open-mercato/ui/backend/FilterBar\"\nimport { useMutation } from '@tanstack/react-query'\nimport { deleteCrud, updateCrud } from \"@open-mercato/ui/backend/utils/crud\";\nimport { Button } from \"@open-mercato/ui/primitives/button\";\nimport { Badge } from \"@open-mercato/ui/primitives/badge\";\nimport Link from \"next/link\";\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog';\nimport { FeatureToggleType } from \"../data/entities\";\n\ntype Row = {\n id: string\n identifier: string\n name: string\n description: string\n category?: string\n type: FeatureToggleType\n}\n\ntype BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'muted'\n\nexport function FeatureTogglesTable() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const queryClient = useQueryClient()\n\n const featureToggleTypeLabelMap = React.useMemo(() => new Map<FeatureToggleType, { label: string; variant: BadgeVariant }>([\n ['boolean', { label: t('feature_toggles.types.boolean', 'Boolean'), variant: 'default' }],\n ['string', { label: t('feature_toggles.types.string', 'String'), variant: 'secondary' }],\n ['number', { label: t('feature_toggles.types.number', 'Number'), variant: 'outline' }],\n ['json', { label: t('feature_toggles.types.json', 'JSON'), variant: 'destructive' }],\n ]), [t])\n\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [sorting, setSorting] = React.useState<SortingState>([])\n const [pagination, setPagination] = React.useState({ page: 1, pageSize: 25 })\n\n const categoryFilterValue = typeof filterValues.category === 'string' ? filterValues.category.trim() : ''\n const nameFilterValue = typeof filterValues.name === 'string' ? filterValues.name.trim() : ''\n const identifierFilterValue = typeof filterValues.identifier === 'string' ? filterValues.identifier.trim() : ''\n const typeFilterValue = typeof filterValues.type === 'string' ? filterValues.type.trim() : ''\n const sortField = sorting.length > 0 ? sorting[0].id : 'category'\n const sortDir = sorting.length > 0 && sorting[0].desc ? 'desc' : 'asc'\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n if (categoryFilterValue) params.set('category', categoryFilterValue)\n if (nameFilterValue) params.set('name', nameFilterValue)\n if (identifierFilterValue) params.set('identifier', identifierFilterValue)\n if (typeFilterValue) params.set('type', typeFilterValue)\n\n params.set('sortField', sortField)\n params.set('sortDir', sortDir)\n\n params.set('page', pagination.page.toString())\n params.set('pageSize', pagination.pageSize.toString())\n\n return params.toString()\n }, [categoryFilterValue, identifierFilterValue, nameFilterValue, sortField, sortDir, typeFilterValue, pagination])\n\n const handleSortingChange = React.useCallback((newSorting: SortingState) => {\n setSorting(newSorting)\n }, [])\n\n const handlePageChange = React.useCallback((page: number) => {\n setPagination(prev => ({ ...prev, page }))\n }, [])\n\n const { data: featureTogglesData, isLoading } = useQuery({\n queryKey: ['feature_toggles', queryParams],\n queryFn: async () => {\n const call = await apiCall<{ items: Row[]; total: number; totalPages: number; page: number; pageSize: number; isSuperAdmin?: boolean }>(\n `/api/feature_toggles/global${queryParams ? `?${queryParams}` : ''}`,\n )\n if (!call.ok) {\n await raiseCrudError(call.response, t('feature_toggles.list.error.load', 'Failed to load feature toggles'))\n }\n return call.result ?? { items: [], total: 0, totalPages: 1, page: 1, pageSize: 25 }\n },\n })\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'identifier',\n label: t('feature_toggles.list.filters.identifier', 'Identifier'),\n type: 'text',\n },\n {\n id: 'name',\n label: t('feature_toggles.list.filters.name', 'Name'),\n type: 'text',\n },\n {\n id: 'category',\n label: t('feature_toggles.list.filters.category', 'Category'),\n type: 'text',\n },\n {\n id: 'type',\n label: t('feature_toggles.list.filters.type', 'Type'),\n type: 'select',\n options: [\n { label: t('feature_toggles.types.boolean', 'Boolean'), value: 'boolean' },\n { label: t('feature_toggles.types.string', 'String'), value: 'string' },\n { label: t('feature_toggles.types.number', 'Number'), value: 'number' },\n { label: t('feature_toggles.types.json', 'JSON'), value: 'json' },\n ],\n },\n ], [t])\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n setFilterValues(values)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n }, [])\n\n const deleteFeatureToggleMutation = useMutation({\n mutationFn: async (row: Row) => {\n await deleteCrud('feature_toggles/global', row.id)\n },\n onSuccess: async () => {\n await queryClient.invalidateQueries({ queryKey: ['feature_toggles'] })\n },\n })\n\n const handleDelete = React.useCallback(async (row: Row) => {\n const confirmed = await confirm({\n title: t('feature_toggles.list.confirmDelete', 'Delete feature toggle \"{identifier}\"?', { identifier: row.identifier }),\n variant: 'destructive',\n })\n if (!confirmed) return\n await deleteFeatureToggleMutation.mutateAsync(row)\n }, [confirm, deleteFeatureToggleMutation, t])\n\n const columns = React.useMemo<ColumnDef<Row>[]>(() => {\n const base: ColumnDef<Row>[] = [\n {\n accessorKey: 'category',\n header: t('feature_toggles.list.headers.category', 'Category'),\n enableSorting: true,\n cell: ({ row }) => {\n return row.original.category || '-'\n },\n },\n {\n accessorKey: 'identifier',\n header: t('feature_toggles.list.headers.identifier', 'Identifier'),\n enableSorting: true\n },\n {\n accessorKey: 'name',\n header: t('feature_toggles.list.headers.name', 'Name'),\n enableSorting: true\n },\n {\n accessorKey: 'type',\n header: t('feature_toggles.list.headers.type', 'Type'),\n enableSorting: true,\n cell: ({ row }) => {\n const typeInfo = featureToggleTypeLabelMap.get(row.original.type)\n if (!typeInfo) return '-'\n\n return (\n <Badge variant={typeInfo.variant}>\n {typeInfo.label}\n </Badge>\n )\n },\n },\n ]\n return base\n }, [t, featureToggleTypeLabelMap])\n\n return (\n <>\n <DataTable\n title={t('feature_toggles.global.help.title', 'Feature Toggles')}\n disableRowClick\n actions={\n <Button asChild>\n <Link href=\"/backend/feature-toggles/global/create\">\n {t('common.create', 'Create')}\n </Link>\n </Button>\n }\n columns={columns}\n data={featureTogglesData?.items ?? []}\n isLoading={isLoading}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n sorting={sorting}\n onSortingChange={handleSortingChange}\n sortable={true}\n pagination={{\n page: featureTogglesData?.page ?? 1,\n pageSize: featureTogglesData?.pageSize ?? 25,\n total: featureTogglesData?.total ?? 0,\n totalPages: featureTogglesData?.totalPages ?? 0,\n onPageChange: handlePageChange,\n }}\n rowActions={(row) => (\n <RowActions items={[\n { id: 'edit', label: t('common.edit', 'Edit'), href: `/backend/feature-toggles/global/${row.id}/edit` },\n { id: 'view', label: t('common.view', 'Overrides'), href: `/backend/feature-toggles/global/${row.id}` },\n { id: 'delete', label: t('common.delete', 'Delete'), destructive: true, onSelect: () => { void handleDelete(row) } },\n ]} />\n )}\n />\n {ConfirmDialogElement}\n </>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -125,7 +125,7 @@ function OverridesTable() {
|
|
|
125
125
|
page: featureTogglesData?.page ?? 1,
|
|
126
126
|
pageSize: featureTogglesData?.pageSize ?? 25,
|
|
127
127
|
total: featureTogglesData?.total ?? 0,
|
|
128
|
-
totalPages: featureTogglesData?.totalPages ??
|
|
128
|
+
totalPages: featureTogglesData?.totalPages ?? 0,
|
|
129
129
|
onPageChange: handlePageChange
|
|
130
130
|
},
|
|
131
131
|
refreshButton: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/feature_toggles/components/OverridesTable.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport { DataTable } from \"@open-mercato/ui/backend/DataTable\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { apiCall } from \"@open-mercato/ui/backend/utils/apiCall\";\nimport { raiseCrudError } from \"@open-mercato/ui/backend/utils/serverErrors\";\nimport { useT } from \"@open-mercato/shared/lib/i18n/context\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { ColumnDef, SortingState } from \"@tanstack/react-table\";\nimport * as React from 'react'\nimport type { FilterDef, FilterValues } from \"@open-mercato/ui/backend/FilterBar\"\nimport { RowActions } from \"@open-mercato/ui/backend/RowActions\";\nimport { OverrideListResponse } from \"../data/validators\";\n\n\nexport default function OverridesTable() {\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [sorting, setSorting] = React.useState<SortingState>([])\n const [pagination, setPagination] = React.useState({ page: 1, pageSize: 25 })\n\n const t = useT()\n const queryClient = useQueryClient()\n\n const sortField = sorting.length > 0 ? sorting[0].id : undefined\n const sortDir = sorting.length > 0 && sorting[0].desc ? 'desc' : 'asc'\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n Object.entries(filterValues).forEach(([key, value]) => {\n params.set(key, value as string)\n })\n if (sortField) params.set('sortField', sortField)\n if (sorting.length > 0) params.set('sortDir', sortDir)\n\n params.set('page', pagination.page.toString())\n params.set('pageSize', pagination.pageSize.toString())\n\n return params.toString()\n }, [filterValues, sortField, sortDir, pagination])\n\n const { data: featureTogglesData, isLoading, error } = useQuery({\n queryKey: ['feature_toggle_overrides', queryParams],\n queryFn: async () => {\n const call = await apiCall<{\n items: OverrideListResponse[];\n total: number;\n totalPages: number;\n page: number;\n pageSize: number;\n isSuperAdmin?: boolean\n }>(`/api/feature_toggles/overrides?${queryParams}`)\n if (!call.ok) {\n await raiseCrudError(call.response, t('feature_toggles.list.error.load', 'Failed to load feature toggles'))\n }\n return call.result ?? {\n items: [],\n total: 0,\n totalPages: 1,\n page: 1,\n pageSize: 25\n }\n },\n })\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n setFilterValues(values)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n }, [])\n\n const handleSortingChange = React.useCallback((newSorting: SortingState) => {\n setSorting(newSorting)\n }, [])\n\n const handlePageChange = React.useCallback((page: number) => {\n setPagination(prev => ({ ...prev, page }))\n }, [])\n\n const handleRefresh = React.useCallback(() => {\n void queryClient.refetchQueries({ queryKey: ['feature_toggle_overrides'] })\n }, [queryClient])\n\n const columns = React.useMemo<ColumnDef<OverrideListResponse>[]>(() => {\n return [\n {\n accessorKey: 'tenantName',\n header: t('feature_toggles.overrides.headers.tenant', 'Tenant'),\n enableSorting: true\n },\n {\n accessorKey: 'identifier',\n header: t('feature_toggles.overrides.headers.identifier', 'Identifier'),\n enableSorting: true\n },\n {\n accessorKey: 'name',\n header: t('feature_toggles.overrides.headers.name', 'Name'),\n enableSorting: true\n },\n {\n accessorKey: 'category',\n header: t('feature_toggles.overrides.headers.category', 'Category'),\n enableSorting: true\n },\n {\n accessorKey: 'isOverride',\n header: t('feature_toggles.overrides.headers.overrideState', 'Override'),\n enableSorting: false,\n cell: ({ row }) => {\n return row.original.isOverride ? t('feature_toggles.overrides.headers.isOverride.true', 'Yes') : t('feature_toggles.overrides.headers.isOverride.false', 'No')\n },\n },\n ]\n }, [])\n\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'identifier',\n label: t('feature_toggles.list.filters.identifier', 'Identifier'),\n type: 'text',\n },\n {\n id: 'name',\n label: t('feature_toggles.list.filters.name', 'Name'),\n type: 'text',\n },\n {\n id: 'category',\n label: t('feature_toggles.list.filters.category', 'Category'),\n type: 'text',\n }\n ], [t])\n\n return (\n <DataTable\n title={t('feature_toggles.overrides.help.title', 'Feature Toggle Overrides')}\n columns={columns}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n data={featureTogglesData?.items ?? []}\n isLoading={isLoading}\n sorting={sorting}\n onSortingChange={handleSortingChange}\n sortable={true}\n pagination={{\n page: featureTogglesData?.page ?? 1,\n pageSize: featureTogglesData?.pageSize ?? 25,\n total: featureTogglesData?.total ?? 0,\n totalPages: featureTogglesData?.totalPages ??
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport { DataTable } from \"@open-mercato/ui/backend/DataTable\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { apiCall } from \"@open-mercato/ui/backend/utils/apiCall\";\nimport { raiseCrudError } from \"@open-mercato/ui/backend/utils/serverErrors\";\nimport { useT } from \"@open-mercato/shared/lib/i18n/context\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { ColumnDef, SortingState } from \"@tanstack/react-table\";\nimport * as React from 'react'\nimport type { FilterDef, FilterValues } from \"@open-mercato/ui/backend/FilterBar\"\nimport { RowActions } from \"@open-mercato/ui/backend/RowActions\";\nimport { OverrideListResponse } from \"../data/validators\";\n\n\nexport default function OverridesTable() {\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [sorting, setSorting] = React.useState<SortingState>([])\n const [pagination, setPagination] = React.useState({ page: 1, pageSize: 25 })\n\n const t = useT()\n const queryClient = useQueryClient()\n\n const sortField = sorting.length > 0 ? sorting[0].id : undefined\n const sortDir = sorting.length > 0 && sorting[0].desc ? 'desc' : 'asc'\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n Object.entries(filterValues).forEach(([key, value]) => {\n params.set(key, value as string)\n })\n if (sortField) params.set('sortField', sortField)\n if (sorting.length > 0) params.set('sortDir', sortDir)\n\n params.set('page', pagination.page.toString())\n params.set('pageSize', pagination.pageSize.toString())\n\n return params.toString()\n }, [filterValues, sortField, sortDir, pagination])\n\n const { data: featureTogglesData, isLoading, error } = useQuery({\n queryKey: ['feature_toggle_overrides', queryParams],\n queryFn: async () => {\n const call = await apiCall<{\n items: OverrideListResponse[];\n total: number;\n totalPages: number;\n page: number;\n pageSize: number;\n isSuperAdmin?: boolean\n }>(`/api/feature_toggles/overrides?${queryParams}`)\n if (!call.ok) {\n await raiseCrudError(call.response, t('feature_toggles.list.error.load', 'Failed to load feature toggles'))\n }\n return call.result ?? {\n items: [],\n total: 0,\n totalPages: 1,\n page: 1,\n pageSize: 25\n }\n },\n })\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n setFilterValues(values)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n }, [])\n\n const handleSortingChange = React.useCallback((newSorting: SortingState) => {\n setSorting(newSorting)\n }, [])\n\n const handlePageChange = React.useCallback((page: number) => {\n setPagination(prev => ({ ...prev, page }))\n }, [])\n\n const handleRefresh = React.useCallback(() => {\n void queryClient.refetchQueries({ queryKey: ['feature_toggle_overrides'] })\n }, [queryClient])\n\n const columns = React.useMemo<ColumnDef<OverrideListResponse>[]>(() => {\n return [\n {\n accessorKey: 'tenantName',\n header: t('feature_toggles.overrides.headers.tenant', 'Tenant'),\n enableSorting: true\n },\n {\n accessorKey: 'identifier',\n header: t('feature_toggles.overrides.headers.identifier', 'Identifier'),\n enableSorting: true\n },\n {\n accessorKey: 'name',\n header: t('feature_toggles.overrides.headers.name', 'Name'),\n enableSorting: true\n },\n {\n accessorKey: 'category',\n header: t('feature_toggles.overrides.headers.category', 'Category'),\n enableSorting: true\n },\n {\n accessorKey: 'isOverride',\n header: t('feature_toggles.overrides.headers.overrideState', 'Override'),\n enableSorting: false,\n cell: ({ row }) => {\n return row.original.isOverride ? t('feature_toggles.overrides.headers.isOverride.true', 'Yes') : t('feature_toggles.overrides.headers.isOverride.false', 'No')\n },\n },\n ]\n }, [])\n\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'identifier',\n label: t('feature_toggles.list.filters.identifier', 'Identifier'),\n type: 'text',\n },\n {\n id: 'name',\n label: t('feature_toggles.list.filters.name', 'Name'),\n type: 'text',\n },\n {\n id: 'category',\n label: t('feature_toggles.list.filters.category', 'Category'),\n type: 'text',\n }\n ], [t])\n\n return (\n <DataTable\n title={t('feature_toggles.overrides.help.title', 'Feature Toggle Overrides')}\n columns={columns}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n data={featureTogglesData?.items ?? []}\n isLoading={isLoading}\n sorting={sorting}\n onSortingChange={handleSortingChange}\n sortable={true}\n pagination={{\n page: featureTogglesData?.page ?? 1,\n pageSize: featureTogglesData?.pageSize ?? 25,\n total: featureTogglesData?.total ?? 0,\n totalPages: featureTogglesData?.totalPages ?? 0,\n onPageChange: handlePageChange,\n }}\n refreshButton={{\n label: t('feature_toggles.list.table.refresh', 'Refresh'),\n onRefresh: handleRefresh,\n isRefreshing: isLoading,\n }}\n rowActions={(row) => (\n <RowActions items={[\n { id: 'edit', label: t('common.edit', 'Edit'), href: `/backend/feature-toggles/global/${row.toggleId}` },\n ]} />\n )}\n error={error ? error.message : undefined}\n />\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -85,7 +85,7 @@ function MessagesInboxPageClient() {
|
|
|
85
85
|
total: Number(call.result?.total ?? 0),
|
|
86
86
|
page: Number(call.result?.page ?? page),
|
|
87
87
|
pageSize: Number(call.result?.pageSize ?? pageSize),
|
|
88
|
-
totalPages: Number(call.result?.totalPages ??
|
|
88
|
+
totalPages: Number(call.result?.totalPages ?? 0)
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
});
|
|
@@ -289,7 +289,7 @@ function MessagesInboxPageClient() {
|
|
|
289
289
|
}, [folderMenuOpen]);
|
|
290
290
|
const rows = listQuery.data?.items ?? [];
|
|
291
291
|
const total = listQuery.data?.total ?? 0;
|
|
292
|
-
const totalPages = listQuery.data?.totalPages ??
|
|
292
|
+
const totalPages = listQuery.data?.totalPages ?? 0;
|
|
293
293
|
return /* @__PURE__ */ jsx("div", { className: "space-y-4", children: /* @__PURE__ */ jsx(
|
|
294
294
|
DataTable,
|
|
295
295
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/components/MessagesInboxPageClient.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Archive, ChevronDown, FilePenLine, Inbox, Layers, Send } from 'lucide-react'\nimport { getMessageUiComponentRegistry } from './utils/typeUiRegistry'\nimport { DefaultMessageListItem } from './defaults/DefaultMessageListItem'\n\ntype MessageFolder = 'inbox' | 'sent' | 'drafts' | 'archived' | 'all'\n\ntype MessageListItem = {\n id: string\n type: string\n subject: string\n bodyPreview: string\n senderUserId: string\n senderName?: string | null\n senderEmail?: string | null\n priority: string\n status: string\n hasObjects: boolean\n objectCount: number\n hasAttachments: boolean\n attachmentCount: number\n hasActions: boolean\n actionTaken?: string | null\n sentAt?: string | null\n readAt?: string | null\n threadId?: string | null\n}\n\ntype MessageListResponse = {\n items?: MessageListItem[]\n total?: number\n page?: number\n pageSize?: number\n totalPages?: number\n}\n\ntype MessageTypeItem = {\n type: string\n module: string\n labelKey: string\n ui?: {\n listItemComponent?: string | null\n } | null\n}\n\ntype UserListItem = {\n id: string\n email?: string | null\n name?: string | null\n}\n\nfunction toErrorMessage(payload: unknown): string | null {\n if (!payload) return null\n if (typeof payload === 'string') return payload\n if (Array.isArray(payload)) {\n for (const item of payload) {\n const nested = toErrorMessage(item)\n if (nested) return nested\n }\n return null\n }\n if (typeof payload === 'object') {\n const record = payload as Record<string, unknown>\n return (\n toErrorMessage(record.error)\n ?? toErrorMessage(record.message)\n ?? toErrorMessage(record.detail)\n ?? toErrorMessage(record.details)\n ?? null\n )\n }\n return null\n}\n\nexport function MessagesInboxPageClient() {\n const router = useRouter()\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n\n const [folder, setFolder] = React.useState<MessageFolder>('inbox')\n const [folderMenuOpen, setFolderMenuOpen] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [page, setPage] = React.useState(1)\n const pageSize = 20\n const folderMenuRef = React.useRef<HTMLDivElement | null>(null)\n const messageUiRegistry = React.useMemo(() => getMessageUiComponentRegistry(), [])\n\n const listQuery = useQuery({\n queryKey: [\n 'messages',\n 'list',\n folder,\n search,\n page,\n pageSize,\n JSON.stringify(filterValues),\n scopeVersion,\n ],\n queryFn: async () => {\n const params = new URLSearchParams()\n params.set('folder', folder)\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n\n if (search.trim()) {\n params.set('search', search.trim())\n }\n\n const status = typeof filterValues.status === 'string' ? filterValues.status.trim() : ''\n const type = typeof filterValues.type === 'string' ? filterValues.type.trim() : ''\n const hasObjects = typeof filterValues.hasObjects === 'string' ? filterValues.hasObjects.trim() : ''\n const hasAttachments = typeof filterValues.hasAttachments === 'string' ? filterValues.hasAttachments.trim() : ''\n const hasActions = typeof filterValues.hasActions === 'string' ? filterValues.hasActions.trim() : ''\n const senderId = typeof filterValues.senderId === 'string' ? filterValues.senderId.trim() : ''\n const since = typeof filterValues.since === 'string' ? filterValues.since.trim() : ''\n\n if (status) params.set('status', status)\n if (type) params.set('type', type)\n if (hasObjects) params.set('hasObjects', hasObjects)\n if (hasAttachments) params.set('hasAttachments', hasAttachments)\n if (hasActions) params.set('hasActions', hasActions)\n if (senderId) params.set('senderId', senderId)\n if (since) params.set('since', since)\n\n const call = await apiCall<MessageListResponse>(`/api/messages?${params.toString()}`)\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadListFailed', 'Failed to load messages.'),\n )\n }\n\n return {\n items: Array.isArray(call.result?.items) ? call.result?.items ?? [] : [],\n total: Number(call.result?.total ?? 0),\n page: Number(call.result?.page ?? page),\n pageSize: Number(call.result?.pageSize ?? pageSize),\n totalPages: Number(call.result?.totalPages ?? 1),\n }\n },\n })\n\n const messageTypesQuery = useQuery({\n queryKey: ['messages', 'types', scopeVersion],\n queryFn: async () => {\n const call = await apiCall<{ items?: MessageTypeItem[] }>('/api/messages/types')\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadTypesFailed', 'Failed to load message types.'),\n )\n }\n return Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n },\n })\n\n React.useEffect(() => {\n if (!listQuery.error) return\n flash(\n listQuery.error instanceof Error\n ? listQuery.error.message\n : t('messages.errors.loadListFailed', 'Failed to load messages.'),\n 'error',\n )\n }, [listQuery.error, t])\n\n React.useEffect(() => {\n if (!messageTypesQuery.error) return\n flash(\n messageTypesQuery.error instanceof Error\n ? messageTypesQuery.error.message\n : t('messages.errors.loadTypesFailed', 'Failed to load message types.'),\n 'error',\n )\n }, [messageTypesQuery.error, t])\n\n const messageTypeLabelMap = React.useMemo(() => {\n const map: Record<string, string> = {}\n for (const item of messageTypesQuery.data ?? []) {\n map[item.type] = t(item.labelKey, item.type)\n }\n return map\n }, [messageTypesQuery.data, t])\n\n const loadSenderOptions = React.useCallback(async (query?: string) => {\n const params = new URLSearchParams()\n params.set('page', '1')\n params.set('pageSize', '20')\n if (query && query.trim().length > 0) {\n params.set('search', query.trim())\n }\n\n const call = await apiCall<{ items?: UserListItem[] }>(`/api/auth/users?${params.toString()}`)\n if (!call.ok) return []\n\n const items = Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n return items.flatMap((item) => {\n if (!item || typeof item.id !== 'string' || item.id.trim().length === 0) return []\n const name = typeof item.name === 'string' && item.name.trim().length > 0 ? item.name.trim() : null\n const email = typeof item.email === 'string' && item.email.trim().length > 0 ? item.email.trim() : null\n const label = name ?? email ?? item.id\n return [{\n value: item.id,\n label,\n description: email && email !== label ? email : null,\n }]\n })\n }, [])\n\n const listItemComponentKeyByType = React.useMemo(() => {\n const map: Record<string, string | null> = {}\n for (const item of messageTypesQuery.data ?? []) {\n map[item.type] = item.ui?.listItemComponent ?? null\n }\n return map\n }, [messageTypesQuery.data])\n\n const filters = React.useMemo<FilterDef[]>(() => {\n const typeOptions = (messageTypesQuery.data ?? []).map((item) => ({\n value: item.type,\n label: t(item.labelKey, item.type),\n }))\n\n return [\n {\n id: 'status',\n label: t('messages.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: '', label: t('messages.filters.all', 'All') },\n { value: 'unread', label: t('messages.status.unread', 'Unread') },\n { value: 'read', label: t('messages.status.read', 'Read') },\n { value: 'archived', label: t('messages.status.archived', 'Archived') },\n ],\n },\n {\n id: 'type',\n label: t('messages.filters.type', 'Type'),\n type: 'select',\n options: [{ value: '', label: t('messages.filters.all', 'All') }, ...typeOptions],\n },\n {\n id: 'hasObjects',\n label: t('messages.filters.hasObjects', 'Objects'),\n type: 'select',\n options: [\n { value: '', label: t('messages.filters.all', 'All') },\n { value: 'true', label: t('common.yes', 'Yes') },\n { value: 'false', label: t('common.no', 'No') },\n ],\n },\n {\n id: 'hasAttachments',\n label: t('messages.filters.hasAttachments', 'Attachments'),\n type: 'select',\n options: [\n { value: '', label: t('messages.filters.all', 'All') },\n { value: 'true', label: t('common.yes', 'Yes') },\n { value: 'false', label: t('common.no', 'No') },\n ],\n },\n {\n id: 'hasActions',\n label: t('messages.filters.hasActions', 'Actions'),\n type: 'select',\n options: [\n { value: '', label: t('messages.filters.all', 'All') },\n { value: 'true', label: t('common.yes', 'Yes') },\n { value: 'false', label: t('common.no', 'No') },\n ],\n },\n {\n id: 'senderId',\n label: t('messages.filters.sender', 'Sender'),\n type: 'select',\n options: [{ value: '', label: t('messages.filters.all', 'All') }],\n loadOptions: loadSenderOptions,\n },\n {\n id: 'since',\n label: t('messages.filters.since', 'Sent after'),\n type: 'text',\n placeholder: t('messages.filters.sincePlaceholder', 'YYYY-MM-DDTHH:mm:ssZ'),\n },\n ]\n }, [loadSenderOptions, messageTypesQuery.data, t])\n\n const columns = React.useMemo<ColumnDef<MessageListItem>[]>(() => [\n {\n accessorKey: 'message',\n header: t('messages.title', 'Messages'),\n meta: {\n truncate: false,\n maxWidth: '100%',\n },\n cell: ({ row }) => {\n const item = row.original\n const listItemComponentKey = listItemComponentKeyByType[item.type]\n const ListItemComponent = listItemComponentKey\n ? messageUiRegistry.listItemComponents[listItemComponentKey] ?? null\n : null\n const ComponentToUse = ListItemComponent || DefaultMessageListItem\n\n return (\n <ComponentToUse\n message={{\n id: item.id,\n type: item.type,\n typeLabel: messageTypeLabelMap[item.type] ?? item.type,\n subject: item.subject,\n body: item.bodyPreview,\n bodyFormat: 'text' as const,\n priority: (item.priority as 'low' | 'normal' | 'high' | 'urgent') ?? 'normal',\n sentAt: item.sentAt ? new Date(item.sentAt) : null,\n senderName: item.senderName || item.senderEmail || item.senderUserId,\n hasObjects: item.hasObjects,\n objectCount: item.objectCount,\n hasAttachments: item.hasAttachments,\n attachmentCount: item.attachmentCount,\n hasActions: item.hasActions,\n actionTaken: item.actionTaken ?? null,\n unread: item.status === 'unread',\n }}\n onClick={() => router.push(`/backend/messages/${item.id}`)}\n />\n )\n },\n },\n ], [listItemComponentKeyByType, messageTypeLabelMap, messageUiRegistry, router, t])\n\n const folderOptions = React.useMemo(() => [\n { id: 'inbox' as const, label: t('messages.folder.inbox', 'Inbox'), icon: Inbox },\n { id: 'sent' as const, label: t('messages.folder.sent', 'Sent'), icon: Send },\n { id: 'drafts' as const, label: t('messages.folder.drafts', 'Drafts'), icon: FilePenLine },\n { id: 'archived' as const, label: t('messages.folder.archived', 'Archived'), icon: Archive },\n { id: 'all' as const, label: t('messages.folder.all', 'All'), icon: Layers },\n ], [t])\n\n const activeFolderOption = folderOptions.find((option) => option.id === folder) ?? folderOptions[0]\n const ActiveFolderIcon = activeFolderOption.icon\n\n React.useEffect(() => {\n if (!folderMenuOpen) return\n const handleClickOutside = (event: MouseEvent) => {\n if (!folderMenuRef.current) return\n const target = event.target\n if (target instanceof Node && !folderMenuRef.current.contains(target)) {\n setFolderMenuOpen(false)\n }\n }\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setFolderMenuOpen(false)\n }\n document.addEventListener('mousedown', handleClickOutside)\n document.addEventListener('keydown', handleEscape)\n return () => {\n document.removeEventListener('mousedown', handleClickOutside)\n document.removeEventListener('keydown', handleEscape)\n }\n }, [folderMenuOpen])\n\n const rows = listQuery.data?.items ?? []\n const total = listQuery.data?.total ?? 0\n const totalPages = listQuery.data?.totalPages ?? 1\n\n return (\n <div className=\"space-y-4\">\n <DataTable\n title={t('messages.title', 'Messages')}\n columns={columns}\n data={rows}\n searchValue={search}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n searchPlaceholder={t('messages.searchPlaceholder', 'Search messages')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(value) => {\n setFilterValues(value)\n setPage(1)\n }}\n onFiltersClear={() => {\n setFilterValues({})\n setPage(1)\n }}\n isLoading={listQuery.isLoading || listQuery.isFetching}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative\" ref={folderMenuRef}>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"gap-2\"\n aria-expanded={folderMenuOpen}\n aria-haspopup=\"menu\"\n onClick={() => setFolderMenuOpen((value) => !value)}\n >\n <ActiveFolderIcon className=\"h-4 w-4\" aria-hidden />\n <span>{t('messages.folder.selector', 'Folder')}:</span>\n <span>{activeFolderOption.label}</span>\n <ChevronDown className=\"h-4 w-4 opacity-70\" aria-hidden />\n </Button>\n {folderMenuOpen ? (\n <div\n className=\"absolute right-0 z-20 mt-1 min-w-52 rounded-md border bg-background p-1 shadow\"\n role=\"menu\"\n >\n {folderOptions.map((option) => {\n const Icon = option.icon\n const isActive = option.id === folder\n return (\n <button\n key={option.id}\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={isActive}\n className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm hover:bg-accent ${isActive ? 'bg-accent/60' : ''}`}\n onClick={() => {\n setFolder(option.id)\n setPage(1)\n setFolderMenuOpen(false)\n }}\n >\n <Icon className=\"h-4 w-4\" aria-hidden />\n <span>{option.label}</span>\n </button>\n )\n })}\n </div>\n ) : null}\n </div>\n <Button asChild>\n <Link href=\"/backend/messages/compose\">{t('messages.compose', 'Compose message')}</Link>\n </Button>\n </div>\n }\n onRowClick={(row) => {\n router.push(`/backend/messages/${row.id}`)\n }}\n perspective={{ tableId: 'messages.inbox' }}\n embedded\n />\n </div>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Archive, ChevronDown, FilePenLine, Inbox, Layers, Send } from 'lucide-react'\nimport { getMessageUiComponentRegistry } from './utils/typeUiRegistry'\nimport { DefaultMessageListItem } from './defaults/DefaultMessageListItem'\n\ntype MessageFolder = 'inbox' | 'sent' | 'drafts' | 'archived' | 'all'\n\ntype MessageListItem = {\n id: string\n type: string\n subject: string\n bodyPreview: string\n senderUserId: string\n senderName?: string | null\n senderEmail?: string | null\n priority: string\n status: string\n hasObjects: boolean\n objectCount: number\n hasAttachments: boolean\n attachmentCount: number\n hasActions: boolean\n actionTaken?: string | null\n sentAt?: string | null\n readAt?: string | null\n threadId?: string | null\n}\n\ntype MessageListResponse = {\n items?: MessageListItem[]\n total?: number\n page?: number\n pageSize?: number\n totalPages?: number\n}\n\ntype MessageTypeItem = {\n type: string\n module: string\n labelKey: string\n ui?: {\n listItemComponent?: string | null\n } | null\n}\n\ntype UserListItem = {\n id: string\n email?: string | null\n name?: string | null\n}\n\nfunction toErrorMessage(payload: unknown): string | null {\n if (!payload) return null\n if (typeof payload === 'string') return payload\n if (Array.isArray(payload)) {\n for (const item of payload) {\n const nested = toErrorMessage(item)\n if (nested) return nested\n }\n return null\n }\n if (typeof payload === 'object') {\n const record = payload as Record<string, unknown>\n return (\n toErrorMessage(record.error)\n ?? toErrorMessage(record.message)\n ?? toErrorMessage(record.detail)\n ?? toErrorMessage(record.details)\n ?? null\n )\n }\n return null\n}\n\nexport function MessagesInboxPageClient() {\n const router = useRouter()\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n\n const [folder, setFolder] = React.useState<MessageFolder>('inbox')\n const [folderMenuOpen, setFolderMenuOpen] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [page, setPage] = React.useState(1)\n const pageSize = 20\n const folderMenuRef = React.useRef<HTMLDivElement | null>(null)\n const messageUiRegistry = React.useMemo(() => getMessageUiComponentRegistry(), [])\n\n const listQuery = useQuery({\n queryKey: [\n 'messages',\n 'list',\n folder,\n search,\n page,\n pageSize,\n JSON.stringify(filterValues),\n scopeVersion,\n ],\n queryFn: async () => {\n const params = new URLSearchParams()\n params.set('folder', folder)\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n\n if (search.trim()) {\n params.set('search', search.trim())\n }\n\n const status = typeof filterValues.status === 'string' ? filterValues.status.trim() : ''\n const type = typeof filterValues.type === 'string' ? filterValues.type.trim() : ''\n const hasObjects = typeof filterValues.hasObjects === 'string' ? filterValues.hasObjects.trim() : ''\n const hasAttachments = typeof filterValues.hasAttachments === 'string' ? filterValues.hasAttachments.trim() : ''\n const hasActions = typeof filterValues.hasActions === 'string' ? filterValues.hasActions.trim() : ''\n const senderId = typeof filterValues.senderId === 'string' ? filterValues.senderId.trim() : ''\n const since = typeof filterValues.since === 'string' ? filterValues.since.trim() : ''\n\n if (status) params.set('status', status)\n if (type) params.set('type', type)\n if (hasObjects) params.set('hasObjects', hasObjects)\n if (hasAttachments) params.set('hasAttachments', hasAttachments)\n if (hasActions) params.set('hasActions', hasActions)\n if (senderId) params.set('senderId', senderId)\n if (since) params.set('since', since)\n\n const call = await apiCall<MessageListResponse>(`/api/messages?${params.toString()}`)\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadListFailed', 'Failed to load messages.'),\n )\n }\n\n return {\n items: Array.isArray(call.result?.items) ? call.result?.items ?? [] : [],\n total: Number(call.result?.total ?? 0),\n page: Number(call.result?.page ?? page),\n pageSize: Number(call.result?.pageSize ?? pageSize),\n totalPages: Number(call.result?.totalPages ?? 0),\n }\n },\n })\n\n const messageTypesQuery = useQuery({\n queryKey: ['messages', 'types', scopeVersion],\n queryFn: async () => {\n const call = await apiCall<{ items?: MessageTypeItem[] }>('/api/messages/types')\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadTypesFailed', 'Failed to load message types.'),\n )\n }\n return Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n },\n })\n\n React.useEffect(() => {\n if (!listQuery.error) return\n flash(\n listQuery.error instanceof Error\n ? listQuery.error.message\n : t('messages.errors.loadListFailed', 'Failed to load messages.'),\n 'error',\n )\n }, [listQuery.error, t])\n\n React.useEffect(() => {\n if (!messageTypesQuery.error) return\n flash(\n messageTypesQuery.error instanceof Error\n ? messageTypesQuery.error.message\n : t('messages.errors.loadTypesFailed', 'Failed to load message types.'),\n 'error',\n )\n }, [messageTypesQuery.error, t])\n\n const messageTypeLabelMap = React.useMemo(() => {\n const map: Record<string, string> = {}\n for (const item of messageTypesQuery.data ?? []) {\n map[item.type] = t(item.labelKey, item.type)\n }\n return map\n }, [messageTypesQuery.data, t])\n\n const loadSenderOptions = React.useCallback(async (query?: string) => {\n const params = new URLSearchParams()\n params.set('page', '1')\n params.set('pageSize', '20')\n if (query && query.trim().length > 0) {\n params.set('search', query.trim())\n }\n\n const call = await apiCall<{ items?: UserListItem[] }>(`/api/auth/users?${params.toString()}`)\n if (!call.ok) return []\n\n const items = Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n return items.flatMap((item) => {\n if (!item || typeof item.id !== 'string' || item.id.trim().length === 0) return []\n const name = typeof item.name === 'string' && item.name.trim().length > 0 ? item.name.trim() : null\n const email = typeof item.email === 'string' && item.email.trim().length > 0 ? item.email.trim() : null\n const label = name ?? email ?? item.id\n return [{\n value: item.id,\n label,\n description: email && email !== label ? email : null,\n }]\n })\n }, [])\n\n const listItemComponentKeyByType = React.useMemo(() => {\n const map: Record<string, string | null> = {}\n for (const item of messageTypesQuery.data ?? []) {\n map[item.type] = item.ui?.listItemComponent ?? null\n }\n return map\n }, [messageTypesQuery.data])\n\n const filters = React.useMemo<FilterDef[]>(() => {\n const typeOptions = (messageTypesQuery.data ?? []).map((item) => ({\n value: item.type,\n label: t(item.labelKey, item.type),\n }))\n\n return [\n {\n id: 'status',\n label: t('messages.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: '', label: t('messages.filters.all', 'All') },\n { value: 'unread', label: t('messages.status.unread', 'Unread') },\n { value: 'read', label: t('messages.status.read', 'Read') },\n { value: 'archived', label: t('messages.status.archived', 'Archived') },\n ],\n },\n {\n id: 'type',\n label: t('messages.filters.type', 'Type'),\n type: 'select',\n options: [{ value: '', label: t('messages.filters.all', 'All') }, ...typeOptions],\n },\n {\n id: 'hasObjects',\n label: t('messages.filters.hasObjects', 'Objects'),\n type: 'select',\n options: [\n { value: '', label: t('messages.filters.all', 'All') },\n { value: 'true', label: t('common.yes', 'Yes') },\n { value: 'false', label: t('common.no', 'No') },\n ],\n },\n {\n id: 'hasAttachments',\n label: t('messages.filters.hasAttachments', 'Attachments'),\n type: 'select',\n options: [\n { value: '', label: t('messages.filters.all', 'All') },\n { value: 'true', label: t('common.yes', 'Yes') },\n { value: 'false', label: t('common.no', 'No') },\n ],\n },\n {\n id: 'hasActions',\n label: t('messages.filters.hasActions', 'Actions'),\n type: 'select',\n options: [\n { value: '', label: t('messages.filters.all', 'All') },\n { value: 'true', label: t('common.yes', 'Yes') },\n { value: 'false', label: t('common.no', 'No') },\n ],\n },\n {\n id: 'senderId',\n label: t('messages.filters.sender', 'Sender'),\n type: 'select',\n options: [{ value: '', label: t('messages.filters.all', 'All') }],\n loadOptions: loadSenderOptions,\n },\n {\n id: 'since',\n label: t('messages.filters.since', 'Sent after'),\n type: 'text',\n placeholder: t('messages.filters.sincePlaceholder', 'YYYY-MM-DDTHH:mm:ssZ'),\n },\n ]\n }, [loadSenderOptions, messageTypesQuery.data, t])\n\n const columns = React.useMemo<ColumnDef<MessageListItem>[]>(() => [\n {\n accessorKey: 'message',\n header: t('messages.title', 'Messages'),\n meta: {\n truncate: false,\n maxWidth: '100%',\n },\n cell: ({ row }) => {\n const item = row.original\n const listItemComponentKey = listItemComponentKeyByType[item.type]\n const ListItemComponent = listItemComponentKey\n ? messageUiRegistry.listItemComponents[listItemComponentKey] ?? null\n : null\n const ComponentToUse = ListItemComponent || DefaultMessageListItem\n\n return (\n <ComponentToUse\n message={{\n id: item.id,\n type: item.type,\n typeLabel: messageTypeLabelMap[item.type] ?? item.type,\n subject: item.subject,\n body: item.bodyPreview,\n bodyFormat: 'text' as const,\n priority: (item.priority as 'low' | 'normal' | 'high' | 'urgent') ?? 'normal',\n sentAt: item.sentAt ? new Date(item.sentAt) : null,\n senderName: item.senderName || item.senderEmail || item.senderUserId,\n hasObjects: item.hasObjects,\n objectCount: item.objectCount,\n hasAttachments: item.hasAttachments,\n attachmentCount: item.attachmentCount,\n hasActions: item.hasActions,\n actionTaken: item.actionTaken ?? null,\n unread: item.status === 'unread',\n }}\n onClick={() => router.push(`/backend/messages/${item.id}`)}\n />\n )\n },\n },\n ], [listItemComponentKeyByType, messageTypeLabelMap, messageUiRegistry, router, t])\n\n const folderOptions = React.useMemo(() => [\n { id: 'inbox' as const, label: t('messages.folder.inbox', 'Inbox'), icon: Inbox },\n { id: 'sent' as const, label: t('messages.folder.sent', 'Sent'), icon: Send },\n { id: 'drafts' as const, label: t('messages.folder.drafts', 'Drafts'), icon: FilePenLine },\n { id: 'archived' as const, label: t('messages.folder.archived', 'Archived'), icon: Archive },\n { id: 'all' as const, label: t('messages.folder.all', 'All'), icon: Layers },\n ], [t])\n\n const activeFolderOption = folderOptions.find((option) => option.id === folder) ?? folderOptions[0]\n const ActiveFolderIcon = activeFolderOption.icon\n\n React.useEffect(() => {\n if (!folderMenuOpen) return\n const handleClickOutside = (event: MouseEvent) => {\n if (!folderMenuRef.current) return\n const target = event.target\n if (target instanceof Node && !folderMenuRef.current.contains(target)) {\n setFolderMenuOpen(false)\n }\n }\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setFolderMenuOpen(false)\n }\n document.addEventListener('mousedown', handleClickOutside)\n document.addEventListener('keydown', handleEscape)\n return () => {\n document.removeEventListener('mousedown', handleClickOutside)\n document.removeEventListener('keydown', handleEscape)\n }\n }, [folderMenuOpen])\n\n const rows = listQuery.data?.items ?? []\n const total = listQuery.data?.total ?? 0\n const totalPages = listQuery.data?.totalPages ?? 0\n\n return (\n <div className=\"space-y-4\">\n <DataTable\n title={t('messages.title', 'Messages')}\n columns={columns}\n data={rows}\n searchValue={search}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n searchPlaceholder={t('messages.searchPlaceholder', 'Search messages')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(value) => {\n setFilterValues(value)\n setPage(1)\n }}\n onFiltersClear={() => {\n setFilterValues({})\n setPage(1)\n }}\n isLoading={listQuery.isLoading || listQuery.isFetching}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative\" ref={folderMenuRef}>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"gap-2\"\n aria-expanded={folderMenuOpen}\n aria-haspopup=\"menu\"\n onClick={() => setFolderMenuOpen((value) => !value)}\n >\n <ActiveFolderIcon className=\"h-4 w-4\" aria-hidden />\n <span>{t('messages.folder.selector', 'Folder')}:</span>\n <span>{activeFolderOption.label}</span>\n <ChevronDown className=\"h-4 w-4 opacity-70\" aria-hidden />\n </Button>\n {folderMenuOpen ? (\n <div\n className=\"absolute right-0 z-20 mt-1 min-w-52 rounded-md border bg-background p-1 shadow\"\n role=\"menu\"\n >\n {folderOptions.map((option) => {\n const Icon = option.icon\n const isActive = option.id === folder\n return (\n <button\n key={option.id}\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={isActive}\n className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm hover:bg-accent ${isActive ? 'bg-accent/60' : ''}`}\n onClick={() => {\n setFolder(option.id)\n setPage(1)\n setFolderMenuOpen(false)\n }}\n >\n <Icon className=\"h-4 w-4\" aria-hidden />\n <span>{option.label}</span>\n </button>\n )\n })}\n </div>\n ) : null}\n </div>\n <Button asChild>\n <Link href=\"/backend/messages/compose\">{t('messages.compose', 'Compose message')}</Link>\n </Button>\n </div>\n }\n onRowClick={(row) => {\n router.push(`/backend/messages/${row.id}`)\n }}\n perspective={{ tableId: 'messages.inbox' }}\n embedded\n />\n </div>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.4.6-develop-
|
|
3
|
+
"version": "0.4.6-develop-c2b70de148",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
209
|
"dependencies": {
|
|
210
|
-
"@open-mercato/shared": "0.4.6-develop-
|
|
210
|
+
"@open-mercato/shared": "0.4.6-develop-c2b70de148",
|
|
211
211
|
"@types/html-to-text": "^9.0.4",
|
|
212
212
|
"@types/semver": "^7.5.8",
|
|
213
213
|
"@xyflow/react": "^12.6.0",
|
|
@@ -1033,7 +1033,7 @@ export function AttachmentLibrary() {
|
|
|
1033
1033
|
}, [deleteTarget, queryClient, selectedRow, t])
|
|
1034
1034
|
|
|
1035
1035
|
const total = data?.total ?? 0
|
|
1036
|
-
const totalPages = data?.totalPages ??
|
|
1036
|
+
const totalPages = data?.totalPages ?? 0
|
|
1037
1037
|
return (
|
|
1038
1038
|
<>
|
|
1039
1039
|
<DataTable<AttachmentRow>
|
|
@@ -111,7 +111,7 @@ export default function CategoriesDataTable() {
|
|
|
111
111
|
|
|
112
112
|
const rows = data?.items ?? []
|
|
113
113
|
const total = data?.total ?? 0
|
|
114
|
-
const totalPages = data?.totalPages ??
|
|
114
|
+
const totalPages = data?.totalPages ?? 0
|
|
115
115
|
|
|
116
116
|
const columns = React.useMemo<ColumnDef<CategoryRow>[]>(() => [
|
|
117
117
|
{
|
|
@@ -155,7 +155,7 @@ export function usePersonTasks({
|
|
|
155
155
|
const mapped = Array.isArray(payload.items) ? payload.items.map(mapRowToSummary) : []
|
|
156
156
|
setPageInfo({
|
|
157
157
|
page: payload.page ?? 1,
|
|
158
|
-
totalPages: payload.totalPages ??
|
|
158
|
+
totalPages: payload.totalPages ?? 0,
|
|
159
159
|
total: payload.total ?? mapped.length,
|
|
160
160
|
})
|
|
161
161
|
setError(null)
|
|
@@ -172,7 +172,7 @@ export default function DirectoryOrganizationsPage() {
|
|
|
172
172
|
return base
|
|
173
173
|
}, [isSuperAdmin, t])
|
|
174
174
|
const total = data?.total ?? 0
|
|
175
|
-
const totalPages = data?.totalPages ??
|
|
175
|
+
const totalPages = data?.totalPages ?? 0
|
|
176
176
|
|
|
177
177
|
const handleDelete = React.useCallback(async (org: OrganizationRow) => {
|
|
178
178
|
const confirmLabel = t('directory.organizations.list.confirmDelete', 'Archive organization "{{name}}"?', { name: org.name })
|
|
@@ -113,7 +113,7 @@ export default function DirectoryTenantsPage() {
|
|
|
113
113
|
|
|
114
114
|
const rows = data?.items ?? []
|
|
115
115
|
const total = data?.total ?? 0
|
|
116
|
-
const totalPages = data?.totalPages ??
|
|
116
|
+
const totalPages = data?.totalPages ?? 0
|
|
117
117
|
|
|
118
118
|
const handleDelete = React.useCallback(async (tenant: TenantRow) => {
|
|
119
119
|
const confirmMessage = t('directory.tenants.list.confirmDelete', 'Delete tenant "{{name}}"? This will archive it.').replace('{{name}}', tenant.name)
|
|
@@ -207,7 +207,7 @@ export function FeatureTogglesTable() {
|
|
|
207
207
|
page: featureTogglesData?.page ?? 1,
|
|
208
208
|
pageSize: featureTogglesData?.pageSize ?? 25,
|
|
209
209
|
total: featureTogglesData?.total ?? 0,
|
|
210
|
-
totalPages: featureTogglesData?.totalPages ??
|
|
210
|
+
totalPages: featureTogglesData?.totalPages ?? 0,
|
|
211
211
|
onPageChange: handlePageChange,
|
|
212
212
|
}}
|
|
213
213
|
rowActions={(row) => (
|
|
@@ -151,7 +151,7 @@ export default function OverridesTable() {
|
|
|
151
151
|
page: featureTogglesData?.page ?? 1,
|
|
152
152
|
pageSize: featureTogglesData?.pageSize ?? 25,
|
|
153
153
|
total: featureTogglesData?.total ?? 0,
|
|
154
|
-
totalPages: featureTogglesData?.totalPages ??
|
|
154
|
+
totalPages: featureTogglesData?.totalPages ?? 0,
|
|
155
155
|
onPageChange: handlePageChange,
|
|
156
156
|
}}
|
|
157
157
|
refreshButton={{
|
|
@@ -149,7 +149,7 @@ export function MessagesInboxPageClient() {
|
|
|
149
149
|
total: Number(call.result?.total ?? 0),
|
|
150
150
|
page: Number(call.result?.page ?? page),
|
|
151
151
|
pageSize: Number(call.result?.pageSize ?? pageSize),
|
|
152
|
-
totalPages: Number(call.result?.totalPages ??
|
|
152
|
+
totalPages: Number(call.result?.totalPages ?? 0),
|
|
153
153
|
}
|
|
154
154
|
},
|
|
155
155
|
})
|
|
@@ -375,7 +375,7 @@ export function MessagesInboxPageClient() {
|
|
|
375
375
|
|
|
376
376
|
const rows = listQuery.data?.items ?? []
|
|
377
377
|
const total = listQuery.data?.total ?? 0
|
|
378
|
-
const totalPages = listQuery.data?.totalPages ??
|
|
378
|
+
const totalPages = listQuery.data?.totalPages ?? 0
|
|
379
379
|
|
|
380
380
|
return (
|
|
381
381
|
<div className="space-y-4">
|