@notis_ai/cli 0.2.1 → 0.2.2
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 +98 -163
- package/dist/scaffolds/notes/app/globals.css +3 -0
- package/dist/scaffolds/notes/app/layout.tsx +6 -0
- package/dist/scaffolds/notes/app/page.tsx +1465 -0
- package/dist/scaffolds/notes/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notes/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notes/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notes/components.json +20 -0
- package/dist/scaffolds/notes/lib/utils.ts +6 -0
- package/dist/scaffolds/notes/notis.config.ts +31 -0
- package/dist/scaffolds/notes/package.json +31 -0
- package/dist/scaffolds/notes/postcss.config.mjs +8 -0
- package/dist/scaffolds/notes/tailwind.config.ts +58 -0
- package/dist/scaffolds/notes/tsconfig.json +22 -0
- package/dist/scaffolds/notes/vite.config.ts +10 -0
- package/dist/scaffolds.json +13 -0
- package/package.json +7 -3
- package/skills/notis-apps/SKILL.md +162 -0
- package/skills/notis-apps/cli.md +208 -0
- package/skills/notis-cli/SKILL.md +258 -0
- package/skills/notis-query/cli.md +40 -0
- package/src/cli.js +1 -1
- package/src/command-specs/apps.js +211 -46
- package/src/command-specs/helpers.js +0 -60
- package/src/command-specs/index.js +0 -6
- package/src/command-specs/meta.js +7 -6
- package/src/command-specs/tools.js +1 -33
- package/src/runtime/app-dev-server.js +32 -4
- package/src/runtime/app-platform.js +404 -24
- package/src/runtime/profiles.js +2 -2
- package/src/runtime/transport.js +2 -2
- 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
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { CliError, EXIT_CODES } from '../runtime/errors.js';
|
|
2
|
-
import {
|
|
3
|
-
clearStoredJwt,
|
|
4
|
-
probeAuth,
|
|
5
|
-
promptForJwt,
|
|
6
|
-
updateStoredProfile,
|
|
7
|
-
} from './helpers.js';
|
|
8
|
-
|
|
9
|
-
async function authLoginHandler(ctx) {
|
|
10
|
-
let jwt = ctx.options.jwt || process.env.NOTIS_JWT;
|
|
11
|
-
if (!jwt) {
|
|
12
|
-
if (ctx.runtime.nonInteractive) {
|
|
13
|
-
throw new CliError({
|
|
14
|
-
code: 'auth_missing',
|
|
15
|
-
message: `No JWT configured for profile ${ctx.runtime.profileName}`,
|
|
16
|
-
exitCode: EXIT_CODES.auth,
|
|
17
|
-
hints: [
|
|
18
|
-
{
|
|
19
|
-
command: 'notis auth login --jwt <token>',
|
|
20
|
-
reason: 'Provide a token explicitly in non-interactive mode',
|
|
21
|
-
},
|
|
22
|
-
],
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
jwt = await promptForJwt();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
updateStoredProfile({
|
|
29
|
-
profileName: ctx.runtime.profileName,
|
|
30
|
-
jwt,
|
|
31
|
-
apiBase: ctx.globalOptions.apiBase,
|
|
32
|
-
setCurrent: true,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
return ctx.output.emitSuccess({
|
|
36
|
-
command: ctx.spec.command_path.join(' '),
|
|
37
|
-
data: {
|
|
38
|
-
profile: ctx.runtime.profileName,
|
|
39
|
-
api_base: ctx.globalOptions.apiBase || ctx.runtime.apiBase,
|
|
40
|
-
},
|
|
41
|
-
humanSummary: `Stored credentials for profile ${ctx.runtime.profileName}`,
|
|
42
|
-
hints: [
|
|
43
|
-
{ command: 'notis whoami', reason: 'Verify your identity and available toolkits' },
|
|
44
|
-
],
|
|
45
|
-
meta: { mutating: true },
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function authLogoutHandler(ctx) {
|
|
50
|
-
clearStoredJwt(ctx.runtime.profileName);
|
|
51
|
-
return ctx.output.emitSuccess({
|
|
52
|
-
command: ctx.spec.command_path.join(' '),
|
|
53
|
-
data: { profile: ctx.runtime.profileName },
|
|
54
|
-
humanSummary: `Removed stored JWT for profile ${ctx.runtime.profileName}`,
|
|
55
|
-
meta: { mutating: true },
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function authStatusHandler(ctx) {
|
|
60
|
-
const hasJwt = Boolean(ctx.runtime.jwt);
|
|
61
|
-
let verified = false;
|
|
62
|
-
let toolkits = [];
|
|
63
|
-
if (hasJwt && ctx.options.verify) {
|
|
64
|
-
const payload = await probeAuth(ctx.runtime);
|
|
65
|
-
toolkits = payload.toolkits || [];
|
|
66
|
-
verified = true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return ctx.output.emitSuccess({
|
|
70
|
-
command: ctx.spec.command_path.join(' '),
|
|
71
|
-
data: {
|
|
72
|
-
profile: ctx.runtime.profileName,
|
|
73
|
-
api_base: ctx.runtime.apiBase,
|
|
74
|
-
has_jwt: hasJwt,
|
|
75
|
-
verified,
|
|
76
|
-
toolkits,
|
|
77
|
-
},
|
|
78
|
-
humanSummary: hasJwt
|
|
79
|
-
? `Profile ${ctx.runtime.profileName} is configured`
|
|
80
|
-
: `Profile ${ctx.runtime.profileName} is missing a JWT`,
|
|
81
|
-
hints: hasJwt
|
|
82
|
-
? []
|
|
83
|
-
: [{ command: 'notis auth login --jwt <token>', reason: 'Configure credentials for this profile' }],
|
|
84
|
-
meta: { mutating: false },
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export const authCommandSpecs = [
|
|
89
|
-
{
|
|
90
|
-
command_path: ['auth', 'login'],
|
|
91
|
-
summary: 'Store credentials for a named CLI profile.',
|
|
92
|
-
when_to_use: 'Use this before authenticated commands, especially in fresh environments or CI profiles.',
|
|
93
|
-
args_schema: {
|
|
94
|
-
arguments: [],
|
|
95
|
-
options: [
|
|
96
|
-
{ flags: '--jwt <token>', description: 'JWT token to store for the profile.' },
|
|
97
|
-
],
|
|
98
|
-
},
|
|
99
|
-
examples: ['notis auth login --jwt <token>', 'notis auth login --profile staging --api-base http://localhost:3001'],
|
|
100
|
-
output_schema: 'Returns the active profile name and API base.',
|
|
101
|
-
mutates: true,
|
|
102
|
-
idempotent: true,
|
|
103
|
-
require_auth: false,
|
|
104
|
-
related_commands: ['notis auth status --verify', 'notis auth logout'],
|
|
105
|
-
backend_call: { type: 'local' },
|
|
106
|
-
handler: authLoginHandler,
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
command_path: ['auth', 'logout'],
|
|
110
|
-
summary: 'Remove the stored JWT for the active profile.',
|
|
111
|
-
when_to_use: 'Use this to clear local credentials without touching other profiles.',
|
|
112
|
-
args_schema: { arguments: [], options: [] },
|
|
113
|
-
examples: ['notis auth logout', 'notis auth logout --profile staging'],
|
|
114
|
-
output_schema: 'Returns the profile that was updated.',
|
|
115
|
-
mutates: true,
|
|
116
|
-
idempotent: true,
|
|
117
|
-
require_auth: false,
|
|
118
|
-
related_commands: ['notis auth login --jwt <token>', 'notis auth status'],
|
|
119
|
-
backend_call: { type: 'local' },
|
|
120
|
-
handler: authLogoutHandler,
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
command_path: ['auth', 'status'],
|
|
124
|
-
summary: 'Inspect local auth configuration and optionally verify it against the API.',
|
|
125
|
-
when_to_use: 'Use this before automating commands to confirm the active profile and token health.',
|
|
126
|
-
args_schema: {
|
|
127
|
-
arguments: [],
|
|
128
|
-
options: [
|
|
129
|
-
{ flags: '--verify', description: 'Perform a live authenticated roundtrip to the API.' },
|
|
130
|
-
],
|
|
131
|
-
},
|
|
132
|
-
examples: ['notis auth status', 'notis auth status --verify --json'],
|
|
133
|
-
output_schema: 'Returns profile, api_base, local auth presence, and optional verification data.',
|
|
134
|
-
mutates: false,
|
|
135
|
-
idempotent: true,
|
|
136
|
-
require_auth: false,
|
|
137
|
-
related_commands: ['notis doctor', 'notis auth login --jwt <token>'],
|
|
138
|
-
backend_call: { type: 'notis_find_toolkits' },
|
|
139
|
-
handler: authStatusHandler,
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
command_path: ['login'],
|
|
143
|
-
display_name: 'login',
|
|
144
|
-
summary: 'Deprecated alias for `notis auth login`.',
|
|
145
|
-
when_to_use: 'Use only for backward compatibility with older scripts.',
|
|
146
|
-
args_schema: {
|
|
147
|
-
arguments: [],
|
|
148
|
-
options: [
|
|
149
|
-
{ flags: '--jwt <token>', description: 'JWT token to store for the profile.' },
|
|
150
|
-
],
|
|
151
|
-
},
|
|
152
|
-
examples: ['notis login --jwt <token>'],
|
|
153
|
-
output_schema: 'Same output as `notis auth login`.',
|
|
154
|
-
mutates: true,
|
|
155
|
-
idempotent: true,
|
|
156
|
-
require_auth: false,
|
|
157
|
-
related_commands: ['notis auth login --jwt <token>'],
|
|
158
|
-
backend_call: { type: 'local', alias_for: 'auth login' },
|
|
159
|
-
deprecated_alias_for: 'notis auth login',
|
|
160
|
-
handler: authLoginHandler,
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
command_path: ['logout'],
|
|
164
|
-
display_name: 'logout',
|
|
165
|
-
summary: 'Deprecated alias for `notis auth logout`.',
|
|
166
|
-
when_to_use: 'Use only for backward compatibility with older scripts.',
|
|
167
|
-
args_schema: { arguments: [], options: [] },
|
|
168
|
-
examples: ['notis logout'],
|
|
169
|
-
output_schema: 'Same output as `notis auth logout`.',
|
|
170
|
-
mutates: true,
|
|
171
|
-
idempotent: true,
|
|
172
|
-
require_auth: false,
|
|
173
|
-
related_commands: ['notis auth logout'],
|
|
174
|
-
backend_call: { type: 'local', alias_for: 'auth logout' },
|
|
175
|
-
deprecated_alias_for: 'notis auth logout',
|
|
176
|
-
handler: authLogoutHandler,
|
|
177
|
-
},
|
|
178
|
-
];
|
package/src/command-specs/db.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { formatTable } from '../runtime/output.js';
|
|
2
|
-
import { nextIdempotencyKey, parseMaybeJson, runToolCommand } from './helpers.js';
|
|
3
|
-
|
|
4
|
-
async function dbListHandler(ctx) {
|
|
5
|
-
const result = await runToolCommand({
|
|
6
|
-
runtime: ctx.runtime,
|
|
7
|
-
toolName: 'notis_list_databases',
|
|
8
|
-
});
|
|
9
|
-
const databases = result.payload.databases || [];
|
|
10
|
-
const firstSlug = databases[0]?.slug;
|
|
11
|
-
return ctx.output.emitSuccess({
|
|
12
|
-
command: ctx.spec.command_path.join(' '),
|
|
13
|
-
data: { databases },
|
|
14
|
-
humanSummary: databases.length ? `Found ${databases.length} databases` : 'No databases found.',
|
|
15
|
-
hints: firstSlug
|
|
16
|
-
? [{ command: `notis db query ${firstSlug}`, reason: 'Query this database' }]
|
|
17
|
-
: [{ command: 'notis db upsert --operation create --title "My DB"', reason: 'Create a new database' }],
|
|
18
|
-
renderHuman: () =>
|
|
19
|
-
databases.length
|
|
20
|
-
? formatTable(databases, [
|
|
21
|
-
{ label: 'ID', value: (database) => database.id || '' },
|
|
22
|
-
{ label: 'Name', value: (database) => database.name || 'Untitled' },
|
|
23
|
-
{ label: 'Slug', value: (database) => database.slug || '-' },
|
|
24
|
-
])
|
|
25
|
-
: 'No databases found.',
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function dbUpsertHandler(ctx) {
|
|
30
|
-
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
31
|
-
const payload = {
|
|
32
|
-
operation: ctx.options.operation,
|
|
33
|
-
};
|
|
34
|
-
if (ctx.options.databaseId) {
|
|
35
|
-
payload.database_id = ctx.options.databaseId;
|
|
36
|
-
}
|
|
37
|
-
if (ctx.options.title) {
|
|
38
|
-
payload.title = ctx.options.title;
|
|
39
|
-
}
|
|
40
|
-
if (ctx.options.description) {
|
|
41
|
-
payload.description = ctx.options.description;
|
|
42
|
-
}
|
|
43
|
-
if (ctx.options.icon) {
|
|
44
|
-
const v = ctx.options.icon;
|
|
45
|
-
payload.icon = v.startsWith('lucide:') ? v : `lucide:${v}`;
|
|
46
|
-
}
|
|
47
|
-
if (ctx.options.properties) {
|
|
48
|
-
payload.properties = parseMaybeJson(ctx.options.properties, 'properties');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const result = await runToolCommand({
|
|
52
|
-
runtime: ctx.runtime,
|
|
53
|
-
toolName: 'notis_upsert_database',
|
|
54
|
-
arguments_: payload,
|
|
55
|
-
mutating: true,
|
|
56
|
-
idempotencyKey,
|
|
57
|
-
});
|
|
58
|
-
return ctx.output.emitSuccess({
|
|
59
|
-
command: ctx.spec.command_path.join(' '),
|
|
60
|
-
data: result.payload,
|
|
61
|
-
humanSummary: `Database ${ctx.options.operation} completed`,
|
|
62
|
-
meta: { mutating: true, idempotency_key: idempotencyKey || null },
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function dbQueryHandler(ctx) {
|
|
67
|
-
const query = {};
|
|
68
|
-
if (ctx.options.filter) {
|
|
69
|
-
query.filter = parseMaybeJson(ctx.options.filter, 'filter');
|
|
70
|
-
}
|
|
71
|
-
if (ctx.options.sort) {
|
|
72
|
-
const parsedSorts = parseMaybeJson(ctx.options.sort, 'sort');
|
|
73
|
-
query.sorts = Array.isArray(parsedSorts) ? parsedSorts : [parsedSorts];
|
|
74
|
-
}
|
|
75
|
-
if (ctx.options.pageSize) {
|
|
76
|
-
query.page_size = Number.parseInt(ctx.options.pageSize, 10);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const payload = {
|
|
80
|
-
database_slug: ctx.args.databaseSlug,
|
|
81
|
-
query,
|
|
82
|
-
};
|
|
83
|
-
if (ctx.options.offset) {
|
|
84
|
-
payload.offset = Number.parseInt(ctx.options.offset, 10);
|
|
85
|
-
}
|
|
86
|
-
if (ctx.options.cursor) {
|
|
87
|
-
payload.start_cursor = ctx.options.cursor;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const result = await runToolCommand({
|
|
91
|
-
runtime: ctx.runtime,
|
|
92
|
-
toolName: 'notis_query',
|
|
93
|
-
arguments_: payload,
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
return ctx.output.emitSuccess({
|
|
97
|
-
command: ctx.spec.command_path.join(' '),
|
|
98
|
-
data: result.payload,
|
|
99
|
-
humanSummary: `Queried database ${ctx.args.databaseSlug}`,
|
|
100
|
-
renderHuman: () => JSON.stringify(result.payload, null, 2),
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export const dbCommandSpecs = [
|
|
105
|
-
{
|
|
106
|
-
command_path: ['db', 'list'],
|
|
107
|
-
summary: 'List native Notis databases.',
|
|
108
|
-
when_to_use: 'Use this to find database ids and slugs before querying or updating schemas.',
|
|
109
|
-
args_schema: { arguments: [], options: [] },
|
|
110
|
-
examples: ['notis db list', 'notis db list --json'],
|
|
111
|
-
output_schema: 'Returns an array of native database records.',
|
|
112
|
-
mutates: false,
|
|
113
|
-
idempotent: true,
|
|
114
|
-
related_commands: ['notis db query <database-slug>', 'notis db upsert'],
|
|
115
|
-
backend_call: { type: 'tool', name: 'notis_list_databases' },
|
|
116
|
-
handler: dbListHandler,
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
command_path: ['db', 'upsert'],
|
|
120
|
-
summary: 'Create or update a native database schema.',
|
|
121
|
-
when_to_use: 'Use this when you need to provision a new database or adjust an existing schema.',
|
|
122
|
-
args_schema: {
|
|
123
|
-
arguments: [],
|
|
124
|
-
options: [
|
|
125
|
-
{ flags: '--operation <create|update>', description: 'Create a new database or update an existing one.' },
|
|
126
|
-
{ flags: '--database-id <id>', description: 'Database id for update operations.' },
|
|
127
|
-
{ flags: '--title <text>', description: 'Database title.' },
|
|
128
|
-
{ flags: '--description <text>', description: 'Database description.' },
|
|
129
|
-
{ flags: '--icon <lucide-icon-name>', description: 'Database icon (Lucide icon name, e.g. database).' },
|
|
130
|
-
{ flags: '--properties <json>', description: 'JSON array of property definitions.' },
|
|
131
|
-
],
|
|
132
|
-
},
|
|
133
|
-
examples: ['notis db upsert --operation create --title "Tasks"', 'notis db upsert --operation update --database-id db_123 --title "Tasks V2"'],
|
|
134
|
-
output_schema: 'Returns the backend database upsert payload.',
|
|
135
|
-
mutates: true,
|
|
136
|
-
idempotent: true,
|
|
137
|
-
related_commands: ['notis db list', 'notis db query <database-slug>'],
|
|
138
|
-
backend_call: { type: 'tool', name: 'notis_upsert_database' },
|
|
139
|
-
handler: dbUpsertHandler,
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
command_path: ['db', 'query'],
|
|
143
|
-
summary: 'Run a structured query against a native Notis database.',
|
|
144
|
-
when_to_use: 'Use this when the database slug is known and you need direct filters, sorts, or pagination.',
|
|
145
|
-
args_schema: {
|
|
146
|
-
arguments: [{ token: '<database-slug>', description: 'Slug of the native database to query.' }],
|
|
147
|
-
options: [
|
|
148
|
-
{ flags: '--filter <json>', description: 'Structured query filter JSON.' },
|
|
149
|
-
{ flags: '--sort <json>', description: 'Sort JSON object or array.' },
|
|
150
|
-
{ flags: '--page-size <n>', description: 'Page size between 1 and 100.' },
|
|
151
|
-
{ flags: '--offset <n>', description: 'Zero-based offset.' },
|
|
152
|
-
{ flags: '--cursor <value>', description: 'Pagination cursor alias for next_offset.' },
|
|
153
|
-
],
|
|
154
|
-
},
|
|
155
|
-
examples: ['notis db query tasks --page-size 50', 'notis db query tasks --filter \'{"property":"Status"}\''],
|
|
156
|
-
output_schema: 'Returns the native database query payload from `notis_query`.',
|
|
157
|
-
mutates: false,
|
|
158
|
-
idempotent: true,
|
|
159
|
-
related_commands: ['notis db list', 'notis db upsert'],
|
|
160
|
-
backend_call: { type: 'tool', name: 'notis_query' },
|
|
161
|
-
handler: dbQueryHandler,
|
|
162
|
-
},
|
|
163
|
-
];
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Property extraction helpers for Notis DocumentRecord properties.
|
|
3
|
-
*
|
|
4
|
-
* Document properties come back in Notion-style nested shapes. These helpers
|
|
5
|
-
* normalize them into simple JS values regardless of the shape.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { DocumentRecord } from './runtime';
|
|
9
|
-
|
|
10
|
-
type Obj = Record<string, unknown>;
|
|
11
|
-
|
|
12
|
-
/** Extract a string from a title, rich_text, or plain text property. */
|
|
13
|
-
export function extractText(val: unknown): string {
|
|
14
|
-
if (typeof val === 'string') return val;
|
|
15
|
-
if (val && typeof val === 'object') {
|
|
16
|
-
const obj = val as Obj;
|
|
17
|
-
if (typeof obj.content === 'string') return obj.content;
|
|
18
|
-
if (typeof obj.plain_text === 'string') return obj.plain_text;
|
|
19
|
-
if (Array.isArray(obj.title)) {
|
|
20
|
-
return obj.title.map((t: Obj) => extractText(t?.text || t)).join('');
|
|
21
|
-
}
|
|
22
|
-
if (Array.isArray(obj.rich_text)) {
|
|
23
|
-
return obj.rich_text.map((t: Obj) => extractText(t?.text || t)).join('');
|
|
24
|
-
}
|
|
25
|
-
if (obj.text) return extractText(obj.text);
|
|
26
|
-
}
|
|
27
|
-
return '';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Extract a string from a select or status property. */
|
|
31
|
-
export function extractSelect(val: unknown): string {
|
|
32
|
-
if (typeof val === 'string') return val;
|
|
33
|
-
if (val && typeof val === 'object') {
|
|
34
|
-
const obj = val as Obj;
|
|
35
|
-
if (obj.select && typeof obj.select === 'object') {
|
|
36
|
-
return (obj.select as Obj).name as string || '';
|
|
37
|
-
}
|
|
38
|
-
if (obj.status && typeof obj.status === 'object') {
|
|
39
|
-
return (obj.status as Obj).name as string || '';
|
|
40
|
-
}
|
|
41
|
-
if (typeof obj.name === 'string') return obj.name;
|
|
42
|
-
}
|
|
43
|
-
return '';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Extract a date string from a date property. */
|
|
47
|
-
export function extractDate(val: unknown): string {
|
|
48
|
-
if (typeof val === 'string') return val;
|
|
49
|
-
if (val && typeof val === 'object') {
|
|
50
|
-
const obj = val as Obj;
|
|
51
|
-
if (obj.date && typeof obj.date === 'object') {
|
|
52
|
-
return (obj.date as Obj).start as string || '';
|
|
53
|
-
}
|
|
54
|
-
if (typeof obj.start === 'string') return obj.start;
|
|
55
|
-
}
|
|
56
|
-
return '';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Extract a number from a number property. */
|
|
60
|
-
export function extractNumber(val: unknown): number | null {
|
|
61
|
-
if (typeof val === 'number') return val;
|
|
62
|
-
if (val && typeof val === 'object') {
|
|
63
|
-
const obj = val as Obj;
|
|
64
|
-
if (typeof obj.number === 'number') return obj.number;
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Extract a boolean from a checkbox property. */
|
|
70
|
-
export function extractCheckbox(val: unknown): boolean {
|
|
71
|
-
if (typeof val === 'boolean') return val;
|
|
72
|
-
if (val && typeof val === 'object') {
|
|
73
|
-
const obj = val as Obj;
|
|
74
|
-
if (typeof obj.checkbox === 'boolean') return obj.checkbox;
|
|
75
|
-
}
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Extract multi-select values as a string array. */
|
|
80
|
-
export function extractMultiSelect(val: unknown): string[] {
|
|
81
|
-
if (Array.isArray(val)) {
|
|
82
|
-
return val
|
|
83
|
-
.map((v) => {
|
|
84
|
-
if (typeof v === 'string') return v;
|
|
85
|
-
if (v && typeof v === 'object' && typeof (v as Obj).name === 'string') return (v as Obj).name as string;
|
|
86
|
-
return '';
|
|
87
|
-
})
|
|
88
|
-
.filter(Boolean);
|
|
89
|
-
}
|
|
90
|
-
if (val && typeof val === 'object') {
|
|
91
|
-
const obj = val as Obj;
|
|
92
|
-
if (Array.isArray(obj.multi_select)) return extractMultiSelect(obj.multi_select);
|
|
93
|
-
}
|
|
94
|
-
if (typeof val === 'string' && val) return [val];
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Extract relation IDs from a relation property. */
|
|
99
|
-
export function extractRelation(val: unknown): string[] {
|
|
100
|
-
if (Array.isArray(val)) {
|
|
101
|
-
return val
|
|
102
|
-
.map((v) => {
|
|
103
|
-
if (typeof v === 'string') return v;
|
|
104
|
-
if (v && typeof v === 'object' && typeof (v as Obj).id === 'string') return (v as Obj).id as string;
|
|
105
|
-
return '';
|
|
106
|
-
})
|
|
107
|
-
.filter(Boolean);
|
|
108
|
-
}
|
|
109
|
-
if (val && typeof val === 'object') {
|
|
110
|
-
const obj = val as Obj;
|
|
111
|
-
if (Array.isArray(obj.relation)) return extractRelation(obj.relation);
|
|
112
|
-
}
|
|
113
|
-
if (typeof val === 'string' && val) return [val];
|
|
114
|
-
return [];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Extract display title from a document. Falls back through several property
|
|
119
|
-
* names and shapes to find a human-readable title.
|
|
120
|
-
*/
|
|
121
|
-
export function docTitle(doc: DocumentRecord): string {
|
|
122
|
-
const t = doc.title;
|
|
123
|
-
if (t && !/^[0-9a-f]{8}-[0-9a-f]{4}/.test(t)) return t;
|
|
124
|
-
const p = doc.properties || {};
|
|
125
|
-
const name = p.Name ?? p.name ?? p.Title ?? p.title;
|
|
126
|
-
if (name) {
|
|
127
|
-
const extracted = extractText(name);
|
|
128
|
-
if (extracted) return extracted;
|
|
129
|
-
}
|
|
130
|
-
return t || 'Untitled';
|
|
131
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useCallback, useState } from 'react';
|
|
4
|
-
import { useNotisRuntime } from '../provider';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Persist app-level state per user per app using localStorage.
|
|
8
|
-
*
|
|
9
|
-
* ```tsx
|
|
10
|
-
* const [order, setOrder] = useAppState<string[]>('task-order', []);
|
|
11
|
-
* ```
|
|
12
|
-
*
|
|
13
|
-
* State is scoped by `app.id` so different apps don't collide.
|
|
14
|
-
* Falls back to defaultValue when localStorage is unavailable.
|
|
15
|
-
*/
|
|
16
|
-
export function useAppState<T>(key: string, defaultValue: T): [T, (value: T | ((prev: T) => T)) => void] {
|
|
17
|
-
const runtime = useNotisRuntime();
|
|
18
|
-
const appId = runtime?.app?.id || '_unknown';
|
|
19
|
-
const storageKey = `notis-app-state:${appId}:${key}`;
|
|
20
|
-
|
|
21
|
-
const [value, setValueInternal] = useState<T>(() => {
|
|
22
|
-
if (typeof window === 'undefined') return defaultValue;
|
|
23
|
-
try {
|
|
24
|
-
const stored = localStorage.getItem(storageKey);
|
|
25
|
-
if (stored !== null) return JSON.parse(stored) as T;
|
|
26
|
-
} catch {
|
|
27
|
-
// Ignore parse errors or storage unavailable
|
|
28
|
-
}
|
|
29
|
-
return defaultValue;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const setValue = useCallback(
|
|
33
|
-
(newValue: T | ((prev: T) => T)) => {
|
|
34
|
-
setValueInternal((prev) => {
|
|
35
|
-
const resolved = typeof newValue === 'function'
|
|
36
|
-
? (newValue as (prev: T) => T)(prev)
|
|
37
|
-
: newValue;
|
|
38
|
-
try {
|
|
39
|
-
localStorage.setItem(storageKey, JSON.stringify(resolved));
|
|
40
|
-
} catch {
|
|
41
|
-
// Ignore quota errors
|
|
42
|
-
}
|
|
43
|
-
return resolved;
|
|
44
|
-
});
|
|
45
|
-
},
|
|
46
|
-
[storageKey],
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
return [value, setValue];
|
|
50
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import { useNotisRuntime } from '../provider';
|
|
5
|
-
import type { CollectionItem } from '../runtime';
|
|
6
|
-
|
|
7
|
-
interface UseCollectionResult {
|
|
8
|
-
/** Items in the collection (from the bound database). */
|
|
9
|
-
items: CollectionItem[];
|
|
10
|
-
loading: boolean;
|
|
11
|
-
error: Error | null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* List collection items for the current route. Only meaningful on routes
|
|
16
|
-
* that have a `collection` binding in `notis.config.ts`.
|
|
17
|
-
*
|
|
18
|
-
* ```tsx
|
|
19
|
-
* const { items, loading } = useCollectionItems();
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
export function useCollectionItems(): UseCollectionResult {
|
|
23
|
-
const runtime = useNotisRuntime();
|
|
24
|
-
const [items, setItems] = useState<CollectionItem[]>([]);
|
|
25
|
-
const [loading, setLoading] = useState(true);
|
|
26
|
-
const [error, setError] = useState<Error | null>(null);
|
|
27
|
-
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
if (!runtime) {
|
|
30
|
-
setLoading(false);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
let cancelled = false;
|
|
35
|
-
setLoading(true);
|
|
36
|
-
|
|
37
|
-
runtime
|
|
38
|
-
.listCollectionItems()
|
|
39
|
-
.then((result) => {
|
|
40
|
-
if (!cancelled) {
|
|
41
|
-
setItems(result.items);
|
|
42
|
-
setLoading(false);
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
.catch((err) => {
|
|
46
|
-
if (!cancelled) {
|
|
47
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
48
|
-
setLoading(false);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
return () => {
|
|
53
|
-
cancelled = true;
|
|
54
|
-
};
|
|
55
|
-
}, [runtime]);
|
|
56
|
-
|
|
57
|
-
return { items, loading, error };
|
|
58
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
-
import { useNotisRuntime } from '../provider';
|
|
5
|
-
import type { DocumentRecord } from '../runtime';
|
|
6
|
-
|
|
7
|
-
interface UseDocumentResult {
|
|
8
|
-
document: DocumentRecord | null;
|
|
9
|
-
loading: boolean;
|
|
10
|
-
error: Error | null;
|
|
11
|
-
refetch: () => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Fetch a single document by ID.
|
|
16
|
-
*
|
|
17
|
-
* ```tsx
|
|
18
|
-
* const { document, loading } = useDocument(documentId);
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
export function useDocument(documentId: string | null | undefined): UseDocumentResult {
|
|
22
|
-
const runtime = useNotisRuntime();
|
|
23
|
-
const [document, setDocument] = useState<DocumentRecord | null>(null);
|
|
24
|
-
const [loading, setLoading] = useState(true);
|
|
25
|
-
const [error, setError] = useState<Error | null>(null);
|
|
26
|
-
const [fetchKey, setFetchKey] = useState(0);
|
|
27
|
-
|
|
28
|
-
const refetch = useCallback(() => setFetchKey((k) => k + 1), []);
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (!runtime || !documentId) {
|
|
32
|
-
setLoading(false);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let cancelled = false;
|
|
37
|
-
setLoading(true);
|
|
38
|
-
setError(null);
|
|
39
|
-
|
|
40
|
-
runtime
|
|
41
|
-
.getDocument({ documentId })
|
|
42
|
-
.then((result) => {
|
|
43
|
-
if (!cancelled) {
|
|
44
|
-
setDocument(result);
|
|
45
|
-
setLoading(false);
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
.catch((err) => {
|
|
49
|
-
if (!cancelled) {
|
|
50
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
51
|
-
setLoading(false);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
return () => {
|
|
56
|
-
cancelled = true;
|
|
57
|
-
};
|
|
58
|
-
}, [runtime, documentId, fetchKey]);
|
|
59
|
-
|
|
60
|
-
return { document, loading, error, refetch };
|
|
61
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|