@stonecrop/nuxt 0.6.3 → 0.7.1
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 +3 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +56 -13
- package/dist/runtime/layouts/StonecropHome.d.vue.ts +3 -0
- package/dist/runtime/pages/DocBuilderDetail.d.vue.ts +3 -0
- package/dist/runtime/pages/DocBuilderDetail.vue +114 -0
- package/dist/runtime/pages/DocBuilderDetail.vue.d.ts +3 -0
- package/dist/runtime/pages/DocBuilderIndex.d.vue.ts +3 -0
- package/dist/runtime/pages/DocBuilderIndex.vue +44 -0
- package/dist/runtime/pages/DocBuilderIndex.vue.d.ts +3 -0
- package/dist/runtime/pages/StonecropPage.d.vue.ts +3 -0
- package/dist/runtime/pages/StonecropPage.vue +146 -11
- package/dist/runtime/server/api/docbuilder/[doctype].get.d.ts +2 -0
- package/dist/runtime/server/api/docbuilder/[doctype].get.js +42 -0
- package/dist/runtime/server/api/docbuilder/doctypes.get.d.ts +2 -0
- package/dist/runtime/server/api/docbuilder/doctypes.get.js +32 -0
- package/dist/runtime/server/api/docbuilder/save.post.d.ts +2 -0
- package/dist/runtime/server/api/docbuilder/save.post.js +40 -0
- package/dist/runtime/server/api/docbuilder/validate.post.d.ts +2 -0
- package/dist/runtime/server/api/docbuilder/validate.post.js +29 -0
- package/package.json +32 -24
package/dist/module.d.mts
CHANGED
|
@@ -2,7 +2,10 @@ import * as _nuxt_schema from '@nuxt/schema';
|
|
|
2
2
|
|
|
3
3
|
interface ModuleOptions {
|
|
4
4
|
router?: Record<string, unknown>;
|
|
5
|
+
/** Enable the DocBuilder feature with /docbuilder routes */
|
|
5
6
|
docbuilder?: boolean;
|
|
7
|
+
/** Path to doctypes folder (defaults to 'doctypes' in srcDir) */
|
|
8
|
+
doctypesDir?: string;
|
|
6
9
|
}
|
|
7
10
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
8
11
|
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readdir, readFile } from 'node:fs/promises';
|
|
3
3
|
import { extname } from 'node:path';
|
|
4
|
-
import { createResolver, defineNuxtModule, useLogger, addLayout, extendPages, addPlugin } from '@nuxt/kit';
|
|
4
|
+
import { createResolver, defineNuxtModule, useLogger, addLayout, extendPages, addServerHandler, addPlugin } from '@nuxt/kit';
|
|
5
5
|
|
|
6
6
|
const { resolve } = createResolver(import.meta.url);
|
|
7
|
-
const module = defineNuxtModule({
|
|
7
|
+
const module$1 = defineNuxtModule({
|
|
8
8
|
meta: {
|
|
9
9
|
name: "@stonecrop/nuxt",
|
|
10
10
|
configKey: "stonecrop"
|
|
@@ -12,7 +12,8 @@ const module = defineNuxtModule({
|
|
|
12
12
|
defaults: (_nuxt) => {
|
|
13
13
|
return {
|
|
14
14
|
router: {},
|
|
15
|
-
docbuilder: false
|
|
15
|
+
docbuilder: false,
|
|
16
|
+
doctypesDir: void 0
|
|
16
17
|
};
|
|
17
18
|
},
|
|
18
19
|
async setup(_options, nuxt) {
|
|
@@ -44,10 +45,6 @@ const module = defineNuxtModule({
|
|
|
44
45
|
for (const schema of schemas) {
|
|
45
46
|
try {
|
|
46
47
|
const schemaName = schema.replace(".json", "");
|
|
47
|
-
if (pagePaths.includes(`/${schemaName}`)) {
|
|
48
|
-
logger.warn(`Skipping doctype '${schemaName}': conflicts with existing page`);
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
48
|
const schemaPath = resolve(doctypesDir, schema);
|
|
52
49
|
const fileContents = await readFile(schemaPath, "utf-8");
|
|
53
50
|
let schemaData;
|
|
@@ -57,18 +54,25 @@ const module = defineNuxtModule({
|
|
|
57
54
|
logger.error(`Failed to parse schema file '${schema}':`, parseError);
|
|
58
55
|
continue;
|
|
59
56
|
}
|
|
60
|
-
|
|
57
|
+
const schemaFields = schemaData.schema || schemaData.fields;
|
|
58
|
+
if (!schemaFields) {
|
|
59
|
+
logger.warn(`Schema file '${schema}' missing 'schema' or 'fields' property, skipping`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const routePath = schemaData.slug || schemaName.toLowerCase();
|
|
63
|
+
if (!pagePaths.includes(`/${routePath}`)) {
|
|
61
64
|
pages.unshift({
|
|
62
65
|
name: `stonecrop-${schemaName}`,
|
|
63
|
-
path: `/${
|
|
66
|
+
path: `/${routePath}`,
|
|
64
67
|
file: stonecropPage,
|
|
65
68
|
meta: {
|
|
66
|
-
schema:
|
|
69
|
+
schema: schemaFields,
|
|
70
|
+
doctype: schemaData
|
|
67
71
|
}
|
|
68
72
|
});
|
|
69
|
-
logger.log(`Added
|
|
73
|
+
logger.log(`Added route: /${routePath} (${schemaName})`);
|
|
70
74
|
} else {
|
|
71
|
-
logger.warn(`
|
|
75
|
+
logger.warn(`Route /${routePath} already exists, skipping ${schemaName}`);
|
|
72
76
|
}
|
|
73
77
|
} catch (schemaError) {
|
|
74
78
|
logger.error(`Error processing schema '${schema}':`, schemaError);
|
|
@@ -90,6 +94,45 @@ const module = defineNuxtModule({
|
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
}
|
|
97
|
+
if (_options.docbuilder) {
|
|
98
|
+
logger.log("DocBuilder enabled, adding routes and handlers");
|
|
99
|
+
const pagesDir = resolve("runtime/pages");
|
|
100
|
+
const docBuilderIndex = resolve(pagesDir, "DocBuilderIndex.vue");
|
|
101
|
+
const docBuilderDetail = resolve(pagesDir, "DocBuilderDetail.vue");
|
|
102
|
+
extendPages((pages) => {
|
|
103
|
+
pages.push({
|
|
104
|
+
name: "docbuilder-index",
|
|
105
|
+
path: "/docbuilder",
|
|
106
|
+
file: docBuilderIndex
|
|
107
|
+
});
|
|
108
|
+
pages.push({
|
|
109
|
+
name: "docbuilder-detail",
|
|
110
|
+
path: "/docbuilder/:doctype",
|
|
111
|
+
file: docBuilderDetail
|
|
112
|
+
});
|
|
113
|
+
logger.log("Added DocBuilder pages at /docbuilder");
|
|
114
|
+
});
|
|
115
|
+
const handlersDir = resolve("runtime/server/api/docbuilder");
|
|
116
|
+
addServerHandler({
|
|
117
|
+
route: "/api/docbuilder/doctypes",
|
|
118
|
+
handler: resolve(handlersDir, "doctypes.get")
|
|
119
|
+
});
|
|
120
|
+
addServerHandler({
|
|
121
|
+
route: "/api/docbuilder/:doctype",
|
|
122
|
+
handler: resolve(handlersDir, "[doctype].get")
|
|
123
|
+
});
|
|
124
|
+
addServerHandler({
|
|
125
|
+
route: "/api/docbuilder/validate",
|
|
126
|
+
method: "post",
|
|
127
|
+
handler: resolve(handlersDir, "validate.post")
|
|
128
|
+
});
|
|
129
|
+
addServerHandler({
|
|
130
|
+
route: "/api/docbuilder/save",
|
|
131
|
+
method: "post",
|
|
132
|
+
handler: resolve(handlersDir, "save.post")
|
|
133
|
+
});
|
|
134
|
+
logger.log("Added DocBuilder API handlers");
|
|
135
|
+
}
|
|
93
136
|
const pluginPath = resolve("./runtime/plugin");
|
|
94
137
|
try {
|
|
95
138
|
addPlugin(pluginPath);
|
|
@@ -100,4 +143,4 @@ const module = defineNuxtModule({
|
|
|
100
143
|
}
|
|
101
144
|
});
|
|
102
145
|
|
|
103
|
-
export { module as default };
|
|
146
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="docbuilder-container">
|
|
3
|
+
<div v-if="loading" class="loading">Loading...</div>
|
|
4
|
+
<div v-else class="docbuilder-wrapper">
|
|
5
|
+
<div class="docbuilder-header">
|
|
6
|
+
<h1>{{ doctypeName }}</h1>
|
|
7
|
+
<div class="docbuilder-actions">
|
|
8
|
+
<button class="btn-secondary" :disabled="validating" @click="validateSchema">
|
|
9
|
+
{{ validating ? "Validating..." : "Validate Schema" }}
|
|
10
|
+
</button>
|
|
11
|
+
<button class="btn-primary" :disabled="saving" @click="saveToDisk">
|
|
12
|
+
{{ saving ? "Saving..." : "Save to Disk" }}
|
|
13
|
+
</button>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- Validation Result -->
|
|
18
|
+
<div v-if="validationResult" class="message-box" :class="validationResult.success ? 'success' : 'error'">
|
|
19
|
+
<div class="message-header">
|
|
20
|
+
<span>{{ validationResult.success ? "\u2713 Schema is valid!" : "\u2717 Validation failed" }}</span>
|
|
21
|
+
<button class="dismiss-btn" @click="validationResult = null">×</button>
|
|
22
|
+
</div>
|
|
23
|
+
<ul v-if="!validationResult.success" class="error-list">
|
|
24
|
+
<li v-for="(error, idx) in validationResult.errors" :key="idx">
|
|
25
|
+
<code>{{ error.path.join(".") || "root" }}</code
|
|
26
|
+
>: {{ error.message }}
|
|
27
|
+
</li>
|
|
28
|
+
</ul>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- Save Message -->
|
|
32
|
+
<div v-if="saveMessage" class="message-box" :class="saveMessage.type">
|
|
33
|
+
<span>{{ saveMessage.text }}</span>
|
|
34
|
+
<button class="dismiss-btn" @click="saveMessage = null">×</button>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Fields -->
|
|
38
|
+
<section class="fields-section">
|
|
39
|
+
<h2>Fields ({{ doctype?.schema?.length || 0 }})</h2>
|
|
40
|
+
<div v-for="field in doctype?.schema" :key="field.fieldname" class="field-item">
|
|
41
|
+
<div class="field-header">
|
|
42
|
+
<span class="field-name">{{ field.fieldname }}</span>
|
|
43
|
+
<span class="field-type">{{ field.fieldtype }}</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="field-label">{{ field.label }}</div>
|
|
46
|
+
<div v-if="field.required || field.readOnly" class="field-badges">
|
|
47
|
+
<span v-if="field.required" class="badge">Required</span>
|
|
48
|
+
<span v-if="field.readOnly" class="badge">Read Only</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</section>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<script setup>
|
|
57
|
+
import { ref, computed, onMounted } from "vue";
|
|
58
|
+
import { useRoute } from "nuxt/app";
|
|
59
|
+
const route = useRoute();
|
|
60
|
+
const doctypeName = computed(() => route.params.doctype);
|
|
61
|
+
const doctype = ref(null);
|
|
62
|
+
const loading = ref(true);
|
|
63
|
+
const validating = ref(false);
|
|
64
|
+
const saving = ref(false);
|
|
65
|
+
const validationResult = ref(null);
|
|
66
|
+
const saveMessage = ref(null);
|
|
67
|
+
onMounted(async () => {
|
|
68
|
+
try {
|
|
69
|
+
const data = await $fetch(`/api/docbuilder/${doctypeName.value}`);
|
|
70
|
+
doctype.value = data;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("Error loading doctype:", error);
|
|
73
|
+
} finally {
|
|
74
|
+
loading.value = false;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
async function validateSchema() {
|
|
78
|
+
validating.value = true;
|
|
79
|
+
validationResult.value = null;
|
|
80
|
+
try {
|
|
81
|
+
const result = await $fetch("/api/docbuilder/validate", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: { fields: doctype.value?.schema || [] }
|
|
84
|
+
});
|
|
85
|
+
validationResult.value = result;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
validationResult.value = {
|
|
88
|
+
success: false,
|
|
89
|
+
errors: [{ path: [], message: error.message || "Validation failed" }]
|
|
90
|
+
};
|
|
91
|
+
} finally {
|
|
92
|
+
validating.value = false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function saveToDisk() {
|
|
96
|
+
saving.value = true;
|
|
97
|
+
saveMessage.value = null;
|
|
98
|
+
try {
|
|
99
|
+
await $fetch("/api/docbuilder/save", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
body: { doctype: doctypeName.value, schema: doctype.value?.schema || [] }
|
|
102
|
+
});
|
|
103
|
+
saveMessage.value = { type: "success", text: "Schema saved successfully!" };
|
|
104
|
+
} catch (error) {
|
|
105
|
+
saveMessage.value = { type: "error", text: error.message || "Failed to save" };
|
|
106
|
+
} finally {
|
|
107
|
+
saving.value = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<style scoped>
|
|
113
|
+
.docbuilder-container{min-height:100vh}.docbuilder-wrapper{margin:0 auto;max-width:1200px;padding:2rem}.docbuilder-header{margin-bottom:2rem;text-align:center}.docbuilder-header h1{font-size:2rem;font-weight:700;margin:0 0 1rem}.docbuilder-actions{display:flex;gap:1rem;justify-content:center}.btn-primary,.btn-secondary{border-radius:.5rem;cursor:pointer;font-weight:500;padding:.5rem 1rem;transition:all .2s}.btn-primary{background:#3b82f6;border:none;color:#fff}.btn-primary:hover:not(:disabled){background:#2563eb}.btn-secondary{background:#e5e7eb;border:1px solid #d1d5db;color:#374151}.btn-secondary:hover:not(:disabled){background:#d1d5db}.btn-primary:disabled,.btn-secondary:disabled{cursor:not-allowed;opacity:.6}.message-box{border-radius:.5rem;margin-bottom:1rem;padding:1rem}.message-box.success{background:#d1fae5;border:1px solid #10b981;color:#065f46}.message-box.error{background:#fee2e2;border:1px solid #ef4444;color:#991b1b}.message-header{align-items:center;display:flex;justify-content:space-between}.dismiss-btn{background:none;border:none;cursor:pointer;font-size:1.5rem;opacity:.6}.dismiss-btn:hover{opacity:1}.error-list{margin:.5rem 0 0;padding-left:1.5rem}.error-list code{background:rgba(0,0,0,.1);border-radius:.25rem;padding:.125rem .25rem}.loading{color:#6b7280;padding:4rem;text-align:center}.fields-section{background:#fff;border-radius:.75rem;box-shadow:0 4px 12px rgba(0,0,0,.1);padding:1.5rem}.fields-section h2{font-size:1.25rem;margin:0 0 1rem}.field-item{background:#f9fafb;border:1px solid #e5e7eb;border-radius:.5rem;margin-bottom:.75rem;padding:1rem}.field-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:.25rem}.field-name{font-family:monospace;font-weight:600}.field-type{background:#dbeafe;border-radius:.25rem;color:#1e40af;font-size:.75rem;padding:.25rem .5rem}.field-label{color:#6b7280;font-size:.875rem}.field-badges{display:flex;gap:.5rem;margin-top:.5rem}.badge{background:#e5e7eb;border-radius:.25rem;font-size:.75rem;padding:.125rem .5rem}
|
|
114
|
+
</style>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="docbuilder-index-container">
|
|
3
|
+
<div class="docbuilder-header">
|
|
4
|
+
<h1>DocType Builder</h1>
|
|
5
|
+
<p class="subtitle">Select a DocType to view and edit its schema</p>
|
|
6
|
+
</div>
|
|
7
|
+
<ClientOnly>
|
|
8
|
+
<div v-if="loading" class="loading">Loading doctypes...</div>
|
|
9
|
+
<ATable v-else :columns="columns" :rows="doctypes" :config="config" @row-click="handleRowClick" />
|
|
10
|
+
</ClientOnly>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup>
|
|
15
|
+
import { ref, onMounted } from "vue";
|
|
16
|
+
import { useRouter } from "nuxt/app";
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
const doctypes = ref([]);
|
|
19
|
+
const loading = ref(true);
|
|
20
|
+
const columns = [
|
|
21
|
+
{ label: "Name", name: "name", fieldtype: "Data", width: "20ch" },
|
|
22
|
+
{ label: "Fields", name: "fieldCount", fieldtype: "Int", width: "10ch" }
|
|
23
|
+
];
|
|
24
|
+
const config = {
|
|
25
|
+
view: "uncounted"
|
|
26
|
+
};
|
|
27
|
+
onMounted(async () => {
|
|
28
|
+
try {
|
|
29
|
+
const data = await $fetch("/api/docbuilder/doctypes");
|
|
30
|
+
doctypes.value = data;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error("Error loading doctypes:", error);
|
|
33
|
+
} finally {
|
|
34
|
+
loading.value = false;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
function handleRowClick(row) {
|
|
38
|
+
router.push(`/docbuilder/${row.slug}`);
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<style scoped>
|
|
43
|
+
.docbuilder-index-container{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{color:#6b7280;font-size:1.125rem;margin:0}.loading{color:#6b7280;padding:2rem;text-align:center}
|
|
44
|
+
</style>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -1,18 +1,153 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<div class="stonecrop-page">
|
|
3
|
+
<ClientOnly>
|
|
4
|
+
<component v-if="!loading" :is="rootComponent" v-bind="componentProps" @row-click="handleRowClick" />
|
|
5
|
+
<div v-else class="loading-state">Loading...</div>
|
|
6
|
+
<template #fallback>
|
|
7
|
+
<div class="loading-state">Loading...</div>
|
|
8
|
+
</template>
|
|
9
|
+
</ClientOnly>
|
|
10
|
+
</div>
|
|
3
11
|
</template>
|
|
4
12
|
|
|
5
13
|
<script setup>
|
|
6
|
-
import { useRoute } from "nuxt/app";
|
|
7
|
-
import { onMounted, ref } from "vue";
|
|
14
|
+
import { useRoute, useRouter } from "nuxt/app";
|
|
15
|
+
import { onMounted, ref, computed, watch, markRaw } from "vue";
|
|
8
16
|
const route = useRoute();
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
const loading = ref(true);
|
|
19
|
+
const data = ref(null);
|
|
20
|
+
const doctype = computed(() => route.meta.doctype);
|
|
21
|
+
const schemaFields = computed(() => route.meta.schema);
|
|
22
|
+
const rootComponent = computed(() => {
|
|
23
|
+
const fields = schemaFields.value || [];
|
|
24
|
+
const rootField = fields.find((f) => f.component);
|
|
25
|
+
if (rootField?.component) {
|
|
26
|
+
return rootField.component;
|
|
27
|
+
}
|
|
28
|
+
return "AForm";
|
|
17
29
|
});
|
|
30
|
+
const componentProps = computed(() => {
|
|
31
|
+
const fields = schemaFields.value || [];
|
|
32
|
+
const rootField = fields.find((f) => f.component);
|
|
33
|
+
if (rootField?.component === "ATable") {
|
|
34
|
+
return {
|
|
35
|
+
columns: rootField.columns || buildColumnsFromFields(fields),
|
|
36
|
+
rows: data.value || [],
|
|
37
|
+
config: rootField.config || { view: "list" }
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
modelValue: buildFormSchema(fields),
|
|
42
|
+
data: data.value || {}
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
function buildColumnsFromFields(fields) {
|
|
46
|
+
const excludeTypes = ["Text", "Attach", "JSON", "Table"];
|
|
47
|
+
return fields.filter((f) => !excludeTypes.includes(f.fieldtype) && !f.component).slice(0, 8).map((f) => ({
|
|
48
|
+
name: f.fieldname,
|
|
49
|
+
label: f.label || f.fieldname,
|
|
50
|
+
fieldtype: f.fieldtype,
|
|
51
|
+
width: f.width || "15ch"
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
function fieldtypeToComponent(fieldtype) {
|
|
55
|
+
const mapping = {
|
|
56
|
+
Data: "ATextInput",
|
|
57
|
+
Text: "ATextInput",
|
|
58
|
+
Check: "ACheckbox",
|
|
59
|
+
Int: "ANumericInput",
|
|
60
|
+
Float: "ANumericInput",
|
|
61
|
+
Date: "ADate",
|
|
62
|
+
Datetime: "ADate",
|
|
63
|
+
Select: "ADropdown",
|
|
64
|
+
Link: "AComboBox",
|
|
65
|
+
Attach: "ATextInput"
|
|
66
|
+
};
|
|
67
|
+
return mapping[fieldtype] || "ATextInput";
|
|
68
|
+
}
|
|
69
|
+
function buildFormSchema(fields) {
|
|
70
|
+
return fields.filter((f) => !f.component).map((f) => ({
|
|
71
|
+
fieldname: f.fieldname,
|
|
72
|
+
label: f.label || f.fieldname,
|
|
73
|
+
component: fieldtypeToComponent(f.fieldtype),
|
|
74
|
+
fieldtype: f.fieldtype,
|
|
75
|
+
required: f.required,
|
|
76
|
+
readOnly: f.readOnly,
|
|
77
|
+
options: f.options,
|
|
78
|
+
default: f.default
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
const isListView = computed(() => {
|
|
82
|
+
const fields = schemaFields.value || [];
|
|
83
|
+
const rootField = fields.find((f) => f.component);
|
|
84
|
+
return rootField?.component === "ATable";
|
|
85
|
+
});
|
|
86
|
+
async function fetchData() {
|
|
87
|
+
loading.value = true;
|
|
88
|
+
const doctypeName = doctype.value?.name;
|
|
89
|
+
if (!doctypeName) {
|
|
90
|
+
loading.value = false;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
if (isListView.value) {
|
|
95
|
+
const query = `
|
|
96
|
+
query GetRecords($doctype: String!) {
|
|
97
|
+
stonecropRecords(doctype: $doctype) {
|
|
98
|
+
data
|
|
99
|
+
count
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
const response = await $fetch("/graphql/", {
|
|
104
|
+
method: "POST",
|
|
105
|
+
body: {
|
|
106
|
+
query,
|
|
107
|
+
variables: { doctype: doctypeName }
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
data.value = response.data?.stonecropRecords?.data || [];
|
|
111
|
+
} else {
|
|
112
|
+
const recordId = route.params.id;
|
|
113
|
+
if (!recordId || recordId === "new") {
|
|
114
|
+
data.value = {};
|
|
115
|
+
loading.value = false;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const query = `
|
|
119
|
+
query GetRecord($doctype: String!, $id: String!) {
|
|
120
|
+
stonecropRecord(doctype: $doctype, id: $id) {
|
|
121
|
+
data
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
`;
|
|
125
|
+
const response = await $fetch("/graphql/", {
|
|
126
|
+
method: "POST",
|
|
127
|
+
body: {
|
|
128
|
+
query,
|
|
129
|
+
variables: { doctype: doctypeName, id: recordId }
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
data.value = response.data?.stonecropRecord?.data || {};
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.warn("[@stonecrop/nuxt] Could not fetch data:", e);
|
|
136
|
+
data.value = isListView.value ? [] : {};
|
|
137
|
+
}
|
|
138
|
+
loading.value = false;
|
|
139
|
+
}
|
|
140
|
+
function handleRowClick(row) {
|
|
141
|
+
const id = row.id || row.name || row.slug;
|
|
142
|
+
if (id && isListView.value) {
|
|
143
|
+
const basePath = route.path.replace(/\/$/, "");
|
|
144
|
+
router.push(`${basePath}/${id}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
onMounted(fetchData);
|
|
148
|
+
watch(() => route.params, fetchData, { deep: true });
|
|
18
149
|
</script>
|
|
150
|
+
|
|
151
|
+
<style scoped>
|
|
152
|
+
.stonecrop-page{width:100%}.loading-state{color:var(--sc-gray-50);padding:2rem;text-align:center}
|
|
153
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { createError, defineEventHandler, getRouterParam, useRuntimeConfig } from "#imports";
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const doctype = getRouterParam(event, "doctype");
|
|
7
|
+
if (!doctype) {
|
|
8
|
+
throw createError({
|
|
9
|
+
statusCode: 400,
|
|
10
|
+
message: "Missing doctype parameter"
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
const config = useRuntimeConfig();
|
|
14
|
+
const doctypesDir = config.stonecrop?.doctypesDir || resolve(process.cwd(), "doctypes");
|
|
15
|
+
const filePath = resolve(doctypesDir, `${doctype}.json`);
|
|
16
|
+
if (!filePath.startsWith(doctypesDir)) {
|
|
17
|
+
throw createError({
|
|
18
|
+
statusCode: 400,
|
|
19
|
+
message: "Invalid doctype name"
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (!existsSync(filePath)) {
|
|
23
|
+
throw createError({
|
|
24
|
+
statusCode: 404,
|
|
25
|
+
message: `DocType '${doctype}' not found`
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const content = await readFile(filePath, "utf-8");
|
|
30
|
+
const data = JSON.parse(content);
|
|
31
|
+
return {
|
|
32
|
+
name: doctype.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
|
|
33
|
+
slug: doctype,
|
|
34
|
+
schema: data.schema || []
|
|
35
|
+
};
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw createError({
|
|
38
|
+
statusCode: 500,
|
|
39
|
+
message: `Failed to read doctype: ${error.message}`
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { extname, resolve } from "node:path";
|
|
4
|
+
import { defineEventHandler, useRuntimeConfig } from "#imports";
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const config = useRuntimeConfig();
|
|
7
|
+
const doctypesDir = config.stonecrop?.doctypesDir || resolve(process.cwd(), "doctypes");
|
|
8
|
+
if (!existsSync(doctypesDir)) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const files = await readdir(doctypesDir);
|
|
13
|
+
const jsonFiles = files.filter((f) => extname(f) === ".json");
|
|
14
|
+
const doctypes = await Promise.all(
|
|
15
|
+
jsonFiles.map(async (file) => {
|
|
16
|
+
const filePath = resolve(doctypesDir, file);
|
|
17
|
+
const content = await readFile(filePath, "utf-8");
|
|
18
|
+
const data = JSON.parse(content);
|
|
19
|
+
const name = file.replace(".json", "");
|
|
20
|
+
return {
|
|
21
|
+
name: name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
|
|
22
|
+
slug: name,
|
|
23
|
+
fieldCount: data.schema?.length || 0
|
|
24
|
+
};
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
return doctypes;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error("Error reading doctypes:", error);
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { createError, defineEventHandler, readBody, useRuntimeConfig } from "#imports";
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const body = await readBody(event);
|
|
6
|
+
if (!body.doctype || typeof body.doctype !== "string") {
|
|
7
|
+
throw createError({
|
|
8
|
+
statusCode: 400,
|
|
9
|
+
message: "Missing or invalid doctype name"
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
if (!body.schema || !Array.isArray(body.schema)) {
|
|
13
|
+
throw createError({
|
|
14
|
+
statusCode: 400,
|
|
15
|
+
message: "Missing or invalid schema array"
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const config = useRuntimeConfig();
|
|
19
|
+
const doctypesDir = config.stonecrop?.doctypesDir || resolve(process.cwd(), "doctypes");
|
|
20
|
+
const filename = body.doctype.toLowerCase().replace(/\s+/g, "-");
|
|
21
|
+
const filePath = resolve(doctypesDir, `${filename}.json`);
|
|
22
|
+
if (!filePath.startsWith(doctypesDir)) {
|
|
23
|
+
throw createError({
|
|
24
|
+
statusCode: 400,
|
|
25
|
+
message: "Invalid doctype name"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const doctypeData = {
|
|
29
|
+
schema: body.schema
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
await writeFile(filePath, JSON.stringify(doctypeData, null, " "), "utf-8");
|
|
33
|
+
return { success: true, path: `doctypes/${filename}.json` };
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw createError({
|
|
36
|
+
statusCode: 500,
|
|
37
|
+
message: `Failed to save file: ${error.message}`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { validateField } from "@stonecrop/schema";
|
|
2
|
+
import { createError, defineEventHandler, readBody } from "#imports";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const body = await readBody(event);
|
|
5
|
+
if (!body.fields || !Array.isArray(body.fields)) {
|
|
6
|
+
throw createError({
|
|
7
|
+
statusCode: 400,
|
|
8
|
+
message: "Missing or invalid fields array"
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
const errors = [];
|
|
12
|
+
for (let i = 0; i < body.fields.length; i++) {
|
|
13
|
+
const field = body.fields[i];
|
|
14
|
+
const result2 = validateField(field);
|
|
15
|
+
if (!result2.success) {
|
|
16
|
+
for (const error of result2.errors) {
|
|
17
|
+
errors.push({
|
|
18
|
+
path: [`fields[${i}]`, field.fieldname || `field_${i}`, ...error.path],
|
|
19
|
+
message: error.message
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const result = {
|
|
25
|
+
success: errors.length === 0,
|
|
26
|
+
errors
|
|
27
|
+
};
|
|
28
|
+
return result;
|
|
29
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonecrop/nuxt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Nuxt module for Stonecrop",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,35 +31,39 @@
|
|
|
31
31
|
"dist"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@nuxt/kit": "^4.2.
|
|
35
|
-
"pinia": "^3.0.
|
|
36
|
-
"@stonecrop/aform": "0.
|
|
37
|
-
"@stonecrop/
|
|
38
|
-
"@stonecrop/
|
|
39
|
-
"@stonecrop/atable": "0.
|
|
34
|
+
"@nuxt/kit": "^4.2.2",
|
|
35
|
+
"pinia": "^3.0.4",
|
|
36
|
+
"@stonecrop/aform": "0.7.1",
|
|
37
|
+
"@stonecrop/casl-middleware": "0.7.1",
|
|
38
|
+
"@stonecrop/graphql-middleware": "0.7.1",
|
|
39
|
+
"@stonecrop/atable": "0.7.1",
|
|
40
|
+
"@stonecrop/node-editor": "0.7.1",
|
|
41
|
+
"@stonecrop/nuxt-grafserv": "0.7.1",
|
|
42
|
+
"@stonecrop/schema": "0.7.1",
|
|
43
|
+
"@stonecrop/stonecrop": "0.7.1"
|
|
40
44
|
},
|
|
41
45
|
"devDependencies": {
|
|
42
|
-
"@eslint/js": "^9.
|
|
43
|
-
"@nuxt/devtools": "^3.
|
|
44
|
-
"@nuxt/eslint": "1.
|
|
45
|
-
"@nuxt/eslint-config": "^1.
|
|
46
|
+
"@eslint/js": "^9.39.2",
|
|
47
|
+
"@nuxt/devtools": "^3.1.1",
|
|
48
|
+
"@nuxt/eslint": "1.12.1",
|
|
49
|
+
"@nuxt/eslint-config": "^1.12.1",
|
|
46
50
|
"@nuxt/module-builder": "^1.0.2",
|
|
47
|
-
"@nuxt/schema": "^4.2.
|
|
48
|
-
"@nuxt/test-utils": "^3.
|
|
51
|
+
"@nuxt/schema": "^4.2.2",
|
|
52
|
+
"@nuxt/test-utils": "^3.23.0",
|
|
49
53
|
"browserslist": "latest",
|
|
50
54
|
"baseline-browser-mapping": "latest",
|
|
51
|
-
"eslint": "^9.
|
|
55
|
+
"eslint": "^9.39.2",
|
|
52
56
|
"eslint-config-prettier": "^10.1.8",
|
|
53
|
-
"eslint-plugin-vue": "^10.
|
|
54
|
-
"globals": "^
|
|
55
|
-
"nuxt": "^4.2.
|
|
57
|
+
"eslint-plugin-vue": "^10.6.2",
|
|
58
|
+
"globals": "^17.0.0",
|
|
59
|
+
"nuxt": "^4.2.2",
|
|
56
60
|
"typescript": "^5.9.3",
|
|
57
|
-
"typescript-eslint": "^8.
|
|
58
|
-
"vue": "^3.5.
|
|
59
|
-
"vite": "^7.
|
|
60
|
-
"vitest": "^4.0.
|
|
61
|
-
"vue-router": "^4.6.
|
|
62
|
-
"vue-tsc": "^3.
|
|
61
|
+
"typescript-eslint": "^8.53.0",
|
|
62
|
+
"vue": "^3.5.26",
|
|
63
|
+
"vite": "^7.3.1",
|
|
64
|
+
"vitest": "^4.0.17",
|
|
65
|
+
"vue-router": "^4.6.4",
|
|
66
|
+
"vue-tsc": "^3.2.2"
|
|
63
67
|
},
|
|
64
68
|
"engines": {
|
|
65
69
|
"node": ">=22.5.0"
|
|
@@ -68,9 +72,13 @@
|
|
|
68
72
|
"_phase:build": "rushx dev:prepare && nuxt-module-build build",
|
|
69
73
|
"prepublish": "rushx dev:prepare && nuxt-module-build build",
|
|
70
74
|
"build": "rushx dev:prepare && nuxt-module-build build",
|
|
75
|
+
"docs": "echo 'Nuxt modules use custom build system - skipping API docs generation'",
|
|
71
76
|
"dev": "rushx dev:prepare && nuxi dev playground",
|
|
77
|
+
"dev:basic": "rushx dev:prepare && nuxi dev playground",
|
|
78
|
+
"dev:full": "rushx dev:prepare:full && nuxi dev fullstack",
|
|
79
|
+
"dev:prepare:full": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare fullstack",
|
|
72
80
|
"dev:build": "nuxi build playground",
|
|
73
|
-
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
|
|
81
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground && nuxi prepare fullstack",
|
|
74
82
|
"lint": "eslint .",
|
|
75
83
|
"test": "vitest run",
|
|
76
84
|
"test:ui": "vitest --ui",
|