deepspace 0.19.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 +23 -0
- package/dist/cli.js +492 -259
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +11 -6
- package/dist/server.js +63 -19
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +8 -1
- package/dist/worker.js +63 -19
- package/dist/worker.js.map +1 -1
- package/features/admin-page/src/AdminPage.tsx +4 -4
- package/features/documents/feature.json +2 -1
- package/features/documents/src/DocumentsEditorPage.tsx +31 -10
- 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/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">
|
|
@@ -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
|
}
|
|
@@ -17,10 +17,14 @@ import { type Editor, useEditorState } from '@tiptap/react'
|
|
|
17
17
|
import { useDocEditor } from './editor/useDocEditor'
|
|
18
18
|
import { DocEditorSurface, PAGE_HEIGHT_PX, PAGE_WIDTH_PX } from './editor/DocEditorSurface'
|
|
19
19
|
import { DocumentsTiptapToolbar } from './editor/DocumentsTiptapToolbar'
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
DocumentsOutlinePanel,
|
|
22
|
+
DOCUMENT_OUTLINE_WIDTH_PX,
|
|
23
|
+
type OutlineEntry,
|
|
24
|
+
} from './DocumentsOutlinePanel'
|
|
21
25
|
import { DocumentsPresence } from './DocumentsPresence'
|
|
22
26
|
import { InviteDialog } from './InviteDialog'
|
|
23
|
-
import type
|
|
27
|
+
import { recordsReadyForMutation, type DocumentsDocumentFields } from './documents-library-types'
|
|
24
28
|
import {
|
|
25
29
|
useDocumentsEditorPresence,
|
|
26
30
|
useDocumentsPresenceAccess,
|
|
@@ -175,7 +179,10 @@ function AccessChangedOverlay({
|
|
|
175
179
|
<AlertTriangle className="h-4 w-4" strokeWidth={2} />
|
|
176
180
|
</span>
|
|
177
181
|
<div className="min-w-0 flex-1">
|
|
178
|
-
<h2
|
|
182
|
+
<h2
|
|
183
|
+
id="documents-access-change-title"
|
|
184
|
+
className="text-base font-semibold tracking-tight"
|
|
185
|
+
>
|
|
179
186
|
{title}
|
|
180
187
|
</h2>
|
|
181
188
|
<p className="mt-1.5 text-sm" style={{ color: 'var(--documents-el-muted)' }}>
|
|
@@ -279,7 +286,8 @@ export default function DocumentsEditorPage() {
|
|
|
279
286
|
[documents, docId],
|
|
280
287
|
)
|
|
281
288
|
|
|
282
|
-
const { put } = useMutations<DocumentsDocumentFields>('documents')
|
|
289
|
+
const { put, ready: documentsMutationsReady } = useMutations<DocumentsDocumentFields>('documents')
|
|
290
|
+
const documentWritesReady = recordsReadyForMutation(status, documentsMutationsReady)
|
|
283
291
|
|
|
284
292
|
// The text helper remains available to consumers of useYjsRoom; this editor
|
|
285
293
|
// binds Tiptap directly to Y.XmlFragment('default') on the returned document.
|
|
@@ -397,10 +405,10 @@ export default function DocumentsEditorPage() {
|
|
|
397
405
|
const docTitle = selectedDoc?.data.title?.trim() || 'Untitled Document'
|
|
398
406
|
const handleTitleSave = useCallback(
|
|
399
407
|
async (next: string) => {
|
|
400
|
-
if (!selectedDoc) return
|
|
408
|
+
if (!documentWritesReady || !selectedDoc) return
|
|
401
409
|
await put(selectedDoc.recordId, { ...selectedDoc.data, title: next }).catch(() => {})
|
|
402
410
|
},
|
|
403
|
-
[put, selectedDoc],
|
|
411
|
+
[documentWritesReady, put, selectedDoc],
|
|
404
412
|
)
|
|
405
413
|
|
|
406
414
|
/**
|
|
@@ -450,7 +458,11 @@ export default function DocumentsEditorPage() {
|
|
|
450
458
|
<ArrowLeft className="h-4 w-4" strokeWidth={2} />
|
|
451
459
|
</button>
|
|
452
460
|
|
|
453
|
-
<InlineTitle
|
|
461
|
+
<InlineTitle
|
|
462
|
+
title={docTitle}
|
|
463
|
+
canEdit={Boolean(isOwner && documentWritesReady)}
|
|
464
|
+
onSave={handleTitleSave}
|
|
465
|
+
/>
|
|
454
466
|
|
|
455
467
|
<DocumentsPresence participants={presenceParticipants} typingNames={typingNames} />
|
|
456
468
|
|
|
@@ -473,7 +485,10 @@ export default function DocumentsEditorPage() {
|
|
|
473
485
|
) : effectiveRole === 'editor' || effectiveRole === 'viewer' ? (
|
|
474
486
|
<span
|
|
475
487
|
className="inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium"
|
|
476
|
-
style={{
|
|
488
|
+
style={{
|
|
489
|
+
borderColor: 'var(--documents-el-line)',
|
|
490
|
+
color: 'var(--documents-el-muted)',
|
|
491
|
+
}}
|
|
477
492
|
>
|
|
478
493
|
{effectiveRole === 'editor' ? 'Shared editor' : 'Shared viewer'}
|
|
479
494
|
</span>
|
|
@@ -639,7 +654,10 @@ export function ErrorBoundary() {
|
|
|
639
654
|
>
|
|
640
655
|
<div
|
|
641
656
|
className="w-full max-w-md rounded-xl border p-6 shadow-md"
|
|
642
|
-
style={{
|
|
657
|
+
style={{
|
|
658
|
+
borderColor: 'var(--documents-el-line)',
|
|
659
|
+
backgroundColor: 'var(--documents-el-surface)',
|
|
660
|
+
}}
|
|
643
661
|
>
|
|
644
662
|
<div className="mb-3 flex items-center justify-center">
|
|
645
663
|
<span
|
|
@@ -654,7 +672,10 @@ export function ErrorBoundary() {
|
|
|
654
672
|
</span>
|
|
655
673
|
</div>
|
|
656
674
|
<h1 className="text-base font-semibold tracking-tight">This document needs to reload</h1>
|
|
657
|
-
<p
|
|
675
|
+
<p
|
|
676
|
+
className="mx-auto mt-1.5 max-w-xs text-sm"
|
|
677
|
+
style={{ color: 'var(--documents-el-muted)' }}
|
|
678
|
+
>
|
|
658
679
|
Your access to this document just changed. Refresh to load the latest version.
|
|
659
680
|
</p>
|
|
660
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" />
|
|
@@ -3,7 +3,11 @@ import type { RecordData } from 'deepspace'
|
|
|
3
3
|
import { useMutations, useQuery, useUser } from 'deepspace'
|
|
4
4
|
import { Mail, ShieldCheck, UserMinus, Users } from 'lucide-react'
|
|
5
5
|
import { Modal, useToast } from '@/components/ui'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
parseDocumentsIdList,
|
|
8
|
+
type DocumentsDocumentFields,
|
|
9
|
+
type InviteAclDiff,
|
|
10
|
+
} from './documents-library-types'
|
|
7
11
|
|
|
8
12
|
interface UserFields {
|
|
9
13
|
email?: string
|
|
@@ -42,12 +46,14 @@ function initialsFor(name: string): string {
|
|
|
42
46
|
|
|
43
47
|
export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }: InviteDialogProps) {
|
|
44
48
|
const { user } = useUser()
|
|
45
|
-
const { records: users } = useQuery<UserFields>('users')
|
|
46
|
-
const {
|
|
49
|
+
const { records: users, status: usersStatus } = useQuery<UserFields>('users')
|
|
50
|
+
const { putConfirmed, ready: documentsMutationsReady } =
|
|
51
|
+
useMutations<DocumentsDocumentFields>('documents')
|
|
47
52
|
const toast = useToast()
|
|
48
53
|
const [email, setEmail] = useState('')
|
|
49
54
|
const [role, setRole] = useState<InviteRole>('editor')
|
|
50
55
|
const [saving, setSaving] = useState(false)
|
|
56
|
+
const ready = usersStatus === 'ready' && documentsMutationsReady
|
|
51
57
|
|
|
52
58
|
const collaborators = useMemo(
|
|
53
59
|
() => parseDocumentsIdList(doc.data.collaborators),
|
|
@@ -70,7 +76,11 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
70
76
|
|
|
71
77
|
if (!isOwner) return null
|
|
72
78
|
|
|
73
|
-
const saveAccess = async (
|
|
79
|
+
const saveAccess = async (
|
|
80
|
+
nextCollaborators: string[],
|
|
81
|
+
nextEditors: string[],
|
|
82
|
+
): Promise<boolean> => {
|
|
83
|
+
if (!ready) return false
|
|
74
84
|
const prevCollaborators = parseDocumentsIdList(doc.data.collaborators)
|
|
75
85
|
const prevEditors = parseDocumentsIdList(doc.data.editors)
|
|
76
86
|
const nextCollabList = uniqueIds(nextCollaborators)
|
|
@@ -80,7 +90,7 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
80
90
|
|
|
81
91
|
setSaving(true)
|
|
82
92
|
try {
|
|
83
|
-
await
|
|
93
|
+
await putConfirmed(doc.recordId, {
|
|
84
94
|
...doc.data,
|
|
85
95
|
collaborators: JSON.stringify(nextCollabList),
|
|
86
96
|
editors: JSON.stringify(nextEditorList),
|
|
@@ -98,6 +108,13 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
98
108
|
onAclChange({ removedUserIds, demotedUserIds, promotedUserIds })
|
|
99
109
|
}
|
|
100
110
|
}
|
|
111
|
+
return true
|
|
112
|
+
} catch {
|
|
113
|
+
toast.error(
|
|
114
|
+
'Access not saved',
|
|
115
|
+
'DeepSpace did not confirm the change. Reconnect and try again.',
|
|
116
|
+
)
|
|
117
|
+
return false
|
|
101
118
|
} finally {
|
|
102
119
|
setSaving(false)
|
|
103
120
|
}
|
|
@@ -109,7 +126,10 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
109
126
|
|
|
110
127
|
const target = users.find((u) => u.data.email?.trim().toLowerCase() === normalized)
|
|
111
128
|
if (!target) {
|
|
112
|
-
toast.error(
|
|
129
|
+
toast.error(
|
|
130
|
+
'User not found',
|
|
131
|
+
'Ask them to sign in to this app once, then invite the same email again.',
|
|
132
|
+
)
|
|
113
133
|
return
|
|
114
134
|
}
|
|
115
135
|
if (target.recordId === doc.data.ownerId || target.recordId === user?.id) {
|
|
@@ -123,7 +143,7 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
123
143
|
|
|
124
144
|
const nextCollaborators = [...collaborators, target.recordId]
|
|
125
145
|
const nextEditors = role === 'editor' ? [...editors, target.recordId] : editors
|
|
126
|
-
await saveAccess(nextCollaborators, nextEditors)
|
|
146
|
+
if (!(await saveAccess(nextCollaborators, nextEditors))) return
|
|
127
147
|
setEmail('')
|
|
128
148
|
toast.success('Invite added', `${target.data.email ?? normalized} now has ${role} access.`)
|
|
129
149
|
}
|
|
@@ -151,7 +171,12 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
151
171
|
'Owner'
|
|
152
172
|
|
|
153
173
|
return (
|
|
154
|
-
<Modal
|
|
174
|
+
<Modal
|
|
175
|
+
open={open}
|
|
176
|
+
onClose={() => onOpenChange(false)}
|
|
177
|
+
size="lg"
|
|
178
|
+
className="documents-feature-scope"
|
|
179
|
+
>
|
|
155
180
|
<Modal.Header>
|
|
156
181
|
<Modal.Title>Share document</Modal.Title>
|
|
157
182
|
<Modal.Description>
|
|
@@ -174,6 +199,7 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
174
199
|
style={{ color: 'var(--documents-el-muted)' }}
|
|
175
200
|
/>
|
|
176
201
|
<input
|
|
202
|
+
disabled={!ready || saving}
|
|
177
203
|
value={email}
|
|
178
204
|
onChange={(e) => setEmail(e.target.value)}
|
|
179
205
|
onKeyDown={(e) => {
|
|
@@ -181,7 +207,10 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
181
207
|
}}
|
|
182
208
|
placeholder="person@gmail.com"
|
|
183
209
|
className="h-10 w-full rounded-lg border bg-transparent pl-9 pr-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
184
|
-
style={{
|
|
210
|
+
style={{
|
|
211
|
+
borderColor: 'var(--documents-el-line)',
|
|
212
|
+
color: 'var(--documents-el-text)',
|
|
213
|
+
}}
|
|
185
214
|
/>
|
|
186
215
|
</div>
|
|
187
216
|
<select
|
|
@@ -196,7 +225,7 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
196
225
|
<button
|
|
197
226
|
type="button"
|
|
198
227
|
onClick={() => void addInvite()}
|
|
199
|
-
disabled={saving || !email.trim()}
|
|
228
|
+
disabled={!ready || saving || !email.trim()}
|
|
200
229
|
className="h-10 rounded-lg px-4 text-sm font-semibold text-white transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
|
|
201
230
|
style={{ backgroundColor: 'var(--documents-el-accent)' }}
|
|
202
231
|
>
|
|
@@ -253,7 +282,10 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
253
282
|
)}
|
|
254
283
|
<div className="min-w-0 flex-1">
|
|
255
284
|
<div className="truncate text-sm font-medium">{name}</div>
|
|
256
|
-
<div
|
|
285
|
+
<div
|
|
286
|
+
className="truncate text-xs"
|
|
287
|
+
style={{ color: 'var(--documents-el-muted)' }}
|
|
288
|
+
>
|
|
257
289
|
{u.data.email ?? 'No email'}
|
|
258
290
|
</div>
|
|
259
291
|
</div>
|
|
@@ -262,9 +294,12 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
262
294
|
onChange={(e) =>
|
|
263
295
|
void setCollaboratorRole(u.recordId, e.target.value as InviteRole)
|
|
264
296
|
}
|
|
265
|
-
disabled={saving}
|
|
297
|
+
disabled={!ready || saving}
|
|
266
298
|
className="h-8 rounded-lg border bg-transparent px-2 text-xs font-medium outline-none"
|
|
267
|
-
style={{
|
|
299
|
+
style={{
|
|
300
|
+
borderColor: 'var(--documents-el-line)',
|
|
301
|
+
color: 'var(--documents-el-text)',
|
|
302
|
+
}}
|
|
268
303
|
>
|
|
269
304
|
<option value="editor">Editor</option>
|
|
270
305
|
<option value="viewer">Viewer</option>
|
|
@@ -272,7 +307,7 @@ export function InviteDialog({ open, onOpenChange, doc, isOwner, onAclChange }:
|
|
|
272
307
|
<button
|
|
273
308
|
type="button"
|
|
274
309
|
onClick={() => void removeCollaborator(u.recordId)}
|
|
275
|
-
disabled={saving}
|
|
310
|
+
disabled={!ready || saving}
|
|
276
311
|
className="rounded-lg p-1.5 text-red-600 transition-colors hover:bg-red-500/10 disabled:opacity-50"
|
|
277
312
|
title="Remove access"
|
|
278
313
|
>
|