@open-mercato/documents 0.10.1-develop.7925.1.ec40f75262 → 0.10.1-develop.7926.1.673c22ad09

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/dist/modules/documents/backend/documents/[id]/DocumentEditorSurface.js +12 -0
  2. package/dist/modules/documents/backend/documents/[id]/DocumentEditorSurface.js.map +2 -2
  3. package/dist/modules/documents/backend/documents/[id]/DocumentPageClient.js +15 -1
  4. package/dist/modules/documents/backend/documents/[id]/DocumentPageClient.js.map +2 -2
  5. package/dist/modules/documents/backend/documents/[id]/VersionHistoryPanel.js +20 -8
  6. package/dist/modules/documents/backend/documents/[id]/VersionHistoryPanel.js.map +2 -2
  7. package/dist/modules/documents/backend/documents/[id]/editorTypes.js.map +2 -2
  8. package/dist/modules/documents/backend/documents/[id]/saveCollaborativeContent.js +46 -0
  9. package/dist/modules/documents/backend/documents/[id]/saveCollaborativeContent.js.map +7 -0
  10. package/dist/modules/documents/data/validators.js +11 -0
  11. package/dist/modules/documents/data/validators.js.map +2 -2
  12. package/dist/modules/documents/lib/collabPersistence.js +13 -0
  13. package/dist/modules/documents/lib/collabPersistence.js.map +7 -0
  14. package/dist/server/documents-collab-server.js +74 -1
  15. package/dist/server/documents-collab-server.js.map +2 -2
  16. package/package.json +6 -6
  17. package/server/documents-collab-server.ts +79 -0
  18. package/src/modules/documents/backend/documents/[id]/DocumentEditorSurface.tsx +16 -0
  19. package/src/modules/documents/backend/documents/[id]/DocumentPageClient.tsx +14 -0
  20. package/src/modules/documents/backend/documents/[id]/VersionHistoryPanel.tsx +23 -8
  21. package/src/modules/documents/backend/documents/[id]/editorTypes.ts +1 -0
  22. package/src/modules/documents/backend/documents/[id]/saveCollaborativeContent.ts +43 -0
  23. package/src/modules/documents/data/validators.ts +12 -0
  24. 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": "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",
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: () => apiCallOrThrow(
112
- `/api/documents/${encodeURIComponent(documentId)}/versions`,
113
- { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ label: nextLabel }) },
114
- { errorMessage: t("documents.versions.error.save") }
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": "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",
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": "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",
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;AA6BZ,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;",
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
+ }