@stonecrop/nuxt 0.13.12 → 0.14.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 +31 -24
  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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/nuxt",
3
- "version": "0.13.12",
3
+ "version": "0.14.0",
4
4
  "description": "Nuxt module for Stonecrop",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,45 +38,53 @@
38
38
  ],
39
39
  "dependencies": {
40
40
  "@nuxt/kit": "^4.4.4",
41
+ "h3": "^1.15.11",
41
42
  "pinia": "^3.0.4",
42
- "citty": "^0.2.1",
43
- "consola": "^3.4.0",
44
- "jiti": "^2.4.2",
43
+ "citty": "^0.2.2",
44
+ "consola": "^3.4.2",
45
+ "jiti": "^2.7.0",
45
46
  "pathe": "^2.0.3",
46
47
  "prompts": "^2.4.2",
47
- "@stonecrop/aform": "0.13.12",
48
- "@stonecrop/atable": "0.13.12",
49
- "@stonecrop/graphql-middleware": "0.13.12",
50
- "@stonecrop/casl-middleware": "0.13.12",
51
- "@stonecrop/node-editor": "0.13.12",
52
- "@stonecrop/schema": "0.13.12",
53
- "@stonecrop/nuxt-grafserv": "0.13.12",
54
- "@stonecrop/stonecrop": "0.13.12"
48
+ "@stonecrop/aform": "0.14.0",
49
+ "@stonecrop/atable": "0.14.0",
50
+ "@stonecrop/desktop": "0.14.0",
51
+ "@stonecrop/code-editor": "0.14.0",
52
+ "@stonecrop/casl-middleware": "0.14.0",
53
+ "@stonecrop/graphql-client": "0.14.0",
54
+ "@stonecrop/graphql-middleware": "0.14.0",
55
+ "@stonecrop/node-editor": "0.14.0",
56
+ "@stonecrop/nuxt-grafserv": "0.14.0",
57
+ "@stonecrop/schema": "0.14.0",
58
+ "@stonecrop/stonecrop": "0.14.0",
59
+ "@stonecrop/themes": "0.14.0"
55
60
  },
56
61
  "devDependencies": {
57
62
  "@eslint/js": "^10.0.1",
58
63
  "@nuxt/devtools": "^3.2.4",
59
- "@nuxt/eslint-config": "^1.15.2",
60
- "@nuxt/eslint": "1.15.2",
64
+ "@nuxt/eslint-config": "^1.16.0",
65
+ "@nuxt/eslint": "1.16.0",
61
66
  "@nuxt/module-builder": "^1.0.2",
62
- "@nuxt/schema": "^4.4.4",
67
+ "@nuxt/schema": "^4.4.8",
63
68
  "@nuxt/test-utils": "^4.0.3",
64
69
  "@types/prompts": "^2.4.9",
65
70
  "@vitest/coverage-istanbul": "^4.1.5",
66
71
  "baseline-browser-mapping": "latest",
67
72
  "browserslist": "latest",
68
73
  "eslint-config-prettier": "^10.1.8",
69
- "eslint-plugin-vue": "^10.9.0",
70
- "eslint": "^10.3.0",
74
+ "eslint-plugin-vue": "^10.9.2",
75
+ "eslint": "^10.6.0",
71
76
  "globals": "^17.6.0",
72
- "nuxt": "^4.4.4",
77
+ "graphql": "^16.14.0",
78
+ "nuxt": "^4.4.8",
79
+ "nuxt-graphql-middleware": "^5.4.0",
73
80
  "postgraphile": "^5.0.3",
74
- "typescript-eslint": "^8.59.1",
81
+ "typescript-eslint": "^8.62.1",
75
82
  "typescript": "^6.0.3",
83
+ "monaco-editor": "^0.55.1",
76
84
  "vite": "^7.3.2",
77
85
  "vitest": "^4.1.5",
78
86
  "vue-router": "^5.0.6",
79
- "vue-tsc": "^3.2.7",
87
+ "vue-tsc": "^3.3.6",
80
88
  "vue": "^3.5.33"
81
89
  },
