@stonecrop/nuxt 0.35.0 → 0.38.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 -5
- package/dist/module.d.mts +2 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/app/components/DocBuilderFieldsPanel.vue +1 -1
- package/dist/runtime/app/pages/DocBuilderDetail.vue +45 -43
- package/dist/runtime/app/pages/DocBuilderIndex.vue +25 -23
- package/package.json +17 -18
- package/src/cli/installers/frontend.ts +2 -1
- package/src/cli/installers/grafserv.ts +2 -0
- package/templates/resolvers.ts +17 -7
- package/templates/schema.graphql +2 -0
package/README.md
CHANGED
|
@@ -427,7 +427,7 @@ await dispatchAction({ name: 'plan' }, 'SUBMIT', [recordId])
|
|
|
427
427
|
| `setMeta(fn)` | `(fn: (ctx) => Doctype \| Promise<Doctype>) => void` | Sets the `getMeta` function on the Registry. Called by `useStonecrop({ doctype: 'slug' })` to lazy-load that doctype's metadata. `ctx` = `{ path, segments }`. |
|
|
428
428
|
| `setClient(client)` | `(client: DataClient) => void` | Set the data client for record fetching. Throws if stonecrop not available. |
|
|
429
429
|
| `getClient()` | `() => DataClient \| undefined` | Get the currently configured client. |
|
|
430
|
-
| `dispatchAction(doctype, action, args)` | `Promise<{ success, data, error }>` | Dispatch an action via the configured client. Returns error if doctype not found in registry. |
|
|
430
|
+
| `dispatchAction(doctype, action, args)` | `Promise<{ success, data, error }>` | Dispatch an action via the configured client, storing the result's `record` (the server's read of the record after the action). Returns error if doctype not found in registry. |
|
|
431
431
|
|
|
432
432
|
## Advanced Features
|
|
433
433
|
|
|
@@ -481,8 +481,13 @@ const taskMachine = createMachine({
|
|
|
481
481
|
|
|
482
482
|
## Examples
|
|
483
483
|
|
|
484
|
-
|
|
485
|
-
|
|
484
|
+
All runnable examples live in **[documentation](./documentation)** at **`/playground`**:
|
|
485
|
+
|
|
486
|
+
- **Workflow** — grafserv + guarded transitions over session-scoped in-memory storage (Orders/Users), Desktop rail panels
|
|
487
|
+
- **Countries** — doctypes introspected from the public [countries GraphQL API](https://countries.trevorblades.com/graphql), same Desktop shell
|
|
488
|
+
- **DocBuilder** — public doctype prototyping at `/docbuilder`
|
|
489
|
+
|
|
490
|
+
`pnpm run dev` (alias for `dev:documentation`) runs the docs site on port 3002.
|
|
486
491
|
|
|
487
492
|
|
|
488
493
|
## Contribution
|
|
@@ -497,10 +502,10 @@ pnpm install
|
|
|
497
502
|
# Generate type stubs
|
|
498
503
|
pnpm run dev:prepare
|
|
499
504
|
|
|
500
|
-
# Develop
|
|
505
|
+
# Develop the documentation site (docs + playgrounds)
|
|
501
506
|
pnpm run dev
|
|
502
507
|
|
|
503
|
-
# Build the
|
|
508
|
+
# Build the documentation site
|
|
504
509
|
pnpm run dev:build
|
|
505
510
|
|
|
506
511
|
# Run ESLint
|
|
@@ -570,8 +575,10 @@ The CLI will detect that you're in a Nuxt project and prompt for features:
|
|
|
570
575
|
|
|
571
576
|
? Select features to install
|
|
572
577
|
◉ @stonecrop/nuxt - Frontend module
|
|
578
|
+
◯ @stonecrop/graphql-client - GraphQL client
|
|
573
579
|
◯ @stonecrop/nuxt-grafserv - GraphQL server
|
|
574
580
|
◯ @stonecrop/casl-middleware - Authorization
|
|
581
|
+
◯ @stonecrop/rockfoil - PostGraphile middleware
|
|
575
582
|
◉ Sample doctypes
|
|
576
583
|
```
|
|
577
584
|
|
package/dist/module.d.mts
CHANGED
|
@@ -48,6 +48,8 @@ type RouteStrategyFn = (doctypes: ParsedDoctype[]) => NuxtPage[];
|
|
|
48
48
|
interface ModuleOptions {
|
|
49
49
|
/** Enable the DocBuilder feature with /docbuilder routes */
|
|
50
50
|
docbuilder?: boolean;
|
|
51
|
+
/** Allow DocBuilder routes and handlers outside development (e.g. public documentation playground) */
|
|
52
|
+
docbuilderPublic?: boolean;
|
|
51
53
|
/** Path to doctypes folder relative to the project root (defaults to 'doctypes') */
|
|
52
54
|
doctypesDir?: string;
|
|
53
55
|
/**
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -196,7 +196,7 @@ const module$1 = defineNuxtModule({
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
});
|
|
199
|
-
if (options.docbuilder && process.env.NODE_ENV === "development") {
|
|
199
|
+
if (options.docbuilder && (options.docbuilderPublic || process.env.NODE_ENV === "development")) {
|
|
200
200
|
logger.log("DocBuilder enabled, adding routes and handlers");
|
|
201
201
|
nuxt.options.css.push("@vue-flow/core/dist/style.css");
|
|
202
202
|
nuxt.options.css.push("@vue-flow/core/dist/theme-default.css");
|
|
@@ -336,5 +336,5 @@ function bool(v) {
|
|
|
336
336
|
</script>
|
|
337
337
|
|
|
338
338
|
<style scoped>
|
|
339
|
-
.fields-panel{padding:.5em 1em}.fields-panel :deep(.atable-row>td){border-top:1px solid var(--sc-row-border-color);padding:var(--sc-atable-row-padding) .75em;vertical-align:middle}.fields-panel :deep(input[type=text]),.fields-panel :deep(select){border:1px solid var(--sc-gray-20);border-radius:var(--sc-border-radius);font-family:inherit;font-size:inherit;padding:.25em .5em;width:100%}.fields-panel :deep(input.locked),.fields-panel :deep(select.locked){background:var(--sc-gray-10);color:var(--sc-header-text-color);cursor:not-allowed}.fields-panel :deep(input.json-invalid){background:var(--sc-badge-danger-bg);border-color:var(--sc-brand-danger)}.center{text-align:center}.badge{border-radius:9999px;display:inline-block;font-size:.75rem;font-weight:500;padding:.125em .5em}.badge-manual{background:var(--sc-badge-brand-bg);color:var(--sc-badge-brand-text)}.badge-introspected{background:var(--sc-badge-neutral-bg);color:var(--sc-badge-neutral-text)}.field-detail{display:grid;gap:.75rem 1rem;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));padding:.75rem 1rem}.field-prop{color:var(--sc-header-text-color);display:flex;flex-direction:column;font-size:.75rem;gap:.25rem}.field-prop-inline{align-items:center;flex-direction:row;gap:.5rem}.field-prop-wide{grid-column:1/-1}.field-detail-actions{align-items:center;display:flex;gap:1rem;justify-content:space-between;padding:0 1rem .75rem}.locked-note{color:var(--sc-header-text-color);font-size:.75rem;font-style:italic}.btn-add{background:none;border:1px dashed var(--sc-gray-20);border-radius:var(--sc-border-radius);color:var(--sc-primary-color);cursor:pointer;font-size:.875rem;margin-top:.5rem;padding:.4em 1em}.fields-empty{color:var(--sc-header-text-color);font-style:italic;padding:1rem 0}
|
|
339
|
+
.fields-panel{box-sizing:border-box;max-width:100%;overflow-x:auto;padding:.5em 1em}.fields-panel :deep(.atable-row>td){border-top:1px solid var(--sc-row-border-color);padding:var(--sc-atable-row-padding) .75em;vertical-align:middle}.fields-panel :deep(input[type=text]),.fields-panel :deep(select){border:1px solid var(--sc-gray-20);border-radius:var(--sc-border-radius);font-family:inherit;font-size:inherit;padding:.25em .5em;width:100%}.fields-panel :deep(input.locked),.fields-panel :deep(select.locked){background:var(--sc-gray-10);color:var(--sc-header-text-color);cursor:not-allowed}.fields-panel :deep(input.json-invalid){background:var(--sc-badge-danger-bg);border-color:var(--sc-brand-danger)}.center{text-align:center}.badge{border-radius:9999px;display:inline-block;font-size:.75rem;font-weight:500;padding:.125em .5em}.badge-manual{background:var(--sc-badge-brand-bg);color:var(--sc-badge-brand-text)}.badge-introspected{background:var(--sc-badge-neutral-bg);color:var(--sc-badge-neutral-text)}.field-detail{display:grid;gap:.75rem 1rem;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));padding:.75rem 1rem}.field-prop{color:var(--sc-header-text-color);display:flex;flex-direction:column;font-size:.75rem;gap:.25rem}.field-prop-inline{align-items:center;flex-direction:row;gap:.5rem}.field-prop-wide{grid-column:1/-1}.field-detail-actions{align-items:center;display:flex;gap:1rem;justify-content:space-between;padding:0 1rem .75rem}.locked-note{color:var(--sc-header-text-color);font-size:.75rem;font-style:italic}.btn-add{background:none;border:1px dashed var(--sc-gray-20);border-radius:var(--sc-border-radius);color:var(--sc-primary-color);cursor:pointer;font-size:.875rem;margin-top:.5rem;padding:.4em 1em}.fields-empty{color:var(--sc-header-text-color);font-style:italic;padding:1rem 0}
|
|
340
340
|
</style>
|
|
@@ -1,55 +1,57 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<ClientOnly>
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
<div v-
|
|
7
|
-
<div v-if="
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<
|
|
17
|
-
|
|
3
|
+
<div class="docbuilder-shell">
|
|
4
|
+
<Desktop class="docbuilder-desktop" :route-adapter="routeAdapter" :host-actions="docbuilderActions">
|
|
5
|
+
<div v-if="loading" class="docbuilder-loading">Loading...</div>
|
|
6
|
+
<div v-else class="docbuilder-page">
|
|
7
|
+
<div v-if="validationIssues.length > 0 && !warningsDismissed" class="validation-panel">
|
|
8
|
+
<div v-if="errorCount > 0" class="validation-errors">
|
|
9
|
+
<strong>⚠️ {{ errorCount }} Error(s) — Cannot Save</strong>
|
|
10
|
+
<ul>
|
|
11
|
+
<li v-for="(issue, idx) in validationIssues.filter(i => i.severity === 'error')" :key="`err-${idx}`">
|
|
12
|
+
<code v-if="issue.fieldname">{{ issue.fieldname }}:</code> {{ issue.message }}
|
|
13
|
+
</li>
|
|
14
|
+
</ul>
|
|
15
|
+
</div>
|
|
16
|
+
<div v-if="warningCount > 0" class="validation-warnings">
|
|
17
|
+
<strong>⚡ {{ warningCount }} Warning(s)</strong>
|
|
18
|
+
<button class="dismiss-button" @click="warningsDismissed = true">Dismiss</button>
|
|
19
|
+
</div>
|
|
18
20
|
</div>
|
|
19
|
-
</div>
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
<AFieldset label="Workflow" :schema="[]" :collapsible="true">
|
|
23
|
+
<div class="builder-workflow">
|
|
24
|
+
<StateEditor
|
|
25
|
+
v-if="workflowConfig && workflowConfig.states && workflowConfig.states.length > 0"
|
|
26
|
+
v-model="workflowConfig"
|
|
27
|
+
v-model:layout="layout"
|
|
28
|
+
node-container-class="node-editor" />
|
|
29
|
+
<div v-else class="empty-workflow">
|
|
30
|
+
<p class="empty-workflow-hint">No workflow yet. Name the first state to start building the workflow.</p>
|
|
31
|
+
<div class="empty-workflow-form">
|
|
32
|
+
<input v-model="newStateName" type="text" placeholder="e.g. Draft" @keyup.enter="seedWorkflow" />
|
|
33
|
+
<button class="btn-primary" type="button" :disabled="!newStateName.trim()" @click="seedWorkflow">
|
|
34
|
+
Add first state
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
35
37
|
</div>
|
|
36
38
|
</div>
|
|
37
|
-
</
|
|
38
|
-
</AFieldset>
|
|
39
|
+
</AFieldset>
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
<AFieldset label="Actions" :schema="[]" :collapsible="true">
|
|
42
|
+
<DocBuilderActionsPanel v-model="workflowConfig" />
|
|
43
|
+
</AFieldset>
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
<AFieldset label="Schema" :schema="[]" :collapsible="true">
|
|
46
|
+
<DocBuilderFieldsPanel v-model="fields" />
|
|
47
|
+
</AFieldset>
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
<div v-if="saveMessage" class="builder-actions">
|
|
50
|
+
<span class="save-message" :class="saveMessage.type">{{ saveMessage.text }}</span>
|
|
51
|
+
</div>
|
|
50
52
|
</div>
|
|
51
|
-
</
|
|
52
|
-
</
|
|
53
|
+
</Desktop>
|
|
54
|
+
</div>
|
|
53
55
|
<template #fallback>
|
|
54
56
|
<div class="docbuilder-loading">Loading...</div>
|
|
55
57
|
</template>
|
|
@@ -150,5 +152,5 @@ const docbuilderActions = computed(() => [
|
|
|
150
152
|
</script>
|
|
151
153
|
|
|
152
154
|
<style scoped>
|
|
153
|
-
.docbuilder-desktop{height:
|
|
155
|
+
.docbuilder-shell{display:flex;flex-direction:column;min-height:100dvh}.docbuilder-desktop{flex:1;min-height:0}.docbuilder-loading{color:var(--sc-header-text-color);padding:2rem;text-align:center}.docbuilder-page{box-sizing:border-box;padding:2rem}.builder-workflow{min-height:8rem;padding:.5em 1em}:deep(.node-editor){height:40vh;overflow:hidden;width:100%}.empty-workflow{padding:1rem 0}.empty-workflow-hint{color:var(--sc-header-text-color);font-style:italic;margin:0 0 .75rem}.empty-workflow-form{align-items:center;display:flex;gap:.5rem}.empty-workflow-form input{border:1px solid var(--sc-input-border-color);border-radius:var(--sc-border-radius);font-family:inherit;font-size:.875rem;padding:.4em .6em}.btn-primary{background:var(--sc-primary-color);border:none;border-radius:var(--sc-border-radius);color:var(--sc-primary-text-color);cursor:pointer;font-size:.875rem;font-weight:500;padding:.45em 1em}.btn-primary:disabled{cursor:not-allowed;opacity:.5}.validation-panel{margin-bottom:1rem}.validation-errors{background:var(--sc-badge-danger-bg);border:1px solid var(--sc-badge-danger-accent);border-radius:var(--sc-border-radius);color:var(--sc-badge-danger-text);margin-bottom:.5rem;padding:1rem}.validation-warnings{background:var(--sc-badge-warning-bg);border:1px solid var(--sc-badge-warning-accent);border-radius:var(--sc-border-radius);color:var(--sc-badge-warning-text);padding:1rem}.dismiss-button{background:none;border:1px solid;border-radius:var(--sc-border-radius);cursor:pointer;font-size:.75rem;margin-left:1rem;padding:.125em .5em}.builder-actions{align-items:center;display:flex;gap:1rem;padding:1rem}.save-message{font-size:.875rem}.save-message.success{color:var(--sc-brand-success)}.save-message.error{color:var(--sc-brand-danger)}
|
|
154
156
|
</style>
|
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<ClientOnly>
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<div class="docbuilder-
|
|
6
|
-
<
|
|
7
|
-
|
|
3
|
+
<div class="docbuilder-shell">
|
|
4
|
+
<Desktop class="docbuilder-desktop" :route-adapter="routeAdapter" :host-actions="indexActions">
|
|
5
|
+
<div class="docbuilder-index-inner">
|
|
6
|
+
<div class="docbuilder-header">
|
|
7
|
+
<h1>DocType Builder</h1>
|
|
8
|
+
<p class="subtitle">Select a DocType to view and edit its schema</p>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="docbuilder-create">
|
|
11
|
+
<input
|
|
12
|
+
v-model="newName"
|
|
13
|
+
type="text"
|
|
14
|
+
placeholder="New doctype name (e.g. Invoice)"
|
|
15
|
+
:disabled="creating"
|
|
16
|
+
@keyup.enter="createDoctype" />
|
|
17
|
+
<button type="button" class="btn-primary" :disabled="creating || !newName.trim()" @click="createDoctype">
|
|
18
|
+
{{ creating ? 'Creating\u2026' : '+ New DocType' }}
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
<p v-if="createError" class="create-error">{{ createError }}</p>
|
|
22
|
+
<div v-if="loading" class="loading">Loading doctypes...</div>
|
|
23
|
+
<p v-else-if="!doctypes.length" class="empty">No doctypes yet — create one above.</p>
|
|
24
|
+
<ATable v-else :columns="columns" :rows="doctypes" :config="config" @row:click="handleRowClick" />
|
|
8
25
|
</div>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
v-model="newName"
|
|
12
|
-
type="text"
|
|
13
|
-
placeholder="New doctype name (e.g. Invoice)"
|
|
14
|
-
:disabled="creating"
|
|
15
|
-
@keyup.enter="createDoctype" />
|
|
16
|
-
<button type="button" class="btn-primary" :disabled="creating || !newName.trim()" @click="createDoctype">
|
|
17
|
-
{{ creating ? 'Creating\u2026' : '+ New DocType' }}
|
|
18
|
-
</button>
|
|
19
|
-
</div>
|
|
20
|
-
<p v-if="createError" class="create-error">{{ createError }}</p>
|
|
21
|
-
<div v-if="loading" class="loading">Loading doctypes...</div>
|
|
22
|
-
<p v-else-if="!doctypes.length" class="empty">No doctypes yet — create one above.</p>
|
|
23
|
-
<ATable v-else :columns="columns" :rows="doctypes" :config="config" @row:click="handleRowClick" />
|
|
24
|
-
</div>
|
|
25
|
-
</Desktop>
|
|
26
|
+
</Desktop>
|
|
27
|
+
</div>
|
|
26
28
|
</ClientOnly>
|
|
27
29
|
</template>
|
|
28
30
|
|
|
@@ -95,5 +97,5 @@ const indexActions = computed(() => [{ type: "button", label: "Home", action: ()
|
|
|
95
97
|
</script>
|
|
96
98
|
|
|
97
99
|
<style scoped>
|
|
98
|
-
.docbuilder-desktop{height:
|
|
100
|
+
.docbuilder-shell{display:flex;flex-direction:column;min-height:100dvh}.docbuilder-desktop{flex:1;min-height:0}.docbuilder-index-inner{margin:0 auto;max-width:1200px;padding:2rem}.docbuilder-header{padding:2rem 0 3rem;text-align:center}.docbuilder-header h1{font-size:2.5rem;font-weight:700;margin:0 0 1rem}.subtitle{font-size:1.125rem;margin:0}.empty,.loading,.subtitle{color:var(--sc-header-text-color)}.empty,.loading{padding:2rem;text-align:center}.docbuilder-create{display:flex;gap:.5rem;margin-bottom:1rem}.docbuilder-create input{border:1px solid var(--sc-input-border-color);border-radius:var(--sc-border-radius);flex:1;font:inherit;padding:.5em .75em}.btn-primary{background:var(--sc-primary-color);border:none;border-radius:var(--sc-border-radius);color:var(--sc-primary-text-color);cursor:pointer;font-weight:500;padding:.5em 1.25em;white-space:nowrap}.btn-primary:disabled{cursor:not-allowed;opacity:.5}.create-error{color:var(--sc-brand-danger);font-size:.875rem;margin:-.5rem 0 1rem}
|
|
99
101
|
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonecrop/nuxt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Nuxt module for Stonecrop",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nuxt",
|
|
@@ -60,18 +60,18 @@
|
|
|
60
60
|
"pathe": "^2.0.3",
|
|
61
61
|
"pinia": "^3.0.4",
|
|
62
62
|
"prompts": "^2.4.2",
|
|
63
|
-
"@stonecrop/aform": "0.
|
|
64
|
-
"@stonecrop/atable": "0.
|
|
65
|
-
"@stonecrop/casl-middleware": "0.
|
|
66
|
-
"@stonecrop/code-editor": "0.
|
|
67
|
-
"@stonecrop/
|
|
68
|
-
"@stonecrop/
|
|
69
|
-
"@stonecrop/
|
|
70
|
-
"@stonecrop/node-editor": "0.
|
|
71
|
-
"@stonecrop/
|
|
72
|
-
"@stonecrop/
|
|
73
|
-
"@stonecrop/
|
|
74
|
-
"@stonecrop/
|
|
63
|
+
"@stonecrop/aform": "0.38.0",
|
|
64
|
+
"@stonecrop/atable": "0.38.0",
|
|
65
|
+
"@stonecrop/casl-middleware": "0.38.0",
|
|
66
|
+
"@stonecrop/code-editor": "0.38.0",
|
|
67
|
+
"@stonecrop/graphql-client": "0.38.0",
|
|
68
|
+
"@stonecrop/nuxt-grafserv": "0.38.0",
|
|
69
|
+
"@stonecrop/graphql-middleware": "0.38.0",
|
|
70
|
+
"@stonecrop/node-editor": "0.38.0",
|
|
71
|
+
"@stonecrop/stonecrop": "0.38.0",
|
|
72
|
+
"@stonecrop/desktop": "0.38.0",
|
|
73
|
+
"@stonecrop/themes": "0.38.0",
|
|
74
|
+
"@stonecrop/schema": "0.38.0"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@eslint/js": "^10.0.1",
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
"nuxt": "^4.4.8",
|
|
96
96
|
"nuxt-graphql-middleware": "^5.4.0",
|
|
97
97
|
"postgraphile": "^5.0.3",
|
|
98
|
+
"temporal-polyfill": "^1.0.5",
|
|
98
99
|
"typescript": "^6.0.3",
|
|
99
100
|
"typescript-eslint": "^8.62.1",
|
|
100
101
|
"vite": "^8.2.2",
|
|
@@ -107,13 +108,11 @@
|
|
|
107
108
|
"node": ">=24.0.0"
|
|
108
109
|
},
|
|
109
110
|
"scripts": {
|
|
110
|
-
"dev": "node --run dev:
|
|
111
|
-
"dev:build": "nuxi build
|
|
111
|
+
"dev": "node --run dev:documentation",
|
|
112
|
+
"dev:build": "nuxi build documentation",
|
|
112
113
|
"dev:documentation": "node --run dev:prepare:documentation && nuxi dev documentation --host",
|
|
113
|
-
"dev:full": "node --run dev:prepare:full && nuxi dev fullstack --host",
|
|
114
114
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && sh scripts/nuxt-apps.sh prepare",
|
|
115
|
-
"dev:prepare:documentation": "nuxi prepare documentation",
|
|
116
|
-
"dev:prepare:full": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare fullstack",
|
|
115
|
+
"dev:prepare:documentation": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare documentation",
|
|
117
116
|
"docs": "echo 'Nuxt modules use custom build system - skipping API docs generation'",
|
|
118
117
|
"generate:documentation": "nuxi generate documentation",
|
|
119
118
|
"lint": "eslint .",
|
|
@@ -26,11 +26,12 @@ export async function installFrontend(options: FrontendInstallerOptions): Promis
|
|
|
26
26
|
|
|
27
27
|
try {
|
|
28
28
|
// Add @stonecrop/nuxt and its required peer dependencies
|
|
29
|
-
// These packages are imported by the Nuxt module and need to be available at runtime
|
|
29
|
+
// These packages are imported by the Nuxt module or the scaffolded app files and need to be available at runtime
|
|
30
30
|
await addDependencies(cwd, {
|
|
31
31
|
'@stonecrop/nuxt': 'latest',
|
|
32
32
|
'@stonecrop/aform': 'latest',
|
|
33
33
|
'@stonecrop/atable': 'latest',
|
|
34
|
+
'@stonecrop/graphql-client': 'latest',
|
|
34
35
|
'@stonecrop/stonecrop': 'latest',
|
|
35
36
|
'@stonecrop/node-editor': 'latest',
|
|
36
37
|
'@stonecrop/schema': 'latest',
|
|
@@ -39,8 +39,10 @@ export async function installGrafserv(options: GrafservInstallerOptions): Promis
|
|
|
39
39
|
await addDependencies(cwd, {
|
|
40
40
|
'@stonecrop/nuxt-grafserv': 'latest',
|
|
41
41
|
'@stonecrop/graphql-middleware': 'latest',
|
|
42
|
+
'@stonecrop/schema': 'latest',
|
|
42
43
|
graphql: '^16.11.0',
|
|
43
44
|
grafast: '^1.0.1',
|
|
45
|
+
'temporal-polyfill': '^1.0.5',
|
|
44
46
|
})
|
|
45
47
|
|
|
46
48
|
// Update nuxt.config.ts
|
package/templates/resolvers.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { constant, lambda, loadOne, object } from 'grafast'
|
|
|
22
22
|
import { getMeta, getAllMeta, applyGuardedTransition } from '@stonecrop/graphql-middleware'
|
|
23
23
|
import { getRecordIdField } from '@stonecrop/schema'
|
|
24
24
|
import type { DoctypeMeta } from '@stonecrop/schema'
|
|
25
|
+
import { Temporal } from 'temporal-polyfill'
|
|
25
26
|
import { projects, tasks, type Project, type Task } from './data'
|
|
26
27
|
|
|
27
28
|
// ============================================================
|
|
@@ -142,8 +143,8 @@ function nextId(doctype: string): string {
|
|
|
142
143
|
// `selfTransition` has nothing for the dispatcher to apply, and without an entry here it fails
|
|
143
144
|
// loudly rather than reporting a false success.
|
|
144
145
|
//
|
|
145
|
-
// Add your own by registering under the doctype's `name`. Throwing rejects the action;
|
|
146
|
-
// the
|
|
146
|
+
// Add your own by registering under the doctype's `name`. Throwing rejects the action; what it
|
|
147
|
+
// returns becomes the result's `data`, while the client stores the record as a read returns it.
|
|
147
148
|
|
|
148
149
|
type ActionHandler = (context: {
|
|
149
150
|
recordId?: string
|
|
@@ -165,9 +166,9 @@ export const actionHandlers: Record<string, Record<string, ActionHandler>> = {
|
|
|
165
166
|
const task = recordId != null ? tasks.get(recordId) : undefined
|
|
166
167
|
if (!task) throw new Error(`Task ${recordId ?? '(none)'} not found`)
|
|
167
168
|
|
|
168
|
-
|
|
169
|
-
from.
|
|
170
|
-
const updated: Task = { ...task, dueDate: from.
|
|
169
|
+
// A task with no due date is snoozed from the server's today.
|
|
170
|
+
const from = task.dueDate ? Temporal.PlainDate.from(task.dueDate) : Temporal.Now.plainDateISO()
|
|
171
|
+
const updated: Task = { ...task, dueDate: from.add({ weeks: 1 }).toString() }
|
|
171
172
|
tasks.set(task.id, updated)
|
|
172
173
|
return Promise.resolve(updated)
|
|
173
174
|
},
|
|
@@ -278,7 +279,7 @@ export const resolvers = {
|
|
|
278
279
|
// The server owns the transition: read current state, guard against allowedStates,
|
|
279
280
|
// write nextState. Reads/writes go straight to the in-memory Maps; a PostGraphile
|
|
280
281
|
// setup swaps in pgClient SQL instead.
|
|
281
|
-
|
|
282
|
+
const outcome = await applyGuardedTransition(
|
|
282
283
|
actionDef,
|
|
283
284
|
{
|
|
284
285
|
readState: async () => {
|
|
@@ -302,7 +303,7 @@ export const resolvers = {
|
|
|
302
303
|
// untouched); creating derives the identity from the doctype's declared
|
|
303
304
|
// primary key when the submitted data carries it, which is how a
|
|
304
305
|
// natural-keyed doctype is identified, and mints one only otherwise.
|
|
305
|
-
// Either way the full record comes back
|
|
306
|
+
// Either way the full record comes back, stating its identity.
|
|
306
307
|
writeData: async (patch: Record<string, unknown>, exists: boolean) => {
|
|
307
308
|
if (exists) {
|
|
308
309
|
if (recordId == null) return {}
|
|
@@ -340,6 +341,15 @@ export const resolvers = {
|
|
|
340
341
|
},
|
|
341
342
|
recordData
|
|
342
343
|
)
|
|
344
|
+
// The record as `stonecropRecord` returns it: the client stores this, never
|
|
345
|
+
// `data`. Keyed by the identity the reply states, since a save may create it.
|
|
346
|
+
const repliedId: unknown =
|
|
347
|
+
outcome.data !== null && typeof outcome.data === 'object'
|
|
348
|
+
? Reflect.get(outcome.data, lookupField)
|
|
349
|
+
: undefined
|
|
350
|
+
const readId = repliedId ?? recordId
|
|
351
|
+
const record = outcome.success && readId != null ? getRecord(d, String(readId), lookupField) : null
|
|
352
|
+
return { ...outcome, record }
|
|
343
353
|
} catch (err) {
|
|
344
354
|
return { success: false, data: null, error: err instanceof Error ? err.message : String(err) }
|
|
345
355
|
}
|
package/templates/schema.graphql
CHANGED
|
@@ -101,6 +101,8 @@ type ActionResult {
|
|
|
101
101
|
success: Boolean!
|
|
102
102
|
data: JSON
|
|
103
103
|
error: String
|
|
104
|
+
"The record as stonecropRecord returns it after the action. Null when the action failed or targets no record."
|
|
105
|
+
record: JSON
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
# ============================================
|