@open-mercato/ui 0.5.1-develop.2663.2c29774b5b → 0.5.1-develop.2681.c559bb2bc3
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/.turbo/turbo-build.log +2 -2
- package/dist/backend/CrudForm.js +187 -39
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/Page.js +12 -4
- package/dist/backend/Page.js.map +2 -2
- package/dist/backend/confirm-dialog/ConfirmDialog.js +7 -4
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
- package/dist/backend/crud/CollapsibleGroup.js +88 -0
- package/dist/backend/crud/CollapsibleGroup.js.map +7 -0
- package/dist/backend/crud/CollapsibleZoneLayout.js +178 -0
- package/dist/backend/crud/CollapsibleZoneLayout.js.map +7 -0
- package/dist/backend/crud/useGroupCollapse.js +24 -0
- package/dist/backend/crud/useGroupCollapse.js.map +7 -0
- package/dist/backend/crud/useGroupOrder.js +61 -0
- package/dist/backend/crud/useGroupOrder.js.map +7 -0
- package/dist/backend/crud/usePersistedBooleanFlag.js +29 -0
- package/dist/backend/crud/usePersistedBooleanFlag.js.map +7 -0
- package/dist/backend/crud/useZoneCollapse.js +24 -0
- package/dist/backend/crud/useZoneCollapse.js.map +7 -0
- package/dist/backend/detail/AttachmentsSection.js +77 -33
- package/dist/backend/detail/AttachmentsSection.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +82 -6
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +16 -2
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/inputs/SwitchableMarkdownInput.js +3 -1
- package/dist/backend/inputs/SwitchableMarkdownInput.js.map +2 -2
- package/dist/primitives/avatar.js +59 -0
- package/dist/primitives/avatar.js.map +7 -0
- package/package.json +3 -3
- package/src/backend/CrudForm.tsx +230 -21
- package/src/backend/Page.tsx +20 -4
- package/src/backend/__tests__/AttachmentsSection.test.tsx +82 -0
- package/src/backend/__tests__/CollapsibleZoneLayout.test.tsx +171 -0
- package/src/backend/__tests__/CrudForm.validation.test.tsx +4 -4
- package/src/backend/__tests__/NotesSection.test.tsx +63 -0
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +9 -4
- package/src/backend/crud/CollapsibleGroup.tsx +111 -0
- package/src/backend/crud/CollapsibleZoneLayout.tsx +234 -0
- package/src/backend/crud/__tests__/useGroupCollapse.test.ts +38 -0
- package/src/backend/crud/__tests__/useGroupOrder.test.ts +63 -0
- package/src/backend/crud/__tests__/usePersistedBooleanFlag.test.ts +49 -0
- package/src/backend/crud/__tests__/useZoneCollapse.test.ts +31 -0
- package/src/backend/crud/useGroupCollapse.ts +22 -0
- package/src/backend/crud/useGroupOrder.ts +74 -0
- package/src/backend/crud/usePersistedBooleanFlag.ts +35 -0
- package/src/backend/crud/useZoneCollapse.ts +22 -0
- package/src/backend/detail/AttachmentsSection.tsx +81 -38
- package/src/backend/detail/NotesSection.tsx +99 -6
- package/src/backend/icons/lucideRegistry.generated.tsx +16 -2
- package/src/backend/inputs/SwitchableMarkdownInput.tsx +3 -1
- package/src/primitives/__tests__/avatar.test.tsx +64 -0
- package/src/primitives/avatar.tsx +75 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
import { usePersistedBooleanFlag } from "./usePersistedBooleanFlag.js";
|
|
4
|
+
function getStorageKey(pageType) {
|
|
5
|
+
return `om:zone1-collapsed:${pageType}`;
|
|
6
|
+
}
|
|
7
|
+
function useZoneCollapse(pageType) {
|
|
8
|
+
const { value: collapsed, toggle, setValue } = usePersistedBooleanFlag(
|
|
9
|
+
getStorageKey(pageType),
|
|
10
|
+
false
|
|
11
|
+
);
|
|
12
|
+
const setCollapsed = useCallback((next) => {
|
|
13
|
+
if (typeof next === "function") {
|
|
14
|
+
setValue((prev) => next(prev));
|
|
15
|
+
} else {
|
|
16
|
+
setValue(next);
|
|
17
|
+
}
|
|
18
|
+
}, [setValue]);
|
|
19
|
+
return { collapsed, toggle, setCollapsed };
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
useZoneCollapse
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=useZoneCollapse.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/backend/crud/useZoneCollapse.ts"],
|
|
4
|
+
"sourcesContent": ["'use client'\nimport { useCallback } from 'react'\nimport { usePersistedBooleanFlag } from './usePersistedBooleanFlag'\n\nfunction getStorageKey(pageType: string) {\n return `om:zone1-collapsed:${pageType}`\n}\n\nexport function useZoneCollapse(pageType: string) {\n const { value: collapsed, toggle, setValue } = usePersistedBooleanFlag(\n getStorageKey(pageType),\n false,\n )\n const setCollapsed = useCallback((next: boolean | ((prev: boolean) => boolean)) => {\n if (typeof next === 'function') {\n setValue((prev) => (next as (prev: boolean) => boolean)(prev))\n } else {\n setValue(next)\n }\n }, [setValue])\n return { collapsed, toggle, setCollapsed }\n}\n"],
|
|
5
|
+
"mappings": ";AACA,SAAS,mBAAmB;AAC5B,SAAS,+BAA+B;AAExC,SAAS,cAAc,UAAkB;AACvC,SAAO,sBAAsB,QAAQ;AACvC;AAEO,SAAS,gBAAgB,UAAkB;AAChD,QAAM,EAAE,OAAO,WAAW,QAAQ,SAAS,IAAI;AAAA,IAC7C,cAAc,QAAQ;AAAA,IACtB;AAAA,EACF;AACA,QAAM,eAAe,YAAY,CAAC,SAAiD;AACjF,QAAI,OAAO,SAAS,YAAY;AAC9B,eAAS,CAAC,SAAU,KAAoC,IAAI,CAAC;AAAA,IAC/D,OAAO;AACL,eAAS,IAAI;AAAA,IACf;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AACb,SAAO,EAAE,WAAW,QAAQ,aAAa;AAC3C;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -23,6 +23,8 @@ function AttachmentsSectionImpl({
|
|
|
23
23
|
}) {
|
|
24
24
|
const t = useT();
|
|
25
25
|
const [items, setItems] = React.useState([]);
|
|
26
|
+
const [page, setPage] = React.useState(1);
|
|
27
|
+
const [totalPages, setTotalPages] = React.useState(1);
|
|
26
28
|
const [loading, setLoading] = React.useState(false);
|
|
27
29
|
const [error, setError] = React.useState(null);
|
|
28
30
|
const [isUploading, setIsUploading] = React.useState(false);
|
|
@@ -32,13 +34,19 @@ function AttachmentsSectionImpl({
|
|
|
32
34
|
const [deleteOpen, setDeleteOpen] = React.useState(false);
|
|
33
35
|
const [deleteTarget, setDeleteTarget] = React.useState(null);
|
|
34
36
|
const fileInputRef = React.useRef(null);
|
|
35
|
-
const load = React.useCallback(async () => {
|
|
37
|
+
const load = React.useCallback(async (targetPage = 1, replace = true) => {
|
|
36
38
|
if (!recordId) return;
|
|
37
39
|
setLoading(true);
|
|
38
40
|
setError(null);
|
|
39
41
|
try {
|
|
42
|
+
const params = new URLSearchParams({
|
|
43
|
+
entityId,
|
|
44
|
+
recordId,
|
|
45
|
+
page: String(targetPage),
|
|
46
|
+
pageSize: "24"
|
|
47
|
+
});
|
|
40
48
|
const call = await apiCall(
|
|
41
|
-
`/api/attachments
|
|
49
|
+
`/api/attachments?${params.toString()}`,
|
|
42
50
|
void 0,
|
|
43
51
|
{ fallback: { items: [] } }
|
|
44
52
|
);
|
|
@@ -47,7 +55,10 @@ function AttachmentsSectionImpl({
|
|
|
47
55
|
throw new Error(message);
|
|
48
56
|
}
|
|
49
57
|
const payload = call.result ?? { items: [] };
|
|
50
|
-
|
|
58
|
+
const nextItems = Array.isArray(payload.items) ? payload.items : [];
|
|
59
|
+
setItems((current) => replace ? nextItems : [...current, ...nextItems]);
|
|
60
|
+
setPage(typeof payload.page === "number" ? payload.page : targetPage);
|
|
61
|
+
setTotalPages(typeof payload.totalPages === "number" ? payload.totalPages : 1);
|
|
51
62
|
} catch (err) {
|
|
52
63
|
setError(err?.message || t("attachments.library.errors.load", "Failed to load attachments."));
|
|
53
64
|
} finally {
|
|
@@ -59,6 +70,8 @@ function AttachmentsSectionImpl({
|
|
|
59
70
|
void load();
|
|
60
71
|
} else {
|
|
61
72
|
setItems([]);
|
|
73
|
+
setPage(1);
|
|
74
|
+
setTotalPages(1);
|
|
62
75
|
setError(null);
|
|
63
76
|
}
|
|
64
77
|
}, [load, recordId]);
|
|
@@ -83,7 +96,7 @@ function AttachmentsSectionImpl({
|
|
|
83
96
|
throw new Error(message);
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
|
-
await load();
|
|
99
|
+
await load(1, true);
|
|
87
100
|
onChanged?.();
|
|
88
101
|
} catch (err) {
|
|
89
102
|
setError(err?.message || t("attachments.library.upload.failed", "Upload failed."));
|
|
@@ -136,7 +149,7 @@ function AttachmentsSectionImpl({
|
|
|
136
149
|
}
|
|
137
150
|
setDeleteOpen(false);
|
|
138
151
|
setDeleteTarget(null);
|
|
139
|
-
await load();
|
|
152
|
+
await load(1, true);
|
|
140
153
|
onChanged?.();
|
|
141
154
|
} catch (err) {
|
|
142
155
|
setError(err?.message || t("attachments.library.errors.delete", "Failed to delete attachment."));
|
|
@@ -161,7 +174,7 @@ function AttachmentsSectionImpl({
|
|
|
161
174
|
throw new Error(message);
|
|
162
175
|
}
|
|
163
176
|
setMetadataOpen(false);
|
|
164
|
-
await load();
|
|
177
|
+
await load(1, true);
|
|
165
178
|
onChanged?.();
|
|
166
179
|
},
|
|
167
180
|
[load, onChanged, t]
|
|
@@ -207,44 +220,75 @@ function AttachmentsSectionImpl({
|
|
|
207
220
|
compact ? "grid-cols-2 sm:grid-cols-4 lg:grid-cols-5" : "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
|
|
208
221
|
), children: items.map((item) => {
|
|
209
222
|
return /* @__PURE__ */ jsxs(
|
|
210
|
-
"
|
|
223
|
+
"div",
|
|
211
224
|
{
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
className: "group flex flex-col overflow-hidden rounded-lg border bg-card text-left cursor-pointer transition-shadow hover:shadow-sm",
|
|
225
|
+
role: "group",
|
|
226
|
+
className: "group relative flex flex-col overflow-hidden rounded-lg border bg-card text-left transition-shadow hover:shadow-sm focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
|
|
215
227
|
children: [
|
|
216
|
-
/* @__PURE__ */
|
|
217
|
-
|
|
228
|
+
/* @__PURE__ */ jsxs(
|
|
229
|
+
Button,
|
|
218
230
|
{
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
variant: "ghost",
|
|
228
|
-
size: "icon",
|
|
229
|
-
className: "absolute right-2 top-2 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100",
|
|
230
|
-
onClick: (event) => {
|
|
231
|
-
event.stopPropagation();
|
|
232
|
-
openDeleteDialog(item);
|
|
233
|
-
},
|
|
234
|
-
children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4 text-destructive" })
|
|
231
|
+
type: "button",
|
|
232
|
+
variant: "ghost",
|
|
233
|
+
"aria-label": item.fileName,
|
|
234
|
+
onClick: () => openMetadataDialog(item),
|
|
235
|
+
onKeyDown: (event) => {
|
|
236
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
openMetadataDialog(item);
|
|
235
239
|
}
|
|
236
|
-
|
|
240
|
+
},
|
|
241
|
+
className: "flex h-auto w-full flex-col items-stretch rounded-lg p-0 text-left hover:bg-transparent focus-visible:outline-none focus-visible:ring-0",
|
|
242
|
+
children: [
|
|
243
|
+
/* @__PURE__ */ jsx(
|
|
244
|
+
AttachmentVisualPreview,
|
|
245
|
+
{
|
|
246
|
+
fileName: item.fileName,
|
|
247
|
+
mimeType: item.mimeType,
|
|
248
|
+
thumbnailUrl: item.thumbnailUrl,
|
|
249
|
+
className: compact ? "aspect-[2/1] w-full" : "aspect-[4/3] w-full"
|
|
250
|
+
}
|
|
251
|
+
),
|
|
252
|
+
/* @__PURE__ */ jsxs("div", { className: cn("space-y-1 w-full", compact ? "p-2" : "p-3"), children: [
|
|
253
|
+
/* @__PURE__ */ jsx("div", { className: cn("truncate font-medium", compact ? "text-xs" : "text-sm"), title: item.fileName, children: item.fileName }),
|
|
254
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: formatAttachmentFileSize(item.fileSize) })
|
|
255
|
+
] })
|
|
256
|
+
]
|
|
237
257
|
}
|
|
238
258
|
),
|
|
239
|
-
/* @__PURE__ */
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
259
|
+
/* @__PURE__ */ jsx(
|
|
260
|
+
Button,
|
|
261
|
+
{
|
|
262
|
+
type: "button",
|
|
263
|
+
variant: "ghost",
|
|
264
|
+
size: "icon",
|
|
265
|
+
className: "absolute right-2 top-2 z-10 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100",
|
|
266
|
+
onClick: (event) => {
|
|
267
|
+
event.stopPropagation();
|
|
268
|
+
openDeleteDialog(item);
|
|
269
|
+
},
|
|
270
|
+
"aria-label": t("attachments.library.delete", "Delete attachment"),
|
|
271
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4 text-destructive" })
|
|
272
|
+
}
|
|
273
|
+
)
|
|
243
274
|
]
|
|
244
275
|
},
|
|
245
276
|
item.id
|
|
246
277
|
);
|
|
247
278
|
}) }) : /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t("attachments.library.table.empty", "No attachments found.") }),
|
|
279
|
+
items.length > 0 && page < totalPages ? /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(
|
|
280
|
+
Button,
|
|
281
|
+
{
|
|
282
|
+
type: "button",
|
|
283
|
+
variant: "outline",
|
|
284
|
+
size: "sm",
|
|
285
|
+
onClick: () => {
|
|
286
|
+
void load(page + 1, false);
|
|
287
|
+
},
|
|
288
|
+
disabled: loading,
|
|
289
|
+
children: t("attachments.library.loadMore", "Load more")
|
|
290
|
+
}
|
|
291
|
+
) }) : null,
|
|
248
292
|
/* @__PURE__ */ jsx(
|
|
249
293
|
AttachmentMetadataDialog,
|
|
250
294
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/detail/AttachmentsSection.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Upload, Trash2, File, FileText, FileSpreadsheet, FileArchive, FileAudio, FileVideo, FileCode } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { AttachmentVisualPreview, formatAttachmentFileSize } from './AttachmentVisualPreview'\nimport { AttachmentDeleteDialog } from './AttachmentDeleteDialog'\nimport { AttachmentMetadataDialog, type AttachmentItem, type AttachmentMetadataSavePayload } from './AttachmentMetadataDialog'\nimport { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '../injection/useRegisteredComponent'\n\ntype AttachmentsResponse = {\n items?: AttachmentItem[]\n error?: string\n}\n\ntype Props = {\n entityId: string\n recordId: string | null\n title?: string\n description?: string\n className?: string\n showHeader?: boolean\n compact?: boolean\n onChanged?: () => void\n}\n\nfunction AttachmentsSectionImpl({\n entityId,\n recordId,\n title,\n description,\n className,\n showHeader = true,\n compact = false,\n onChanged,\n}: Props) {\n const t = useT()\n const [items, setItems] = React.useState<AttachmentItem[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [isUploading, setIsUploading] = React.useState(false)\n const [isDragOver, setIsDragOver] = React.useState(false)\n const [metadataOpen, setMetadataOpen] = React.useState(false)\n const [selectedItem, setSelectedItem] = React.useState<AttachmentItem | null>(null)\n const [deleteOpen, setDeleteOpen] = React.useState(false)\n const [deleteTarget, setDeleteTarget] = React.useState<AttachmentItem | null>(null)\n const fileInputRef = React.useRef<HTMLInputElement | null>(null)\n\n const load = React.useCallback(async () => {\n if (!recordId) return\n setLoading(true)\n setError(null)\n try {\n const call = await apiCall<AttachmentsResponse>(\n `/api/attachments?entityId=${encodeURIComponent(entityId)}&recordId=${encodeURIComponent(recordId)}`,\n undefined,\n { fallback: { items: [] } },\n )\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.errors.load', 'Failed to load attachments.')\n throw new Error(message)\n }\n const payload = call.result ?? { items: [] }\n setItems(Array.isArray(payload.items) ? payload.items : [])\n } catch (err: any) {\n setError(err?.message || t('attachments.library.errors.load', 'Failed to load attachments.'))\n } finally {\n setLoading(false)\n }\n }, [entityId, recordId, t])\n\n React.useEffect(() => {\n if (recordId) {\n void load()\n } else {\n setItems([])\n setError(null)\n }\n }, [load, recordId])\n\n const acceptFiles = React.useCallback(\n async (files: FileList | null) => {\n if (!files || !files.length || !recordId) return\n setError(null)\n setIsUploading(true)\n try {\n for (const file of Array.from(files)) {\n const fd = new FormData()\n fd.set('entityId', entityId)\n fd.set('recordId', recordId)\n fd.set('file', file)\n const call = await apiCall<{ ok?: boolean; item?: AttachmentItem; error?: string }>(\n '/api/attachments',\n { method: 'POST', body: fd },\n { fallback: null },\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 }\n await load()\n onChanged?.()\n } catch (err: any) {\n setError(err?.message || t('attachments.library.upload.failed', 'Upload failed.'))\n } finally {\n setIsUploading(false)\n if (fileInputRef.current) {\n fileInputRef.current.value = ''\n }\n }\n },\n [entityId, load, onChanged, recordId, t],\n )\n\n const handleDrop = React.useCallback(\n (event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragOver(false)\n void acceptFiles(event.dataTransfer?.files ?? null)\n },\n [acceptFiles],\n )\n\n const handleDragOver = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragOver(true)\n }, [])\n\n const handleDragLeave = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragOver(false)\n }, [])\n\n const openMetadataDialog = React.useCallback((item: AttachmentItem) => {\n setSelectedItem(item)\n setMetadataOpen(true)\n }, [])\n\n const openDeleteDialog = React.useCallback((item: AttachmentItem) => {\n setDeleteTarget(item)\n setDeleteOpen(true)\n }, [])\n\n const handleDelete = React.useCallback(async () => {\n if (!deleteTarget) return\n try {\n const call = await apiCall<{ error?: string }>(\n `/api/attachments?id=${encodeURIComponent(deleteTarget.id)}`,\n { method: 'DELETE' },\n )\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.errors.delete', 'Failed to delete attachment.')\n throw new Error(message)\n }\n setDeleteOpen(false)\n setDeleteTarget(null)\n await load()\n onChanged?.()\n } catch (err: any) {\n setError(err?.message || t('attachments.library.errors.delete', 'Failed to delete attachment.'))\n }\n }, [deleteTarget, load, onChanged, t])\n\n const handleMetadataSave = React.useCallback(\n async (id: string, payload: AttachmentMetadataSavePayload) => {\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 = call.result?.error || t('attachments.library.metadata.error', 'Failed to update metadata.')\n throw new Error(message)\n }\n setMetadataOpen(false)\n await load()\n onChanged?.()\n },\n [load, onChanged, t],\n )\n\n const sectionTitle = title ?? t('attachments.library.title', 'Attachments')\n const sectionDescription =\n description ?? t('attachments.library.description', 'Browse, tag, and manage every file stored in this workspace.')\n\n return (\n <div className={cn('space-y-4', className)}>\n {showHeader ? (\n <div className=\"space-y-1\">\n <div className=\"text-base font-medium\">{sectionTitle}</div>\n <div className=\"text-sm text-muted-foreground\">{sectionDescription}</div>\n </div>\n ) : null}\n\n {!recordId ? (\n <div className=\"rounded-md border border-dashed border-border/70 px-4 py-6 text-sm text-muted-foreground\">\n {t('attachments.library.upload.saveFirst', 'Save the record before uploading files.')}\n </div>\n ) : (\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 )}\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\">\n {t('attachments.library.upload.dropHint', 'Drag and drop files here or click to upload.')}\n </p>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={() => fileInputRef.current?.click()} disabled={isUploading}>\n {isUploading ? t('attachments.library.upload.submitting', 'Uploading\u2026') : t('attachments.library.upload.choose', 'Choose files')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n className=\"hidden\"\n onChange={(event) => void acceptFiles(event.target.files)}\n />\n </div>\n )}\n\n {error ? <p className=\"text-xs font-medium text-red-600\">{error}</p> : null}\n\n {loading ? (\n <div className=\"text-sm text-muted-foreground\">{t('attachments.library.loading', 'Loading attachments\u2026')}</div>\n ) : items.length ? (\n <div className={cn(\n 'grid gap-3',\n compact ? 'grid-cols-2 sm:grid-cols-4 lg:grid-cols-5' : 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',\n )}>\n {items.map((item) => {\n return (\n <button\n key={item.id}\n type=\"button\"\n onClick={() => openMetadataDialog(item)}\n className=\"group flex flex-col overflow-hidden rounded-lg border bg-card text-left cursor-pointer transition-shadow hover:shadow-sm\"\n >\n <AttachmentVisualPreview\n fileName={item.fileName}\n mimeType={item.mimeType}\n thumbnailUrl={item.thumbnailUrl}\n className={compact ? 'aspect-[2/1]' : 'aspect-[4/3]'}\n overlay={(\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute right-2 top-2 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100\"\n onClick={(event) => {\n event.stopPropagation()\n openDeleteDialog(item)\n }}\n >\n <Trash2 className=\"h-4 w-4 text-destructive\" />\n </Button>\n )}\n />\n <div className={cn('space-y-1', compact ? 'p-2' : 'p-3')}>\n <div className={cn('truncate font-medium', compact ? 'text-xs' : 'text-sm')} title={item.fileName}>\n {item.fileName}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {formatAttachmentFileSize(item.fileSize)}\n </div>\n </div>\n </button>\n )\n })}\n </div>\n ) : (\n <div className=\"text-sm text-muted-foreground\">\n {t('attachments.library.table.empty', 'No attachments found.')}\n </div>\n )}\n\n <AttachmentMetadataDialog\n open={metadataOpen}\n onOpenChange={setMetadataOpen}\n item={selectedItem}\n availableTags={[]}\n onSave={handleMetadataSave}\n />\n <AttachmentDeleteDialog\n open={deleteOpen}\n onOpenChange={setDeleteOpen}\n fileName={deleteTarget?.fileName}\n onConfirm={handleDelete}\n isDeleting={false}\n />\n </div>\n )\n}\n\nexport function AttachmentsSection(props: Props) {\n const handle = ComponentReplacementHandles.section('ui.detail', 'AttachmentsSection')\n const Resolved = useRegisteredComponent<Props>(\n handle,\n AttachmentsSectionImpl as React.ComponentType<Props>,\n )\n\n return (\n <div data-component-handle={handle}>\n <Resolved {...props} />\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Upload, Trash2, File, FileText, FileSpreadsheet, FileArchive, FileAudio, FileVideo, FileCode } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { AttachmentVisualPreview, formatAttachmentFileSize } from './AttachmentVisualPreview'\nimport { AttachmentDeleteDialog } from './AttachmentDeleteDialog'\nimport { AttachmentMetadataDialog, type AttachmentItem, type AttachmentMetadataSavePayload } from './AttachmentMetadataDialog'\nimport { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '../injection/useRegisteredComponent'\n\ntype AttachmentsResponse = {\n items?: AttachmentItem[]\n total?: number\n page?: number\n pageSize?: number\n totalPages?: number\n error?: string\n}\n\ntype Props = {\n entityId: string\n recordId: string | null\n title?: string\n description?: string\n className?: string\n showHeader?: boolean\n compact?: boolean\n onChanged?: () => void\n}\n\nfunction AttachmentsSectionImpl({\n entityId,\n recordId,\n title,\n description,\n className,\n showHeader = true,\n compact = false,\n onChanged,\n}: Props) {\n const t = useT()\n const [items, setItems] = React.useState<AttachmentItem[]>([])\n const [page, setPage] = React.useState(1)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [isUploading, setIsUploading] = React.useState(false)\n const [isDragOver, setIsDragOver] = React.useState(false)\n const [metadataOpen, setMetadataOpen] = React.useState(false)\n const [selectedItem, setSelectedItem] = React.useState<AttachmentItem | null>(null)\n const [deleteOpen, setDeleteOpen] = React.useState(false)\n const [deleteTarget, setDeleteTarget] = React.useState<AttachmentItem | null>(null)\n const fileInputRef = React.useRef<HTMLInputElement | null>(null)\n\n const load = React.useCallback(async (targetPage = 1, replace = true) => {\n if (!recordId) return\n setLoading(true)\n setError(null)\n try {\n const params = new URLSearchParams({\n entityId,\n recordId,\n page: String(targetPage),\n pageSize: '24',\n })\n const call = await apiCall<AttachmentsResponse>(\n `/api/attachments?${params.toString()}`,\n undefined,\n { fallback: { items: [] } },\n )\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.errors.load', 'Failed to load attachments.')\n throw new Error(message)\n }\n const payload = call.result ?? { items: [] }\n const nextItems = Array.isArray(payload.items) ? payload.items : []\n setItems((current) => (replace ? nextItems : [...current, ...nextItems]))\n setPage(typeof payload.page === 'number' ? payload.page : targetPage)\n setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : 1)\n } catch (err: any) {\n setError(err?.message || t('attachments.library.errors.load', 'Failed to load attachments.'))\n } finally {\n setLoading(false)\n }\n }, [entityId, recordId, t])\n\n React.useEffect(() => {\n if (recordId) {\n void load()\n } else {\n setItems([])\n setPage(1)\n setTotalPages(1)\n setError(null)\n }\n }, [load, recordId])\n\n const acceptFiles = React.useCallback(\n async (files: FileList | null) => {\n if (!files || !files.length || !recordId) return\n setError(null)\n setIsUploading(true)\n try {\n for (const file of Array.from(files)) {\n const fd = new FormData()\n fd.set('entityId', entityId)\n fd.set('recordId', recordId)\n fd.set('file', file)\n const call = await apiCall<{ ok?: boolean; item?: AttachmentItem; error?: string }>(\n '/api/attachments',\n { method: 'POST', body: fd },\n { fallback: null },\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 }\n await load(1, true)\n onChanged?.()\n } catch (err: any) {\n setError(err?.message || t('attachments.library.upload.failed', 'Upload failed.'))\n } finally {\n setIsUploading(false)\n if (fileInputRef.current) {\n fileInputRef.current.value = ''\n }\n }\n },\n [entityId, load, onChanged, recordId, t],\n )\n\n const handleDrop = React.useCallback(\n (event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragOver(false)\n void acceptFiles(event.dataTransfer?.files ?? null)\n },\n [acceptFiles],\n )\n\n const handleDragOver = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragOver(true)\n }, [])\n\n const handleDragLeave = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragOver(false)\n }, [])\n\n const openMetadataDialog = React.useCallback((item: AttachmentItem) => {\n setSelectedItem(item)\n setMetadataOpen(true)\n }, [])\n\n const openDeleteDialog = React.useCallback((item: AttachmentItem) => {\n setDeleteTarget(item)\n setDeleteOpen(true)\n }, [])\n\n const handleDelete = React.useCallback(async () => {\n if (!deleteTarget) return\n try {\n const call = await apiCall<{ error?: string }>(\n `/api/attachments?id=${encodeURIComponent(deleteTarget.id)}`,\n { method: 'DELETE' },\n )\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.errors.delete', 'Failed to delete attachment.')\n throw new Error(message)\n }\n setDeleteOpen(false)\n setDeleteTarget(null)\n await load(1, true)\n onChanged?.()\n } catch (err: any) {\n setError(err?.message || t('attachments.library.errors.delete', 'Failed to delete attachment.'))\n }\n }, [deleteTarget, load, onChanged, t])\n\n const handleMetadataSave = React.useCallback(\n async (id: string, payload: AttachmentMetadataSavePayload) => {\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 = call.result?.error || t('attachments.library.metadata.error', 'Failed to update metadata.')\n throw new Error(message)\n }\n setMetadataOpen(false)\n await load(1, true)\n onChanged?.()\n },\n [load, onChanged, t],\n )\n\n const sectionTitle = title ?? t('attachments.library.title', 'Attachments')\n const sectionDescription =\n description ?? t('attachments.library.description', 'Browse, tag, and manage every file stored in this workspace.')\n\n return (\n <div className={cn('space-y-4', className)}>\n {showHeader ? (\n <div className=\"space-y-1\">\n <div className=\"text-base font-medium\">{sectionTitle}</div>\n <div className=\"text-sm text-muted-foreground\">{sectionDescription}</div>\n </div>\n ) : null}\n\n {!recordId ? (\n <div className=\"rounded-md border border-dashed border-border/70 px-4 py-6 text-sm text-muted-foreground\">\n {t('attachments.library.upload.saveFirst', 'Save the record before uploading files.')}\n </div>\n ) : (\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 )}\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\">\n {t('attachments.library.upload.dropHint', 'Drag and drop files here or click to upload.')}\n </p>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={() => fileInputRef.current?.click()} disabled={isUploading}>\n {isUploading ? t('attachments.library.upload.submitting', 'Uploading\u2026') : t('attachments.library.upload.choose', 'Choose files')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n className=\"hidden\"\n onChange={(event) => void acceptFiles(event.target.files)}\n />\n </div>\n )}\n\n {error ? <p className=\"text-xs font-medium text-red-600\">{error}</p> : null}\n\n {loading ? (\n <div className=\"text-sm text-muted-foreground\">{t('attachments.library.loading', 'Loading attachments\u2026')}</div>\n ) : items.length ? (\n <div className={cn(\n 'grid gap-3',\n compact ? 'grid-cols-2 sm:grid-cols-4 lg:grid-cols-5' : 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',\n )}>\n {items.map((item) => {\n return (\n <div\n key={item.id}\n role=\"group\"\n className=\"group relative flex flex-col overflow-hidden rounded-lg border bg-card text-left transition-shadow hover:shadow-sm focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2\"\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n aria-label={item.fileName}\n onClick={() => openMetadataDialog(item)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n openMetadataDialog(item)\n }\n }}\n className=\"flex h-auto w-full flex-col items-stretch rounded-lg p-0 text-left hover:bg-transparent focus-visible:outline-none focus-visible:ring-0\"\n >\n <AttachmentVisualPreview\n fileName={item.fileName}\n mimeType={item.mimeType}\n thumbnailUrl={item.thumbnailUrl}\n className={compact ? 'aspect-[2/1] w-full' : 'aspect-[4/3] w-full'}\n />\n <div className={cn('space-y-1 w-full', compact ? 'p-2' : 'p-3')}>\n <div className={cn('truncate font-medium', compact ? 'text-xs' : 'text-sm')} title={item.fileName}>\n {item.fileName}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {formatAttachmentFileSize(item.fileSize)}\n </div>\n </div>\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute right-2 top-2 z-10 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100\"\n onClick={(event) => {\n event.stopPropagation()\n openDeleteDialog(item)\n }}\n aria-label={t('attachments.library.delete', 'Delete attachment')}\n >\n <Trash2 className=\"h-4 w-4 text-destructive\" />\n </Button>\n </div>\n )\n })}\n </div>\n ) : (\n <div className=\"text-sm text-muted-foreground\">\n {t('attachments.library.table.empty', 'No attachments found.')}\n </div>\n )}\n\n {items.length > 0 && page < totalPages ? (\n <div className=\"flex justify-center\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => { void load(page + 1, false) }}\n disabled={loading}\n >\n {t('attachments.library.loadMore', 'Load more')}\n </Button>\n </div>\n ) : null}\n\n <AttachmentMetadataDialog\n open={metadataOpen}\n onOpenChange={setMetadataOpen}\n item={selectedItem}\n availableTags={[]}\n onSave={handleMetadataSave}\n />\n <AttachmentDeleteDialog\n open={deleteOpen}\n onOpenChange={setDeleteOpen}\n fileName={deleteTarget?.fileName}\n onConfirm={handleDelete}\n isDeleting={false}\n />\n </div>\n )\n}\n\nexport function AttachmentsSection(props: Props) {\n const handle = ComponentReplacementHandles.section('ui.detail', 'AttachmentsSection')\n const Resolved = useRegisteredComponent<Props>(\n handle,\n AttachmentsSectionImpl as React.ComponentType<Props>,\n )\n\n return (\n <div data-component-handle={handle}>\n <Resolved {...props} />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -262,7 +262,10 @@ function NotesSectionImpl({
|
|
|
262
262
|
const [contentError, setContentError] = React.useState(null);
|
|
263
263
|
const contentTextareaRef = React.useRef(null);
|
|
264
264
|
const [visibleCount, setVisibleCount] = React.useState(0);
|
|
265
|
+
const [currentPage, setCurrentPage] = React.useState(1);
|
|
266
|
+
const [totalPages, setTotalPages] = React.useState(1);
|
|
265
267
|
const [deletingNoteId, setDeletingNoteId] = React.useState(null);
|
|
268
|
+
const pagedMode = typeof dataAdapter.listPage === "function";
|
|
266
269
|
React.useEffect(() => {
|
|
267
270
|
const queryEntityId = typeof entityId === "string" ? entityId : "";
|
|
268
271
|
const queryDealId = typeof dealId === "string" ? dealId : "";
|
|
@@ -270,6 +273,8 @@ function NotesSectionImpl({
|
|
|
270
273
|
setNotes([]);
|
|
271
274
|
setLoadError(null);
|
|
272
275
|
setIsLoading(false);
|
|
276
|
+
setCurrentPage(1);
|
|
277
|
+
setTotalPages(1);
|
|
273
278
|
return;
|
|
274
279
|
}
|
|
275
280
|
let cancelled = false;
|
|
@@ -278,6 +283,20 @@ function NotesSectionImpl({
|
|
|
278
283
|
pushLoading();
|
|
279
284
|
async function loadNotes() {
|
|
280
285
|
try {
|
|
286
|
+
if (dataAdapter.listPage) {
|
|
287
|
+
const pageResult = await dataAdapter.listPage({
|
|
288
|
+
entityId: queryEntityId || null,
|
|
289
|
+
dealId: queryDealId || null,
|
|
290
|
+
page: 1,
|
|
291
|
+
pageSize: 20,
|
|
292
|
+
context: dataContext
|
|
293
|
+
});
|
|
294
|
+
if (cancelled) return;
|
|
295
|
+
setNotes(pageResult.items);
|
|
296
|
+
setCurrentPage(pageResult.page);
|
|
297
|
+
setTotalPages(pageResult.totalPages);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
281
300
|
const mapped = await dataAdapter.list({
|
|
282
301
|
entityId: queryEntityId || null,
|
|
283
302
|
dealId: queryDealId || null,
|
|
@@ -285,11 +304,15 @@ function NotesSectionImpl({
|
|
|
285
304
|
});
|
|
286
305
|
if (cancelled) return;
|
|
287
306
|
setNotes(mapped);
|
|
307
|
+
setCurrentPage(1);
|
|
308
|
+
setTotalPages(1);
|
|
288
309
|
} catch (err) {
|
|
289
310
|
if (cancelled) return;
|
|
290
311
|
const message = err instanceof Error ? err.message : label("loadError", "Failed to load notes.");
|
|
291
312
|
setNotes([]);
|
|
292
313
|
setLoadError(message);
|
|
314
|
+
setCurrentPage(1);
|
|
315
|
+
setTotalPages(1);
|
|
293
316
|
flash(message, "error");
|
|
294
317
|
} finally {
|
|
295
318
|
if (!cancelled) setIsLoading(false);
|
|
@@ -301,7 +324,7 @@ function NotesSectionImpl({
|
|
|
301
324
|
return () => {
|
|
302
325
|
cancelled = true;
|
|
303
326
|
};
|
|
304
|
-
}, [dataAdapter, dataContext, dealId, entityId, popLoading, pushLoading
|
|
327
|
+
}, [dataAdapter, dataContext, dealId, entityId, label, popLoading, pushLoading]);
|
|
305
328
|
const youLabel = label("you", "You");
|
|
306
329
|
const viewerLabel = React.useMemo(() => viewerName ?? viewerEmail ?? null, [viewerEmail, viewerName]);
|
|
307
330
|
const handleMarkdownToggle = React.useCallback(() => {
|
|
@@ -342,6 +365,10 @@ function NotesSectionImpl({
|
|
|
342
365
|
}
|
|
343
366
|
}, [readMarkdownPreference]);
|
|
344
367
|
React.useEffect(() => {
|
|
368
|
+
if (pagedMode) {
|
|
369
|
+
setVisibleCount(notes.length);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
345
372
|
if (!notes.length) {
|
|
346
373
|
setVisibleCount(0);
|
|
347
374
|
return;
|
|
@@ -351,7 +378,7 @@ function NotesSectionImpl({
|
|
|
351
378
|
if (prev >= notes.length) return prev;
|
|
352
379
|
return Math.min(Math.max(prev, baseline), notes.length);
|
|
353
380
|
});
|
|
354
|
-
}, [notes.length]);
|
|
381
|
+
}, [notes.length, pagedMode]);
|
|
355
382
|
React.useEffect(() => {
|
|
356
383
|
if (hasEntity) return;
|
|
357
384
|
setComposerOpen(false);
|
|
@@ -359,8 +386,14 @@ function NotesSectionImpl({
|
|
|
359
386
|
setDraftIcon(null);
|
|
360
387
|
setDraftColor(null);
|
|
361
388
|
}, [hasEntity]);
|
|
362
|
-
const visibleNotes = React.useMemo(
|
|
363
|
-
|
|
389
|
+
const visibleNotes = React.useMemo(
|
|
390
|
+
() => pagedMode ? notes : notes.slice(0, visibleCount),
|
|
391
|
+
[notes, pagedMode, visibleCount]
|
|
392
|
+
);
|
|
393
|
+
const hasVisibleNotes = React.useMemo(
|
|
394
|
+
() => pagedMode ? notes.length > 0 : visibleCount > 0,
|
|
395
|
+
[notes.length, pagedMode, visibleCount]
|
|
396
|
+
);
|
|
364
397
|
const loadMoreLabel = label("loadMore");
|
|
365
398
|
const handleCreateNote = React.useCallback(
|
|
366
399
|
async (input) => {
|
|
@@ -418,6 +451,7 @@ function NotesSectionImpl({
|
|
|
418
451
|
};
|
|
419
452
|
return [newNote, ...prev];
|
|
420
453
|
});
|
|
454
|
+
setVisibleCount((prev) => Math.max(prev, 1));
|
|
421
455
|
flash(label("success"), "success");
|
|
422
456
|
return true;
|
|
423
457
|
} catch (err) {
|
|
@@ -478,6 +512,9 @@ function NotesSectionImpl({
|
|
|
478
512
|
try {
|
|
479
513
|
await dataAdapter.delete({ id: note.id, context: dataContext });
|
|
480
514
|
setNotes((prev) => prev.filter((existing) => existing.id !== note.id));
|
|
515
|
+
if (pagedMode) {
|
|
516
|
+
setVisibleCount((prev) => Math.max(0, prev - 1));
|
|
517
|
+
}
|
|
481
518
|
flash(label("deleteSuccess", "Note deleted"), "success");
|
|
482
519
|
} catch (err) {
|
|
483
520
|
const message = err instanceof Error ? err.message : label("deleteError", "Failed to delete note");
|
|
@@ -506,11 +543,36 @@ function NotesSectionImpl({
|
|
|
506
543
|
[draftBody, draftColor, draftIcon, handleCreateNote]
|
|
507
544
|
);
|
|
508
545
|
const handleLoadMore = React.useCallback(() => {
|
|
546
|
+
if (pagedMode && dataAdapter.listPage) {
|
|
547
|
+
if (currentPage >= totalPages || isLoading) return;
|
|
548
|
+
const queryEntityId = typeof entityId === "string" ? entityId : "";
|
|
549
|
+
const queryDealId = typeof dealId === "string" ? dealId : "";
|
|
550
|
+
setIsLoading(true);
|
|
551
|
+
pushLoading();
|
|
552
|
+
void dataAdapter.listPage({
|
|
553
|
+
entityId: queryEntityId || null,
|
|
554
|
+
dealId: queryDealId || null,
|
|
555
|
+
page: currentPage + 1,
|
|
556
|
+
pageSize: 20,
|
|
557
|
+
context: dataContext
|
|
558
|
+
}).then((pageResult) => {
|
|
559
|
+
setNotes((prev) => [...prev, ...pageResult.items]);
|
|
560
|
+
setCurrentPage(pageResult.page);
|
|
561
|
+
setTotalPages(pageResult.totalPages);
|
|
562
|
+
}).catch((error) => {
|
|
563
|
+
const message = error instanceof Error ? error.message : label("loadError", "Failed to load notes.");
|
|
564
|
+
flash(message, "error");
|
|
565
|
+
}).finally(() => {
|
|
566
|
+
setIsLoading(false);
|
|
567
|
+
popLoading();
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
509
571
|
setVisibleCount((prev) => {
|
|
510
572
|
if (prev >= notes.length) return prev;
|
|
511
573
|
return Math.min(prev + 5, notes.length);
|
|
512
574
|
});
|
|
513
|
-
}, [notes.length]);
|
|
575
|
+
}, [currentPage, dataAdapter, dataContext, dealId, entityId, flash, isLoading, label, notes.length, pagedMode, popLoading, pushLoading, totalPages]);
|
|
514
576
|
const handleAppearanceDialogSubmit = React.useCallback(async () => {
|
|
515
577
|
if (!appearanceDialogState) return;
|
|
516
578
|
setAppearanceDialogError(null);
|
|
@@ -803,6 +865,20 @@ function NotesSectionImpl({
|
|
|
803
865
|
),
|
|
804
866
|
loadError ? /* @__PURE__ */ jsx(ErrorMessage, { label: loadError, className: "mt-3" }) : null,
|
|
805
867
|
/* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
868
|
+
!composerOpen && hasVisibleNotes && !onActionChange ? /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxs(
|
|
869
|
+
Button,
|
|
870
|
+
{
|
|
871
|
+
type: "button",
|
|
872
|
+
variant: "outline",
|
|
873
|
+
size: "sm",
|
|
874
|
+
onClick: focusComposer,
|
|
875
|
+
disabled: isSubmitting || isLoading || !hasEntity,
|
|
876
|
+
children: [
|
|
877
|
+
/* @__PURE__ */ jsx(Plus, { className: "size-4" }),
|
|
878
|
+
addActionLabel
|
|
879
|
+
]
|
|
880
|
+
}
|
|
881
|
+
) }) : null,
|
|
806
882
|
isLoading ? /* @__PURE__ */ jsx(
|
|
807
883
|
LoadingMessage,
|
|
808
884
|
{
|
|
@@ -975,7 +1051,7 @@ function NotesSectionImpl({
|
|
|
975
1051
|
}
|
|
976
1052
|
}
|
|
977
1053
|
),
|
|
978
|
-
isLoading || visibleCount >= notes.length ? null : /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: handleLoadMore, children: loadMoreLabel }) })
|
|
1054
|
+
isLoading || (pagedMode ? currentPage >= totalPages : visibleCount >= notes.length) ? null : /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: handleLoadMore, children: loadMoreLabel }) })
|
|
979
1055
|
] }),
|
|
980
1056
|
/* @__PURE__ */ jsx(
|
|
981
1057
|
AppearanceDialog,
|