@crossworks/client-types 0.300.2 → 0.300.8
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/package.json +1 -1
- package/src/dto/member-apps.ts +23 -0
- package/src/dto/rows.ts +6 -6
- package/src/dto/workspaces.ts +25 -1
- package/src/index.ts +2 -0
- package/src/kinds.test.ts +239 -0
- package/src/kinds.ts +472 -0
- package/src/member-kinds.ts +25 -2
- package/src/search-query.ts +5 -19
- package/src/tree.ts +31 -109
package/package.json
CHANGED
package/src/dto/member-apps.ts
CHANGED
|
@@ -77,6 +77,29 @@ export type MemberHomeApp = {
|
|
|
77
77
|
export type MemberHomeData<THub = unknown> =
|
|
78
78
|
{ homeApp: MemberHomeApp; hub: THub } | { homeApp: null; hub: null };
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* GET /api/home?ws=<id>: the landing page of a workspace, for every login
|
|
82
|
+
* (task d78e7bde). Without `ws` it is "All my workspaces": the Built-in
|
|
83
|
+
* home over every workspace of the login, never a home app.
|
|
84
|
+
*
|
|
85
|
+
* `ws` echoes the workspace asked for (null: all). `homeApp` is the
|
|
86
|
+
* workspace's home app when the workspace still holds it, its published
|
|
87
|
+
* build is green and the login may run it; null means the Built-in home.
|
|
88
|
+
* `hub` is always there: the data of the Built-in home and what a home
|
|
89
|
+
* app's `host.hub.get()` answers, read in the login's scope and kept to
|
|
90
|
+
* what the workspace reads (sections are its newest pages, counts its items
|
|
91
|
+
* per kind, apps the other apps it holds that the login may run). A
|
|
92
|
+
* workspace the login may not name is 404 `{ error, code: 'not_found' }`.
|
|
93
|
+
*
|
|
94
|
+
* `THub` is `HubData` from `@mantle/share-ui/app-bridge-protocol` (see
|
|
95
|
+
* MemberHomeData).
|
|
96
|
+
*/
|
|
97
|
+
export type WorkspaceHomeData<THub = unknown> = {
|
|
98
|
+
ws: string | null;
|
|
99
|
+
homeApp: MemberHomeApp | null;
|
|
100
|
+
hub: THub;
|
|
101
|
+
};
|
|
102
|
+
|
|
80
103
|
// ── Admin: member chats ──────────────────────────────────────────────────
|
|
81
104
|
|
|
82
105
|
/** GET /api/team-admin/member-chats: one member login and its chat. */
|
package/src/dto/rows.ts
CHANGED
|
@@ -184,7 +184,8 @@ export type AppRow = {
|
|
|
184
184
|
* has never been shared / the share is revoked (owner-only).
|
|
185
185
|
*/
|
|
186
186
|
shareMode: ShareMode | null;
|
|
187
|
-
/** Whether this app is the
|
|
187
|
+
/** Whether this app is the home app of a workspace the reader sees (its
|
|
188
|
+
* landing page, set on the workspace screen; task d78e7bde). */
|
|
188
189
|
isHub: boolean;
|
|
189
190
|
/** A live open public link exists (not revoked, not expired): the
|
|
190
191
|
* "Public" pill. Absent from a brain before W5b2. */
|
|
@@ -567,11 +568,10 @@ export type ProfilePreferences = {
|
|
|
567
568
|
* group grant, so the switch can't be bypassed by a manifest change. Flip it
|
|
568
569
|
* from the Team admin surface. */
|
|
569
570
|
teamPrivateReads?: boolean;
|
|
570
|
-
/**
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
* member home is the fallback. No share link is involved. Read via
|
|
571
|
+
/** RETIRED (task d78e7bde, migration 0276): the brain-wide members' home
|
|
572
|
+
* app. 0276 made it the Team workspace's home app
|
|
573
|
+
* (workspaces.landing_app_id); nothing reads it any more. It stays in
|
|
574
|
+
* the stored preferences so a rollback finds it. Read via
|
|
575
575
|
* projectTeamHubAppId, never raw. */
|
|
576
576
|
teamHubAppId?: string;
|
|
577
577
|
/** The email account client sign-in codes are sent from (client logins
|
package/src/dto/workspaces.ts
CHANGED
|
@@ -92,16 +92,38 @@ export type WorkspaceDetailResponse = {
|
|
|
92
92
|
/** The assistant has history (threads or messages): it cannot be swapped
|
|
93
93
|
* or removed, only cloned to use elsewhere. */
|
|
94
94
|
hasHistory: boolean;
|
|
95
|
+
/** The workspace's landing page (task d78e7bde): its home app, or null
|
|
96
|
+
* for the Built-in home. Absent from an older brain. */
|
|
97
|
+
landingApp?: WorkspaceLandingApp | null;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A workspace's home app, as its screen shows it. `held`: the workspace
|
|
102
|
+
* still holds the app; false is a pointer left from before (the landing
|
|
103
|
+
* page shows the Built-in home), with no id or title: only a clear
|
|
104
|
+
* (`landingAppId: null`) makes sense then. `appId` and `title` are named
|
|
105
|
+
* only to a user of the workspace (an Admin user outside it gets null for
|
|
106
|
+
* both). `ready`: a green published build, so the landing page shows it.
|
|
107
|
+
*/
|
|
108
|
+
export type WorkspaceLandingApp = {
|
|
109
|
+
held: boolean;
|
|
110
|
+
appId: string | null;
|
|
111
|
+
title: string | null;
|
|
112
|
+
ready: boolean;
|
|
95
113
|
};
|
|
96
114
|
|
|
97
115
|
/** PATCH /api/workspaces/:id. Name and description: a Moderator or an Admin
|
|
98
116
|
* user. Contact and assistant: an Admin user (assistant: area
|
|
99
|
-
* 'assistants'). `
|
|
117
|
+
* 'assistants'). The landing page (`landingAppId`): a Moderator of this
|
|
118
|
+
* workspace only, to an app the workspace holds with a green published
|
|
119
|
+
* build (409 `landing_app_not_held` or `landing_app_not_ready`). `null`
|
|
120
|
+
* clears (the Built-in home). Answers the updated Workspace. */
|
|
100
121
|
export type WorkspacePatchBody = {
|
|
101
122
|
name?: string;
|
|
102
123
|
description?: string;
|
|
103
124
|
contactNodeId?: string | null;
|
|
104
125
|
assistantId?: string | null;
|
|
126
|
+
landingAppId?: string | null;
|
|
105
127
|
};
|
|
106
128
|
|
|
107
129
|
/** GET /api/workspaces/:id/archive-preview: what archiving would do.
|
|
@@ -203,6 +225,8 @@ export type WorkspaceErrorCode =
|
|
|
203
225
|
| 'assistant_has_history'
|
|
204
226
|
| 'assistant_in_use'
|
|
205
227
|
| 'resource_exists'
|
|
228
|
+
| 'landing_app_not_held'
|
|
229
|
+
| 'landing_app_not_ready'
|
|
206
230
|
| 'rate_limited';
|
|
207
231
|
|
|
208
232
|
export type WorkspaceError = { error: string; code: WorkspaceErrorCode };
|
package/src/index.ts
CHANGED
|
@@ -94,6 +94,7 @@ export type {
|
|
|
94
94
|
MemberChatsResponse,
|
|
95
95
|
MemberHomeApp,
|
|
96
96
|
MemberHomeData,
|
|
97
|
+
WorkspaceHomeData,
|
|
97
98
|
} from './dto/member-apps';
|
|
98
99
|
export type {
|
|
99
100
|
MemberInviteAccepted,
|
|
@@ -192,6 +193,7 @@ export type {
|
|
|
192
193
|
WorkspaceDetailResponse,
|
|
193
194
|
WorkspaceError,
|
|
194
195
|
WorkspaceErrorCode,
|
|
196
|
+
WorkspaceLandingApp,
|
|
195
197
|
WorkspaceListResponse,
|
|
196
198
|
WorkspacePatchBody,
|
|
197
199
|
WorkspaceResource,
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kind registry (workspaces arch A1) and the lists of this package that
|
|
3
|
+
* derive from it. Each derived list is pinned to its value before the
|
|
4
|
+
* registry (v0.300.2): a change to who may share, search or see a kind is a
|
|
5
|
+
* reviewed edit of the pin below, never a side effect of a registry field.
|
|
6
|
+
*
|
|
7
|
+
* A list whose order is a contract (the tree's) is pinned in order; a list
|
|
8
|
+
* the code only asks membership of is pinned as a set (it is now in schema
|
|
9
|
+
* order). The parity with SQL (the enum, mantle_kind_class) is a db test:
|
|
10
|
+
* packages/db/src/kind-registry.db.test.ts.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, it } from 'vitest';
|
|
13
|
+
import { ITEM_KINDS, ITEM_KIND_NAMES, kindClassOf, kindSpecOf, kindsWhere } from './kinds';
|
|
14
|
+
import { TREE_KIND_SPECS, TREE_KINDS } from './tree';
|
|
15
|
+
import { SEARCH_NODE_TYPES } from './search-query';
|
|
16
|
+
import { CLIENT_ITEM_KINDS, MEMBER_ITEM_KINDS } from './member-kinds';
|
|
17
|
+
|
|
18
|
+
const set = (xs: readonly string[]) => [...xs].sort();
|
|
19
|
+
|
|
20
|
+
describe('the kind registry', () => {
|
|
21
|
+
it('names every node_type value once, in the schema order', () => {
|
|
22
|
+
// The enum (packages/db/src/schema/nodes.ts); the db test checks the
|
|
23
|
+
// live enum_range too.
|
|
24
|
+
expect(ITEM_KIND_NAMES).toEqual([
|
|
25
|
+
'branch',
|
|
26
|
+
'email',
|
|
27
|
+
'email_thread',
|
|
28
|
+
'file',
|
|
29
|
+
'note',
|
|
30
|
+
'sermon',
|
|
31
|
+
'contact',
|
|
32
|
+
'secret',
|
|
33
|
+
'task',
|
|
34
|
+
'event',
|
|
35
|
+
'printer_project',
|
|
36
|
+
'telegram_message',
|
|
37
|
+
'page',
|
|
38
|
+
'table',
|
|
39
|
+
'mantle_peer',
|
|
40
|
+
'documentation',
|
|
41
|
+
'journal',
|
|
42
|
+
'location',
|
|
43
|
+
'app',
|
|
44
|
+
'formula',
|
|
45
|
+
'draw',
|
|
46
|
+
'recall',
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('classes: the workspace kinds, one conversation kind, the rest Admin only', () => {
|
|
51
|
+
expect(set(kindsWhere((s) => s.class === 'workspace'))).toEqual(
|
|
52
|
+
set([
|
|
53
|
+
'branch',
|
|
54
|
+
'file',
|
|
55
|
+
'note',
|
|
56
|
+
'page',
|
|
57
|
+
'table',
|
|
58
|
+
'app',
|
|
59
|
+
'formula',
|
|
60
|
+
'draw',
|
|
61
|
+
'task',
|
|
62
|
+
'event',
|
|
63
|
+
'recall',
|
|
64
|
+
]),
|
|
65
|
+
);
|
|
66
|
+
expect(kindsWhere((s) => s.class === 'conversation')).toEqual(['telegram_message']);
|
|
67
|
+
expect(kindClassOf('journal')).toBe('admin_only');
|
|
68
|
+
expect(kindClassOf('no-such-kind')).toBe('admin_only');
|
|
69
|
+
expect(kindSpecOf('no-such-kind')).toBeNull();
|
|
70
|
+
expect(kindSpecOf('toString')).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('an Admin-only kind is never linked, shared with a contact or a folder share', () => {
|
|
74
|
+
for (const t of ITEM_KIND_NAMES) {
|
|
75
|
+
const s = ITEM_KINDS[t];
|
|
76
|
+
if (s.class !== 'admin_only') continue;
|
|
77
|
+
expect(s.publicLink, t).toBe(false);
|
|
78
|
+
expect(s.contactShare, t).toBe(false);
|
|
79
|
+
expect(s.tree?.shareable ?? false, t).toBe(false);
|
|
80
|
+
expect(s.library, t).toBe(false);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('tree orders are unique and dense', () => {
|
|
85
|
+
const trees = ITEM_KIND_NAMES.flatMap((t) => {
|
|
86
|
+
const tree = ITEM_KINDS[t].tree as { order: number; liveOrder: number } | null;
|
|
87
|
+
return tree ? [tree] : [];
|
|
88
|
+
});
|
|
89
|
+
const n = trees.length;
|
|
90
|
+
expect(set(trees.map((t) => String(t.order)))).toEqual(set([...Array(n).keys()].map(String)));
|
|
91
|
+
expect(set(trees.map((t) => String(t.liveOrder)))).toEqual(
|
|
92
|
+
set([...Array(n).keys()].map(String)),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('route prefixes are unique', () => {
|
|
97
|
+
const content = ITEM_KIND_NAMES.map((t) => ITEM_KINDS[t].route.content).filter(Boolean);
|
|
98
|
+
expect(new Set(content).size).toBe(content.length);
|
|
99
|
+
const v1 = ITEM_KIND_NAMES.map((t) => ITEM_KINDS[t].route.v1).filter(Boolean);
|
|
100
|
+
expect(new Set(v1).size).toBe(v1.length);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('derived lists, pinned to their value before the registry', () => {
|
|
105
|
+
it('TREE_KINDS keeps its order (a jackdaw contract)', () => {
|
|
106
|
+
expect(TREE_KINDS).toEqual([
|
|
107
|
+
'files',
|
|
108
|
+
'notes',
|
|
109
|
+
'pages',
|
|
110
|
+
'draw',
|
|
111
|
+
'tables',
|
|
112
|
+
'formulas',
|
|
113
|
+
'apps',
|
|
114
|
+
'tasks',
|
|
115
|
+
'events',
|
|
116
|
+
'contacts',
|
|
117
|
+
'secrets',
|
|
118
|
+
'recall',
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('TREE_KIND_SPECS: every spec as before, keyed in navigation order', () => {
|
|
123
|
+
expect(Object.keys(TREE_KIND_SPECS)).toEqual([...TREE_KINDS]);
|
|
124
|
+
expect(TREE_KIND_SPECS).toEqual({
|
|
125
|
+
files: {
|
|
126
|
+
kind: 'files',
|
|
127
|
+
root: 'files',
|
|
128
|
+
nodeType: 'file',
|
|
129
|
+
shareable: true,
|
|
130
|
+
sorts: ['name', 'updated'],
|
|
131
|
+
},
|
|
132
|
+
notes: {
|
|
133
|
+
kind: 'notes',
|
|
134
|
+
root: 'notes',
|
|
135
|
+
nodeType: 'note',
|
|
136
|
+
shareable: true,
|
|
137
|
+
sorts: ['updated', 'name'],
|
|
138
|
+
},
|
|
139
|
+
pages: {
|
|
140
|
+
kind: 'pages',
|
|
141
|
+
root: 'pages',
|
|
142
|
+
nodeType: 'page',
|
|
143
|
+
shareable: true,
|
|
144
|
+
sorts: ['updated', 'name'],
|
|
145
|
+
},
|
|
146
|
+
draw: {
|
|
147
|
+
kind: 'draw',
|
|
148
|
+
root: 'draw',
|
|
149
|
+
nodeType: 'draw',
|
|
150
|
+
shareable: true,
|
|
151
|
+
sorts: ['updated', 'name'],
|
|
152
|
+
},
|
|
153
|
+
tables: {
|
|
154
|
+
kind: 'tables',
|
|
155
|
+
root: 'tables',
|
|
156
|
+
nodeType: 'table',
|
|
157
|
+
shareable: true,
|
|
158
|
+
sorts: ['updated', 'name'],
|
|
159
|
+
},
|
|
160
|
+
formulas: {
|
|
161
|
+
kind: 'formulas',
|
|
162
|
+
root: 'formulas',
|
|
163
|
+
nodeType: 'formula',
|
|
164
|
+
shareable: true,
|
|
165
|
+
sorts: ['name', 'updated'],
|
|
166
|
+
},
|
|
167
|
+
apps: {
|
|
168
|
+
kind: 'apps',
|
|
169
|
+
root: 'apps',
|
|
170
|
+
nodeType: 'app',
|
|
171
|
+
shareable: true,
|
|
172
|
+
sorts: ['name', 'updated'],
|
|
173
|
+
},
|
|
174
|
+
tasks: {
|
|
175
|
+
kind: 'tasks',
|
|
176
|
+
root: 'tasks',
|
|
177
|
+
nodeType: 'task',
|
|
178
|
+
shareable: false,
|
|
179
|
+
sorts: ['due', 'updated', 'name'],
|
|
180
|
+
},
|
|
181
|
+
events: {
|
|
182
|
+
kind: 'events',
|
|
183
|
+
root: 'events',
|
|
184
|
+
nodeType: 'event',
|
|
185
|
+
shareable: false,
|
|
186
|
+
sorts: ['start', 'name'],
|
|
187
|
+
},
|
|
188
|
+
contacts: {
|
|
189
|
+
kind: 'contacts',
|
|
190
|
+
root: 'contacts',
|
|
191
|
+
nodeType: 'contact',
|
|
192
|
+
shareable: false,
|
|
193
|
+
sorts: ['name', 'updated'],
|
|
194
|
+
},
|
|
195
|
+
secrets: {
|
|
196
|
+
kind: 'secrets',
|
|
197
|
+
root: 'secrets',
|
|
198
|
+
nodeType: 'secret',
|
|
199
|
+
shareable: false,
|
|
200
|
+
sorts: ['name', 'updated'],
|
|
201
|
+
},
|
|
202
|
+
recall: {
|
|
203
|
+
kind: 'recall',
|
|
204
|
+
root: 'recall',
|
|
205
|
+
nodeType: 'recall',
|
|
206
|
+
shareable: false,
|
|
207
|
+
sorts: ['name', 'updated'],
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('SEARCH_NODE_TYPES (a set)', () => {
|
|
213
|
+
expect(set(SEARCH_NODE_TYPES)).toEqual(
|
|
214
|
+
set([
|
|
215
|
+
'branch',
|
|
216
|
+
'email',
|
|
217
|
+
'email_thread',
|
|
218
|
+
'file',
|
|
219
|
+
'note',
|
|
220
|
+
'page',
|
|
221
|
+
'sermon',
|
|
222
|
+
'contact',
|
|
223
|
+
'task',
|
|
224
|
+
'event',
|
|
225
|
+
'printer_project',
|
|
226
|
+
'telegram_message',
|
|
227
|
+
'documentation',
|
|
228
|
+
'journal',
|
|
229
|
+
'formula',
|
|
230
|
+
'draw',
|
|
231
|
+
]),
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('MEMBER_ITEM_KINDS and CLIENT_ITEM_KINDS keep their order (deprecated, one pair release)', () => {
|
|
236
|
+
expect(MEMBER_ITEM_KINDS).toEqual(['page', 'note', 'draw', 'table', 'file']);
|
|
237
|
+
expect(CLIENT_ITEM_KINDS).toEqual(['page', 'note', 'file']);
|
|
238
|
+
});
|
|
239
|
+
});
|
package/src/kinds.ts
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kind registry (workspaces arch plan v2, section 9; A1): ONE entry per
|
|
3
|
+
* value of the `node_type` enum (packages/db/src/schema/nodes.ts). Every other
|
|
4
|
+
* TS kind list is derived from it, never copied:
|
|
5
|
+
*
|
|
6
|
+
* - WORKSPACE_NODE_TYPES (@mantle/db), the class;
|
|
7
|
+
* - TREE_KINDS and TREE_KIND_SPECS (./tree), TREE_LIVE_KINDS and
|
|
8
|
+
* READER_TREE_KINDS (@mantle/content/tree), TREE_TOOL_KINDS
|
|
9
|
+
* (@mantle/tools), the tree entry;
|
|
10
|
+
* - SHAREABLE_TYPES and canShareNode, CONTACT_SHAREABLE_TYPES and
|
|
11
|
+
* PEER_SHAREABLE_TYPES (@mantle/content), the share fields;
|
|
12
|
+
* - SEARCH_NODE_TYPES (./search-query), searchable;
|
|
13
|
+
* - TEAM_PRIVATE_CORPUS_TYPES and TEAM_NEVER_TYPES
|
|
14
|
+
* (@mantle/content-core/profile-projections), teamHidden;
|
|
15
|
+
* - MEMBER_ITEM_KINDS and CLIENT_ITEM_KINDS (./member-kinds), library: kept
|
|
16
|
+
* one pair release, then they go with the field.
|
|
17
|
+
*
|
|
18
|
+
* Each field holds the value the code had when the registry came in (A1).
|
|
19
|
+
* Who may share or see a kind changes here only as a reviewed diff of the
|
|
20
|
+
* snapshot tests (kinds.test.ts and the tests next to each derived list),
|
|
21
|
+
* never as a side effect of another list.
|
|
22
|
+
*
|
|
23
|
+
* `class` must equal mantle_kind_class(t) in SQL (migration 0241); a db test
|
|
24
|
+
* checks it, and that the enum and these keys are the same set. The keys are
|
|
25
|
+
* in the order of the Drizzle enum (schema/nodes.ts); a list derived here
|
|
26
|
+
* keeps that order unless it says otherwise (the tree lists and the old
|
|
27
|
+
* Library lists keep their own).
|
|
28
|
+
*
|
|
29
|
+
* Pure and dependency free: the `@mantle/client-types/kinds` subpath, so
|
|
30
|
+
* jackdaw reads the same registry.
|
|
31
|
+
*/
|
|
32
|
+
import type { TreeSort } from './tree';
|
|
33
|
+
|
|
34
|
+
/** 'workspace': granted to any workspace. 'conversation': belongs to the
|
|
35
|
+
* workspace of the assistant that holds the chat. 'admin_only': lives in the
|
|
36
|
+
* Admin workspace only. */
|
|
37
|
+
export type KindClass = 'workspace' | 'conversation' | 'admin_only';
|
|
38
|
+
|
|
39
|
+
/** A kind's place in the item tree. */
|
|
40
|
+
export type KindTreeSpec = {
|
|
41
|
+
/** The tree kind (the URL segment, /api/tree/:kind). */
|
|
42
|
+
readonly kind: string;
|
|
43
|
+
/** The ltree root label of the kind (the `*_ROOT_LABEL` constants). */
|
|
44
|
+
readonly root: string;
|
|
45
|
+
/** Navigation order (TREE_KINDS). A jackdaw contract: never reorder. */
|
|
46
|
+
readonly order: number;
|
|
47
|
+
/** The order the brain lists the kinds it serves (TREE_LIVE_KINDS, the
|
|
48
|
+
* shell's `treeKinds`, the tree tools' enum): apps joined after secrets. */
|
|
49
|
+
readonly liveOrder: number;
|
|
50
|
+
/** The item orders offered, the first being the default. */
|
|
51
|
+
readonly sorts: readonly TreeSort[];
|
|
52
|
+
/** Whether a folder of this kind may be shared below Admin. */
|
|
53
|
+
readonly shareable: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type KindSpec = {
|
|
57
|
+
readonly class: KindClass;
|
|
58
|
+
readonly tree: KindTreeSpec | null;
|
|
59
|
+
/** May carry a public link (SHAREABLE_TYPES). 'files-folder': a folder,
|
|
60
|
+
* only strictly under the `files` root (canShareNode). */
|
|
61
|
+
readonly publicLink: boolean | 'files-folder';
|
|
62
|
+
/** May be shared with one contact (CONTACT_SHAREABLE_TYPES). */
|
|
63
|
+
readonly contactShare: boolean;
|
|
64
|
+
/** May be granted to a linked brain by category (PEER_SHAREABLE_TYPES). */
|
|
65
|
+
readonly peerShare: boolean;
|
|
66
|
+
/** A `type` filter of GET /api/search (SEARCH_NODE_TYPES). */
|
|
67
|
+
readonly searchable: boolean;
|
|
68
|
+
/** The kind's item routes: the content prefix (`:id` follows it; the
|
|
69
|
+
* member route lists are generated from it, route-scope.ts) and the
|
|
70
|
+
* public API v1 prefix. Null where the kind has none. Nothing reads `v1`
|
|
71
|
+
* yet: A4 generates the content rows of ROUTE_POLICY from both (plan v2,
|
|
72
|
+
* section 7; D3, v1 answers as its base route). */
|
|
73
|
+
readonly route: { readonly content: string | null; readonly v1: string | null };
|
|
74
|
+
/** Hidden from a team surface: 'never' (opt-in or not), 'private-corpus'
|
|
75
|
+
* (unless the owner opted in, teamPrivateReads), or not. */
|
|
76
|
+
readonly teamHidden: 'never' | 'private-corpus' | false;
|
|
77
|
+
/** The old Library lists: in MEMBER_ITEM_KINDS at `order`, and in
|
|
78
|
+
* CLIENT_ITEM_KINDS too when `client`. Kept one pair release (A1), then
|
|
79
|
+
* removed with them. */
|
|
80
|
+
readonly library: { readonly order: number; readonly client: boolean } | false;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const none = { content: null, v1: null } as const;
|
|
84
|
+
|
|
85
|
+
export const ITEM_KINDS = {
|
|
86
|
+
branch: {
|
|
87
|
+
class: 'workspace',
|
|
88
|
+
tree: null,
|
|
89
|
+
publicLink: 'files-folder',
|
|
90
|
+
contactShare: false,
|
|
91
|
+
peerShare: false,
|
|
92
|
+
searchable: true,
|
|
93
|
+
route: { content: '/api/files/folders', v1: null },
|
|
94
|
+
teamHidden: false,
|
|
95
|
+
library: false,
|
|
96
|
+
},
|
|
97
|
+
email: {
|
|
98
|
+
class: 'admin_only',
|
|
99
|
+
tree: null,
|
|
100
|
+
publicLink: false,
|
|
101
|
+
contactShare: false,
|
|
102
|
+
peerShare: false,
|
|
103
|
+
searchable: true,
|
|
104
|
+
route: { content: '/api/email/messages', v1: null },
|
|
105
|
+
teamHidden: 'private-corpus',
|
|
106
|
+
library: false,
|
|
107
|
+
},
|
|
108
|
+
email_thread: {
|
|
109
|
+
class: 'admin_only',
|
|
110
|
+
tree: null,
|
|
111
|
+
publicLink: false,
|
|
112
|
+
contactShare: false,
|
|
113
|
+
peerShare: false,
|
|
114
|
+
searchable: true,
|
|
115
|
+
route: none,
|
|
116
|
+
teamHidden: 'private-corpus',
|
|
117
|
+
library: false,
|
|
118
|
+
},
|
|
119
|
+
file: {
|
|
120
|
+
class: 'workspace',
|
|
121
|
+
tree: {
|
|
122
|
+
kind: 'files',
|
|
123
|
+
root: 'files',
|
|
124
|
+
order: 0,
|
|
125
|
+
liveOrder: 0,
|
|
126
|
+
sorts: ['name', 'updated'],
|
|
127
|
+
shareable: true,
|
|
128
|
+
},
|
|
129
|
+
publicLink: true,
|
|
130
|
+
contactShare: true,
|
|
131
|
+
peerShare: true,
|
|
132
|
+
searchable: true,
|
|
133
|
+
route: { content: '/api/files/files', v1: '/api/v1/files' },
|
|
134
|
+
teamHidden: false,
|
|
135
|
+
library: { order: 4, client: true },
|
|
136
|
+
},
|
|
137
|
+
note: {
|
|
138
|
+
class: 'workspace',
|
|
139
|
+
tree: {
|
|
140
|
+
kind: 'notes',
|
|
141
|
+
root: 'notes',
|
|
142
|
+
order: 1,
|
|
143
|
+
liveOrder: 1,
|
|
144
|
+
sorts: ['updated', 'name'],
|
|
145
|
+
shareable: true,
|
|
146
|
+
},
|
|
147
|
+
publicLink: true,
|
|
148
|
+
contactShare: true,
|
|
149
|
+
peerShare: true,
|
|
150
|
+
searchable: true,
|
|
151
|
+
route: { content: '/api/notes', v1: '/api/v1/notes' },
|
|
152
|
+
teamHidden: false,
|
|
153
|
+
library: { order: 1, client: true },
|
|
154
|
+
},
|
|
155
|
+
sermon: {
|
|
156
|
+
class: 'admin_only',
|
|
157
|
+
tree: null,
|
|
158
|
+
publicLink: false,
|
|
159
|
+
contactShare: false,
|
|
160
|
+
peerShare: false,
|
|
161
|
+
searchable: true,
|
|
162
|
+
route: none,
|
|
163
|
+
teamHidden: false,
|
|
164
|
+
library: false,
|
|
165
|
+
},
|
|
166
|
+
contact: {
|
|
167
|
+
class: 'admin_only',
|
|
168
|
+
tree: {
|
|
169
|
+
kind: 'contacts',
|
|
170
|
+
root: 'contacts',
|
|
171
|
+
order: 9,
|
|
172
|
+
liveOrder: 8,
|
|
173
|
+
sorts: ['name', 'updated'],
|
|
174
|
+
shareable: false,
|
|
175
|
+
},
|
|
176
|
+
publicLink: false,
|
|
177
|
+
contactShare: false,
|
|
178
|
+
peerShare: true,
|
|
179
|
+
searchable: true,
|
|
180
|
+
route: { content: '/api/contacts', v1: '/api/v1/contacts' },
|
|
181
|
+
teamHidden: false,
|
|
182
|
+
library: false,
|
|
183
|
+
},
|
|
184
|
+
secret: {
|
|
185
|
+
class: 'admin_only',
|
|
186
|
+
tree: {
|
|
187
|
+
kind: 'secrets',
|
|
188
|
+
root: 'secrets',
|
|
189
|
+
order: 10,
|
|
190
|
+
liveOrder: 9,
|
|
191
|
+
sorts: ['name', 'updated'],
|
|
192
|
+
shareable: false,
|
|
193
|
+
},
|
|
194
|
+
publicLink: false,
|
|
195
|
+
contactShare: false,
|
|
196
|
+
peerShare: false,
|
|
197
|
+
searchable: false,
|
|
198
|
+
route: { content: '/api/secrets', v1: null },
|
|
199
|
+
teamHidden: 'never',
|
|
200
|
+
library: false,
|
|
201
|
+
},
|
|
202
|
+
task: {
|
|
203
|
+
class: 'workspace',
|
|
204
|
+
tree: {
|
|
205
|
+
kind: 'tasks',
|
|
206
|
+
root: 'tasks',
|
|
207
|
+
order: 7,
|
|
208
|
+
liveOrder: 6,
|
|
209
|
+
sorts: ['due', 'updated', 'name'],
|
|
210
|
+
shareable: false,
|
|
211
|
+
},
|
|
212
|
+
publicLink: true,
|
|
213
|
+
contactShare: false,
|
|
214
|
+
peerShare: true,
|
|
215
|
+
searchable: true,
|
|
216
|
+
route: { content: '/api/tasks', v1: '/api/v1/tasks' },
|
|
217
|
+
teamHidden: false,
|
|
218
|
+
library: false,
|
|
219
|
+
},
|
|
220
|
+
event: {
|
|
221
|
+
class: 'workspace',
|
|
222
|
+
tree: {
|
|
223
|
+
kind: 'events',
|
|
224
|
+
root: 'events',
|
|
225
|
+
order: 8,
|
|
226
|
+
liveOrder: 7,
|
|
227
|
+
sorts: ['start', 'name'],
|
|
228
|
+
shareable: false,
|
|
229
|
+
},
|
|
230
|
+
publicLink: true,
|
|
231
|
+
contactShare: false,
|
|
232
|
+
peerShare: true,
|
|
233
|
+
searchable: true,
|
|
234
|
+
route: { content: '/api/events', v1: '/api/v1/events' },
|
|
235
|
+
teamHidden: false,
|
|
236
|
+
library: false,
|
|
237
|
+
},
|
|
238
|
+
printer_project: {
|
|
239
|
+
class: 'admin_only',
|
|
240
|
+
tree: null,
|
|
241
|
+
publicLink: false,
|
|
242
|
+
contactShare: false,
|
|
243
|
+
peerShare: false,
|
|
244
|
+
searchable: true,
|
|
245
|
+
route: none,
|
|
246
|
+
teamHidden: false,
|
|
247
|
+
library: false,
|
|
248
|
+
},
|
|
249
|
+
telegram_message: {
|
|
250
|
+
class: 'conversation',
|
|
251
|
+
tree: null,
|
|
252
|
+
publicLink: false,
|
|
253
|
+
contactShare: false,
|
|
254
|
+
peerShare: false,
|
|
255
|
+
searchable: true,
|
|
256
|
+
route: none,
|
|
257
|
+
teamHidden: 'never',
|
|
258
|
+
library: false,
|
|
259
|
+
},
|
|
260
|
+
page: {
|
|
261
|
+
class: 'workspace',
|
|
262
|
+
tree: {
|
|
263
|
+
kind: 'pages',
|
|
264
|
+
root: 'pages',
|
|
265
|
+
order: 2,
|
|
266
|
+
liveOrder: 2,
|
|
267
|
+
sorts: ['updated', 'name'],
|
|
268
|
+
shareable: true,
|
|
269
|
+
},
|
|
270
|
+
publicLink: true,
|
|
271
|
+
contactShare: true,
|
|
272
|
+
peerShare: true,
|
|
273
|
+
searchable: true,
|
|
274
|
+
route: { content: '/api/pages', v1: '/api/v1/pages' },
|
|
275
|
+
teamHidden: false,
|
|
276
|
+
library: { order: 0, client: true },
|
|
277
|
+
},
|
|
278
|
+
table: {
|
|
279
|
+
class: 'workspace',
|
|
280
|
+
tree: {
|
|
281
|
+
kind: 'tables',
|
|
282
|
+
root: 'tables',
|
|
283
|
+
order: 4,
|
|
284
|
+
liveOrder: 4,
|
|
285
|
+
sorts: ['updated', 'name'],
|
|
286
|
+
shareable: true,
|
|
287
|
+
},
|
|
288
|
+
publicLink: true,
|
|
289
|
+
contactShare: true,
|
|
290
|
+
peerShare: true,
|
|
291
|
+
searchable: false,
|
|
292
|
+
route: { content: '/api/tables', v1: '/api/v1/tables' },
|
|
293
|
+
teamHidden: false,
|
|
294
|
+
library: { order: 3, client: false },
|
|
295
|
+
},
|
|
296
|
+
mantle_peer: {
|
|
297
|
+
class: 'admin_only',
|
|
298
|
+
tree: null,
|
|
299
|
+
publicLink: false,
|
|
300
|
+
contactShare: false,
|
|
301
|
+
peerShare: false,
|
|
302
|
+
searchable: false,
|
|
303
|
+
route: none,
|
|
304
|
+
teamHidden: 'never',
|
|
305
|
+
library: false,
|
|
306
|
+
},
|
|
307
|
+
documentation: {
|
|
308
|
+
class: 'admin_only',
|
|
309
|
+
tree: null,
|
|
310
|
+
publicLink: false,
|
|
311
|
+
contactShare: false,
|
|
312
|
+
peerShare: false,
|
|
313
|
+
searchable: true,
|
|
314
|
+
route: { content: '/api/docs/reader', v1: null },
|
|
315
|
+
teamHidden: false,
|
|
316
|
+
library: false,
|
|
317
|
+
},
|
|
318
|
+
journal: {
|
|
319
|
+
class: 'admin_only',
|
|
320
|
+
tree: null,
|
|
321
|
+
publicLink: false,
|
|
322
|
+
contactShare: false,
|
|
323
|
+
peerShare: false,
|
|
324
|
+
searchable: true,
|
|
325
|
+
route: { content: '/api/journal', v1: '/api/v1/journal' },
|
|
326
|
+
teamHidden: 'private-corpus',
|
|
327
|
+
library: false,
|
|
328
|
+
},
|
|
329
|
+
location: {
|
|
330
|
+
class: 'admin_only',
|
|
331
|
+
tree: null,
|
|
332
|
+
publicLink: false,
|
|
333
|
+
contactShare: false,
|
|
334
|
+
peerShare: false,
|
|
335
|
+
searchable: false,
|
|
336
|
+
route: none,
|
|
337
|
+
teamHidden: 'never',
|
|
338
|
+
library: false,
|
|
339
|
+
},
|
|
340
|
+
app: {
|
|
341
|
+
class: 'workspace',
|
|
342
|
+
tree: {
|
|
343
|
+
kind: 'apps',
|
|
344
|
+
root: 'apps',
|
|
345
|
+
order: 6,
|
|
346
|
+
liveOrder: 10,
|
|
347
|
+
sorts: ['name', 'updated'],
|
|
348
|
+
shareable: true,
|
|
349
|
+
},
|
|
350
|
+
publicLink: true,
|
|
351
|
+
contactShare: true,
|
|
352
|
+
peerShare: false,
|
|
353
|
+
searchable: false,
|
|
354
|
+
route: { content: '/api/apps', v1: null },
|
|
355
|
+
teamHidden: false,
|
|
356
|
+
library: false,
|
|
357
|
+
},
|
|
358
|
+
formula: {
|
|
359
|
+
class: 'workspace',
|
|
360
|
+
tree: {
|
|
361
|
+
kind: 'formulas',
|
|
362
|
+
root: 'formulas',
|
|
363
|
+
order: 5,
|
|
364
|
+
liveOrder: 5,
|
|
365
|
+
sorts: ['name', 'updated'],
|
|
366
|
+
shareable: true,
|
|
367
|
+
},
|
|
368
|
+
publicLink: true,
|
|
369
|
+
contactShare: true,
|
|
370
|
+
peerShare: false,
|
|
371
|
+
searchable: true,
|
|
372
|
+
route: { content: '/api/formulas', v1: null },
|
|
373
|
+
teamHidden: false,
|
|
374
|
+
library: false,
|
|
375
|
+
},
|
|
376
|
+
draw: {
|
|
377
|
+
class: 'workspace',
|
|
378
|
+
tree: {
|
|
379
|
+
kind: 'draw',
|
|
380
|
+
root: 'draw',
|
|
381
|
+
order: 3,
|
|
382
|
+
liveOrder: 3,
|
|
383
|
+
sorts: ['updated', 'name'],
|
|
384
|
+
shareable: true,
|
|
385
|
+
},
|
|
386
|
+
publicLink: true,
|
|
387
|
+
contactShare: true,
|
|
388
|
+
peerShare: true,
|
|
389
|
+
searchable: true,
|
|
390
|
+
route: { content: '/api/draws', v1: null },
|
|
391
|
+
teamHidden: false,
|
|
392
|
+
library: { order: 2, client: false },
|
|
393
|
+
},
|
|
394
|
+
recall: {
|
|
395
|
+
class: 'workspace',
|
|
396
|
+
// The tree lists maps only; a map's cards are recall_nodes rows, never
|
|
397
|
+
// nodes. Its folders are not shared below Admin yet (Recall R6).
|
|
398
|
+
tree: {
|
|
399
|
+
kind: 'recall',
|
|
400
|
+
root: 'recall',
|
|
401
|
+
order: 11,
|
|
402
|
+
liveOrder: 11,
|
|
403
|
+
sorts: ['name', 'updated'],
|
|
404
|
+
shareable: false,
|
|
405
|
+
},
|
|
406
|
+
publicLink: false,
|
|
407
|
+
contactShare: false,
|
|
408
|
+
peerShare: false,
|
|
409
|
+
searchable: false,
|
|
410
|
+
// A native map's id is its node's id (createRecallMap).
|
|
411
|
+
route: { content: '/api/recall/maps', v1: null },
|
|
412
|
+
teamHidden: false,
|
|
413
|
+
library: false,
|
|
414
|
+
},
|
|
415
|
+
} as const satisfies Record<string, KindSpec>;
|
|
416
|
+
|
|
417
|
+
type Kinds = typeof ITEM_KINDS;
|
|
418
|
+
|
|
419
|
+
/** Every node type, as the registry names it (the `node_type` enum). */
|
|
420
|
+
export type ItemKind = keyof Kinds;
|
|
421
|
+
|
|
422
|
+
/** The registry keys, in the Drizzle enum's order. */
|
|
423
|
+
export const ITEM_KIND_NAMES = Object.keys(ITEM_KINDS) as ItemKind[];
|
|
424
|
+
|
|
425
|
+
/** The node types whose spec matches `P` (a type-level filter). */
|
|
426
|
+
export type KindsWhere<P> = { [K in ItemKind]: Kinds[K] extends P ? K : never }[ItemKind];
|
|
427
|
+
|
|
428
|
+
/** The node types whose spec passes `pick`, in registry order. Never empty for
|
|
429
|
+
* the lists derived here (their snapshot tests say so). */
|
|
430
|
+
export function kindsWhere<K extends ItemKind = ItemKind>(
|
|
431
|
+
pick: (spec: KindSpec, type: ItemKind) => boolean,
|
|
432
|
+
): readonly [K, ...K[]] {
|
|
433
|
+
return ITEM_KIND_NAMES.filter((t) => pick(ITEM_KINDS[t], t)) as unknown as readonly [K, ...K[]];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The spec of `type`, or null for a value the registry does not know. */
|
|
437
|
+
export function kindSpecOf(type: string): KindSpec | null {
|
|
438
|
+
return Object.prototype.hasOwnProperty.call(ITEM_KINDS, type)
|
|
439
|
+
? ITEM_KINDS[type as ItemKind]
|
|
440
|
+
: null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** The class of `type` (mantle_kind_class in TS); an unknown value is
|
|
444
|
+
* 'admin_only', as the SQL's ELSE. */
|
|
445
|
+
export function kindClassOf(type: string): KindClass {
|
|
446
|
+
return kindSpecOf(type)?.class ?? 'admin_only';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ── Typed selections the derived lists use ──────────────────────────────────
|
|
450
|
+
|
|
451
|
+
export type WorkspaceKind = KindsWhere<{ class: 'workspace' }>;
|
|
452
|
+
export type PublicLinkKind = KindsWhere<{ publicLink: true | 'files-folder' }>;
|
|
453
|
+
export type ContactShareKind = KindsWhere<{ contactShare: true }>;
|
|
454
|
+
export type PeerShareKind = KindsWhere<{ peerShare: true }>;
|
|
455
|
+
export type SearchableKind = KindsWhere<{ searchable: true }>;
|
|
456
|
+
export type MemberLibraryKind = KindsWhere<{ library: { order: number } }>;
|
|
457
|
+
export type ClientLibraryKind = KindsWhere<{ library: { client: true } }>;
|
|
458
|
+
|
|
459
|
+
/** The tree kinds the registry names (`files`, `notes`, ...). */
|
|
460
|
+
export type RegistryTreeKind = {
|
|
461
|
+
[K in ItemKind]: Kinds[K]['tree'] extends { kind: infer T } ? T : never;
|
|
462
|
+
}[ItemKind];
|
|
463
|
+
|
|
464
|
+
/** The node types with a tree entry, ordered by `key` of that entry. */
|
|
465
|
+
export function treeKindsBy(key: 'order' | 'liveOrder'): readonly RegistryTreeKind[] {
|
|
466
|
+
return ITEM_KIND_NAMES.flatMap((t) => {
|
|
467
|
+
const tree: KindTreeSpec | null = ITEM_KINDS[t].tree;
|
|
468
|
+
return tree ? [tree] : [];
|
|
469
|
+
})
|
|
470
|
+
.sort((a, b) => a[key] - b[key])
|
|
471
|
+
.map((t) => t.kind as RegistryTreeKind);
|
|
472
|
+
}
|
package/src/member-kinds.ts
CHANGED
|
@@ -3,8 +3,31 @@
|
|
|
3
3
|
* what the Library lists. The ONE list, for the brain (member-space.ts,
|
|
4
4
|
* member-library.ts) and the client alike (audit M2); a runtime constant, so
|
|
5
5
|
* it is the `@mantle/client-types/member-kinds` subpath.
|
|
6
|
+
*
|
|
7
|
+
* DEPRECATED (workspaces arch A1): derived from the kind registry's `library`
|
|
8
|
+
* field (./kinds) and kept exported for one pair release, then removed. Use
|
|
9
|
+
* the registry.
|
|
6
10
|
*/
|
|
7
|
-
|
|
11
|
+
import {
|
|
12
|
+
ITEM_KINDS,
|
|
13
|
+
kindsWhere,
|
|
14
|
+
type ClientLibraryKind,
|
|
15
|
+
type ItemKind,
|
|
16
|
+
type MemberLibraryKind,
|
|
17
|
+
} from './kinds';
|
|
18
|
+
|
|
19
|
+
/** The registry's `library` kinds that pass `pick`, in the lists' own order. */
|
|
20
|
+
function libraryKinds<K extends ItemKind>(pick: (client: boolean) => boolean): readonly K[] {
|
|
21
|
+
const order = (t: ItemKind) => {
|
|
22
|
+
const lib = ITEM_KINDS[t].library as { order: number } | false;
|
|
23
|
+
return lib ? lib.order : 0;
|
|
24
|
+
};
|
|
25
|
+
return kindsWhere<K>((s) => s.library !== false && pick(s.library.client))
|
|
26
|
+
.slice()
|
|
27
|
+
.sort((a, b) => order(a) - order(b));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const MEMBER_ITEM_KINDS = libraryKinds<MemberLibraryKind>(() => true);
|
|
8
31
|
export type MemberItemKind = (typeof MEMBER_ITEM_KINDS)[number];
|
|
9
32
|
|
|
10
33
|
export function isMemberItemKind(v: unknown): v is MemberItemKind {
|
|
@@ -47,7 +70,7 @@ export function adminListStateOf(v: unknown): AdminListState {
|
|
|
47
70
|
|
|
48
71
|
/** The kinds a CLIENT works with in their own space (client logins C5, plan
|
|
49
72
|
* section 6): pages and notes they write, files they upload. */
|
|
50
|
-
export const CLIENT_ITEM_KINDS =
|
|
73
|
+
export const CLIENT_ITEM_KINDS = libraryKinds<ClientLibraryKind>((client) => client);
|
|
51
74
|
export type ClientItemKind = (typeof CLIENT_ITEM_KINDS)[number];
|
|
52
75
|
|
|
53
76
|
export function isClientItemKind(v: unknown): v is ClientItemKind {
|
package/src/search-query.ts
CHANGED
|
@@ -3,26 +3,12 @@
|
|
|
3
3
|
* the `search_nodes` / `search_chunks` MCP tools (mobile companion's search).
|
|
4
4
|
* Pure so it can be unit-tested without the route runtime.
|
|
5
5
|
*/
|
|
6
|
+
import { kindsWhere, type SearchableKind } from './kinds';
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'email_thread',
|
|
12
|
-
'file',
|
|
13
|
-
'note',
|
|
14
|
-
'page',
|
|
15
|
-
'sermon',
|
|
16
|
-
'contact',
|
|
17
|
-
'task',
|
|
18
|
-
'event',
|
|
19
|
-
'printer_project',
|
|
20
|
-
'telegram_message',
|
|
21
|
-
'documentation',
|
|
22
|
-
'journal',
|
|
23
|
-
'formula',
|
|
24
|
-
'draw',
|
|
25
|
-
] as const;
|
|
8
|
+
/** The `type` filters of GET /api/search: the kind registry's `searchable`
|
|
9
|
+
* (./kinds). Near the `search_nodes` tool's `type` enum, which also takes
|
|
10
|
+
* `secret` (an Admin-only tool argument). */
|
|
11
|
+
export const SEARCH_NODE_TYPES = kindsWhere<SearchableKind>((s) => s.searchable);
|
|
26
12
|
|
|
27
13
|
export type SearchNodeType = (typeof SEARCH_NODE_TYPES)[number];
|
|
28
14
|
|
package/src/tree.ts
CHANGED
|
@@ -15,25 +15,19 @@ import type { AccessLevel } from './dto/access';
|
|
|
15
15
|
import type { GrantWorkspaceRef, ListWorkspace } from './dto/grants';
|
|
16
16
|
import type { TaskStatus } from './dto/rows';
|
|
17
17
|
import type { AppTint } from './app-nav';
|
|
18
|
+
import {
|
|
19
|
+
ITEM_KINDS,
|
|
20
|
+
ITEM_KIND_NAMES,
|
|
21
|
+
treeKindsBy,
|
|
22
|
+
type KindTreeSpec,
|
|
23
|
+
type RegistryTreeKind,
|
|
24
|
+
} from './kinds';
|
|
18
25
|
|
|
19
|
-
/** The kinds the tree serves, in navigation order
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'draw',
|
|
25
|
-
'tables',
|
|
26
|
-
'formulas',
|
|
27
|
-
'apps',
|
|
28
|
-
'tasks',
|
|
29
|
-
'events',
|
|
30
|
-
'contacts',
|
|
31
|
-
'secrets',
|
|
32
|
-
// Recall v2's maps (its own content type, not pages). Appended last so no
|
|
33
|
-
// earlier kind's index moves. Served since the Recall screen (R3).
|
|
34
|
-
'recall',
|
|
35
|
-
] as const;
|
|
36
|
-
export type TreeKind = (typeof TREE_KINDS)[number];
|
|
26
|
+
/** The kinds the tree serves, in navigation order: derived from the kind
|
|
27
|
+
* registry (./kinds, each kind's `tree.order`). A jackdaw contract: the
|
|
28
|
+
* order never changes (Recall, appended last, kept every earlier index). */
|
|
29
|
+
export const TREE_KINDS = treeKindsBy('order') as readonly RegistryTreeKind[];
|
|
30
|
+
export type TreeKind = RegistryTreeKind;
|
|
37
31
|
|
|
38
32
|
export function isTreeKind(v: unknown): v is TreeKind {
|
|
39
33
|
return typeof v === 'string' && (TREE_KINDS as readonly string[]).includes(v);
|
|
@@ -77,97 +71,25 @@ export type TreeKindSpec = {
|
|
|
77
71
|
sorts: readonly TreeSort[];
|
|
78
72
|
};
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
shareable: true,
|
|
100
|
-
sorts: ['updated', 'name'],
|
|
101
|
-
},
|
|
102
|
-
draw: {
|
|
103
|
-
kind: 'draw',
|
|
104
|
-
root: 'draw',
|
|
105
|
-
nodeType: 'draw',
|
|
106
|
-
shareable: true,
|
|
107
|
-
sorts: ['updated', 'name'],
|
|
108
|
-
},
|
|
109
|
-
tables: {
|
|
110
|
-
kind: 'tables',
|
|
111
|
-
root: 'tables',
|
|
112
|
-
nodeType: 'table',
|
|
113
|
-
shareable: true,
|
|
114
|
-
sorts: ['updated', 'name'],
|
|
115
|
-
},
|
|
116
|
-
formulas: {
|
|
117
|
-
kind: 'formulas',
|
|
118
|
-
root: 'formulas',
|
|
119
|
-
nodeType: 'formula',
|
|
120
|
-
shareable: true,
|
|
121
|
-
sorts: ['name', 'updated'],
|
|
122
|
-
},
|
|
123
|
-
apps: {
|
|
124
|
-
kind: 'apps',
|
|
125
|
-
root: 'apps',
|
|
126
|
-
nodeType: 'app',
|
|
127
|
-
shareable: true,
|
|
128
|
-
sorts: ['name', 'updated'],
|
|
129
|
-
},
|
|
130
|
-
tasks: {
|
|
131
|
-
kind: 'tasks',
|
|
132
|
-
root: 'tasks',
|
|
133
|
-
nodeType: 'task',
|
|
134
|
-
shareable: false,
|
|
135
|
-
sorts: ['due', 'updated', 'name'],
|
|
136
|
-
},
|
|
137
|
-
events: {
|
|
138
|
-
kind: 'events',
|
|
139
|
-
root: 'events',
|
|
140
|
-
nodeType: 'event',
|
|
141
|
-
shareable: false,
|
|
142
|
-
sorts: ['start', 'name'],
|
|
143
|
-
},
|
|
144
|
-
contacts: {
|
|
145
|
-
kind: 'contacts',
|
|
146
|
-
root: 'contacts',
|
|
147
|
-
nodeType: 'contact',
|
|
148
|
-
shareable: false,
|
|
149
|
-
sorts: ['name', 'updated'],
|
|
150
|
-
},
|
|
151
|
-
secrets: {
|
|
152
|
-
kind: 'secrets',
|
|
153
|
-
root: 'secrets',
|
|
154
|
-
nodeType: 'secret',
|
|
155
|
-
shareable: false,
|
|
156
|
-
sorts: ['name', 'updated'],
|
|
157
|
-
},
|
|
158
|
-
// The tree lists maps only; a map's cards are recall_nodes rows, never
|
|
159
|
-
// nodes, so they never appear as items.
|
|
160
|
-
// Not shareable YET: a map is granted beyond Admin only once Recall R6
|
|
161
|
-
// lets a workspace's assistant read maps. Until then the tree must not
|
|
162
|
-
// offer it.
|
|
163
|
-
recall: {
|
|
164
|
-
kind: 'recall',
|
|
165
|
-
root: 'recall',
|
|
166
|
-
nodeType: 'recall',
|
|
167
|
-
shareable: false,
|
|
168
|
-
sorts: ['name', 'updated'],
|
|
169
|
-
},
|
|
170
|
-
};
|
|
74
|
+
/** Derived from the kind registry (./kinds): one spec per tree kind, keyed
|
|
75
|
+
* in navigation order. */
|
|
76
|
+
export const TREE_KIND_SPECS: Readonly<Record<TreeKind, TreeKindSpec>> = Object.fromEntries(
|
|
77
|
+
ITEM_KIND_NAMES.flatMap((nodeType) => {
|
|
78
|
+
const tree: KindTreeSpec | null = ITEM_KINDS[nodeType].tree;
|
|
79
|
+
return tree ? [{ tree, nodeType }] : [];
|
|
80
|
+
})
|
|
81
|
+
.sort((a, b) => a.tree.order - b.tree.order)
|
|
82
|
+
.map(({ tree, nodeType }): [string, TreeKindSpec] => [
|
|
83
|
+
tree.kind,
|
|
84
|
+
{
|
|
85
|
+
kind: tree.kind as TreeKind,
|
|
86
|
+
root: tree.root,
|
|
87
|
+
nodeType,
|
|
88
|
+
shareable: tree.shareable,
|
|
89
|
+
sorts: tree.sorts,
|
|
90
|
+
},
|
|
91
|
+
]),
|
|
92
|
+
) as Record<TreeKind, TreeKindSpec>;
|
|
171
93
|
|
|
172
94
|
/** One folder as the tree shows it. Counts are what THIS reader can see. */
|
|
173
95
|
export type TreeFolder = {
|