82
90
  "engines": {
@@ -87,9 +95,8 @@
87
95
  "prepublish": "rushx dev:prepare && nuxt-module-build build",
88
96
  "build": "rushx dev:prepare && nuxt-module-build build",
89
97
  "docs": "echo 'Nuxt modules use custom build system - skipping API docs generation'",
90
- "dev": "rushx dev:prepare && nuxi dev playground",
91
- "dev:basic": "rushx dev:prepare && nuxi dev playground",
92
- "dev:full": "rushx dev:prepare:full && nuxi dev fullstack",
98
+ "dev": "rushx dev:prepare && nuxi dev playground --host",
99
+ "dev:full": "rushx dev:prepare:full && nuxi dev fullstack --host",
93
100
  "dev:prepare:full": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare fullstack",
94
101
  "dev:build": "nuxi build playground",
95
102
  "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground && nuxi prepare fullstack",
@@ -98,6 +105,6 @@
98
105
  "test:ui": "vitest --ui",
99
106
  "test:coverage": "vitest run --coverage.enabled --coverage.provider=istanbul",
100
107
  "test:watch": "vitest watch",
101
- "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
108
+ "test:types": "vue-tsc --noEmit -p tsconfig.runtime.json && cd playground && vue-tsc --noEmit && cd ../fullstack && vue-tsc --noEmit"
102
109
  }
103
110
  }
@@ -6,35 +6,30 @@
6
6
  "fieldname": "id",
7
7
  "label": "ID",
8
8
  "component": "ATextInput",
9
- "fieldtype": "Data",
10
9
  "mode": "display"
11
10
  },
12
11
  {
13
12
  "fieldname": "title",
14
13
  "label": "Title",
15
14
  "component": "ATextInput",
16
- "fieldtype": "Data",
17
15
  "required": true
18
16
  },
19
17
  {
20
18
  "fieldname": "description",
21
19
  "label": "Description",
22
- "component": "ATextarea",
23
- "fieldtype": "Text"
20
+ "component": "ATextarea"
24
21
  },
25
22
  {
26
23
  "fieldname": "status",
27
24
  "label": "Status",
28
25
  "component": "ADropdown",
29
- "fieldtype": "Select",
30
26
  "options": ["Active", "Archived"],
31
27
  "default": "Active"
32
28
  },
33
29
  {
34
30
  "fieldname": "createdAt",
35
31
  "label": "Created At",
36
- "component": "ATextInput",
37
- "fieldtype": "Datetime",
32
+ "component": "ADateTime",
38
33
  "mode": "display"
39
34
  }
40
35
  ],
@@ -43,13 +38,12 @@
43
38
  "actions": {
44
39
  "save": {
45
40
  "label": "Save",
46
- "handler": "project:save"
41
+ "stateless": true
47
42
  },
48
43
  "archive": {
49
44
  "label": "Archive Project",
50
- "handler": "archive_project",
51
45
  "allowedStates": ["Active"],
52
- "confirm": true
46
+ "nextState": "Archived"
53
47
  }
54
48
  }
55
49
  }
@@ -6,49 +6,42 @@
6
6
  "fieldname": "id",
7
7
  "label": "ID",
8
8
  "component": "ATextInput",
9
- "fieldtype": "Data",
10
9
  "mode": "display"
11
10
  },
12
11
  {
13
12
  "fieldname": "title",
14
13
  "label": "Title",
15
14
  "component": "ATextInput",
16
- "fieldtype": "Data",
17
15
  "required": true
18
16
  },
19
17
  {
20
18
  "fieldname": "projectId",
21
19
  "label": "Project",
22
- "component": "ACombobox",
23
- "fieldtype": "Link",
24
- "options": "project",
20
+ "component": "AFormLink",
21
+ "doctype": "project",
25
22
  "required": true
26
23
  },
27
24
  {
28
25
  "fieldname": "status",
29
26
  "label": "Status",
30
27
  "component": "ADropdown",
31
- "fieldtype": "Select",
32
28
  "options": ["Todo", "In Progress", "Done"],
33
29
  "default": "Todo"
34
30
  },
35
31
  {
36
32
  "fieldname": "description",
37
33
  "label": "Description",
38
- "component": "ATextarea",
39
- "fieldtype": "Text"
34
+ "component": "ATextarea"
40
35
  },
41
36
  {
42
37
  "fieldname": "dueDate",
43
38
  "label": "Due Date",
44
- "component": "ADatepicker",
45
- "fieldtype": "Date"
39
+ "component": "ADatePicker"
46
40
  },
47
41
  {
48
42
  "fieldname": "createdAt",
49
43
  "label": "Created At",
50
- "component": "ATextInput",
51
- "fieldtype": "Datetime",
44
+ "component": "ADateTime",
52
45
  "mode": "display"
53
46
  }
