@stonecrop/nuxt 0.13.14 → 0.15.0

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.
Files changed (33) hide show
  1. package/README.md +12 -19
  2. package/bin/init.mjs +4 -1
  3. package/dist/module.d.mts +10 -0
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +91 -50
  6. package/dist/runtime/app/components/DocBuilderActionsPanel.d.vue.ts +59 -0
  7. package/dist/runtime/app/components/DocBuilderActionsPanel.vue +175 -0
  8. package/dist/runtime/app/components/DocBuilderActionsPanel.vue.d.ts +59 -0
  9. package/dist/runtime/app/components/DocBuilderFieldsPanel.d.vue.ts +11 -0
  10. package/dist/runtime/app/components/DocBuilderFieldsPanel.vue +353 -0
  11. package/dist/runtime/app/components/DocBuilderFieldsPanel.vue.d.ts +11 -0
  12. package/dist/runtime/app/components/docbuilderActions.d.ts +62 -0
  13. package/dist/runtime/app/components/docbuilderActions.js +87 -0
  14. package/dist/runtime/app/composables/useClientAction.d.ts +30 -0
  15. package/dist/runtime/app/composables/useClientAction.js +51 -0
  16. package/dist/runtime/app/pages/DocBuilderDetail.vue +116 -71
  17. package/dist/runtime/app/pages/DocBuilderIndex.vue +83 -22
  18. package/dist/runtime/server/api/docbuilder/[doctype].get.d.ts +6 -1
  19. package/dist/runtime/server/api/docbuilder/[doctype].get.js +15 -5
  20. package/dist/runtime/server/api/docbuilder/doctypes.get.d.ts +5 -1
  21. package/dist/runtime/server/api/docbuilder/doctypes.get.js +5 -3
  22. package/dist/runtime/server/api/docbuilder/mergeDoctype.d.ts +11 -0
  23. package/dist/runtime/server/api/docbuilder/mergeDoctype.js +12 -0
  24. package/dist/runtime/server/api/docbuilder/save.post.d.ts +4 -1
  25. package/dist/runtime/server/api/docbuilder/save.post.js +48 -11
  26. package/dist/runtime/server/api/docbuilder/validate.post.d.ts +2 -1
  27. package/dist/runtime/server/api/docbuilder/validate.post.js +1 -1
  28. package/package.json +46 -43
  29. package/templates/Project.json +4 -10
  30. package/templates/Task.json +10 -17
  31. package/templates/resolvers.ts +49 -44
  32. package/templates/schema.graphql +23 -15
  33. package/templates/stonecrop.ts +3 -69
package/README.md CHANGED
@@ -77,22 +77,22 @@ Create a JSON schema in `/doctypes/task.json`:
77
77
  {
78
78
  "name": "task",
79
79
  "label": "Task",
80
- "schema": [
80
+ "fields": [
81
81
  {
82
82
  "fieldname": "title",
83
83
  "label": "Title",
84
- "fieldtype": "Data",
84
+ "component": "ATextInput",
85
85
  "required": true
86
86
  },
87
87
  {
88
88
  "fieldname": "description",
89
89
  "label": "Description",
90
- "fieldtype": "Text"
90
+ "component": "ATextarea"
91
91
  },
92
92
  {
93
93
  "fieldname": "completed",
94
94
  "label": "Completed",
95
- "fieldtype": "Check"
95
+ "component": "ACheckbox"
96
96
  }
97
97
  ]
98
98
  }
@@ -219,13 +219,11 @@ export default defineNuxtConfig({
219
219
 
220
220
  // Enable DocBuilder for visual schema editing
221
221
  docbuilder: false,
222
- },
223
222
 
224
- // Import Stonecrop theme
225
- css: [
226
- '@stonecrop/themes/default/default.css',
227
- // or your custom theme
228
- ]
223
+ // The default theme (@stonecrop/themes/default.css) loads automatically.
224
+ // Override it, or set `false` to bring your own:
225
+ // theme: '@stonecrop/themes/dark.css',
226
+ },
229
227
  })
