Skip to content

Commit 64b41e8

Browse files
authored
Add Nuxt.js generator (#220)
* Add Nuxt.js generator Fix #109 * - * minor fixes * update the help block * change relation text value * add a title on edit page * add a link to relation column * add NuxtGenerator test
1 parent f7ddabd commit 64b41e8

30 files changed

+1797
-20
lines changed

src/generators.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import AdminOnRestGenerator from "./generators/AdminOnRestGenerator";
22
import NextGenerator from "./generators/NextGenerator";
3+
import NuxtGenerator from "./generators/NuxtGenerator";
34
import ReactGenerator from "./generators/ReactGenerator";
45
import ReactNativeGenerator from "./generators/ReactNativeGenerator";
56
import TypescriptInterfaceGenerator from "./generators/TypescriptInterfaceGenerator";
@@ -18,6 +19,8 @@ export default function generators(generator = "react") {
1819
return wrap(AdminOnRestGenerator);
1920
case "next":
2021
return wrap(NextGenerator);
22+
case "nuxt":
23+
return wrap(NuxtGenerator);
2124
case "react":
2225
return wrap(ReactGenerator);
2326
case "react-native":

src/generators/NuxtGenerator.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import chalk from "chalk";
2+
import BaseVueGenerator from "./VueBaseGenerator";
3+
4+
export default class extends BaseVueGenerator {
5+
constructor(params) {
6+
super(params);
7+
8+
this.registerTemplates(`nuxt/`, [
9+
// components
10+
"components/ActionCell.vue",
11+
"components/Alert.vue",
12+
"components/ConfirmDelete.vue",
13+
"components/DataFilter.vue",
14+
"components/InputDate.vue",
15+
"components/Loading.vue",
16+
"components/Toolbar.vue",
17+
"components/foo/Filter.vue",
18+
"components/foo/Form.vue",
19+
20+
// mixins
21+
"mixins/create.js",
22+
"mixins/list.js",
23+
"mixins/notification.js",
24+
"mixins/show.js",
25+
"mixins/update.js",
26+
27+
// pages
28+
"pages/foos/new.vue",
29+
"pages/foos/index.vue",
30+
"pages/foos/_id.vue",
31+
32+
// store
33+
"store/crud.js",
34+
"store/foo.js"
35+
]);
36+
}
37+
38+
help(resource) {
39+
console.log(
40+
chalk.green('Code for the "%s" resource type has been generated!'),
41+
resource.title
42+
);
43+
}
44+
45+
generateFiles(api, resource, dir, params) {
46+
const context = super.getContextForResource(resource, params);
47+
const lc = context.lc;
48+
49+
[
50+
`${dir}/config`,
51+
`${dir}/error`,
52+
`${dir}/mixins`,
53+
`${dir}/services`,
54+
`${dir}/utils`,
55+
`${dir}/validators`
56+
].forEach(dir => this.createDir(dir, false));
57+
58+
// error
59+
this.createFile(
60+
"error/SubmissionError.js",
61+
`${dir}/error/SubmissionError.js`,
62+
{},
63+
false
64+
);
65+
66+
// mixins
67+
[
68+
"mixins/create.js",
69+
"mixins/list.js",
70+
"mixins/notification.js",
71+
"mixins/show.js",
72+
"mixins/update.js"
73+
].forEach(file => this.createFile(file, `${dir}/${file}`, context, false));
74+
75+
// stores
76+
this.createFile(
77+
`store/modules/notifications.js`,
78+
`${dir}/store/notifications.js`,
79+
{ hydraPrefix: this.hydraPrefix },
80+
false
81+
);
82+
83+
this.createFile(
84+
`store/crud.js`,
85+
`${dir}/store/crud.js`,
86+
{ hydraPrefix: this.hydraPrefix },
87+
false
88+
);
89+
90+
// validators
91+
this.createFile(
92+
"validators/date.js",
93+
`${dir}/validators/date.js`,
94+
{ hydraPrefix: this.hydraPrefix },
95+
false
96+
);
97+
98+
// utils
99+
["dates.js", "fetch.js", "hydra.js"].forEach(file =>
100+
this.createFile(`utils/${file}`, `${dir}/utils/${file}`, {}, false)
101+
);
102+
103+
this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.js`);
104+
105+
for (let dir of [`${dir}/components/${lc}`, `${dir}/pages/${lc}s`]) {
106+
this.createDir(dir);
107+
}
108+
109+
this.createFile("services/api.js", `${dir}/services/api.js`, {}, false);
110+
111+
[
112+
// components
113+
"components/%s/Filter.vue",
114+
"components/%s/Form.vue",
115+
116+
// pages
117+
"pages/%ss/new.vue",
118+
"pages/%ss/index.vue",
119+
"pages/%ss/_id.vue",
120+
121+
// service
122+
"services/%s.js",
123+
124+
// store
125+
"store/%s.js"
126+
].forEach(pattern => this.createFileFromPattern(pattern, dir, lc, context));
127+
128+
// components
129+
[
130+
"ActionCell.vue",
131+
"Alert.vue",
132+
"ConfirmDelete.vue",
133+
"DataFilter.vue",
134+
"InputDate.vue",
135+
"Loading.vue",
136+
"Toolbar.vue"
137+
].forEach(file =>
138+
this.createFile(
139+
`components/${file}`,
140+
`${dir}/components/${file}`,
141+
context,
142+
false
143+
)
144+
);
145+
}
146+
}

src/generators/NuxtGenerator.test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Api, Resource, Field } from "@api-platform/api-doc-parser/lib";
2+
import fs from "fs";
3+
import tmp from "tmp";
4+
import NuxtGenerator from "./NuxtGenerator";
5+
6+
const generator = new NuxtGenerator({
7+
hydraPrefix: "hydra:",
8+
templateDirectory: `${__dirname}/../../templates`
9+
});
10+
11+
afterEach(() => {
12+
jest.resetAllMocks();
13+
});
14+
15+
describe("generate", () => {
16+
test("Generate a Nuxt app", () => {
17+
const tmpobj = tmp.dirSync({ unsafeCleanup: true });
18+
19+
const fields = [
20+
new Field("bar", {
21+
id: "http://schema.org/url",
22+
range: "http://www.w3.org/2001/XMLSchema#string",
23+
reference: null,
24+
required: true,
25+
description: "An URL"
26+
})
27+
];
28+
const resource = new Resource("abc", "http://example.com/foos", {
29+
id: "foo",
30+
title: "Foo",
31+
readableFields: fields,
32+
writableFields: fields,
33+
getParameters: function getParameters() {
34+
return Promise.resolve([]);
35+
}
36+
});
37+
const api = new Api("http://example.com", {
38+
entrypoint: "http://example.com:8080",
39+
title: "My API",
40+
resources: [resource]
41+
});
42+
43+
generator.generate(api, resource, tmpobj.name).then(() => {
44+
[
45+
"/components/ActionCell",
46+
"/components/ConfirmDelete",
47+
"/components/DataFilter",
48+
"/components/foo/Filter",
49+
"/components/foo/Form.vue",
50+
"/components/foo/Layout",
51+
"/components/InputDate.vue",
52+
"/components/Loading.vue",
53+
"/components/Alert.vue",
54+
"/components/Toolbar.vue",
55+
"/config/entrypoint.js",
56+
"/error/SubmissionError.js",
57+
"/locales/en.js",
58+
"/services/api.js",
59+
"/services/foo.js",
60+
"/store/foo.js",
61+
"/utils/dates.js",
62+
"/utils/fetch.js",
63+
"/utils/hydra.js",
64+
"/pages/foos/_id.vue",
65+
"/pages/foos/index.vue",
66+
"/pages/foos/new.vue"
67+
].forEach(file => {
68+
expect(fs.existsSync(tmpobj.name + file)).toBe(true);
69+
});
70+
71+
tmpobj.removeCallback();
72+
});
73+
});
74+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<template>
2+
<div>
3+
<v-row justify="space-around">
4+
<v-icon v-if="handleShow" small class="mr-2" @click="handleShow">mdi-eye</v-icon>
5+
<v-icon v-if="handleEdit" small class="mr-2" @click="handleEdit">mdi-pencil</v-icon>
6+
<v-icon v-if="handleDelete" small @click="confirmDelete = true">mdi-delete</v-icon>
7+
</v-row>
8+
<ConfirmDelete
9+
v-if="handleDelete"
10+
:visible="confirmDelete"
11+
:handle-delete="handleDelete"
12+
@close="confirmDelete = false"
13+
/>
14+
</div>
15+
</template>
16+
17+
<script>
18+
import ConfirmDelete from './ConfirmDelete';
19+
20+
export default {
21+
name: 'ActionCell',
22+
components: {
23+
ConfirmDelete
24+
},
25+
data: () => ({
26+
confirmDelete: false
27+
}),
28+
props: {
29+
handleShow: {
30+
type: Function,
31+
required: false
32+
},
33+
handleEdit: {
34+
type: Function,
35+
required: false
36+
},
37+
handleDelete: {
38+
type: Function,
39+
required: false
40+
}
41+
}
42+
};
43+
</script>

templates/nuxt/components/Alert.vue

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<template>
2+
<v-snackbar
3+
v-model="show"
4+
:color="color"
5+
:multi-line="true"
6+
:timeout="timeout"
7+
right
8+
top
9+
>
10+
\{{ text }}
11+
<template v-if="subText">
12+
<p>\{{ subText }}</p>
13+
</template>
14+
15+
<template v-slot:action="{ attrs }">
16+
<v-btn
17+
dark
18+
text
19+
v-bind="attrs"
20+
@click="show = false"
21+
>
22+
Close
23+
</v-btn>
24+
</template>
25+
</v-snackbar>
26+
</template>
27+
28+
<script>
29+
import { mapFields } from 'vuex-map-fields';
30+
31+
export default {
32+
computed: {
33+
...mapFields('notifications', ['color', 'show', 'subText', 'text', 'timeout'])
34+
},
35+
36+
methods: {
37+
close() {
38+
this.show = false;
39+
}
40+
}
41+
};
42+
</script>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<template>
2+
<v-dialog v-model="show" persistent width="300">
3+
<v-card>
4+
<v-card-text>Are you sure you want to delete this item?</v-card-text>
5+
<v-card-actions>
6+
<v-spacer></v-spacer>
7+
<v-btn color="error darken-1" @click="handleDelete">
8+
Delete
9+
</v-btn>
10+
<v-btn color="secondary darken-1" text @click.stop="show = false">
11+
Cancel
12+
</v-btn>
13+
</v-card-actions>
14+
</v-card>
15+
</v-dialog>
16+
</template>
17+
18+
<script>
19+
export default {
20+
name: 'ConfirmDelete',
21+
props: {
22+
visible: {
23+
type: Boolean,
24+
required: true,
25+
default: () => false
26+
},
27+
handleDelete: {
28+
type: Function,
29+
required: true
30+
}
31+
},
32+
computed: {
33+
show: {
34+
get() {
35+
return this.visible;
36+
},
37+
set(value) {
38+
if (!value) {
39+
this.$emit('close');
40+
}
41+
}
42+
}
43+
}
44+
};
45+
</script>

0 commit comments

Comments
 (0)