54
47
  ],
@@ -57,17 +50,17 @@
57
50
  "actions": {
58
51
  "save": {
59
52
  "label": "Save",
60
- "handler": "task:save"
53
+ "stateless": true
61
54
  },
62
55
  "start_task": {
63
56
  "label": "Start Task",
64
- "handler": "start_task",
65
- "allowedStates": ["Todo"]
57
+ "allowedStates": ["Todo"],
58
+ "nextState": "In Progress"
66
59
  },
67
60
  "complete_task": {
68
61
  "label": "Complete",
69
- "handler": "complete_task",
70
- "allowedStates": ["In Progress"]
62
+ "allowedStates": ["In Progress"],
63
+ "nextState": "Done"
71
64
  }
72
65
  }
73
66
  }
@@ -9,16 +9,16 @@
9
9
  * - loadOne($step, fn) — batch-load data ASYNCHRONOUSLY (for DB queries)
10
10
  * - object({ ... }) — group multiple steps into a single step object
11
11
  *
12
- * Data reads go through loadOne; data writes happen in server/plugins/stonecrop.ts
13
- * via registered action handlers (start_task, complete_task, archive_project).
12
+ * Data reads go through loadOne. Workflow state transitions are applied by the
13
+ * stonecropAction resolver itself (server-owns-transition, guarded by allowedStates);
14
+ * side-effecting saves go through registered handlers (project:save, task:save).
14
15
  *
15
16
  * To connect a real database, replace the imports from ./data with your
16
17
  * PostGraphile setup. See: https://stonecrop.io/docs/guides/postgraphile
17
18
  */
18
19
 
19
20
  import { constant, lambda, loadOne, object } from 'grafast'
20
- import { getMeta, getAllMeta, getHandler } from '@stonecrop/graphql-middleware'
21
- import type { ActionContext } from '@stonecrop/graphql-middleware'
21
+ import { getMeta, getAllMeta, applyGuardedTransition } from '@stonecrop/graphql-middleware'
22
22
  import type { DoctypeMeta } from '@stonecrop/schema'
23
23
  import { projects, tasks, type Project, type Task } from './data'
24
24
 
@@ -27,45 +27,22 @@ import { projects, tasks, type Project, type Task } from './data'
27
27
  // response type defined in server/schema.graphql)
28
28
  // ============================================================
29
29
 
