@nbt-dev/components 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-5JKRR7HY.js → chunk-WLGJKJF4.js} +53 -24
- package/dist/chunk-WLGJKJF4.js.map +7 -0
- package/dist/core/index.d.ts +3 -1
- package/dist/core/use-permissions-api.d.ts +30 -0
- package/dist/core/use-workflows.d.ts +13 -18
- package/dist/editor/index.js +1 -1
- package/dist/editor/nbt-editor.d.ts +2 -0
- package/dist/index.js +81 -46
- package/dist/index.js.map +4 -4
- package/package.json +2 -1
- package/src/core/index.ts +3 -2
- package/src/core/use-permissions-api.ts +111 -0
- package/src/core/use-workflows.ts +32 -66
- package/src/editor/lsp-extensions.ts +6 -4
- package/src/editor/nbt-editor.tsx +53 -17
- package/dist/chunk-5JKRR7HY.js.map +0 -7
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/core/use-cartridge-info.ts", "../src/core/use-gsheets.ts", "../src/core/use-workflows.ts"],
|
|
4
|
-
"sourcesContent": ["import { useEffect, useState } from \"react\";\nimport { useDevToolsConfig } from \"./config\";\nimport { authHeaders } from \"./auth\";\n\n// Live cartridge/entity registry for the Data tab. Instead of the build-time\n// `BULK_REGISTRY` (every cart in the repo, generated by `nbt generate`), we\n// read the daemon's `/_console/contracts` \u2014 which only lists *running*\n// cartridges and carries each entity's `searchFields`. Bulk WS routes are\n// derived live (`/_ws/bulk/<cart>/<entity-lower>`) and fed straight into\n// useBulkStream. So the tab reflects what is actually installed right now.\n\nexport type BulkEntity = {\n name: string;\n route: string;\n searchFields: readonly string[];\n};\nexport type BulkRegistry = Record<string, BulkEntity[]>;\n\ntype Contract = {\n cartridge?: string;\n core?: boolean;\n installed?: boolean;\n owns?: Record<string, { searchFields?: string[] }>;\n};\n\nfunction buildRegistry(\n contracts: Contract[],\n): { reg: BulkRegistry; core: Set<string>; installed: Set<string> } {\n const reg: BulkRegistry = {};\n const core = new Set<string>();\n const installed = new Set<string>();\n for (const c of contracts) {\n const cart = c.cartridge;\n if (!cart || !c.owns) continue;\n const entities: BulkEntity[] = [];\n for (const [name, ent] of Object.entries(c.owns)) {\n const sf = Array.isArray(ent?.searchFields) ? ent.searchFields : [];\n entities.push({\n name,\n route: `/_ws/bulk/${cart}/${name.toLowerCase()}`,\n searchFields: sf,\n });\n }\n if (entities.length > 0) {\n reg[cart] = entities;\n if (c.core) core.add(cart);\n if (c.installed) installed.add(cart);\n }\n }\n return { reg, core, installed };\n}\n\nexport type LiveRegistryState = {\n registry: BulkRegistry;\n carts: string[];\n // Cartridge slugs flagged `core` by the daemon. The Data tab keeps `auth`\n // visible but tucks the rest of these behind a \"more cartridges\" menu so\n // deployed (user) cartridges are what's shown by default.\n coreCarts: Set<string>;\n // Cartridge slugs the user (or boot) explicitly installed/activated. The Data\n // tab shows these (plus `auth`) by default; bundled-but-not-installed carts\n // stay behind the \"more cartridges\" menu until the user opens them.\n installedCarts: Set<string>;\n loading: boolean;\n error: string | null;\n};\n\nexport function useLiveBulkRegistry(): LiveRegistryState {\n const { apiBaseUrl } = useDevToolsConfig();\n const [registry, setRegistry] = useState<BulkRegistry>({});\n const [coreCarts, setCoreCarts] = useState<Set<string>>(new Set());\n const [installedCarts, setInstalledCarts] = useState<Set<string>>(new Set());\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const ac = new AbortController();\n let cancelled = false;\n setLoading(true);\n setError(null);\n (async () => {\n try {\n const r = await fetch(`${apiBaseUrl}/_console/contracts`, {\n signal: ac.signal,\n credentials: \"include\",\n headers: authHeaders(),\n });\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\n const data = (await r.json()) as Contract[];\n if (!Array.isArray(data)) throw new Error(\"malformed contracts response\");\n if (cancelled) return;\n const { reg, core, installed } = buildRegistry(data);\n setRegistry(reg);\n setCoreCarts(core);\n setInstalledCarts(installed);\n } catch (e) {\n if (cancelled || (e as { name?: string }).name === \"AbortError\") return;\n setError(e instanceof Error ? e.message : String(e));\n } finally {\n if (!cancelled) setLoading(false);\n }\n })();\n return () => {\n cancelled = true;\n ac.abort();\n };\n }, [apiBaseUrl]);\n\n const carts = Object.keys(registry).sort();\n return { registry, carts, coreCarts, installedCarts, loading, error };\n}\n", "// Fetchers + hook for the Google Sheets cartridge-sync admin endpoints\n// (/_console/integrations/gsheets/*). Admin-gated server-side \u2014 the requests\n// carry the devtools Bearer token (authHeaders) like the contracts probe.\n\nimport { useCallback, useMemo } from \"react\";\nimport { useDevToolsConfig } from \"./config\";\nimport { authHeaders } from \"./auth\";\n\nexport type GsheetSyncConfig = {\n cartridge: string;\n spreadsheetId: string;\n saEmail: string;\n hasServiceAccount: boolean;\n intervalSeconds: number;\n enabled: boolean;\n lastSyncAt: number; // ms epoch, 0 = never\n status: string; // \"ok\" | \"error\" | \"\"\n lastError: string;\n};\n\nexport type GsheetSaveInput = {\n cartridge: string;\n spreadsheetUrl: string;\n serviceAccountJson?: string; // omit to keep the stored key\n intervalSeconds: number;\n enabled: boolean;\n};\n\nasync function jsonOrThrow(r: Response) {\n const body = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error((body as { error?: string })?.error ?? `HTTP ${r.status}`);\n return body;\n}\n\nexport function useGsheetsApi() {\n const { apiBaseUrl } = useDevToolsConfig();\n\n const list = useCallback(\n async (): Promise<GsheetSyncConfig[]> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets`, {\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n const save = useCallback(\n async (input: GsheetSaveInput): Promise<GsheetSyncConfig> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify(input),\n }),\n ),\n [apiBaseUrl],\n );\n\n const syncNow = useCallback(\n async (cartridge: string): Promise<{ ok: boolean; error: string }> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets/sync`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify({ cartridge }),\n }),\n ),\n [apiBaseUrl],\n );\n\n const remove = useCallback(\n async (cartridge: string): Promise<{ ok: boolean }> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets/${encodeURIComponent(cartridge)}`, {\n method: \"DELETE\",\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n return useMemo(() => ({ list, save, syncNow, remove }), [list, save, syncNow, remove]);\n}\n", "// Fetchers + hook for the workflow-engine introspection endpoints\n// (/_console/wf*). The read routes are ungated like /_console/metrics; the two\n// mutating ones (cancel/advance) are admin-gated server-side, so every request\n// carries the devtools Bearer token (authHeaders) like the contracts probe.\n\nimport { useCallback, useMemo } from \"react\";\nimport { useDevToolsConfig } from \"./config\";\nimport { authHeaders } from \"./auth\";\n\n// Terminal statuses a workflow can settle into \u2014 used to decide whether to keep\n// polling an open instance and whether Cancel is offered.\nexport const WF_TERMINAL = [\"COMPLETED\", \"FAILED\", \"CANCELLED\", \"CONTINUED\"] as const;\n\nexport type WfStatus =\n | \"RUNNING\"\n | \"SUSPENDED\"\n | \"COMPLETED\"\n | \"FAILED\"\n | \"CANCELLED\"\n | \"CONTINUED\";\n\n// Row from GET /_console/wf \u2014 the enriched list (createdAt/updatedAt are unix ms).\nexport type WfListItem = {\n id: string;\n status: WfStatus;\n workflowName: string;\n createdAt: number;\n updatedAt: number;\n lane: string;\n deployVersion: number;\n};\n\n// The execution record from GET /_console/wf/:id (raw entity field names).\nexport type WfExecution = {\n id: string;\n status: WfStatus;\n workflowName: string;\n args?: string;\n result?: string;\n error?: string;\n cursor: number;\n lane?: string;\n deployVersion: number;\n createdAt: number;\n updatedAt: number;\n};\n\n// One journal entry. kind \u2208 HOST_CALL_INTENT | HOST_CALL_RESULT | SUSPENDED |\n// RESUMED | RETRY | COMPLETED | FAILED | CANCELLED | CONTINUED.\nexport type WfEvent = {\n id: string;\n seq: number;\n kind: string;\n capability?: string;\n target?: string;\n op?: string;\n payload?: string;\n};\n\nexport type WfDetail = { execution: WfExecution; events: WfEvent[] };\n\nexport type WfStats = {\n writes: number;\n reads: number;\n emails: number;\n fetches: number;\n emits: number;\n retries: number;\n async_inflight: number;\n async_peak: number;\n};\n\nexport type WfVersion = { version: number; refcount: number; current: boolean };\n\n// The exec-reply shape returned by run/signal/advance/cancel.\nexport type WfExecReply = {\n id: string;\n outcome: string;\n status: string;\n result?: string;\n error?: string;\n};\n\nasync function jsonOrThrow(r: Response) {\n const body = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error((body as { error?: string })?.error ?? `HTTP ${r.status}`);\n return body;\n}\n\nexport function isWfTerminal(status: string): boolean {\n return (WF_TERMINAL as readonly string[]).includes(status);\n}\n\nexport function useWorkflowsApi() {\n const { apiBaseUrl } = useDevToolsConfig();\n\n const listExecutions = useCallback(\n async (): Promise<WfListItem[]> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf`, {\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n const getExecution = useCallback(\n async (id: string): Promise<WfDetail> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf/${encodeURIComponent(id)}`, {\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n const getStats = useCallback(\n async (): Promise<WfStats> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf/stats`, {\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n const getVersions = useCallback(\n async (): Promise<WfVersion[]> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf/versions`, {\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n const cancel = useCallback(\n async (id: string): Promise<WfExecReply> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf/cancel`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify({ id }),\n }),\n ),\n [apiBaseUrl],\n );\n\n const advance = useCallback(\n async (id: string): Promise<WfExecReply> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf/advance`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify({ id }),\n }),\n ),\n [apiBaseUrl],\n );\n\n return useMemo(\n () => ({ listExecutions, getExecution, getStats, getVersions, cancel, advance }),\n [listExecutions, getExecution, getStats, getVersions, cancel, advance],\n );\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
6
|
-
"names": ["useCallback", "useMemo", "jsonOrThrow", "useCallback", "useMemo"]
|
|
3
|
+
"sources": ["../src/core/use-cartridge-info.ts", "../src/core/use-gsheets.ts", "../src/core/use-permissions-api.ts", "../src/core/use-workflows.ts"],
|
|
4
|
+
"sourcesContent": ["import { useEffect, useState } from \"react\";\nimport { useDevToolsConfig } from \"./config\";\nimport { authHeaders } from \"./auth\";\n\n// Live cartridge/entity registry for the Data tab. Instead of the build-time\n// `BULK_REGISTRY` (every cart in the repo, generated by `nbt generate`), we\n// read the daemon's `/_console/contracts` \u2014 which only lists *running*\n// cartridges and carries each entity's `searchFields`. Bulk WS routes are\n// derived live (`/_ws/bulk/<cart>/<entity-lower>`) and fed straight into\n// useBulkStream. So the tab reflects what is actually installed right now.\n\nexport type BulkEntity = {\n name: string;\n route: string;\n searchFields: readonly string[];\n};\nexport type BulkRegistry = Record<string, BulkEntity[]>;\n\ntype Contract = {\n cartridge?: string;\n core?: boolean;\n installed?: boolean;\n owns?: Record<string, { searchFields?: string[] }>;\n};\n\nfunction buildRegistry(\n contracts: Contract[],\n): { reg: BulkRegistry; core: Set<string>; installed: Set<string> } {\n const reg: BulkRegistry = {};\n const core = new Set<string>();\n const installed = new Set<string>();\n for (const c of contracts) {\n const cart = c.cartridge;\n if (!cart || !c.owns) continue;\n const entities: BulkEntity[] = [];\n for (const [name, ent] of Object.entries(c.owns)) {\n const sf = Array.isArray(ent?.searchFields) ? ent.searchFields : [];\n entities.push({\n name,\n route: `/_ws/bulk/${cart}/${name.toLowerCase()}`,\n searchFields: sf,\n });\n }\n if (entities.length > 0) {\n reg[cart] = entities;\n if (c.core) core.add(cart);\n if (c.installed) installed.add(cart);\n }\n }\n return { reg, core, installed };\n}\n\nexport type LiveRegistryState = {\n registry: BulkRegistry;\n carts: string[];\n // Cartridge slugs flagged `core` by the daemon. The Data tab keeps `auth`\n // visible but tucks the rest of these behind a \"more cartridges\" menu so\n // deployed (user) cartridges are what's shown by default.\n coreCarts: Set<string>;\n // Cartridge slugs the user (or boot) explicitly installed/activated. The Data\n // tab shows these (plus `auth`) by default; bundled-but-not-installed carts\n // stay behind the \"more cartridges\" menu until the user opens them.\n installedCarts: Set<string>;\n loading: boolean;\n error: string | null;\n};\n\nexport function useLiveBulkRegistry(): LiveRegistryState {\n const { apiBaseUrl } = useDevToolsConfig();\n const [registry, setRegistry] = useState<BulkRegistry>({});\n const [coreCarts, setCoreCarts] = useState<Set<string>>(new Set());\n const [installedCarts, setInstalledCarts] = useState<Set<string>>(new Set());\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const ac = new AbortController();\n let cancelled = false;\n setLoading(true);\n setError(null);\n (async () => {\n try {\n const r = await fetch(`${apiBaseUrl}/_console/contracts`, {\n signal: ac.signal,\n credentials: \"include\",\n headers: authHeaders(),\n });\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\n const data = (await r.json()) as Contract[];\n if (!Array.isArray(data)) throw new Error(\"malformed contracts response\");\n if (cancelled) return;\n const { reg, core, installed } = buildRegistry(data);\n setRegistry(reg);\n setCoreCarts(core);\n setInstalledCarts(installed);\n } catch (e) {\n if (cancelled || (e as { name?: string }).name === \"AbortError\") return;\n setError(e instanceof Error ? e.message : String(e));\n } finally {\n if (!cancelled) setLoading(false);\n }\n })();\n return () => {\n cancelled = true;\n ac.abort();\n };\n }, [apiBaseUrl]);\n\n const carts = Object.keys(registry).sort();\n return { registry, carts, coreCarts, installedCarts, loading, error };\n}\n", "// Fetchers + hook for the Google Sheets cartridge-sync admin endpoints\n// (/_console/integrations/gsheets/*). Admin-gated server-side \u2014 the requests\n// carry the devtools Bearer token (authHeaders) like the contracts probe.\n\nimport { useCallback, useMemo } from \"react\";\nimport { useDevToolsConfig } from \"./config\";\nimport { authHeaders } from \"./auth\";\n\nexport type GsheetSyncConfig = {\n cartridge: string;\n spreadsheetId: string;\n saEmail: string;\n hasServiceAccount: boolean;\n intervalSeconds: number;\n enabled: boolean;\n lastSyncAt: number; // ms epoch, 0 = never\n status: string; // \"ok\" | \"error\" | \"\"\n lastError: string;\n};\n\nexport type GsheetSaveInput = {\n cartridge: string;\n spreadsheetUrl: string;\n serviceAccountJson?: string; // omit to keep the stored key\n intervalSeconds: number;\n enabled: boolean;\n};\n\nasync function jsonOrThrow(r: Response) {\n const body = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error((body as { error?: string })?.error ?? `HTTP ${r.status}`);\n return body;\n}\n\nexport function useGsheetsApi() {\n const { apiBaseUrl } = useDevToolsConfig();\n\n const list = useCallback(\n async (): Promise<GsheetSyncConfig[]> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets`, {\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n const save = useCallback(\n async (input: GsheetSaveInput): Promise<GsheetSyncConfig> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify(input),\n }),\n ),\n [apiBaseUrl],\n );\n\n const syncNow = useCallback(\n async (cartridge: string): Promise<{ ok: boolean; error: string }> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets/sync`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify({ cartridge }),\n }),\n ),\n [apiBaseUrl],\n );\n\n const remove = useCallback(\n async (cartridge: string): Promise<{ ok: boolean }> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets/${encodeURIComponent(cartridge)}`, {\n method: \"DELETE\",\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n return useMemo(() => ({ list, save, syncNow, remove }), [list, save, syncNow, remove]);\n}\n", "import { useCallback, useEffect, useState } from \"react\";\nimport { useDevToolsConfig } from \"./config\";\nimport { authHeaders } from \"./auth\";\n\n// Operator permission management \u2014 the data layer behind the devtools Permissions\n// tab. Reads the catalog (GET /_console/permissions) and the user\u00D7role matrix\n// (GET /_console/users), and toggles grants (POST /_console/roles/{assign,revoke}).\n// All routes are operator-gated server-side; on an open/dev node they answer\n// freely. Mirrors useLiveBulkRegistry's fetch/abort shape.\n\nexport type CartPermissions = {\n slug: string;\n permissions: string[];\n roles: { name: string; perms: string[] }[];\n};\n\nexport type UserGrant = { cart: string; role: string };\n\nexport type PermUser = {\n id: string;\n email: string;\n name: string;\n capsVersion: number;\n roles: UserGrant[];\n caps: string[];\n};\n\nexport type PermissionsState = {\n carts: CartPermissions[];\n users: PermUser[];\n loading: boolean;\n error: string | null;\n refresh: () => void;\n assign: (userId: string, cart: string, role: string) => Promise<void>;\n revoke: (userId: string, cart: string, role: string) => Promise<void>;\n};\n\nexport function usePermissionsApi(): PermissionsState {\n const { apiBaseUrl } = useDevToolsConfig();\n const [carts, setCarts] = useState<CartPermissions[]>([]);\n const [users, setUsers] = useState<PermUser[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [nonce, setNonce] = useState(0);\n\n const refresh = useCallback(() => setNonce((n) => n + 1), []);\n\n useEffect(() => {\n const ac = new AbortController();\n let cancelled = false;\n setLoading(true);\n setError(null);\n (async () => {\n try {\n const [pr, ur] = await Promise.all([\n fetch(`${apiBaseUrl}/_console/permissions`, {\n signal: ac.signal,\n credentials: \"include\",\n headers: authHeaders(),\n }),\n fetch(`${apiBaseUrl}/_console/users`, {\n signal: ac.signal,\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ]);\n if (!pr.ok) throw new Error(`permissions HTTP ${pr.status}`);\n if (!ur.ok) throw new Error(`users HTTP ${ur.status}`);\n const pdata = (await pr.json()) as { carts?: CartPermissions[] };\n const udata = (await ur.json()) as PermUser[];\n if (cancelled) return;\n setCarts(Array.isArray(pdata.carts) ? pdata.carts : []);\n setUsers(Array.isArray(udata) ? udata : []);\n } catch (e) {\n if (cancelled || (e as { name?: string }).name === \"AbortError\") return;\n setError(e instanceof Error ? e.message : String(e));\n } finally {\n if (!cancelled) setLoading(false);\n }\n })();\n return () => {\n cancelled = true;\n ac.abort();\n };\n }, [apiBaseUrl, nonce]);\n\n const mutate = useCallback(\n async (path: string, userId: string, cart: string, role: string) => {\n const r = await fetch(`${apiBaseUrl}${path}`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify({ userId, cart, role }),\n });\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\n refresh();\n },\n [apiBaseUrl, refresh],\n );\n\n const assign = useCallback(\n (userId: string, cart: string, role: string) => mutate(\"/_console/roles/assign\", userId, cart, role),\n [mutate],\n );\n const revoke = useCallback(\n (userId: string, cart: string, role: string) => mutate(\"/_console/roles/revoke\", userId, cart, role),\n [mutate],\n );\n\n return { carts, users, loading, error, refresh, assign, revoke };\n}\n", "// Fetchers + hook for the workflow-engine introspection endpoints\n// (/_console/wf*). The read routes are ungated like /_console/metrics; the\n// mutating one (cancel) is admin-gated server-side, so every request carries the\n// devtools Bearer token (authHeaders) like the contracts probe.\n\nimport { useCallback, useMemo } from \"react\";\nimport { useDevToolsConfig } from \"./config\";\nimport { authHeaders } from \"./auth\";\n\n// Terminal statuses a workflow can settle into \u2014 used to decide whether to keep\n// polling an open instance and whether Cancel is offered.\nexport const WF_TERMINAL = [\"COMPLETED\", \"FAILED\", \"CANCELLED\", \"CONTINUED\"] as const;\n\nexport type WfStatus =\n | \"RUNNING\"\n | \"SUSPENDED\"\n | \"COMPLETED\"\n | \"FAILED\"\n | \"CANCELLED\"\n | \"CONTINUED\";\n\n// Row from GET /_console/wf \u2014 the enriched list (createdAt/updatedAt are unix ms).\nexport type WfListItem = {\n id: string;\n status: WfStatus;\n workflowName: string;\n createdAt: number;\n updatedAt: number;\n lane: string;\n deployVersion: number;\n branchId?: string;\n // Provenance of what started this run: \"event:<slug>:<event>\" | \"cron:<id>\" |\n // \"manual\" | \"child:<id>\" | \"continue:<id>\" | \"\" (legacy/unknown).\n trigger?: string;\n};\n\n// The execution record from GET /_console/wf/:id (raw entity field names).\nexport type WfExecution = {\n id: string;\n status: WfStatus;\n workflowName: string;\n args?: string;\n result?: string;\n error?: string;\n cursor: number;\n lane?: string;\n deployVersion: number;\n createdAt: number;\n updatedAt: number;\n branchId?: string;\n trigger?: string;\n // High-res unix MICROSECONDS (start / last activity). Serialized by\n // cart_record_json as quoted strings (s64 wire form); coerce with Number().\n // 0/absent on legacy rows \u2192 fall back to createdAt/updatedAt.\n startedUs?: string | number;\n endedUs?: string | number;\n};\n\n// One journal entry. kind \u2208 HOST_CALL_INTENT | HOST_CALL_RESULT | SUSPENDED |\n// RESUMED | RETRY | COMPLETED | FAILED | CANCELLED | CONTINUED.\nexport type WfEvent = {\n id: string;\n seq: number;\n kind: string;\n capability?: string;\n target?: string;\n op?: string;\n payload?: string;\n // unix-ms when this entry was journaled (second-grained DateTime auto-stamp).\n createdAt: number;\n // High-res unix MICROSECONDS this entry was journaled \u2014 the timeline's real\n // start/end source (keeps sub-ms host calls distinct). Raw integer on the wire.\n // 0/absent on legacy rows \u2192 fall back to createdAt.\n tsUs?: number;\n // HOST_CALL_INTENT only: \"<line>:<col>;\u2026\" call-site in final-bundle coords,\n // mapped to authored source via the bundle sourcemap (Phase C).\n loc?: string;\n};\n\nexport type WfDetail = { execution: WfExecution; events: WfEvent[] };\n\n// The compiled workflow bundle for a version + how many leading lines are the\n// auto-prepended runtime prelude (so the inspector can skip past it to the\n// authored call site when resolving a journal entry's `loc`).\nexport type WfBundleSource = { version: number; preludeLines: number; source: string };\n\n// The exec-reply shape returned by run/signal/advance/cancel.\nexport type WfExecReply = {\n id: string;\n outcome: string;\n status: string;\n result?: string;\n error?: string;\n};\n\nasync function jsonOrThrow(r: Response) {\n const body = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error((body as { error?: string })?.error ?? `HTTP ${r.status}`);\n return body;\n}\n\nexport function isWfTerminal(status: string): boolean {\n return (WF_TERMINAL as readonly string[]).includes(status);\n}\n\nexport function useWorkflowsApi() {\n const { apiBaseUrl } = useDevToolsConfig();\n\n const getBundleSource = useCallback(\n async (version: number): Promise<WfBundleSource> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf/source?version=${encodeURIComponent(String(version))}`, {\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n const cancel = useCallback(\n async (id: string): Promise<WfExecReply> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf/cancel`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify({ id }),\n }),\n ),\n [apiBaseUrl],\n );\n\n return useMemo(\n () => ({ getBundleSource, cancel }),\n [getBundleSource, cancel],\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["useCallback", "useEffect", "useState", "useState", "useCallback", "useEffect", "useCallback", "useMemo", "jsonOrThrow", "useCallback", "useMemo"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nbt-dev/components",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Reusable React building blocks for NBT-console apps: the CodeMirror NBT editor (+LSP), the entity graph, and the live data table.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"@codemirror/state": "^6.5.4",
|
|
59
59
|
"@codemirror/view": "^6.39.5",
|
|
60
60
|
"@dagrejs/dagre": "^3.0.0",
|
|
61
|
+
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
|
61
62
|
"@xyflow/react": "^12.11.0",
|
|
62
63
|
"clsx": "^2.1.1",
|
|
63
64
|
"lucide-react": ">=0.5.0",
|
package/src/core/index.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type {
|
|
|
23
23
|
} from "./use-cartridge-info";
|
|
24
24
|
export { useGsheetsApi } from "./use-gsheets";
|
|
25
25
|
export type { GsheetSyncConfig, GsheetSaveInput } from "./use-gsheets";
|
|
26
|
+
export { usePermissionsApi } from "./use-permissions-api";
|
|
27
|
+
export type { CartPermissions, UserGrant, PermUser, PermissionsState } from "./use-permissions-api";
|
|
26
28
|
export { useWorkflowsApi, isWfTerminal, WF_TERMINAL } from "./use-workflows";
|
|
27
29
|
export type {
|
|
28
30
|
WfStatus,
|
|
@@ -30,8 +32,7 @@ export type {
|
|
|
30
32
|
WfExecution,
|
|
31
33
|
WfEvent,
|
|
32
34
|
WfDetail,
|
|
33
|
-
|
|
34
|
-
WfVersion,
|
|
35
|
+
WfBundleSource,
|
|
35
36
|
WfExecReply,
|
|
36
37
|
} from "./use-workflows";
|
|
37
38
|
export {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { useDevToolsConfig } from "./config";
|
|
3
|
+
import { authHeaders } from "./auth";
|
|
4
|
+
|
|
5
|
+
// Operator permission management — the data layer behind the devtools Permissions
|
|
6
|
+
// tab. Reads the catalog (GET /_console/permissions) and the user×role matrix
|
|
7
|
+
// (GET /_console/users), and toggles grants (POST /_console/roles/{assign,revoke}).
|
|
8
|
+
// All routes are operator-gated server-side; on an open/dev node they answer
|
|
9
|
+
// freely. Mirrors useLiveBulkRegistry's fetch/abort shape.
|
|
10
|
+
|
|
11
|
+
export type CartPermissions = {
|
|
12
|
+
slug: string;
|
|
13
|
+
permissions: string[];
|
|
14
|
+
roles: { name: string; perms: string[] }[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type UserGrant = { cart: string; role: string };
|
|
18
|
+
|
|
19
|
+
export type PermUser = {
|
|
20
|
+
id: string;
|
|
21
|
+
email: string;
|
|
22
|
+
name: string;
|
|
23
|
+
capsVersion: number;
|
|
24
|
+
roles: UserGrant[];
|
|
25
|
+
caps: string[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type PermissionsState = {
|
|
29
|
+
carts: CartPermissions[];
|
|
30
|
+
users: PermUser[];
|
|
31
|
+
loading: boolean;
|
|
32
|
+
error: string | null;
|
|
33
|
+
refresh: () => void;
|
|
34
|
+
assign: (userId: string, cart: string, role: string) => Promise<void>;
|
|
35
|
+
revoke: (userId: string, cart: string, role: string) => Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function usePermissionsApi(): PermissionsState {
|
|
39
|
+
const { apiBaseUrl } = useDevToolsConfig();
|
|
40
|
+
const [carts, setCarts] = useState<CartPermissions[]>([]);
|
|
41
|
+
const [users, setUsers] = useState<PermUser[]>([]);
|
|
42
|
+
const [loading, setLoading] = useState(true);
|
|
43
|
+
const [error, setError] = useState<string | null>(null);
|
|
44
|
+
const [nonce, setNonce] = useState(0);
|
|
45
|
+
|
|
46
|
+
const refresh = useCallback(() => setNonce((n) => n + 1), []);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const ac = new AbortController();
|
|
50
|
+
let cancelled = false;
|
|
51
|
+
setLoading(true);
|
|
52
|
+
setError(null);
|
|
53
|
+
(async () => {
|
|
54
|
+
try {
|
|
55
|
+
const [pr, ur] = await Promise.all([
|
|
56
|
+
fetch(`${apiBaseUrl}/_console/permissions`, {
|
|
57
|
+
signal: ac.signal,
|
|
58
|
+
credentials: "include",
|
|
59
|
+
headers: authHeaders(),
|
|
60
|
+
}),
|
|
61
|
+
fetch(`${apiBaseUrl}/_console/users`, {
|
|
62
|
+
signal: ac.signal,
|
|
63
|
+
credentials: "include",
|
|
64
|
+
headers: authHeaders(),
|
|
65
|
+
}),
|
|
66
|
+
]);
|
|
67
|
+
if (!pr.ok) throw new Error(`permissions HTTP ${pr.status}`);
|
|
68
|
+
if (!ur.ok) throw new Error(`users HTTP ${ur.status}`);
|
|
69
|
+
const pdata = (await pr.json()) as { carts?: CartPermissions[] };
|
|
70
|
+
const udata = (await ur.json()) as PermUser[];
|
|
71
|
+
if (cancelled) return;
|
|
72
|
+
setCarts(Array.isArray(pdata.carts) ? pdata.carts : []);
|
|
73
|
+
setUsers(Array.isArray(udata) ? udata : []);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (cancelled || (e as { name?: string }).name === "AbortError") return;
|
|
76
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
77
|
+
} finally {
|
|
78
|
+
if (!cancelled) setLoading(false);
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
return () => {
|
|
82
|
+
cancelled = true;
|
|
83
|
+
ac.abort();
|
|
84
|
+
};
|
|
85
|
+
}, [apiBaseUrl, nonce]);
|
|
86
|
+
|
|
87
|
+
const mutate = useCallback(
|
|
88
|
+
async (path: string, userId: string, cart: string, role: string) => {
|
|
89
|
+
const r = await fetch(`${apiBaseUrl}${path}`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
credentials: "include",
|
|
92
|
+
headers: authHeaders({ "content-type": "application/json" }),
|
|
93
|
+
body: JSON.stringify({ userId, cart, role }),
|
|
94
|
+
});
|
|
95
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
96
|
+
refresh();
|
|
97
|
+
},
|
|
98
|
+
[apiBaseUrl, refresh],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const assign = useCallback(
|
|
102
|
+
(userId: string, cart: string, role: string) => mutate("/_console/roles/assign", userId, cart, role),
|
|
103
|
+
[mutate],
|
|
104
|
+
);
|
|
105
|
+
const revoke = useCallback(
|
|
106
|
+
(userId: string, cart: string, role: string) => mutate("/_console/roles/revoke", userId, cart, role),
|
|
107
|
+
[mutate],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return { carts, users, loading, error, refresh, assign, revoke };
|
|
111
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Fetchers + hook for the workflow-engine introspection endpoints
|
|
2
|
-
// (/_console/wf*). The read routes are ungated like /_console/metrics; the
|
|
3
|
-
// mutating
|
|
4
|
-
//
|
|
2
|
+
// (/_console/wf*). The read routes are ungated like /_console/metrics; the
|
|
3
|
+
// mutating one (cancel) is admin-gated server-side, so every request carries the
|
|
4
|
+
// devtools Bearer token (authHeaders) like the contracts probe.
|
|
5
5
|
|
|
6
6
|
import { useCallback, useMemo } from "react";
|
|
7
7
|
import { useDevToolsConfig } from "./config";
|
|
@@ -28,6 +28,10 @@ export type WfListItem = {
|
|
|
28
28
|
updatedAt: number;
|
|
29
29
|
lane: string;
|
|
30
30
|
deployVersion: number;
|
|
31
|
+
branchId?: string;
|
|
32
|
+
// Provenance of what started this run: "event:<slug>:<event>" | "cron:<id>" |
|
|
33
|
+
// "manual" | "child:<id>" | "continue:<id>" | "" (legacy/unknown).
|
|
34
|
+
trigger?: string;
|
|
31
35
|
};
|
|
32
36
|
|
|
33
37
|
// The execution record from GET /_console/wf/:id (raw entity field names).
|
|
@@ -43,6 +47,13 @@ export type WfExecution = {
|
|
|
43
47
|
deployVersion: number;
|
|
44
48
|
createdAt: number;
|
|
45
49
|
updatedAt: number;
|
|
50
|
+
branchId?: string;
|
|
51
|
+
trigger?: string;
|
|
52
|
+
// High-res unix MICROSECONDS (start / last activity). Serialized by
|
|
53
|
+
// cart_record_json as quoted strings (s64 wire form); coerce with Number().
|
|
54
|
+
// 0/absent on legacy rows → fall back to createdAt/updatedAt.
|
|
55
|
+
startedUs?: string | number;
|
|
56
|
+
endedUs?: string | number;
|
|
46
57
|
};
|
|
47
58
|
|
|
48
59
|
// One journal entry. kind ∈ HOST_CALL_INTENT | HOST_CALL_RESULT | SUSPENDED |
|
|
@@ -55,22 +66,23 @@ export type WfEvent = {
|
|
|
55
66
|
target?: string;
|
|
56
67
|
op?: string;
|
|
57
68
|
payload?: string;
|
|
69
|
+
// unix-ms when this entry was journaled (second-grained DateTime auto-stamp).
|
|
70
|
+
createdAt: number;
|
|
71
|
+
// High-res unix MICROSECONDS this entry was journaled — the timeline's real
|
|
72
|
+
// start/end source (keeps sub-ms host calls distinct). Raw integer on the wire.
|
|
73
|
+
// 0/absent on legacy rows → fall back to createdAt.
|
|
74
|
+
tsUs?: number;
|
|
75
|
+
// HOST_CALL_INTENT only: "<line>:<col>;…" call-site in final-bundle coords,
|
|
76
|
+
// mapped to authored source via the bundle sourcemap (Phase C).
|
|
77
|
+
loc?: string;
|
|
58
78
|
};
|
|
59
79
|
|
|
60
80
|
export type WfDetail = { execution: WfExecution; events: WfEvent[] };
|
|
61
81
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
fetches: number;
|
|
67
|
-
emits: number;
|
|
68
|
-
retries: number;
|
|
69
|
-
async_inflight: number;
|
|
70
|
-
async_peak: number;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export type WfVersion = { version: number; refcount: number; current: boolean };
|
|
82
|
+
// The compiled workflow bundle for a version + how many leading lines are the
|
|
83
|
+
// auto-prepended runtime prelude (so the inspector can skip past it to the
|
|
84
|
+
// authored call site when resolving a journal entry's `loc`).
|
|
85
|
+
export type WfBundleSource = { version: number; preludeLines: number; source: string };
|
|
74
86
|
|
|
75
87
|
// The exec-reply shape returned by run/signal/advance/cancel.
|
|
76
88
|
export type WfExecReply = {
|
|
@@ -94,43 +106,10 @@ export function isWfTerminal(status: string): boolean {
|
|
|
94
106
|
export function useWorkflowsApi() {
|
|
95
107
|
const { apiBaseUrl } = useDevToolsConfig();
|
|
96
108
|
|
|
97
|
-
const
|
|
98
|
-
async (): Promise<
|
|
99
|
-
jsonOrThrow(
|
|
100
|
-
await fetch(`${apiBaseUrl}/_console/wf`, {
|
|
101
|
-
credentials: "include",
|
|
102
|
-
headers: authHeaders(),
|
|
103
|
-
}),
|
|
104
|
-
),
|
|
105
|
-
[apiBaseUrl],
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
const getExecution = useCallback(
|
|
109
|
-
async (id: string): Promise<WfDetail> =>
|
|
110
|
-
jsonOrThrow(
|
|
111
|
-
await fetch(`${apiBaseUrl}/_console/wf/${encodeURIComponent(id)}`, {
|
|
112
|
-
credentials: "include",
|
|
113
|
-
headers: authHeaders(),
|
|
114
|
-
}),
|
|
115
|
-
),
|
|
116
|
-
[apiBaseUrl],
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
const getStats = useCallback(
|
|
120
|
-
async (): Promise<WfStats> =>
|
|
109
|
+
const getBundleSource = useCallback(
|
|
110
|
+
async (version: number): Promise<WfBundleSource> =>
|
|
121
111
|
jsonOrThrow(
|
|
122
|
-
await fetch(`${apiBaseUrl}/_console/wf/
|
|
123
|
-
credentials: "include",
|
|
124
|
-
headers: authHeaders(),
|
|
125
|
-
}),
|
|
126
|
-
),
|
|
127
|
-
[apiBaseUrl],
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const getVersions = useCallback(
|
|
131
|
-
async (): Promise<WfVersion[]> =>
|
|
132
|
-
jsonOrThrow(
|
|
133
|
-
await fetch(`${apiBaseUrl}/_console/wf/versions`, {
|
|
112
|
+
await fetch(`${apiBaseUrl}/_console/wf/source?version=${encodeURIComponent(String(version))}`, {
|
|
134
113
|
credentials: "include",
|
|
135
114
|
headers: authHeaders(),
|
|
136
115
|
}),
|
|
@@ -151,21 +130,8 @@ export function useWorkflowsApi() {
|
|
|
151
130
|
[apiBaseUrl],
|
|
152
131
|
);
|
|
153
132
|
|
|
154
|
-
const advance = useCallback(
|
|
155
|
-
async (id: string): Promise<WfExecReply> =>
|
|
156
|
-
jsonOrThrow(
|
|
157
|
-
await fetch(`${apiBaseUrl}/_console/wf/advance`, {
|
|
158
|
-
method: "POST",
|
|
159
|
-
credentials: "include",
|
|
160
|
-
headers: authHeaders({ "content-type": "application/json" }),
|
|
161
|
-
body: JSON.stringify({ id }),
|
|
162
|
-
}),
|
|
163
|
-
),
|
|
164
|
-
[apiBaseUrl],
|
|
165
|
-
);
|
|
166
|
-
|
|
167
133
|
return useMemo(
|
|
168
|
-
() => ({
|
|
169
|
-
[
|
|
134
|
+
() => ({ getBundleSource, cancel }),
|
|
135
|
+
[getBundleSource, cancel],
|
|
170
136
|
);
|
|
171
137
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// full-text didChange sync (debounced), pushed publishDiagnostics → lint,
|
|
3
3
|
// completion, hover, and F12 go-to-definition.
|
|
4
4
|
|
|
5
|
-
import { type Extension, type Text } from "@codemirror/state";
|
|
5
|
+
import { Prec, type Extension, type Text } from "@codemirror/state";
|
|
6
6
|
import {
|
|
7
7
|
EditorView,
|
|
8
8
|
ViewPlugin,
|
|
@@ -154,7 +154,7 @@ export function lspExtensions(
|
|
|
154
154
|
};
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
-
const gotoDef = keymap.of([
|
|
157
|
+
const gotoDef = Prec.high(keymap.of([
|
|
158
158
|
{
|
|
159
159
|
key: "F12",
|
|
160
160
|
run: (view) => {
|
|
@@ -180,11 +180,13 @@ export function lspExtensions(
|
|
|
180
180
|
return true;
|
|
181
181
|
},
|
|
182
182
|
},
|
|
183
|
-
]);
|
|
183
|
+
]));
|
|
184
184
|
|
|
185
185
|
return [
|
|
186
186
|
syncPlugin,
|
|
187
|
-
|
|
187
|
+
// The active keymap (vscode or stock CM) owns completion keys; disable
|
|
188
|
+
// autocompletion's own bindings so they don't double up / conflict.
|
|
189
|
+
autocompletion({ override: [completionSource], defaultKeymap: false }),
|
|
188
190
|
hover,
|
|
189
191
|
gotoDef,
|
|
190
192
|
];
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
// changing a setting doesn't blow away cursor + scroll.
|
|
5
5
|
|
|
6
6
|
import React from "react";
|
|
7
|
-
import { Compartment, EditorState, Prec } from "@codemirror/state";
|
|
7
|
+
import { Compartment, EditorState, Prec, type Extension } from "@codemirror/state";
|
|
8
8
|
import {
|
|
9
9
|
EditorView,
|
|
10
10
|
keymap,
|
|
11
11
|
lineNumbers,
|
|
12
12
|
drawSelection,
|
|
13
|
+
rectangularSelection,
|
|
14
|
+
crosshairCursor,
|
|
13
15
|
highlightActiveLine,
|
|
14
16
|
} from "@codemirror/view";
|
|
15
17
|
import {
|
|
@@ -27,6 +29,7 @@ import {
|
|
|
27
29
|
import { lintGutter } from "@codemirror/lint";
|
|
28
30
|
import { searchKeymap } from "@codemirror/search";
|
|
29
31
|
import { completionKeymap, closeBrackets } from "@codemirror/autocomplete";
|
|
32
|
+
import { vscodeKeymap } from "@replit/codemirror-vscode-keymap";
|
|
30
33
|
import { nbtLanguageSupport } from "./nbt-language";
|
|
31
34
|
import { themeExtension } from "./editor-themes";
|
|
32
35
|
import { lspExtensions, type GotoDefHandler } from "./lsp-extensions";
|
|
@@ -51,6 +54,32 @@ function indentConfig(tabSize: number) {
|
|
|
51
54
|
]);
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
export type EditorKeymap = "vscode" | "codemirror";
|
|
58
|
+
|
|
59
|
+
// The mode-specific bindings, swapped live via a compartment. `vscode` is the
|
|
60
|
+
// full VSCode set (multi-cursor, history, search, completion, folding — all of
|
|
61
|
+
// it) INCLUDING the Alt-click / Alt-drag modifier for adding cursors (CM6
|
|
62
|
+
// defaults that to Ctrl/Cmd). `codemirror` is the stock CM6 stack — keyboard
|
|
63
|
+
// bindings plus the default Mod-click modifier. The Mod-s save is NOT here —
|
|
64
|
+
// it's a separate high-prec binding so it survives the swap and wins over both.
|
|
65
|
+
function keymapExtension(mode: EditorKeymap): Extension {
|
|
66
|
+
if (mode === "codemirror") {
|
|
67
|
+
return keymap.of([
|
|
68
|
+
...defaultKeymap,
|
|
69
|
+
...historyKeymap,
|
|
70
|
+
...searchKeymap,
|
|
71
|
+
...completionKeymap,
|
|
72
|
+
indentWithTab,
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
return [
|
|
76
|
+
keymap.of(vscodeKeymap),
|
|
77
|
+
// VSCode adds a cursor with Alt-click (Alt-drag → box select, already wired
|
|
78
|
+
// via rectangularSelection); CM6's default is Ctrl/Cmd-click.
|
|
79
|
+
EditorView.clickAddsSelectionRange.of((e) => e.altKey),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
54
83
|
export type NbtEditorProps = {
|
|
55
84
|
uri: string;
|
|
56
85
|
value: string;
|
|
@@ -63,6 +92,7 @@ export type NbtEditorProps = {
|
|
|
63
92
|
fontSize?: number;
|
|
64
93
|
lineWrap?: boolean;
|
|
65
94
|
tabSize?: number;
|
|
95
|
+
keymap?: EditorKeymap;
|
|
66
96
|
};
|
|
67
97
|
|
|
68
98
|
export const NbtEditor: React.FC<NbtEditorProps> = ({
|
|
@@ -77,6 +107,7 @@ export const NbtEditor: React.FC<NbtEditorProps> = ({
|
|
|
77
107
|
fontSize = 12,
|
|
78
108
|
lineWrap = false,
|
|
79
109
|
tabSize = 2,
|
|
110
|
+
keymap: keymapMode = "vscode",
|
|
80
111
|
}) => {
|
|
81
112
|
const hostRef = React.useRef<HTMLDivElement | null>(null);
|
|
82
113
|
const viewRef = React.useRef<EditorView | null>(null);
|
|
@@ -95,8 +126,9 @@ export const NbtEditor: React.FC<NbtEditorProps> = ({
|
|
|
95
126
|
const themeComp = React.useRef(new Compartment());
|
|
96
127
|
const wrapComp = React.useRef(new Compartment());
|
|
97
128
|
const indentComp = React.useRef(new Compartment());
|
|
98
|
-
const
|
|
99
|
-
settingsRef
|
|
129
|
+
const keymapComp = React.useRef(new Compartment());
|
|
130
|
+
const settingsRef = React.useRef({ theme, fontSize, lineWrap, tabSize, keymapMode });
|
|
131
|
+
settingsRef.current = { theme, fontSize, lineWrap, tabSize, keymapMode };
|
|
100
132
|
|
|
101
133
|
React.useEffect(() => {
|
|
102
134
|
const host = hostRef.current;
|
|
@@ -107,6 +139,9 @@ export const NbtEditor: React.FC<NbtEditorProps> = ({
|
|
|
107
139
|
lineNumbers(),
|
|
108
140
|
history(),
|
|
109
141
|
drawSelection(),
|
|
142
|
+
rectangularSelection(),
|
|
143
|
+
crosshairCursor(),
|
|
144
|
+
EditorState.allowMultipleSelections.of(true),
|
|
110
145
|
highlightActiveLine(),
|
|
111
146
|
bracketMatching(),
|
|
112
147
|
closeBrackets(),
|
|
@@ -116,20 +151,20 @@ export const NbtEditor: React.FC<NbtEditorProps> = ({
|
|
|
116
151
|
indentComp.current.of(indentConfig(s.tabSize)),
|
|
117
152
|
themeComp.current.of([sizingTheme(s.fontSize), themeExtension(s.theme)]),
|
|
118
153
|
wrapComp.current.of(s.lineWrap ? EditorView.lineWrapping : []),
|
|
119
|
-
keymap
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
154
|
+
// Save wins over whichever keymap set is active — kept out of the swappable
|
|
155
|
+
// compartment so a mode change can't drop it.
|
|
156
|
+
Prec.high(
|
|
157
|
+
keymap.of([
|
|
158
|
+
{
|
|
159
|
+
key: "Mod-s",
|
|
160
|
+
run: (view) => {
|
|
161
|
+
onSaveRef.current?.(view.state.doc.toString());
|
|
162
|
+
return true;
|
|
163
|
+
},
|
|
125
164
|
},
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
...searchKeymap,
|
|
130
|
-
...completionKeymap,
|
|
131
|
-
indentWithTab,
|
|
132
|
-
]),
|
|
165
|
+
]),
|
|
166
|
+
),
|
|
167
|
+
keymapComp.current.of(keymapExtension(s.keymapMode)),
|
|
133
168
|
EditorView.updateListener.of((u) => {
|
|
134
169
|
if (u.docChanged) onChangeRef.current?.(u.state.doc.toString());
|
|
135
170
|
}),
|
|
@@ -168,9 +203,10 @@ export const NbtEditor: React.FC<NbtEditorProps> = ({
|
|
|
168
203
|
]),
|
|
169
204
|
wrapComp.current.reconfigure(lineWrap ? EditorView.lineWrapping : []),
|
|
170
205
|
indentComp.current.reconfigure(indentConfig(tabSize)),
|
|
206
|
+
keymapComp.current.reconfigure(keymapExtension(keymapMode)),
|
|
171
207
|
],
|
|
172
208
|
});
|
|
173
|
-
}, [theme, fontSize, lineWrap, tabSize]);
|
|
209
|
+
}, [theme, fontSize, lineWrap, tabSize, keymapMode]);
|
|
174
210
|
|
|
175
211
|
return <div ref={hostRef} className="h-full min-h-0 overflow-hidden" />;
|
|
176
212
|
};
|