@open-mercato/documents 0.10.1-develop.7925.1.ec40f75262 → 0.10.1-develop.7929.1.0822b5d5fd
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/documents/backend/documents/[id]/DocumentEditorSurface.js +12 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorSurface.js.map +2 -2
- package/dist/modules/documents/backend/documents/[id]/DocumentPageClient.js +15 -1
- package/dist/modules/documents/backend/documents/[id]/DocumentPageClient.js.map +2 -2
- package/dist/modules/documents/backend/documents/[id]/VersionHistoryPanel.js +20 -8
- package/dist/modules/documents/backend/documents/[id]/VersionHistoryPanel.js.map +2 -2
- package/dist/modules/documents/backend/documents/[id]/editorTypes.js.map +2 -2
- package/dist/modules/documents/backend/documents/[id]/saveCollaborativeContent.js +46 -0
- package/dist/modules/documents/backend/documents/[id]/saveCollaborativeContent.js.map +7 -0
- package/dist/modules/documents/data/validators.js +11 -0
- package/dist/modules/documents/data/validators.js.map +2 -2
- package/dist/modules/documents/lib/collabPersistence.js +13 -0
- package/dist/modules/documents/lib/collabPersistence.js.map +7 -0
- package/dist/server/documents-collab-server.js +74 -1
- package/dist/server/documents-collab-server.js.map +2 -2
- package/package.json +6 -6
- package/server/documents-collab-server.ts +79 -0
- package/src/modules/documents/backend/documents/[id]/DocumentEditorSurface.tsx +16 -0
- package/src/modules/documents/backend/documents/[id]/DocumentPageClient.tsx +14 -0
- package/src/modules/documents/backend/documents/[id]/VersionHistoryPanel.tsx +23 -8
- package/src/modules/documents/backend/documents/[id]/editorTypes.ts +1 -0
- package/src/modules/documents/backend/documents/[id]/saveCollaborativeContent.ts +43 -0
- package/src/modules/documents/data/validators.ts +12 -0
- package/src/modules/documents/lib/collabPersistence.ts +10 -0
|
@@ -19,6 +19,7 @@ import { useDocumentEditor } from "./useDocumentEditor.js";
|
|
|
19
19
|
import { useFallbackContentPersistence } from "./useFallbackContentPersistence.js";
|
|
20
20
|
import { useDocumentTitle } from "./useDocumentTitle.js";
|
|
21
21
|
import { useEditorInsertions } from "./useEditorInsertions.js";
|
|
22
|
+
import { saveCollaborativeContent } from "./saveCollaborativeContent.js";
|
|
22
23
|
function DocumentEditorSurface(input) {
|
|
23
24
|
const t = useT();
|
|
24
25
|
const nonce = useContentSecurityPolicyNonce();
|
|
@@ -52,6 +53,17 @@ function DocumentEditorSurface(input) {
|
|
|
52
53
|
readOnly: effectiveReadOnly,
|
|
53
54
|
onTitleChange: input.onTitleChange
|
|
54
55
|
});
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
input.onSaveReady?.(async () => {
|
|
59
|
+
if (controller.signal.aborted || input.readOnly || !editorModel.editorRef.current) return false;
|
|
60
|
+
return input.transport === "fallback" ? fallbackPersistence.saveNow() : input.collabResources ? saveCollaborativeContent(input.collabResources, controller.signal) : false;
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
controller.abort();
|
|
64
|
+
input.onSaveReady?.(null);
|
|
65
|
+
};
|
|
66
|
+
}, [editorModel.editorRef, fallbackPersistence.saveNow, input.collabResources, input.onSaveReady, input.readOnly, input.transport]);
|
|
55
67
|
const insertions = useEditorInsertions({
|
|
56
68
|
documentId: input.documentId,
|
|
57
69
|
editorRef: editorModel.editorRef,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/documents/backend/documents/%5Bid%5D/DocumentEditorSurface.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'\nimport { Link2, Unlink } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { EntityPicker } from '../components/EntityPicker'\nimport { DocumentCanvas } from './DocumentCanvas'\nimport { EditorStatusPresence } from './EditorStatusPresence'\nimport { EditorToolbar } from './EditorToolbar'\nimport { RecordFieldsDialog } from './RecordFieldsDialog'\nimport { EDITOR_PRESENCE_STYLES } from './editorPresenceStyles'\nimport { canCreateSelectionComment, type CollabResources, type ConnectionStatus, type DocumentEditorIslandProps, type EditorMode, type PresenceUser } from './editorTypes'\nimport { useDocumentEditor } from './useDocumentEditor'\nimport { useFallbackContentPersistence } from './useFallbackContentPersistence'\nimport { useDocumentTitle } from './useDocumentTitle'\nimport { useEditorInsertions } from './useEditorInsertions'\n\ntype Props = DocumentEditorIslandProps & {\n transport: 'collab' | 'fallback'\n collabResources?: CollabResources\n connectionStatus: ConnectionStatus\n presenceUsers: PresenceUser[]\n mode: EditorMode\n onModeChange: (mode: EditorMode) => void\n}\n\nexport function DocumentEditorSurface(input: Props) {\n const t = useT()\n const nonce = useContentSecurityPolicyNonce()\n const [outlineOpen, setOutlineOpen] = React.useState(false)\n const [suggestionRange, setSuggestionRange] = React.useState<{ from: number; to: number } | null>(null)\n const modeChangeInFlightRef = React.useRef(false)\n const effectiveReadOnly = input.readOnly || input.mode === 'preview'\n const openSuggestion = React.useCallback((range: { from: number; to: number }) => setSuggestionRange(range), [])\n const closeSuggestion = React.useCallback(() => setSuggestionRange(null), [])\n const fallbackPersistence = useFallbackContentPersistence({\n documentId: input.documentId,\n initialUpdatedAt: input.contentUpdatedAt ?? null,\n enabled: input.transport === 'fallback' && !input.readOnly,\n onConflictRefresh: input.onContentConflict,\n })\n const editorModel = useDocumentEditor({\n documentId: input.documentId,\n initialContentHtml: input.initialContentHtml,\n editorMode: input.transport,\n collabResources: input.collabResources,\n readOnly: effectiveReadOnly,\n onEditorReady: input.onEditorReady,\n onUpdate: fallbackPersistence.onEditorUpdate,\n onEntitySuggestion: openSuggestion,\n onSuggestionClose: closeSuggestion,\n })\n const title = useDocumentTitle({\n documentId: input.documentId,\n title: input.title,\n updatedAt: input.documentUpdatedAt ?? null,\n readOnly: effectiveReadOnly,\n onTitleChange: input.onTitleChange,\n })\n const insertions = useEditorInsertions({\n documentId: input.documentId,\n editorRef: editorModel.editorRef,\n disabled: effectiveReadOnly,\n suggestionRange,\n setSuggestionRange,\n })\n React.useEffect(() => {\n if (suggestionRange && !effectiveReadOnly) insertions.setPickerOpen(true)\n }, [effectiveReadOnly, insertions.setPickerOpen, suggestionRange])\n\n const status = input.transport === 'fallback' ? 'offline' : input.connectionStatus\n const notice = input.transport === 'fallback'\n ? t(input.readOnly ? 'documents.editor.realtime.readOnlyFallback' : 'documents.editor.realtime.singleUserFallback')\n : input.readOnly\n ? t('documents.editor.readOnly')\n : input.mode === 'preview'\n ? t('documents.editor.mode.previewNotice')\n : null\n const selectionCommentHandler = canCreateSelectionComment({\n permissionReadOnly: input.readOnly,\n mode: input.mode,\n transport: input.transport,\n hasCommentHandler: Boolean(input.onComment),\n }) ? input.onComment : undefined\n const handleModeChange = React.useCallback(async (nextMode: EditorMode) => {\n if (nextMode === input.mode || modeChangeInFlightRef.current) return\n if (input.transport !== 'fallback' || nextMode !== 'preview') {\n input.onModeChange(nextMode)\n return\n }\n\n modeChangeInFlightRef.current = true\n try {\n if (await fallbackPersistence.saveNow()) input.onModeChange(nextMode)\n } finally {\n modeChangeInFlightRef.current = false\n }\n }, [fallbackPersistence.saveNow, input.mode, input.onModeChange, input.transport])\n\n return (\n <div className={cn('space-y-3', input.transport === 'collab' && 'om-doc-collab', input.mode === 'preview' && 'om-doc-preview')}>\n <style nonce={nonce}>{EDITOR_PRESENCE_STYLES}</style>\n <div className=\"overflow-hidden rounded-lg border border-border bg-muted shadow-sm\">\n <div className=\"sticky top-0 z-sticky border-b border-border bg-card/95\">\n <div className=\"flex flex-wrap items-center justify-between gap-2 px-3 py-2\">\n <EditorStatusPresence\n status={status}\n users={input.presenceUsers}\n counts={editorModel.counts}\n mode={input.mode}\n canEdit={!input.readOnly}\n onModeChange={(nextMode) => { void handleModeChange(nextMode) }}\n fallbackSave={input.transport === 'fallback' && !input.readOnly ? {\n status: fallbackPersistence.status,\n onSave: fallbackPersistence.saveNow,\n } : undefined}\n />\n </div>\n {input.mode === 'edit' ? (\n <EditorToolbar\n editor={editorModel.editor}\n disabled={effectiveReadOnly || !editorModel.editor}\n outlineOpen={outlineOpen}\n uploading={insertions.uploading}\n onToggleOutline={() => setOutlineOpen((value) => !value)}\n onOpenEntityPicker={insertions.openEntityPicker}\n onOpenLink={insertions.openLink}\n onImage={() => insertions.fileInputRef.current?.click()}\n />\n ) : null}\n {insertions.linkOpen && !effectiveReadOnly ? (\n <div className=\"flex flex-col gap-3 border-t border-border bg-muted/30 p-3 md:flex-row md:items-end\" onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { event.preventDefault(); insertions.applyLink() }\n if (event.key === 'Escape') { event.preventDefault(); insertions.setLinkOpen(false) }\n }}>\n <div className=\"min-w-0 flex-1 space-y-2\">\n <Label htmlFor={`document-link-${input.documentId}`}>{t('documents.editor.link.url')}</Label>\n <Input id={`document-link-${input.documentId}`} type=\"url\" value={insertions.linkHref} onChange={(event) => insertions.setLinkHref(event.target.value)} placeholder={t('documents.editor.link.placeholder')} />\n </div>\n <Button type=\"button\" onClick={insertions.applyLink}><Link2 />{t('documents.editor.link.apply')}</Button>\n <Button type=\"button\" variant=\"outline\" onClick={insertions.removeLink}><Unlink />{t('documents.editor.link.remove')}</Button>\n <Button type=\"button\" variant=\"ghost\" onClick={() => insertions.setLinkOpen(false)}>{t('documents.actions.cancel')}</Button>\n </div>\n ) : null}\n </div>\n <DocumentCanvas editor={editorModel.editor} title={title} readOnly={effectiveReadOnly} outlineOpen={outlineOpen} notice={notice} onOpenLink={insertions.openLink} onComment={selectionCommentHandler} />\n </div>\n <EntityPicker open={insertions.entityPickerOpen} onOpenChange={insertions.setPickerOpen} onPick={(pick) => { void insertions.handleEntityPick(pick) }} />\n <RecordFieldsDialog\n documentId={input.documentId}\n linkId={insertions.recordFieldsLinkId}\n editor={editorModel.editor}\n canInsert={!effectiveReadOnly && Boolean(editorModel.editor)}\n onOpenChange={(open) => { if (!open) insertions.closeRecordFields() }}\n />\n <input ref={insertions.fileInputRef} type=\"file\" accept=\"image/*\" className=\"hidden\" aria-label={t('documents.editor.imageInput')} onChange={(event) => { void insertions.handleFiles(event.target.files) }} />\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'\nimport { Link2, Unlink } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { EntityPicker } from '../components/EntityPicker'\nimport { DocumentCanvas } from './DocumentCanvas'\nimport { EditorStatusPresence } from './EditorStatusPresence'\nimport { EditorToolbar } from './EditorToolbar'\nimport { RecordFieldsDialog } from './RecordFieldsDialog'\nimport { EDITOR_PRESENCE_STYLES } from './editorPresenceStyles'\nimport { canCreateSelectionComment, type CollabResources, type ConnectionStatus, type DocumentEditorIslandProps, type EditorMode, type PresenceUser } from './editorTypes'\nimport { useDocumentEditor } from './useDocumentEditor'\nimport { useFallbackContentPersistence } from './useFallbackContentPersistence'\nimport { useDocumentTitle } from './useDocumentTitle'\nimport { useEditorInsertions } from './useEditorInsertions'\nimport { saveCollaborativeContent } from './saveCollaborativeContent'\n\ntype Props = DocumentEditorIslandProps & {\n transport: 'collab' | 'fallback'\n collabResources?: CollabResources\n connectionStatus: ConnectionStatus\n presenceUsers: PresenceUser[]\n mode: EditorMode\n onModeChange: (mode: EditorMode) => void\n}\n\nexport function DocumentEditorSurface(input: Props) {\n const t = useT()\n const nonce = useContentSecurityPolicyNonce()\n const [outlineOpen, setOutlineOpen] = React.useState(false)\n const [suggestionRange, setSuggestionRange] = React.useState<{ from: number; to: number } | null>(null)\n const modeChangeInFlightRef = React.useRef(false)\n const effectiveReadOnly = input.readOnly || input.mode === 'preview'\n const openSuggestion = React.useCallback((range: { from: number; to: number }) => setSuggestionRange(range), [])\n const closeSuggestion = React.useCallback(() => setSuggestionRange(null), [])\n const fallbackPersistence = useFallbackContentPersistence({\n documentId: input.documentId,\n initialUpdatedAt: input.contentUpdatedAt ?? null,\n enabled: input.transport === 'fallback' && !input.readOnly,\n onConflictRefresh: input.onContentConflict,\n })\n const editorModel = useDocumentEditor({\n documentId: input.documentId,\n initialContentHtml: input.initialContentHtml,\n editorMode: input.transport,\n collabResources: input.collabResources,\n readOnly: effectiveReadOnly,\n onEditorReady: input.onEditorReady,\n onUpdate: fallbackPersistence.onEditorUpdate,\n onEntitySuggestion: openSuggestion,\n onSuggestionClose: closeSuggestion,\n })\n const title = useDocumentTitle({\n documentId: input.documentId,\n title: input.title,\n updatedAt: input.documentUpdatedAt ?? null,\n readOnly: effectiveReadOnly,\n onTitleChange: input.onTitleChange,\n })\n React.useEffect(() => {\n const controller = new AbortController()\n input.onSaveReady?.(async () => {\n if (controller.signal.aborted || input.readOnly || !editorModel.editorRef.current) return false\n return input.transport === 'fallback'\n ? fallbackPersistence.saveNow()\n : input.collabResources\n ? saveCollaborativeContent(input.collabResources, controller.signal)\n : false\n })\n return () => {\n controller.abort()\n input.onSaveReady?.(null)\n }\n }, [editorModel.editorRef, fallbackPersistence.saveNow, input.collabResources, input.onSaveReady, input.readOnly, input.transport])\n const insertions = useEditorInsertions({\n documentId: input.documentId,\n editorRef: editorModel.editorRef,\n disabled: effectiveReadOnly,\n suggestionRange,\n setSuggestionRange,\n })\n React.useEffect(() => {\n if (suggestionRange && !effectiveReadOnly) insertions.setPickerOpen(true)\n }, [effectiveReadOnly, insertions.setPickerOpen, suggestionRange])\n\n const status = input.transport === 'fallback' ? 'offline' : input.connectionStatus\n const notice = input.transport === 'fallback'\n ? t(input.readOnly ? 'documents.editor.realtime.readOnlyFallback' : 'documents.editor.realtime.singleUserFallback')\n : input.readOnly\n ? t('documents.editor.readOnly')\n : input.mode === 'preview'\n ? t('documents.editor.mode.previewNotice')\n : null\n const selectionCommentHandler = canCreateSelectionComment({\n permissionReadOnly: input.readOnly,\n mode: input.mode,\n transport: input.transport,\n hasCommentHandler: Boolean(input.onComment),\n }) ? input.onComment : undefined\n const handleModeChange = React.useCallback(async (nextMode: EditorMode) => {\n if (nextMode === input.mode || modeChangeInFlightRef.current) return\n if (input.transport !== 'fallback' || nextMode !== 'preview') {\n input.onModeChange(nextMode)\n return\n }\n\n modeChangeInFlightRef.current = true\n try {\n if (await fallbackPersistence.saveNow()) input.onModeChange(nextMode)\n } finally {\n modeChangeInFlightRef.current = false\n }\n }, [fallbackPersistence.saveNow, input.mode, input.onModeChange, input.transport])\n\n return (\n <div className={cn('space-y-3', input.transport === 'collab' && 'om-doc-collab', input.mode === 'preview' && 'om-doc-preview')}>\n <style nonce={nonce}>{EDITOR_PRESENCE_STYLES}</style>\n <div className=\"overflow-hidden rounded-lg border border-border bg-muted shadow-sm\">\n <div className=\"sticky top-0 z-sticky border-b border-border bg-card/95\">\n <div className=\"flex flex-wrap items-center justify-between gap-2 px-3 py-2\">\n <EditorStatusPresence\n status={status}\n users={input.presenceUsers}\n counts={editorModel.counts}\n mode={input.mode}\n canEdit={!input.readOnly}\n onModeChange={(nextMode) => { void handleModeChange(nextMode) }}\n fallbackSave={input.transport === 'fallback' && !input.readOnly ? {\n status: fallbackPersistence.status,\n onSave: fallbackPersistence.saveNow,\n } : undefined}\n />\n </div>\n {input.mode === 'edit' ? (\n <EditorToolbar\n editor={editorModel.editor}\n disabled={effectiveReadOnly || !editorModel.editor}\n outlineOpen={outlineOpen}\n uploading={insertions.uploading}\n onToggleOutline={() => setOutlineOpen((value) => !value)}\n onOpenEntityPicker={insertions.openEntityPicker}\n onOpenLink={insertions.openLink}\n onImage={() => insertions.fileInputRef.current?.click()}\n />\n ) : null}\n {insertions.linkOpen && !effectiveReadOnly ? (\n <div className=\"flex flex-col gap-3 border-t border-border bg-muted/30 p-3 md:flex-row md:items-end\" onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { event.preventDefault(); insertions.applyLink() }\n if (event.key === 'Escape') { event.preventDefault(); insertions.setLinkOpen(false) }\n }}>\n <div className=\"min-w-0 flex-1 space-y-2\">\n <Label htmlFor={`document-link-${input.documentId}`}>{t('documents.editor.link.url')}</Label>\n <Input id={`document-link-${input.documentId}`} type=\"url\" value={insertions.linkHref} onChange={(event) => insertions.setLinkHref(event.target.value)} placeholder={t('documents.editor.link.placeholder')} />\n </div>\n <Button type=\"button\" onClick={insertions.applyLink}><Link2 />{t('documents.editor.link.apply')}</Button>\n <Button type=\"button\" variant=\"outline\" onClick={insertions.removeLink}><Unlink />{t('documents.editor.link.remove')}</Button>\n <Button type=\"button\" variant=\"ghost\" onClick={() => insertions.setLinkOpen(false)}>{t('documents.actions.cancel')}</Button>\n </div>\n ) : null}\n </div>\n <DocumentCanvas editor={editorModel.editor} title={title} readOnly={effectiveReadOnly} outlineOpen={outlineOpen} notice={notice} onOpenLink={insertions.openLink} onComment={selectionCommentHandler} />\n </div>\n <EntityPicker open={insertions.entityPickerOpen} onOpenChange={insertions.setPickerOpen} onPick={(pick) => { void insertions.handleEntityPick(pick) }} />\n <RecordFieldsDialog\n documentId={input.documentId}\n linkId={insertions.recordFieldsLinkId}\n editor={editorModel.editor}\n canInsert={!effectiveReadOnly && Boolean(editorModel.editor)}\n onOpenChange={(open) => { if (!open) insertions.closeRecordFields() }}\n />\n <input ref={insertions.fileInputRef} type=\"file\" accept=\"image/*\" className=\"hidden\" aria-label={t('documents.editor.imageInput')} onChange={(event) => { void insertions.handleFiles(event.target.files) }} />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -66,6 +66,17 @@ function DocumentPageClient({ documentId }) {
|
|
|
66
66
|
const scrollVersionsIntoView = React.useRef(false);
|
|
67
67
|
const [editorEpoch, setEditorEpoch] = React.useState(0);
|
|
68
68
|
const [editor, setEditor] = React.useState(null);
|
|
69
|
+
const contentSaveRef = React.useRef(null);
|
|
70
|
+
const displayedDocumentId = state.status === "ready" ? state.document.id : null;
|
|
71
|
+
const registerContentSave = React.useCallback((save) => {
|
|
72
|
+
contentSaveRef.current = save && displayedDocumentId ? { documentId: displayedDocumentId, save } : null;
|
|
73
|
+
}, [displayedDocumentId]);
|
|
74
|
+
const saveBeforeSnapshot = React.useCallback(async () => {
|
|
75
|
+
const registered = contentSaveRef.current;
|
|
76
|
+
if (!registered || registered.documentId !== documentId) return false;
|
|
77
|
+
const saved = await registered.save();
|
|
78
|
+
return saved && contentSaveRef.current === registered;
|
|
79
|
+
}, [documentId]);
|
|
69
80
|
const [commentFocusRequest, setCommentFocusRequest] = React.useState(null);
|
|
70
81
|
const requestSequence = React.useRef(0);
|
|
71
82
|
const mutationContextId = `documents-detail:${documentId}`;
|
|
@@ -112,6 +123,7 @@ function DocumentPageClient({ documentId }) {
|
|
|
112
123
|
throw new Error(t("documents.editor.error.loadContent"));
|
|
113
124
|
}
|
|
114
125
|
const nextContent = contentCall.status === 404 ? { contentHtml: "", updatedAt: null } : normalizeDocumentContent(contentCall.result);
|
|
126
|
+
contentSaveRef.current = null;
|
|
115
127
|
setState((current) => current.status === "ready" ? {
|
|
116
128
|
...current,
|
|
117
129
|
content: nextContent
|
|
@@ -364,6 +376,7 @@ function DocumentPageClient({ documentId }) {
|
|
|
364
376
|
documentUpdatedAt: document.updatedAt,
|
|
365
377
|
readOnly: !capabilities.canEdit,
|
|
366
378
|
onEditorReady: setEditor,
|
|
379
|
+
onSaveReady: registerContentSave,
|
|
367
380
|
onContentConflict: handleContentConflict,
|
|
368
381
|
onComment: capabilities.canComment ? (anchor) => setCommentFocusRequest((current) => ({ anchor, requestId: (current?.requestId ?? 0) + 1 })) : void 0,
|
|
369
382
|
onTitleChange: (title, updatedAt) => setState((current) => current.status === "ready" ? {
|
|
@@ -400,7 +413,8 @@ function DocumentPageClient({ documentId }) {
|
|
|
400
413
|
documentId: document.id,
|
|
401
414
|
canRestore: capabilities.canEdit,
|
|
402
415
|
contentUpdatedAt: content.updatedAt,
|
|
403
|
-
onRestored: reloadEditor
|
|
416
|
+
onRestored: reloadEditor,
|
|
417
|
+
onBeforeSnapshot: saveBeforeSnapshot
|
|
404
418
|
}
|
|
405
419
|
)
|
|
406
420
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/documents/backend/documents/%5Bid%5D/DocumentPageClient.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport dynamic from 'next/dynamic'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Archive, ArchiveRestore, Bell, Copy, History, Star, Trash2 } from 'lucide-react'\nimport type { Editor } from '@tiptap/core'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall, apiCallOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { LinkButton } from '@open-mercato/ui/primitives/link-button'\nimport { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { ShareDialog } from '../components/ShareDialog'\nimport { normalizeDocumentContent, normalizeDocumentDetail, type DocumentContent, type DocumentDetail } from '../documentUi'\nimport { CommentsRail } from './CommentsRail'\nimport type { CommentAnchor } from './CommentAnchorNavigation'\nimport { ExportMenu } from './ExportMenu'\nimport { RelatedRecordsPanel } from './RelatedRecordsPanel'\nimport { DocumentNavigator } from './DocumentNavigator'\nimport { DocumentEditorErrorBoundary } from './DocumentEditorErrorBoundary'\nimport { VersionHistoryPanel } from './VersionHistoryPanel'\n\nfunction DocumentEditorLoading({ error, retry }: { error?: Error | null; retry?: () => void }) {\n const t = useT()\n if (error) {\n return (\n <ErrorMessage\n label={t('documents.editor.error.load')}\n action={<Button type=\"button\" variant=\"outline\" onClick={retry}>{t('documents.actions.retry')}</Button>}\n />\n )\n }\n return <div role=\"status\" aria-live=\"polite\"><LoadingMessage label={t('documents.editor.loading')} /></div>\n}\n\nconst DocumentEditorIsland = dynamic(() => import('./DocumentEditorIsland'), { ssr: false, loading: DocumentEditorLoading })\n\nconst PANEL_REVEAL_OFFSET_PX = 16\n\nexport function revealPanelInScrollContainer(panel: HTMLElement): void {\n const behavior: ScrollBehavior = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth'\n let container = panel.parentElement\n while (container) {\n const overflowY = window.getComputedStyle(container).overflowY\n if ((overflowY === 'auto' || overflowY === 'scroll') && container.scrollHeight > container.clientHeight) {\n const offset = panel.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop\n container.scrollTo({ top: Math.max(0, offset - PANEL_REVEAL_OFFSET_PX), behavior })\n return\n }\n container = container.parentElement\n }\n panel.scrollIntoView({ block: 'start', behavior })\n}\n\ntype CommentFocusRequest = { anchor: CommentAnchor; requestId: number }\ntype LoadState =\n | { status: 'loading' }\n | { status: 'notFound' }\n | { status: 'error'; message: string }\n | { status: 'ready'; document: DocumentDetail; content: DocumentContent }\n\nexport function DocumentPageClient({ documentId }: { documentId: string }) {\n const t = useT()\n const router = useRouter()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [state, setState] = React.useState<LoadState>({ status: 'loading' })\n const [shareOpen, setShareOpen] = React.useState(false)\n const [showVersions, setShowVersions] = React.useState(false)\n const versionsPanelRef = React.useRef<HTMLDivElement | null>(null)\n const scrollVersionsIntoView = React.useRef(false)\n const [editorEpoch, setEditorEpoch] = React.useState(0)\n const [editor, setEditor] = React.useState<Editor | null>(null)\n const [commentFocusRequest, setCommentFocusRequest] = React.useState<CommentFocusRequest | null>(null)\n const requestSequence = React.useRef(0)\n const mutationContextId = `documents-detail:${documentId}`\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({ contextId: mutationContextId, blockedMessage: t('ui.forms.flash.saveBlocked') })\n\n const loadDocument = React.useCallback(async () => {\n const requestId = ++requestSequence.current\n setState({ status: 'loading' })\n try {\n const [documentCall, contentCall] = await Promise.all([\n apiCall<unknown>(`/api/documents/${encodeURIComponent(documentId)}`),\n apiCall<unknown>(`/api/documents/${encodeURIComponent(documentId)}/content`),\n ])\n if (requestSequence.current !== requestId) return\n if (documentCall.status === 404) return setState({ status: 'notFound' })\n if (!documentCall.ok) return setState({ status: 'error', message: t('documents.editor.error.load') })\n if (!contentCall.ok && contentCall.status !== 404) {\n return setState({ status: 'error', message: t('documents.editor.error.loadContent') })\n }\n const document = normalizeDocumentDetail(documentCall.result)\n if (!document) return setState({ status: 'error', message: t('documents.editor.error.load') })\n setState({\n status: 'ready',\n document,\n content: contentCall.status === 404\n ? { contentHtml: '', updatedAt: null }\n : normalizeDocumentContent(contentCall.result),\n })\n } catch (error) {\n if (requestSequence.current !== requestId) return\n setState({ status: 'error', message: error instanceof Error ? error.message : t('documents.editor.error.load') })\n }\n }, [documentId, t])\n\n React.useEffect(() => {\n void loadDocument()\n return () => { requestSequence.current += 1 }\n }, [loadDocument])\n\n const reloadEditor = React.useCallback(async () => {\n const requestId = ++requestSequence.current\n const contentCall = await apiCall<unknown>(`/api/documents/${encodeURIComponent(documentId)}/content`)\n if (requestSequence.current !== requestId) {\n throw new Error(t('documents.editor.error.loadContent'))\n }\n if (!contentCall.ok && contentCall.status !== 404) {\n throw new Error(t('documents.editor.error.loadContent'))\n }\n const nextContent = contentCall.status === 404\n ? { contentHtml: '', updatedAt: null }\n : normalizeDocumentContent(contentCall.result)\n setState((current) => current.status === 'ready' ? {\n ...current,\n content: nextContent,\n } : current)\n setEditor(null)\n setEditorEpoch((current) => current + 1)\n }, [documentId, t])\n\n const handleContentConflict = React.useCallback(() => {\n void reloadEditor().catch((error) => {\n flash(error instanceof Error ? error.message : t('documents.editor.error.loadContent'), 'error')\n })\n }, [reloadEditor, t])\n\n const retryEditorIsland = React.useCallback(() => {\n setEditor(null)\n setEditorEpoch((current) => current + 1)\n }, [])\n\n const toggleVersions = React.useCallback(() => {\n setShowVersions((value) => {\n scrollVersionsIntoView.current = !value\n return !value\n })\n }, [])\n\n // The versions panel mounts at the bottom of the side rail, below the fold on\n // a laptop screen, so opening it gave no visible feedback. Bring it to the\n // top of whichever container scrolls the rail (the sticky aside on wide\n // screens, the page otherwise) and hand it focus for keyboard users.\n React.useLayoutEffect(() => {\n if (!showVersions || !scrollVersionsIntoView.current) return\n scrollVersionsIntoView.current = false\n const panel = versionsPanelRef.current\n if (!panel) return\n revealPanelInScrollContainer(panel)\n panel.focus({ preventScroll: true })\n }, [showVersions])\n\n const setDocumentState = React.useCallback((mutate: (document: DocumentDetail) => DocumentDetail) => {\n setState((current) => current.status === 'ready'\n ? { ...current, document: mutate(current.document) }\n : current)\n }, [])\n\n const pendingToggles = React.useRef<Set<'favorite' | 'watch'>>(new Set())\n const runPersonalToggle = React.useCallback(async (\n kind: 'favorite' | 'watch',\n active: boolean,\n ) => {\n if (state.status !== 'ready' || pendingToggles.current.has(kind)) return\n pendingToggles.current.add(kind)\n const flag = kind === 'favorite' ? 'isFavorite' as const : 'isWatching' as const\n setDocumentState((document) => ({ ...document, [flag]: active }))\n try {\n await runMutation({\n operation: () => apiCallOrThrow(\n `/api/documents/${encodeURIComponent(documentId)}/${kind}`,\n { method: active ? 'POST' : 'DELETE' },\n ),\n context: {\n formId: mutationContextId,\n resourceKind: kind === 'favorite' ? 'documents.document_favorite' : 'documents.document_watcher',\n resourceId: documentId,\n retryLastMutation,\n },\n mutationPayload: { documentId, active },\n })\n } catch (error) {\n setDocumentState((document) => ({ ...document, [flag]: !active }))\n flash(error instanceof Error ? error.message : t('documents.editor.error.load'), 'error')\n } finally {\n pendingToggles.current.delete(kind)\n }\n }, [documentId, mutationContextId, retryLastMutation, runMutation, setDocumentState, state.status, t])\n\n const duplicating = React.useRef(false)\n const [isDuplicating, setIsDuplicating] = React.useState(false)\n const handleDuplicate = React.useCallback(async () => {\n if (state.status !== 'ready' || duplicating.current) return\n duplicating.current = true\n setIsDuplicating(true)\n try {\n const created = await runMutation({\n operation: () => apiCallOrThrow<{ id: string }>(\n `/api/documents/${encodeURIComponent(documentId)}/duplicate`,\n { method: 'POST', body: JSON.stringify({}), headers: { 'content-type': 'application/json' } },\n ),\n context: {\n formId: mutationContextId,\n resourceKind: 'documents.document',\n resourceId: documentId,\n retryLastMutation,\n },\n mutationPayload: { action: 'duplicate', sourceDocumentId: documentId },\n })\n flash(t('documents.duplicate.success'), 'success')\n const createdId = created?.result?.id\n if (createdId) router.push(`/backend/documents/${encodeURIComponent(createdId)}`)\n } catch (error) {\n flash(error instanceof Error ? error.message : t('documents.duplicate.error'), 'error')\n } finally {\n duplicating.current = false\n setIsDuplicating(false)\n }\n }, [documentId, mutationContextId, retryLastMutation, router, runMutation, state.status, t])\n\n const handleArchiveToggle = React.useCallback(async () => {\n if (state.status !== 'ready' || !state.document.capabilities.canArchive) return\n const archived = state.document.archivedAt !== null\n const action = archived ? 'unarchive' : 'archive'\n try {\n await runMutation({\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(state.document.updatedAt),\n () => apiCallOrThrow(`/api/documents/${encodeURIComponent(documentId)}/${action}`, { method: 'POST' }),\n ),\n context: {\n formId: mutationContextId,\n resourceKind: 'documents.document',\n resourceId: documentId,\n retryLastMutation,\n },\n mutationPayload: { action, documentId },\n })\n flash(t(archived ? 'documents.archive.success.unarchive' : 'documents.archive.success.archive'), 'success')\n await loadDocument()\n } catch (error) {\n if (!surfaceRecordConflict(error, t, { onRefresh: () => { void loadDocument() } })) {\n flash(error instanceof Error ? error.message : t('documents.archive.error'), 'error')\n }\n }\n }, [documentId, loadDocument, mutationContextId, retryLastMutation, runMutation, state, t])\n\n const handleDelete = React.useCallback(async () => {\n if (state.status !== 'ready' || !state.document.capabilities.canDelete) return\n const confirmed = await confirm({\n title: t('documents.list.confirmDelete', { title: state.document.title }),\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutation({\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(state.document.updatedAt),\n () => apiCallOrThrow(`/api/documents/${encodeURIComponent(documentId)}`, { method: 'DELETE' }),\n ),\n context: {\n formId: mutationContextId,\n resourceKind: 'documents.document',\n resourceId: documentId,\n retryLastMutation,\n },\n mutationPayload: { id: documentId },\n })\n flash(t('documents.list.success.delete'), 'success')\n router.push('/backend/documents')\n } catch (error) {\n if (!surfaceRecordConflict(error, t, { onRefresh: () => { void loadDocument() } })) {\n flash(error instanceof Error ? error.message : t('documents.list.error.delete'), 'error')\n }\n }\n }, [confirm, documentId, loadDocument, mutationContextId, retryLastMutation, router, runMutation, state, t])\n\n if (state.status !== 'ready') {\n if (state.status === 'notFound') {\n return (\n <Page><PageBody>\n <RecordNotFoundState\n label={t('documents.editor.notFound')}\n backHref=\"/backend/documents\"\n backLabel={t('documents.actions.backToList')}\n />\n </PageBody></Page>\n )\n }\n const label = state.status === 'loading' ? t('documents.editor.loading') : state.message\n return (\n <Page><PageBody>\n {state.status === 'loading' ? <LoadingMessage label={label} /> : (\n <ErrorMessage label={label} action={(\n <div className=\"flex flex-wrap gap-2\">\n <Button type=\"button\" onClick={() => void loadDocument()}>{t('documents.actions.retry')}</Button>\n <LinkButton asChild variant=\"gray\"><Link href=\"/backend/documents\">{t('documents.actions.backToList')}</Link></LinkButton>\n </div>\n )} />\n )}\n </PageBody></Page>\n )\n }\n\n const { document, content } = state\n const capabilities = document.capabilities\n return (\n <Page>\n <PageHeader title={t('documents.nav.document')} actions={(\n <>\n <LinkButton asChild variant=\"gray\"><Link href=\"/backend/documents\">{t('documents.actions.backToList')}</Link></LinkButton>\n <SimpleTooltip content={t(document.isFavorite ? 'documents.actions.unfavorite' : 'documents.actions.favorite')} size=\"sm\">\n <IconButton\n type=\"button\"\n variant=\"outline\"\n size=\"lg\"\n onClick={() => void runPersonalToggle('favorite', !document.isFavorite)}\n aria-pressed={document.isFavorite}\n aria-label={t(document.isFavorite ? 'documents.actions.unfavorite' : 'documents.actions.favorite')}\n >\n <Star />\n </IconButton>\n </SimpleTooltip>\n <SimpleTooltip content={t(document.isWatching ? 'documents.actions.unwatch' : 'documents.actions.watch')} size=\"sm\">\n <IconButton\n type=\"button\"\n variant=\"outline\"\n size=\"lg\"\n onClick={() => void runPersonalToggle('watch', !document.isWatching)}\n aria-pressed={document.isWatching}\n aria-label={t(document.isWatching ? 'documents.actions.unwatch' : 'documents.actions.watch')}\n >\n <Bell />\n </IconButton>\n </SimpleTooltip>\n <Button type=\"button\" variant={showVersions ? 'secondary' : 'outline'} onClick={toggleVersions} aria-pressed={showVersions}>\n <History />{t('documents.actions.versions')}\n </Button>\n <ExportMenu documentId={document.id} editor={editor} />\n {capabilities.canDuplicate ? (\n <Button type=\"button\" variant=\"outline\" disabled={isDuplicating} onClick={() => void handleDuplicate()}>\n <Copy />{t('documents.actions.duplicate')}\n </Button>\n ) : null}\n {capabilities.canArchive ? (\n <Button type=\"button\" variant=\"outline\" onClick={() => void handleArchiveToggle()}>\n {document.archivedAt ? <ArchiveRestore /> : <Archive />}\n {t(document.archivedAt ? 'documents.actions.unarchive' : 'documents.actions.archive')}\n </Button>\n ) : null}\n {capabilities.canShare ? <Button type=\"button\" variant=\"outline\" onClick={() => setShareOpen(true)}>{t('documents.actions.share')}</Button> : null}\n {capabilities.canDelete ? <Button type=\"button\" variant=\"destructive\" onClick={() => void handleDelete()}><Trash2 />{t('documents.actions.delete')}</Button> : null}\n </>\n )} />\n <PageBody>\n {document.archivedAt ? (\n <div\n role=\"status\"\n className=\"mb-4 flex flex-wrap items-center justify-between gap-2 rounded-md border border-status-warning-border bg-status-warning-bg px-4 py-3 text-sm text-status-warning-text\"\n >\n <span>{t('documents.archive.banner')}</span>\n {capabilities.canArchive ? (\n <Button type=\"button\" variant=\"outline\" onClick={() => void handleArchiveToggle()}>\n <ArchiveRestore />{t('documents.actions.unarchive')}\n </Button>\n ) : null}\n </div>\n ) : null}\n <div className=\"flex flex-col gap-4 xl:flex-row\">\n <div className=\"min-w-0 flex-1\">\n <DocumentEditorErrorBoundary\n resetKey={`${document.id}:${editorEpoch}`}\n onRetry={retryEditorIsland}\n fallback={(retry) => (\n <ErrorMessage\n label={t('documents.editor.error.load')}\n action={<Button type=\"button\" variant=\"outline\" onClick={retry}>{t('documents.actions.retry')}</Button>}\n />\n )}\n >\n <DocumentEditorIsland\n key={`${document.id}:${editorEpoch}`}\n documentId={document.id}\n title={document.title}\n initialContentHtml={content.contentHtml}\n contentUpdatedAt={content.updatedAt}\n documentUpdatedAt={document.updatedAt}\n readOnly={!capabilities.canEdit}\n onEditorReady={setEditor}\n onContentConflict={handleContentConflict}\n onComment={capabilities.canComment ? (anchor) => setCommentFocusRequest((current) => ({ anchor, requestId: (current?.requestId ?? 0) + 1 })) : undefined}\n onTitleChange={(title, updatedAt) => setState((current) => current.status === 'ready' ? {\n ...current,\n document: { ...current.document, title, updatedAt },\n } : current)}\n />\n </DocumentEditorErrorBoundary>\n </div>\n <aside className=\"space-y-4 xl:sticky xl:top-[calc(var(--topbar-height,0px)+1rem)] xl:max-h-[calc(100dvh-var(--topbar-height,0px)-2rem)] xl:w-80 xl:shrink-0 xl:self-start xl:overflow-y-auto\">\n <DocumentNavigator editor={editor} />\n <RelatedRecordsPanel documentId={document.id} canEdit={capabilities.canEdit} editor={editor} />\n <CommentsRail\n documentId={document.id}\n editor={editor}\n commentFocusRequest={commentFocusRequest}\n canComment={capabilities.canComment}\n canShare={capabilities.canShare}\n />\n {showVersions ? (\n <div\n ref={versionsPanelRef}\n tabIndex={-1}\n className=\"scroll-mt-4 rounded-md focus-visible:outline-none focus-visible:shadow-focus\"\n >\n <VersionHistoryPanel\n documentId={document.id}\n canRestore={capabilities.canEdit}\n contentUpdatedAt={content.updatedAt}\n onRestored={reloadEditor}\n />\n </div>\n ) : null}\n </aside>\n </div>\n </PageBody>\n {capabilities.canShare ? (\n <ShareDialog\n documentId={document.id}\n open={shareOpen}\n onOpenChange={setShareOpen}\n canManage={capabilities.canShare}\n />\n ) : null}\n {ConfirmDialogElement}\n </Page>\n )\n}\n\nexport default DocumentPageClient\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport dynamic from 'next/dynamic'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Archive, ArchiveRestore, Bell, Copy, History, Star, Trash2 } from 'lucide-react'\nimport type { Editor } from '@tiptap/core'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall, apiCallOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { LinkButton } from '@open-mercato/ui/primitives/link-button'\nimport { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { ShareDialog } from '../components/ShareDialog'\nimport { normalizeDocumentContent, normalizeDocumentDetail, type DocumentContent, type DocumentDetail } from '../documentUi'\nimport { CommentsRail } from './CommentsRail'\nimport type { CommentAnchor } from './CommentAnchorNavigation'\nimport { ExportMenu } from './ExportMenu'\nimport { RelatedRecordsPanel } from './RelatedRecordsPanel'\nimport { DocumentNavigator } from './DocumentNavigator'\nimport { DocumentEditorErrorBoundary } from './DocumentEditorErrorBoundary'\nimport { VersionHistoryPanel } from './VersionHistoryPanel'\n\nfunction DocumentEditorLoading({ error, retry }: { error?: Error | null; retry?: () => void }) {\n const t = useT()\n if (error) {\n return (\n <ErrorMessage\n label={t('documents.editor.error.load')}\n action={<Button type=\"button\" variant=\"outline\" onClick={retry}>{t('documents.actions.retry')}</Button>}\n />\n )\n }\n return <div role=\"status\" aria-live=\"polite\"><LoadingMessage label={t('documents.editor.loading')} /></div>\n}\n\nconst DocumentEditorIsland = dynamic(() => import('./DocumentEditorIsland'), { ssr: false, loading: DocumentEditorLoading })\n\nconst PANEL_REVEAL_OFFSET_PX = 16\n\nexport function revealPanelInScrollContainer(panel: HTMLElement): void {\n const behavior: ScrollBehavior = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth'\n let container = panel.parentElement\n while (container) {\n const overflowY = window.getComputedStyle(container).overflowY\n if ((overflowY === 'auto' || overflowY === 'scroll') && container.scrollHeight > container.clientHeight) {\n const offset = panel.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop\n container.scrollTo({ top: Math.max(0, offset - PANEL_REVEAL_OFFSET_PX), behavior })\n return\n }\n container = container.parentElement\n }\n panel.scrollIntoView({ block: 'start', behavior })\n}\n\ntype CommentFocusRequest = { anchor: CommentAnchor; requestId: number }\ntype LoadState =\n | { status: 'loading' }\n | { status: 'notFound' }\n | { status: 'error'; message: string }\n | { status: 'ready'; document: DocumentDetail; content: DocumentContent }\n\nexport function DocumentPageClient({ documentId }: { documentId: string }) {\n const t = useT()\n const router = useRouter()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [state, setState] = React.useState<LoadState>({ status: 'loading' })\n const [shareOpen, setShareOpen] = React.useState(false)\n const [showVersions, setShowVersions] = React.useState(false)\n const versionsPanelRef = React.useRef<HTMLDivElement | null>(null)\n const scrollVersionsIntoView = React.useRef(false)\n const [editorEpoch, setEditorEpoch] = React.useState(0)\n const [editor, setEditor] = React.useState<Editor | null>(null)\n const contentSaveRef = React.useRef<{ documentId: string; save: () => Promise<boolean> } | null>(null)\n const displayedDocumentId = state.status === 'ready' ? state.document.id : null\n const registerContentSave = React.useCallback((save: (() => Promise<boolean>) | null) => {\n contentSaveRef.current = save && displayedDocumentId ? { documentId: displayedDocumentId, save } : null\n }, [displayedDocumentId])\n const saveBeforeSnapshot = React.useCallback(async () => {\n const registered = contentSaveRef.current\n if (!registered || registered.documentId !== documentId) return false\n const saved = await registered.save()\n return saved && contentSaveRef.current === registered\n }, [documentId])\n const [commentFocusRequest, setCommentFocusRequest] = React.useState<CommentFocusRequest | null>(null)\n const requestSequence = React.useRef(0)\n const mutationContextId = `documents-detail:${documentId}`\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({ contextId: mutationContextId, blockedMessage: t('ui.forms.flash.saveBlocked') })\n\n const loadDocument = React.useCallback(async () => {\n const requestId = ++requestSequence.current\n setState({ status: 'loading' })\n try {\n const [documentCall, contentCall] = await Promise.all([\n apiCall<unknown>(`/api/documents/${encodeURIComponent(documentId)}`),\n apiCall<unknown>(`/api/documents/${encodeURIComponent(documentId)}/content`),\n ])\n if (requestSequence.current !== requestId) return\n if (documentCall.status === 404) return setState({ status: 'notFound' })\n if (!documentCall.ok) return setState({ status: 'error', message: t('documents.editor.error.load') })\n if (!contentCall.ok && contentCall.status !== 404) {\n return setState({ status: 'error', message: t('documents.editor.error.loadContent') })\n }\n const document = normalizeDocumentDetail(documentCall.result)\n if (!document) return setState({ status: 'error', message: t('documents.editor.error.load') })\n setState({\n status: 'ready',\n document,\n content: contentCall.status === 404\n ? { contentHtml: '', updatedAt: null }\n : normalizeDocumentContent(contentCall.result),\n })\n } catch (error) {\n if (requestSequence.current !== requestId) return\n setState({ status: 'error', message: error instanceof Error ? error.message : t('documents.editor.error.load') })\n }\n }, [documentId, t])\n\n React.useEffect(() => {\n void loadDocument()\n return () => { requestSequence.current += 1 }\n }, [loadDocument])\n\n const reloadEditor = React.useCallback(async () => {\n const requestId = ++requestSequence.current\n const contentCall = await apiCall<unknown>(`/api/documents/${encodeURIComponent(documentId)}/content`)\n if (requestSequence.current !== requestId) {\n throw new Error(t('documents.editor.error.loadContent'))\n }\n if (!contentCall.ok && contentCall.status !== 404) {\n throw new Error(t('documents.editor.error.loadContent'))\n }\n const nextContent = contentCall.status === 404\n ? { contentHtml: '', updatedAt: null }\n : normalizeDocumentContent(contentCall.result)\n contentSaveRef.current = null\n setState((current) => current.status === 'ready' ? {\n ...current,\n content: nextContent,\n } : current)\n setEditor(null)\n setEditorEpoch((current) => current + 1)\n }, [documentId, t])\n\n const handleContentConflict = React.useCallback(() => {\n void reloadEditor().catch((error) => {\n flash(error instanceof Error ? error.message : t('documents.editor.error.loadContent'), 'error')\n })\n }, [reloadEditor, t])\n\n const retryEditorIsland = React.useCallback(() => {\n setEditor(null)\n setEditorEpoch((current) => current + 1)\n }, [])\n\n const toggleVersions = React.useCallback(() => {\n setShowVersions((value) => {\n scrollVersionsIntoView.current = !value\n return !value\n })\n }, [])\n\n // The versions panel mounts at the bottom of the side rail, below the fold on\n // a laptop screen, so opening it gave no visible feedback. Bring it to the\n // top of whichever container scrolls the rail (the sticky aside on wide\n // screens, the page otherwise) and hand it focus for keyboard users.\n React.useLayoutEffect(() => {\n if (!showVersions || !scrollVersionsIntoView.current) return\n scrollVersionsIntoView.current = false\n const panel = versionsPanelRef.current\n if (!panel) return\n revealPanelInScrollContainer(panel)\n panel.focus({ preventScroll: true })\n }, [showVersions])\n\n const setDocumentState = React.useCallback((mutate: (document: DocumentDetail) => DocumentDetail) => {\n setState((current) => current.status === 'ready'\n ? { ...current, document: mutate(current.document) }\n : current)\n }, [])\n\n const pendingToggles = React.useRef<Set<'favorite' | 'watch'>>(new Set())\n const runPersonalToggle = React.useCallback(async (\n kind: 'favorite' | 'watch',\n active: boolean,\n ) => {\n if (state.status !== 'ready' || pendingToggles.current.has(kind)) return\n pendingToggles.current.add(kind)\n const flag = kind === 'favorite' ? 'isFavorite' as const : 'isWatching' as const\n setDocumentState((document) => ({ ...document, [flag]: active }))\n try {\n await runMutation({\n operation: () => apiCallOrThrow(\n `/api/documents/${encodeURIComponent(documentId)}/${kind}`,\n { method: active ? 'POST' : 'DELETE' },\n ),\n context: {\n formId: mutationContextId,\n resourceKind: kind === 'favorite' ? 'documents.document_favorite' : 'documents.document_watcher',\n resourceId: documentId,\n retryLastMutation,\n },\n mutationPayload: { documentId, active },\n })\n } catch (error) {\n setDocumentState((document) => ({ ...document, [flag]: !active }))\n flash(error instanceof Error ? error.message : t('documents.editor.error.load'), 'error')\n } finally {\n pendingToggles.current.delete(kind)\n }\n }, [documentId, mutationContextId, retryLastMutation, runMutation, setDocumentState, state.status, t])\n\n const duplicating = React.useRef(false)\n const [isDuplicating, setIsDuplicating] = React.useState(false)\n const handleDuplicate = React.useCallback(async () => {\n if (state.status !== 'ready' || duplicating.current) return\n duplicating.current = true\n setIsDuplicating(true)\n try {\n const created = await runMutation({\n operation: () => apiCallOrThrow<{ id: string }>(\n `/api/documents/${encodeURIComponent(documentId)}/duplicate`,\n { method: 'POST', body: JSON.stringify({}), headers: { 'content-type': 'application/json' } },\n ),\n context: {\n formId: mutationContextId,\n resourceKind: 'documents.document',\n resourceId: documentId,\n retryLastMutation,\n },\n mutationPayload: { action: 'duplicate', sourceDocumentId: documentId },\n })\n flash(t('documents.duplicate.success'), 'success')\n const createdId = created?.result?.id\n if (createdId) router.push(`/backend/documents/${encodeURIComponent(createdId)}`)\n } catch (error) {\n flash(error instanceof Error ? error.message : t('documents.duplicate.error'), 'error')\n } finally {\n duplicating.current = false\n setIsDuplicating(false)\n }\n }, [documentId, mutationContextId, retryLastMutation, router, runMutation, state.status, t])\n\n const handleArchiveToggle = React.useCallback(async () => {\n if (state.status !== 'ready' || !state.document.capabilities.canArchive) return\n const archived = state.document.archivedAt !== null\n const action = archived ? 'unarchive' : 'archive'\n try {\n await runMutation({\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(state.document.updatedAt),\n () => apiCallOrThrow(`/api/documents/${encodeURIComponent(documentId)}/${action}`, { method: 'POST' }),\n ),\n context: {\n formId: mutationContextId,\n resourceKind: 'documents.document',\n resourceId: documentId,\n retryLastMutation,\n },\n mutationPayload: { action, documentId },\n })\n flash(t(archived ? 'documents.archive.success.unarchive' : 'documents.archive.success.archive'), 'success')\n await loadDocument()\n } catch (error) {\n if (!surfaceRecordConflict(error, t, { onRefresh: () => { void loadDocument() } })) {\n flash(error instanceof Error ? error.message : t('documents.archive.error'), 'error')\n }\n }\n }, [documentId, loadDocument, mutationContextId, retryLastMutation, runMutation, state, t])\n\n const handleDelete = React.useCallback(async () => {\n if (state.status !== 'ready' || !state.document.capabilities.canDelete) return\n const confirmed = await confirm({\n title: t('documents.list.confirmDelete', { title: state.document.title }),\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutation({\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(state.document.updatedAt),\n () => apiCallOrThrow(`/api/documents/${encodeURIComponent(documentId)}`, { method: 'DELETE' }),\n ),\n context: {\n formId: mutationContextId,\n resourceKind: 'documents.document',\n resourceId: documentId,\n retryLastMutation,\n },\n mutationPayload: { id: documentId },\n })\n flash(t('documents.list.success.delete'), 'success')\n router.push('/backend/documents')\n } catch (error) {\n if (!surfaceRecordConflict(error, t, { onRefresh: () => { void loadDocument() } })) {\n flash(error instanceof Error ? error.message : t('documents.list.error.delete'), 'error')\n }\n }\n }, [confirm, documentId, loadDocument, mutationContextId, retryLastMutation, router, runMutation, state, t])\n\n if (state.status !== 'ready') {\n if (state.status === 'notFound') {\n return (\n <Page><PageBody>\n <RecordNotFoundState\n label={t('documents.editor.notFound')}\n backHref=\"/backend/documents\"\n backLabel={t('documents.actions.backToList')}\n />\n </PageBody></Page>\n )\n }\n const label = state.status === 'loading' ? t('documents.editor.loading') : state.message\n return (\n <Page><PageBody>\n {state.status === 'loading' ? <LoadingMessage label={label} /> : (\n <ErrorMessage label={label} action={(\n <div className=\"flex flex-wrap gap-2\">\n <Button type=\"button\" onClick={() => void loadDocument()}>{t('documents.actions.retry')}</Button>\n <LinkButton asChild variant=\"gray\"><Link href=\"/backend/documents\">{t('documents.actions.backToList')}</Link></LinkButton>\n </div>\n )} />\n )}\n </PageBody></Page>\n )\n }\n\n const { document, content } = state\n const capabilities = document.capabilities\n return (\n <Page>\n <PageHeader title={t('documents.nav.document')} actions={(\n <>\n <LinkButton asChild variant=\"gray\"><Link href=\"/backend/documents\">{t('documents.actions.backToList')}</Link></LinkButton>\n <SimpleTooltip content={t(document.isFavorite ? 'documents.actions.unfavorite' : 'documents.actions.favorite')} size=\"sm\">\n <IconButton\n type=\"button\"\n variant=\"outline\"\n size=\"lg\"\n onClick={() => void runPersonalToggle('favorite', !document.isFavorite)}\n aria-pressed={document.isFavorite}\n aria-label={t(document.isFavorite ? 'documents.actions.unfavorite' : 'documents.actions.favorite')}\n >\n <Star />\n </IconButton>\n </SimpleTooltip>\n <SimpleTooltip content={t(document.isWatching ? 'documents.actions.unwatch' : 'documents.actions.watch')} size=\"sm\">\n <IconButton\n type=\"button\"\n variant=\"outline\"\n size=\"lg\"\n onClick={() => void runPersonalToggle('watch', !document.isWatching)}\n aria-pressed={document.isWatching}\n aria-label={t(document.isWatching ? 'documents.actions.unwatch' : 'documents.actions.watch')}\n >\n <Bell />\n </IconButton>\n </SimpleTooltip>\n <Button type=\"button\" variant={showVersions ? 'secondary' : 'outline'} onClick={toggleVersions} aria-pressed={showVersions}>\n <History />{t('documents.actions.versions')}\n </Button>\n <ExportMenu documentId={document.id} editor={editor} />\n {capabilities.canDuplicate ? (\n <Button type=\"button\" variant=\"outline\" disabled={isDuplicating} onClick={() => void handleDuplicate()}>\n <Copy />{t('documents.actions.duplicate')}\n </Button>\n ) : null}\n {capabilities.canArchive ? (\n <Button type=\"button\" variant=\"outline\" onClick={() => void handleArchiveToggle()}>\n {document.archivedAt ? <ArchiveRestore /> : <Archive />}\n {t(document.archivedAt ? 'documents.actions.unarchive' : 'documents.actions.archive')}\n </Button>\n ) : null}\n {capabilities.canShare ? <Button type=\"button\" variant=\"outline\" onClick={() => setShareOpen(true)}>{t('documents.actions.share')}</Button> : null}\n {capabilities.canDelete ? <Button type=\"button\" variant=\"destructive\" onClick={() => void handleDelete()}><Trash2 />{t('documents.actions.delete')}</Button> : null}\n </>\n )} />\n <PageBody>\n {document.archivedAt ? (\n <div\n role=\"status\"\n className=\"mb-4 flex flex-wrap items-center justify-between gap-2 rounded-md border border-status-warning-border bg-status-warning-bg px-4 py-3 text-sm text-status-warning-text\"\n >\n <span>{t('documents.archive.banner')}</span>\n {capabilities.canArchive ? (\n <Button type=\"button\" variant=\"outline\" onClick={() => void handleArchiveToggle()}>\n <ArchiveRestore />{t('documents.actions.unarchive')}\n </Button>\n ) : null}\n </div>\n ) : null}\n <div className=\"flex flex-col gap-4 xl:flex-row\">\n <div className=\"min-w-0 flex-1\">\n <DocumentEditorErrorBoundary\n resetKey={`${document.id}:${editorEpoch}`}\n onRetry={retryEditorIsland}\n fallback={(retry) => (\n <ErrorMessage\n label={t('documents.editor.error.load')}\n action={<Button type=\"button\" variant=\"outline\" onClick={retry}>{t('documents.actions.retry')}</Button>}\n />\n )}\n >\n <DocumentEditorIsland\n key={`${document.id}:${editorEpoch}`}\n documentId={document.id}\n title={document.title}\n initialContentHtml={content.contentHtml}\n contentUpdatedAt={content.updatedAt}\n documentUpdatedAt={document.updatedAt}\n readOnly={!capabilities.canEdit}\n onEditorReady={setEditor}\n onSaveReady={registerContentSave}\n onContentConflict={handleContentConflict}\n onComment={capabilities.canComment ? (anchor) => setCommentFocusRequest((current) => ({ anchor, requestId: (current?.requestId ?? 0) + 1 })) : undefined}\n onTitleChange={(title, updatedAt) => setState((current) => current.status === 'ready' ? {\n ...current,\n document: { ...current.document, title, updatedAt },\n } : current)}\n />\n </DocumentEditorErrorBoundary>\n </div>\n <aside className=\"space-y-4 xl:sticky xl:top-[calc(var(--topbar-height,0px)+1rem)] xl:max-h-[calc(100dvh-var(--topbar-height,0px)-2rem)] xl:w-80 xl:shrink-0 xl:self-start xl:overflow-y-auto\">\n <DocumentNavigator editor={editor} />\n <RelatedRecordsPanel documentId={document.id} canEdit={capabilities.canEdit} editor={editor} />\n <CommentsRail\n documentId={document.id}\n editor={editor}\n commentFocusRequest={commentFocusRequest}\n canComment={capabilities.canComment}\n canShare={capabilities.canShare}\n />\n {showVersions ? (\n <div\n ref={versionsPanelRef}\n tabIndex={-1}\n className=\"scroll-mt-4 rounded-md focus-visible:outline-none focus-visible:shadow-focus\"\n >\n <VersionHistoryPanel\n documentId={document.id}\n canRestore={capabilities.canEdit}\n contentUpdatedAt={content.updatedAt}\n onRestored={reloadEditor}\n onBeforeSnapshot={saveBeforeSnapshot}\n />\n </div>\n ) : null}\n </aside>\n </div>\n </PageBody>\n {capabilities.canShare ? (\n <ShareDialog\n documentId={document.id}\n open={shareOpen}\n onOpenChange={setShareOpen}\n canManage={capabilities.canShare}\n />\n ) : null}\n {ConfirmDialogElement}\n </Page>\n )\n}\n\nexport default DocumentPageClient\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["document"]
|
|
7
7
|
}
|
|
@@ -48,7 +48,8 @@ function VersionHistoryPanel({
|
|
|
48
48
|
documentId,
|
|
49
49
|
tier,
|
|
50
50
|
canRestore,
|
|
51
|
-
onRestored
|
|
51
|
+
onRestored,
|
|
52
|
+
onBeforeSnapshot
|
|
52
53
|
}) {
|
|
53
54
|
const t = useT();
|
|
54
55
|
const mayRestore = resolveVersionRestoreCapability(canRestore, tier);
|
|
@@ -59,6 +60,7 @@ function VersionHistoryPanel({
|
|
|
59
60
|
const [restoringVersionId, setRestoringVersionId] = React.useState(null);
|
|
60
61
|
const [previewVersionId, setPreviewVersionId] = React.useState(null);
|
|
61
62
|
const restoreInFlight = React.useRef(false);
|
|
63
|
+
const snapshotInFlight = React.useRef(false);
|
|
62
64
|
const reloadSequence = React.useRef(0);
|
|
63
65
|
const activeReload = React.useRef(null);
|
|
64
66
|
const activeDocumentId = React.useRef(null);
|
|
@@ -103,16 +105,25 @@ function VersionHistoryPanel({
|
|
|
103
105
|
};
|
|
104
106
|
}, [reload]);
|
|
105
107
|
const handleSnapshot = React.useCallback(async () => {
|
|
106
|
-
if (!mayRestore) return;
|
|
108
|
+
if (!mayRestore || snapshotInFlight.current) return;
|
|
109
|
+
snapshotInFlight.current = true;
|
|
107
110
|
setIsCreating(true);
|
|
108
111
|
const nextLabel = label.trim() || null;
|
|
109
112
|
try {
|
|
110
113
|
await runMutation({
|
|
111
|
-
operation: () =>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
operation: async () => {
|
|
115
|
+
if (onBeforeSnapshot && !await onBeforeSnapshot()) {
|
|
116
|
+
throw new Error(t("documents.versions.error.save"));
|
|
117
|
+
}
|
|
118
|
+
if (currentDocumentIdRef.current !== documentId) {
|
|
119
|
+
throw new Error(t("documents.versions.error.save"));
|
|
120
|
+
}
|
|
121
|
+
return apiCallOrThrow(
|
|
122
|
+
`/api/documents/${encodeURIComponent(documentId)}/versions`,
|
|
123
|
+
{ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ label: nextLabel }) },
|
|
124
|
+
{ errorMessage: t("documents.versions.error.save") }
|
|
125
|
+
);
|
|
126
|
+
},
|
|
116
127
|
context: { formId: mutationContextId, resourceKind: "documents.document_version", resourceId: documentId, retryLastMutation },
|
|
117
128
|
mutationPayload: { label: nextLabel }
|
|
118
129
|
});
|
|
@@ -125,9 +136,10 @@ function VersionHistoryPanel({
|
|
|
125
136
|
if (currentDocumentIdRef.current !== documentId) return;
|
|
126
137
|
flash(error instanceof Error ? error.message : t("documents.versions.error.save"), "error");
|
|
127
138
|
} finally {
|
|
139
|
+
snapshotInFlight.current = false;
|
|
128
140
|
setIsCreating(false);
|
|
129
141
|
}
|
|
130
|
-
}, [documentId, label, mayRestore, mutationContextId, reload, retryLastMutation, runMutation, t]);
|
|
142
|
+
}, [documentId, label, mayRestore, mutationContextId, onBeforeSnapshot, reload, retryLastMutation, runMutation, t]);
|
|
131
143
|
const handleRestore = React.useCallback(async (version) => {
|
|
132
144
|
if (!mayRestore || restoreInFlight.current) return;
|
|
133
145
|
restoreInFlight.current = true;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/documents/backend/documents/%5Bid%5D/VersionHistoryPanel.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport dynamic from 'next/dynamic'\nimport { Clock, Eye, History, Save } from 'lucide-react'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { readArrayPayload } from '../documentUi'\nimport { resolveVersionRestoreCapability, type DocumentTier } from './componentCapabilities'\nimport { restoreVersionWithCurrentContentToken } from './restoreVersion'\nimport type { VersionPreview } from './VersionPreviewDialog'\nimport { normalizeVersion, type DocumentVersion } from './versionHistoryModel'\n\nfunction VersionPreviewDialogLoading({ error, retry }: { error?: Error | null; retry?: () => void }) {\n const t = useT()\n // `next/dynamic` never forwards the caller's `onOpenChange` to a loading\n // shell, so own the dismissal here. Without it a chunk that fails for good\n // (a deploy invalidated it) traps the user in a modal Escape cannot close.\n const [open, setOpen] = React.useState(true)\n if (!open) return null\n return (\n <Dialog open onOpenChange={(next) => { if (!next) setOpen(false) }}>\n <DialogContent size=\"lg\" dismissible={Boolean(error)}>\n <DialogHeader>\n <DialogTitle>{t('documents.versions.actions.preview')}</DialogTitle>\n <DialogDescription>{t('documents.versions.preview.loading')}</DialogDescription>\n </DialogHeader>\n {error ? (\n <ErrorMessage\n label={t('documents.versions.preview.error')}\n action={<Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={retry}>{t('documents.actions.retry')}</Button>}\n />\n ) : (\n <div role=\"status\" aria-live=\"polite\"><LoadingMessage label={t('documents.versions.preview.loading')} /></div>\n )}\n </DialogContent>\n </Dialog>\n )\n}\n\nconst VersionPreviewDialog = dynamic(\n () => import('./VersionPreviewDialog').then((module) => module.VersionPreviewDialog),\n { ssr: false, loading: VersionPreviewDialogLoading },\n)\n\ntype VersionsState = { status: 'loading' } | { status: 'error'; message: string } | { status: 'ready'; versions: DocumentVersion[] }\ntype VersionHistoryPanelProps = {\n documentId: string\n /** Legacy compatibility; an explicit capability projection takes precedence. */\n tier?: DocumentTier\n canRestore?: boolean\n /**\n * Accepted for compatibility. Restore reads the content row's current\n * optimistic-lock token immediately before the request instead of relying\n * on a token observed at page load, which every own edit since invalidated.\n */\n contentUpdatedAt?: string | null\n onRestored?: () => void | Promise<void>\n}\n\nexport { resolveVersionRestoreCapability } from './componentCapabilities'\nexport { normalizeVersion } from './versionHistoryModel'\n\nexport function VersionHistoryPanel({\n documentId,\n tier,\n canRestore,\n onRestored,\n}: VersionHistoryPanelProps) {\n const t = useT()\n const mayRestore = resolveVersionRestoreCapability(canRestore, tier)\n const labelInputId = React.useId()\n const [state, setState] = React.useState<VersionsState>({ status: 'loading' })\n const [label, setLabel] = React.useState('')\n const [isCreating, setIsCreating] = React.useState(false)\n const [restoringVersionId, setRestoringVersionId] = React.useState<string | null>(null)\n const [previewVersionId, setPreviewVersionId] = React.useState<string | null>(null)\n const restoreInFlight = React.useRef(false)\n const reloadSequence = React.useRef(0)\n const activeReload = React.useRef<AbortController | null>(null)\n const activeDocumentId = React.useRef<string | null>(null)\n const currentDocumentIdRef = React.useRef(documentId)\n currentDocumentIdRef.current = documentId\n const mutationContextId = `documents-versions:${documentId}`\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({ contextId: mutationContextId, blockedMessage: t('ui.forms.flash.saveBlocked') })\n\n const reload = React.useCallback(async () => {\n if (currentDocumentIdRef.current !== documentId) return\n const reloadId = ++reloadSequence.current\n const controller = new AbortController()\n activeReload.current?.abort()\n activeReload.current = controller\n const isCurrent = () => currentDocumentIdRef.current === documentId\n && reloadSequence.current === reloadId\n && !controller.signal.aborted\n const documentChanged = activeDocumentId.current !== documentId\n activeDocumentId.current = documentId\n setState((current) => !documentChanged && current.status === 'ready' ? current : { status: 'loading' })\n try {\n const call = await apiCall<unknown>(\n `/api/documents/${encodeURIComponent(documentId)}/versions`,\n { signal: controller.signal },\n )\n if (!isCurrent()) return\n if (!call.ok) return setState({ status: 'error', message: t('documents.versions.error.load') })\n setState({\n status: 'ready',\n versions: readArrayPayload(call.result, 'items', 'data')\n .map((version) => normalizeVersion(version, t('documents.users.unknown')))\n .filter((version): version is DocumentVersion => version !== null),\n })\n } catch (error) {\n if (!isCurrent()) return\n setState({ status: 'error', message: error instanceof Error ? error.message : t('documents.versions.error.load') })\n } finally {\n if (activeReload.current === controller) activeReload.current = null\n }\n }, [documentId, t])\n\n React.useEffect(() => {\n void reload()\n return () => {\n reloadSequence.current += 1\n activeReload.current?.abort()\n activeReload.current = null\n }\n }, [reload])\n\n const handleSnapshot = React.useCallback(async () => {\n if (!mayRestore) return\n setIsCreating(true)\n const nextLabel = label.trim() || null\n try {\n await runMutation({\n operation: () => apiCallOrThrow(\n `/api/documents/${encodeURIComponent(documentId)}/versions`,\n { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ label: nextLabel }) },\n { errorMessage: t('documents.versions.error.save') },\n ),\n context: { formId: mutationContextId, resourceKind: 'documents.document_version', resourceId: documentId, retryLastMutation },\n mutationPayload: { label: nextLabel },\n })\n if (currentDocumentIdRef.current !== documentId) return\n setLabel('')\n await reload()\n if (currentDocumentIdRef.current !== documentId) return\n flash(t('documents.versions.snapshot.created'), 'success')\n } catch (error) {\n if (currentDocumentIdRef.current !== documentId) return\n flash(error instanceof Error ? error.message : t('documents.versions.error.save'), 'error')\n } finally { setIsCreating(false) }\n }, [documentId, label, mayRestore, mutationContextId, reload, retryLastMutation, runMutation, t])\n\n const handleRestore = React.useCallback(async (version: VersionPreview) => {\n if (!mayRestore || restoreInFlight.current) return\n restoreInFlight.current = true\n try {\n if (currentDocumentIdRef.current !== documentId) return\n setRestoringVersionId(version.id)\n await runMutation({\n operation: () => restoreVersionWithCurrentContentToken({\n documentId,\n versionId: version.id,\n errorMessage: t('documents.versions.error.restore'),\n }),\n context: { formId: mutationContextId, resourceKind: 'documents.document_version', resourceId: version.id, retryLastMutation },\n mutationPayload: { action: 'restore', versionId: version.id },\n })\n if (currentDocumentIdRef.current !== documentId) return\n await reload()\n if (currentDocumentIdRef.current !== documentId) return\n await onRestored?.()\n if (currentDocumentIdRef.current !== documentId) return\n setPreviewVersionId(null)\n flash(t('documents.versions.restored'), 'success')\n } catch (error) {\n if (currentDocumentIdRef.current !== documentId) return\n if (!surfaceRecordConflict(error, t, {\n onRefresh: onRestored ? () => {\n void Promise.resolve(onRestored()).catch((refreshError) => {\n flash(refreshError instanceof Error ? refreshError.message : t('documents.versions.error.restore'), 'error')\n })\n } : undefined,\n })) {\n flash(error instanceof Error ? error.message : t('documents.versions.error.restore'), 'error')\n }\n } finally {\n restoreInFlight.current = false\n setRestoringVersionId(null)\n }\n }, [documentId, mayRestore, mutationContextId, onRestored, reload, retryLastMutation, runMutation, t])\n\n const versions = state.status === 'ready' ? state.versions : []\n return (\n <section className=\"rounded-lg border border-border bg-card p-4\">\n <div className=\"mb-4 flex items-center gap-2\"><History className=\"size-4 text-muted-foreground\" aria-hidden=\"true\" /><h2 className=\"text-sm font-semibold\">{t('documents.versions.title')}</h2></div>\n <div className=\"space-y-4\">\n {mayRestore ? (\n <form className=\"space-y-3 rounded-lg border border-border bg-muted/20 p-3\" onSubmit={(event) => { event.preventDefault(); void handleSnapshot() }} onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { event.preventDefault(); void handleSnapshot() }\n if (event.key === 'Escape') setLabel('')\n }}>\n <Label htmlFor={labelInputId}>{t('documents.versions.snapshot.labelPlaceholder')}</Label>\n <Input id={labelInputId} value={label} onChange={(event) => setLabel(event.target.value)} placeholder={t('documents.versions.snapshot.labelPlaceholder')} disabled={isCreating} />\n <Button type=\"submit\" disabled={isCreating}><Save />{t('documents.versions.actions.snapshot')}</Button>\n </form>\n ) : null}\n {state.status === 'loading' ? <LoadingMessage label={t('documents.versions.loading')} /> : null}\n {state.status === 'error' ? (\n <ErrorMessage\n label={state.message}\n action={<Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={() => void reload()}>{t('documents.actions.retry')}</Button>}\n />\n ) : null}\n {state.status === 'ready' && versions.length === 0 ? <EmptyState size=\"sm\" variant=\"subtle\" title={t('documents.versions.empty')} icon={<History className=\"size-5\" />} /> : null}\n {versions.map((version) => (\n <article key={version.id} className=\"space-y-3 rounded-lg border border-border bg-background p-3\">\n <p className=\"text-sm font-medium\">{version.label ?? t('documents.versions.snapshot.defaultLabel')}</p>\n <p className=\"flex items-center gap-2 text-xs text-muted-foreground\"><Clock className=\"size-3\" aria-hidden=\"true\" />{new Date(version.createdAt).toLocaleString()}</p>\n <p className=\"text-xs text-muted-foreground\">{version.creatorLabel || t('documents.users.unknown')}</p>\n <Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={() => setPreviewVersionId(version.id)}><Eye />{t('documents.versions.actions.preview')}</Button>\n </article>\n ))}\n </div>\n {previewVersionId ? <VersionPreviewDialog documentId={documentId} versionId={previewVersionId} canRestore={mayRestore} isRestoring={restoringVersionId !== null} onOpenChange={(open) => { if (!open) setPreviewVersionId(null) }} onRestore={handleRestore} /> : null}\n </section>\n )\n}\n\nexport default VersionHistoryPanel\n"],
|
|
5
|
-
"mappings": ";AAgCQ,SACE,KADF;AA9BR,YAAY,WAAW;AACvB,OAAO,aAAa;AACpB,SAAS,OAAO,KAAK,SAAS,YAAY;AAC1C,SAAS,SAAS,sBAAsB;AACxC,SAAS,0BAA0B;AACnC,SAAS,6BAA6B;AACtC,SAAS,aAAa;AACtB,SAAS,gBAAgB,oBAAoB;AAC7C,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB,SAAS,QAAQ,eAAe,mBAAmB,cAAc,mBAAmB;AACpF,SAAS,aAAa;AACtB,SAAS,aAAa;AACtB,SAAS,YAAY;AACrB,SAAS,wBAAwB;AACjC,SAAS,uCAA0D;AACnE,SAAS,6CAA6C;AAEtD,SAAS,wBAA8C;AAEvD,SAAS,4BAA4B,EAAE,OAAO,MAAM,GAAiD;AACnG,QAAM,IAAI,KAAK;AAIf,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,IAAI;AAC3C,MAAI,CAAC,KAAM,QAAO;AAClB,SACE,oBAAC,UAAO,MAAI,MAAC,cAAc,CAAC,SAAS;AAAE,QAAI,CAAC,KAAM,SAAQ,KAAK;AAAA,EAAE,GAC/D,+BAAC,iBAAc,MAAK,MAAK,aAAa,QAAQ,KAAK,GACjD;AAAA,yBAAC,gBACC;AAAA,0BAAC,eAAa,YAAE,oCAAoC,GAAE;AAAA,MACtD,oBAAC,qBAAmB,YAAE,oCAAoC,GAAE;AAAA,OAC9D;AAAA,IACC,QACC;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,kCAAkC;AAAA,QAC3C,QAAQ,oBAAC,UAAO,MAAK,UAAS,MAAK,MAAK,SAAQ,WAAU,SAAS,OAAQ,YAAE,yBAAyB,GAAE;AAAA;AAAA,IAC1G,IAEA,oBAAC,SAAI,MAAK,UAAS,aAAU,UAAS,8BAAC,kBAAe,OAAO,EAAE,oCAAoC,GAAG,GAAE;AAAA,KAE5G,GACF;AAEJ;AAEA,MAAM,uBAAuB;AAAA,EAC3B,MAAM,OAAO,wBAAwB,EAAE,KAAK,CAAC,WAAW,OAAO,oBAAoB;AAAA,EACnF,EAAE,KAAK,OAAO,SAAS,4BAA4B;AACrD;
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport dynamic from 'next/dynamic'\nimport { Clock, Eye, History, Save } from 'lucide-react'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { readArrayPayload } from '../documentUi'\nimport { resolveVersionRestoreCapability, type DocumentTier } from './componentCapabilities'\nimport { restoreVersionWithCurrentContentToken } from './restoreVersion'\nimport type { VersionPreview } from './VersionPreviewDialog'\nimport { normalizeVersion, type DocumentVersion } from './versionHistoryModel'\n\nfunction VersionPreviewDialogLoading({ error, retry }: { error?: Error | null; retry?: () => void }) {\n const t = useT()\n // `next/dynamic` never forwards the caller's `onOpenChange` to a loading\n // shell, so own the dismissal here. Without it a chunk that fails for good\n // (a deploy invalidated it) traps the user in a modal Escape cannot close.\n const [open, setOpen] = React.useState(true)\n if (!open) return null\n return (\n <Dialog open onOpenChange={(next) => { if (!next) setOpen(false) }}>\n <DialogContent size=\"lg\" dismissible={Boolean(error)}>\n <DialogHeader>\n <DialogTitle>{t('documents.versions.actions.preview')}</DialogTitle>\n <DialogDescription>{t('documents.versions.preview.loading')}</DialogDescription>\n </DialogHeader>\n {error ? (\n <ErrorMessage\n label={t('documents.versions.preview.error')}\n action={<Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={retry}>{t('documents.actions.retry')}</Button>}\n />\n ) : (\n <div role=\"status\" aria-live=\"polite\"><LoadingMessage label={t('documents.versions.preview.loading')} /></div>\n )}\n </DialogContent>\n </Dialog>\n )\n}\n\nconst VersionPreviewDialog = dynamic(\n () => import('./VersionPreviewDialog').then((module) => module.VersionPreviewDialog),\n { ssr: false, loading: VersionPreviewDialogLoading },\n)\n\ntype VersionsState = { status: 'loading' } | { status: 'error'; message: string } | { status: 'ready'; versions: DocumentVersion[] }\ntype VersionHistoryPanelProps = {\n documentId: string\n /** Legacy compatibility; an explicit capability projection takes precedence. */\n tier?: DocumentTier\n canRestore?: boolean\n /**\n * Accepted for compatibility. Restore reads the content row's current\n * optimistic-lock token immediately before the request instead of relying\n * on a token observed at page load, which every own edit since invalidated.\n */\n contentUpdatedAt?: string | null\n onRestored?: () => void | Promise<void>\n onBeforeSnapshot?: () => Promise<boolean>\n}\n\nexport { resolveVersionRestoreCapability } from './componentCapabilities'\nexport { normalizeVersion } from './versionHistoryModel'\n\nexport function VersionHistoryPanel({\n documentId,\n tier,\n canRestore,\n onRestored,\n onBeforeSnapshot,\n}: VersionHistoryPanelProps) {\n const t = useT()\n const mayRestore = resolveVersionRestoreCapability(canRestore, tier)\n const labelInputId = React.useId()\n const [state, setState] = React.useState<VersionsState>({ status: 'loading' })\n const [label, setLabel] = React.useState('')\n const [isCreating, setIsCreating] = React.useState(false)\n const [restoringVersionId, setRestoringVersionId] = React.useState<string | null>(null)\n const [previewVersionId, setPreviewVersionId] = React.useState<string | null>(null)\n const restoreInFlight = React.useRef(false)\n const snapshotInFlight = React.useRef(false)\n const reloadSequence = React.useRef(0)\n const activeReload = React.useRef<AbortController | null>(null)\n const activeDocumentId = React.useRef<string | null>(null)\n const currentDocumentIdRef = React.useRef(documentId)\n currentDocumentIdRef.current = documentId\n const mutationContextId = `documents-versions:${documentId}`\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({ contextId: mutationContextId, blockedMessage: t('ui.forms.flash.saveBlocked') })\n\n const reload = React.useCallback(async () => {\n if (currentDocumentIdRef.current !== documentId) return\n const reloadId = ++reloadSequence.current\n const controller = new AbortController()\n activeReload.current?.abort()\n activeReload.current = controller\n const isCurrent = () => currentDocumentIdRef.current === documentId\n && reloadSequence.current === reloadId\n && !controller.signal.aborted\n const documentChanged = activeDocumentId.current !== documentId\n activeDocumentId.current = documentId\n setState((current) => !documentChanged && current.status === 'ready' ? current : { status: 'loading' })\n try {\n const call = await apiCall<unknown>(\n `/api/documents/${encodeURIComponent(documentId)}/versions`,\n { signal: controller.signal },\n )\n if (!isCurrent()) return\n if (!call.ok) return setState({ status: 'error', message: t('documents.versions.error.load') })\n setState({\n status: 'ready',\n versions: readArrayPayload(call.result, 'items', 'data')\n .map((version) => normalizeVersion(version, t('documents.users.unknown')))\n .filter((version): version is DocumentVersion => version !== null),\n })\n } catch (error) {\n if (!isCurrent()) return\n setState({ status: 'error', message: error instanceof Error ? error.message : t('documents.versions.error.load') })\n } finally {\n if (activeReload.current === controller) activeReload.current = null\n }\n }, [documentId, t])\n\n React.useEffect(() => {\n void reload()\n return () => {\n reloadSequence.current += 1\n activeReload.current?.abort()\n activeReload.current = null\n }\n }, [reload])\n\n const handleSnapshot = React.useCallback(async () => {\n if (!mayRestore || snapshotInFlight.current) return\n snapshotInFlight.current = true\n setIsCreating(true)\n const nextLabel = label.trim() || null\n try {\n await runMutation({\n operation: async () => {\n if (onBeforeSnapshot && !(await onBeforeSnapshot())) {\n throw new Error(t('documents.versions.error.save'))\n }\n if (currentDocumentIdRef.current !== documentId) {\n throw new Error(t('documents.versions.error.save'))\n }\n return apiCallOrThrow(\n `/api/documents/${encodeURIComponent(documentId)}/versions`,\n { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ label: nextLabel }) },\n { errorMessage: t('documents.versions.error.save') },\n )\n },\n context: { formId: mutationContextId, resourceKind: 'documents.document_version', resourceId: documentId, retryLastMutation },\n mutationPayload: { label: nextLabel },\n })\n if (currentDocumentIdRef.current !== documentId) return\n setLabel('')\n await reload()\n if (currentDocumentIdRef.current !== documentId) return\n flash(t('documents.versions.snapshot.created'), 'success')\n } catch (error) {\n if (currentDocumentIdRef.current !== documentId) return\n flash(error instanceof Error ? error.message : t('documents.versions.error.save'), 'error')\n } finally {\n snapshotInFlight.current = false\n setIsCreating(false)\n }\n }, [documentId, label, mayRestore, mutationContextId, onBeforeSnapshot, reload, retryLastMutation, runMutation, t])\n\n const handleRestore = React.useCallback(async (version: VersionPreview) => {\n if (!mayRestore || restoreInFlight.current) return\n restoreInFlight.current = true\n try {\n if (currentDocumentIdRef.current !== documentId) return\n setRestoringVersionId(version.id)\n await runMutation({\n operation: () => restoreVersionWithCurrentContentToken({\n documentId,\n versionId: version.id,\n errorMessage: t('documents.versions.error.restore'),\n }),\n context: { formId: mutationContextId, resourceKind: 'documents.document_version', resourceId: version.id, retryLastMutation },\n mutationPayload: { action: 'restore', versionId: version.id },\n })\n if (currentDocumentIdRef.current !== documentId) return\n await reload()\n if (currentDocumentIdRef.current !== documentId) return\n await onRestored?.()\n if (currentDocumentIdRef.current !== documentId) return\n setPreviewVersionId(null)\n flash(t('documents.versions.restored'), 'success')\n } catch (error) {\n if (currentDocumentIdRef.current !== documentId) return\n if (!surfaceRecordConflict(error, t, {\n onRefresh: onRestored ? () => {\n void Promise.resolve(onRestored()).catch((refreshError) => {\n flash(refreshError instanceof Error ? refreshError.message : t('documents.versions.error.restore'), 'error')\n })\n } : undefined,\n })) {\n flash(error instanceof Error ? error.message : t('documents.versions.error.restore'), 'error')\n }\n } finally {\n restoreInFlight.current = false\n setRestoringVersionId(null)\n }\n }, [documentId, mayRestore, mutationContextId, onRestored, reload, retryLastMutation, runMutation, t])\n\n const versions = state.status === 'ready' ? state.versions : []\n return (\n <section className=\"rounded-lg border border-border bg-card p-4\">\n <div className=\"mb-4 flex items-center gap-2\"><History className=\"size-4 text-muted-foreground\" aria-hidden=\"true\" /><h2 className=\"text-sm font-semibold\">{t('documents.versions.title')}</h2></div>\n <div className=\"space-y-4\">\n {mayRestore ? (\n <form className=\"space-y-3 rounded-lg border border-border bg-muted/20 p-3\" onSubmit={(event) => { event.preventDefault(); void handleSnapshot() }} onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { event.preventDefault(); void handleSnapshot() }\n if (event.key === 'Escape') setLabel('')\n }}>\n <Label htmlFor={labelInputId}>{t('documents.versions.snapshot.labelPlaceholder')}</Label>\n <Input id={labelInputId} value={label} onChange={(event) => setLabel(event.target.value)} placeholder={t('documents.versions.snapshot.labelPlaceholder')} disabled={isCreating} />\n <Button type=\"submit\" disabled={isCreating}><Save />{t('documents.versions.actions.snapshot')}</Button>\n </form>\n ) : null}\n {state.status === 'loading' ? <LoadingMessage label={t('documents.versions.loading')} /> : null}\n {state.status === 'error' ? (\n <ErrorMessage\n label={state.message}\n action={<Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={() => void reload()}>{t('documents.actions.retry')}</Button>}\n />\n ) : null}\n {state.status === 'ready' && versions.length === 0 ? <EmptyState size=\"sm\" variant=\"subtle\" title={t('documents.versions.empty')} icon={<History className=\"size-5\" />} /> : null}\n {versions.map((version) => (\n <article key={version.id} className=\"space-y-3 rounded-lg border border-border bg-background p-3\">\n <p className=\"text-sm font-medium\">{version.label ?? t('documents.versions.snapshot.defaultLabel')}</p>\n <p className=\"flex items-center gap-2 text-xs text-muted-foreground\"><Clock className=\"size-3\" aria-hidden=\"true\" />{new Date(version.createdAt).toLocaleString()}</p>\n <p className=\"text-xs text-muted-foreground\">{version.creatorLabel || t('documents.users.unknown')}</p>\n <Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={() => setPreviewVersionId(version.id)}><Eye />{t('documents.versions.actions.preview')}</Button>\n </article>\n ))}\n </div>\n {previewVersionId ? <VersionPreviewDialog documentId={documentId} versionId={previewVersionId} canRestore={mayRestore} isRestoring={restoringVersionId !== null} onOpenChange={(open) => { if (!open) setPreviewVersionId(null) }} onRestore={handleRestore} /> : null}\n </section>\n )\n}\n\nexport default VersionHistoryPanel\n"],
|
|
5
|
+
"mappings": ";AAgCQ,SACE,KADF;AA9BR,YAAY,WAAW;AACvB,OAAO,aAAa;AACpB,SAAS,OAAO,KAAK,SAAS,YAAY;AAC1C,SAAS,SAAS,sBAAsB;AACxC,SAAS,0BAA0B;AACnC,SAAS,6BAA6B;AACtC,SAAS,aAAa;AACtB,SAAS,gBAAgB,oBAAoB;AAC7C,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB,SAAS,QAAQ,eAAe,mBAAmB,cAAc,mBAAmB;AACpF,SAAS,aAAa;AACtB,SAAS,aAAa;AACtB,SAAS,YAAY;AACrB,SAAS,wBAAwB;AACjC,SAAS,uCAA0D;AACnE,SAAS,6CAA6C;AAEtD,SAAS,wBAA8C;AAEvD,SAAS,4BAA4B,EAAE,OAAO,MAAM,GAAiD;AACnG,QAAM,IAAI,KAAK;AAIf,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,IAAI;AAC3C,MAAI,CAAC,KAAM,QAAO;AAClB,SACE,oBAAC,UAAO,MAAI,MAAC,cAAc,CAAC,SAAS;AAAE,QAAI,CAAC,KAAM,SAAQ,KAAK;AAAA,EAAE,GAC/D,+BAAC,iBAAc,MAAK,MAAK,aAAa,QAAQ,KAAK,GACjD;AAAA,yBAAC,gBACC;AAAA,0BAAC,eAAa,YAAE,oCAAoC,GAAE;AAAA,MACtD,oBAAC,qBAAmB,YAAE,oCAAoC,GAAE;AAAA,OAC9D;AAAA,IACC,QACC;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,kCAAkC;AAAA,QAC3C,QAAQ,oBAAC,UAAO,MAAK,UAAS,MAAK,MAAK,SAAQ,WAAU,SAAS,OAAQ,YAAE,yBAAyB,GAAE;AAAA;AAAA,IAC1G,IAEA,oBAAC,SAAI,MAAK,UAAS,aAAU,UAAS,8BAAC,kBAAe,OAAO,EAAE,oCAAoC,GAAG,GAAE;AAAA,KAE5G,GACF;AAEJ;AAEA,MAAM,uBAAuB;AAAA,EAC3B,MAAM,OAAO,wBAAwB,EAAE,KAAK,CAAC,WAAW,OAAO,oBAAoB;AAAA,EACnF,EAAE,KAAK,OAAO,SAAS,4BAA4B;AACrD;AAkBA,SAAS,mCAAAA,wCAAuC;AAChD,SAAS,oBAAAC,yBAAwB;AAE1B,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,IAAI,KAAK;AACf,QAAM,aAAa,gCAAgC,YAAY,IAAI;AACnE,QAAM,eAAe,MAAM,MAAM;AACjC,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,EAAE,QAAQ,UAAU,CAAC;AAC7E,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAC3C,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AACxD,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,MAAM,SAAwB,IAAI;AACtF,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAwB,IAAI;AAClF,QAAM,kBAAkB,MAAM,OAAO,KAAK;AAC1C,QAAM,mBAAmB,MAAM,OAAO,KAAK;AAC3C,QAAM,iBAAiB,MAAM,OAAO,CAAC;AACrC,QAAM,eAAe,MAAM,OAA+B,IAAI;AAC9D,QAAM,mBAAmB,MAAM,OAAsB,IAAI;AACzD,QAAM,uBAAuB,MAAM,OAAO,UAAU;AACpD,uBAAqB,UAAU;AAC/B,QAAM,oBAAoB,sBAAsB,UAAU;AAC1D,QAAM,EAAE,aAAa,kBAAkB,IAAI,mBAKxC,EAAE,WAAW,mBAAmB,gBAAgB,EAAE,4BAA4B,EAAE,CAAC;AAEpF,QAAM,SAAS,MAAM,YAAY,YAAY;AAC3C,QAAI,qBAAqB,YAAY,WAAY;AACjD,UAAM,WAAW,EAAE,eAAe;AAClC,UAAM,aAAa,IAAI,gBAAgB;AACvC,iBAAa,SAAS,MAAM;AAC5B,iBAAa,UAAU;AACvB,UAAM,YAAY,MAAM,qBAAqB,YAAY,cACpD,eAAe,YAAY,YAC3B,CAAC,WAAW,OAAO;AACxB,UAAM,kBAAkB,iBAAiB,YAAY;AACrD,qBAAiB,UAAU;AAC3B,aAAS,CAAC,YAAY,CAAC,mBAAmB,QAAQ,WAAW,UAAU,UAAU,EAAE,QAAQ,UAAU,CAAC;AACtG,QAAI;AACF,YAAM,OAAO,MAAM;AAAA,QACjB,kBAAkB,mBAAmB,UAAU,CAAC;AAAA,QAChD,EAAE,QAAQ,WAAW,OAAO;AAAA,MAC9B;AACA,UAAI,CAAC,UAAU,EAAG;AAClB,UAAI,CAAC,KAAK,GAAI,QAAO,SAAS,EAAE,QAAQ,SAAS,SAAS,EAAE,+BAA+B,EAAE,CAAC;AAC9F,eAAS;AAAA,QACP,QAAQ;AAAA,QACR,UAAU,iBAAiB,KAAK,QAAQ,SAAS,MAAM,EACpD,IAAI,CAAC,YAAY,iBAAiB,SAAS,EAAE,yBAAyB,CAAC,CAAC,EACxE,OAAO,CAAC,YAAwC,YAAY,IAAI;AAAA,MACrE,CAAC;AAAA,IACH,SAAS,OAAO;AACd,UAAI,CAAC,UAAU,EAAG;AAClB,eAAS,EAAE,QAAQ,SAAS,SAAS,iBAAiB,QAAQ,MAAM,UAAU,EAAE,+BAA+B,EAAE,CAAC;AAAA,IACpH,UAAE;AACA,UAAI,aAAa,YAAY,WAAY,cAAa,UAAU;AAAA,IAClE;AAAA,EACF,GAAG,CAAC,YAAY,CAAC,CAAC;AAElB,QAAM,UAAU,MAAM;AACpB,SAAK,OAAO;AACZ,WAAO,MAAM;AACX,qBAAe,WAAW;AAC1B,mBAAa,SAAS,MAAM;AAC5B,mBAAa,UAAU;AAAA,IACzB;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,iBAAiB,MAAM,YAAY,YAAY;AACnD,QAAI,CAAC,cAAc,iBAAiB,QAAS;AAC7C,qBAAiB,UAAU;AAC3B,kBAAc,IAAI;AAClB,UAAM,YAAY,MAAM,KAAK,KAAK;AAClC,QAAI;AACF,YAAM,YAAY;AAAA,QAChB,WAAW,YAAY;AACrB,cAAI,oBAAoB,CAAE,MAAM,iBAAiB,GAAI;AACnD,kBAAM,IAAI,MAAM,EAAE,+BAA+B,CAAC;AAAA,UACpD;AACA,cAAI,qBAAqB,YAAY,YAAY;AAC/C,kBAAM,IAAI,MAAM,EAAE,+BAA+B,CAAC;AAAA,UACpD;AACA,iBAAO;AAAA,YACL,kBAAkB,mBAAmB,UAAU,CAAC;AAAA,YAChD,EAAE,QAAQ,QAAQ,SAAS,EAAE,gBAAgB,mBAAmB,GAAG,MAAM,KAAK,UAAU,EAAE,OAAO,UAAU,CAAC,EAAE;AAAA,YAC9G,EAAE,cAAc,EAAE,+BAA+B,EAAE;AAAA,UACrD;AAAA,QACF;AAAA,QACA,SAAS,EAAE,QAAQ,mBAAmB,cAAc,8BAA8B,YAAY,YAAY,kBAAkB;AAAA,QAC5H,iBAAiB,EAAE,OAAO,UAAU;AAAA,MACtC,CAAC;AACD,UAAI,qBAAqB,YAAY,WAAY;AACjD,eAAS,EAAE;AACX,YAAM,OAAO;AACb,UAAI,qBAAqB,YAAY,WAAY;AACjD,YAAM,EAAE,qCAAqC,GAAG,SAAS;AAAA,IAC3D,SAAS,OAAO;AACd,UAAI,qBAAqB,YAAY,WAAY;AACjD,YAAM,iBAAiB,QAAQ,MAAM,UAAU,EAAE,+BAA+B,GAAG,OAAO;AAAA,IAC5F,UAAE;AACA,uBAAiB,UAAU;AAC3B,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,YAAY,OAAO,YAAY,mBAAmB,kBAAkB,QAAQ,mBAAmB,aAAa,CAAC,CAAC;AAElH,QAAM,gBAAgB,MAAM,YAAY,OAAO,YAA4B;AACzE,QAAI,CAAC,cAAc,gBAAgB,QAAS;AAC5C,oBAAgB,UAAU;AAC1B,QAAI;AACF,UAAI,qBAAqB,YAAY,WAAY;AACjD,4BAAsB,QAAQ,EAAE;AAChC,YAAM,YAAY;AAAA,QAChB,WAAW,MAAM,sCAAsC;AAAA,UACrD;AAAA,UACA,WAAW,QAAQ;AAAA,UACnB,cAAc,EAAE,kCAAkC;AAAA,QACpD,CAAC;AAAA,QACD,SAAS,EAAE,QAAQ,mBAAmB,cAAc,8BAA8B,YAAY,QAAQ,IAAI,kBAAkB;AAAA,QAC5H,iBAAiB,EAAE,QAAQ,WAAW,WAAW,QAAQ,GAAG;AAAA,MAC9D,CAAC;AACD,UAAI,qBAAqB,YAAY,WAAY;AACjD,YAAM,OAAO;AACb,UAAI,qBAAqB,YAAY,WAAY;AACjD,YAAM,aAAa;AACnB,UAAI,qBAAqB,YAAY,WAAY;AACjD,0BAAoB,IAAI;AACxB,YAAM,EAAE,6BAA6B,GAAG,SAAS;AAAA,IACnD,SAAS,OAAO;AACd,UAAI,qBAAqB,YAAY,WAAY;AACjD,UAAI,CAAC,sBAAsB,OAAO,GAAG;AAAA,QACnC,WAAW,aAAa,MAAM;AAC5B,eAAK,QAAQ,QAAQ,WAAW,CAAC,EAAE,MAAM,CAAC,iBAAiB;AACzD,kBAAM,wBAAwB,QAAQ,aAAa,UAAU,EAAE,kCAAkC,GAAG,OAAO;AAAA,UAC7G,CAAC;AAAA,QACH,IAAI;AAAA,MACN,CAAC,GAAG;AACF,cAAM,iBAAiB,QAAQ,MAAM,UAAU,EAAE,kCAAkC,GAAG,OAAO;AAAA,MAC/F;AAAA,IACF,UAAE;AACA,sBAAgB,UAAU;AAC1B,4BAAsB,IAAI;AAAA,IAC5B;AAAA,EACF,GAAG,CAAC,YAAY,YAAY,mBAAmB,YAAY,QAAQ,mBAAmB,aAAa,CAAC,CAAC;AAErG,QAAM,WAAW,MAAM,WAAW,UAAU,MAAM,WAAW,CAAC;AAC9D,SACE,qBAAC,aAAQ,WAAU,+CACjB;AAAA,yBAAC,SAAI,WAAU,gCAA+B;AAAA,0BAAC,WAAQ,WAAU,gCAA+B,eAAY,QAAO;AAAA,MAAE,oBAAC,QAAG,WAAU,yBAAyB,YAAE,0BAA0B,GAAE;AAAA,OAAK;AAAA,IAC/L,qBAAC,SAAI,WAAU,aACZ;AAAA,mBACC,qBAAC,UAAK,WAAU,6DAA4D,UAAU,CAAC,UAAU;AAAE,cAAM,eAAe;AAAG,aAAK,eAAe;AAAA,MAAE,GAAG,WAAW,CAAC,UAAU;AACxK,aAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;AAAE,gBAAM,eAAe;AAAG,eAAK,eAAe;AAAA,QAAE;AAC/G,YAAI,MAAM,QAAQ,SAAU,UAAS,EAAE;AAAA,MACzC,GACE;AAAA,4BAAC,SAAM,SAAS,cAAe,YAAE,8CAA8C,GAAE;AAAA,QACjF,oBAAC,SAAM,IAAI,cAAc,OAAO,OAAO,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK,GAAG,aAAa,EAAE,8CAA8C,GAAG,UAAU,YAAY;AAAA,QAChL,qBAAC,UAAO,MAAK,UAAS,UAAU,YAAY;AAAA,8BAAC,QAAK;AAAA,UAAG,EAAE,qCAAqC;AAAA,WAAE;AAAA,SAChG,IACE;AAAA,MACH,MAAM,WAAW,YAAY,oBAAC,kBAAe,OAAO,EAAE,4BAA4B,GAAG,IAAK;AAAA,MAC1F,MAAM,WAAW,UAChB;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,MAAM;AAAA,UACb,QAAQ,oBAAC,UAAO,MAAK,UAAS,MAAK,MAAK,SAAQ,WAAU,SAAS,MAAM,KAAK,OAAO,GAAI,YAAE,yBAAyB,GAAE;AAAA;AAAA,MACxH,IACE;AAAA,MACH,MAAM,WAAW,WAAW,SAAS,WAAW,IAAI,oBAAC,cAAW,MAAK,MAAK,SAAQ,UAAS,OAAO,EAAE,0BAA0B,GAAG,MAAM,oBAAC,WAAQ,WAAU,UAAS,GAAI,IAAK;AAAA,MAC5K,SAAS,IAAI,CAAC,YACb,qBAAC,aAAyB,WAAU,+DAClC;AAAA,4BAAC,OAAE,WAAU,uBAAuB,kBAAQ,SAAS,EAAE,0CAA0C,GAAE;AAAA,QACnG,qBAAC,OAAE,WAAU,yDAAwD;AAAA,8BAAC,SAAM,WAAU,UAAS,eAAY,QAAO;AAAA,UAAG,IAAI,KAAK,QAAQ,SAAS,EAAE,eAAe;AAAA,WAAE;AAAA,QAClK,oBAAC,OAAE,WAAU,iCAAiC,kBAAQ,gBAAgB,EAAE,yBAAyB,GAAE;AAAA,QACnG,qBAAC,UAAO,MAAK,UAAS,MAAK,MAAK,SAAQ,WAAU,SAAS,MAAM,oBAAoB,QAAQ,EAAE,GAAG;AAAA,8BAAC,OAAI;AAAA,UAAG,EAAE,oCAAoC;AAAA,WAAE;AAAA,WAJtI,QAAQ,EAKtB,CACD;AAAA,OACH;AAAA,IACC,mBAAmB,oBAAC,wBAAqB,YAAwB,WAAW,kBAAkB,YAAY,YAAY,aAAa,uBAAuB,MAAM,cAAc,CAAC,SAAS;AAAE,UAAI,CAAC,KAAM,qBAAoB,IAAI;AAAA,IAAE,GAAG,WAAW,eAAe,IAAK;AAAA,KACpQ;AAEJ;AAEA,IAAO,8BAAQ;",
|
|
6
6
|
"names": ["resolveVersionRestoreCapability", "normalizeVersion"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/documents/backend/documents/%5Bid%5D/editorTypes.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Editor } from '@tiptap/core'\nimport type { HocuspocusProvider } from '@hocuspocus/provider'\nimport type * as Y from 'yjs'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport type { CommentAnchor } from './CommentAnchorNavigation'\n\nexport type CollabTokenUser = { id: string; name: string; color: string }\nexport type CollabResources = { ydoc: Y.Doc; provider: HocuspocusProvider; user: CollabTokenUser }\nexport type PresenceUser = { key: string; name: string; color: string }\nexport type ConnectionStatus = 'connecting' | 'connected' | 'reconnecting' | 'offline'\nexport type EditorSelectionRange = { from: number; to: number }\nexport type EditorWordCount = { words: number; characters: number }\nexport type EditorMode = 'edit' | 'preview'\n\nexport type DocumentEditorIslandProps = {\n documentId: string\n title: string\n initialContentHtml: string\n contentUpdatedAt?: string | null\n documentUpdatedAt?: string | null\n readOnly: boolean\n onEditorReady?: (editor: Editor | null) => void\n onComment?: (anchor: CommentAnchor) => void\n onContentConflict?: () => void\n onTitleChange?: (title: string, updatedAt: string | null) => void\n}\n\nexport type CollabState =\n | { mode: 'connecting' }\n | { mode: 'fallback'; readOnly: boolean }\n | { mode: 'collab'; resources: CollabResources; connectionStatus: ConnectionStatus; presenceUsers: PresenceUser[]; serverReadOnly: boolean }\n\nexport function resolveEditorMode(readOnly: boolean, serverReadOnly: boolean, requested: EditorMode): EditorMode {\n return readOnly || serverReadOnly ? 'preview' : requested\n}\n\nexport function canCreateSelectionComment(input: {\n permissionReadOnly: boolean\n mode: EditorMode\n transport: 'collab' | 'fallback'\n hasCommentHandler: boolean\n}): boolean {\n if (!input.hasCommentHandler || input.transport !== 'collab') return false\n const effectiveReadOnly = input.permissionReadOnly || input.mode === 'preview'\n return !effectiveReadOnly || input.permissionReadOnly\n}\n\nexport function readRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null\n}\n\nexport function readString(record: Record<string, unknown>, ...keys: string[]): string | null {\n for (const key of keys) {\n const value = record[key]\n if (typeof value === 'string' && value.trim()) return value\n }\n return null\n}\n\nexport function readNumber(record: Record<string, unknown>, ...keys: string[]): number | null {\n for (const key of keys) {\n const value = record[key]\n if (typeof value === 'number' && Number.isFinite(value)) return value\n }\n return null\n}\n\nexport const DOCUMENT_EDITOR_CONTENT_CLASS = cn(\n 'max-w-none text-foreground',\n '[&_.ProseMirror]:min-h-96 [&_.ProseMirror]:focus-visible:outline-none',\n '[&_.ProseMirror>*:first-child]:mt-0 [&_.ProseMirror>*:last-child]:mb-0',\n '[&_h1]:mb-4 [&_h1]:mt-8 [&_h1]:text-3xl [&_h1]:font-bold [&_h1]:leading-tight',\n '[&_h2]:mb-3 [&_h2]:mt-7 [&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:leading-tight',\n '[&_h3]:mb-2 [&_h3]:mt-6 [&_h3]:text-xl [&_h3]:font-semibold',\n '[&_p]:my-4 [&_p]:leading-7 [&_ul]:my-4 [&_ul]:list-disc [&_ul]:pl-6',\n '[&_ol]:my-4 [&_ol]:list-decimal [&_ol]:pl-6 [&_li]:my-1',\n '[&_blockquote]:my-6 [&_blockquote]:border-l-4 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:italic',\n '[&_pre]:my-6 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-sm',\n '[&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4',\n '[&_img]:my-6 [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-md',\n '[&_table]:my-6 [&_table]:w-full [&_table]:border-collapse',\n '[&_td]:border [&_td]:border-border [&_td]:px-3 [&_td]:py-2',\n '[&_th]:border [&_th]:border-border [&_th]:bg-muted [&_th]:px-3 [&_th]:py-2 [&_th]:text-left',\n)\n"],
|
|
5
|
-
"mappings": "AAGA,SAAS,UAAU;
|
|
4
|
+
"sourcesContent": ["import type { Editor } from '@tiptap/core'\nimport type { HocuspocusProvider } from '@hocuspocus/provider'\nimport type * as Y from 'yjs'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport type { CommentAnchor } from './CommentAnchorNavigation'\n\nexport type CollabTokenUser = { id: string; name: string; color: string }\nexport type CollabResources = { ydoc: Y.Doc; provider: HocuspocusProvider; user: CollabTokenUser }\nexport type PresenceUser = { key: string; name: string; color: string }\nexport type ConnectionStatus = 'connecting' | 'connected' | 'reconnecting' | 'offline'\nexport type EditorSelectionRange = { from: number; to: number }\nexport type EditorWordCount = { words: number; characters: number }\nexport type EditorMode = 'edit' | 'preview'\n\nexport type DocumentEditorIslandProps = {\n documentId: string\n title: string\n initialContentHtml: string\n contentUpdatedAt?: string | null\n documentUpdatedAt?: string | null\n readOnly: boolean\n onEditorReady?: (editor: Editor | null) => void\n onSaveReady?: (save: (() => Promise<boolean>) | null) => void\n onComment?: (anchor: CommentAnchor) => void\n onContentConflict?: () => void\n onTitleChange?: (title: string, updatedAt: string | null) => void\n}\n\nexport type CollabState =\n | { mode: 'connecting' }\n | { mode: 'fallback'; readOnly: boolean }\n | { mode: 'collab'; resources: CollabResources; connectionStatus: ConnectionStatus; presenceUsers: PresenceUser[]; serverReadOnly: boolean }\n\nexport function resolveEditorMode(readOnly: boolean, serverReadOnly: boolean, requested: EditorMode): EditorMode {\n return readOnly || serverReadOnly ? 'preview' : requested\n}\n\nexport function canCreateSelectionComment(input: {\n permissionReadOnly: boolean\n mode: EditorMode\n transport: 'collab' | 'fallback'\n hasCommentHandler: boolean\n}): boolean {\n if (!input.hasCommentHandler || input.transport !== 'collab') return false\n const effectiveReadOnly = input.permissionReadOnly || input.mode === 'preview'\n return !effectiveReadOnly || input.permissionReadOnly\n}\n\nexport function readRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null\n}\n\nexport function readString(record: Record<string, unknown>, ...keys: string[]): string | null {\n for (const key of keys) {\n const value = record[key]\n if (typeof value === 'string' && value.trim()) return value\n }\n return null\n}\n\nexport function readNumber(record: Record<string, unknown>, ...keys: string[]): number | null {\n for (const key of keys) {\n const value = record[key]\n if (typeof value === 'number' && Number.isFinite(value)) return value\n }\n return null\n}\n\nexport const DOCUMENT_EDITOR_CONTENT_CLASS = cn(\n 'max-w-none text-foreground',\n '[&_.ProseMirror]:min-h-96 [&_.ProseMirror]:focus-visible:outline-none',\n '[&_.ProseMirror>*:first-child]:mt-0 [&_.ProseMirror>*:last-child]:mb-0',\n '[&_h1]:mb-4 [&_h1]:mt-8 [&_h1]:text-3xl [&_h1]:font-bold [&_h1]:leading-tight',\n '[&_h2]:mb-3 [&_h2]:mt-7 [&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:leading-tight',\n '[&_h3]:mb-2 [&_h3]:mt-6 [&_h3]:text-xl [&_h3]:font-semibold',\n '[&_p]:my-4 [&_p]:leading-7 [&_ul]:my-4 [&_ul]:list-disc [&_ul]:pl-6',\n '[&_ol]:my-4 [&_ol]:list-decimal [&_ol]:pl-6 [&_li]:my-1',\n '[&_blockquote]:my-6 [&_blockquote]:border-l-4 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:italic',\n '[&_pre]:my-6 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-sm',\n '[&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4',\n '[&_img]:my-6 [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-md',\n '[&_table]:my-6 [&_table]:w-full [&_table]:border-collapse',\n '[&_td]:border [&_td]:border-border [&_td]:px-3 [&_td]:py-2',\n '[&_th]:border [&_th]:border-border [&_th]:bg-muted [&_th]:px-3 [&_th]:py-2 [&_th]:text-left',\n)\n"],
|
|
5
|
+
"mappings": "AAGA,SAAS,UAAU;AA8BZ,SAAS,kBAAkB,UAAmB,gBAAyB,WAAmC;AAC/G,SAAO,YAAY,iBAAiB,YAAY;AAClD;AAEO,SAAS,0BAA0B,OAK9B;AACV,MAAI,CAAC,MAAM,qBAAqB,MAAM,cAAc,SAAU,QAAO;AACrE,QAAM,oBAAoB,MAAM,sBAAsB,MAAM,SAAS;AACrE,SAAO,CAAC,qBAAqB,MAAM;AACrC;AAEO,SAAS,WAAW,OAAgD;AACzE,SAAO,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAAI,QAAmC;AAC1G;AAEO,SAAS,WAAW,WAAoC,MAA+B;AAC5F,aAAW,OAAO,MAAM;AACtB,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAG,QAAO;AAAA,EACxD;AACA,SAAO;AACT;AAEO,SAAS,WAAW,WAAoC,MAA+B;AAC5F,aAAW,OAAO,MAAM;AACtB,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,EAAG,QAAO;AAAA,EAClE;AACA,SAAO;AACT;AAEO,MAAM,gCAAgC;AAAA,EAC3C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { collabContentSaveResponseSchema } from "../../../data/validators.js";
|
|
2
|
+
import { COLLAB_CONTENT_SAVE_TIMEOUT_MS } from "../../../lib/collabPersistence.js";
|
|
3
|
+
function saveCollaborativeContent(resources, signal) {
|
|
4
|
+
const { provider } = resources;
|
|
5
|
+
if (signal.aborted || !provider.synced || provider.configuration.websocketProvider.status !== "connected") {
|
|
6
|
+
return Promise.resolve(false);
|
|
7
|
+
}
|
|
8
|
+
const requestId = crypto.randomUUID();
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const finish = (saved) => {
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
provider.off("stateless", onStateless);
|
|
13
|
+
provider.off("close", onFailure);
|
|
14
|
+
provider.off("disconnect", onFailure);
|
|
15
|
+
provider.off("destroy", onFailure);
|
|
16
|
+
signal.removeEventListener("abort", onFailure);
|
|
17
|
+
resolve(saved);
|
|
18
|
+
};
|
|
19
|
+
const onFailure = () => finish(false);
|
|
20
|
+
const onStateless = ({ payload }) => {
|
|
21
|
+
let value;
|
|
22
|
+
try {
|
|
23
|
+
value = JSON.parse(payload);
|
|
24
|
+
} catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const response = collabContentSaveResponseSchema.safeParse(value);
|
|
28
|
+
if (response.success && response.data.requestId === requestId) finish(response.data.saved);
|
|
29
|
+
};
|
|
30
|
+
const timer = setTimeout(onFailure, COLLAB_CONTENT_SAVE_TIMEOUT_MS);
|
|
31
|
+
provider.on("stateless", onStateless);
|
|
32
|
+
provider.on("close", onFailure);
|
|
33
|
+
provider.on("disconnect", onFailure);
|
|
34
|
+
provider.on("destroy", onFailure);
|
|
35
|
+
signal.addEventListener("abort", onFailure, { once: true });
|
|
36
|
+
try {
|
|
37
|
+
provider.sendStateless(JSON.stringify({ type: "documents.content.save", requestId }));
|
|
38
|
+
} catch {
|
|
39
|
+
finish(false);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
saveCollaborativeContent
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=saveCollaborativeContent.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/documents/backend/documents/%5Bid%5D/saveCollaborativeContent.ts"],
|
|
4
|
+
"sourcesContent": ["import type { CollabResources } from './editorTypes'\nimport { collabContentSaveResponseSchema } from '../../../data/validators'\nimport { COLLAB_CONTENT_SAVE_TIMEOUT_MS } from '../../../lib/collabPersistence'\n\nexport function saveCollaborativeContent(\n resources: CollabResources,\n signal: AbortSignal,\n): Promise<boolean> {\n const { provider } = resources\n if (signal.aborted || !provider.synced || provider.configuration.websocketProvider.status !== 'connected') {\n return Promise.resolve(false)\n }\n const requestId = crypto.randomUUID()\n return new Promise<boolean>((resolve) => {\n const finish = (saved: boolean) => {\n clearTimeout(timer)\n provider.off('stateless', onStateless)\n provider.off('close', onFailure)\n provider.off('disconnect', onFailure)\n provider.off('destroy', onFailure)\n signal.removeEventListener('abort', onFailure)\n resolve(saved)\n }\n const onFailure = () => finish(false)\n const onStateless = ({ payload }: { payload: string }) => {\n let value: unknown\n try { value = JSON.parse(payload) } catch { return }\n const response = collabContentSaveResponseSchema.safeParse(value)\n if (response.success && response.data.requestId === requestId) finish(response.data.saved)\n }\n const timer = setTimeout(onFailure, COLLAB_CONTENT_SAVE_TIMEOUT_MS)\n provider.on('stateless', onStateless)\n provider.on('close', onFailure)\n provider.on('disconnect', onFailure)\n provider.on('destroy', onFailure)\n signal.addEventListener('abort', onFailure, { once: true })\n try {\n provider.sendStateless(JSON.stringify({ type: 'documents.content.save', requestId }))\n } catch {\n finish(false)\n }\n })\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,uCAAuC;AAChD,SAAS,sCAAsC;AAExC,SAAS,yBACd,WACA,QACkB;AAClB,QAAM,EAAE,SAAS,IAAI;AACrB,MAAI,OAAO,WAAW,CAAC,SAAS,UAAU,SAAS,cAAc,kBAAkB,WAAW,aAAa;AACzG,WAAO,QAAQ,QAAQ,KAAK;AAAA,EAC9B;AACA,QAAM,YAAY,OAAO,WAAW;AACpC,SAAO,IAAI,QAAiB,CAAC,YAAY;AACvC,UAAM,SAAS,CAAC,UAAmB;AACjC,mBAAa,KAAK;AAClB,eAAS,IAAI,aAAa,WAAW;AACrC,eAAS,IAAI,SAAS,SAAS;AAC/B,eAAS,IAAI,cAAc,SAAS;AACpC,eAAS,IAAI,WAAW,SAAS;AACjC,aAAO,oBAAoB,SAAS,SAAS;AAC7C,cAAQ,KAAK;AAAA,IACf;AACA,UAAM,YAAY,MAAM,OAAO,KAAK;AACpC,UAAM,cAAc,CAAC,EAAE,QAAQ,MAA2B;AACxD,UAAI;AACJ,UAAI;AAAE,gBAAQ,KAAK,MAAM,OAAO;AAAA,MAAE,QAAQ;AAAE;AAAA,MAAO;AACnD,YAAM,WAAW,gCAAgC,UAAU,KAAK;AAChE,UAAI,SAAS,WAAW,SAAS,KAAK,cAAc,UAAW,QAAO,SAAS,KAAK,KAAK;AAAA,IAC3F;AACA,UAAM,QAAQ,WAAW,WAAW,8BAA8B;AAClE,aAAS,GAAG,aAAa,WAAW;AACpC,aAAS,GAAG,SAAS,SAAS;AAC9B,aAAS,GAAG,cAAc,SAAS;AACnC,aAAS,GAAG,WAAW,SAAS;AAChC,WAAO,iBAAiB,SAAS,WAAW,EAAE,MAAM,KAAK,CAAC;AAC1D,QAAI;AACF,eAAS,cAAc,KAAK,UAAU,EAAE,MAAM,0BAA0B,UAAU,CAAC,CAAC;AAAA,IACtF,QAAQ;AACN,aAAO,KAAK;AAAA,IACd;AAAA,EACF,CAAC;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|