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
|
@@ -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
|
>
|
|
@@ -182,6 +182,7 @@ export interface LibrarySidebarProps {
|
|
|
182
182
|
selection: LibraryNavSelection
|
|
183
183
|
onSelect: (s: LibraryNavSelection) => void
|
|
184
184
|
folders: RecordData<DocFolderFields>[]
|
|
185
|
+
canManageFolders: boolean
|
|
185
186
|
collapsed: boolean
|
|
186
187
|
onToggleCollapsed: () => void
|
|
187
188
|
onCreateFolder: (name: string) => void | Promise<void>
|
|
@@ -198,6 +199,7 @@ export function LibrarySidebar({
|
|
|
198
199
|
selection,
|
|
199
200
|
onSelect,
|
|
200
201
|
folders,
|
|
202
|
+
canManageFolders,
|
|
201
203
|
collapsed,
|
|
202
204
|
onToggleCollapsed,
|
|
203
205
|
onCreateFolder,
|
|
@@ -337,7 +339,7 @@ export function LibrarySidebar({
|
|
|
337
339
|
</span>
|
|
338
340
|
<div className="space-y-0.5">
|
|
339
341
|
{sortedFolders.map((f) =>
|
|
340
|
-
renamingFolderId === f.recordId ? (
|
|
342
|
+
canManageFolders && renamingFolderId === f.recordId ? (
|
|
341
343
|
<div
|
|
342
344
|
key={f.recordId}
|
|
343
345
|
className="group flex w-full min-w-0 items-center gap-0.5 rounded-md bg-[color:var(--documents-el-accent)]/10 py-0.5 pl-0.5 dark:bg-[color:var(--documents-el-accent)]/15"
|
|
@@ -369,96 +371,102 @@ export function LibrarySidebar({
|
|
|
369
371
|
onClick={() => onSelect({ kind: 'folder', folderId: f.recordId })}
|
|
370
372
|
collapsed={collapsed}
|
|
371
373
|
trailing={
|
|
372
|
-
|
|
373
|
-
<
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
e
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
e
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
e
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
374
|
+
canManageFolders ? (
|
|
375
|
+
<span className="relative z-[1] flex shrink-0 items-center gap-0.5">
|
|
376
|
+
<button
|
|
377
|
+
type="button"
|
|
378
|
+
data-testid={`rename-folder-${f.recordId}`}
|
|
379
|
+
onMouseDown={(e) => {
|
|
380
|
+
e.preventDefault()
|
|
381
|
+
e.stopPropagation()
|
|
382
|
+
}}
|
|
383
|
+
onClick={(e) => {
|
|
384
|
+
e.stopPropagation()
|
|
385
|
+
onStartRenameFolder(f)
|
|
386
|
+
}}
|
|
387
|
+
className="rounded p-1 text-[color:var(--documents-el-muted)] opacity-0 transition-all hover:bg-black/[0.04] hover:text-[color:var(--documents-el-accent)] group-hover:opacity-100 dark:hover:bg-white/[0.06]"
|
|
388
|
+
title="Rename folder"
|
|
389
|
+
aria-label={`Rename folder ${f.data.name}`}
|
|
390
|
+
>
|
|
391
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
392
|
+
</button>
|
|
393
|
+
<button
|
|
394
|
+
type="button"
|
|
395
|
+
data-testid={`delete-folder-${f.recordId}`}
|
|
396
|
+
onClick={(e) => {
|
|
397
|
+
e.stopPropagation()
|
|
398
|
+
if (
|
|
399
|
+
confirm(
|
|
400
|
+
`Delete folder “${f.data.name}”? Documents inside will move to uncategorized.`,
|
|
401
|
+
)
|
|
402
|
+
) {
|
|
403
|
+
void onDeleteFolder(f.recordId)
|
|
404
|
+
}
|
|
405
|
+
}}
|
|
406
|
+
className="rounded p-1 text-[color:var(--documents-el-muted)] opacity-0 transition-all hover:bg-red-500/10 hover:text-red-600 group-hover:opacity-100"
|
|
407
|
+
title="Delete folder"
|
|
408
|
+
aria-label={`Delete folder ${f.data.name}`}
|
|
409
|
+
>
|
|
410
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
411
|
+
</button>
|
|
412
|
+
</span>
|
|
413
|
+
) : undefined
|
|
410
414
|
}
|
|
411
415
|
/>
|
|
412
416
|
),
|
|
413
417
|
)}
|
|
414
418
|
|
|
415
|
-
{
|
|
416
|
-
|
|
417
|
-
<
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
419
|
+
{canManageFolders &&
|
|
420
|
+
(addingFolder ? (
|
|
421
|
+
<div className="py-1">
|
|
422
|
+
<input
|
|
423
|
+
autoFocus
|
|
424
|
+
value={newFolderName}
|
|
425
|
+
onChange={(e) => setNewFolderName(e.target.value)}
|
|
426
|
+
onBlur={() => void submitNewFolder()}
|
|
427
|
+
onKeyDown={(e) => {
|
|
428
|
+
if (e.key === 'Enter') void submitNewFolder()
|
|
429
|
+
if (e.key === 'Escape') {
|
|
430
|
+
setAddingFolder(false)
|
|
431
|
+
setNewFolderName('')
|
|
432
|
+
}
|
|
433
|
+
}}
|
|
434
|
+
placeholder="Folder name"
|
|
435
|
+
data-testid="new-folder-input"
|
|
436
|
+
className="w-full rounded-md border px-2 py-1.5 text-[13px] outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
437
|
+
style={{
|
|
438
|
+
borderColor: 'var(--documents-el-line)',
|
|
439
|
+
backgroundColor: 'var(--documents-el-bg)',
|
|
440
|
+
color: 'var(--documents-el-text)',
|
|
441
|
+
}}
|
|
442
|
+
/>
|
|
443
|
+
</div>
|
|
444
|
+
) : (
|
|
445
|
+
<button
|
|
446
|
+
type="button"
|
|
447
|
+
data-testid="new-folder-btn"
|
|
448
|
+
onClick={() => {
|
|
449
|
+
if (!collapsed) {
|
|
450
|
+
setAddingFolder(true)
|
|
451
|
+
return
|
|
427
452
|
}
|
|
453
|
+
const name = window.prompt('Folder name')?.trim()
|
|
454
|
+
if (name) void onCreateFolder(name)
|
|
428
455
|
}}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
onClick={() => {
|
|
444
|
-
if (!collapsed) {
|
|
445
|
-
setAddingFolder(true)
|
|
446
|
-
return
|
|
447
|
-
}
|
|
448
|
-
const name = window.prompt('Folder name')?.trim()
|
|
449
|
-
if (name) void onCreateFolder(name)
|
|
450
|
-
}}
|
|
451
|
-
title={collapsed ? 'New folder' : undefined}
|
|
452
|
-
className="flex h-9 w-full min-w-0 items-center gap-0 rounded-md pl-0.5 pr-0 text-left text-[13px] text-[color:var(--documents-el-muted)] transition-colors hover:bg-black/[0.04] hover:text-[color:var(--documents-el-accent)] dark:hover:bg-white/[0.06]"
|
|
453
|
-
>
|
|
454
|
-
<span className="flex h-9 w-10 shrink-0 items-center justify-center" aria-hidden>
|
|
455
|
-
<FolderPlus className="h-3.5 w-3.5 shrink-0" />
|
|
456
|
-
</span>
|
|
457
|
-
<span className={collapsed ? 'sr-only' : 'min-w-0 flex-1 truncate pr-2'}>
|
|
458
|
-
New folder
|
|
459
|
-
</span>
|
|
460
|
-
</button>
|
|
461
|
-
)}
|
|
456
|
+
title={collapsed ? 'New folder' : undefined}
|
|
457
|
+
className="flex h-9 w-full min-w-0 items-center gap-0 rounded-md pl-0.5 pr-0 text-left text-[13px] text-[color:var(--documents-el-muted)] transition-colors hover:bg-black/[0.04] hover:text-[color:var(--documents-el-accent)] dark:hover:bg-white/[0.06]"
|
|
458
|
+
>
|
|
459
|
+
<span
|
|
460
|
+
className="flex h-9 w-10 shrink-0 items-center justify-center"
|
|
461
|
+
aria-hidden
|
|
462
|
+
>
|
|
463
|
+
<FolderPlus className="h-3.5 w-3.5 shrink-0" />
|
|
464
|
+
</span>
|
|
465
|
+
<span className={collapsed ? 'sr-only' : 'min-w-0 flex-1 truncate pr-2'}>
|
|
466
|
+
New folder
|
|
467
|
+
</span>
|
|
468
|
+
</button>
|
|
469
|
+
))}
|
|
462
470
|
</div>
|
|
463
471
|
</div>
|
|
464
472
|
</nav>
|
|
@@ -5,6 +5,15 @@ import {
|
|
|
5
5
|
isDocumentsOwner,
|
|
6
6
|
resolveDocumentsAccessRole,
|
|
7
7
|
} from '../use-documents-presence-access'
|
|
8
|
+
import { recordsReadyForMutation } from '../documents-library-types'
|
|
9
|
+
|
|
10
|
+
describe('document mutation readiness', () => {
|
|
11
|
+
it('requires both a ready query and a ready mutation transport', () => {
|
|
12
|
+
expect(recordsReadyForMutation('loading', true)).toBe(false)
|
|
13
|
+
expect(recordsReadyForMutation('ready', false)).toBe(false)
|
|
14
|
+
expect(recordsReadyForMutation('ready', true)).toBe(true)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
8
17
|
|
|
9
18
|
describe('document access policy', () => {
|
|
10
19
|
const document = {
|
|
@@ -42,8 +51,12 @@ describe('document access policy', () => {
|
|
|
42
51
|
expect(deriveDocumentsAccessChange('viewer', 'editor', null)).toBe('upgrade')
|
|
43
52
|
expect(deriveDocumentsAccessChange('viewer', 'none', null)).toBe('revoked')
|
|
44
53
|
expect(deriveDocumentsAccessChange('owner', 'owner', null)).toBeNull()
|
|
45
|
-
expect(deriveDocumentsAccessChange(null, 'viewer', { kind: 'downgrade', at: 20 })).toBe(
|
|
46
|
-
|
|
54
|
+
expect(deriveDocumentsAccessChange(null, 'viewer', { kind: 'downgrade', at: 20 })).toBe(
|
|
55
|
+
'downgrade',
|
|
56
|
+
)
|
|
57
|
+
expect(deriveDocumentsAccessChange('editor', 'viewer', { kind: 'revoked', at: 20 })).toBe(
|
|
58
|
+
'revoked',
|
|
59
|
+
)
|
|
47
60
|
})
|
|
48
61
|
|
|
49
62
|
it('accepts only a fresh ACL signal addressed to the current user', () => {
|
|
@@ -29,6 +29,12 @@ export type LibraryNavSelection =
|
|
|
29
29
|
| { kind: 'uncategorized' }
|
|
30
30
|
| { kind: 'folder'; folderId: string }
|
|
31
31
|
|
|
32
|
+
/** Record controls are usable only after both the initial query and mutation
|
|
33
|
+
* transport are ready. Keep that boundary identical across feature pages. */
|
|
34
|
+
export function recordsReadyForMutation(queryStatus: string, mutationsReady: boolean): boolean {
|
|
35
|
+
return queryStatus === 'ready' && mutationsReady
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
export function parseDocumentsIdList(raw: string | undefined): string[] {
|
|
33
39
|
if (!raw) return []
|
|
34
40
|
try {
|
|
@@ -120,7 +120,7 @@ export function useDocEditor({
|
|
|
120
120
|
canWrite,
|
|
121
121
|
placeholder = 'Start typing — toolbar above for formatting…',
|
|
122
122
|
}: UseDocEditorOptions): Editor | null {
|
|
123
|
-
/** Structured CRDT field that ProseMirror binds to
|
|
123
|
+
/** Structured CRDT field that ProseMirror binds to. */
|
|
124
124
|
const fragment = useMemo(() => doc.getXmlFragment('default'), [doc])
|
|
125
125
|
|
|
126
126
|
/** CollaborationCaret expects a provider-shaped `{ awareness }` object. */
|
|
@@ -96,13 +96,11 @@ function peerToDocumentsParticipant(peer: PresencePeerClient): DocumentsPresence
|
|
|
96
96
|
const participant: DocumentsPresenceParticipant = {
|
|
97
97
|
clientId: 0,
|
|
98
98
|
userId: peer.userId,
|
|
99
|
-
name: peer.userName?.trim() ||
|
|
99
|
+
name: peer.userName?.trim() || 'Guest',
|
|
100
100
|
mode,
|
|
101
101
|
typing: state.typing === true,
|
|
102
102
|
isSelf: false,
|
|
103
103
|
}
|
|
104
|
-
if (peer.userEmail) participant.email = peer.userEmail
|
|
105
|
-
if (peer.userImageUrl) participant.imageUrl = peer.userImageUrl
|
|
106
104
|
if (lastTypedAt != null) participant.lastTypedAt = lastTypedAt
|
|
107
105
|
return participant
|
|
108
106
|
}
|
|
@@ -460,7 +460,10 @@ export default function TasksPage({ className }: TasksPageProps) {
|
|
|
460
460
|
const handleUnclaim = async (challenge: { recordId: string; data: Challenge }) => {
|
|
461
461
|
await put(challenge.recordId, {
|
|
462
462
|
...challenge.data,
|
|
463
|
-
|
|
463
|
+
// Empty strings are normalized to SQL NULL by the record writer. The
|
|
464
|
+
// read model omits null database fields, so this also keeps Challenge's
|
|
465
|
+
// optional-string shape honest.
|
|
466
|
+
claimedById: '',
|
|
464
467
|
})
|
|
465
468
|
}
|
|
466
469
|
|
|
@@ -469,8 +472,8 @@ export default function TasksPage({ className }: TasksPageProps) {
|
|
|
469
472
|
await put(submitChallenge.recordId, {
|
|
470
473
|
...submitChallenge.data,
|
|
471
474
|
submitted: true,
|
|
472
|
-
submissionUrl: url
|
|
473
|
-
submissionNotes: notes
|
|
475
|
+
submissionUrl: url,
|
|
476
|
+
submissionNotes: notes,
|
|
474
477
|
})
|
|
475
478
|
setSubmitChallenge(null)
|
|
476
479
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22.15.0 <23 || >=24 <25 || >=26 <27"
|
|
6
6
|
},
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
"prepublishOnly": "pnpm build"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@ai-sdk/anthropic": "
|
|
60
|
-
"@ai-sdk/openai": "
|
|
61
|
-
"@ai-sdk/openai-compatible": "
|
|
59
|
+
"@ai-sdk/anthropic": "2.0.91",
|
|
60
|
+
"@ai-sdk/openai": "2.0.116",
|
|
61
|
+
"@ai-sdk/openai-compatible": "1.0.46",
|
|
62
62
|
"@clack/prompts": "^1.2.0",
|
|
63
63
|
"@mdx-js/mdx": "^3.1.1",
|
|
64
64
|
"acorn": "^8.16.0",
|
|
65
|
-
"ai": "
|
|
65
|
+
"ai": "5.0.222",
|
|
66
66
|
"better-auth": "1.6.23",
|
|
67
67
|
"citty": "^0.2.0",
|
|
68
68
|
"cross-spawn": "^7.0.6",
|