@notis_ai/cli 0.2.0-beta.191.1 → 0.2.0-beta.197.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +485 -86
- package/config/portal-route-roots.json +47 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +32663 -6357
- package/dist/base-skills/notis-apps/SKILL.md +88 -67
- package/dist/base-skills/notis-apps/references/architecture.md +114 -167
- package/dist/base-skills/notis-apps/references/design.md +26 -107
- package/dist/base-skills/notis-apps/references/memory.md +58 -0
- package/dist/base-skills/notis-apps/references/reading.md +52 -74
- package/dist/base-skills/notis-apps/references/release.md +104 -142
- package/dist/base-skills/notis-apps/references/sdk.md +113 -60
- package/dist/base-skills/notis-apps/references/troubleshooting.md +22 -21
- package/dist/base-skills/notis-apps/references/views.md +142 -0
- package/dist/base-skills/notis-cli/SKILL.md +4 -4
- package/dist/base-skills/notis-cli/references/app-delivery.md +12 -11
- package/dist/base-skills/notis-cli/references/native-databases.md +6 -6
- package/dist/base-skills/notis-cli/references/tool-examples.md +4 -4
- package/dist/base-skills/notis-query/SKILL.md +34 -60
- package/dist/base-skills/notis-query/references/database-discovery.md +59 -69
- package/dist/base-skills/notis-query/references/documents.md +63 -50
- package/dist/base-skills/notis-query/references/query.md +57 -524
- package/dist/sdk/src/bundleModule.ts +79 -0
- package/dist/sdk/src/components/DocumentEditor.tsx +6 -3
- package/dist/sdk/src/components/DocumentPage.tsx +141 -0
- package/dist/sdk/src/components/RecordComponents.tsx +33 -0
- package/dist/sdk/src/components/RenderChartData.tsx +19 -0
- package/dist/sdk/src/config.ts +4 -26
- package/dist/sdk/src/documents.ts +2 -0
- package/dist/sdk/src/hooks/useAction.ts +28 -0
- package/dist/sdk/src/hooks/useCloudComputer.ts +35 -4
- package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
- package/dist/sdk/src/hooks/useHandover.ts +5 -4
- package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
- package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
- package/dist/sdk/src/hooks/useNotis.ts +3 -0
- package/dist/sdk/src/hooks/useNotisNavigation.ts +26 -11
- package/dist/sdk/src/hooks/useQuery.ts +7 -4
- package/dist/sdk/src/hooks/useViewParams.ts +121 -0
- package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
- package/dist/sdk/src/index.ts +36 -0
- package/dist/sdk/src/interactions/shortcuts.tsx +17 -3
- package/dist/sdk/src/nativeData.ts +39 -0
- package/dist/sdk/src/presentation.tsx +82 -0
- package/dist/sdk/src/provider.tsx +9 -4
- package/dist/sdk/src/queryCache.ts +5 -3
- package/dist/sdk/src/queryErrors.ts +51 -0
- package/dist/sdk/src/runtime.ts +146 -8
- package/dist/sdk/src/space.ts +425 -0
- package/dist/sdk/src/styles.css +96 -72
- package/dist/sdk/src/vite.ts +11 -1
- package/dist/skill-sync/index.js +458 -68
- package/dist/skill-sync/index.js.map +4 -4
- package/dist/skill-sync-worker.mjs +451 -77
- package/dist/space-harness/host.js +66 -0
- package/dist/view-renderer/dist/host.css +1 -0
- package/dist/view-renderer/dist/host.js +1042 -0
- package/dist/view-renderer/dist/theme.css +73 -0
- package/dist/view-renderer/src/dom.js +180 -0
- package/dist/view-renderer/src/errors.js +8 -0
- package/dist/view-renderer/src/host.tsx +173 -0
- package/dist/view-renderer/src/index.js +4 -0
- package/dist/view-renderer/src/pool.js +76 -0
- package/dist/view-renderer/src/remote-broker.js +59 -0
- package/dist/view-renderer/src/renderer.js +201 -0
- package/package.json +11 -5
- package/skills/notis-apps/cli.md +270 -163
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +3 -3
- package/skills/notis-onboarding/BRIEF.md +7 -3
- package/src/command-specs/apps.js +1 -8
- package/src/command-specs/files.js +29 -0
- package/src/command-specs/handover.js +45 -16
- package/src/command-specs/helpers.js +1 -0
- package/src/command-specs/index.js +10 -4
- package/src/command-specs/skills.js +229 -0
- package/src/command-specs/spaces.js +572 -0
- package/src/command-specs/views.js +69 -0
- package/src/runtime/agent-browser.js +30 -7
- package/src/runtime/app-boundary-validator.js +7 -4
- package/src/runtime/app-data-literals.js +82 -0
- package/src/runtime/app-platform.js +321 -253
- package/src/runtime/confined-space-build.js +74 -0
- package/src/runtime/confined-space-verify.js +49 -0
- package/src/runtime/native-record-files.js +47 -0
- package/src/runtime/retired-sdk-files.js +20 -0
- package/src/runtime/skill-sync/cloud-client.ts +26 -4
- package/src/runtime/skill-sync/index.ts +154 -35
- package/src/runtime/skill-sync/local-scanner.ts +125 -17
- package/src/runtime/skill-sync/native-identity.ts +102 -0
- package/src/runtime/skill-sync/native-plan.ts +59 -0
- package/src/runtime/skill-sync/sync-plan.ts +16 -9
- package/src/runtime/skill-sync/types.ts +32 -6
- package/src/runtime/source-root.js +32 -0
- package/src/runtime/space-collection-scaffold.js +66 -0
- package/src/runtime/space-database-structures.js +229 -0
- package/src/runtime/space-fixture-broker.js +50 -0
- package/src/runtime/space-lock.js +337 -0
- package/src/runtime/space-platform.js +105 -0
- package/src/runtime/space-source.js +281 -0
- package/src/runtime/space-test-server.js +327 -0
- package/src/runtime/space-verification.js +89 -0
- package/src/runtime/space-verify-browser-worker.js +51 -0
- package/src/runtime/space-view-manifest.js +254 -0
- package/src/runtime/sync-skills.js +1 -1
- package/src/runtime/transport.js +8 -1
- package/src/runtime/view-render.js +49 -0
- package/src/space-harness-host.ts +103 -0
- package/template/app/globals.css +1 -0
- package/src/command-specs/reports.js +0 -97
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import { normalizeDocumentRecord } from '../documents';
|
|
6
|
+
import type { DocumentRecord, NotisRuntime } from '../runtime';
|
|
7
|
+
import type { NativeDelete, NativeInsert, NativeMutationReceipt, NativeQuery, NativeUpdate } from '../nativeData';
|
|
8
|
+
import { useQuery, type UseQueryResult } from './useQuery';
|
|
9
|
+
import { SdkQueryError } from '../queryErrors';
|
|
10
|
+
|
|
11
|
+
function actionFor(runtime: NotisRuntime | null, binding: string, operation: 'query' | 'insert' | 'update' | 'delete'): string {
|
|
12
|
+
const descriptor = runtime?.space?.bindings[binding];
|
|
13
|
+
const action = descriptor?.kind === 'database' ? descriptor.operations?.[operation] : undefined;
|
|
14
|
+
if (runtime?.resource?.kind !== 'space' || !runtime.callAction || !action) {
|
|
15
|
+
const diagnostic = `This Space does not expose ${operation} for database binding "${binding}".`;
|
|
16
|
+
if (operation === 'query') throw new SdkQueryError('bindingUnavailable', diagnostic);
|
|
17
|
+
throw new Error(`This Space does not expose ${operation} for that database binding.`);
|
|
18
|
+
}
|
|
19
|
+
return action;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A declared query adapter, never an account-wide slug or owner lookup.
|
|
24
|
+
*
|
|
25
|
+
* `subscribe: true` follows the binding's live change feed where the host has one (a
|
|
26
|
+
* signed-in Editor of the Space, owner or teammate): `live` is then true and changes made
|
|
27
|
+
* elsewhere (another device, a teammate, an agent, an automation) re-read the rows on their
|
|
28
|
+
* own. Where `live` is false, keep offering a manual reload.
|
|
29
|
+
*/
|
|
30
|
+
export function useNativeDocuments(binding: string, options: {
|
|
31
|
+
query?: NativeQuery; fetchAll?: boolean; enabled?: boolean; staleTimeMs?: number; subscribe?: boolean;
|
|
32
|
+
} = {}): UseQueryResult<{ documents: DocumentRecord[]; schemaRevision: number }> & { documents: DocumentRecord[]; schemaRevision?: number; live: boolean } {
|
|
33
|
+
const runtime = useNotisRuntime();
|
|
34
|
+
const [live, setLive] = useState(false);
|
|
35
|
+
const follow = options.subscribe === true && options.enabled !== false;
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!follow || !runtime?.subscribeDatabase) { setLive(false); return; }
|
|
38
|
+
let cancelled = false;
|
|
39
|
+
// The Space host re-reads every query of the view when the database changes, so the
|
|
40
|
+
// change callback has nothing left to do; the feed is a signal and never carries rows.
|
|
41
|
+
const stop = runtime.subscribeDatabase(binding, () => {}, { onStatusChange: value => { if (!cancelled) setLive(value); } });
|
|
42
|
+
return () => { cancelled = true; setLive(false); stop?.(); };
|
|
43
|
+
}, [runtime, binding, follow]);
|
|
44
|
+
const query = useQuery(['native-documents', binding, options.query ?? {}, Boolean(options.fetchAll)], async () => {
|
|
45
|
+
const action = actionFor(runtime, binding, 'query');
|
|
46
|
+
if (options.query?.mode && !['rows', 'search'].includes(options.query.mode)) throw new Error('Use useActionQuery for counts and aggregates.');
|
|
47
|
+
const rows: unknown[] = [];
|
|
48
|
+
let offset = options.query?.offset ?? 0;
|
|
49
|
+
let revision: number | undefined;
|
|
50
|
+
while (true) {
|
|
51
|
+
const result = await runtime!.callAction!<{ rows: unknown[]; schema_revision: number; has_more: boolean; next_offset: number | null }>(
|
|
52
|
+
action, { request: { ...options.query, offset } }, { readOnly: true, dedupe: true, schemaRevision: revision });
|
|
53
|
+
if (!result || !Array.isArray(result.rows) || !Number.isSafeInteger(result.schema_revision)) throw new SdkQueryError('responseIncomplete', 'Native query must return rows and a safe integer schema_revision.');
|
|
54
|
+
if (revision !== undefined && revision !== result.schema_revision) throw new SdkQueryError('schemaChanged');
|
|
55
|
+
revision = result.schema_revision;
|
|
56
|
+
rows.push(...result.rows);
|
|
57
|
+
if (!options.fetchAll || !result.has_more) break;
|
|
58
|
+
if (typeof result.next_offset !== 'number' || result.next_offset <= offset) throw new SdkQueryError('responseIncomplete', 'Native query pagination did not advance next_offset.');
|
|
59
|
+
offset = result.next_offset;
|
|
60
|
+
}
|
|
61
|
+
return { documents: rows.map(normalizeDocumentRecord), schemaRevision: revision! };
|
|
62
|
+
}, { readOnly: true, enabled: options.enabled, staleTimeMs: options.staleTimeMs });
|
|
63
|
+
return { ...query, documents: query.data?.documents ?? EMPTY_DOCUMENTS, schemaRevision: query.data?.schemaRevision, live };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Explicit insert/update/trash; update never silently creates a missing row. */
|
|
67
|
+
export function useNativeMutation<TOperation extends 'insert' | 'update' | 'delete'>(binding: string, operation: TOperation): {
|
|
68
|
+
mutate: (input: TOperation extends 'insert' ? NativeInsert : TOperation extends 'update' ? NativeUpdate : NativeDelete,
|
|
69
|
+
options: { schemaRevision: number; requestId: string }) => Promise<NativeMutationReceipt>;
|
|
70
|
+
loading: boolean; error: Error | null;
|
|
71
|
+
} {
|
|
72
|
+
const runtime = useNotisRuntime();
|
|
73
|
+
const [pending, setPending] = useState(0), [error, setError] = useState<Error | null>(null);
|
|
74
|
+
const mutate = useCallback(async (input: NativeInsert | NativeUpdate | NativeDelete, options: { schemaRevision: number; requestId: string }) => {
|
|
75
|
+
const action = actionFor(runtime, binding, operation);
|
|
76
|
+
setPending((value) => value + 1); setError(null);
|
|
77
|
+
try { return await runtime!.callAction!<NativeMutationReceipt>(action, { request: input }, options); }
|
|
78
|
+
catch (cause) { const failure = cause instanceof Error ? cause : new Error(String(cause)); setError(failure); throw failure; }
|
|
79
|
+
finally { setPending((value) => value - 1); }
|
|
80
|
+
}, [runtime, binding, operation]);
|
|
81
|
+
return { mutate, loading: pending > 0, error };
|
|
82
|
+
}
|
|
83
|
+
const EMPTY_DOCUMENTS: DocumentRecord[] = [];
|
|
@@ -4,6 +4,8 @@ import { useNotisRuntime } from '../provider';
|
|
|
4
4
|
import type { RuntimeResource, AppDescriptor, CollectionItemDetail, DatabaseDescriptor, RouteDescriptor } from '../runtime';
|
|
5
5
|
|
|
6
6
|
interface NotisContext {
|
|
7
|
+
/** UI selection only; it grants no access and is never a record scope. */
|
|
8
|
+
selection: string | null;
|
|
7
9
|
resource: RuntimeResource | null;
|
|
8
10
|
/** App metadata (id, name, icon, description). Null before runtime loads. */
|
|
9
11
|
app: AppDescriptor | null;
|
|
@@ -28,6 +30,7 @@ export function useNotis(): NotisContext {
|
|
|
28
30
|
const runtime = useNotisRuntime();
|
|
29
31
|
|
|
30
32
|
return {
|
|
33
|
+
selection: runtime?.context?.selection ?? null,
|
|
31
34
|
resource: runtime?.resource ?? null,
|
|
32
35
|
app: runtime?.app ?? null,
|
|
33
36
|
route: runtime?.route ?? null,
|
|
@@ -3,28 +3,49 @@
|
|
|
3
3
|
import { useCallback } from 'react';
|
|
4
4
|
import { useNotisRuntime } from '../provider';
|
|
5
5
|
|
|
6
|
+
export interface SpaceNavigationOptions {
|
|
7
|
+
recordKey?: string | null;
|
|
8
|
+
/** Declared view parameters. The destination authorizes records and drops unknown names. */
|
|
9
|
+
params?: Record<string, string | number | boolean | null | undefined>;
|
|
10
|
+
selection?: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
interface NavigationActions {
|
|
14
|
+
/** Resolve a declared portable destination under the current viewer's access. */
|
|
15
|
+
toNamedSpace: (alias: string, options?: SpaceNavigationOptions) => Promise<void>;
|
|
16
|
+
/** Open an exact Space, optionally at a native record's stable record_key. */
|
|
17
|
+
toSpace: (spaceId: string, options?: SpaceNavigationOptions) => void;
|
|
7
18
|
/** Navigate to a route within the app by its path. */
|
|
8
19
|
toRoute: (path: string, options?: { resourceId?: string | null }) => void;
|
|
9
|
-
/** Navigate to a document detail view. */
|
|
10
|
-
toDocument: (documentId: string, title?: string | null) => void;
|
|
11
20
|
/** Navigate to the app's default route. */
|
|
12
21
|
toApp: () => void;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
/**
|
|
16
|
-
* Navigation helpers for
|
|
25
|
+
* Navigation helpers for Spaces and app routes.
|
|
17
26
|
* When rendered inside the portal, uses the runtime.navigate() bridge.
|
|
18
27
|
* Without a portal runtime, falls back to window.location for route navigation.
|
|
19
28
|
*
|
|
20
29
|
* ```tsx
|
|
21
30
|
* const nav = useNotisNavigation();
|
|
22
|
-
* nav.
|
|
31
|
+
* nav.toSpace(spaceId, { recordKey });
|
|
23
32
|
* ```
|
|
24
33
|
*/
|
|
25
34
|
export function useNotisNavigation(): NavigationActions {
|
|
26
35
|
const runtime = useNotisRuntime();
|
|
27
36
|
|
|
37
|
+
const toNamedSpace = useCallback(async (alias: string, options?: SpaceNavigationOptions) => {
|
|
38
|
+
if (runtime?.resource?.kind !== 'space' || !runtime.navigate) throw new Error('Open this Space in a current Notis host to navigate.');
|
|
39
|
+
await runtime.navigate({ kind: 'space-reference', alias, documentId: options?.recordKey ?? null, selection: options?.selection ?? null,
|
|
40
|
+
...(options?.params ? { params: options.params } : {}) });
|
|
41
|
+
}, [runtime]);
|
|
42
|
+
|
|
43
|
+
const toSpace = useCallback((spaceId: string, options?: SpaceNavigationOptions) => {
|
|
44
|
+
if (!runtime?.navigate) throw new Error('Open this Space in a current Notis host to navigate.');
|
|
45
|
+
runtime.navigate({ kind: 'space', spaceId, documentId: options?.recordKey ?? null,
|
|
46
|
+
...(options?.params ? { params: options.params } : {}), ...(options?.selection ? { selection: options.selection } : {}) });
|
|
47
|
+
}, [runtime]);
|
|
48
|
+
|
|
28
49
|
const toRoute = useCallback((path: string, options?: { resourceId?: string | null }) => {
|
|
29
50
|
if (runtime?.navigate) {
|
|
30
51
|
runtime.navigate({ kind: 'route', path, resourceId: options?.resourceId ?? null });
|
|
@@ -36,17 +57,11 @@ export function useNotisNavigation(): NavigationActions {
|
|
|
36
57
|
}
|
|
37
58
|
}, [runtime]);
|
|
38
59
|
|
|
39
|
-
const toDocument = useCallback((documentId: string, title?: string | null) => {
|
|
40
|
-
if (runtime?.navigate) {
|
|
41
|
-
runtime.navigate({ kind: 'document', documentId, title: title ?? undefined });
|
|
42
|
-
}
|
|
43
|
-
}, [runtime]);
|
|
44
|
-
|
|
45
60
|
const toApp = useCallback(() => {
|
|
46
61
|
if (runtime?.navigate) {
|
|
47
62
|
runtime.navigate({ kind: 'app' });
|
|
48
63
|
}
|
|
49
64
|
}, [runtime]);
|
|
50
65
|
|
|
51
|
-
return {
|
|
66
|
+
return { toNamedSpace, toSpace, toRoute, toApp };
|
|
52
67
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
4
4
|
import { useNotisRuntime } from '../provider';
|
|
5
5
|
import { createQueryClient, queryKey } from '../queryCache';
|
|
6
|
+
import { spaceQueryKey } from '../space';
|
|
7
|
+
import { localizeQueryError } from '../queryErrors';
|
|
6
8
|
|
|
7
9
|
export interface UseQueryOptions {
|
|
8
10
|
/** Required acknowledgement: only idempotent reads may run on mount or prefetch. */
|
|
@@ -26,7 +28,7 @@ export function useQuery<T>(key: readonly unknown[], read: () => Promise<T>, opt
|
|
|
26
28
|
// Older hosts deliberately get a hook-local cache: no cross-runtime/account reuse.
|
|
27
29
|
const fallback = useMemo(() => createQueryClient(), [runtime]);
|
|
28
30
|
const client = runtime?.queryClient ?? fallback;
|
|
29
|
-
const cacheKey = queryKey(key);
|
|
31
|
+
const cacheKey = queryKey(spaceQueryKey(runtime, key));
|
|
30
32
|
const enabled = Boolean(runtime) && options.enabled !== false && options.readOnly === true;
|
|
31
33
|
const readRef = useRef(read);
|
|
32
34
|
useLayoutEffect(() => { readRef.current = read; });
|
|
@@ -45,12 +47,13 @@ export function useQuery<T>(key: readonly unknown[], read: () => Promise<T>, opt
|
|
|
45
47
|
client.invalidate(cacheKey);
|
|
46
48
|
await fetch().catch(() => {});
|
|
47
49
|
}, [cacheKey, client, enabled, fetch]);
|
|
50
|
+
const error = useMemo(() => localizeQueryError(snapshot.error, runtime?.context?.locale), [snapshot.error, runtime?.context?.locale]);
|
|
48
51
|
return {
|
|
49
52
|
data: enabled ? snapshot.data : undefined,
|
|
50
53
|
hasData: enabled && snapshot.hasData,
|
|
51
54
|
loading: enabled && !snapshot.hasData && !snapshot.error,
|
|
52
55
|
isFetching: enabled && snapshot.isFetching,
|
|
53
|
-
error: enabled ?
|
|
56
|
+
error: enabled ? error : null,
|
|
54
57
|
refetch,
|
|
55
58
|
};
|
|
56
59
|
}
|
|
@@ -62,10 +65,10 @@ export function useQueryClient(): {
|
|
|
62
65
|
} {
|
|
63
66
|
const runtime = useNotisRuntime();
|
|
64
67
|
return useMemo(() => ({
|
|
65
|
-
invalidate: (key?: readonly unknown[]) => runtime?.queryClient?.invalidate(key ? queryKey(key) : undefined),
|
|
68
|
+
invalidate: (key?: readonly unknown[]) => runtime?.queryClient?.invalidate(key ? queryKey(spaceQueryKey(runtime, key)) : undefined),
|
|
66
69
|
prefetch: <T,>(key: readonly unknown[], read: () => Promise<T>, options: UseQueryOptions) => {
|
|
67
70
|
if (!runtime?.queryClient || options.enabled === false || options.readOnly !== true) return Promise.resolve(undefined);
|
|
68
|
-
return runtime.queryClient.prefetch(queryKey(key), read, { staleTimeMs: options.staleTimeMs });
|
|
71
|
+
return runtime.queryClient.prefetch(queryKey(spaceQueryKey(runtime, key)), read, { staleTimeMs: options.staleTimeMs });
|
|
69
72
|
},
|
|
70
73
|
}), [runtime]);
|
|
71
74
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import { useQuery, type UseQueryResult } from './useQuery';
|
|
6
|
+
import { SdkQueryError } from '../queryErrors';
|
|
7
|
+
import type { SpaceDefinition, SpaceShownOptions, SpaceShownResult, SpaceViewParam } from '../space';
|
|
8
|
+
|
|
9
|
+
type ParamValue<P> = P extends { type: 'enum'; values: readonly (infer V)[] } ? V
|
|
10
|
+
: P extends { type: 'number' } ? number : P extends { type: 'boolean' } ? boolean : string;
|
|
11
|
+
|
|
12
|
+
/** Typed params of a definition: `useViewParams<typeof definition>()`. */
|
|
13
|
+
export type ViewParamsOf<D extends Pick<SpaceDefinition, 'params'>> = D['params'] extends Record<string, SpaceViewParam>
|
|
14
|
+
? { [K in keyof D['params']]?: ParamValue<D['params'][K]> } : Record<string, string | number | boolean | undefined>;
|
|
15
|
+
|
|
16
|
+
export interface ViewParams<T> {
|
|
17
|
+
/** Declared params only, typed; invalid URL values are left out (their default applies). */
|
|
18
|
+
params: T;
|
|
19
|
+
/** Declared params whose URL value was invalid and ignored. */
|
|
20
|
+
invalid: string[];
|
|
21
|
+
/** Required params without a value. */
|
|
22
|
+
missing: string[];
|
|
23
|
+
declared: Record<string, SpaceViewParam>;
|
|
24
|
+
ready: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const RECORD_KEY = /^(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
28
|
+
const NUMBER = /^-?\d+(?:\.\d+)?$/;
|
|
29
|
+
// eslint-disable-next-line no-control-regex
|
|
30
|
+
const CONTROL = /[\u0000-\u001f\u007f]/;
|
|
31
|
+
|
|
32
|
+
function parseValue(declaration: SpaceViewParam, raw: string): string | number | boolean | undefined {
|
|
33
|
+
switch (declaration.type) {
|
|
34
|
+
case 'text': return raw.length <= 500 && !CONTROL.test(raw) ? raw : undefined;
|
|
35
|
+
case 'enum': return declaration.values?.includes(raw) ? raw : undefined;
|
|
36
|
+
case 'number': { const value = Number(raw); return NUMBER.test(raw) && Number.isFinite(value) ? value : undefined; }
|
|
37
|
+
case 'boolean': return raw === 'true' ? true : raw === 'false' ? false : undefined;
|
|
38
|
+
case 'date': {
|
|
39
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) return undefined;
|
|
40
|
+
const parsed = new Date(`${raw}T00:00:00Z`);
|
|
41
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === raw ? raw : undefined;
|
|
42
|
+
}
|
|
43
|
+
case 'record': {
|
|
44
|
+
if (!RECORD_KEY.test(raw)) return undefined;
|
|
45
|
+
const hex = raw.replaceAll('-', '').toLowerCase();
|
|
46
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
47
|
+
}
|
|
48
|
+
default: return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Keep declared, valid values only; unknown params are dropped and defaults applied. */
|
|
53
|
+
export function parseViewParams(declared: Record<string, SpaceViewParam> | undefined,
|
|
54
|
+
raw: Record<string, string> | null | undefined): Omit<ViewParams<Record<string, string | number | boolean>>, 'declared' | 'ready'> {
|
|
55
|
+
const params: Record<string, string | number | boolean> = {};
|
|
56
|
+
const invalid: string[] = [], missing: string[] = [];
|
|
57
|
+
for (const [name, declaration] of Object.entries(declared || {})) {
|
|
58
|
+
const value = raw && Object.hasOwn(raw, name) && typeof raw[name] === 'string' && raw[name] !== '' ? raw[name] : undefined;
|
|
59
|
+
const parsed = value === undefined ? undefined : parseValue(declaration, value);
|
|
60
|
+
if (value !== undefined && parsed === undefined) invalid.push(name);
|
|
61
|
+
if (parsed !== undefined) params[name] = parsed;
|
|
62
|
+
else if (declaration.default !== undefined) params[name] = declaration.default;
|
|
63
|
+
else if (declaration.required) missing.push(name);
|
|
64
|
+
}
|
|
65
|
+
return { params, invalid, missing };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The view's declared URL params, typed and validated. Unknown params (such as `?foo=`) never
|
|
70
|
+
* reach the view. Record params are record keys; the server authorizes them where it uses them.
|
|
71
|
+
*/
|
|
72
|
+
export function useViewParams<D extends Pick<SpaceDefinition, 'params'> = SpaceDefinition>(): ViewParams<ViewParamsOf<D>> {
|
|
73
|
+
const runtime = useNotisRuntime();
|
|
74
|
+
const declared = runtime?.space?.params;
|
|
75
|
+
const raw = runtime?.context?.params;
|
|
76
|
+
return useMemo(() => ({ ...parseViewParams(declared, raw), declared: declared || {}, ready: runtime !== null }) as ViewParams<ViewParamsOf<D>>,
|
|
77
|
+
[declared, raw, runtime]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface UseShownOptions extends SpaceShownOptions {
|
|
81
|
+
enabled?: boolean;
|
|
82
|
+
staleTimeMs?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** True when a declared list has an authorized host reader (Editor or exact record Site). */
|
|
86
|
+
export function useShownAvailable(name: string): boolean {
|
|
87
|
+
const runtime = useNotisRuntime();
|
|
88
|
+
const list = runtime?.space?.shows && Object.hasOwn(runtime.space.shows, name) ? runtime.space.shows[name] : null;
|
|
89
|
+
return runtime?.resource?.kind === 'space' && typeof runtime.shown === 'function' && Boolean(list && 'database' in list);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Runs exactly the list this view declares in `shows` with the current params, so the declaration
|
|
94
|
+
* is the list the view displays. Only sorting and paging are chosen here.
|
|
95
|
+
*
|
|
96
|
+
* A list this view context cannot read (undeclared, described only, or no host reader) answers
|
|
97
|
+
* with `error` instead of a query that never starts, so the view can show an error state. Missing
|
|
98
|
+
* required params are not an error: they are listed by `useViewParams().missing`.
|
|
99
|
+
*/
|
|
100
|
+
export function useShown<TRow = Record<string, unknown>>(name: string, options: UseShownOptions = {}):
|
|
101
|
+
UseQueryResult<SpaceShownResult & { rows: TRow[] }> {
|
|
102
|
+
const runtime = useNotisRuntime();
|
|
103
|
+
const available = useShownAvailable(name);
|
|
104
|
+
const { params, missing } = useViewParams();
|
|
105
|
+
const { sorts, pageSize, offset, includeContent } = options;
|
|
106
|
+
const list = runtime?.space?.shows && Object.hasOwn(runtime.space.shows, name) ? runtime.space.shows[name] : null;
|
|
107
|
+
const reads = list && 'database' in list && Array.isArray(list.params) ? list.params : null;
|
|
108
|
+
// Rows are keyed by the params the list reads: picking another record in the view keeps the rows
|
|
109
|
+
// already shown instead of reading the same list again. The read itself always sends every param.
|
|
110
|
+
const keyed = useMemo(() => reads ? Object.fromEntries(Object.entries(params).filter(([param]) => reads.includes(param))) : params,
|
|
111
|
+
[params, reads]);
|
|
112
|
+
const result = useQuery(['shown', name, keyed, sorts ?? null, pageSize ?? null, offset ?? null, includeContent ?? false], async () => {
|
|
113
|
+
if (!runtime?.shown) throw new SdkQueryError('listUnavailable');
|
|
114
|
+
return runtime.shown<SpaceShownResult & { rows: TRow[] }>(name, params as Record<string, unknown>,
|
|
115
|
+
{ sorts, pageSize, offset, includeContent });
|
|
116
|
+
}, { readOnly: true, enabled: available && !missing.length && options.enabled !== false, staleTimeMs: options.staleTimeMs });
|
|
117
|
+
const unavailable = runtime !== null && !available && options.enabled !== false;
|
|
118
|
+
const error = useMemo(() => unavailable ? new SdkQueryError('listUnavailable', `The list "${name}" is unavailable in this view: declare it in \`shows\` `
|
|
119
|
+
+ `with a database, or gate it on useShownAvailable("${name}").`, runtime?.context?.locale) : null, [unavailable, name, runtime?.context?.locale]);
|
|
120
|
+
return unavailable ? { ...result, loading: false, isFetching: false, error } : result;
|
|
121
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useNotisRuntime } from '../provider';
|
|
4
|
+
import { useQuery, type UseQueryResult } from './useQuery';
|
|
5
|
+
import {
|
|
6
|
+
SPACE_VIEWER_READ_OPERATIONS,
|
|
7
|
+
type SpaceViewerDatabase,
|
|
8
|
+
type SpaceViewerReadFamily,
|
|
9
|
+
type SpaceViewerReadInput,
|
|
10
|
+
type SpaceViewerReadOperation,
|
|
11
|
+
type SpaceViewerReadResult,
|
|
12
|
+
type SpaceViewerSkill,
|
|
13
|
+
} from '../space';
|
|
14
|
+
|
|
15
|
+
export interface UseViewerReadOptions {
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
staleTimeMs?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Bounded crawl of the viewer's Skill inventory: 50 pages of 100.
|
|
21
|
+
const MAX_SKILL_PAGES = 50;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* True when this page may read what the signed-in viewer can open: the Space declares
|
|
25
|
+
* `viewerReads` for this family and the host offers it (never on a Site or anonymous link).
|
|
26
|
+
* Render a fallback when false.
|
|
27
|
+
*/
|
|
28
|
+
export function useViewerReadAvailable(family: SpaceViewerReadFamily): boolean {
|
|
29
|
+
const runtime = useNotisRuntime();
|
|
30
|
+
return runtime?.resource?.kind === 'space' && typeof runtime.viewerRead === 'function'
|
|
31
|
+
&& Boolean(runtime.space?.viewerReads?.includes(family));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One cached, read-only viewer read. Results are scoped to the current viewer by the host cache scope. */
|
|
35
|
+
export function useViewerRead<TOperation extends SpaceViewerReadOperation>(operation: TOperation,
|
|
36
|
+
input: SpaceViewerReadInput[TOperation], options: UseViewerReadOptions = {}): UseQueryResult<SpaceViewerReadResult[TOperation]> {
|
|
37
|
+
const runtime = useNotisRuntime();
|
|
38
|
+
const available = useViewerReadAvailable(SPACE_VIEWER_READ_OPERATIONS[operation]);
|
|
39
|
+
return useQuery(['viewer-read', operation, input], async () => {
|
|
40
|
+
if (!runtime?.viewerRead) throw new Error('Viewer reads are unavailable here.');
|
|
41
|
+
return runtime.viewerRead<SpaceViewerReadResult[TOperation]>(operation, input as Record<string, unknown>);
|
|
42
|
+
}, { readOnly: true, enabled: available && options.enabled !== false, staleTimeMs: options.staleTimeMs });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Every database the signed-in viewer can open, with its available links and read target. */
|
|
46
|
+
export function useViewerDatabases(options: UseViewerReadOptions = {}): UseQueryResult<SpaceViewerDatabase[]> {
|
|
47
|
+
const runtime = useNotisRuntime();
|
|
48
|
+
const available = useViewerReadAvailable('databases');
|
|
49
|
+
return useQuery(['viewer-databases'], async () => {
|
|
50
|
+
if (!runtime?.viewerRead) throw new Error('Viewer reads are unavailable here.');
|
|
51
|
+
const result = await runtime.viewerRead<SpaceViewerReadResult['list_databases']>('list_databases', {});
|
|
52
|
+
if (!Array.isArray(result?.databases)) throw new Error('The database list is incomplete.');
|
|
53
|
+
return result.databases;
|
|
54
|
+
}, { readOnly: true, enabled: available && options.enabled !== false, staleTimeMs: options.staleTimeMs });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type ListSkillsRead = (input: SpaceViewerReadInput['list_skills']) => Promise<SpaceViewerReadResult['list_skills']>;
|
|
58
|
+
|
|
59
|
+
// Content pages read at once. Each one holds an API worker while its snapshots are read, and the API
|
|
60
|
+
// has only a few, so a large inventory never takes them all: two pages, plus one quick cursor read.
|
|
61
|
+
export const SKILL_CONTENT_PAGES_AT_ONCE = 2;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Every Skill page, in order. Pages with instructions are slow (each reads its snapshots), so the next
|
|
65
|
+
* page's cursor comes from a quick read without them and that page starts while the previous one is
|
|
66
|
+
* still read, at most SKILL_CONTENT_PAGES_AT_ONCE content pages at a time. A cursor that no longer
|
|
67
|
+
* matches (the list changed meanwhile) drops the pages started from it, waits for them to end and
|
|
68
|
+
* continues from the real cursor.
|
|
69
|
+
*/
|
|
70
|
+
export async function readAllViewerSkills(read: ListSkillsRead,
|
|
71
|
+
{ includeContent, includeDisabled }: { includeContent: boolean; includeDisabled: boolean }): Promise<SpaceViewerSkill[]> {
|
|
72
|
+
const page = (after: string | null, content: boolean) => read({ limit: 100, include_content: content,
|
|
73
|
+
include_disabled: includeDisabled, ...(after ? { after } : {}) }).then(result => {
|
|
74
|
+
if (!Array.isArray(result?.skills)) throw new Error('The Skill list is incomplete.');
|
|
75
|
+
return result;
|
|
76
|
+
});
|
|
77
|
+
const skills: SpaceViewerSkill[] = [];
|
|
78
|
+
// The pages started and not read yet, in order, each with the cursor it started from.
|
|
79
|
+
const started: { after: string | null; result: Promise<SpaceViewerReadResult['list_skills']> }[] = [];
|
|
80
|
+
// Where the next page to start begins, and whether a quick read (or the last page read) says it exists.
|
|
81
|
+
let cursor: string | null = null;
|
|
82
|
+
let known = true;
|
|
83
|
+
let pages = 0;
|
|
84
|
+
for (;;) {
|
|
85
|
+
if (known && started.length < (includeContent ? SKILL_CONTENT_PAGES_AT_ONCE : 1) && pages + started.length < MAX_SKILL_PAGES) {
|
|
86
|
+
const current = { after: cursor, result: page(cursor, includeContent) };
|
|
87
|
+
void current.result.catch(() => {});
|
|
88
|
+
started.push(current);
|
|
89
|
+
known = false;
|
|
90
|
+
if (includeContent) {
|
|
91
|
+
const probe = await page(cursor, false);
|
|
92
|
+
known = Boolean(probe.has_more && probe.next);
|
|
93
|
+
cursor = probe.next ?? null;
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const head = started.shift();
|
|
98
|
+
if (!head) break;
|
|
99
|
+
const result = await head.result;
|
|
100
|
+
skills.push(...result.skills);
|
|
101
|
+
pages += 1;
|
|
102
|
+
if (!result.has_more || !result.next) return skills;
|
|
103
|
+
// A page started ahead is kept only when it continues exactly where this one ended; otherwise (and
|
|
104
|
+
// when none was started) the next page starts from this page's own cursor.
|
|
105
|
+
if (started[0]?.after !== result.next) {
|
|
106
|
+
const dropped = started.splice(0);
|
|
107
|
+
await Promise.allSettled(dropped.map(entry => entry.result));
|
|
108
|
+
cursor = result.next;
|
|
109
|
+
known = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
throw new Error('This account has more Skills than a page can read at once.');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Every Skill the signed-in viewer can see, all pages. With `includeContent`, `skill_md`
|
|
117
|
+
* is present where the viewer holds content access and null elsewhere (curated, legacy).
|
|
118
|
+
*/
|
|
119
|
+
export function useViewerSkills(options: UseViewerReadOptions & { includeContent?: boolean; includeDisabled?: boolean } = {}):
|
|
120
|
+
UseQueryResult<SpaceViewerSkill[]> {
|
|
121
|
+
const runtime = useNotisRuntime();
|
|
122
|
+
const available = useViewerReadAvailable('skills');
|
|
123
|
+
const includeContent = options.includeContent === true, includeDisabled = options.includeDisabled === true;
|
|
124
|
+
return useQuery(['viewer-skills', includeContent, includeDisabled], async () => {
|
|
125
|
+
if (!runtime?.viewerRead) throw new Error('Viewer reads are unavailable here.');
|
|
126
|
+
return readAllViewerSkills((input: SpaceViewerReadInput['list_skills']) => runtime.viewerRead!('list_skills', input),
|
|
127
|
+
{ includeContent, includeDisabled });
|
|
128
|
+
}, { readOnly: true, enabled: available && options.enabled !== false, staleTimeMs: options.staleTimeMs });
|
|
129
|
+
}
|
package/dist/sdk/src/index.ts
CHANGED
|
@@ -10,6 +10,28 @@
|
|
|
10
10
|
export { NotisProvider, useNotisRuntime } from './provider';
|
|
11
11
|
|
|
12
12
|
// Hooks
|
|
13
|
+
export { useAction, useActionQuery } from './hooks/useAction';
|
|
14
|
+
export { useNativeDocuments, useNativeMutation } from './hooks/useNativeDocuments';
|
|
15
|
+
export type { NativeField, NativeFilter, NativeQuery, NativeInsert, NativeUpdate, NativeDelete, NativeMutationReceipt } from './nativeData';
|
|
16
|
+
export type { SpaceDatabaseCreate, SpaceResourceReference } from './space';
|
|
17
|
+
export type { SpaceActionTemplate, SpaceActionDescriptor, SpaceRuntimeDescriptor, SpaceActionOptions } from './space';
|
|
18
|
+
export type { SpaceDocumentBodyRequest, SpaceDocumentBodyRead, SpaceDocumentBodySave, SpaceDocumentBodyResult } from './space';
|
|
19
|
+
export { useNativeDocumentBody } from './hooks/useNativeDocumentBody';
|
|
20
|
+
export { useViewerRead, useViewerReadAvailable, useViewerDatabases, useViewerSkills } from './hooks/useViewerReads';
|
|
21
|
+
export type { UseViewerReadOptions } from './hooks/useViewerReads';
|
|
22
|
+
export { useViewParams, useShown, useShownAvailable, parseViewParams } from './hooks/useViewParams';
|
|
23
|
+
export type { ViewParams, ViewParamsOf, UseShownOptions } from './hooks/useViewParams';
|
|
24
|
+
export { SPACE_VIEW_SPEC_VERSION } from './space';
|
|
25
|
+
export type { SpaceViewParam, SpaceViewParamType, SpaceShows, SpaceShownDescriptor, SpaceShownOptions, SpaceShownResult,
|
|
26
|
+
SpaceMemoryPolicy, SpaceMemorySnapshot, SpaceChrome, SpaceMarkdownInput } from './space';
|
|
27
|
+
export { SPACE_VIEWER_READ_OPERATIONS } from './space';
|
|
28
|
+
export { SPACE_CLOUD_COMPUTER_OPERATIONS, spaceCloudComputerAllows } from './space';
|
|
29
|
+
export type { SpaceCloudComputerLevel, SpaceCloudComputerOperation, SpaceCloudComputerStatus, SpaceCloudComputerRelease, SpaceCloudComputerRunInput,
|
|
30
|
+
SpaceCloudComputerRunResult, SpaceCloudComputerUploadInput, SpaceCloudComputerUploadResult, SpaceCloudComputerTransport } from './space';
|
|
31
|
+
export type { SpaceViewerReadFamily, SpaceViewerReadOperation, SpaceViewerReadInput, SpaceViewerReadResult, SpaceViewerLink,
|
|
32
|
+
SpaceViewerDatabase, SpaceViewerDatabaseSchema, SpaceViewerSkill, SpaceViewerSkillPage } from './space';
|
|
33
|
+
export { SpaceActionError, SpaceNavigationCancelled, isSpaceNavigationCancelled } from './space';
|
|
34
|
+
export type { SpaceActionFailureDetails } from './space';
|
|
13
35
|
export { useQuery, useQueryClient } from './hooks/useQuery';
|
|
14
36
|
export type { UseQueryOptions, UseQueryResult } from './hooks/useQuery';
|
|
15
37
|
export { createQueryClient, createPrefetchQueue, queryKey } from './queryCache';
|
|
@@ -35,6 +57,8 @@ export { useHandover } from './hooks/useHandover';
|
|
|
35
57
|
export type { UseHandoverResult } from './hooks/useHandover';
|
|
36
58
|
export { useCloudComputer } from './hooks/useCloudComputer';
|
|
37
59
|
export type { UseCloudComputerResult } from './hooks/useCloudComputer';
|
|
60
|
+
export { useCloudComputerShell } from './hooks/useCloudComputerShell';
|
|
61
|
+
export type { UseCloudComputerShellResult } from './hooks/useCloudComputerShell';
|
|
38
62
|
export { useNotisNavigation } from './hooks/useNotisNavigation';
|
|
39
63
|
export { useTopBarSearch } from './hooks/useTopBarSearch';
|
|
40
64
|
export { useBackend } from './hooks/useBackend';
|
|
@@ -53,6 +77,9 @@ export {
|
|
|
53
77
|
export { Markdown } from './components/Markdown';
|
|
54
78
|
export type { MarkdownProps } from './components/Markdown';
|
|
55
79
|
export { DocumentEditor } from './components/DocumentEditor';
|
|
80
|
+
export { DocumentPage, DocumentPageSkeleton, usePrefetchRecord } from './components/DocumentPage';
|
|
81
|
+
export { RecordProperties, HtmlFrame, ReportFrame, ShareControl } from './components/RecordComponents';
|
|
82
|
+
export { RenderChartData } from './components/RenderChartData';
|
|
56
83
|
export { MarkdownEditor } from './components/MarkdownEditor';
|
|
57
84
|
export { NotisSelectionBoundary, NOTIS_CONTEXT_CLIPBOARD_TYPE } from './components/NotisSelectionBoundary';
|
|
58
85
|
export type { NotisSelectionBoundaryProps } from './components/NotisSelectionBoundary';
|
|
@@ -135,6 +162,15 @@ export type {
|
|
|
135
162
|
HandoverPayload,
|
|
136
163
|
HandoverResult,
|
|
137
164
|
NotisDocumentEditorProps,
|
|
165
|
+
NotisDocumentPageProps,
|
|
166
|
+
NotisDocumentPageCrumb,
|
|
167
|
+
NotisDocumentPageMenuItem,
|
|
168
|
+
NotisDocumentPageParts,
|
|
169
|
+
NotisRecordProps,
|
|
170
|
+
NotisRecordPropertiesProps,
|
|
171
|
+
NotisHtmlFrameProps,
|
|
172
|
+
NotisReportFrameProps,
|
|
173
|
+
NotisShareControlProps,
|
|
138
174
|
NotisMarkdownEditorProps,
|
|
139
175
|
NotisMarkdownEditorSavePayload,
|
|
140
176
|
NotisMarkdownEditorSaveResult,
|
|
@@ -5,6 +5,7 @@ import React, {
|
|
|
5
5
|
useCallback,
|
|
6
6
|
useContext,
|
|
7
7
|
useEffect,
|
|
8
|
+
useLayoutEffect,
|
|
8
9
|
useMemo,
|
|
9
10
|
useRef,
|
|
10
11
|
useState,
|
|
@@ -244,10 +245,10 @@ function isDialogEvent(event: KeyboardEvent): boolean {
|
|
|
244
245
|
);
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
export function ShortcutProvider({ children }: { children: ReactNode }) {
|
|
248
|
+
export function ShortcutProvider({ children, isAvailable }: { children: ReactNode; isAvailable?: () => boolean }) {
|
|
248
249
|
const parentRegistry = useContext(ShortcutContext);
|
|
249
250
|
if (parentRegistry) return <>{children}</>;
|
|
250
|
-
return <ShortcutProviderRoot>{children}</ShortcutProviderRoot>;
|
|
251
|
+
return <ShortcutProviderRoot isAvailable={isAvailable}>{children}</ShortcutProviderRoot>;
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
export interface ShortcutHelpEntry {
|
|
@@ -370,7 +371,7 @@ function collectShortcutHelpEntries(
|
|
|
370
371
|
return entries;
|
|
371
372
|
}
|
|
372
373
|
|
|
373
|
-
function ShortcutProviderRoot({ children }: { children: ReactNode }) {
|
|
374
|
+
function ShortcutProviderRoot({ children, isAvailable }: { children: ReactNode; isAvailable?: () => boolean }) {
|
|
374
375
|
const registrationsRef = useRef(new Map<number, ShortcutRegistration>());
|
|
375
376
|
const nextIdRef = useRef(1);
|
|
376
377
|
const nextOrderRef = useRef(1);
|
|
@@ -378,12 +379,21 @@ function ShortcutProviderRoot({ children }: { children: ReactNode }) {
|
|
|
378
379
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
379
380
|
const helpOpenRef = useRef(false);
|
|
380
381
|
const [, setRegistryVersion] = useState(0);
|
|
382
|
+
const availabilityRef = useRef(isAvailable);
|
|
381
383
|
|
|
382
384
|
const updateHelpOpen = useCallback((open: boolean) => {
|
|
383
385
|
helpOpenRef.current = open;
|
|
384
386
|
setHelpOpen(open);
|
|
385
387
|
}, []);
|
|
386
388
|
|
|
389
|
+
useLayoutEffect(() => {
|
|
390
|
+
availabilityRef.current = isAvailable;
|
|
391
|
+
if (isAvailable?.() === false) {
|
|
392
|
+
sequenceRef.current = { chords: [], at: 0 };
|
|
393
|
+
updateHelpOpen(false);
|
|
394
|
+
}
|
|
395
|
+
}, [isAvailable, updateHelpOpen]);
|
|
396
|
+
|
|
387
397
|
const register = useCallback((input: Omit<ShortcutRegistration, 'id' | 'order'>) => {
|
|
388
398
|
const id = nextIdRef.current++;
|
|
389
399
|
registrationsRef.current.set(id, {
|
|
@@ -400,6 +410,10 @@ function ShortcutProviderRoot({ children }: { children: ReactNode }) {
|
|
|
400
410
|
|
|
401
411
|
useEffect(() => {
|
|
402
412
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
413
|
+
if (availabilityRef.current?.() === false) {
|
|
414
|
+
sequenceRef.current = { chords: [], at: 0 };
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
403
417
|
if (event.defaultPrevented || isDialogEvent(event)) return;
|
|
404
418
|
const editable = isEditableShortcutEvent(event);
|
|
405
419
|
if (!editable && !event.repeat && event.key === '?' && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The native row contract, shared by direct tools and scoped Space actions. */
|
|
2
|
+
export type NativeField = { property_id: string } | { property: string } | {
|
|
3
|
+
column: 'record_key' | 'id' | 'title' | 'created_at' | 'updated_at' | 'last_edited_time' | 'content_type' | 'file_type';
|
|
4
|
+
};
|
|
5
|
+
export type NativeFilter = { and: NativeFilter[] } | { or: NativeFilter[] } | {
|
|
6
|
+
field: NativeField;
|
|
7
|
+
op: 'equals' | 'not_equals' | 'in' | 'contains' | 'not_contains' | 'does_not_contain'
|
|
8
|
+
| 'starts_with' | 'ends_with' | 'is_empty' | 'is_not_empty' | 'greater_than' | 'less_than'
|
|
9
|
+
| 'greater_than_or_equal_to' | 'less_than_or_equal_to' | 'before' | 'after' | 'on_or_before' | 'on_or_after';
|
|
10
|
+
value?: unknown;
|
|
11
|
+
};
|
|
12
|
+
export interface NativeQuery {
|
|
13
|
+
include_content?: boolean;
|
|
14
|
+
include_archived?: boolean;
|
|
15
|
+
mode?: 'rows' | 'search' | 'count' | 'aggregate';
|
|
16
|
+
filter?: NativeFilter;
|
|
17
|
+
sorts?: Array<{ field: NativeField; direction: 'asc' | 'desc' }>;
|
|
18
|
+
search?: { text: string; fields: NativeField[] };
|
|
19
|
+
aggregate?: { op: 'sum' | 'avg' | 'min' | 'max'; field: NativeField };
|
|
20
|
+
page_size?: number;
|
|
21
|
+
offset?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface NativeInsert {
|
|
24
|
+
columns?: { title?: string | null; icon?: string | null; cover?: string | null };
|
|
25
|
+
properties?: Record<string, unknown>;
|
|
26
|
+
body?: { blocks: Array<Record<string, unknown>> };
|
|
27
|
+
}
|
|
28
|
+
export interface NativeUpdate extends NativeInsert { record_key: string; expected_revision: number; }
|
|
29
|
+
export interface NativeDelete { record_key: string; expected_revision: number; }
|
|
30
|
+
export interface NativeMutationReceipt {
|
|
31
|
+
record_key: string;
|
|
32
|
+
id: string;
|
|
33
|
+
database_id: string;
|
|
34
|
+
revision: number;
|
|
35
|
+
schema_revision: number;
|
|
36
|
+
receipt_id: string;
|
|
37
|
+
operation: 'insert' | 'update' | 'delete';
|
|
38
|
+
replayed?: boolean;
|
|
39
|
+
}
|