30
- function formatFieldMeta(field: { kind?: string; fieldname: string; fieldtype?: string; [key: string]: unknown }) {
31
- return {
32
- fieldname: field.fieldname,
33
- fieldtype: field.fieldtype ?? null,
34
- label: field.label ?? null,
35
- required: field.required ?? false,
36
- readOnly: field.readOnly ?? false,
37
- options: field.options ?? null,
38
- default: field.default ?? null,
39
- width: field.width ?? null,
40
- validation: field.validation ?? null,
41
- component: field.component ?? null,
42
- align: field.align ?? null,
43
- edit: field.edit ?? null,
44
- hidden: field.hidden ?? null,
45
- mask: field.mask ?? null,
46
- precision: field.precision ?? null,
47
- scale: field.scale ?? null,
48
- mode: field.mode ?? null,
49
- }
50
- }
51
-
52
- function formatDoctypeMeta(meta: DoctypeMeta) {
30
+ export function formatDoctypeMeta(meta: DoctypeMeta) {
31
+ // Fields and actions pass through verbatim — the SDL alone decides what is
32
+ // selectable. Enumerating keys here silently drops any field the schema gains
33
+ // later; the only computed addition is `name` (an action's key in the
34
+ // WorkflowMeta.actions record, flattened into the list the SDL declares).
53
35
  const actions = meta.workflow?.actions
54
36
  const actionList = actions
55
37
  ? Object.entries(actions as Record<string, Record<string, unknown>>).map(([name, action]) => ({
56
38
  name,
57
- label: action.label ?? null,
58
- handler: action.handler ?? null,
59
- requiredFields: (action.requiredFields as string[]) ?? [],
60
- allowedStates: (action.allowedStates as string[]) ?? [],
61
- confirm: action.confirm ?? false,
62
- args: action.args ?? null,
39
+ ...action,
63
40
  }))
64
41
  : []
65
42
  return {
66
43
  name: meta.name,
67
44
  slug: meta.slug ?? null,
68
- fields: meta.fields.map(formatFieldMeta),
45
+ fields: meta.fields,
69
46
  workflow: meta.workflow
70
47
  ? {
71
48
  states: meta.workflow.states ?? null,
@@ -188,18 +165,46 @@ export const resolvers = {
188
165
  const actionDef = meta.workflow?.actions?.[spec.action]
189
166
  if (!actionDef) return { success: false, data: null, error: `Unknown action: ${spec.action}` }
190
167
 
191
- const handler = getHandler(actionDef.handler)
192
- if (!handler)
193
- return { success: false, data: null, error: `Handler not registered: ${actionDef.handler}` }
194
-
195
- // Pass doctype metadata via ActionContext. The handler in server/plugins/stonecrop.ts
196
- // imports and mutates the data Maps directly — it does not need additional context here.
197
- // In a PostGraphile setup, context.pgClient would provide the database connection instead.
198
- const actionContext: ActionContext = { doctype: meta }
168
+ // Record envelope: [{ id, data }] — the transition keys off the record id, and a
169
+ // self-transition persists the edited field `data` in place.
170
+ const argList = Array.isArray(spec.actionArgs) ? spec.actionArgs : []
171
+ const recordId = argList[0]?.id != null ? String(argList[0].id) : undefined
172
+ const recordData: Record<string, unknown> = argList[0]?.data ?? {}
173
+ const d = spec.doctype.toLowerCase()
199
174
 
200
175
  try {
201
- const result = await handler(spec.actionArgs ?? [], actionContext)
202
- return { success: true, data: result, error: null }
176
+ // The server owns the transition: read current state, guard against allowedStates,
177
+ // write nextState. Reads/writes go straight to the in-memory Maps; a PostGraphile
178
+ // setup swaps in pgClient SQL instead.
179
+ return await applyGuardedTransition(
180
+ actionDef,
181
+ {
182
+ readState: async () => {
183
+ if (recordId == null) return undefined
184
+ const record = getRecord(d, recordId)
185
+ return record?.status == null ? undefined : String(record.status)
186
+ },
187
+ writeState: async (nextState: string) => {
188
+ if (recordId == null) return
189
+ const existing = getRecord(d, recordId)
190
+ if (!existing) return
191
+ if (d === 'project') projects.set(recordId, { ...existing, status: nextState } as Project)
192
+ else if (d === 'task') tasks.set(recordId, { ...existing, status: nextState } as Task)
193
+ },
194
+ // Self-transition data write: merge the edited fields into the record (status
195
+ // untouched) and return the full record for the client writeback.
196
+ writeData: async (patch: Record<string, unknown>) => {
197
+ if (recordId == null) return {}
198
+ const existing = getRecord(d, recordId)
199
+ if (!existing) return {}
200
+ const updated = { ...existing, ...patch }
201
+ if (d === 'project') projects.set(recordId, updated as Project)
202
+ else if (d === 'task') tasks.set(recordId, updated as Task)
203
+ return updated as Record<string, unknown>
204
+ },
205
+ },
206
+ recordData
207
+ )
203
208
  } catch (err) {
204
209
  return { success: false, data: null, error: err instanceof Error ? err.message : String(err) }
205
210
  }
@@ -11,36 +11,44 @@ scalar JSON
11
11
  Field metadata from doctype schema
12
12
  """
13
13
  type StonecropField {
14
+ kind: String!
14
15
  fieldname: String!
15
- fieldtype: String!
16
+ component: String
17
+ primaryKey: Boolean
18
+ computed: Boolean
19
+ language: String
20
+ doctype: String
16
21
  label: String
17
- required: Boolean
18
- readOnly: Boolean
19
- options: JSON
20
- default: JSON
21
22
  width: String
22
- validation: JSON
23
- component: String
24
23
  align: String
25
24
  edit: Boolean
26
- hidden: Boolean
27
25
  mask: String
28
- precision: Int
29
- scale: Int
26
+ format: String
30
27
  mode: String
28
+ options: JSON
29
+ required: Boolean
30
+ readOnly: Boolean
31
+ hidden: Boolean
32
+ default: JSON
33
+ validation: JSON
34
+ cardinality: String
35
+ source: String
31
36
  }
32
37
 
33
38
  """
34
- A single workflow action definition
39
+ A single workflow action definition (`name` is the action's key in
40
+ WorkflowMeta.actions; the other fields mirror ActionDefinition in
41
+ @stonecrop/schema)
35
42
  """
36
43
  type WorkflowAction {
37
44
  name: String!
38
- label: String
39
- handler: String
45
+ label: String!
40
46
  requiredFields: [String!]
41
47
  allowedStates: [String!]
42
- confirm: Boolean
43
- args: JSON
48
+ nextState: String
49
+ stateless: Boolean
50
+ selfTransition: Boolean
51
+ clientHandler: String
44
52
  }
45
53
 
46
54
  """
@@ -1,82 +1,16 @@
1
1
  /**
2
2
  * Stonecrop Server Plugin
3
3
  *
4
- * Runs at server startup (Nitro plugin) to:
5
- * 1. Load doctype definitions from the doctypes/ directory
6
- * 2. Register built-in action handlers (validation, etc.)
7
- * 3. Register custom workflow action handlers
8
- *
9
- * Action handlers receive (args, context) where args is the array sent by the
10
- * client — the app's frontend (app/composables/useDoctypes.ts) sends a single
11
- * `{ id, data? }` object per action — and context.doctype is the DoctypeMeta
12
- * for the doctype being acted upon. For this in-memory setup, data is imported
13
- * directly from server/data.ts. In production with PostGraphile, context.pgClient
14
- * provides an active database connection instead — see:
15
- * https://stonecrop.io/docs/guides/postgraphile
4
+ * Runs at server startup (Nitro plugin) to load doctype definitions from the
5
+ * doctypes/ directory into the middleware registry.
16
6
  */
17
7
 
18
8
  import { resolve } from 'node:path'
19
- import { clearRegistry, loadDoctypes, registerBuiltinHandlers, registerHandler } from '@stonecrop/graphql-middleware'
20
- import { projects, tasks, type Project, type Task } from '../data'
21
-
22
- /** Action argument shape sent by the frontend (see runDoctypeAction in useDoctypes.ts) */
23
- type ActionArgs = [{ id: string; data?: Record<string, unknown> }]
9
+ import { clearRegistry, loadDoctypes } from '@stonecrop/graphql-middleware'
24
10
 
25
11
  export default defineNitroPlugin(async () => {
26
12
  clearRegistry()
27
13
 
28
14
  const doctypesDir = resolve(process.cwd(), 'doctypes')
29
15
  loadDoctypes(doctypesDir, { continueOnError: true })
30
-
31
- registerBuiltinHandlers()
32
-
33
- // Persist form edits to a Project
34
- registerHandler('project:save', async (args: unknown[]) => {
35
- const [{ id, data }] = args as ActionArgs
36
- const project = projects.get(id)
37
- if (!project) throw new Error(`Project not found: ${id}`)
38
- const updated = { ...project, ...data, id } as Project
39
- projects.set(id, updated)
40
- return updated
41
- })
42
-
43
- // Persist form edits to a Task
44
- registerHandler('task:save', async (args: unknown[]) => {
45
- const [{ id, data }] = args as ActionArgs
46
- const task = tasks.get(id)
47
- if (!task) throw new Error(`Task not found: ${id}`)
48
- const updated = { ...task, ...data, id } as Task
49
- tasks.set(id, updated)
50
- return updated
51
- })
52
-
53
- // Transition a Task from Todo → In Progress
54
- registerHandler('start_task', async (args: unknown[]) => {
55
- const [{ id }] = args as ActionArgs
56
- const task = tasks.get(id)
57
- if (!task) throw new Error(`Task not found: ${id}`)
58
- const updated = { ...task, status: 'In Progress' as const }
59
- tasks.set(id, updated)
60
- return updated
61
- })
62
-
63
- // Transition a Task from In Progress → Done
64
- registerHandler('complete_task', async (args: unknown[]) => {
65
- const [{ id }] = args as ActionArgs
66
- const task = tasks.get(id)
67
- if (!task) throw new Error(`Task not found: ${id}`)
68
- const updated = { ...task, status: 'Done' as const }
69
- tasks.set(id, updated)
70
- return updated
71
- })
72
-
73
- // Transition a Project from Active → Archived
74
- registerHandler('archive_project', async (args: unknown[]) => {
75
- const [{ id }] = args as ActionArgs
76
- const project = projects.get(id)
77
- if (!project) throw new Error(`Project not found: ${id}`)
78
- const updated = { ...project, status: 'Archived' as const }
79
- projects.set(id, updated)
80
- return updated
81
- })
82
16
  })