230
228
  ```
231
229
 
@@ -237,6 +235,7 @@ export default defineNuxtConfig({
237
235
  | `routeStrategy` | `RouteStrategyFn` | Custom function receiving all parsed doctypes; returns a `NuxtPage[]`. Takes priority over `pageComponent`. |
238
236
  | `docbuilder` | `boolean` | Enable the DocBuilder feature at `/docbuilder`. Defaults to `false`. |
239
237
  | `doctypesDir` | `string` | Override the doctypes directory path. Defaults to `doctypes/` inside `srcDir`. |
238
+ | `theme` | `string \| false` | Base theme stylesheet loaded into the host app (supplies the `--sc-*` CSS variables). Defaults to `'@stonecrop/themes/default.css'`; set `false` to load none and bring your own. |
240
239
 
241
240
  If neither `pageComponent` nor `routeStrategy` is configured the module logs a warning and skips doctype route registration.
242
241
 
@@ -375,10 +374,8 @@ export default defineNuxtPlugin(() => {
375
374
  | `stonecrop` | `Stonecrop \| undefined` | The Stonecrop instance (undefined if not ready) |
376
375
  | `isReady` | `boolean` | `true` when both registry and stonecrop are available |
377
376
  | `registerClient(client)` | `(client: DataClient) => void` | Set the data client for record fetching. Throws if stonecrop not available. |
378
- | `getClient()` | `() => DataClient \| undefined` | Get the currently configured client. |
379
377
  | `registerMeta(fn)` | `(fn: (ctx) => Doctype) => void` | Set the `getMeta` function on the Registry. Throws if registry not available. |
380
378
  | `registerDoctype(doctype)` | `(doctype: Doctype) => void` | Pre-load a doctype into the Registry. Throws if registry not available. |
381
- | `dispatchAction(...)` | `Promise<{ success, data, error }>` | Dispatch an action via the configured client. |
382
379
 
383
380
  ## `useStonecropRegistry()` — Using the Framework in Components
384
381
 
@@ -460,12 +457,8 @@ const taskMachine = createMachine({
460
457
 
461
458
  ## Examples
462
459
 
463
- Check out the [playground](./playground) for an example featuring:
464
- - Permission management system (RBAC)
465
- - DocType builder with visual state machine editor
466
- - Complex nested forms with relationships
467
- - Table views with inline editing
468
- - HST state visualization
460
+ - **[playground](./playground)** — the default example: doctypes introspected from a live public GraphQL API with the `stonecrop-schema generate` CLI, browsed through the Desktop shell, and refined in the built-in DocBuilder (provenance-locked introspected fields, plus hand-authored workflow fixtures for the graph editor). `npm run dev` runs it.
461
+ - **[fullstack](./fullstack)** — the middleware core on the app's own GraphQL server (grafserv + guarded workflow transitions over in-memory storage, no database). `npm run dev:full` runs it.
469
462
 
470
463
 
471
464
  ## Contribution
@@ -531,7 +524,7 @@ npx stonecrop-nuxt init
531
524
  cd /path/to/stonecrop/nuxt
532
525
  npm pack
533
526
  cd /tmp/my-test-app
534
- npm install /path/to/stonecrop/nuxt/stonecrop-nuxt-0.6.3.tgz
527
+ npm install /path/to/stonecrop/nuxt/stonecrop-nuxt-<version>.tgz
535
528
  npx stonecrop-nuxt init
536
529
  ```
537
530
 
package/bin/init.mjs CHANGED
@@ -10,11 +10,14 @@
10
10
  import { defineCommand, runMain } from 'citty'
11
11
  import consola from 'consola'
12
12
  import { createJiti } from 'jiti'
13
+ import { readFileSync } from 'node:fs'
13
14
  import { fileURLToPath } from 'node:url'
14
15
  import { dirname, join } from 'node:path'
15
16
 
16
17
  const __dirname = dirname(fileURLToPath(import.meta.url))
17
18
 
19
+ const { version } = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'))
20
+
18
21
  // Use jiti to load TypeScript CLI code
