@stonecrop/nuxt 0.13.8 → 0.13.10
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/module.d.mts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/package.json +9 -9
- package/src/cli/index.ts +4 -3
- package/src/cli/installers/doctypes.ts +17 -162
- package/src/cli/installers/frontend.ts +79 -1
- package/src/cli/installers/grafserv.ts +28 -180
- package/src/cli/utils/config.ts +39 -0
- package/src/cli/utils/templates.ts +34 -0
- package/templates/Project.json +56 -0
- package/templates/Task.json +74 -0
- package/templates/data.ts +90 -0
- package/templates/index.vue +98 -0
- package/templates/resolvers.ts +222 -205
- package/templates/schema.graphql +31 -4
- package/templates/stonecrop.client.ts +42 -0
- package/templates/stonecrop.ts +82 -0
- package/templates/useDoctypes.ts +53 -0
- package/templates/useRouteAdapter.ts +36 -0
- package/templates/Example.json +0 -84
- package/templates/example-table.json +0 -58
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stonecrop Server Plugin
|
|
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
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
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> }]
|
|
24
|
+
|
|
25
|
+
export default defineNitroPlugin(async () => {
|
|
26
|
+
clearRegistry()
|
|
27
|
+
|
|
28
|
+
const doctypesDir = resolve(process.cwd(), 'doctypes')
|
|
29
|
+
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
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { DoctypeRef } from '@stonecrop/schema'
|
|
2
|
+
import type { DoctypeConfig } from '@stonecrop/stonecrop'
|
|
3
|
+
import { useNuxtApp } from 'nuxt/app'
|
|
4
|
+
|
|
5
|
+
const modules = import.meta.glob<DoctypeConfig>('../../doctypes/*.json', {
|
|
6
|
+
eager: true,
|
|
7
|
+
import: 'default',
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export const doctypeMap = new Map<string, DoctypeConfig>()
|
|
11
|
+
|
|
12
|
+
for (const [path, doctype] of Object.entries(modules)) {
|
|
13
|
+
const filename = path.split('/').pop()!.replace('.json', '')
|
|
14
|
+
const slug = filename
|
|
15
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
16
|
+
.replace(/[\s_]+/g, '-')
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
doctypeMap.set(slug, doctype)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useDoctypeConfig(slug: string): DoctypeConfig | undefined {
|
|
22
|
+
return doctypeMap.get(slug)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function fetchDoctypeRecords(doctype: DoctypeRef, limit = 200): Promise<{ data: any[]; count: number }> {
|
|
26
|
+
const { $stonecropClient } = useNuxtApp()
|
|
27
|
+
const data = (await $stonecropClient.getRecords({ name: doctype.name }, { limit })) as any[]
|
|
28
|
+
return { data, count: data.length }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function fetchDoctypeRecord(
|
|
32
|
+
doctype: DoctypeRef,
|
|
33
|
+
recordId: string
|
|
34
|
+
): Promise<Record<string, unknown> | null> {
|
|
35
|
+
const { $stonecropClient } = useNuxtApp()
|
|
36
|
+
const result = await $stonecropClient.getRecord(doctype, recordId)
|
|
37
|
+
return result.record
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ActionResult {
|
|
41
|
+
success: boolean
|
|
42
|
+
data?: unknown
|
|
43
|
+
error?: string | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runDoctypeAction(
|
|
47
|
+
doctype: DoctypeConfig,
|
|
48
|
+
action: string,
|
|
49
|
+
args: { id: string; data?: Record<string, unknown> }
|
|
50
|
+
): Promise<ActionResult> {
|
|
51
|
+
const { $stonecropClient } = useNuxtApp()
|
|
52
|
+
return $stonecropClient.runAction({ name: doctype.name }, action, [args])
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { NavigationTarget, RouteAdapter } from '@stonecrop/desktop'
|
|
2
|
+
import { navigateTo, useRoute } from 'nuxt/app'
|
|
3
|
+
|
|
4
|
+
export function useRouteAdapter(): RouteAdapter {
|
|
5
|
+
const route = useRoute()
|
|
6
|
+
|
|
7
|
+
const getCurrentDoctype = (): string => {
|
|
8
|
+
const pathMatch = route.params.pathMatch as string[] | undefined
|
|
9
|
+
if (pathMatch && pathMatch.length > 0) return pathMatch[0] ?? ''
|
|
10
|
+
return ''
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const getCurrentRecordId = (): string => {
|
|
14
|
+
const pathMatch = route.params.pathMatch as string[] | undefined
|
|
15
|
+
if (pathMatch && pathMatch.length > 1) return pathMatch[1] ?? ''
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const getCurrentView = (): 'doctypes' | 'records' | 'record' => {
|
|
20
|
+
if (!getCurrentDoctype()) return 'doctypes'
|
|
21
|
+
if (getCurrentRecordId()) return 'record'
|
|
22
|
+
return 'records'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const navigate = async (target: NavigationTarget): Promise<void> => {
|
|
26
|
+
if (target.view === 'doctypes') {
|
|
27
|
+
await navigateTo('/')
|
|
28
|
+
} else if (target.view === 'records' && target.doctype) {
|
|
29
|
+
await navigateTo(`/${target.doctype}`)
|
|
30
|
+
} else if (target.view === 'record' && target.doctype && target.recordId) {
|
|
31
|
+
await navigateTo(`/${target.doctype}/${target.recordId}`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { getCurrentDoctype, getCurrentRecordId, getCurrentView, navigate }
|
|
36
|
+
}
|
package/templates/Example.json
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "Example",
|
|
3
|
-
"slug": "example/:id",
|
|
4
|
-
"fields": [
|
|
5
|
-
{
|
|
6
|
-
"fieldname": "id",
|
|
7
|
-
"fieldtype": "Data",
|
|
8
|
-
"label": "ID",
|
|
9
|
-
"readOnly": true
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"fieldname": "title",
|
|
13
|
-
"fieldtype": "Data",
|
|
14
|
-
"label": "Title",
|
|
15
|
-
"required": true,
|
|
16
|
-
"width": "30ch"
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"fieldname": "description",
|
|
20
|
-
"fieldtype": "Text",
|
|
21
|
-
"label": "Description"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"fieldname": "status",
|
|
25
|
-
"fieldtype": "Select",
|
|
26
|
-
"label": "Status",
|
|
27
|
-
"options": ["Draft", "Active", "Archived"],
|
|
28
|
-
"default": "Draft"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"fieldname": "priority",
|
|
32
|
-
"fieldtype": "Select",
|
|
33
|
-
"label": "Priority",
|
|
34
|
-
"options": ["Low", "Medium", "High"],
|
|
35
|
-
"default": "Medium"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"fieldname": "assignee",
|
|
39
|
-
"fieldtype": "Link",
|
|
40
|
-
"label": "Assignee",
|
|
41
|
-
"options": "User"
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"fieldname": "dueDate",
|
|
45
|
-
"fieldtype": "Date",
|
|
46
|
-
"label": "Due Date"
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"fieldname": "createdAt",
|
|
50
|
-
"fieldtype": "Datetime",
|
|
51
|
-
"label": "Created At",
|
|
52
|
-
"readOnly": true
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"fieldname": "updatedAt",
|
|
56
|
-
"fieldtype": "Datetime",
|
|
57
|
-
"label": "Updated At",
|
|
58
|
-
"readOnly": true
|
|
59
|
-
}
|
|
60
|
-
],
|
|
61
|
-
"workflow": {
|
|
62
|
-
"states": ["Draft", "Active", "Archived"],
|
|
63
|
-
"actions": {
|
|
64
|
-
"activate": {
|
|
65
|
-
"label": "Activate",
|
|
66
|
-
"handler": "activate_example",
|
|
67
|
-
"allowedStates": ["Draft"],
|
|
68
|
-
"confirm": true
|
|
69
|
-
},
|
|
70
|
-
"archive": {
|
|
71
|
-
"label": "Archive",
|
|
72
|
-
"handler": "archive_example",
|
|
73
|
-
"allowedStates": ["Active"],
|
|
74
|
-
"confirm": true
|
|
75
|
-
},
|
|
76
|
-
"reopen": {
|
|
77
|
-
"label": "Reopen",
|
|
78
|
-
"handler": "reopen_example",
|
|
79
|
-
"allowedStates": ["Archived"],
|
|
80
|
-
"confirm": false
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "Example",
|
|
3
|
-
"slug": "example",
|
|
4
|
-
"schema": [
|
|
5
|
-
{
|
|
6
|
-
"component": "ATable",
|
|
7
|
-
"columns": [
|
|
8
|
-
{
|
|
9
|
-
"name": "id",
|
|
10
|
-
"label": "ID",
|
|
11
|
-
"fieldtype": "Data",
|
|
12
|
-
"width": "8ch"
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
"name": "title",
|
|
16
|
-
"label": "Title",
|
|
17
|
-
"fieldtype": "Data",
|
|
18
|
-
"width": "25ch"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"name": "status",
|
|
22
|
-
"label": "Status",
|
|
23
|
-
"fieldtype": "Data",
|
|
24
|
-
"width": "10ch"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"name": "priority",
|
|
28
|
-
"label": "Priority",
|
|
29
|
-
"fieldtype": "Data",
|
|
30
|
-
"width": "10ch"
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
"name": "assignee",
|
|
34
|
-
"label": "Assignee",
|
|
35
|
-
"fieldtype": "Data",
|
|
36
|
-
"width": "15ch"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"name": "dueDate",
|
|
40
|
-
"label": "Due Date",
|
|
41
|
-
"fieldtype": "Date",
|
|
42
|
-
"width": "12ch"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"name": "createdAt",
|
|
46
|
-
"label": "Created",
|
|
47
|
-
"fieldtype": "Datetime",
|
|
48
|
-
"width": "18ch"
|
|
49
|
-
}
|
|
50
|
-
],
|
|
51
|
-
"config": {
|
|
52
|
-
"view": "list",
|
|
53
|
-
"sortable": true,
|
|
54
|
-
"filterable": true
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|