@notis_ai/cli 0.2.1 → 0.2.3
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/README.md +106 -170
- package/config/notis_app_boundary_rules.json +50 -0
- package/dist/scaffolds/notis-database/app/globals.css +44 -0
- package/dist/scaffolds/notis-database/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
- package/dist/scaffolds/notis-database/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-database/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-database/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
- package/dist/scaffolds/notis-database/components.json +20 -0
- package/dist/scaffolds/notis-database/index.html +12 -0
- package/dist/scaffolds/notis-database/lib/types.ts +134 -0
- package/dist/scaffolds/notis-database/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-database/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-database/notis.config.ts +27 -0
- package/dist/scaffolds/notis-database/package.json +31 -0
- package/dist/scaffolds/notis-database/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +557 -0
- package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
- package/dist/scaffolds/notis-database/tsconfig.json +23 -0
- package/dist/scaffolds/notis-database/vite.config.ts +22 -0
- package/dist/scaffolds/notis-notes/app/globals.css +3 -0
- package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-notes/app/page.tsx +2168 -0
- package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-notes/components.json +20 -0
- package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/notis.config.ts +36 -0
- package/dist/scaffolds/notis-notes/package.json +31 -0
- package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-notes/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
- package/dist/scaffolds/notis-notes/vite.config.ts +10 -0
- package/dist/scaffolds/notis-random/README.md +33 -0
- package/dist/scaffolds/notis-random/app/globals.css +11 -0
- package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
- package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
- package/dist/scaffolds/notis-random/app/page.tsx +222 -0
- package/dist/scaffolds/notis-random/index.html +12 -0
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
- package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
- package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
- package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
- package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/notis.config.ts +43 -0
- package/dist/scaffolds/notis-random/package.json +32 -0
- package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
- package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +128 -0
- package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
- package/dist/scaffolds/notis-random/tsconfig.json +23 -0
- package/dist/scaffolds/notis-random/vite.config.ts +11 -0
- package/dist/scaffolds.json +36 -0
- package/package.json +8 -3
- package/skills/notis-apps/SKILL.md +162 -0
- package/skills/notis-apps/cli.md +208 -0
- package/skills/notis-cli/SKILL.md +260 -0
- package/skills/notis-query/cli.md +39 -0
- package/src/cli.js +31 -3
- package/src/command-specs/apps.js +248 -56
- package/src/command-specs/helpers.js +72 -104
- package/src/command-specs/index.js +0 -6
- package/src/command-specs/meta.js +14 -13
- package/src/command-specs/tools.js +196 -115
- package/src/runtime/app-boundary-validator.js +65 -14
- package/src/runtime/app-dev-server.js +32 -4
- package/src/runtime/app-platform.js +404 -24
- package/src/runtime/profiles.js +12 -6
- package/src/runtime/transport.js +124 -39
- package/template/.harness/index.html.tmpl +1 -50
- package/template/app/page.tsx +41 -6
- package/template/metadata/cover.png +0 -0
- package/template/metadata/screenshot-1.png +0 -0
- package/template/metadata/screenshot-2.png +0 -0
- package/template/metadata/screenshot-3.png +0 -0
- package/template/notis.config.ts +10 -6
- package/template/package.json +2 -2
- package/template/packages/{notis-sdk → sdk}/package.json +6 -0
- package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectActionBar.tsx +2 -1
- package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectCheckbox.tsx +2 -2
- package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectDragOverlay.tsx +2 -2
- package/template/packages/{notis-sdk → sdk}/src/config.ts +1 -1
- package/template/packages/{notis-sdk → sdk}/src/hooks/useDatabase.ts +1 -13
- package/template/packages/{notis-sdk → sdk}/src/hooks/useMultiSelect.ts +4 -3
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +4 -3
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +3 -3
- package/template/packages/{notis-sdk → sdk}/src/hooks/useTool.ts +22 -7
- package/template/packages/{notis-sdk → sdk}/src/hooks/useUpsertDocument.ts +0 -8
- package/template/packages/{notis-sdk → sdk}/src/index.ts +2 -15
- package/template/packages/{notis-sdk → sdk}/src/provider.tsx +1 -1
- package/template/packages/{notis-sdk → sdk}/src/runtime.ts +5 -26
- package/src/command-specs/auth.js +0 -178
- package/src/command-specs/db.js +0 -163
- package/template/packages/notis-sdk/src/helpers.ts +0 -131
- package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
- package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
- package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useTopBarSearch.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/styles.css +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/vite.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/tsconfig.json +0 -0
package/src/runtime/transport.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
2
3
|
import { CliError, EXIT_CODES } from './errors.js';
|
|
3
4
|
import {
|
|
4
5
|
DEFAULT_PROFILE,
|
|
@@ -8,31 +9,71 @@ import {
|
|
|
8
9
|
saveConfig,
|
|
9
10
|
} from './profiles.js';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
'
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
function canonicalizeToolName(toolName) {
|
|
19
|
-
if (typeof toolName !== 'string' || !toolName.length) {
|
|
20
|
-
return toolName;
|
|
21
|
-
}
|
|
12
|
+
function escapeMultipartHeaderValue(value) {
|
|
13
|
+
return String(value ?? '')
|
|
14
|
+
.replace(/"/g, '%22')
|
|
15
|
+
.replace(/\r/g, '%0D')
|
|
16
|
+
.replace(/\n/g, '%0A');
|
|
17
|
+
}
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
function multipartTextPart(boundary, name, value) {
|
|
20
|
+
return [
|
|
21
|
+
`--${boundary}`,
|
|
22
|
+
`Content-Disposition: form-data; name="${escapeMultipartHeaderValue(name)}"`,
|
|
23
|
+
'',
|
|
24
|
+
value,
|
|
25
|
+
'',
|
|
26
|
+
].join('\r\n');
|
|
27
|
+
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
function multipartFileHeader(boundary, binding) {
|
|
30
|
+
const filename = escapeMultipartHeaderValue(binding.basename || binding.field_name);
|
|
31
|
+
return [
|
|
32
|
+
`--${boundary}`,
|
|
33
|
+
`Content-Disposition: form-data; name="${escapeMultipartHeaderValue(binding.field_name)}"; filename="${filename}"`,
|
|
34
|
+
`Content-Type: ${binding.contentType || 'application/octet-stream'}`,
|
|
35
|
+
'',
|
|
36
|
+
'',
|
|
37
|
+
].join('\r\n');
|
|
38
|
+
}
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
async function* multipartFileBody(boundary, payload, bindingMetadata, fileBindings) {
|
|
41
|
+
yield Buffer.from(multipartTextPart(boundary, 'payload', JSON.stringify(payload)));
|
|
42
|
+
yield Buffer.from(multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)));
|
|
43
|
+
for (const binding of fileBindings) {
|
|
44
|
+
yield Buffer.from(multipartFileHeader(boundary, binding));
|
|
45
|
+
for await (const chunk of createReadStream(binding.localPath)) {
|
|
46
|
+
yield chunk;
|
|
47
|
+
}
|
|
48
|
+
yield Buffer.from('\r\n');
|
|
33
49
|
}
|
|
50
|
+
yield Buffer.from(`--${boundary}--\r\n`);
|
|
51
|
+
}
|
|
34
52
|
|
|
35
|
-
|
|
53
|
+
function createMultipartFileUpload(payload, bindingMetadata, fileBindings) {
|
|
54
|
+
const boundary = `----notis-cli-${randomUUID().replace(/-/g, '')}`;
|
|
55
|
+
const textParts = [
|
|
56
|
+
multipartTextPart(boundary, 'payload', JSON.stringify(payload)),
|
|
57
|
+
multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)),
|
|
58
|
+
];
|
|
59
|
+
const fileHeaderParts = fileBindings.map((binding) => multipartFileHeader(boundary, binding));
|
|
60
|
+
const closingPart = `--${boundary}--\r\n`;
|
|
61
|
+
const contentLength = [
|
|
62
|
+
...textParts,
|
|
63
|
+
...fileHeaderParts,
|
|
64
|
+
...fileBindings.map(() => '\r\n'),
|
|
65
|
+
closingPart,
|
|
66
|
+
].reduce((total, part) => total + Buffer.byteLength(part), 0)
|
|
67
|
+
+ fileBindings.reduce((total, binding) => total + binding.size, 0);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
body: multipartFileBody(boundary, payload, bindingMetadata, fileBindings),
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
73
|
+
'Content-Length': String(contentLength),
|
|
74
|
+
},
|
|
75
|
+
duplex: 'half',
|
|
76
|
+
};
|
|
36
77
|
}
|
|
37
78
|
|
|
38
79
|
function normalizeBackendError(status, payload) {
|
|
@@ -51,8 +92,8 @@ function normalizeBackendError(status, payload) {
|
|
|
51
92
|
exitCode: EXIT_CODES.auth,
|
|
52
93
|
hints: [
|
|
53
94
|
{
|
|
54
|
-
command: '
|
|
55
|
-
reason: '
|
|
95
|
+
command: 'Open the Notis desktop app and sign in',
|
|
96
|
+
reason: 'Open the Notis desktop app to refresh CLI auth, or set NOTIS_JWT',
|
|
56
97
|
},
|
|
57
98
|
],
|
|
58
99
|
details: payload || {},
|
|
@@ -197,6 +238,7 @@ export async function httpRequest({
|
|
|
197
238
|
method = 'POST',
|
|
198
239
|
path,
|
|
199
240
|
body,
|
|
241
|
+
multipart = false,
|
|
200
242
|
requireAuth = true,
|
|
201
243
|
}) {
|
|
202
244
|
await maybeRefreshDevPortalAuth(runtime);
|
|
@@ -211,21 +253,41 @@ export async function httpRequest({
|
|
|
211
253
|
};
|
|
212
254
|
|
|
213
255
|
const headers = {
|
|
214
|
-
'Content-Type': 'application/json',
|
|
215
256
|
'X-Notis-CLI-Version': runtime.cliVersion,
|
|
216
257
|
'X-Notis-Request-Id': requestId,
|
|
217
258
|
};
|
|
259
|
+
if (!multipart) {
|
|
260
|
+
headers['Content-Type'] = 'application/json';
|
|
261
|
+
}
|
|
218
262
|
|
|
219
263
|
if (requireAuth && runtime.jwt) {
|
|
220
264
|
headers.Authorization = `Bearer ${runtime.jwt}`;
|
|
221
265
|
}
|
|
222
266
|
|
|
267
|
+
const resolveBody = () => {
|
|
268
|
+
if (!multipart) {
|
|
269
|
+
return {
|
|
270
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
271
|
+
headers: {},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (typeof body === 'function') {
|
|
275
|
+
return body();
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
body,
|
|
279
|
+
headers: {},
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
|
|
223
283
|
try {
|
|
284
|
+
let requestBody = resolveBody();
|
|
224
285
|
let response = await fetch(`${runtime.apiBase}${path}`, {
|
|
225
286
|
method,
|
|
226
|
-
headers,
|
|
227
|
-
body:
|
|
287
|
+
headers: { ...headers, ...requestBody.headers },
|
|
288
|
+
body: requestBody.body,
|
|
228
289
|
signal: controller.signal,
|
|
290
|
+
...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
|
|
229
291
|
});
|
|
230
292
|
|
|
231
293
|
let payload = null;
|
|
@@ -243,11 +305,13 @@ export async function httpRequest({
|
|
|
243
305
|
headers.Authorization = `Bearer ${runtime.jwt}`;
|
|
244
306
|
}
|
|
245
307
|
resetTimeout();
|
|
308
|
+
requestBody = resolveBody();
|
|
246
309
|
response = await fetch(`${runtime.apiBase}${path}`, {
|
|
247
310
|
method,
|
|
248
|
-
headers,
|
|
249
|
-
body:
|
|
311
|
+
headers: { ...headers, ...requestBody.headers },
|
|
312
|
+
body: requestBody.body,
|
|
250
313
|
signal: controller.signal,
|
|
314
|
+
...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
|
|
251
315
|
});
|
|
252
316
|
try {
|
|
253
317
|
payload = await response.json();
|
|
@@ -302,22 +366,43 @@ export async function callTool({
|
|
|
302
366
|
toolName,
|
|
303
367
|
arguments_: argumentsPayload = {},
|
|
304
368
|
idempotencyKey,
|
|
369
|
+
fileBindings = [],
|
|
305
370
|
}) {
|
|
306
371
|
const requestId = idempotencyKey || (runtime.mutating ? randomUUID() : null);
|
|
372
|
+
const payload = {
|
|
373
|
+
tool_name: toolName,
|
|
374
|
+
arguments: argumentsPayload,
|
|
375
|
+
idempotency_key: requestId,
|
|
376
|
+
cli_context: {
|
|
377
|
+
output_mode: runtime.outputMode,
|
|
378
|
+
profile: runtime.profileName,
|
|
379
|
+
cwd: process.cwd(),
|
|
380
|
+
agent_mode: runtime.agentMode,
|
|
381
|
+
cli_version: runtime.cliVersion,
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
if (Array.isArray(fileBindings) && fileBindings.length) {
|
|
386
|
+
const bindingMetadata = fileBindings.map((binding) => {
|
|
387
|
+
const {
|
|
388
|
+
contentType,
|
|
389
|
+
localPath,
|
|
390
|
+
...metadata
|
|
391
|
+
} = binding;
|
|
392
|
+
return metadata;
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return httpRequest({
|
|
396
|
+
runtime,
|
|
397
|
+
path: '/cli_tools',
|
|
398
|
+
body: () => createMultipartFileUpload(payload, bindingMetadata, fileBindings),
|
|
399
|
+
multipart: true,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
307
403
|
return httpRequest({
|
|
308
404
|
runtime,
|
|
309
405
|
path: '/cli_tools',
|
|
310
|
-
body:
|
|
311
|
-
tool_name: canonicalizeToolName(toolName),
|
|
312
|
-
arguments: argumentsPayload,
|
|
313
|
-
idempotency_key: requestId,
|
|
314
|
-
cli_context: {
|
|
315
|
-
output_mode: runtime.outputMode,
|
|
316
|
-
profile: runtime.profileName,
|
|
317
|
-
cwd: process.cwd(),
|
|
318
|
-
agent_mode: runtime.agentMode,
|
|
319
|
-
cli_version: runtime.cliVersion,
|
|
320
|
-
},
|
|
321
|
-
},
|
|
406
|
+
body: payload,
|
|
322
407
|
});
|
|
323
408
|
}
|
|
@@ -107,8 +107,7 @@
|
|
|
107
107
|
return {
|
|
108
108
|
app: descriptor.app,
|
|
109
109
|
route: descriptor.route,
|
|
110
|
-
|
|
111
|
-
context: descriptor.context || { collectionItem: null },
|
|
110
|
+
context: descriptor.context || {},
|
|
112
111
|
navigate: (args) => record('navigate', args),
|
|
113
112
|
registerTopBarSearch: () => {},
|
|
114
113
|
setTopBarSearchValue: () => {},
|
|
@@ -121,34 +120,6 @@
|
|
|
121
120
|
record('callTool', { name, arguments: args || {} });
|
|
122
121
|
return { ok: true, result: null };
|
|
123
122
|
},
|
|
124
|
-
queryDatabase: async (args) => {
|
|
125
|
-
record('queryDatabase', args || {});
|
|
126
|
-
return { documents: [], next_offset: null };
|
|
127
|
-
},
|
|
128
|
-
getDocument: async (args) => {
|
|
129
|
-
record('getDocument', args || {});
|
|
130
|
-
return null;
|
|
131
|
-
},
|
|
132
|
-
upsertDocument: async (args) => {
|
|
133
|
-
record('upsertDocument', args || {});
|
|
134
|
-
return {
|
|
135
|
-
status: 'ok',
|
|
136
|
-
document: {
|
|
137
|
-
id: (args && args.documentId) || 'mock-doc',
|
|
138
|
-
title: (args && args.title) || '',
|
|
139
|
-
properties: (args && args.properties) || {},
|
|
140
|
-
databaseSlug: args && args.databaseSlug,
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
},
|
|
144
|
-
listCollectionItems: async (args) => {
|
|
145
|
-
record('listCollectionItems', args || {});
|
|
146
|
-
return { items: [] };
|
|
147
|
-
},
|
|
148
|
-
listCollectionTree: async (args) => {
|
|
149
|
-
record('listCollectionTree', args || {});
|
|
150
|
-
return { items: [] };
|
|
151
|
-
},
|
|
152
123
|
request: async (path, options) => {
|
|
153
124
|
record('request', { path, options });
|
|
154
125
|
return null;
|
|
@@ -168,26 +139,6 @@
|
|
|
168
139
|
record('callTool', { name, arguments: args || {} });
|
|
169
140
|
return runtimeQuery({ method: 'tools/call', name, arguments: args || {} });
|
|
170
141
|
},
|
|
171
|
-
queryDatabase: async (args) => {
|
|
172
|
-
record('queryDatabase', args || {});
|
|
173
|
-
const result = await runtimeQuery({ method: 'tools/call', name: 'notis_query_database', arguments: args || {} });
|
|
174
|
-
return result.result || result;
|
|
175
|
-
},
|
|
176
|
-
getDocument: async (args) => {
|
|
177
|
-
record('getDocument', args || {});
|
|
178
|
-
const result = await runtimeQuery({ method: 'tools/call', name: 'notis_get_document', arguments: args || {} });
|
|
179
|
-
return result.result || result;
|
|
180
|
-
},
|
|
181
|
-
upsertDocument: async (args) => {
|
|
182
|
-
record('upsertDocument', args || {});
|
|
183
|
-
const result = await runtimeQuery({ method: 'tools/call', name: 'notis_upsert_document', arguments: args || {} });
|
|
184
|
-
return result.result || result;
|
|
185
|
-
},
|
|
186
|
-
listCollectionItems: async (args) => {
|
|
187
|
-
record('listCollectionItems', args || {});
|
|
188
|
-
const result = await runtimeQuery({ method: 'tools/call', name: 'notis_list_collection_items', arguments: args || {} });
|
|
189
|
-
return result.result || result;
|
|
190
|
-
},
|
|
191
142
|
request: async (path, options) => {
|
|
192
143
|
record('request', { path, options });
|
|
193
144
|
const response = await fetch(`${String(apiBase).replace(/\/$/, '')}${path}`, {
|
package/template/app/page.tsx
CHANGED
|
@@ -1,12 +1,47 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useNotis, useTool } from '@notis/sdk';
|
|
4
5
|
import { Badge } from '@/components/ui/badge';
|
|
5
6
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
6
7
|
|
|
8
|
+
type ItemDocument = {
|
|
9
|
+
id?: string;
|
|
10
|
+
document_id?: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
properties?: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type QueryItemsArgs = {
|
|
16
|
+
database_slug: string;
|
|
17
|
+
query: {
|
|
18
|
+
page_size?: number;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type QueryItemsResult = {
|
|
23
|
+
documents?: ItemDocument[];
|
|
24
|
+
};
|
|
25
|
+
|
|
7
26
|
export default function HomePage() {
|
|
8
27
|
const { app, ready } = useNotis();
|
|
9
|
-
const
|
|
28
|
+
const queryItems = useTool<QueryItemsArgs, QueryItemsResult>('LOCAL_NOTIS_DATABASE_QUERY');
|
|
29
|
+
const [documents, setDocuments] = useState<ItemDocument[]>([]);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
let cancelled = false;
|
|
33
|
+
queryItems
|
|
34
|
+
.call({ database_slug: 'items', query: { page_size: 25 } })
|
|
35
|
+
.then((result) => {
|
|
36
|
+
if (!cancelled) setDocuments(result.documents || []);
|
|
37
|
+
})
|
|
38
|
+
.catch(() => {
|
|
39
|
+
if (!cancelled) setDocuments([]);
|
|
40
|
+
});
|
|
41
|
+
return () => {
|
|
42
|
+
cancelled = true;
|
|
43
|
+
};
|
|
44
|
+
}, [queryItems.call]);
|
|
10
45
|
|
|
11
46
|
return (
|
|
12
47
|
<main className="notis-app-shell space-y-6">
|
|
@@ -28,7 +63,7 @@ export default function HomePage() {
|
|
|
28
63
|
<CardDescription>Use shadcn surfaces and portal tokens so the app feels native inside Notis.</CardDescription>
|
|
29
64
|
</CardHeader>
|
|
30
65
|
<CardContent>
|
|
31
|
-
{loading ? (
|
|
66
|
+
{queryItems.loading ? (
|
|
32
67
|
<p className="text-sm text-muted-foreground">Loading...</p>
|
|
33
68
|
) : documents.length === 0 ? (
|
|
34
69
|
<div className="rounded-xl border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground">
|
|
@@ -37,10 +72,10 @@ export default function HomePage() {
|
|
|
37
72
|
) : (
|
|
38
73
|
<div className="space-y-3">
|
|
39
74
|
{documents.map((doc) => (
|
|
40
|
-
<div key={doc.id} className="rounded-xl border border-border bg-background px-4 py-3">
|
|
75
|
+
<div key={doc.id || doc.document_id || doc.title} className="rounded-xl border border-border bg-background px-4 py-3">
|
|
41
76
|
<div className="flex items-center justify-between gap-3">
|
|
42
|
-
<p className="font-medium">{doc.title}</p>
|
|
43
|
-
{doc.properties
|
|
77
|
+
<p className="font-medium">{doc.title || 'Untitled'}</p>
|
|
78
|
+
{doc.properties?.status ? (
|
|
44
79
|
<Badge variant="outline">{String(doc.properties.status)}</Badge>
|
|
45
80
|
) : null}
|
|
46
81
|
</div>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/template/notis.config.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { defineNotisApp } from '@notis/sdk/config';
|
|
2
2
|
|
|
3
3
|
export default defineNotisApp({
|
|
4
|
-
name: '
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
name: 'my-notis-app',
|
|
5
|
+
title: 'My Notis App',
|
|
6
|
+
description: 'A focused Notis app with one route and one database-backed workflow.',
|
|
7
|
+
icon: 'phosphor:squares-four',
|
|
8
|
+
author: { name: 'Notis User' },
|
|
9
|
+
categories: ['Productivity'],
|
|
10
|
+
tagline: 'A clean starting point for a focused workflow.',
|
|
11
|
+
versionNotes: 'Initial release.',
|
|
7
12
|
|
|
8
13
|
databases: ['items'],
|
|
9
14
|
|
|
10
15
|
routes: [
|
|
11
|
-
{ path: '/', slug: 'home', name: 'Home', icon: '
|
|
16
|
+
{ path: '/', slug: 'home', name: 'Home', icon: 'phosphor:house', default: true },
|
|
12
17
|
// Example collection tree route:
|
|
13
18
|
// {
|
|
14
19
|
// path: '/notes',
|
|
@@ -27,7 +32,6 @@ export default defineNotisApp({
|
|
|
27
32
|
],
|
|
28
33
|
|
|
29
34
|
tools: [
|
|
30
|
-
'
|
|
31
|
-
'notis_upsert_document',
|
|
35
|
+
'LOCAL_NOTIS_DATABASE_QUERY',
|
|
32
36
|
],
|
|
33
37
|
});
|
package/template/package.json
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@notis/sdk": "file:./packages/
|
|
11
|
+
"@notis/sdk": "file:./packages/sdk",
|
|
12
|
+
"@phosphor-icons/react": "^2.1.10",
|
|
12
13
|
"@radix-ui/react-slot": "^1.1.0",
|
|
13
14
|
"class-variance-authority": "^0.7.0",
|
|
14
15
|
"clsx": "^2.1.1",
|
|
15
|
-
"lucide-react": "^0.474.0",
|
|
16
16
|
"react": "^19.0.0",
|
|
17
17
|
"react-dom": "^19.0.0",
|
|
18
18
|
"tailwind-merge": "^2.6.0",
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"src",
|
|
15
15
|
"template"
|
|
16
16
|
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"type-check": "tsc --noEmit"
|
|
19
|
+
},
|
|
17
20
|
"peerDependencies": {
|
|
18
21
|
"react": ">=18.0.0",
|
|
19
22
|
"react-dom": ">=18.0.0",
|
|
@@ -21,6 +24,9 @@
|
|
|
21
24
|
"@vitejs/plugin-react": ">=4.0.0"
|
|
22
25
|
},
|
|
23
26
|
"devDependencies": {
|
|
27
|
+
"@types/node": "^20.0.0",
|
|
28
|
+
"@types/react": "^19.0.0",
|
|
29
|
+
"@types/react-dom": "^19.0.0",
|
|
24
30
|
"typescript": "^5.0.0"
|
|
25
31
|
}
|
|
26
32
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useRef,
|
|
7
7
|
useState,
|
|
8
8
|
type CSSProperties,
|
|
9
|
+
type ReactElement,
|
|
9
10
|
type ReactNode,
|
|
10
11
|
} from 'react';
|
|
11
12
|
|
|
@@ -174,7 +175,7 @@ export function MultiSelectActionBar({
|
|
|
174
175
|
itemLabel,
|
|
175
176
|
className,
|
|
176
177
|
style,
|
|
177
|
-
}: MultiSelectActionBarProps):
|
|
178
|
+
}: MultiSelectActionBarProps): ReactElement | null {
|
|
178
179
|
const actionsRef = useRef(actions);
|
|
179
180
|
actionsRef.current = actions;
|
|
180
181
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { type CSSProperties, type MouseEvent as ReactMouseEvent } from 'react';
|
|
3
|
+
import { type CSSProperties, type MouseEvent as ReactMouseEvent, type ReactElement } from 'react';
|
|
4
4
|
|
|
5
5
|
export interface MultiSelectCheckboxProps {
|
|
6
6
|
isSelected: boolean;
|
|
@@ -44,7 +44,7 @@ export function MultiSelectCheckbox({
|
|
|
44
44
|
ariaLabel,
|
|
45
45
|
className,
|
|
46
46
|
style,
|
|
47
|
-
}: MultiSelectCheckboxProps):
|
|
47
|
+
}: MultiSelectCheckboxProps): ReactElement {
|
|
48
48
|
const merged: CSSProperties = {
|
|
49
49
|
...baseStyle,
|
|
50
50
|
...(isSelected ? selectedStyle : null),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { type CSSProperties } from 'react';
|
|
3
|
+
import { type CSSProperties, type ReactElement } from 'react';
|
|
4
4
|
import type { DragRect } from '../hooks/useMultiSelect';
|
|
5
5
|
|
|
6
6
|
export interface MultiSelectDragOverlayProps {
|
|
@@ -22,7 +22,7 @@ export function MultiSelectDragOverlay({
|
|
|
22
22
|
rect,
|
|
23
23
|
className,
|
|
24
24
|
style,
|
|
25
|
-
}: MultiSelectDragOverlayProps):
|
|
25
|
+
}: MultiSelectDragOverlayProps): ReactElement | null {
|
|
26
26
|
if (!rect) return null;
|
|
27
27
|
if (rect.width === 0 && rect.height === 0) return null;
|
|
28
28
|
|
|
@@ -8,7 +8,6 @@ interface UseDatabaseOptions {
|
|
|
8
8
|
filter?: QueryFilter;
|
|
9
9
|
pageSize?: number;
|
|
10
10
|
offset?: number;
|
|
11
|
-
/** Set to false to skip the initial fetch. */
|
|
12
11
|
enabled?: boolean;
|
|
13
12
|
}
|
|
14
13
|
|
|
@@ -19,16 +18,6 @@ interface UseDatabaseResult {
|
|
|
19
18
|
refetch: () => void;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
/**
|
|
23
|
-
* Query documents from a Notis database owned by this app.
|
|
24
|
-
*
|
|
25
|
-
* ```tsx
|
|
26
|
-
* const { documents, loading } = useDatabase('transactions', {
|
|
27
|
-
* filter: { sorts: [{ property: 'date', direction: 'desc' }] },
|
|
28
|
-
* pageSize: 20,
|
|
29
|
-
* });
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
21
|
export function useDatabase(
|
|
33
22
|
databaseSlug: string,
|
|
34
23
|
options: UseDatabaseOptions = {},
|
|
@@ -43,7 +32,7 @@ export function useDatabase(
|
|
|
43
32
|
const filterKey = JSON.stringify(options.filter ?? null);
|
|
44
33
|
|
|
45
34
|
const refetch = useCallback(() => {
|
|
46
|
-
setFetchKey((
|
|
35
|
+
setFetchKey((key) => key + 1);
|
|
47
36
|
}, []);
|
|
48
37
|
|
|
49
38
|
useEffect(() => {
|
|
@@ -81,7 +70,6 @@ export function useDatabase(
|
|
|
81
70
|
return () => {
|
|
82
71
|
cancelled = true;
|
|
83
72
|
};
|
|
84
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
85
73
|
}, [runtime, databaseSlug, fetchKey, enabled, filterKey, options.offset, options.pageSize]);
|
|
86
74
|
|
|
87
75
|
return { documents, loading, error, refetch };
|
|
@@ -149,7 +149,7 @@ export function useMultiSelect<T>(opts: UseMultiSelectOptions<T>): MultiSelectCo
|
|
|
149
149
|
|
|
150
150
|
const select = useCallback((ids: string[]) => {
|
|
151
151
|
setSelectedIds(new Set(ids));
|
|
152
|
-
setLastClickedId(ids.length > 0 ? ids[ids.length - 1] : null);
|
|
152
|
+
setLastClickedId(ids.length > 0 ? ids[ids.length - 1] ?? null : null);
|
|
153
153
|
}, []);
|
|
154
154
|
|
|
155
155
|
const clear = useCallback(() => {
|
|
@@ -159,7 +159,7 @@ export function useMultiSelect<T>(opts: UseMultiSelectOptions<T>): MultiSelectCo
|
|
|
159
159
|
const selectAll = useCallback(() => {
|
|
160
160
|
const all = orderedIdsRef.current;
|
|
161
161
|
setSelectedIds(new Set(all));
|
|
162
|
-
setLastClickedId(all.length > 0 ? all[all.length - 1] : null);
|
|
162
|
+
setLastClickedId(all.length > 0 ? all[all.length - 1] ?? null : null);
|
|
163
163
|
}, []);
|
|
164
164
|
|
|
165
165
|
const selectRange = useCallback((anchorId: string, targetId: string) => {
|
|
@@ -346,7 +346,7 @@ export function useMultiSelect<T>(opts: UseMultiSelectOptions<T>): MultiSelectCo
|
|
|
346
346
|
bottom: rect.top + rect.height,
|
|
347
347
|
});
|
|
348
348
|
setSelectedIds(new Set(ids));
|
|
349
|
-
setLastClickedId(ids.length > 0 ? ids[ids.length - 1] : null);
|
|
349
|
+
setLastClickedId(ids.length > 0 ? ids[ids.length - 1] ?? null : null);
|
|
350
350
|
};
|
|
351
351
|
|
|
352
352
|
const handleMouseUp = () => {
|
|
@@ -447,6 +447,7 @@ export function useMultiSelect<T>(opts: UseMultiSelectOptions<T>): MultiSelectCo
|
|
|
447
447
|
: Math.min(all.length - 1, Math.max(0, currentIdx + direction));
|
|
448
448
|
if (newIdx === currentIdx) return;
|
|
449
449
|
const newHead = all[newIdx];
|
|
450
|
+
if (!newHead) return;
|
|
450
451
|
const anchorId = lastClickedIdRef.current ?? currentHead ?? newHead;
|
|
451
452
|
const anchorIdx = all.indexOf(anchorId);
|
|
452
453
|
if (anchorIdx === -1) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useNotisRuntime } from '../provider';
|
|
4
|
-
import type { AppDescriptor,
|
|
4
|
+
import type { AppDescriptor, CollectionItemDetail, DatabaseDescriptor, RouteDescriptor } from '../runtime';
|
|
5
5
|
|
|
6
6
|
interface NotisContext {
|
|
7
7
|
/** App metadata (id, name, icon, description). Null before runtime loads. */
|
|
@@ -17,8 +17,9 @@ interface NotisContext {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Access app-level metadata
|
|
21
|
-
*
|
|
20
|
+
* Access app-level metadata, the current route, declared databases, and the
|
|
21
|
+
* selected collection item. Returns safe defaults when the portal runtime is
|
|
22
|
+
* not available.
|
|
22
23
|
*/
|
|
23
24
|
export function useNotis(): NotisContext {
|
|
24
25
|
const runtime = useNotisRuntime();
|
|
@@ -7,7 +7,7 @@ interface NavigationActions {
|
|
|
7
7
|
/** Navigate to a route within the app by its path. */
|
|
8
8
|
toRoute: (path: string) => void;
|
|
9
9
|
/** Navigate to a document detail view. */
|
|
10
|
-
toDocument: (documentId: string) => void;
|
|
10
|
+
toDocument: (documentId: string, title?: string | null) => void;
|
|
11
11
|
/** Navigate to the app's default route. */
|
|
12
12
|
toApp: () => void;
|
|
13
13
|
}
|
|
@@ -33,9 +33,9 @@ export function useNotisNavigation(): NavigationActions {
|
|
|
33
33
|
}
|
|
34
34
|
}, [runtime]);
|
|
35
35
|
|
|
36
|
-
const toDocument = useCallback((documentId: string) => {
|
|
36
|
+
const toDocument = useCallback((documentId: string, title?: string | null) => {
|
|
37
37
|
if (runtime?.navigate) {
|
|
38
|
-
runtime.navigate({ kind: 'document', documentId });
|
|
38
|
+
runtime.navigate({ kind: 'document', documentId, title: title ?? undefined });
|
|
39
39
|
}
|
|
40
40
|
}, [runtime]);
|
|
41
41
|
|