19
22
  const jiti = createJiti(import.meta.url, {
20
23
  interopDefault: true,
@@ -26,7 +29,7 @@ const { runInstaller } = await jiti.import(join(__dirname, '..', 'src', 'cli', '
26
29
  const main = defineCommand({
27
30
  meta: {
28
31
  name: 'stonecrop-nuxt',
29
- version: '0.6.3',
32
+ version,
30
33
  description: 'Install Stonecrop modules into your Nuxt project',
31
34
  },
32
35
  subCommands: {
package/dist/module.d.mts CHANGED
@@ -76,6 +76,16 @@ interface ModuleOptions {
76
76
  * ```
77
77
  */
78
78
  routeStrategy?: RouteStrategyFn;
79
+ /**
80
+ * Base theme stylesheet loaded into the host app. Provides the `--sc-*` CSS variables,
81
+ * applies the document font, and normalizes form controls / code to inherit it. Defaults
82
+ * to Stonecrop's default theme. Set to `false` to load no theme (bring your own), or pass
83
+ * a different theme's module specifier / path (e.g. `'@stonecrop/themes/dark.css'`).
84
+ *
85
+ * The Stonecrop component packages are theme-agnostic — they consume `--sc-*` variables
86
+ * but never bundle a theme — so the host must supply one; the module does that here.
87
+ */
88
+ theme?: string | false;
79
89
  }
80
90
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
81
91
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stonecrop/nuxt",
3
3
  "configKey": "stonecrop",
4
- "version": "0.13.14",
4
+ "version": "0.15.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, realpathSync } from 'node:fs';
2
2
  import { readdir, readFile } from 'node:fs/promises';
3
3
  import { dirname, extname } from 'node:path';
4
- import { createResolver, defineNuxtModule, useLogger, addVitePlugin, addLayout, extendPages, addServerHandler, addPlugin, addImportsDir } from '@nuxt/kit';
4
+ import { createResolver, defineNuxtModule, useLogger, addVitePlugin, addLayout, extendPages, addServerHandler, addComponent, addPlugin, addImportsDir } from '@nuxt/kit';
5
5
 
6
6
  function createSymlinkedPackagesPlugin(options) {
7
7
  const { rootDir, packages, logger } = options;
@@ -66,7 +66,8 @@ const module$1 = defineNuxtModule({
66
66
  defaults: (_nuxt) => {
67
67
  return {
68
68
  docbuilder: false,
69
- doctypesDir: "doctypes"
69
+ doctypesDir: "doctypes",
70
+ theme: "@stonecrop/themes/default.css"
70
71
  };
71
72
  },
72
73
  async setup(options, nuxt) {
@@ -78,6 +79,10 @@ const module$1 = defineNuxtModule({
78
79
  }
79
80
  }
80
81
  logger.log("Added Stonecrop packages to build.transpile for SSR CSS handling");
82
+ if (options.theme && !nuxt.options.css.includes(options.theme)) {
83
+ nuxt.options.css.unshift(options.theme);
84
+ logger.log(`Added Stonecrop theme to app CSS: ${options.theme}`);
85
+ }
81
86
  nuxt.hook("nitro:config", (config) => {
82
87
  config.externals = config.externals || {};
83
88
  config.externals.inline = config.externals.inline || [];
@@ -104,11 +109,11 @@ const module$1 = defineNuxtModule({
104
109
  ...nuxt.options.runtimeConfig.stonecrop ?? {},
105
110
  doctypesDir
106
111
  };
112
+ const doctypes = [];
107
113
  if (existsSync(doctypesDir)) {
108
114
  try {
109
115
  const dirContents = await readdir(doctypesDir);
110
116
  const schemas = dirContents.filter((file) => extname(file) === ".json");
111
- const doctypes = [];
112
117
  for (const schema of schemas) {
113
118
  try {
114
119
  const schemaPath = resolve(doctypesDir, schema);
@@ -120,60 +125,19 @@ const module$1 = defineNuxtModule({
120
125
  logger.error(`Failed to parse schema file '${schema}':`, parseError);
121
126
  continue;
122
127
  }
123
- const schemaFields = schemaData.schema || schemaData.fields;
124
- if (!schemaFields) {
125
- logger.warn(`Schema file '${schema}' missing 'schema' or 'fields' property, skipping`);
128
+ if (!schemaData.fields) {
129
+ logger.warn(`Schema file '${schema}' missing 'fields' property, skipping`);
126
130
  continue;
127
131
  }
128
132
  doctypes.push({
129
133
  fileName: schema.replace(".json", ""),
130
134
  data: schemaData,
131
- fields: schemaFields
135
+ fields: schemaData.fields
132
136
  });
133
137
  } catch (schemaError) {
134
138
  logger.error(`Error processing schema '${schema}':`, schemaError);
135
139
  }
136
140
  }
137
- extendPages((pages) => {
138
- const pagePaths = pages.map((page) => page.path);
139
- if (!pagePaths.includes("/")) {
140
- pages.unshift({
141
- name: "stonecrop-home",
142
- path: "/",
143
- file: homepage
144
- });
145
- logger.log("Added Stonecrop home page at /");
146
- } else {
147
- logger.log("Skipping Stonecrop home page: root page already exists");
148
- }
149
- let generatedPages = [];
150
- if (options.routeStrategy) {
151
- generatedPages = options.routeStrategy(doctypes);
152
- } else if (options.pageComponent) {
153
- const componentPath = resolve(appDir, options.pageComponent);
154
- generatedPages = doctypes.map(({ fileName, data, fields }) => {
155
- const slug = data.slug || fileName.toLowerCase();
156
- return {
157
- name: `stonecrop-${fileName}`,
158
- path: `/${slug}`,
159
- file: componentPath,
160
- meta: { schema: fields, doctype: data }
161
- };
162
- });
163
- } else {
164
- logger.warn(
165
- "No routeStrategy or pageComponent configured \u2014 doctype routes will not be registered. Set pageComponent to a page path or provide a routeStrategy function."
166
- );
167
- }
168
- for (const page of generatedPages) {
169
- if (!pagePaths.includes(page.path)) {
170
- pages.unshift(page);
171
- logger.log(`Added route: ${page.path} (${page.name})`);
172
- } else {
173
- logger.warn(`Route ${page.path} already exists, skipping ${page.name}`);
174
- }
175
- }
176
- });
177
141
  } catch (doctypeError) {
178
142
  logger.error("Error setting up doctype pages:", doctypeError);
179
143
  if (nuxt.options.dev) {
@@ -183,20 +147,93 @@ const module$1 = defineNuxtModule({
183
147
  }
184
148
  }
185
149
  }
186
- if (options.docbuilder) {
150
+ extendPages((pages) => {
151
+ const pagePaths = pages.map((page) => page.path);
152
+ if (!pagePaths.includes("/")) {
153
+ pages.unshift({
154
+ name: "stonecrop-home",
155
+ path: "/",
156
+ file: homepage
157
+ });
158
+ logger.log("Added Stonecrop home page at /");
159
+ } else {
160
+ logger.log("Skipping Stonecrop home page: root page already exists");
161
+ }
162
+ let generatedPages = [];
163
+ if (options.routeStrategy) {
164
+ generatedPages = options.routeStrategy(doctypes);
165
+ } else if (options.pageComponent) {
166
+ const componentPath = resolve(appDir, options.pageComponent);
167
+ generatedPages = doctypes.map(({ fileName, data, fields }) => {
168
+ const slug = data.slug || fileName.toLowerCase();
169
+ return {
170
+ name: `stonecrop-${fileName}`,
171
+ path: `/${slug}`,
172
+ file: componentPath,
173
+ meta: { schema: fields, doctype: data }
174
+ };
175
+ });
176
+ } else {
177
+ logger.warn(
178
+ "No routeStrategy or pageComponent configured \u2014 doctype routes will not be registered. Set pageComponent to a page path or provide a routeStrategy function."
179
+ );
180
+ }
181
+ for (const page of generatedPages) {
182
+ if (!pagePaths.includes(page.path)) {
183
+ pages.unshift(page);
184
+ logger.log(`Added route: ${page.path} (${page.name})`);
185
+ } else {
186
+ logger.warn(`Route ${page.path} already exists, skipping ${page.name}`);
187
+ }
188
+ }
189
+ });
190
+ if (options.docbuilder && process.env.NODE_ENV === "development") {
187
191
  logger.log("DocBuilder enabled, adding routes and handlers");
192
+ nuxt.options.css.push("@vue-flow/core/dist/style.css");
193
+ nuxt.options.css.push("@vue-flow/core/dist/theme-default.css");
194
+ nuxt.options.css.push("@stonecrop/node-editor/styles");
195
+ nuxt.options.css.push("@stonecrop/desktop/styles");
196
+ nuxt.options.vite.optimizeDeps = nuxt.options.vite.optimizeDeps || {};
197
+ nuxt.options.vite.optimizeDeps.include = nuxt.options.vite.optimizeDeps.include || [];
198
+ for (const dep of ["@stonecrop/code-editor", "@stonecrop/schema"]) {
199
+ if (!nuxt.options.vite.optimizeDeps.include.includes(dep)) {
200
+ nuxt.options.vite.optimizeDeps.include.push(dep);
201
+ }
202
+ }
203
+ nuxt.options.vite.optimizeDeps.force = true;
204
+ nuxt.options.vite.plugins = nuxt.options.vite.plugins || [];
205
+ nuxt.options.vite.plugins.push({
206
+ name: "stonecrop:docbuilder-suppress-doctype-reload",
207
+ handleHotUpdate(ctx) {
208
+ if (ctx.file.startsWith(doctypesDir)) return [];
209
+ }
210
+ });
211
+ const { createRequire } = await import('node:module');
212
+ const { resolve: nodeResolve } = await import('node:path');
213
+ const req = createRequire(import.meta.url);
214
+ const monacoMinPath = nodeResolve(req.resolve("monaco-editor/package.json"), "../min");
215
+ nuxt.hook("nitro:config", (nitroConfig) => {
216
+ nitroConfig.publicAssets = nitroConfig.publicAssets ?? [];
217
+ nitroConfig.publicAssets.push({
218
+ dir: monacoMinPath,
219
+ baseURL: "/stonecrop-monaco",
220
+ maxAge: 60 * 60 * 24 * 365
221
+ });
222
+ });
188
223
  const docBuilderIndex = resolve("runtime/app/pages/DocBuilderIndex.vue");
189
224
  const docBuilderDetail = resolve("runtime/app/pages/DocBuilderDetail.vue");
190
225
  extendPages((pages) => {
191
226
  pages.push({
192
227
  name: "docbuilder-index",
193
228
  path: "/docbuilder",
194
- file: docBuilderIndex
229
+ file: docBuilderIndex,
230
+ meta: { layout: false }
195
231
  });
196
232
  pages.push({
197
233
  name: "docbuilder-detail",
198
234
  path: "/docbuilder/:doctype",
199
- file: docBuilderDetail
235
+ file: docBuilderDetail,
236
+ meta: { layout: false }
200
237
  });
201
238
  logger.log("Added DocBuilder pages at /docbuilder");
202
239
  });
@@ -220,6 +257,10 @@ const module$1 = defineNuxtModule({
220
257
  handler: resolve(handlersDir, "save.post")
221
258
  });
222
259
  logger.log("Added DocBuilder API handlers at /api/_stonecrop/docbuilder/");
260
+ addComponent({
261
+ name: "DocBuilderActionsPanel",
262
+ filePath: resolve("runtime/app/components/DocBuilderActionsPanel.vue")
263
+ });
223
264
  }
224
265
  const pluginPath = resolve("./runtime/plugin");
225
266
  try {
@@ -0,0 +1,59 @@
1
+ import type { WorkflowMeta } from '@stonecrop/schema';
2
+ type __VLS_Props = {
3
+ modelValue: WorkflowMeta | undefined;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ "update:modelValue": (value: {
7
+ states?: string[] | undefined;
8
+ actions?: Record<string, {
9
+ label: string;
10
+ requiredFields?: string[] | undefined;
11
+ allowedStates?: string[] | undefined;
12
+ nextState?: string | undefined;
13
+ stateless?: boolean | undefined;
14
+ selfTransition?: boolean | undefined;
15
+ clientHandler?: string | undefined;
16
+ }> | undefined;
17
+ triggers?: Record<string, {
18
+ on: string[];
19
+ clientHandler: string;
20
+ label?: string | undefined;
21
+ }> | undefined;
22
+ layout?: Record<string, {
23
+ position?: {
24
+ x: number;
25
+ y: number;
26
+ } | undefined;
27
+ targetPosition?: "left" | "right" | "top" | "bottom" | undefined;
28
+ sourcePosition?: "left" | "right" | "top" | "bottom" | undefined;
29
+ }> | undefined;
30
+ }) => any;
31
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
32
+ "onUpdate:modelValue"?: ((value: {
33
+ states?: string[] | undefined;
34
+ actions?: Record<string, {
35
+ label: string;
36
+ requiredFields?: string[] | undefined;
37
+ allowedStates?: string[] | undefined;
38
+ nextState?: string | undefined;
39
+ stateless?: boolean | undefined;
40
+ selfTransition?: boolean | undefined;
41
+ clientHandler?: string | undefined;
42
+ }> | undefined;
43
+ triggers?: Record<string, {
44
+ on: string[];
45
+ clientHandler: string;
46
+ label?: string | undefined;
47
+ }> | undefined;
48
+ layout?: Record<string, {
49
+ position?: {
50
+ x: number;
51
+ y: number;
52
+ } | undefined;
53
+ targetPosition?: "left" | "right" | "top" | "bottom" | undefined;
54
+ sourcePosition?: "left" | "right" | "top" | "bottom" | undefined;
55
+ }> | undefined;
56
+ }) => any) | undefined;
57
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
58
+ declare const _default: typeof __VLS_export;
59
+ export default _default;
@@ -0,0 +1,175 @@
1
+ <template>
2
+ <div class="actions-panel">
3
+ <ATable v-if="hasRows" :columns="ACTION_COLUMNS" :rows="rows" :config="{ view: 'list-expansion', fullWidth: true }">
4
+ <!--
5
+ ATable owns the frame (header, filter row, expansion chrome). We override #body to keep
6
+ the per-row #content editor and to bind cells straight to the source via the write helpers
7
+ — ATable never calls setCellData/handleRowAction, so it never mutates the projection it renders.
8
+ -->
9
+ <template #body="{ data: store }">
10
+ <ARow v-for="row in store.filteredRows" :key="String(row.__key)" :row-index="row.originalIndex" :store="store">
11
+ <template #default>
12
+ <td>
13
+ <!-- A Transition is named on its edge in the graph (single source of truth), so its
14
+ name is read-only here; Commands and Triggers have no graph, so they're named here. -->
15
+ <span
16
+ v-if="row.kind === 'transition' || row.kind === 'self-transition'"
17
+ class="cell-readonly"
18
+ :title="TRANSITION_NAME_HINT">
19
+ {{ row.label }}
20
+ </span>
21
+ <input
22
+ v-else
23
+ type="text"
24
+ :value="row.label"
25
+ placeholder="Name"
26
+ @input="onFieldInput(row, 'label', $event.target.value)" />
27
+ </td>
28
+ <td>
29
+ <span class="badge" :class="`badge-${row.kind}`">
30
+ {{ row.type }}
31
+ </span>
32
+ </td>
33
+ </template>
34
+ <template #content>
35
+ <div class="row-detail">
36
+ <!-- Type-specific fields live here so the collapsed row keeps to the common columns. -->
37
+ <div v-if="row.kind === 'trigger'" class="detail-field">
38
+ <label class="detail-label">On (fields)</label>
39
+ <input
40
+ type="text"
41
+ :value="onDisplay(row)"
42
+ placeholder="fields, comma-separated"
43
+ @input="onOnInput(row, $event.target.value)" />
44
+ </div>
45
+ <template v-else>
46
+ <div class="detail-field">
47
+ <label class="detail-label">Allowed States</label>
48
+ <span class="detail-value">{{ row.allowedStates }}</span>
49
+ </div>
50
+ <div v-if="row.kind === 'transition'" class="detail-field">
51
+ <label class="detail-label">Next State</label>
52
+ <span class="detail-value">{{ row.nextState }}</span>
53
+ </div>
54
+ </template>
55
+
56
+ <div class="client-handler-editor">
57
+ <label class="handler-label">Client Handler (JS)</label>
58
+ <ACodeEditor
59
+ :model-value="(row.__action ?? row.__trigger)?.clientHandler ?? ''"
60
+ :extra-libs="row.kind === 'trigger' ? TRIGGER_API_STUBS : INJECTED_API_STUBS"
61
+ :libs="EDITOR_LIBS"
62
+ :vs-path="VS_PATH"
63
+ :options="row.kind === 'trigger' ? TRIGGER_EDITOR_OPTIONS : EDITOR_OPTIONS"
64
+ language="javascript"
65
+ height="200px"
66
+ @update:model-value="onFieldInput(row, 'clientHandler', $event)" />
67
+ </div>
68
+
69
+ <!-- Transitions and self-transitions are graph-owned — removed by deleting their
70
+ edges (a self-loop is an edge too), not here. -->
71
+ <button
72
+ v-if="row.kind !== 'transition' && row.kind !== 'self-transition'"
73
+ type="button"
74
+ class="remove-row"
75
+ @click="onRemoveRow(row)">
76
+ Remove {{ row.kind }}
77
+ </button>
78
+ </div>
79
+ </template>
80
+ </ARow>
81
+ </template>
82
+ </ATable>
83
+ <p v-else class="actions-empty">
84
+ No actions or triggers yet. Draw transitions in the workflow graph above, or add a command or trigger below.
85
+ </p>
86
+ <div class="panel-footer">
87
+ <button type="button" class="add-row" @click="onAddCommand">+ Add Command</button>
88
+ <button type="button" class="add-row" @click="onAddTrigger">+ Add Trigger</button>
89
+ </div>
90
+ </div>
91
+ </template>
92
+
93
+ <script setup>
94
+ import { ATable, ARow } from "@stonecrop/atable";
95
+ import { ACodeEditor } from "@stonecrop/code-editor";
96
+ import { computed, reactive } from "vue";
97
+ import {
98
+ addCommand,
99
+ addTrigger,
100
+ parseOnInput,
101
+ projectWorkflowRows,
102
+ removeAction,
103
+ removeTrigger,
104
+ writeActionField,
105
+ writeTriggerField
106
+ } from "./docbuilderActions";
107
+ const TRANSITION_NAME_HINT = "Rename this transition on its edge in the workflow graph above";
108
+ const INJECTED_API_STUBS = `
109
+ declare const router: {
110
+ push(to: string | object): Promise<void>
111
+ replace(to: string | object): Promise<void>
112
+ back(): void
113
+ forward(): void
114
+ }
115
+ declare function runAction(action: string, args?: Record<string, unknown>): Promise<{ success: boolean; data: unknown; error: string | null }>
116
+ declare const graphql: {
117
+ query(query: string, variables?: Record<string, unknown>): Promise<unknown>
118
+ }
119
+ declare const record: Record<string, unknown>
120
+ `;
121
+ const TRIGGER_API_STUBS = `
122
+ declare const record: Record<string, unknown>
123
+ declare const value: unknown
124
+ declare function setError(field: string, message: string): void
125
+ `;
126
+ const VS_PATH = "/stonecrop-monaco/vs";
127
+ const EDITOR_OPTIONS = {
128
+ placeholder: "e.g. await runAction('Submit'); router.push(`/issue/${record.id}`)"
129
+ };
130
+ const TRIGGER_EDITOR_OPTIONS = {
131
+ placeholder: "e.g. if (value < 0) setError('amount', 'Must be positive')"
132
+ };
133
+ const EDITOR_LIBS = ["es2020"];
134
+ const ACTION_COLUMNS = [
135
+ { name: "label", label: "Label", sortable: false },
136
+ { name: "type", label: "Type", sortable: false, filterable: true, filterType: "select" }
137
+ ];
138
+ const props = defineProps({
139
+ modelValue: { type: null, required: true }
140
+ });
141
+ const emit = defineEmits(["update:modelValue"]);
142
+ const rows = computed(() => projectWorkflowRows(props.modelValue));
143
+ const hasRows = computed(() => rows.value.length > 0);
144
+ function onFieldInput(row, field, value) {
145
+ if (!props.modelValue) return;
146
+ const next = row.kind === "trigger" ? writeTriggerField(props.modelValue, row.__key, field, value) : writeActionField(props.modelValue, row.__key, field, value);
147
+ emit("update:modelValue", next);
148
+ }
149
+ const onDrafts = reactive({});
150
+ function onDisplay(row) {
151
+ const draft = onDrafts[row.__key];
152
+ return draft ?? row.on;
153
+ }
154
+ function onOnInput(row, value) {
155
+ if (!props.modelValue) return;
156
+ onDrafts[row.__key] = value;
157
+ emit("update:modelValue", writeTriggerField(props.modelValue, row.__key, "on", parseOnInput(value)));
158
+ }
159
+ function onAddTrigger() {
160
+ emit("update:modelValue", addTrigger(props.modelValue));
161
+ }
162
+ function onAddCommand() {
163
+ emit("update:modelValue", addCommand(props.modelValue));
164
+ }
165
+ function onRemoveRow(row) {
166
+ if (!props.modelValue) return;
167
+ onDrafts[row.__key] = void 0;
168
+ const next = row.kind === "trigger" ? removeTrigger(props.modelValue, row.__key) : removeAction(props.modelValue, row.__key);
169
+ emit("update:modelValue", next);
170
+ }
171
+ </script>
172
+
173
+ <style scoped>
174
+ .actions-panel{padding:.5em 1em}.actions-panel :deep(.atable-row>td){border-top:1px solid var(--sc-row-border-color,#e5e7eb);padding:var(--sc-atable-row-padding,.25rem) .75em;vertical-align:middle}.actions-panel :deep(input[type=text]){border:1px solid var(--sc-gray-20,#d1d5db);border-radius:3px;font-family:inherit;font-size:inherit;padding:.25em .5em;width:100%}.actions-panel :deep(input[type=text]:focus){border-color:var(--sc-blue-40,#3b82f6);outline:none}.cell-readonly{color:var(--sc-gray-60,#4b5563);cursor:default}.badge{border-radius:9999px;display:inline-block;font-size:.75rem;font-weight:500;padding:.125em .5em}.badge-transition{background:#dbeafe;color:#1e40af}.badge-self-transition{background:#fef3c7;color:#92400e}.badge-command{background:#f3e8ff;color:#6b21a8}.badge-trigger{background:#dcfce7;color:#166534}.actions-empty{color:#9ca3af;font-style:italic;padding:1rem 0;text-align:center}.panel-footer{display:flex;gap:.5rem;padding:.75rem 0 .25rem}.add-row{background:var(--sc-blue-40,#3b82f6);border:none;border-radius:.4rem;color:#fff;cursor:pointer;font-size:.8125rem;font-weight:500;padding:.4em .9em}.row-detail{gap:.75rem}.detail-field,.row-detail{display:flex;flex-direction:column}.detail-field{gap:.25rem}.detail-label{color:var(--sc-header-text-color,#374151);font-size:.75rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase}.detail-value{color:var(--sc-gray-60,#4b5563);font-size:.8125rem}.client-handler-editor{display:flex;flex-direction:column;gap:.5rem}.handler-label{color:var(--sc-header-text-color,#374151);font-size:.75rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase}.remove-row{align-self:flex-start;background:none;border:1px solid #fca5a5;border-radius:.3rem;color:#b91c1c;cursor:pointer;font-size:.75rem;padding:.3em .75em}
175
+ </style>
@@ -0,0 +1,59 @@
1
+ import type { WorkflowMeta } from '@stonecrop/schema';
2
+ type __VLS_Props = {
3
+ modelValue: WorkflowMeta | undefined;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ "update:modelValue": (value: {
7
+ states?: string[] | undefined;
8
+ actions?: Record<string, {
9
+ label: string;
10
+ requiredFields?: string[] | undefined;
11
+ allowedStates?: string[] | undefined;
12
+ nextState?: string | undefined;
13
+ stateless?: boolean | undefined;
14
+ selfTransition?: boolean | undefined;
15
+ clientHandler?: string | undefined;
16
+ }> | undefined;
17
+ triggers?: Record<string, {
18
+ on: string[];
19
+ clientHandler: string;
20
+ label?: string | undefined;
21
+ }> | undefined;
22
+ layout?: Record<string, {
23
+ position?: {
24
+ x: number;
25
+ y: number;
26
+ } | undefined;
27
+ targetPosition?: "left" | "right" | "top" | "bottom" | undefined;
28
+ sourcePosition?: "left" | "right" | "top" | "bottom" | undefined;
29
+ }> | undefined;
30
+ }) => any;
31
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
32
+ "onUpdate:modelValue"?: ((value: {
33
+ states?: string[] | undefined;
34
+ actions?: Record<string, {
35
+ label: string;
36
+ requiredFields?: string[] | undefined;
37
+ allowedStates?: string[] | undefined;
38
+ nextState?: string | undefined;
39
+ stateless?: boolean | undefined;
40
+ selfTransition?: boolean | undefined;
41
+ clientHandler?: string | undefined;
42
+ }> | undefined;
43
+ triggers?: Record<string, {
44
+ on: string[];
45
+ clientHandler: string;
46
+ label?: string | undefined;
47
+ }> | undefined;
48
+ layout?: Record<string, {
49
+ position?: {
50
+ x: number;
51
+ y: number;
52
+ } | undefined;
53
+ targetPosition?: "left" | "right" | "top" | "bottom" | undefined;
54
+ sourcePosition?: "left" | "right" | "top" | "bottom" | undefined;
55
+ }> | undefined;
56
+ }) => any) | undefined;
57
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
58
+ declare const _default: typeof __VLS_export;
59
+ export default _default;
@@ -0,0 +1,11 @@
1
+ type Field = Record<string, unknown>;
2
+ type __VLS_Props = {
3
+ modelValue: Field[];
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ "update:modelValue": (value: Field[]) => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
8
+ "onUpdate:modelValue"?: ((value: Field[]) => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;