@kernhq/module-quire 0.10.9 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contract/models.d.ts +87 -0
- package/dist/contract/models.d.ts.map +1 -1
- package/dist/contract/models.js +73 -0
- package/dist/contract/models.js.map +1 -1
- package/dist/contract/permissions.d.ts.map +1 -1
- package/dist/contract/permissions.js +32 -0
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/router.d.ts +645 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +137 -2
- package/dist/contract/router.js.map +1 -1
- package/dist/server/_impl.d.ts +877 -0
- package/dist/server/_impl.d.ts.map +1 -1
- package/dist/server/_impl.js +127 -1
- package/dist/server/_impl.js.map +1 -1
- package/dist/server/schema.d.ts +482 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +115 -1
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/index.d.ts +3 -0
- package/dist/server/services/index.d.ts.map +1 -1
- package/dist/server/services/index.js +3 -0
- package/dist/server/services/index.js.map +1 -1
- package/dist/server/services/organisation.d.ts +117 -0
- package/dist/server/services/organisation.d.ts.map +1 -0
- package/dist/server/services/organisation.js +319 -0
- package/dist/server/services/organisation.js.map +1 -0
- package/dist/server/services/pages.d.ts +16 -1
- package/dist/server/services/pages.d.ts.map +1 -1
- package/dist/server/services/pages.js +62 -3
- package/dist/server/services/pages.js.map +1 -1
- package/migrations/0007_organisation.sql +114 -0
- package/migrations/meta/0007_snapshot.json +1588 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/client/components/ConfirmDialog.svelte +123 -0
- package/src/client/components/FavoriteStar.svelte +81 -0
- package/src/client/components/LabelChip.svelte +46 -0
- package/src/client/components/LabelManager.svelte +336 -0
- package/src/client/components/PageLabels.svelte +158 -0
- package/src/client/components/SidebarFavorites.svelte +262 -0
- package/src/client/components/SidebarRecents.svelte +98 -0
- package/src/client/components/SidebarSpaces.svelte +266 -1
- package/src/client/i18n.ts +387 -0
- package/src/client/index.ts +8 -0
- package/src/client/mock.ts +306 -0
- package/src/client/module.ts +18 -0
- package/src/client/pages/PageView.svelte +227 -4
- package/src/client/pages/TrashPage.svelte +378 -0
- package/src/client/query.ts +24 -0
- package/src/contract/models.ts +83 -0
- package/src/contract/permissions.ts +36 -0
- package/src/contract/router.ts +153 -1
package/src/contract/router.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { baseContract, Id, PageInput, page, WorkspaceId } from '@kernhq/contracts'
|
|
1
|
+
import { baseContract, Id, PageInput, page, UserId, WorkspaceId } from '@kernhq/contracts'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import {
|
|
4
4
|
CommentAnchor,
|
|
5
5
|
CommentThread,
|
|
6
|
+
Favorite,
|
|
7
|
+
Label,
|
|
8
|
+
LabelColour,
|
|
6
9
|
Ok,
|
|
7
10
|
Page,
|
|
8
11
|
PageKind,
|
|
9
12
|
PageNode,
|
|
10
13
|
PageVersion,
|
|
14
|
+
RecentView,
|
|
11
15
|
RichDoc,
|
|
12
16
|
Space,
|
|
13
17
|
SpaceVisibility,
|
|
@@ -28,6 +32,41 @@ import {
|
|
|
28
32
|
const ws = z.object({ workspaceId: WorkspaceId })
|
|
29
33
|
const t = (...tags: string[]) => ({ tags })
|
|
30
34
|
|
|
35
|
+
/** The page fields a shortcut row draws, so a sidebar is one request rather than one per entry. */
|
|
36
|
+
const pageBits = {
|
|
37
|
+
spaceId: Id,
|
|
38
|
+
title: z.string(),
|
|
39
|
+
icon: z.string().nullable(),
|
|
40
|
+
kind: PageKind,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A favourite as a sidebar draws it: the shortcut, plus enough of its page to render the row.
|
|
45
|
+
*
|
|
46
|
+
* Composed here rather than stored, for the same reason `versions.get` composes its output here —
|
|
47
|
+
* it is the shape of one answer, not a second copy of the page. A favourite whose page has been
|
|
48
|
+
* trashed or purged is not in this list at all: the row survives (nothing cascades), and every read
|
|
49
|
+
* joins to `pages`, so it is simply never drawn.
|
|
50
|
+
*/
|
|
51
|
+
export const FavoriteEntry = Favorite.extend(pageBits)
|
|
52
|
+
export type FavoriteEntry = z.infer<typeof FavoriteEntry>
|
|
53
|
+
|
|
54
|
+
/** The same, for a page somebody opened recently. */
|
|
55
|
+
export const RecentEntry = RecentView.extend(pageBits)
|
|
56
|
+
export type RecentEntry = z.infer<typeof RecentEntry>
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether you are watching a page, and who else is.
|
|
60
|
+
*
|
|
61
|
+
* Both in one answer because a watch button has to draw both — its own pressed state and the number
|
|
62
|
+
* beside it — and asking twice within a keystroke of each other is two requests for one control.
|
|
63
|
+
*/
|
|
64
|
+
export const WatchState = z.object({
|
|
65
|
+
watching: z.boolean(),
|
|
66
|
+
watchers: z.array(UserId),
|
|
67
|
+
})
|
|
68
|
+
export type WatchState = z.infer<typeof WatchState>
|
|
69
|
+
|
|
31
70
|
export const quireContract = {
|
|
32
71
|
spaces: {
|
|
33
72
|
list: baseContract
|
|
@@ -137,6 +176,19 @@ export const quireContract = {
|
|
|
137
176
|
.route({ method: 'DELETE', path: '/pages/{pageId}', ...t('pages') })
|
|
138
177
|
.input(ws.extend({ pageId: Id }))
|
|
139
178
|
.output(z.object({ ok: z.literal(true), count: z.number().int().nonnegative() })),
|
|
179
|
+
/**
|
|
180
|
+
* The labels on this page, replaced by exactly this set.
|
|
181
|
+
*
|
|
182
|
+
* `set`, not `add`: a picker with three labels ticked sends three, and a picker with one ticked
|
|
183
|
+
* sends one and means the other two are gone. An additive procedure cannot express unticking
|
|
184
|
+
* without a second one to pair with it, and the pair then has to agree about a page two people
|
|
185
|
+
* are editing. Every label has to be one of the space's own — a label belongs to a space, so
|
|
186
|
+
* putting another space's label on a page would leak its vocabulary across the boundary.
|
|
187
|
+
*/
|
|
188
|
+
setLabels: baseContract
|
|
189
|
+
.route({ method: 'POST', path: '/pages/{pageId}/labels', ...t('pages') })
|
|
190
|
+
.input(ws.extend({ pageId: Id, labelIds: z.array(Id).max(50) }))
|
|
191
|
+
.output(z.array(Label)),
|
|
140
192
|
},
|
|
141
193
|
|
|
142
194
|
versions: {
|
|
@@ -355,6 +407,106 @@ export const quireContract = {
|
|
|
355
407
|
.output(Ok),
|
|
356
408
|
},
|
|
357
409
|
|
|
410
|
+
/**
|
|
411
|
+
* The vocabulary a space puts on its pages.
|
|
412
|
+
*
|
|
413
|
+
* Reading is `quire.space.view` and writing is `quire.space.manage`, because a label is part of a
|
|
414
|
+
* space's configuration rather than a page's content: renaming "Draft" changes what it means on
|
|
415
|
+
* every page that wears it, which is not a thing somebody who may edit one page should be able to
|
|
416
|
+
* do to everybody else's.
|
|
417
|
+
*/
|
|
418
|
+
labels: {
|
|
419
|
+
list: baseContract
|
|
420
|
+
.route({ method: 'GET', path: '/spaces/{spaceId}/labels', ...t('labels') })
|
|
421
|
+
.input(ws.extend({ spaceId: Id }))
|
|
422
|
+
.output(z.array(Label)),
|
|
423
|
+
/** What one page wears. `pages.setLabels` is the other half; without this one nothing draws it. */
|
|
424
|
+
forPage: baseContract
|
|
425
|
+
.route({ method: 'GET', path: '/pages/{pageId}/labels', ...t('labels') })
|
|
426
|
+
.input(ws.extend({ pageId: Id }))
|
|
427
|
+
.output(z.array(Label)),
|
|
428
|
+
create: baseContract
|
|
429
|
+
.route({ method: 'POST', path: '/spaces/{spaceId}/labels', ...t('labels') })
|
|
430
|
+
.input(ws.extend({ spaceId: Id, name: Label.shape.name, colour: LabelColour.default('grey') }))
|
|
431
|
+
.output(Label),
|
|
432
|
+
update: baseContract
|
|
433
|
+
.route({ method: 'PATCH', path: '/labels/{labelId}', ...t('labels') })
|
|
434
|
+
.input(
|
|
435
|
+
ws.extend({
|
|
436
|
+
labelId: Id,
|
|
437
|
+
name: Label.shape.name.optional(),
|
|
438
|
+
colour: LabelColour.optional(),
|
|
439
|
+
}),
|
|
440
|
+
)
|
|
441
|
+
.output(Label),
|
|
442
|
+
/** Off every page that wore it, too — a label nothing can name is not a label anybody can remove. */
|
|
443
|
+
remove: baseContract
|
|
444
|
+
.route({ method: 'DELETE', path: '/labels/{labelId}', ...t('labels') })
|
|
445
|
+
.input(ws.extend({ labelId: Id }))
|
|
446
|
+
.output(Ok),
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* One person's own shortcuts, in the order they arranged them.
|
|
451
|
+
*
|
|
452
|
+
* Every procedure here is filtered to the caller. Row-level security fences the *workspace*, which
|
|
453
|
+
* is the tenant boundary and not a privacy boundary — one colleague's favourites are as visible to
|
|
454
|
+
* the policy as your own — so the `user_id` in each of these queries is the only thing keeping a
|
|
455
|
+
* sidebar personal. The three mutations all answer with the whole ordered list rather than the one
|
|
456
|
+
* row they touched: a fractional-index reorder is only meaningful as an ordering, the list is one
|
|
457
|
+
* person's and therefore short, and it saves the sidebar a refetch to redraw itself.
|
|
458
|
+
*/
|
|
459
|
+
favorites: {
|
|
460
|
+
list: baseContract
|
|
461
|
+
.route({ method: 'GET', path: '/favorites', ...t('favorites') })
|
|
462
|
+
.input(ws)
|
|
463
|
+
.output(z.array(FavoriteEntry)),
|
|
464
|
+
/** Lands at the end. Starring the same page twice is the same star, not an error. */
|
|
465
|
+
add: baseContract
|
|
466
|
+
.route({ method: 'POST', path: '/favorites', ...t('favorites') })
|
|
467
|
+
.input(ws.extend({ pageId: Id }))
|
|
468
|
+
.output(z.array(FavoriteEntry)),
|
|
469
|
+
remove: baseContract
|
|
470
|
+
.route({ method: 'DELETE', path: '/favorites/{pageId}', ...t('favorites') })
|
|
471
|
+
.input(ws.extend({ pageId: Id }))
|
|
472
|
+
.output(z.array(FavoriteEntry)),
|
|
473
|
+
/** `afterId` is the favourite to land behind; null means first. The rank is minted server-side. */
|
|
474
|
+
reorder: baseContract
|
|
475
|
+
.route({ method: 'POST', path: '/favorites/{pageId}/move', ...t('favorites') })
|
|
476
|
+
.input(ws.extend({ pageId: Id, afterId: Id.nullable().default(null) }))
|
|
477
|
+
.output(z.array(FavoriteEntry)),
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
/** Who asked to hear about a page. Deliberately not the same list as who bookmarked it. */
|
|
481
|
+
watchers: {
|
|
482
|
+
get: baseContract
|
|
483
|
+
.route({ method: 'GET', path: '/pages/{pageId}/watchers', ...t('watchers') })
|
|
484
|
+
.input(ws.extend({ pageId: Id }))
|
|
485
|
+
.output(WatchState),
|
|
486
|
+
set: baseContract
|
|
487
|
+
.route({ method: 'POST', path: '/pages/{pageId}/watchers', ...t('watchers') })
|
|
488
|
+
.input(ws.extend({ pageId: Id, watching: z.boolean().default(true) }))
|
|
489
|
+
.output(WatchState),
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Where this person has just been, newest first — and only this person.
|
|
494
|
+
*
|
|
495
|
+
* `record` is what a page view calls; it bumps one row rather than appending to a log, so the
|
|
496
|
+
* table is bounded by pages-times-people instead of growing for ever to answer a question that
|
|
497
|
+
* only ever wants the most recent handful.
|
|
498
|
+
*/
|
|
499
|
+
recents: {
|
|
500
|
+
list: baseContract
|
|
501
|
+
.route({ method: 'GET', path: '/recents', ...t('recents') })
|
|
502
|
+
.input(ws.extend({ limit: z.number().int().min(1).max(50).default(10) }))
|
|
503
|
+
.output(z.array(RecentEntry)),
|
|
504
|
+
record: baseContract
|
|
505
|
+
.route({ method: 'POST', path: '/recents', ...t('recents') })
|
|
506
|
+
.input(ws.extend({ pageId: Id }))
|
|
507
|
+
.output(Ok),
|
|
508
|
+
},
|
|
509
|
+
|
|
358
510
|
publishing: {
|
|
359
511
|
/** Make what is written now the version readers are served. Only meaningful for a `page`. */
|
|
360
512
|
publish: baseContract
|