@kernhq/module-quire 0.10.9 → 0.11.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/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 +284 -9
- 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/client/mock.ts
CHANGED
|
@@ -5,18 +5,24 @@ import type {
|
|
|
5
5
|
Database,
|
|
6
6
|
DatabaseRef,
|
|
7
7
|
Row as DatabaseRow,
|
|
8
|
+
FavoriteEntry,
|
|
9
|
+
Label,
|
|
10
|
+
LabelColour,
|
|
8
11
|
Page,
|
|
9
12
|
PageNode,
|
|
10
13
|
PageVersion,
|
|
11
14
|
Property,
|
|
12
15
|
PropertyConfig,
|
|
13
16
|
PropertyType,
|
|
17
|
+
RecentEntry,
|
|
14
18
|
RowRef,
|
|
15
19
|
Space,
|
|
16
20
|
View,
|
|
17
21
|
ViewConfig,
|
|
18
22
|
ViewKind,
|
|
23
|
+
WatchState,
|
|
19
24
|
} from '../contract/index.js'
|
|
25
|
+
import { rankBetween, rankSequence } from './rank.js'
|
|
20
26
|
|
|
21
27
|
/**
|
|
22
28
|
* The in-memory quire API.
|
|
@@ -150,6 +156,16 @@ export function createMockQuireApi() {
|
|
|
150
156
|
page(103, uid(1), 'Your first week', 'ba', 102),
|
|
151
157
|
page(104, uid(1), 'Time off', 'bb', 102),
|
|
152
158
|
page(105, uid(1), 'Expenses', 'c', null, { kind: 'live' }),
|
|
159
|
+
/*
|
|
160
|
+
* A subtree in the trash, because that is the case the trash screen exists for.
|
|
161
|
+
*
|
|
162
|
+
* Deleting a page takes everything under it, and the flat listing the server answers with has
|
|
163
|
+
* one row per page — so a demo whose trash holds a single orphan never exercises the grouping,
|
|
164
|
+
* which is the whole difference between "one page was deleted" and "three were". These two are
|
|
165
|
+
* a parent and its child, deleted together.
|
|
166
|
+
*/
|
|
167
|
+
page(106, uid(1), 'Old expenses policy', 'ca', null, { deletedAt: iso(1728e5) }),
|
|
168
|
+
page(107, uid(1), 'Receipts', 'caa', 106, { deletedAt: iso(1728e5) }),
|
|
153
169
|
page(110, uid(1), 'Onboarding tasks', 'd', null, { kind: 'database' }),
|
|
154
170
|
page(201, uid(2), 'Architecture', 'a'),
|
|
155
171
|
page(202, uid(2), 'Runbooks', 'b'),
|
|
@@ -391,6 +407,70 @@ export function createMockQuireApi() {
|
|
|
391
407
|
comment(161, uid(101), uid(160), uid(160), ME, 'Good point — I will link to it from here.', 36e5),
|
|
392
408
|
]
|
|
393
409
|
|
|
410
|
+
/**
|
|
411
|
+
* How a space is organised, and what one person has made of it.
|
|
412
|
+
*
|
|
413
|
+
* Seeded rather than left empty for the same reason the versions and comments above are: a screen
|
|
414
|
+
* with no data in the demo is a screen the end-to-end sweep cannot see, so an empty favourites
|
|
415
|
+
* group and an empty label picker would ship without anything ever rendering the populated case.
|
|
416
|
+
*
|
|
417
|
+
* Two spaces on purpose. Labels belong to a space — two teams both wanting "Draft" should not
|
|
418
|
+
* have to agree on what it means — and a seed with one space's vocabulary would let a bug that
|
|
419
|
+
* leaks labels across the boundary pass unnoticed.
|
|
420
|
+
*/
|
|
421
|
+
const label = (n: number, spaceId: string, name: string, colour: LabelColour, msAgo: number): Label => ({
|
|
422
|
+
id: uid(n),
|
|
423
|
+
workspaceId: '' as Label['workspaceId'],
|
|
424
|
+
spaceId,
|
|
425
|
+
name,
|
|
426
|
+
colour,
|
|
427
|
+
createdAt: iso(msAgo),
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
const labels: Label[] = [
|
|
431
|
+
label(300, uid(1), 'Draft', 'warning', 8e7),
|
|
432
|
+
label(301, uid(1), 'Needs review', 'info', 79e6),
|
|
433
|
+
label(302, uid(1), 'Reference', 'purple', 78e6),
|
|
434
|
+
label(303, uid(2), 'ADR', 'slate', 77e6),
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
/** page id → label ids. A page wears a set, and `pages.setLabels` replaces the whole of it. */
|
|
438
|
+
const pageLabels = new Map<string, string[]>([
|
|
439
|
+
[uid(101), [uid(302)]],
|
|
440
|
+
[uid(102), [uid(300), uid(301)]],
|
|
441
|
+
[uid(103), [uid(300)]],
|
|
442
|
+
[uid(201), [uid(303)]],
|
|
443
|
+
])
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* One person's shortcuts, in the order they arranged them.
|
|
447
|
+
*
|
|
448
|
+
* Ranks come from the real `rankBetween` rather than from the plain sortable strings the rest of
|
|
449
|
+
* this file uses. Reordering favourites is a drag, so the mock is the only thing the interaction
|
|
450
|
+
* is ever tested against — and a second implementation of fractional indexing is a second place
|
|
451
|
+
* for it to be wrong. Three of them, across two spaces, because a favourite is a workspace list
|
|
452
|
+
* and a demo confined to one space would never show that.
|
|
453
|
+
*/
|
|
454
|
+
const favouriteSeed = rankSequence(3)
|
|
455
|
+
const favorites: { pageId: string; position: string; createdAt: string }[] = [
|
|
456
|
+
{ pageId: uid(101), position: favouriteSeed[0] as string, createdAt: iso(72e5) },
|
|
457
|
+
{ pageId: uid(105), position: favouriteSeed[1] as string, createdAt: iso(54e5) },
|
|
458
|
+
{ pageId: uid(202), position: favouriteSeed[2] as string, createdAt: iso(36e5) },
|
|
459
|
+
]
|
|
460
|
+
|
|
461
|
+
/** page id → the people watching it. Deliberately not the same list as the favourites above. */
|
|
462
|
+
const watchers = new Map<string, string[]>([
|
|
463
|
+
[uid(101), [ME, COLLEAGUE]],
|
|
464
|
+
[uid(102), [COLLEAGUE]],
|
|
465
|
+
])
|
|
466
|
+
|
|
467
|
+
/** One row per page, bumped in place — never a visit log. */
|
|
468
|
+
const recents: { pageId: string; viewedAt: string }[] = [
|
|
469
|
+
{ pageId: uid(102), viewedAt: iso(6e5) },
|
|
470
|
+
{ pageId: uid(110), viewedAt: iso(18e5) },
|
|
471
|
+
{ pageId: uid(201), viewedAt: iso(9e6) },
|
|
472
|
+
]
|
|
473
|
+
|
|
394
474
|
let seq = 900
|
|
395
475
|
const nextId = () => uid(++seq)
|
|
396
476
|
const strip = ({ _order, ...p }: Row): Page => p
|
|
@@ -472,6 +552,73 @@ export function createMockQuireApi() {
|
|
|
472
552
|
throw notFound('View')
|
|
473
553
|
}
|
|
474
554
|
|
|
555
|
+
const theLabel = (id: string): Label => {
|
|
556
|
+
const found = labels.find((l) => l.id === id)
|
|
557
|
+
if (!found) throw notFound('Label')
|
|
558
|
+
return found
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The page fields a shortcut row draws — or nothing, when the page has been trashed or purged.
|
|
563
|
+
*
|
|
564
|
+
* The server composes these with a join to `pages`, which is why a favourite whose page is gone
|
|
565
|
+
* simply stops being drawn: the row survives (nothing cascades from a purge), and every read
|
|
566
|
+
* joins. Reproducing that here is the difference between a demo where trashing a favourited page
|
|
567
|
+
* quietly removes it from the sidebar and one where the sidebar keeps a shortcut to nothing.
|
|
568
|
+
*/
|
|
569
|
+
const pageBitsOf = (pageId: string) => {
|
|
570
|
+
const row = pages.find((p) => p.id === pageId && !p.deletedAt)
|
|
571
|
+
return row ? { spaceId: row.spaceId, title: row.title, icon: row.icon, kind: row.kind } : null
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const byPosition = (a: { position: string }, b: { position: string }) =>
|
|
575
|
+
a.position < b.position ? -1 : a.position > b.position ? 1 : 0
|
|
576
|
+
|
|
577
|
+
const favoriteList = (): FavoriteEntry[] =>
|
|
578
|
+
[...favorites].sort(byPosition).flatMap((f) => {
|
|
579
|
+
const bits = pageBitsOf(f.pageId)
|
|
580
|
+
if (!bits) return []
|
|
581
|
+
return [
|
|
582
|
+
{
|
|
583
|
+
workspaceId: '' as FavoriteEntry['workspaceId'],
|
|
584
|
+
userId: ME as FavoriteEntry['userId'],
|
|
585
|
+
pageId: f.pageId,
|
|
586
|
+
position: f.position,
|
|
587
|
+
createdAt: f.createdAt,
|
|
588
|
+
...bits,
|
|
589
|
+
},
|
|
590
|
+
]
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
const recentList = (limit: number): RecentEntry[] =>
|
|
594
|
+
[...recents]
|
|
595
|
+
.sort((a, b) => (a.viewedAt < b.viewedAt ? 1 : -1))
|
|
596
|
+
.flatMap((r) => {
|
|
597
|
+
const bits = pageBitsOf(r.pageId)
|
|
598
|
+
if (!bits) return []
|
|
599
|
+
return [
|
|
600
|
+
{
|
|
601
|
+
workspaceId: '' as RecentEntry['workspaceId'],
|
|
602
|
+
userId: ME as RecentEntry['userId'],
|
|
603
|
+
pageId: r.pageId,
|
|
604
|
+
viewedAt: r.viewedAt,
|
|
605
|
+
...bits,
|
|
606
|
+
},
|
|
607
|
+
]
|
|
608
|
+
})
|
|
609
|
+
.slice(0, limit)
|
|
610
|
+
|
|
611
|
+
const watchStateOf = (pageId: string): WatchState => {
|
|
612
|
+
const list = watchers.get(pageId) ?? []
|
|
613
|
+
return { watching: list.includes(ME), watchers: list as WatchState['watchers'] }
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const labelsOn = (pageId: string): Label[] =>
|
|
617
|
+
(pageLabels.get(pageId) ?? []).flatMap((id) => {
|
|
618
|
+
const found = labels.find((l) => l.id === id)
|
|
619
|
+
return found ? [found] : []
|
|
620
|
+
})
|
|
621
|
+
|
|
475
622
|
/**
|
|
476
623
|
* A copy on the way out, because a real API answers with fresh JSON every time.
|
|
477
624
|
*
|
|
@@ -750,8 +897,167 @@ export function createMockQuireApi() {
|
|
|
750
897
|
purge: async ({ pageId }: { pageId: string }) => {
|
|
751
898
|
const ids = new Set(subtree(pageId).map((r) => r.id))
|
|
752
899
|
for (let i = pages.length - 1; i >= 0; i--) if (ids.has(pages[i]!.id)) pages.splice(i, 1)
|
|
900
|
+
/*
|
|
901
|
+
* The favourites, watches and labels of a purged page are deliberately left behind, because
|
|
902
|
+
* that is what the server does: there is no foreign key, so the rows survive and every read
|
|
903
|
+
* joins to `pages` — which is why they are invisible rather than broken. Cleaning them up
|
|
904
|
+
* here would hide the one thing worth noticing about that decision.
|
|
905
|
+
*/
|
|
753
906
|
return { ok: true as const, count: ids.size }
|
|
754
907
|
},
|
|
908
|
+
|
|
909
|
+
setLabels: async ({ pageId, labelIds }: { pageId: string; labelIds: string[] }) => {
|
|
910
|
+
const row = found(pageId)
|
|
911
|
+
for (const id of labelIds) {
|
|
912
|
+
const label = theLabel(id)
|
|
913
|
+
// A label belongs to a space; putting another space's on a page would leak its vocabulary.
|
|
914
|
+
if (label.spaceId !== row.spaceId)
|
|
915
|
+
throw Object.assign(new Error('Every label has to be one this space declares'), {
|
|
916
|
+
code: 'BAD_REQUEST',
|
|
917
|
+
})
|
|
918
|
+
}
|
|
919
|
+
pageLabels.set(pageId, [...labelIds])
|
|
920
|
+
return labelsOn(pageId).map((l) => ({ ...l }))
|
|
921
|
+
},
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* The space's vocabulary. Names clash case-insensitively, as they do in the database — "Draft"
|
|
926
|
+
* beside "draft" in one picker is broken data rather than two labels — and the capitalisation
|
|
927
|
+
* somebody typed is what is kept.
|
|
928
|
+
*/
|
|
929
|
+
labels: {
|
|
930
|
+
list: async ({ spaceId }: { spaceId: string }) =>
|
|
931
|
+
labels.filter((l) => l.spaceId === spaceId).map((l) => ({ ...l })),
|
|
932
|
+
|
|
933
|
+
forPage: async ({ pageId }: { pageId: string }) => labelsOn(pageId).map((l) => ({ ...l })),
|
|
934
|
+
|
|
935
|
+
create: async (input: { spaceId: string; name: string; colour?: LabelColour }) => {
|
|
936
|
+
const name = input.name.trim()
|
|
937
|
+
if (!name) throw Object.assign(new Error('A label needs a name'), { code: 'BAD_REQUEST' })
|
|
938
|
+
if (labels.some((l) => l.spaceId === input.spaceId && l.name.toLowerCase() === name.toLowerCase()))
|
|
939
|
+
throw Object.assign(new Error(`This space already has a label called "${name}"`), {
|
|
940
|
+
code: 'CONFLICT',
|
|
941
|
+
})
|
|
942
|
+
const made: Label = {
|
|
943
|
+
id: nextId(),
|
|
944
|
+
workspaceId: '' as Label['workspaceId'],
|
|
945
|
+
spaceId: input.spaceId,
|
|
946
|
+
name,
|
|
947
|
+
colour: input.colour ?? 'grey',
|
|
948
|
+
createdAt: new Date().toISOString(),
|
|
949
|
+
}
|
|
950
|
+
labels.push(made)
|
|
951
|
+
return { ...made }
|
|
952
|
+
},
|
|
953
|
+
|
|
954
|
+
update: async (input: { labelId: string; name?: string; colour?: LabelColour }) => {
|
|
955
|
+
const label = theLabel(input.labelId)
|
|
956
|
+
if (input.name !== undefined) {
|
|
957
|
+
const name = input.name.trim()
|
|
958
|
+
if (!name) throw Object.assign(new Error('A label needs a name'), { code: 'BAD_REQUEST' })
|
|
959
|
+
if (
|
|
960
|
+
labels.some(
|
|
961
|
+
(l) =>
|
|
962
|
+
l.id !== label.id &&
|
|
963
|
+
l.spaceId === label.spaceId &&
|
|
964
|
+
l.name.toLowerCase() === name.toLowerCase(),
|
|
965
|
+
)
|
|
966
|
+
)
|
|
967
|
+
throw Object.assign(new Error(`This space already has a label called "${name}"`), {
|
|
968
|
+
code: 'CONFLICT',
|
|
969
|
+
})
|
|
970
|
+
label.name = name
|
|
971
|
+
}
|
|
972
|
+
if (input.colour !== undefined) label.colour = input.colour
|
|
973
|
+
return { ...label }
|
|
974
|
+
},
|
|
975
|
+
|
|
976
|
+
remove: async ({ labelId }: { labelId: string }) => {
|
|
977
|
+
const label = theLabel(labelId)
|
|
978
|
+
labels.splice(labels.indexOf(label), 1)
|
|
979
|
+
// Off every page that wore it — a label nothing can name is not one anybody can remove.
|
|
980
|
+
for (const [pageId, ids] of pageLabels)
|
|
981
|
+
if (ids.includes(labelId))
|
|
982
|
+
pageLabels.set(
|
|
983
|
+
pageId,
|
|
984
|
+
ids.filter((id) => id !== labelId),
|
|
985
|
+
)
|
|
986
|
+
return { ok: true as const }
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* One person's own shortcuts. Every mutation answers with the whole ordered list, because a
|
|
992
|
+
* fractional-index reorder is only meaningful as an ordering — and because it saves the sidebar
|
|
993
|
+
* a refetch to redraw itself.
|
|
994
|
+
*/
|
|
995
|
+
favorites: {
|
|
996
|
+
list: async () => favoriteList(),
|
|
997
|
+
|
|
998
|
+
add: async ({ pageId }: { pageId: string }) => {
|
|
999
|
+
found(pageId)
|
|
1000
|
+
// Starring the same page twice is the same star, not an error.
|
|
1001
|
+
if (!favorites.some((f) => f.pageId === pageId)) {
|
|
1002
|
+
const last = [...favorites].sort(byPosition).at(-1)?.position ?? null
|
|
1003
|
+
favorites.push({
|
|
1004
|
+
pageId,
|
|
1005
|
+
position: rankBetween(last, null),
|
|
1006
|
+
createdAt: new Date().toISOString(),
|
|
1007
|
+
})
|
|
1008
|
+
}
|
|
1009
|
+
return favoriteList()
|
|
1010
|
+
},
|
|
1011
|
+
|
|
1012
|
+
remove: async ({ pageId }: { pageId: string }) => {
|
|
1013
|
+
const at = favorites.findIndex((f) => f.pageId === pageId)
|
|
1014
|
+
if (at >= 0) favorites.splice(at, 1)
|
|
1015
|
+
return favoriteList()
|
|
1016
|
+
},
|
|
1017
|
+
|
|
1018
|
+
reorder: async ({ pageId, afterId = null }: { pageId: string; afterId?: string | null }) => {
|
|
1019
|
+
const moving = favorites.find((f) => f.pageId === pageId)
|
|
1020
|
+
if (!moving) throw notFound('Favourite')
|
|
1021
|
+
const rest = [...favorites].filter((f) => f.pageId !== pageId).sort(byPosition)
|
|
1022
|
+
const at = afterId ? rest.findIndex((f) => f.pageId === afterId) : -1
|
|
1023
|
+
if (afterId && at < 0)
|
|
1024
|
+
throw Object.assign(new Error('afterId is not one of your favourites'), {
|
|
1025
|
+
code: 'BAD_REQUEST',
|
|
1026
|
+
})
|
|
1027
|
+
moving.position = rankBetween(
|
|
1028
|
+
at >= 0 ? (rest[at]?.position ?? null) : null,
|
|
1029
|
+
rest[at + 1]?.position ?? null,
|
|
1030
|
+
)
|
|
1031
|
+
return favoriteList()
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
|
|
1035
|
+
watchers: {
|
|
1036
|
+
get: async ({ pageId }: { pageId: string }) => {
|
|
1037
|
+
found(pageId)
|
|
1038
|
+
return watchStateOf(pageId)
|
|
1039
|
+
},
|
|
1040
|
+
|
|
1041
|
+
set: async ({ pageId, watching = true }: { pageId: string; watching?: boolean }) => {
|
|
1042
|
+
found(pageId)
|
|
1043
|
+
const list = watchers.get(pageId) ?? []
|
|
1044
|
+
const next = watching ? (list.includes(ME) ? list : [...list, ME]) : list.filter((id) => id !== ME)
|
|
1045
|
+
watchers.set(pageId, next)
|
|
1046
|
+
return watchStateOf(pageId)
|
|
1047
|
+
},
|
|
1048
|
+
},
|
|
1049
|
+
|
|
1050
|
+
recents: {
|
|
1051
|
+
list: async ({ limit = 10 }: { limit?: number } = {}) => recentList(limit),
|
|
1052
|
+
|
|
1053
|
+
record: async ({ pageId }: { pageId: string }) => {
|
|
1054
|
+
found(pageId)
|
|
1055
|
+
const existing = recents.find((r) => r.pageId === pageId)
|
|
1056
|
+
// Bumped in place, never appended: this is one row per page, not a visit log.
|
|
1057
|
+
if (existing) existing.viewedAt = new Date().toISOString()
|
|
1058
|
+
else recents.push({ pageId, viewedAt: new Date().toISOString() })
|
|
1059
|
+
return { ok: true as const }
|
|
1060
|
+
},
|
|
755
1061
|
},
|
|
756
1062
|
|
|
757
1063
|
versions: {
|
package/src/client/module.ts
CHANGED
|
@@ -49,6 +49,24 @@ export const quireClientModule = defineClientModule({
|
|
|
49
49
|
component: () => import('./pages/SpacePage.svelte'),
|
|
50
50
|
permission: QUIRE_PERMISSIONS.spaceView,
|
|
51
51
|
},
|
|
52
|
+
/**
|
|
53
|
+
* Declared before `/quire/:space/:page` for a reader's benefit only — the shell resolves by
|
|
54
|
+
* specificity, not by order, so the two literal segments here beat the one in the page route
|
|
55
|
+
* whatever sequence they are written in. A page whose id happened to be "trash" could not
|
|
56
|
+
* shadow this, and a space could not shadow the spaces index either.
|
|
57
|
+
*
|
|
58
|
+
* `page.edit`, matching what `pages.trash` asks on the server: the trash is where an edit is
|
|
59
|
+
* undone, so anybody who may edit a page in this space may see what has been taken out of it.
|
|
60
|
+
* Purging is `page.delete` and is gated on the row.
|
|
61
|
+
*/
|
|
62
|
+
{
|
|
63
|
+
path: '/quire/:space/trash',
|
|
64
|
+
component: () => import('./pages/TrashPage.svelte'),
|
|
65
|
+
get title() {
|
|
66
|
+
return t('trash')
|
|
67
|
+
},
|
|
68
|
+
permission: QUIRE_PERMISSIONS.pageEdit,
|
|
69
|
+
},
|
|
52
70
|
{
|
|
53
71
|
path: '/quire/:space/:page',
|
|
54
72
|
component: () => import('./pages/PageView.svelte'),
|