deepspace 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +21 -0
- package/dist/cli.js +889 -323
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +26 -12
- package/dist/index.js +100 -27
- package/dist/index.js.map +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +51 -15
- package/dist/server.js +212 -61
- package/dist/server.js.map +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +28 -25
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +69 -10
- package/dist/worker.js +251 -61
- package/dist/worker.js.map +1 -1
- package/features/admin-page/src/AdminPage.tsx +4 -4
- package/features/documentation/feature.json +2 -1
- package/features/documents/feature.json +3 -2
- package/features/documents/src/DocumentsEditorPage.tsx +47 -62
- package/features/documents/src/DocumentsListPage.tsx +59 -26
- package/features/documents/src/InviteDialog.tsx +49 -14
- package/features/documents/src/LibrarySidebar.tsx +92 -84
- package/features/documents/src/__tests__/documents-presence-access.test.ts +15 -2
- package/features/documents/src/documents-library-types.ts +6 -0
- package/features/documents/src/editor/useDocEditor.ts +1 -1
- package/features/documents/src/use-documents-presence-access.ts +1 -3
- package/features/tasks/src/TasksPage.tsx +6 -3
- package/package.json +5 -5
|
@@ -118,11 +118,11 @@ export default function AdminPage() {
|
|
|
118
118
|
interface UsersPanelProps {
|
|
119
119
|
users: Array<{
|
|
120
120
|
id: string
|
|
121
|
-
email
|
|
121
|
+
email?: string
|
|
122
122
|
name: string
|
|
123
123
|
imageUrl?: string
|
|
124
124
|
role: string
|
|
125
|
-
lastSeenAt
|
|
125
|
+
lastSeenAt?: string
|
|
126
126
|
}>
|
|
127
127
|
currentUserId?: string
|
|
128
128
|
onSetRole: (userId: string, role: string) => void
|
|
@@ -132,7 +132,7 @@ function UsersPanel({ users, currentUserId, onSetRole }: UsersPanelProps) {
|
|
|
132
132
|
// Sort users by last seen
|
|
133
133
|
const sortedUsers = useMemo(() => {
|
|
134
134
|
return [...users].sort((a, b) =>
|
|
135
|
-
new Date(b.lastSeenAt).getTime() - new Date(a.lastSeenAt).getTime()
|
|
135
|
+
new Date(b.lastSeenAt ?? '').getTime() - new Date(a.lastSeenAt ?? '').getTime()
|
|
136
136
|
)
|
|
137
137
|
}, [users])
|
|
138
138
|
|
|
@@ -163,7 +163,7 @@ function UsersPanel({ users, currentUserId, onSetRole }: UsersPanelProps) {
|
|
|
163
163
|
|
|
164
164
|
<div className="flex items-center gap-3">
|
|
165
165
|
<p className="text-xs text-muted-foreground">
|
|
166
|
-
Last seen: {new Date(user.lastSeenAt).toLocaleDateString()}
|
|
166
|
+
Last seen: {user.lastSeenAt ? new Date(user.lastSeenAt).toLocaleDateString() : '—'}
|
|
167
167
|
</p>
|
|
168
168
|
|
|
169
169
|
<div className="flex items-center gap-2">
|
|
@@ -59,7 +59,8 @@
|
|
|
59
59
|
"The feature is served at /docs on the app's existing hostname.",
|
|
60
60
|
"A hostname listed in documentation.json domains serves the same release at that hostname's root after you explicitly attach it to the app.",
|
|
61
61
|
"Documentation builds only when documentation.json exists; apps without this feature are unaffected.",
|
|
62
|
-
"MDX may import app components directly. documentation.tsx is the single optional shell customization point."
|
|
62
|
+
"MDX may import app components directly. documentation.tsx is the single optional shell customization point.",
|
|
63
|
+
"Agent discovery is available at /docs/llms.txt, /docs/llms-full.txt, /docs/skill.md, /docs/.well-known/mcp, and /docs/mcp."
|
|
63
64
|
],
|
|
64
65
|
"agentNotes": [
|
|
65
66
|
"Treat documentation.json, documentation/**/*.md, documentation/**/*.mdx, and optional documentation.tsx as one app-owned documentation feature.",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Documents",
|
|
4
4
|
"category": "data",
|
|
5
5
|
"description": "Real-time collaborative document editing with Tiptap + Yjs",
|
|
6
|
-
"details": "Document library with folders & favorites. Collaborative editor built on Tiptap (ProseMirror) with Y.XmlFragment via @tiptap/extension-collaboration and live remote cursors via @tiptap/extension-collaboration-caret — buttery typing, no innerHTML rebuilds, no contenteditable quirks. Invite-only documents with viewer/editor roles: owner shares via an in-app dialog that resolves an email to a DeepSpace user. RBAC is enforced by the documents schema and the starter template's built-in Yjs route.
|
|
6
|
+
"details": "Document library with folders & favorites. Collaborative editor built on Tiptap (ProseMirror) with Y.XmlFragment via @tiptap/extension-collaboration and live remote cursors via @tiptap/extension-collaboration-caret — buttery typing, no innerHTML rebuilds, no contenteditable quirks. Invite-only documents with viewer/editor roles: owner shares via an in-app dialog that resolves an email to a DeepSpace user. RBAC is enforced by the documents schema and the starter template's built-in Yjs route.",
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"src": "src/DocumentsListPage.tsx",
|
|
@@ -86,7 +86,8 @@
|
|
|
86
86
|
"@tiptap/extension-task-item": "^3.22.0"
|
|
87
87
|
},
|
|
88
88
|
"instructions": [
|
|
89
|
-
"Run `npm install` (or your package manager) to install the Tiptap dependencies that were just added to package.json — the editor imports @tiptap/react and friends, so the dev server will fail to resolve them otherwise."
|
|
89
|
+
"Run `npm install` (or your package manager) to install the Tiptap dependencies that were just added to package.json — the editor imports @tiptap/react and friends, so the dev server will fail to resolve them otherwise.",
|
|
90
|
+
"Before sharing a document by email, ask that person to sign in to the deployed app once. The privacy-preserving invite lookup uses the app-local users collection, not a global user directory."
|
|
90
91
|
],
|
|
91
92
|
"patterns": []
|
|
92
93
|
}
|
|
@@ -6,11 +6,6 @@
|
|
|
6
6
|
* applied as granular PM transactions (no `innerHTML` swap), the local
|
|
7
7
|
* caret is migrated through concurrent edits by PM's position mapping,
|
|
8
8
|
* and remote cursors are painted by `@tiptap/extension-collaboration-caret`.
|
|
9
|
-
*
|
|
10
|
-
* Legacy documents created with the old editor stored HTML in Y.Text('content').
|
|
11
|
-
* On first open by an editor/owner we migrate that into the new XmlFragment
|
|
12
|
-
* via `editor.commands.setContent(html)` and clear the legacy field. The
|
|
13
|
-
* migration is a single Yjs transaction so all peers converge.
|
|
14
9
|
*/
|
|
15
10
|
|
|
16
11
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
@@ -22,10 +17,14 @@ import { type Editor, useEditorState } from '@tiptap/react'
|
|
|
22
17
|
import { useDocEditor } from './editor/useDocEditor'
|
|
23
18
|
import { DocEditorSurface, PAGE_HEIGHT_PX, PAGE_WIDTH_PX } from './editor/DocEditorSurface'
|
|
24
19
|
import { DocumentsTiptapToolbar } from './editor/DocumentsTiptapToolbar'
|
|
25
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
DocumentsOutlinePanel,
|
|
22
|
+
DOCUMENT_OUTLINE_WIDTH_PX,
|
|
23
|
+
type OutlineEntry,
|
|
24
|
+
} from './DocumentsOutlinePanel'
|
|
26
25
|
import { DocumentsPresence } from './DocumentsPresence'
|
|
27
26
|
import { InviteDialog } from './InviteDialog'
|
|
28
|
-
import type
|
|
27
|
+
import { recordsReadyForMutation, type DocumentsDocumentFields } from './documents-library-types'
|
|
29
28
|
import {
|
|
30
29
|
useDocumentsEditorPresence,
|
|
31
30
|
useDocumentsPresenceAccess,
|
|
@@ -180,7 +179,10 @@ function AccessChangedOverlay({
|
|
|
180
179
|
<AlertTriangle className="h-4 w-4" strokeWidth={2} />
|
|
181
180
|
</span>
|
|
182
181
|
<div className="min-w-0 flex-1">
|
|
183
|
-
<h2
|
|
182
|
+
<h2
|
|
183
|
+
id="documents-access-change-title"
|
|
184
|
+
className="text-base font-semibold tracking-tight"
|
|
185
|
+
>
|
|
184
186
|
{title}
|
|
185
187
|
</h2>
|
|
186
188
|
<p className="mt-1.5 text-sm" style={{ color: 'var(--documents-el-muted)' }}>
|
|
@@ -284,22 +286,15 @@ export default function DocumentsEditorPage() {
|
|
|
284
286
|
[documents, docId],
|
|
285
287
|
)
|
|
286
288
|
|
|
287
|
-
const { put } = useMutations<DocumentsDocumentFields>('documents')
|
|
289
|
+
const { put, ready: documentsMutationsReady } = useMutations<DocumentsDocumentFields>('documents')
|
|
290
|
+
const documentWritesReady = recordsReadyForMutation(status, documentsMutationsReady)
|
|
288
291
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const {
|
|
296
|
-
doc,
|
|
297
|
-
text: legacyText,
|
|
298
|
-
synced,
|
|
299
|
-
canWrite,
|
|
300
|
-
writeAuthResolved,
|
|
301
|
-
awareness,
|
|
302
|
-
} = useYjsRoom(docId ?? 'noop', 'content')
|
|
292
|
+
// The text helper remains available to consumers of useYjsRoom; this editor
|
|
293
|
+
// binds Tiptap directly to Y.XmlFragment('default') on the returned document.
|
|
294
|
+
const { doc, synced, connected, canWrite, writeAuthResolved, awareness } = useYjsRoom(
|
|
295
|
+
docId ?? 'noop',
|
|
296
|
+
'content',
|
|
297
|
+
)
|
|
303
298
|
|
|
304
299
|
const presenceAccess = useDocumentsPresenceAccess({
|
|
305
300
|
docId,
|
|
@@ -388,37 +383,6 @@ export default function DocumentsEditorPage() {
|
|
|
388
383
|
placeholder: showReadOnlyDocUx ? 'View only' : 'Start typing — toolbar above for formatting…',
|
|
389
384
|
})
|
|
390
385
|
|
|
391
|
-
// Legacy migration: if doc.getXmlFragment('default') is empty but the
|
|
392
|
-
// legacy Y.Text('content') field has HTML, parse it once into Tiptap.
|
|
393
|
-
// Only the first writer to open does this; viewers never see partial state
|
|
394
|
-
// because both the wipe and the parse run inside a single Yjs transaction.
|
|
395
|
-
const migratedRef = useRef(false)
|
|
396
|
-
useEffect(() => {
|
|
397
|
-
if (migratedRef.current) return
|
|
398
|
-
if (!editor || !synced || !effectiveCanWrite) return
|
|
399
|
-
const fragment = doc.getXmlFragment('default')
|
|
400
|
-
if (fragment.length > 0) {
|
|
401
|
-
migratedRef.current = true
|
|
402
|
-
return
|
|
403
|
-
}
|
|
404
|
-
const yText = doc.getText('content')
|
|
405
|
-
const legacy = (legacyText ?? yText.toString()).trim()
|
|
406
|
-
if (!legacy) {
|
|
407
|
-
migratedRef.current = true
|
|
408
|
-
return
|
|
409
|
-
}
|
|
410
|
-
migratedRef.current = true
|
|
411
|
-
/**
|
|
412
|
-
* Tiptap parses the HTML against its schema, then writes the resulting
|
|
413
|
-
* nodes into the bound Y.XmlFragment. We then clear the legacy field
|
|
414
|
-
* inside the same transaction so peers see one atomic step.
|
|
415
|
-
*/
|
|
416
|
-
doc.transact(() => {
|
|
417
|
-
editor.commands.setContent(legacy, { emitUpdate: true })
|
|
418
|
-
if (yText.length > 0) yText.delete(0, yText.length)
|
|
419
|
-
})
|
|
420
|
-
}, [editor, synced, effectiveCanWrite, doc, legacyText])
|
|
421
|
-
|
|
422
386
|
const { participants: presenceParticipants, typingNames } = useDocumentsEditorPresence({
|
|
423
387
|
editor,
|
|
424
388
|
document: selectedDoc,
|
|
@@ -441,10 +405,10 @@ export default function DocumentsEditorPage() {
|
|
|
441
405
|
const docTitle = selectedDoc?.data.title?.trim() || 'Untitled Document'
|
|
442
406
|
const handleTitleSave = useCallback(
|
|
443
407
|
async (next: string) => {
|
|
444
|
-
if (!selectedDoc) return
|
|
408
|
+
if (!documentWritesReady || !selectedDoc) return
|
|
445
409
|
await put(selectedDoc.recordId, { ...selectedDoc.data, title: next }).catch(() => {})
|
|
446
410
|
},
|
|
447
|
-
[put, selectedDoc],
|
|
411
|
+
[documentWritesReady, put, selectedDoc],
|
|
448
412
|
)
|
|
449
413
|
|
|
450
414
|
/**
|
|
@@ -494,7 +458,11 @@ export default function DocumentsEditorPage() {
|
|
|
494
458
|
<ArrowLeft className="h-4 w-4" strokeWidth={2} />
|
|
495
459
|
</button>
|
|
496
460
|
|
|
497
|
-
<InlineTitle
|
|
461
|
+
<InlineTitle
|
|
462
|
+
title={docTitle}
|
|
463
|
+
canEdit={Boolean(isOwner && documentWritesReady)}
|
|
464
|
+
onSave={handleTitleSave}
|
|
465
|
+
/>
|
|
498
466
|
|
|
499
467
|
<DocumentsPresence participants={presenceParticipants} typingNames={typingNames} />
|
|
500
468
|
|
|
@@ -517,12 +485,15 @@ export default function DocumentsEditorPage() {
|
|
|
517
485
|
) : effectiveRole === 'editor' || effectiveRole === 'viewer' ? (
|
|
518
486
|
<span
|
|
519
487
|
className="inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium"
|
|
520
|
-
style={{
|
|
488
|
+
style={{
|
|
489
|
+
borderColor: 'var(--documents-el-line)',
|
|
490
|
+
color: 'var(--documents-el-muted)',
|
|
491
|
+
}}
|
|
521
492
|
>
|
|
522
493
|
{effectiveRole === 'editor' ? 'Shared editor' : 'Shared viewer'}
|
|
523
494
|
</span>
|
|
524
495
|
) : null}
|
|
525
|
-
{synced ? (
|
|
496
|
+
{connected && synced ? (
|
|
526
497
|
<span
|
|
527
498
|
className="flex items-center gap-1.5 text-xs tabular-nums"
|
|
528
499
|
style={{ color: 'var(--documents-el-muted)' }}
|
|
@@ -530,7 +501,7 @@ export default function DocumentsEditorPage() {
|
|
|
530
501
|
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden />
|
|
531
502
|
Synced
|
|
532
503
|
</span>
|
|
533
|
-
) : (
|
|
504
|
+
) : connected ? (
|
|
534
505
|
<span
|
|
535
506
|
className="flex items-center gap-1.5 text-xs tabular-nums"
|
|
536
507
|
style={{ color: 'var(--documents-el-muted)' }}
|
|
@@ -538,6 +509,14 @@ export default function DocumentsEditorPage() {
|
|
|
538
509
|
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-amber-500" aria-hidden />
|
|
539
510
|
Connecting…
|
|
540
511
|
</span>
|
|
512
|
+
) : (
|
|
513
|
+
<span
|
|
514
|
+
className="flex items-center gap-1.5 text-xs tabular-nums"
|
|
515
|
+
style={{ color: 'var(--documents-el-muted)' }}
|
|
516
|
+
>
|
|
517
|
+
<span className="h-1.5 w-1.5 rounded-full bg-slate-400" aria-hidden />
|
|
518
|
+
Offline
|
|
519
|
+
</span>
|
|
541
520
|
)}
|
|
542
521
|
{effectiveRole === 'owner' ? (
|
|
543
522
|
<Badge variant="success">Owner</Badge>
|
|
@@ -675,7 +654,10 @@ export function ErrorBoundary() {
|
|
|
675
654
|
>
|
|
676
655
|
<div
|
|
677
656
|
className="w-full max-w-md rounded-xl border p-6 shadow-md"
|
|
678
|
-
style={{
|
|
657
|
+
style={{
|
|
658
|
+
borderColor: 'var(--documents-el-line)',
|
|
659
|
+
backgroundColor: 'var(--documents-el-surface)',
|
|
660
|
+
}}
|
|
679
661
|
>
|
|
680
662
|
<div className="mb-3 flex items-center justify-center">
|
|
681
663
|
<span
|
|
@@ -690,7 +672,10 @@ export function ErrorBoundary() {
|
|
|
690
672
|
</span>
|
|
691
673
|
</div>
|
|
692
674
|
<h1 className="text-base font-semibold tracking-tight">This document needs to reload</h1>
|
|
693
|
-
<p
|
|
675
|
+
<p
|
|
676
|
+
className="mx-auto mt-1.5 max-w-xs text-sm"
|
|
677
|
+
style={{ color: 'var(--documents-el-muted)' }}
|
|
678
|
+
>
|
|
694
679
|
Your access to this document just changed. Refresh to load the latest version.
|
|
695
680
|
</p>
|
|
696
681
|
<p
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import { LibrarySidebar, readSidebarCollapsed, writeSidebarCollapsed } from './LibrarySidebar'
|
|
28
28
|
import {
|
|
29
29
|
parseDocumentsIdList,
|
|
30
|
+
recordsReadyForMutation,
|
|
30
31
|
type DocFolderFields,
|
|
31
32
|
type DocumentsDocumentFields,
|
|
32
33
|
type LibraryNavSelection,
|
|
@@ -81,7 +82,7 @@ export default function DocumentsListPage() {
|
|
|
81
82
|
const { user } = useUser()
|
|
82
83
|
const navigate = useNavigate()
|
|
83
84
|
const userRole = (user?.role ?? ROLES.VIEWER) as Role
|
|
84
|
-
const
|
|
85
|
+
const roleCanCreate = userRole === ROLES.MEMBER || userRole === ROLES.ADMIN
|
|
85
86
|
|
|
86
87
|
const [searchQuery, setSearchQuery] = useState('')
|
|
87
88
|
const [sortBy, setSortBy] = useState<SortOption>('lastEdited')
|
|
@@ -101,17 +102,31 @@ export default function DocumentsListPage() {
|
|
|
101
102
|
orderBy: 'createdAt',
|
|
102
103
|
orderDir: 'desc',
|
|
103
104
|
})
|
|
104
|
-
const {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
})
|
|
105
|
+
const {
|
|
106
|
+
createConfirmed,
|
|
107
|
+
remove,
|
|
108
|
+
put,
|
|
109
|
+
ready: documentsMutationsReady,
|
|
110
|
+
} = useMutations<DocumentsDocumentFields>('documents')
|
|
111
|
+
|
|
112
|
+
const { records: folderRecords, status: foldersStatus } = useQuery<DocFolderFields>(
|
|
113
|
+
'doc_folders',
|
|
114
|
+
{
|
|
115
|
+
orderBy: 'createdAt',
|
|
116
|
+
orderDir: 'asc',
|
|
117
|
+
},
|
|
118
|
+
)
|
|
110
119
|
const {
|
|
111
120
|
create: createFolder,
|
|
112
121
|
put: putFolder,
|
|
113
122
|
remove: removeFolder,
|
|
123
|
+
ready: folderMutationsReady,
|
|
114
124
|
} = useMutations<DocFolderFields>('doc_folders')
|
|
125
|
+
const documentWritesReady = recordsReadyForMutation(status, documentsMutationsReady)
|
|
126
|
+
const folderWritesReady = recordsReadyForMutation(foldersStatus, folderMutationsReady)
|
|
127
|
+
const canCreate = roleCanCreate && documentWritesReady
|
|
128
|
+
const canManageFolders = canCreate && folderWritesReady
|
|
129
|
+
const canMoveDocuments = canCreate && foldersStatus === 'ready'
|
|
115
130
|
|
|
116
131
|
const myFolders = useMemo(
|
|
117
132
|
() => (folderRecords ?? []).filter((f) => f.data.ownerId === user?.id),
|
|
@@ -218,7 +233,7 @@ export default function DocumentsListPage() {
|
|
|
218
233
|
|
|
219
234
|
const handleNewDocument = useCallback(async () => {
|
|
220
235
|
if (!user || !canCreate) return
|
|
221
|
-
const id = await
|
|
236
|
+
const id = await createConfirmed({
|
|
222
237
|
title: UNTITLED,
|
|
223
238
|
ownerId: user.id,
|
|
224
239
|
collaborators: '[]',
|
|
@@ -226,36 +241,42 @@ export default function DocumentsListPage() {
|
|
|
226
241
|
folderId: folderIdForNew,
|
|
227
242
|
})
|
|
228
243
|
navigate(`/documents/${id}`)
|
|
229
|
-
}, [canCreate,
|
|
244
|
+
}, [canCreate, createConfirmed, folderIdForNew, navigate, user])
|
|
230
245
|
|
|
231
246
|
const handleDelete = useCallback(
|
|
232
247
|
async (docId: string) => {
|
|
248
|
+
if (!canCreate) return
|
|
233
249
|
if (confirm('Delete this document?')) await remove(docId)
|
|
234
250
|
},
|
|
235
|
-
[remove],
|
|
251
|
+
[canCreate, remove],
|
|
236
252
|
)
|
|
237
253
|
|
|
238
254
|
const handleRenameCommit = useCallback(
|
|
239
255
|
async (docId: string) => {
|
|
256
|
+
if (!canCreate) {
|
|
257
|
+
setRenamingId(null)
|
|
258
|
+
return
|
|
259
|
+
}
|
|
240
260
|
const rec = documents.find((d) => d.recordId === docId)
|
|
241
261
|
const t = renameValue.trim()
|
|
242
262
|
setRenamingId(null)
|
|
243
263
|
if (!rec || !t || t === rec.data.title) return
|
|
244
264
|
await put(docId, { ...rec.data, title: t }).catch(() => {})
|
|
245
265
|
},
|
|
246
|
-
[documents, put, renameValue],
|
|
266
|
+
[canCreate, documents, put, renameValue],
|
|
247
267
|
)
|
|
248
268
|
|
|
249
269
|
const handleCreateFolder = useCallback(
|
|
250
270
|
async (name: string) => {
|
|
251
|
-
if (!user) return
|
|
271
|
+
if (!user || !canManageFolders) return
|
|
252
272
|
await createFolder({ name, ownerId: user.id })
|
|
253
273
|
},
|
|
254
|
-
[createFolder, user],
|
|
274
|
+
[canManageFolders, createFolder, user],
|
|
255
275
|
)
|
|
256
276
|
|
|
257
277
|
const handleDeleteFolder = useCallback(
|
|
258
278
|
async (folderId: string) => {
|
|
279
|
+
if (!canManageFolders) return
|
|
259
280
|
const owned = (documents ?? []).filter(
|
|
260
281
|
(d) => d.data.ownerId === user?.id && (d.data.folderId ?? '') === folderId,
|
|
261
282
|
)
|
|
@@ -266,7 +287,7 @@ export default function DocumentsListPage() {
|
|
|
266
287
|
nav.kind === 'folder' && nav.folderId === folderId ? { kind: 'all' } : nav,
|
|
267
288
|
)
|
|
268
289
|
},
|
|
269
|
-
[documents, put, removeFolder, user?.id],
|
|
290
|
+
[canManageFolders, documents, put, removeFolder, user?.id],
|
|
270
291
|
)
|
|
271
292
|
|
|
272
293
|
const commitRenameFolderRef = useRef<() => Promise<void>>(async () => {})
|
|
@@ -274,22 +295,23 @@ export default function DocumentsListPage() {
|
|
|
274
295
|
const id = folderRenamingId
|
|
275
296
|
const trimmed = folderRenameValue.trim()
|
|
276
297
|
setFolderRenamingId(null)
|
|
277
|
-
if (!id || !trimmed) return
|
|
298
|
+
if (!canManageFolders || !id || !trimmed) return
|
|
278
299
|
const folder = sortedFolders.find((f) => f.recordId === id)
|
|
279
300
|
if (!folder || trimmed === (folder.data.name ?? '').trim()) return
|
|
280
301
|
await putFolder(id, { ...folder.data, name: trimmed }).catch(() => {})
|
|
281
|
-
}, [folderRenameValue, folderRenamingId, putFolder, sortedFolders])
|
|
302
|
+
}, [canManageFolders, folderRenameValue, folderRenamingId, putFolder, sortedFolders])
|
|
282
303
|
|
|
283
304
|
commitRenameFolderRef.current = commitRenameFolder
|
|
284
305
|
|
|
285
306
|
const handleMoveDoc = useCallback(
|
|
286
307
|
async (docId: string, folderId: string) => {
|
|
308
|
+
if (!canMoveDocuments) return
|
|
287
309
|
const rec = documents.find((d) => d.recordId === docId)
|
|
288
310
|
if (!rec) return
|
|
289
311
|
await put(docId, { ...rec.data, folderId })
|
|
290
312
|
setMoveMenuId(null)
|
|
291
313
|
},
|
|
292
|
-
[documents, put],
|
|
314
|
+
[canMoveDocuments, documents, put],
|
|
293
315
|
)
|
|
294
316
|
|
|
295
317
|
const isLoading = status === 'loading'
|
|
@@ -316,6 +338,7 @@ export default function DocumentsListPage() {
|
|
|
316
338
|
selection={libraryNav}
|
|
317
339
|
onSelect={setLibraryNav}
|
|
318
340
|
folders={myFolders}
|
|
341
|
+
canManageFolders={canManageFolders}
|
|
319
342
|
collapsed={sidebarCollapsed}
|
|
320
343
|
onToggleCollapsed={toggleSidebar}
|
|
321
344
|
onCreateFolder={handleCreateFolder}
|
|
@@ -342,7 +365,10 @@ export default function DocumentsListPage() {
|
|
|
342
365
|
{greetingForTime()}, {displayFirstName}
|
|
343
366
|
</h1>
|
|
344
367
|
</div>
|
|
345
|
-
<p
|
|
368
|
+
<p
|
|
369
|
+
className="mb-8 text-[13px] font-medium"
|
|
370
|
+
style={{ color: 'var(--documents-el-muted)' }}
|
|
371
|
+
>
|
|
346
372
|
{ownedDocumentCount} {ownedDocumentCount === 1 ? 'document' : 'documents'} in your
|
|
347
373
|
documents.
|
|
348
374
|
</p>
|
|
@@ -571,7 +597,10 @@ export default function DocumentsListPage() {
|
|
|
571
597
|
aria-hidden
|
|
572
598
|
/>
|
|
573
599
|
</div>
|
|
574
|
-
<h3
|
|
600
|
+
<h3
|
|
601
|
+
className="mb-2 text-lg font-semibold"
|
|
602
|
+
style={{ color: 'var(--documents-el-text)' }}
|
|
603
|
+
>
|
|
575
604
|
{searchQuery ? 'No matches' : 'No documents yet'}
|
|
576
605
|
</h3>
|
|
577
606
|
<p className="mb-6 max-w-sm text-sm" style={{ color: 'var(--documents-el-muted)' }}>
|
|
@@ -591,7 +620,8 @@ export default function DocumentsListPage() {
|
|
|
591
620
|
className="group w-full cursor-pointer rounded-xl border-2 border-dashed p-4 text-left shadow-sm transition-all hover:shadow-md"
|
|
592
621
|
style={{
|
|
593
622
|
borderColor: 'var(--documents-el-line)',
|
|
594
|
-
backgroundColor:
|
|
623
|
+
backgroundColor:
|
|
624
|
+
'color-mix(in srgb, var(--documents-el-surface) 20%, transparent)',
|
|
595
625
|
}}
|
|
596
626
|
>
|
|
597
627
|
<div
|
|
@@ -668,6 +698,7 @@ export default function DocumentsListPage() {
|
|
|
668
698
|
<div className="min-w-0 flex-1">
|
|
669
699
|
{renamingId === doc.recordId ? (
|
|
670
700
|
<input
|
|
701
|
+
disabled={!canCreate}
|
|
671
702
|
value={renameValue}
|
|
672
703
|
onChange={(e) => setRenameValue(e.target.value)}
|
|
673
704
|
onBlur={() => void handleRenameCommit(doc.recordId)}
|
|
@@ -676,7 +707,7 @@ export default function DocumentsListPage() {
|
|
|
676
707
|
if (e.key === 'Escape') setRenamingId(null)
|
|
677
708
|
}}
|
|
678
709
|
onClick={(e) => e.stopPropagation()}
|
|
679
|
-
className="mb-0.5 w-full rounded-md border px-2 py-1 text-[13px] font-semibold outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
710
|
+
className="mb-0.5 w-full rounded-md border px-2 py-1 text-[13px] font-semibold outline-none focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50"
|
|
680
711
|
style={{
|
|
681
712
|
borderColor: 'var(--documents-el-line)',
|
|
682
713
|
backgroundColor: 'var(--documents-el-bg)',
|
|
@@ -724,6 +755,7 @@ export default function DocumentsListPage() {
|
|
|
724
755
|
<button
|
|
725
756
|
type="button"
|
|
726
757
|
title="More"
|
|
758
|
+
disabled={!canCreate}
|
|
727
759
|
onClick={(e) => {
|
|
728
760
|
e.stopPropagation()
|
|
729
761
|
setActionsMenuId((id) =>
|
|
@@ -731,12 +763,12 @@ export default function DocumentsListPage() {
|
|
|
731
763
|
)
|
|
732
764
|
setMoveMenuId(null)
|
|
733
765
|
}}
|
|
734
|
-
className="rounded-lg p-1.5"
|
|
766
|
+
className="rounded-lg p-1.5 disabled:cursor-not-allowed disabled:opacity-40"
|
|
735
767
|
style={{ color: 'var(--documents-el-muted)' }}
|
|
736
768
|
>
|
|
737
769
|
<MoreVertical className="h-4 w-4" />
|
|
738
770
|
</button>
|
|
739
|
-
{actionsMenuId === doc.recordId ? (
|
|
771
|
+
{canCreate && actionsMenuId === doc.recordId ? (
|
|
740
772
|
<>
|
|
741
773
|
<button
|
|
742
774
|
type="button"
|
|
@@ -786,7 +818,7 @@ export default function DocumentsListPage() {
|
|
|
786
818
|
<button
|
|
787
819
|
type="button"
|
|
788
820
|
className="block w-full rounded px-2 py-1 text-left text-xs hover:bg-black/[0.04]"
|
|
789
|
-
disabled={fid === ''}
|
|
821
|
+
disabled={!canMoveDocuments || fid === ''}
|
|
790
822
|
onClick={() => void handleMoveDoc(doc.recordId, '')}
|
|
791
823
|
>
|
|
792
824
|
Uncategorized
|
|
@@ -795,7 +827,7 @@ export default function DocumentsListPage() {
|
|
|
795
827
|
<button
|
|
796
828
|
key={f.recordId}
|
|
797
829
|
type="button"
|
|
798
|
-
disabled={fid === f.recordId}
|
|
830
|
+
disabled={!canMoveDocuments || fid === f.recordId}
|
|
799
831
|
className="block w-full rounded px-2 py-1 text-left text-xs hover:bg-black/[0.04] disabled:opacity-40"
|
|
800
832
|
onClick={() =>
|
|
801
833
|
void handleMoveDoc(doc.recordId, f.recordId)
|
|
@@ -931,11 +963,12 @@ export default function DocumentsListPage() {
|
|
|
931
963
|
</button>
|
|
932
964
|
<button
|
|
933
965
|
type="button"
|
|
966
|
+
disabled={!canCreate}
|
|
934
967
|
onClick={(e) => {
|
|
935
968
|
e.stopPropagation()
|
|
936
969
|
void handleDelete(doc.recordId)
|
|
937
970
|
}}
|
|
938
|
-
className="rounded-lg p-1.5 opacity-0 transition-opacity group-hover:opacity-100"
|
|
971
|
+
className="rounded-lg p-1.5 opacity-0 transition-opacity group-hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-0"
|
|
939
972
|
style={{ color: 'var(--documents-el-muted)' }}
|
|
940
973
|
>
|
|
941
974
|
<Trash2 className="h-4 w-4" />
|