@kernhq/module-quire 0.13.1 → 0.15.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 +394 -0
- package/dist/contract/models.d.ts.map +1 -1
- package/dist/contract/models.js +284 -0
- package/dist/contract/models.js.map +1 -1
- package/dist/contract/permissions.d.ts +14 -0
- package/dist/contract/permissions.d.ts.map +1 -1
- package/dist/contract/permissions.js +114 -0
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/properties.d.ts +7 -7
- package/dist/contract/router.d.ts +1124 -96
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +343 -1
- package/dist/contract/router.js.map +1 -1
- package/dist/server/_impl.d.ts +983 -1168
- package/dist/server/_impl.d.ts.map +1 -1
- package/dist/server/_impl.js +308 -0
- package/dist/server/_impl.js.map +1 -1
- package/dist/server/export/html.d.ts +67 -0
- package/dist/server/export/html.d.ts.map +1 -0
- package/dist/server/export/html.js +206 -0
- package/dist/server/export/html.js.map +1 -0
- package/dist/server/export/markdown.d.ts +51 -0
- package/dist/server/export/markdown.d.ts.map +1 -0
- package/dist/server/export/markdown.js +351 -0
- package/dist/server/export/markdown.js.map +1 -0
- package/dist/server/export/pdf.d.ts +20 -0
- package/dist/server/export/pdf.d.ts.map +1 -0
- package/dist/server/export/pdf.js +91 -0
- package/dist/server/export/pdf.js.map +1 -0
- package/dist/server/export/zip.d.ts +31 -0
- package/dist/server/export/zip.d.ts.map +1 -0
- package/dist/server/export/zip.js +158 -0
- package/dist/server/export/zip.js.map +1 -0
- package/dist/server/import/csv.d.ts +77 -0
- package/dist/server/import/csv.d.ts.map +1 -0
- package/dist/server/import/csv.js +263 -0
- package/dist/server/import/csv.js.map +1 -0
- package/dist/server/import/html.d.ts +52 -0
- package/dist/server/import/html.d.ts.map +1 -0
- package/dist/server/import/html.js +472 -0
- package/dist/server/import/html.js.map +1 -0
- package/dist/server/import/markdown.d.ts +63 -0
- package/dist/server/import/markdown.d.ts.map +1 -0
- package/dist/server/import/markdown.js +692 -0
- package/dist/server/import/markdown.js.map +1 -0
- package/dist/server/import/plan.d.ts +70 -0
- package/dist/server/import/plan.d.ts.map +1 -0
- package/dist/server/import/plan.js +761 -0
- package/dist/server/import/plan.js.map +1 -0
- package/dist/server/import/ydoc.d.ts +35 -0
- package/dist/server/import/ydoc.d.ts.map +1 -0
- package/dist/server/import/ydoc.js +91 -0
- package/dist/server/import/ydoc.js.map +1 -0
- package/dist/server/import/zip.d.ts +63 -0
- package/dist/server/import/zip.d.ts.map +1 -0
- package/dist/server/import/zip.js +308 -0
- package/dist/server/import/zip.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/render.d.ts +158 -0
- package/dist/server/render.d.ts.map +1 -1
- package/dist/server/render.js +240 -0
- package/dist/server/render.js.map +1 -1
- package/dist/server/schema.d.ts +704 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +254 -1
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/databases.d.ts +10 -10
- package/dist/server/services/export.d.ts +176 -0
- package/dist/server/services/export.d.ts.map +1 -0
- package/dist/server/services/export.js +822 -0
- package/dist/server/services/export.js.map +1 -0
- package/dist/server/services/import.d.ts +109 -0
- package/dist/server/services/import.d.ts.map +1 -0
- package/dist/server/services/import.js +570 -0
- package/dist/server/services/import.js.map +1 -0
- package/dist/server/services/index.d.ts +32 -1
- package/dist/server/services/index.d.ts.map +1 -1
- package/dist/server/services/index.js +87 -4
- package/dist/server/services/index.js.map +1 -1
- package/dist/server/services/macros.d.ts +83 -0
- package/dist/server/services/macros.d.ts.map +1 -0
- package/dist/server/services/macros.js +488 -0
- package/dist/server/services/macros.js.map +1 -0
- package/dist/server/services/publications.d.ts +2 -1
- package/dist/server/services/publications.d.ts.map +1 -1
- package/dist/server/services/publications.js +43 -4
- package/dist/server/services/publications.js.map +1 -1
- package/dist/server/services/templates.d.ts +135 -0
- package/dist/server/services/templates.d.ts.map +1 -0
- package/dist/server/services/templates.js +897 -0
- package/dist/server/services/templates.js.map +1 -0
- package/dist/server/services/versions.d.ts +13 -1
- package/dist/server/services/versions.d.ts.map +1 -1
- package/dist/server/services/versions.js +3 -1
- package/dist/server/services/versions.js.map +1 -1
- package/migrations/0010_transfers.sql +154 -0
- package/migrations/0011_templates.sql +157 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +5 -5
- package/src/client/components/ExportDialog.svelte +685 -0
- package/src/client/components/ImportDialog.svelte +702 -0
- package/src/client/components/ImportReport.svelte +310 -0
- package/src/client/components/NewSpaceDialog.svelte +77 -8
- package/src/client/components/PageEditor.svelte +80 -0
- package/src/client/components/PagePicker.svelte +264 -0
- package/src/client/components/SaveAsTemplateDialog.svelte +502 -0
- package/src/client/components/SidebarSpaces.svelte +112 -1
- package/src/client/components/TemplatePicker.svelte +437 -0
- package/src/client/i18n.ts +941 -0
- package/src/client/index.ts +51 -0
- package/src/client/mock.ts +592 -0
- package/src/client/module.ts +36 -0
- package/src/client/pages/PageView.svelte +86 -0
- package/src/client/pages/SpacePage.svelte +20 -4
- package/src/client/pages/TransfersPage.svelte +570 -0
- package/src/client/permissions.ts +11 -0
- package/src/client/query.ts +40 -0
- package/src/client/transfers.ts +142 -0
- package/src/contract/models.ts +343 -0
- package/src/contract/permissions.ts +117 -0
- package/src/contract/router.ts +375 -0
package/src/client/index.ts
CHANGED
|
@@ -27,8 +27,17 @@ export {
|
|
|
27
27
|
type Comment,
|
|
28
28
|
type CommentAnchor,
|
|
29
29
|
type CommentThread,
|
|
30
|
+
type ExportFormat,
|
|
31
|
+
type ExportJob,
|
|
32
|
+
type ExportJobDetail,
|
|
33
|
+
type ExportScope,
|
|
30
34
|
type Favorite,
|
|
31
35
|
type FavoriteEntry,
|
|
36
|
+
type ImportJob,
|
|
37
|
+
type ImportJobSummary,
|
|
38
|
+
type ImportOutcome,
|
|
39
|
+
type ImportReportEntry,
|
|
40
|
+
type ImportSource,
|
|
32
41
|
type Label,
|
|
33
42
|
type LabelColour,
|
|
34
43
|
MODULE_ID,
|
|
@@ -49,6 +58,27 @@ export {
|
|
|
49
58
|
type RecentView,
|
|
50
59
|
type Space,
|
|
51
60
|
type SpaceVisibility,
|
|
61
|
+
TEMPLATE_BUILT_IN_VARIABLES,
|
|
62
|
+
TEMPLATE_STARTER_KEYS,
|
|
63
|
+
type Template,
|
|
64
|
+
/**
|
|
65
|
+
* A picker row: a shipped starter, or a template somebody saved.
|
|
66
|
+
*
|
|
67
|
+
* Exported because a screen outside this package will want to draw one — the "New page" picker
|
|
68
|
+
* lives here, but a dashboard widget offering "start a meeting note" is the obvious next thing,
|
|
69
|
+
* and it should read the shape from here rather than restate it.
|
|
70
|
+
*/
|
|
71
|
+
type TemplateChoice,
|
|
72
|
+
type TemplateKind,
|
|
73
|
+
type TemplateResult,
|
|
74
|
+
type TemplateSpaceBody,
|
|
75
|
+
type TemplateSpaceNode,
|
|
76
|
+
type TemplateStarterKey,
|
|
77
|
+
type TemplateSummary,
|
|
78
|
+
type TemplateVariable,
|
|
79
|
+
type TemplateVariableType,
|
|
80
|
+
type TransferCounts,
|
|
81
|
+
type TransferState,
|
|
52
82
|
type VersionKind,
|
|
53
83
|
type Watcher,
|
|
54
84
|
type WatchState,
|
|
@@ -141,3 +171,24 @@ export {
|
|
|
141
171
|
publicSiteBasePath,
|
|
142
172
|
publicSiteUrl,
|
|
143
173
|
} from './public-url.js'
|
|
174
|
+
/**
|
|
175
|
+
* The vocabulary of a transfer, as labels rather than as enum members.
|
|
176
|
+
*
|
|
177
|
+
* Exported for the same reason `descriptorFor` and `viewIcon` are: a state, a scope, a format and a
|
|
178
|
+
* source have exactly one right name each in each language, and anything that ends up drawing one —
|
|
179
|
+
* a widget, a notification renderer, a screen written later — should read it from here rather than
|
|
180
|
+
* re-deriving a `switch` that has to be kept in step with the contract's enums.
|
|
181
|
+
*/
|
|
182
|
+
export {
|
|
183
|
+
EXPORT_FORMATS,
|
|
184
|
+
formatDescription,
|
|
185
|
+
formatLabel,
|
|
186
|
+
IMPORT_SOURCES,
|
|
187
|
+
isRunning,
|
|
188
|
+
progressRatio,
|
|
189
|
+
scopeLabel,
|
|
190
|
+
sourceDescription,
|
|
191
|
+
sourceLabel,
|
|
192
|
+
stateIcon,
|
|
193
|
+
stateLabel,
|
|
194
|
+
} from './transfers.js'
|
package/src/client/mock.ts
CHANGED
|
@@ -5,7 +5,13 @@ import type {
|
|
|
5
5
|
Database,
|
|
6
6
|
DatabaseRef,
|
|
7
7
|
Row as DatabaseRow,
|
|
8
|
+
ExportFormat,
|
|
9
|
+
ExportJob,
|
|
10
|
+
ExportScope,
|
|
8
11
|
FavoriteEntry,
|
|
12
|
+
ImportJob,
|
|
13
|
+
ImportReportEntry,
|
|
14
|
+
ImportSource,
|
|
9
15
|
Label,
|
|
10
16
|
LabelColour,
|
|
11
17
|
Page,
|
|
@@ -18,6 +24,12 @@ import type {
|
|
|
18
24
|
RecentEntry,
|
|
19
25
|
RowRef,
|
|
20
26
|
Space,
|
|
27
|
+
Template,
|
|
28
|
+
TemplateChoice,
|
|
29
|
+
TemplateKind,
|
|
30
|
+
TemplateResult,
|
|
31
|
+
TemplateVariable,
|
|
32
|
+
TransferCounts,
|
|
21
33
|
View,
|
|
22
34
|
ViewConfig,
|
|
23
35
|
ViewKind,
|
|
@@ -918,6 +930,302 @@ export function createMockQuireApi() {
|
|
|
918
930
|
return String(left).localeCompare(String(right))
|
|
919
931
|
}
|
|
920
932
|
|
|
933
|
+
// ----------------------------------------------------------------------------------------------
|
|
934
|
+
// Getting work in and out
|
|
935
|
+
// ----------------------------------------------------------------------------------------------
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* A transfer is a job, so the mock has to behave like one rather than answer instantly.
|
|
939
|
+
*
|
|
940
|
+
* Every screen here is a progress bar, a spinner and a report that arrives afterwards, and a mock
|
|
941
|
+
* that returned `done` from `start` would leave all three unreachable — the demo and the
|
|
942
|
+
* end-to-end sweep would only ever see the finished state. So a started job carries a real
|
|
943
|
+
* `createdAt` and its state is **computed from how long ago that was**: queued for the first
|
|
944
|
+
* second, running while it counts, then done. No timers, so nothing keeps running after the tab
|
|
945
|
+
* that made it has gone.
|
|
946
|
+
*
|
|
947
|
+
* The seeded rows cover the three states somebody has to be able to look at without waiting: a
|
|
948
|
+
* finished export with a file, a failed one with a reason worth reading, and a finished import
|
|
949
|
+
* whose report has all three outcomes in it.
|
|
950
|
+
*/
|
|
951
|
+
const EXPORT_STEP_MS = 320
|
|
952
|
+
const EXPORT_START_MS = 800
|
|
953
|
+
const IMPORT_READ_MS = 3200
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* A valid, empty zip — the 22-byte end-of-central-directory record and nothing else.
|
|
957
|
+
*
|
|
958
|
+
* There is no object storage behind `dev:mock`, so a download has to be something the browser can
|
|
959
|
+
* actually save; a `data:` URL is the only address that works without one. Empty rather than
|
|
960
|
+
* fabricated: a demo zip full of invented pages is a file somebody opens expecting their handbook.
|
|
961
|
+
*/
|
|
962
|
+
const EMPTY_ZIP = 'data:application/zip;base64,UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA=='
|
|
963
|
+
|
|
964
|
+
const countsOf = (report: ImportReportEntry[]): TransferCounts => ({
|
|
965
|
+
total: report.length,
|
|
966
|
+
done: report.filter((r) => r.outcome === 'imported').length,
|
|
967
|
+
skipped: report.filter((r) => r.outcome === 'skipped').length,
|
|
968
|
+
failed: report.filter((r) => r.outcome === 'failed').length,
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
const exportJobs: ExportJob[] = [
|
|
972
|
+
{
|
|
973
|
+
id: uid(701),
|
|
974
|
+
workspaceId: '' as ExportJob['workspaceId'],
|
|
975
|
+
requestedBy: ME as ExportJob['requestedBy'],
|
|
976
|
+
scope: 'space',
|
|
977
|
+
targetId: uid(1),
|
|
978
|
+
format: 'markdown',
|
|
979
|
+
state: 'done',
|
|
980
|
+
fileId: uid(781),
|
|
981
|
+
error: null,
|
|
982
|
+
counts: { total: 8, done: 7, skipped: 1, failed: 0 },
|
|
983
|
+
createdAt: iso(72e5),
|
|
984
|
+
finishedAt: iso(71e5),
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
id: uid(702),
|
|
988
|
+
workspaceId: '' as ExportJob['workspaceId'],
|
|
989
|
+
requestedBy: ME as ExportJob['requestedBy'],
|
|
990
|
+
scope: 'subtree',
|
|
991
|
+
targetId: uid(102),
|
|
992
|
+
format: 'pdf',
|
|
993
|
+
state: 'failed',
|
|
994
|
+
fileId: null,
|
|
995
|
+
// The failure worth seeding is the one an operator can act on, written the way the service
|
|
996
|
+
// writes it: the PDF renderer is a separate container, and a stack with it switched off is by
|
|
997
|
+
// far the most common reason a PDF export dies.
|
|
998
|
+
error: 'The PDF service did not answer at http://gotenberg:3000 (connect ECONNREFUSED)',
|
|
999
|
+
counts: { total: 3, done: 3, skipped: 0, failed: 0 },
|
|
1000
|
+
createdAt: iso(864e5),
|
|
1001
|
+
finishedAt: iso(8636e4),
|
|
1002
|
+
},
|
|
1003
|
+
]
|
|
1004
|
+
|
|
1005
|
+
/** One report with all three outcomes in it, because that is the whole point of the screen. */
|
|
1006
|
+
const seededImportReport: ImportReportEntry[] = [
|
|
1007
|
+
{ path: 'Company Handbook 8f2c41d0.md', outcome: 'imported', pageId: uid(101), reason: null },
|
|
1008
|
+
{
|
|
1009
|
+
path: 'Company Handbook 8f2c41d0/Getting started 5b91aa02.md',
|
|
1010
|
+
outcome: 'imported',
|
|
1011
|
+
pageId: uid(103),
|
|
1012
|
+
reason: null,
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
path: 'Company Handbook 8f2c41d0/Tasks 71cc9e10.csv',
|
|
1016
|
+
outcome: 'imported',
|
|
1017
|
+
pageId: uid(110),
|
|
1018
|
+
// The one `imported` row that carries a reason, exactly as the server writes it: a CSV becomes
|
|
1019
|
+
// a database, and the column types it guessed are the thing somebody has to check.
|
|
1020
|
+
reason:
|
|
1021
|
+
'the first column, Name, became each row’s title; Status: read as a select with 3 choices, from 4 of 4 values; Due: read as a date; Done: read as a checkbox',
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
path: 'Company Handbook 8f2c41d0/Tasks 71cc9e10_all.csv',
|
|
1025
|
+
outcome: 'skipped',
|
|
1026
|
+
pageId: null,
|
|
1027
|
+
reason: 'a second view of the same database; its rows came from “Tasks 71cc9e10.csv”',
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
path: 'Company Handbook 8f2c41d0/diagram.png',
|
|
1031
|
+
outcome: 'skipped',
|
|
1032
|
+
pageId: null,
|
|
1033
|
+
reason: 'a picture, which an import cannot yet attach to a page',
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
path: '__MACOSX/._Company Handbook 8f2c41d0.md',
|
|
1037
|
+
outcome: 'skipped',
|
|
1038
|
+
pageId: null,
|
|
1039
|
+
reason: 'a file the operating system added to the archive',
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
path: 'Company Handbook 8f2c41d0/Expenses 04ab7731.md',
|
|
1043
|
+
outcome: 'failed',
|
|
1044
|
+
pageId: null,
|
|
1045
|
+
reason: 'its checksum does not match, so the file is damaged',
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
path: 'Archive/Old wiki 22b0cc71.md',
|
|
1049
|
+
outcome: 'failed',
|
|
1050
|
+
pageId: null,
|
|
1051
|
+
reason:
|
|
1052
|
+
'nothing in the archive is at this path, so the link to it in “Company Handbook” is now plain text',
|
|
1053
|
+
},
|
|
1054
|
+
]
|
|
1055
|
+
|
|
1056
|
+
const importJobs: ImportJob[] = [
|
|
1057
|
+
{
|
|
1058
|
+
id: uid(711),
|
|
1059
|
+
workspaceId: '' as ImportJob['workspaceId'],
|
|
1060
|
+
requestedBy: ME as ImportJob['requestedBy'],
|
|
1061
|
+
source: 'notion',
|
|
1062
|
+
targetId: uid(1),
|
|
1063
|
+
sourceFileId: uid(791),
|
|
1064
|
+
state: 'done',
|
|
1065
|
+
error: null,
|
|
1066
|
+
counts: countsOf(seededImportReport),
|
|
1067
|
+
report: seededImportReport,
|
|
1068
|
+
createdAt: iso(1728e5),
|
|
1069
|
+
finishedAt: iso(17274e4),
|
|
1070
|
+
},
|
|
1071
|
+
]
|
|
1072
|
+
|
|
1073
|
+
/** Move a queued export along by however long it has been sitting there. */
|
|
1074
|
+
function advanceExport(row: ExportJob) {
|
|
1075
|
+
if (row.state === 'done' || row.state === 'failed') return
|
|
1076
|
+
const elapsed = Date.now() - new Date(row.createdAt).getTime()
|
|
1077
|
+
if (elapsed < EXPORT_START_MS) return
|
|
1078
|
+
const total = row.counts.total || 12
|
|
1079
|
+
const seen = Math.min(total, Math.floor((elapsed - EXPORT_START_MS) / EXPORT_STEP_MS))
|
|
1080
|
+
// One page withheld, so the "some pages were left out" sentence is reachable in the demo.
|
|
1081
|
+
const skipped = Math.min(total > 4 ? 1 : 0, seen)
|
|
1082
|
+
row.counts = { total, done: seen - skipped, skipped, failed: 0 }
|
|
1083
|
+
row.state = seen >= total ? 'done' : 'running'
|
|
1084
|
+
if (row.state === 'done') {
|
|
1085
|
+
row.fileId = row.fileId ?? nextId()
|
|
1086
|
+
row.finishedAt = new Date().toISOString()
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Move a queued import along, and — when it lands — actually write the pages.
|
|
1092
|
+
*
|
|
1093
|
+
* The pages matter: an import whose report says three pages arrived and whose sidebar is unchanged
|
|
1094
|
+
* is a demo of the wrong thing, and "Open the space" would lead somewhere that has not moved.
|
|
1095
|
+
*/
|
|
1096
|
+
function advanceImport(row: ImportJob) {
|
|
1097
|
+
if (row.state === 'done' || row.state === 'failed') return
|
|
1098
|
+
const elapsed = Date.now() - new Date(row.createdAt).getTime()
|
|
1099
|
+
if (elapsed < EXPORT_START_MS) return
|
|
1100
|
+
if (elapsed < IMPORT_READ_MS) {
|
|
1101
|
+
row.state = 'running'
|
|
1102
|
+
return
|
|
1103
|
+
}
|
|
1104
|
+
const made = ['Company Handbook', 'Getting started', 'How we work'].map((title, at) => {
|
|
1105
|
+
const created = page(++seq, row.targetId, title, `z${at}`, null, {})
|
|
1106
|
+
created.id = uid(seq)
|
|
1107
|
+
created.createdAt = new Date().toISOString()
|
|
1108
|
+
created.updatedAt = created.createdAt
|
|
1109
|
+
pages.push(created)
|
|
1110
|
+
return created.id
|
|
1111
|
+
})
|
|
1112
|
+
row.report = [
|
|
1113
|
+
{ path: 'Company Handbook 8f2c41d0.md', outcome: 'imported', pageId: made[0] ?? null, reason: null },
|
|
1114
|
+
{
|
|
1115
|
+
path: 'Company Handbook 8f2c41d0/Getting started 5b91aa02.md',
|
|
1116
|
+
outcome: 'imported',
|
|
1117
|
+
pageId: made[1] ?? null,
|
|
1118
|
+
reason: null,
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
path: 'Company Handbook 8f2c41d0/How we work 90ff1a44.md',
|
|
1122
|
+
outcome: 'imported',
|
|
1123
|
+
pageId: made[2] ?? null,
|
|
1124
|
+
reason: null,
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
path: 'Company Handbook 8f2c41d0/logo.png',
|
|
1128
|
+
outcome: 'skipped',
|
|
1129
|
+
pageId: null,
|
|
1130
|
+
reason: 'a picture, which an import cannot yet attach to a page',
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
path: 'Company Handbook 8f2c41d0/Notes 04ab7731.md',
|
|
1134
|
+
outcome: 'failed',
|
|
1135
|
+
pageId: null,
|
|
1136
|
+
reason: 'its checksum does not match, so the file is damaged',
|
|
1137
|
+
},
|
|
1138
|
+
]
|
|
1139
|
+
row.counts = countsOf(row.report)
|
|
1140
|
+
row.state = 'done'
|
|
1141
|
+
row.finishedAt = new Date().toISOString()
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* The five starters, **in English only**.
|
|
1146
|
+
*
|
|
1147
|
+
* The real ones are constants on the server with a five-locale table beside them, resolved against
|
|
1148
|
+
* the reader's own locale — `server/services/templates.ts` explains why they cannot live in this
|
|
1149
|
+
* package's message bundle. This mock has no server and no principal, so it carries the English
|
|
1150
|
+
* names and nothing else: it is the demo interface, and a demo of the picker is a demo of the
|
|
1151
|
+
* shape rather than of the translation. Anything that needs to see a starter in Persian needs a
|
|
1152
|
+
* server.
|
|
1153
|
+
*/
|
|
1154
|
+
const STARTER_NAMES: Array<[string, string, string, string]> = [
|
|
1155
|
+
['meeting-notes', 'users', 'Meeting notes', 'Who was there, what was decided, and who does what next.'],
|
|
1156
|
+
[
|
|
1157
|
+
'decision-record',
|
|
1158
|
+
'flag',
|
|
1159
|
+
'Decision record',
|
|
1160
|
+
'One decision, why it was taken, and what it commits you to.',
|
|
1161
|
+
],
|
|
1162
|
+
[
|
|
1163
|
+
'requirements',
|
|
1164
|
+
'target',
|
|
1165
|
+
'Requirements',
|
|
1166
|
+
'What a piece of work has to do, and what it deliberately does not.',
|
|
1167
|
+
],
|
|
1168
|
+
[
|
|
1169
|
+
'retrospective',
|
|
1170
|
+
'refresh-cw',
|
|
1171
|
+
'Retrospective',
|
|
1172
|
+
'What went well, what got in the way, and what to change.',
|
|
1173
|
+
],
|
|
1174
|
+
['how-to', 'wrench', 'How-to', 'A task somebody can follow from start to finish.'],
|
|
1175
|
+
]
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* One template somebody in this workspace already saved, so the picker is not five shipped entries
|
|
1179
|
+
* and nothing else — the interesting half of the feature is the one a colleague made.
|
|
1180
|
+
*/
|
|
1181
|
+
const templateRows: Template[] = [
|
|
1182
|
+
{
|
|
1183
|
+
id: uid(700),
|
|
1184
|
+
workspaceId: '' as Template['workspaceId'],
|
|
1185
|
+
spaceId: uid(1),
|
|
1186
|
+
kind: 'page',
|
|
1187
|
+
key: null,
|
|
1188
|
+
builtIn: false,
|
|
1189
|
+
name: 'Weekly review',
|
|
1190
|
+
description: 'What moved this week, and what is stuck.',
|
|
1191
|
+
icon: 'calendar-days',
|
|
1192
|
+
doc: {
|
|
1193
|
+
type: 'doc',
|
|
1194
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Week {{week}} · {{author}}' }] }],
|
|
1195
|
+
},
|
|
1196
|
+
variables: [
|
|
1197
|
+
{
|
|
1198
|
+
name: 'week',
|
|
1199
|
+
label: 'Which week',
|
|
1200
|
+
type: 'text',
|
|
1201
|
+
options: [],
|
|
1202
|
+
default: null,
|
|
1203
|
+
required: true,
|
|
1204
|
+
},
|
|
1205
|
+
],
|
|
1206
|
+
createdBy: COLLEAGUE as Template['createdBy'],
|
|
1207
|
+
createdAt: iso(9e6),
|
|
1208
|
+
updatedAt: iso(9e6),
|
|
1209
|
+
},
|
|
1210
|
+
]
|
|
1211
|
+
|
|
1212
|
+
const templateChoice = (row: Template): TemplateChoice => ({
|
|
1213
|
+
id: row.id,
|
|
1214
|
+
key: row.key,
|
|
1215
|
+
builtIn: row.builtIn,
|
|
1216
|
+
kind: row.kind,
|
|
1217
|
+
spaceId: row.spaceId,
|
|
1218
|
+
name: row.name,
|
|
1219
|
+
description: row.description,
|
|
1220
|
+
icon: row.icon,
|
|
1221
|
+
variables: row.variables,
|
|
1222
|
+
updatedAt: row.updatedAt,
|
|
1223
|
+
})
|
|
1224
|
+
|
|
1225
|
+
/** The same single-pass, text-only substitution the server does — see `services/templates.ts`. */
|
|
1226
|
+
const fill = (text: string, values: Record<string, string>) =>
|
|
1227
|
+
text.replace(/\{\{\s*([a-z][a-z0-9_]*)\s*\}\}/g, (whole, name: string) => values[name] ?? whole)
|
|
1228
|
+
|
|
921
1229
|
return {
|
|
922
1230
|
spaces: {
|
|
923
1231
|
list: async ({ includeArchived = false }: { includeArchived?: boolean } = {}) =>
|
|
@@ -1380,6 +1688,290 @@ export function createMockQuireApi() {
|
|
|
1380
1688
|
},
|
|
1381
1689
|
},
|
|
1382
1690
|
|
|
1691
|
+
/**
|
|
1692
|
+
* Taking work out.
|
|
1693
|
+
*
|
|
1694
|
+
* `list` and `get` advance every job before answering, which is what makes the progress bar move
|
|
1695
|
+
* in `dev:mock` — there is no worker here, so the passage of time is the worker. `docx` is
|
|
1696
|
+
* refused exactly as the server refuses it, with the same shape of message: a mock that quietly
|
|
1697
|
+
* accepted it would make the one format the product does not have look like the one it does.
|
|
1698
|
+
*/
|
|
1699
|
+
exports: {
|
|
1700
|
+
start: async (input: { scope: ExportScope; targetId: string; format: ExportFormat }) => {
|
|
1701
|
+
if (input.format === 'docx')
|
|
1702
|
+
throw Object.assign(
|
|
1703
|
+
new Error(
|
|
1704
|
+
'Word export is not available yet. Export as HTML or PDF, both of which Word opens, ' +
|
|
1705
|
+
'or as Markdown to move the pages somewhere else.',
|
|
1706
|
+
),
|
|
1707
|
+
{ code: 'BAD_REQUEST' },
|
|
1708
|
+
)
|
|
1709
|
+
if (input.scope === 'space') {
|
|
1710
|
+
if (!spaces.some((s) => s.id === input.targetId)) throw notFound('Space')
|
|
1711
|
+
} else found(input.targetId)
|
|
1712
|
+
const row: ExportJob = {
|
|
1713
|
+
id: nextId(),
|
|
1714
|
+
workspaceId: '' as ExportJob['workspaceId'],
|
|
1715
|
+
requestedBy: ME as ExportJob['requestedBy'],
|
|
1716
|
+
scope: input.scope,
|
|
1717
|
+
targetId: input.targetId,
|
|
1718
|
+
format: input.format,
|
|
1719
|
+
state: 'queued',
|
|
1720
|
+
fileId: null,
|
|
1721
|
+
error: null,
|
|
1722
|
+
counts: { total: 0, done: 0, skipped: 0, failed: 0 },
|
|
1723
|
+
createdAt: new Date().toISOString(),
|
|
1724
|
+
finishedAt: null,
|
|
1725
|
+
}
|
|
1726
|
+
exportJobs.unshift(row)
|
|
1727
|
+
return { ...row, downloadUrl: null }
|
|
1728
|
+
},
|
|
1729
|
+
|
|
1730
|
+
get: async ({ jobId }: { jobId: string }) => {
|
|
1731
|
+
const row = exportJobs.find((j) => j.id === jobId)
|
|
1732
|
+
if (!row) throw notFound('Export')
|
|
1733
|
+
advanceExport(row)
|
|
1734
|
+
/*
|
|
1735
|
+
* The link is minted here and not stored, exactly as the server does it — the download is a
|
|
1736
|
+
* signed URL that expires, so a row carrying one would be a row carrying an address that
|
|
1737
|
+
* stops working. A PDF export hands back the same empty zip: there is nothing to render.
|
|
1738
|
+
*/
|
|
1739
|
+
return { ...row, downloadUrl: row.state === 'done' ? EMPTY_ZIP : null }
|
|
1740
|
+
},
|
|
1741
|
+
|
|
1742
|
+
list: async ({ limit = 20 }: { limit?: number } = {}) => {
|
|
1743
|
+
for (const row of exportJobs) advanceExport(row)
|
|
1744
|
+
return exportJobs.slice(0, limit).map((row) => ({ ...row }))
|
|
1745
|
+
},
|
|
1746
|
+
},
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Getting work in.
|
|
1750
|
+
*
|
|
1751
|
+
* `start` does not read the upload — there is no storage behind `dev:mock` and nothing to read —
|
|
1752
|
+
* but it does keep the two checks that decide whether the job may exist at all: the space is
|
|
1753
|
+
* real, and the archive is a file this mock knows about. The pages appear when the job lands,
|
|
1754
|
+
* so the sidebar changes underneath the report rather than the report claiming pages nobody has.
|
|
1755
|
+
*/
|
|
1756
|
+
imports: {
|
|
1757
|
+
start: async (input: { spaceId: string; source: ImportSource; fileId: string }) => {
|
|
1758
|
+
if (!spaces.some((s) => s.id === input.spaceId)) throw notFound('Space')
|
|
1759
|
+
const row: ImportJob = {
|
|
1760
|
+
id: nextId(),
|
|
1761
|
+
workspaceId: '' as ImportJob['workspaceId'],
|
|
1762
|
+
requestedBy: ME as ImportJob['requestedBy'],
|
|
1763
|
+
source: input.source,
|
|
1764
|
+
targetId: input.spaceId,
|
|
1765
|
+
sourceFileId: input.fileId,
|
|
1766
|
+
state: 'queued',
|
|
1767
|
+
error: null,
|
|
1768
|
+
counts: { total: 0, done: 0, skipped: 0, failed: 0 },
|
|
1769
|
+
report: [],
|
|
1770
|
+
createdAt: new Date().toISOString(),
|
|
1771
|
+
finishedAt: null,
|
|
1772
|
+
}
|
|
1773
|
+
importJobs.unshift(row)
|
|
1774
|
+
return { ...row }
|
|
1775
|
+
},
|
|
1776
|
+
|
|
1777
|
+
get: async ({ jobId }: { jobId: string }) => {
|
|
1778
|
+
const row = importJobs.find((j) => j.id === jobId)
|
|
1779
|
+
if (!row) throw notFound('Import')
|
|
1780
|
+
advanceImport(row)
|
|
1781
|
+
return { ...row }
|
|
1782
|
+
},
|
|
1783
|
+
|
|
1784
|
+
/** Without the reports, as the server answers it — a list of thousands-of-rows reports is not a list. */
|
|
1785
|
+
list: async ({ limit = 20 }: { limit?: number } = {}) => {
|
|
1786
|
+
for (const row of importJobs) advanceImport(row)
|
|
1787
|
+
return importJobs.slice(0, limit).map(({ report: _report, ...summary }) => summary)
|
|
1788
|
+
},
|
|
1789
|
+
},
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* What somebody writes with.
|
|
1793
|
+
*
|
|
1794
|
+
* The starters and the rows come back as one list with the override rule applied, exactly as the
|
|
1795
|
+
* server does it — a row carrying a starter's key stands in that starter's place rather than
|
|
1796
|
+
* appearing beside it, which is the whole shape a picker has to draw.
|
|
1797
|
+
*
|
|
1798
|
+
* `instantiate` makes a real page in the tree. It cannot write a *body*: there is no collab
|
|
1799
|
+
* service behind `dev:mock`, and a page's prose lives there rather than in this list. So the
|
|
1800
|
+
* title is substituted and the page appears where it should, and the body is the one thing a
|
|
1801
|
+
* demo of this feature cannot show.
|
|
1802
|
+
*/
|
|
1803
|
+
templates: {
|
|
1804
|
+
list: async ({
|
|
1805
|
+
kind = 'page',
|
|
1806
|
+
spaceId = null,
|
|
1807
|
+
}: {
|
|
1808
|
+
kind?: TemplateKind
|
|
1809
|
+
spaceId?: string | null
|
|
1810
|
+
} = {}) => {
|
|
1811
|
+
const rows = templateRows.filter(
|
|
1812
|
+
(row) => row.kind === kind && (row.spaceId === null || row.spaceId === spaceId),
|
|
1813
|
+
)
|
|
1814
|
+
const byKey = new Map(rows.filter((row) => row.key).map((row) => [row.key as string, row]))
|
|
1815
|
+
const out: TemplateChoice[] = []
|
|
1816
|
+
if (kind === 'page')
|
|
1817
|
+
for (const [key, icon, name, description] of STARTER_NAMES) {
|
|
1818
|
+
const override = byKey.get(key)
|
|
1819
|
+
out.push(
|
|
1820
|
+
override
|
|
1821
|
+
? templateChoice(override)
|
|
1822
|
+
: {
|
|
1823
|
+
id: null,
|
|
1824
|
+
key,
|
|
1825
|
+
builtIn: true,
|
|
1826
|
+
kind: 'page',
|
|
1827
|
+
spaceId: null,
|
|
1828
|
+
name,
|
|
1829
|
+
description,
|
|
1830
|
+
icon,
|
|
1831
|
+
variables: [],
|
|
1832
|
+
updatedAt: null,
|
|
1833
|
+
},
|
|
1834
|
+
)
|
|
1835
|
+
}
|
|
1836
|
+
for (const row of rows)
|
|
1837
|
+
if (!row.key || !STARTER_NAMES.some(([key]) => key === row.key)) out.push(templateChoice(row))
|
|
1838
|
+
return out
|
|
1839
|
+
},
|
|
1840
|
+
|
|
1841
|
+
get: async ({ templateId }: { templateId: string }) => {
|
|
1842
|
+
const row = templateRows.find((t) => t.id === templateId)
|
|
1843
|
+
if (!row) throw notFound('Template')
|
|
1844
|
+
return row
|
|
1845
|
+
},
|
|
1846
|
+
|
|
1847
|
+
createFromPage: async (input: {
|
|
1848
|
+
kind?: TemplateKind
|
|
1849
|
+
sourceId: string
|
|
1850
|
+
spaceId?: string | null
|
|
1851
|
+
name: string
|
|
1852
|
+
description?: string
|
|
1853
|
+
icon?: string | null
|
|
1854
|
+
variables?: TemplateVariable[]
|
|
1855
|
+
key?: string | null
|
|
1856
|
+
}) => {
|
|
1857
|
+
if (input.key && templateRows.some((t) => t.key === input.key))
|
|
1858
|
+
throw Object.assign(new Error('This workspace already has its own version of that template'), {
|
|
1859
|
+
code: 'CONFLICT',
|
|
1860
|
+
})
|
|
1861
|
+
const source = input.kind === 'space' ? null : found(input.sourceId)
|
|
1862
|
+
const row: Template = {
|
|
1863
|
+
id: nextId(),
|
|
1864
|
+
workspaceId: '' as Template['workspaceId'],
|
|
1865
|
+
spaceId: input.kind === 'space' ? null : (input.spaceId ?? null),
|
|
1866
|
+
kind: input.kind ?? 'page',
|
|
1867
|
+
key: input.key ?? null,
|
|
1868
|
+
builtIn: Boolean(input.key),
|
|
1869
|
+
name: input.name,
|
|
1870
|
+
description: input.description ?? '',
|
|
1871
|
+
icon: input.icon ?? null,
|
|
1872
|
+
// No collab service, so there is no body to read — the title stands in for the prose, which
|
|
1873
|
+
// is enough for the picker and honest about what a mock can know.
|
|
1874
|
+
doc:
|
|
1875
|
+
input.kind === 'space'
|
|
1876
|
+
? { pages: [] }
|
|
1877
|
+
: {
|
|
1878
|
+
type: 'doc',
|
|
1879
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text: source?.title ?? '' }] }],
|
|
1880
|
+
},
|
|
1881
|
+
variables: input.variables ?? [],
|
|
1882
|
+
createdBy: ME as Template['createdBy'],
|
|
1883
|
+
createdAt: new Date().toISOString(),
|
|
1884
|
+
updatedAt: new Date().toISOString(),
|
|
1885
|
+
}
|
|
1886
|
+
templateRows.push(row)
|
|
1887
|
+
return row
|
|
1888
|
+
},
|
|
1889
|
+
|
|
1890
|
+
update: async ({ templateId, ...patch }: { templateId: string } & Partial<Template>) => {
|
|
1891
|
+
const row = templateRows.find((t) => t.id === templateId)
|
|
1892
|
+
if (!row) throw notFound('Template')
|
|
1893
|
+
Object.assign(row, patch, { updatedAt: new Date().toISOString() })
|
|
1894
|
+
return row
|
|
1895
|
+
},
|
|
1896
|
+
|
|
1897
|
+
remove: async ({ templateId }: { templateId: string }) => {
|
|
1898
|
+
const at = templateRows.findIndex((t) => t.id === templateId)
|
|
1899
|
+
if (at < 0) throw notFound('Template')
|
|
1900
|
+
templateRows.splice(at, 1)
|
|
1901
|
+
return { ok: true as const }
|
|
1902
|
+
},
|
|
1903
|
+
|
|
1904
|
+
instantiate: async (input: {
|
|
1905
|
+
templateId?: string | null
|
|
1906
|
+
starterKey?: string | null
|
|
1907
|
+
spaceId?: string | null
|
|
1908
|
+
parentId?: string | null
|
|
1909
|
+
afterId?: string | null
|
|
1910
|
+
title?: string
|
|
1911
|
+
key?: string | null
|
|
1912
|
+
name?: string
|
|
1913
|
+
values?: Record<string, string>
|
|
1914
|
+
}): Promise<TemplateResult> => {
|
|
1915
|
+
const row = input.templateId
|
|
1916
|
+
? templateRows.find((t) => t.id === input.templateId)
|
|
1917
|
+
: templateRows.find((t) => t.key === input.starterKey)
|
|
1918
|
+
const starter = STARTER_NAMES.find(([key]) => key === input.starterKey)
|
|
1919
|
+
if (!row && !starter) throw notFound('Template')
|
|
1920
|
+
|
|
1921
|
+
const values: Record<string, string> = {
|
|
1922
|
+
date: new Date().toLocaleDateString(),
|
|
1923
|
+
time: new Date().toLocaleTimeString(),
|
|
1924
|
+
author: 'You',
|
|
1925
|
+
space: spaces.find((s) => s.id === input.spaceId)?.name ?? '',
|
|
1926
|
+
...(input.values ?? {}),
|
|
1927
|
+
}
|
|
1928
|
+
for (const variable of row?.variables ?? [])
|
|
1929
|
+
if (variable.required && !values[variable.name])
|
|
1930
|
+
throw Object.assign(new Error(`"${variable.label}" is needed before this can be made`), {
|
|
1931
|
+
code: 'BAD_REQUEST',
|
|
1932
|
+
})
|
|
1933
|
+
|
|
1934
|
+
if (row?.kind === 'space') {
|
|
1935
|
+
if (!input.key || !input.name)
|
|
1936
|
+
throw Object.assign(new Error('A space template needs a name and an address'), {
|
|
1937
|
+
code: 'BAD_REQUEST',
|
|
1938
|
+
})
|
|
1939
|
+
const space: Space = {
|
|
1940
|
+
id: nextId(),
|
|
1941
|
+
workspaceId: '' as Space['workspaceId'],
|
|
1942
|
+
key: input.key,
|
|
1943
|
+
name: fill(input.name, values),
|
|
1944
|
+
description: '',
|
|
1945
|
+
icon: null,
|
|
1946
|
+
visibility: 'open',
|
|
1947
|
+
homepageId: null,
|
|
1948
|
+
createdBy: ME as Space['createdBy'],
|
|
1949
|
+
createdAt: new Date().toISOString(),
|
|
1950
|
+
updatedAt: new Date().toISOString(),
|
|
1951
|
+
archivedAt: null,
|
|
1952
|
+
}
|
|
1953
|
+
spaces.push(space)
|
|
1954
|
+
return { spaceId: space.id, pageId: null, pageCount: 0 }
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
const spaceId = input.spaceId
|
|
1958
|
+
if (!spaceId) throw Object.assign(new Error('A page template needs a space'), { code: 'BAD_REQUEST' })
|
|
1959
|
+
const siblings = pages
|
|
1960
|
+
.filter((p) => p.spaceId === spaceId && p.parentId === (input.parentId ?? null) && !p.deletedAt)
|
|
1961
|
+
.sort((a, b) => (a._order < b._order ? -1 : 1))
|
|
1962
|
+
const title = fill(input.title || row?.name || starter?.[2] || '', values)
|
|
1963
|
+
const created = page(++seq, spaceId, title, `${siblings.at(-1)?._order ?? 'a'}m`, null, {
|
|
1964
|
+
icon: row?.icon ?? starter?.[1] ?? null,
|
|
1965
|
+
})
|
|
1966
|
+
created.id = uid(seq)
|
|
1967
|
+
created.parentId = input.parentId ?? null
|
|
1968
|
+
created.createdAt = new Date().toISOString()
|
|
1969
|
+
created.updatedAt = created.createdAt
|
|
1970
|
+
pages.push(created)
|
|
1971
|
+
return { spaceId, pageId: created.id, pageCount: 1 }
|
|
1972
|
+
},
|
|
1973
|
+
},
|
|
1974
|
+
|
|
1383
1975
|
publishing: {
|
|
1384
1976
|
publish: async ({ pageId, label = null }: { pageId: string; label?: string | null }) => {
|
|
1385
1977
|
const row = found(pageId)
|