@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.
- package/README.md +12 -19
- package/bin/init.mjs +4 -1
- package/dist/module.d.mts +10 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +91 -50
- package/dist/runtime/app/components/DocBuilderActionsPanel.d.vue.ts +59 -0
- package/dist/runtime/app/components/DocBuilderActionsPanel.vue +175 -0
- package/dist/runtime/app/components/DocBuilderActionsPanel.vue.d.ts +59 -0
- package/dist/runtime/app/components/DocBuilderFieldsPanel.d.vue.ts +11 -0
- package/dist/runtime/app/components/DocBuilderFieldsPanel.vue +353 -0
- package/dist/runtime/app/components/DocBuilderFieldsPanel.vue.d.ts +11 -0
- package/dist/runtime/app/components/docbuilderActions.d.ts +62 -0
- package/dist/runtime/app/components/docbuilderActions.js +87 -0
- package/dist/runtime/app/composables/useClientAction.d.ts +30 -0
- package/dist/runtime/app/composables/useClientAction.js +51 -0
- package/dist/runtime/app/pages/DocBuilderDetail.vue +116 -71
- package/dist/runtime/app/pages/DocBuilderIndex.vue +83 -22
- package/dist/runtime/server/api/docbuilder/[doctype].get.d.ts +6 -1
- package/dist/runtime/server/api/docbuilder/[doctype].get.js +15 -5
- package/dist/runtime/server/api/docbuilder/doctypes.get.d.ts +5 -1
- package/dist/runtime/server/api/docbuilder/doctypes.get.js +5 -3
- package/dist/runtime/server/api/docbuilder/mergeDoctype.d.ts +11 -0
- package/dist/runtime/server/api/docbuilder/mergeDoctype.js +12 -0
- package/dist/runtime/server/api/docbuilder/save.post.d.ts +4 -1
- package/dist/runtime/server/api/docbuilder/save.post.js +48 -11
- package/dist/runtime/server/api/docbuilder/validate.post.d.ts +2 -1
- package/dist/runtime/server/api/docbuilder/validate.post.js +1 -1
- package/package.json +46 -43
- package/templates/Project.json +4 -10
- package/templates/Task.json +10 -17
- package/templates/resolvers.ts +49 -44
- package/templates/schema.graphql +23 -15
- 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
|
-
"
|
|
80
|
+
"fields": [
|
|
81
81
|
{
|
|
82
82
|
"fieldname": "title",
|
|
83
83
|
"label": "Title",
|
|
84
|
-
"
|
|
84
|
+
"component": "ATextInput",
|
|
85
85
|
"required": true
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
"fieldname": "description",
|
|
89
89
|
"label": "Description",
|
|
90
|
-
"
|
|
90
|
+
"component": "ATextarea"
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
"fieldname": "completed",
|
|
94
94
|
"label": "Completed",
|
|
95
|
-
"
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
'@stonecrop/themes/
|
|
227
|
-
|
|
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
|
-
|
|
464
|
-
-
|
|
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
|
|
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
|
|
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
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
|
-
|
|
124
|
-
|
|
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:
|
|
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
|
-
|
|
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;
|