From 5e31799ee29dc0f8cb261f86ae39eb153ee85020 Mon Sep 17 00:00:00 2001 From: Valentin Date: Wed, 25 Jan 2023 12:26:58 +0100 Subject: [PATCH] feat: migrate templates from Nuxt 2 to Nuxt 3 (#356) --- src/generators/NuxtGenerator.js | 299 +++++++++++------- src/generators/NuxtGenerator.test.js | 135 ++++---- templates/common/tailwind.config.js | 8 +- templates/common/types/collection.ts | 16 +- templates/common/types/item.ts | 2 +- templates/common/types/view.ts | 10 +- templates/nuxt/components/ActionCell.vue | 70 ---- templates/nuxt/components/Alert.vue | 42 --- templates/nuxt/components/ConfirmDelete.vue | 45 --- templates/nuxt/components/DataFilter.vue | 58 ---- templates/nuxt/components/InputDate.vue | 50 --- templates/nuxt/components/Loading.vue | 18 -- templates/nuxt/components/Toolbar.vue | 123 ------- .../nuxt/components/common/FormRepeater.vue | 67 ++++ templates/nuxt/components/foo/Filter.vue | 136 -------- templates/nuxt/components/foo/FooCreate.vue | 52 +++ templates/nuxt/components/foo/FooForm.vue | 97 ++++++ templates/nuxt/components/foo/FooList.vue | 281 ++++++++++++++++ templates/nuxt/components/foo/FooShow.vue | 206 ++++++++++++ templates/nuxt/components/foo/FooUpdate.vue | 136 ++++++++ templates/nuxt/components/foo/Form.vue | 221 ------------- templates/nuxt/composables/api.ts | 181 +++++++++++ templates/nuxt/mixins/create.js | 39 --- templates/nuxt/mixins/list.js | 78 ----- templates/nuxt/mixins/notification.js | 37 --- templates/nuxt/mixins/show.js | 37 --- templates/nuxt/mixins/update.js | 75 ----- templates/nuxt/nuxt.config.ts | 21 ++ templates/nuxt/pages/foos/[id]/edit.vue | 9 + templates/nuxt/pages/foos/[id]/index.vue | 9 + templates/nuxt/pages/foos/_id/edit.vue | 66 ---- templates/nuxt/pages/foos/_id/index.vue | 77 ----- templates/nuxt/pages/foos/create.vue | 45 +-- templates/nuxt/pages/foos/index.vue | 181 +---------- templates/nuxt/pages/foos/page/[page].vue | 9 + templates/nuxt/pages/index.vue | 5 + templates/nuxt/store/crud.js | 273 ---------------- templates/nuxt/store/foo.js | 6 - templates/nuxt/store/notifications.js | 18 -- templates/nuxt/stores/foo/create.ts | 48 +++ templates/nuxt/stores/foo/delete.ts | 36 +++ templates/nuxt/stores/foo/list.ts | 71 +++++ templates/nuxt/stores/foo/show.ts | 46 +++ templates/nuxt/stores/foo/update.ts | 75 +++++ templates/nuxt/types/api.ts | 32 ++ templates/nuxt/utils/resource.ts | 11 + .../components/common/CommonFormRepeater.vue | 2 +- templates/quasar/components/foo/FooForm.vue | 2 +- .../vue/components/common/FormRepeater.vue | 2 +- testapp.sh | 22 +- tests/show.spec.ts | 2 + 51 files changed, 1701 insertions(+), 1886 deletions(-) delete mode 100644 templates/nuxt/components/ActionCell.vue delete mode 100644 templates/nuxt/components/Alert.vue delete mode 100644 templates/nuxt/components/ConfirmDelete.vue delete mode 100644 templates/nuxt/components/DataFilter.vue delete mode 100644 templates/nuxt/components/InputDate.vue delete mode 100644 templates/nuxt/components/Loading.vue delete mode 100644 templates/nuxt/components/Toolbar.vue create mode 100644 templates/nuxt/components/common/FormRepeater.vue delete mode 100644 templates/nuxt/components/foo/Filter.vue create mode 100644 templates/nuxt/components/foo/FooCreate.vue create mode 100644 templates/nuxt/components/foo/FooForm.vue create mode 100644 templates/nuxt/components/foo/FooList.vue create mode 100644 templates/nuxt/components/foo/FooShow.vue create mode 100644 templates/nuxt/components/foo/FooUpdate.vue delete mode 100644 templates/nuxt/components/foo/Form.vue create mode 100644 templates/nuxt/composables/api.ts delete mode 100644 templates/nuxt/mixins/create.js delete mode 100644 templates/nuxt/mixins/list.js delete mode 100644 templates/nuxt/mixins/notification.js delete mode 100644 templates/nuxt/mixins/show.js delete mode 100644 templates/nuxt/mixins/update.js create mode 100644 templates/nuxt/nuxt.config.ts create mode 100644 templates/nuxt/pages/foos/[id]/edit.vue create mode 100644 templates/nuxt/pages/foos/[id]/index.vue delete mode 100644 templates/nuxt/pages/foos/_id/edit.vue delete mode 100644 templates/nuxt/pages/foos/_id/index.vue create mode 100644 templates/nuxt/pages/foos/page/[page].vue create mode 100644 templates/nuxt/pages/index.vue delete mode 100644 templates/nuxt/store/crud.js delete mode 100644 templates/nuxt/store/foo.js delete mode 100644 templates/nuxt/store/notifications.js create mode 100644 templates/nuxt/stores/foo/create.ts create mode 100644 templates/nuxt/stores/foo/delete.ts create mode 100644 templates/nuxt/stores/foo/list.ts create mode 100644 templates/nuxt/stores/foo/show.ts create mode 100644 templates/nuxt/stores/foo/update.ts create mode 100644 templates/nuxt/types/api.ts create mode 100644 templates/nuxt/utils/resource.ts diff --git a/src/generators/NuxtGenerator.js b/src/generators/NuxtGenerator.js index e2096df1..b8d64fd6 100644 --- a/src/generators/NuxtGenerator.js +++ b/src/generators/NuxtGenerator.js @@ -1,40 +1,74 @@ +import handlebars from "handlebars"; +import hbh_comparison from "handlebars-helpers/lib/comparison.js"; +import hbh_array from "handlebars-helpers/lib/array.js"; +import hbh_string from "handlebars-helpers/lib/string.js"; import chalk from "chalk"; -import BaseVueGenerator from "./VueBaseGenerator.js"; +import BaseGenerator from "./BaseGenerator.js"; -export default class NuxtGenerator extends BaseVueGenerator { +export default class NuxtGenerator extends BaseGenerator { constructor(params) { super(params); + this.registerTemplates("common/", [ + // types + "types/collection.ts", + "types/error.ts", + "types/foo.ts", + "types/item.ts", + "types/view.ts", + + // utils + "utils/config.ts", + "utils/date.ts", + "utils/error.ts", + "utils/mercure.ts", + ]); + + this.registerTemplates("vue-common/", [ + // composables + "composables/mercureItem.ts", + "composables/mercureList.ts", + ]); + this.registerTemplates(`nuxt/`, [ + // common components + "components/common/FormRepeater.vue", + // components - "components/ActionCell.vue", - "components/Alert.vue", - "components/ConfirmDelete.vue", - "components/DataFilter.vue", - "components/InputDate.vue", - "components/Loading.vue", - "components/Toolbar.vue", - "components/foo/Filter.vue", - "components/foo/Form.vue", - - // mixins - "mixins/create.js", - "mixins/list.js", - "mixins/notification.js", - "mixins/show.js", - "mixins/update.js", + "components/foo/FooCreate.vue", + "components/foo/FooForm.vue", + "components/foo/FooList.vue", + "components/foo/FooShow.vue", + "components/foo/FooUpdate.vue", + + // composables + "composables/api.ts", // pages + "pages/index.vue", "pages/foos/create.vue", "pages/foos/index.vue", - "pages/foos/_id/edit.vue", - "pages/foos/_id/index.vue", - - // store - "store/crud.js", - "store/notifications.js", - "store/foo.js", + "pages/foos/[id]/edit.vue", + "pages/foos/[id]/index.vue", + "pages/foos/page/[page].vue", + + // stores + "stores/foo/create.ts", + "stores/foo/delete.ts", + "stores/foo/list.ts", + "stores/foo/show.ts", + "stores/foo/update.ts", + + // types + "types/api.ts", + + // utils + "utils/resource.ts", ]); + + handlebars.registerHelper("compare", hbh_comparison.compare); + handlebars.registerHelper("forEach", hbh_array.forEach); + handlebars.registerHelper("lowercase", hbh_string.lowercase); } help(resource) { @@ -44,115 +78,144 @@ export default class NuxtGenerator extends BaseVueGenerator { ); } - generateFiles(api, resource, dir, params) { - const context = super.getContextForResource(resource, params); - const lc = context.lc; - - [ - `${dir}/config`, - `${dir}/error`, - `${dir}/mixins`, - `${dir}/services`, - `${dir}/store`, - `${dir}/utils`, - `${dir}/validators`, - ].forEach((dir) => this.createDir(dir, false)); - - // error - this.createFile( - "error/SubmissionError.js", - `${dir}/error/SubmissionError.js`, - {}, - false - ); - - // mixins - [ - "mixins/create.js", - "mixins/list.js", - "mixins/notification.js", - "mixins/show.js", - "mixins/update.js", - ].forEach((file) => - this.createFile(file, `${dir}/${file}`, context, false) - ); - - // stores - this.createFile( - `store/modules/notifications.js`, - `${dir}/store/notifications.js`, - { hydraPrefix: this.hydraPrefix }, - false - ); - - this.createFile( - `store/crud.js`, - `${dir}/store/crud.js`, - { hydraPrefix: this.hydraPrefix }, - false - ); - - // validators - this.createFile( - "validators/date.js", - `${dir}/validators/date.js`, - { hydraPrefix: this.hydraPrefix }, - false - ); - - // utils - ["dates.js", "fetch.js", "hydra.js"].forEach((file) => - this.createFile(`utils/${file}`, `${dir}/utils/${file}`, {}, false) - ); + getContextForResource(resource) { + const lc = resource.title.toLowerCase(); + const titleUcFirst = + resource.title.charAt(0).toUpperCase() + resource.title.slice(1); + const fields = this.parseFields(resource); + const hasIsRelation = fields.some((field) => field.isRelation); + const hasIsRelations = fields.some((field) => field.isRelations); + const hasRelations = hasIsRelation || hasIsRelations; + + const formFields = this.buildFields(fields); + + return { + title: resource.title, + name: resource.name, + lc, + uc: resource.title.toUpperCase(), + fields, + hasIsRelation, + hasIsRelations, + hasRelations, + formFields, + hydraPrefix: this.hydraPrefix, + titleUcFirst, + }; + } - this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.js`); + generate(api, resource, dir) { + const context = this.getContextForResource(resource); + const { lc, titleUcFirst } = context; [ + `${dir}/assets`, + `${dir}/assets/css`, + `${dir}/components`, + `${dir}/components/common`, `${dir}/components/${lc}`, + `${dir}/composables`, + `${dir}/pages`, `${dir}/pages/${lc}s`, - `${dir}/pages/${lc}s/_id`, - ].forEach((dir) => { - this.createDir(dir); - }); - - this.createFile("services/api.js", `${dir}/services/api.js`, {}, false); + `${dir}/pages/${lc}s/[id]`, + `${dir}/pages/${lc}s/page`, + `${dir}/stores`, + `${dir}/stores/${lc}`, + `${dir}/types`, + `${dir}/utils`, + ].forEach((dir) => this.createDir(dir, false)); [ // components - "components/%s/Filter.vue", - "components/%s/Form.vue", + "components/%s/%sCreate.vue", + "components/%s/%sForm.vue", + "components/%s/%sList.vue", + "components/%s/%sShow.vue", + "components/%s/%sUpdate.vue", // pages "pages/%ss/create.vue", "pages/%ss/index.vue", - "pages/%ss/_id/edit.vue", - "pages/%ss/_id/index.vue", - - // service - "services/%s.js", - - // store - "store/%s.js", + "pages/%ss/[id]/edit.vue", + "pages/%ss/[id]/index.vue", + "pages/%ss/page/[page].vue", + + // stores + "stores/%s/create.ts", + "stores/%s/delete.ts", + "stores/%s/list.ts", + "stores/%s/show.ts", + "stores/%s/update.ts", + + // types + "types/%s.ts", ].forEach((pattern) => - this.createFileFromPattern(pattern, dir, [lc], context) + this.createFileFromPattern(pattern, dir, [lc, titleUcFirst], context) ); - // components [ - "ActionCell.vue", - "Alert.vue", - "ConfirmDelete.vue", - "DataFilter.vue", - "InputDate.vue", - "Loading.vue", - "Toolbar.vue", - ].forEach((file) => - this.createFile( - `components/${file}`, - `${dir}/components/${file}`, - context, - false - ) + // components + "components/common/FormRepeater.vue", + + // composables + "composables/api.ts", + "composables/mercureItem.ts", + "composables/mercureList.ts", + + // pages + "pages/index.vue", + + // types + "types/api.ts", + "types/collection.ts", + "types/error.ts", + "types/item.ts", + "types/view.ts", + + // utils + "utils/date.ts", + "utils/error.ts", + "utils/mercure.ts", + + // utils + "utils/resource.ts", + ].forEach((path) => + this.createFile(path, `${dir}/${path}`, context, false) ); + + // config + this.createConfigFile(`${dir}/utils/config.ts`, { + entrypoint: api.entrypoint, + }); + } + + parseFields(resource) { + const fields = [ + ...resource.writableFields, + ...resource.readableFields, + ].reduce((list, field) => { + if (list[field.name]) { + return list; + } + + const isReferences = Boolean( + field.reference && field.maxCardinality !== 1 + ); + const isEmbeddeds = Boolean(field.embedded && field.maxCardinality !== 1); + + return { + ...list, + [field.name]: { + ...field, + readonly: false, + isReferences, + isEmbeddeds, + isRelation: field.reference || field.embedded, + isRelations: isEmbeddeds || isReferences, + }, + }; + }, {}); + + return Object.values(fields); } } diff --git a/src/generators/NuxtGenerator.test.js b/src/generators/NuxtGenerator.test.js index 8fb84b56..496dc66b 100644 --- a/src/generators/NuxtGenerator.test.js +++ b/src/generators/NuxtGenerator.test.js @@ -7,68 +7,85 @@ import NuxtGenerator from "./NuxtGenerator.js"; const dirname = path.dirname(fileURLToPath(import.meta.url)); -const generator = new NuxtGenerator({ - hydraPrefix: "hydra:", - templateDirectory: `${dirname}/../../templates`, -}); +test("Generate a Nuxt app", () => { + const generator = new NuxtGenerator({ + hydraPrefix: "hydra:", + templateDirectory: `${dirname}/../../templates`, + }); + const tmpobj = tmp.dirSync({ unsafeCleanup: true }); -afterEach(() => { - jest.resetAllMocks(); -}); + const fields = [ + new Field("bar", { + id: "http://schema.org/url", + range: "http://www.w3.org/2001/XMLSchema#string", + reference: null, + required: true, + description: "An URL", + type: "string", + }), + ]; + const resource = new Resource("abc", "http://example.com/foos", { + id: "foo", + title: "Foo", + readableFields: fields, + writableFields: fields, + }); + const api = new Api("http://example.com", { + entrypoint: "http://example.com:8080", + title: "My API", + resources: [resource], + }); -describe("generate", () => { - test("Generate a Nuxt app", () => { - const tmpobj = tmp.dirSync({ unsafeCleanup: true }); + generator.generate(api, resource, tmpobj.name); - const fields = [ - new Field("bar", { - id: "http://schema.org/url", - range: "http://www.w3.org/2001/XMLSchema#string", - reference: null, - required: true, - description: "An URL", - }), - ]; - const resource = new Resource("prefix/aBe_cd", "http://example.com/foos", { - id: "foo", - title: "Foo", - readableFields: fields, - writableFields: fields, - getParameters: function getParameters() { - return Promise.resolve([]); - }, - }); - const api = new Api("http://example.com", { - entrypoint: "http://example.com:8080", - title: "My API", - resources: [resource], - }); + // common components + expect( + fs.existsSync(`${tmpobj.name}/components/common/FormRepeater.vue`) + ).toBe(true); - generator.generate(api, resource, tmpobj.name).then(() => { - [ - "/components/foo/Form.vue", - "/components/InputDate.vue", - "/components/Loading.vue", - "/components/Alert.vue", - "/components/Toolbar.vue", - "/config/entrypoint.js", - "/error/SubmissionError.js", - "/services/api.js", - "/services/foo.js", - "/store/foo.js", - "/store/notifications.js", - "/utils/dates.js", - "/utils/fetch.js", - "/utils/hydra.js", - "/pages/foos/_id/edit.vue", - "/pages/foos/_id/index.vue", - "/pages/foos/index.vue", - "/pages/foos/create.vue", - ].forEach((file) => { - expect(fs.existsSync(tmpobj.name + file)).toBe(true); - }); + // components + expect(fs.existsSync(`${tmpobj.name}/components/foo/FooCreate.vue`)).toBe( + true + ); + expect(fs.existsSync(`${tmpobj.name}/components/foo/FooForm.vue`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/components/foo/FooList.vue`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/components/foo/FooShow.vue`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/components/foo/FooUpdate.vue`)).toBe( + true + ); - tmpobj.removeCallback(); - }); - }); + // composables + expect(fs.existsSync(`${tmpobj.name}/composables/api.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/composables/mercureItem.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/composables/mercureList.ts`)).toBe(true); + + // pages + expect(fs.existsSync(`${tmpobj.name}/pages/foos/[id]/edit.vue`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/pages/foos/[id]/index.vue`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/pages/foos/create.vue`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/pages/foos/index.vue`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/pages/index.vue`)).toBe(true); + + // stores + expect(fs.existsSync(`${tmpobj.name}/stores/foo/create.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/stores/foo/delete.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/stores/foo/list.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/stores/foo/show.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/stores/foo/update.ts`)).toBe(true); + + // types + expect(fs.existsSync(`${tmpobj.name}/types/api.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/types/collection.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/types/error.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/types/foo.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/types/item.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/types/view.ts`)).toBe(true); + + // utils + expect(fs.existsSync(`${tmpobj.name}/utils/config.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/utils/date.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/utils/error.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/utils/mercure.ts`)).toBe(true); + + tmpobj.removeCallback(); }); diff --git a/templates/common/tailwind.config.js b/templates/common/tailwind.config.js index b523189e..5035b7a9 100644 --- a/templates/common/tailwind.config.js +++ b/templates/common/tailwind.config.js @@ -7,9 +7,15 @@ module.exports = { // Vue "./index.html", "./src/**/*.{vue,ts}", + // Nuxt + "./components/**/*.{vue,ts}", + "./layouts/**/*.vue", + "./pages/**/*.vue", + "./plugins/**/*.ts", + "./nuxt.config.ts", ], theme: { extend: {}, }, plugins: [], -} +}; diff --git a/templates/common/types/collection.ts b/templates/common/types/collection.ts index f710e6fd..b5245962 100644 --- a/templates/common/types/collection.ts +++ b/templates/common/types/collection.ts @@ -1,11 +1,11 @@ -import type { View } from './view'; +import type { View } from "./view"; export interface PagedCollection { - '@context'?: string; - '@id'?: string; - '@type'?: string; - '{{hydraPrefix}}member': T[]; - '{{hydraPrefix}}search'?: object; - '{{hydraPrefix}}totalItems'?: number; - '{{hydraPrefix}}view': View; + "@context"?: string; + "@id"?: string; + "@type"?: string; + "{{hydraPrefix}}member": T[]; + "{{hydraPrefix}}search"?: object; + "{{hydraPrefix}}totalItems"?: number; + "{{hydraPrefix}}view": View; } diff --git a/templates/common/types/item.ts b/templates/common/types/item.ts index 766cc0f1..f8d95cdc 100644 --- a/templates/common/types/item.ts +++ b/templates/common/types/item.ts @@ -1,3 +1,3 @@ export interface Item { - '@id'?: string; + "@id"?: string; } diff --git a/templates/common/types/view.ts b/templates/common/types/view.ts index 19cd75d1..9a6bb10a 100644 --- a/templates/common/types/view.ts +++ b/templates/common/types/view.ts @@ -1,7 +1,7 @@ export interface View { - '@id': string; - '{{hydraPrefix}}first': string; - '{{hydraPrefix}}last': string; - '{{hydraPrefix}}next': string; - '{{hydraPrefix}}previous': string; + "@id": string; + "{{hydraPrefix}}first": string; + "{{hydraPrefix}}last": string; + "{{hydraPrefix}}next": string; + "{{hydraPrefix}}previous": string; } diff --git a/templates/nuxt/components/ActionCell.vue b/templates/nuxt/components/ActionCell.vue deleted file mode 100644 index 289c3a01..00000000 --- a/templates/nuxt/components/ActionCell.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - diff --git a/templates/nuxt/components/Alert.vue b/templates/nuxt/components/Alert.vue deleted file mode 100644 index 3e9c8ea8..00000000 --- a/templates/nuxt/components/Alert.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - diff --git a/templates/nuxt/components/ConfirmDelete.vue b/templates/nuxt/components/ConfirmDelete.vue deleted file mode 100644 index ca2038c1..00000000 --- a/templates/nuxt/components/ConfirmDelete.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/templates/nuxt/components/DataFilter.vue b/templates/nuxt/components/DataFilter.vue deleted file mode 100644 index 12041848..00000000 --- a/templates/nuxt/components/DataFilter.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - diff --git a/templates/nuxt/components/InputDate.vue b/templates/nuxt/components/InputDate.vue deleted file mode 100644 index 6fd0cc67..00000000 --- a/templates/nuxt/components/InputDate.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - diff --git a/templates/nuxt/components/Loading.vue b/templates/nuxt/components/Loading.vue deleted file mode 100644 index bceef719..00000000 --- a/templates/nuxt/components/Loading.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/templates/nuxt/components/Toolbar.vue b/templates/nuxt/components/Toolbar.vue deleted file mode 100644 index 9c8d558b..00000000 --- a/templates/nuxt/components/Toolbar.vue +++ /dev/null @@ -1,123 +0,0 @@ - - - diff --git a/templates/nuxt/components/common/FormRepeater.vue b/templates/nuxt/components/common/FormRepeater.vue new file mode 100644 index 00000000..2db8028f --- /dev/null +++ b/templates/nuxt/components/common/FormRepeater.vue @@ -0,0 +1,67 @@ + + + diff --git a/templates/nuxt/components/foo/Filter.vue b/templates/nuxt/components/foo/Filter.vue deleted file mode 100644 index 8061e851..00000000 --- a/templates/nuxt/components/foo/Filter.vue +++ /dev/null @@ -1,136 +0,0 @@ - - - diff --git a/templates/nuxt/components/foo/FooCreate.vue b/templates/nuxt/components/foo/FooCreate.vue new file mode 100644 index 00000000..2f3f9ef7 --- /dev/null +++ b/templates/nuxt/components/foo/FooCreate.vue @@ -0,0 +1,52 @@ + + + diff --git a/templates/nuxt/components/foo/FooForm.vue b/templates/nuxt/components/foo/FooForm.vue new file mode 100644 index 00000000..f9f809de --- /dev/null +++ b/templates/nuxt/components/foo/FooForm.vue @@ -0,0 +1,97 @@ + + + diff --git a/templates/nuxt/components/foo/FooList.vue b/templates/nuxt/components/foo/FooList.vue new file mode 100644 index 00000000..f3137907 --- /dev/null +++ b/templates/nuxt/components/foo/FooList.vue @@ -0,0 +1,281 @@ + + + diff --git a/templates/nuxt/components/foo/FooShow.vue b/templates/nuxt/components/foo/FooShow.vue new file mode 100644 index 00000000..65627f41 --- /dev/null +++ b/templates/nuxt/components/foo/FooShow.vue @@ -0,0 +1,206 @@ + + + diff --git a/templates/nuxt/components/foo/FooUpdate.vue b/templates/nuxt/components/foo/FooUpdate.vue new file mode 100644 index 00000000..94e66c54 --- /dev/null +++ b/templates/nuxt/components/foo/FooUpdate.vue @@ -0,0 +1,136 @@ + + + diff --git a/templates/nuxt/components/foo/Form.vue b/templates/nuxt/components/foo/Form.vue deleted file mode 100644 index 854d6d57..00000000 --- a/templates/nuxt/components/foo/Form.vue +++ /dev/null @@ -1,221 +0,0 @@ - - - diff --git a/templates/nuxt/composables/api.ts b/templates/nuxt/composables/api.ts new file mode 100644 index 00000000..84bac452 --- /dev/null +++ b/templates/nuxt/composables/api.ts @@ -0,0 +1,181 @@ +import { PagedCollection } from "~~/types/collection"; +import { FetchAllData, FetchItemData } from "~~/types/api"; +import { Ref } from "vue"; +import { View } from "~~/types/view"; +import { UseFetchOptions } from "#app"; +import { SubmissionErrors } from "~~/types/error"; +import { Item } from "~~/types/item"; + +const MIME_TYPE = 'application/ld+json'; + +async function useApi(path: string, options: UseFetchOptions) { + const response = await useFetch(path, { + baseURL: ENTRYPOINT, + + mode: "cors", + + headers: { + Accept: MIME_TYPE, + }, + + onResponseError({ response }) { + const data = response._data; + const error = data["hydra:description"] || response.statusText; + + throw new Error(error); + }, + + ...options, + }); + + return response; +} + +export async function useFetchList( + resource: string +): Promise> { + const route = useRoute(); + + const items: Ref = ref([]); + const view: Ref = ref(undefined); + const hubUrl: Ref = ref(undefined); + + const page = ref(route.params.page); + + const { data, pending, error } = await useApi(resource, { + params: { page }, + + onResponse({ response }) { + hubUrl.value = extractHubURL(response); + }, + }); + + const value = data.value as PagedCollection; + items.value = value["hydra:member"]; + view.value = value["hydra:view"]; + + return { + items, + view, + isLoading: pending, + error, + hubUrl, + }; +} + +export async function useFetchItem(path: string): Promise> { + const retrieved: Ref = ref(undefined); + const hubUrl: Ref = ref(undefined); + + const { data, pending, error } = await useApi(path, { + onResponse({ response }) { + retrieved.value = response._data; + hubUrl.value = extractHubURL(response); + }, + }); + + retrieved.value = data.value as T; + + return { + retrieved, + isLoading: pending, + error, + hubUrl, + }; +} + +export async function useCreateItem(resource: string, payload: Item) { + const created: Ref = ref(undefined); + const violations: Ref = ref(undefined); + + const { data, pending, error } = await useApi(resource, { + method: "POST", + body: payload, + + onResponseError({ response }) { + const data = response._data; + const error = data["hydra:description"] || response.statusText; + + if (!data.violations) throw new Error(error); + + const errors: SubmissionErrors = { _error: error }; + data.violations.forEach( + (violation: { propertyPath: string; message: string }) => { + errors[violation.propertyPath] = violation.message; + } + ); + + violations.value = errors; + + throw new SubmissionError(errors); + }, + }); + + created.value = data.value as T; + + return { + created, + isLoading: pending, + error, + violations, + }; +} + +export async function useUpdateItem(item: Item, payload: Item) { + const updated: Ref = ref(undefined); + const violations: Ref = ref(undefined); + + const { data, pending, error } = await useApi(item["@id"] ?? "", { + method: "PUT", + body: payload, + headers: { + Accept: MIME_TYPE, + "Content-Type": MIME_TYPE, + }, + + onResponseError({ response }) { + const data = response._data; + const error = data["hydra:description"] || response.statusText; + + if (!data.violations) throw new Error(error); + + const errors: SubmissionErrors = { _error: error }; + data.violations.forEach( + (violation: { propertyPath: string; message: string }) => { + errors[violation.propertyPath] = violation.message; + } + ); + + violations.value = errors; + + throw new SubmissionError(errors); + }, + }); + + updated.value = data.value as T; + + return { + updated, + isLoading: pending, + error, + violations, + }; +} + +export async function useDeleteItem(item: Item) { + const error: Ref = ref(undefined); + + if (!item?.["@id"]) { + error.value = "No item found. Please reload"; + return { + error, + }; + } + + const { pending } = await useApi(item["@id"] ?? "", { method: "DELETE" }); + + return { + isLoading: pending, + error, + }; +} diff --git a/templates/nuxt/mixins/create.js b/templates/nuxt/mixins/create.js deleted file mode 100644 index 356e7861..00000000 --- a/templates/nuxt/mixins/create.js +++ /dev/null @@ -1,39 +0,0 @@ -import notification from './notification'; -import { formatDateTime } from '../utils/dates'; -import { getPath } from '../utils/fetch'; - -export default { - mixins: [notification], - methods: { - formatDateTime, - onCreated(item) { - this.showMessage(`${item['@id']} created`); - - this.$router.push(getPath(item['@id'], this.$options.pathTemplate)); - }, - onSendForm() { - const createForm = this.$refs.createForm; - createForm.$v.$touch(); - if (!createForm.$v.$invalid) { - this.create(createForm.$v.item.$model); - } - }, - resetForm() { - this.$refs.createForm.$v.$reset(); - this.item = {}; - } - }, - watch: { - created(created) { - if (!created) { - return; - } - - this.onCreated(created); - }, - - error(message) { - message && this.showError(message); - } - } -}; diff --git a/templates/nuxt/mixins/list.js b/templates/nuxt/mixins/list.js deleted file mode 100644 index bc21f20b..00000000 --- a/templates/nuxt/mixins/list.js +++ /dev/null @@ -1,78 +0,0 @@ -import isEmpty from 'lodash/isEmpty'; -import { formatDateTime } from '../utils/dates'; -import notification from './notification'; - -export default { - mixins: [notification], - - async fetch({ store }) { - await store.dispatch('{{{lc}}}/fetchAll') - }, - - data() { - return { - options: { - sortBy: [], - sortDesc: [], - page: 1, - itemsPerPage: 30 - }, - filters: {} - }; - }, - - watch: { - deletedItem(item) { - this.showMessage(`${item['@id']} deleted.`); - }, - - error(message) { - message && this.showError(message); - }, - - items() { - this.options.totalItems = this.totalItems; - } - }, - - methods: { - onUpdateOptions({ page, itemsPerPage, sortBy, sortDesc, totalItems } = {}) { - let params = { - ...this.filters - }; - if (itemsPerPage > 0) { - params = { ...params, itemsPerPage, page }; - } - - if (!isEmpty(sortBy) && !isEmpty(sortDesc)) { - params[`order[${sortBy[0]}]`] = sortDesc[0] ? 'desc' : 'asc' - } - - this.resetList = true; - - Object.assign(this.options, { - sortBy, - sortDesc, - itemsPerPage, - totalItems - }); - - this.fetchAll(params); - }, - - onSendFilter() { - this.resetList = true; - this.onUpdateOptions(this.options); - }, - - resetFilter() { - this.filters = {}; - this.onSendFilter(); - }, - - deleteHandler(item) { - this.deleteItem(item).then(() => this.onUpdateOptions(this.options)); - }, - formatDateTime - } -}; diff --git a/templates/nuxt/mixins/notification.js b/templates/nuxt/mixins/notification.js deleted file mode 100644 index 9ad61347..00000000 --- a/templates/nuxt/mixins/notification.js +++ /dev/null @@ -1,37 +0,0 @@ -import { mapFields } from 'vuex-map-fields'; - -export default { - computed: { - ...mapFields('notifications', ['color', 'show', 'subText', 'text', 'timeout']) - }, - - methods: { - cleanState() { - setTimeout(() => { - this.show = false; - }, this.timeout); - }, - - showError(error) { - this.showMessage(error, 'danger'); - }, - - showMessage(message, color = 'success') { - this.show = true; - this.color = color; - - if (typeof message === 'string') { - this.text = message; - this.cleanState(); - - return; - } - - this.text = message.message; - - if (message.response) this.subText = message.response.data.message; - - this.cleanState(); - } - } -}; diff --git a/templates/nuxt/mixins/show.js b/templates/nuxt/mixins/show.js deleted file mode 100644 index 5739742a..00000000 --- a/templates/nuxt/mixins/show.js +++ /dev/null @@ -1,37 +0,0 @@ -import notification from './notification'; -import { formatDateTime } from '../utils/dates'; - -export default { - mixins: [notification], - created() { - this.retrieve(`/${this.$options.name}/${this.$route.params.id}`); - }, - beforeDestroy() { - this.reset(); - }, - computed: { - item() { - return this.find(`/${this.$options.name}/${this.$route.params.id}`); - } - }, - methods: { - del() { - this.deleteItem(this.item).then(() => { - this.showMessage(`${this.deleted['@id']} deleted.`); - this.$router.push(`/${this.$options.servicePrefix}`) - }); - }, - formatDateTime, - reset() { - this.delReset(); - }, - }, - watch: { - error(message) { - message && this.showError(message); - }, - deleteError(message) { - message && this.showError(message); - } - } -}; diff --git a/templates/nuxt/mixins/update.js b/templates/nuxt/mixins/update.js deleted file mode 100644 index cf024c3a..00000000 --- a/templates/nuxt/mixins/update.js +++ /dev/null @@ -1,75 +0,0 @@ -import notification from './notification'; -import { formatDateTime } from '../utils/dates'; - -export default { - mixins: [notification], - data() { - return { - item: {} - }; - }, - created() { - this.retrieve(`/${this.$options.name}/${this.$route.params.id}`); - }, - beforeDestroy() { - this.reset(); - }, - computed: { - retrieved() { - return this.find(`/${this.$options.name}/${this.$route.params.id}`); - } - }, - methods: { - del() { - this.deleteItem(this.retrieved).then(() => { - this.showMessage(`${this.deleted['@id']} deleted.`); - this.$router.push(`/${this.$options.servicePrefix}`) - }); - }, - formatDateTime, - reset() { - this.$refs.updateForm.$v.$reset(); - this.updateReset(); - this.delReset(); - this.createReset(); - }, - - onSendForm() { - const updateForm = this.$refs.updateForm; - updateForm.$v.$touch(); - - if (!updateForm.$v.$invalid) { - this.update(updateForm.$v.item.$model); - } - }, - - resetForm() { - this.$refs.updateForm.$v.$reset(); - this.item = { ...this.retrieved }; - } - }, - watch: { - deleted(deleted) { - if (!deleted) { - return; - } - this.$router.push(`/${this.$options.servicePrefix}`) - }, - - error(message) { - message && this.showError(message); - }, - - deleteError(message) { - message && this.showError(message); - }, - - updated(val) { - this.showMessage(`${val['@id']} updated.`); - }, - - retrieved(val) { - this.item = { ...val }; - } - } -}; diff --git a/templates/nuxt/nuxt.config.ts b/templates/nuxt/nuxt.config.ts new file mode 100644 index 00000000..639a7749 --- /dev/null +++ b/templates/nuxt/nuxt.config.ts @@ -0,0 +1,21 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + modules: ["@pinia/nuxt"], + css: ["~/assets/css/style.css"], + postcss: { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, + }, + // Waiting for https://github.com/unjs/nitro/issues/603 to enable SSR (SWR). + ssr: false, + routeRules: { + "/**": { swr: 1 } + }, + nitro: { + commands: { + preview: 'npx serve ./public' + } + } +}) diff --git a/templates/nuxt/pages/foos/[id]/edit.vue b/templates/nuxt/pages/foos/[id]/edit.vue new file mode 100644 index 00000000..3f13a87f --- /dev/null +++ b/templates/nuxt/pages/foos/[id]/edit.vue @@ -0,0 +1,9 @@ + + + diff --git a/templates/nuxt/pages/foos/[id]/index.vue b/templates/nuxt/pages/foos/[id]/index.vue new file mode 100644 index 00000000..7c81ef7e --- /dev/null +++ b/templates/nuxt/pages/foos/[id]/index.vue @@ -0,0 +1,9 @@ + + + diff --git a/templates/nuxt/pages/foos/_id/edit.vue b/templates/nuxt/pages/foos/_id/edit.vue deleted file mode 100644 index 6075c847..00000000 --- a/templates/nuxt/pages/foos/_id/edit.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - - diff --git a/templates/nuxt/pages/foos/_id/index.vue b/templates/nuxt/pages/foos/_id/index.vue deleted file mode 100644 index 83c8ffc6..00000000 --- a/templates/nuxt/pages/foos/_id/index.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - - diff --git a/templates/nuxt/pages/foos/create.vue b/templates/nuxt/pages/foos/create.vue index cdbac200..826658fc 100644 --- a/templates/nuxt/pages/foos/create.vue +++ b/templates/nuxt/pages/foos/create.vue @@ -1,46 +1,9 @@ - diff --git a/templates/nuxt/pages/foos/index.vue b/templates/nuxt/pages/foos/index.vue index eb7fa3ba..43d56525 100644 --- a/templates/nuxt/pages/foos/index.vue +++ b/templates/nuxt/pages/foos/index.vue @@ -1,182 +1,9 @@ - diff --git a/templates/nuxt/pages/foos/page/[page].vue b/templates/nuxt/pages/foos/page/[page].vue new file mode 100644 index 00000000..43d56525 --- /dev/null +++ b/templates/nuxt/pages/foos/page/[page].vue @@ -0,0 +1,9 @@ + + + diff --git a/templates/nuxt/pages/index.vue b/templates/nuxt/pages/index.vue new file mode 100644 index 00000000..a495b757 --- /dev/null +++ b/templates/nuxt/pages/index.vue @@ -0,0 +1,5 @@ + diff --git a/templates/nuxt/store/crud.js b/templates/nuxt/store/crud.js deleted file mode 100644 index 0f310927..00000000 --- a/templates/nuxt/store/crud.js +++ /dev/null @@ -1,273 +0,0 @@ -import Vue from 'vue'; -import { getField, updateField } from 'vuex-map-fields'; -import remove from 'lodash/remove'; -import SubmissionError from '../error/SubmissionError'; - -const initialState = () => ({ - allIds: [], - byId: {}, - created: null, - deleted: null, - error: "", - isLoading: false, - resetList: false, - selectItems: null, - totalItems: 0, - updated: null, - view: null, - violations: null -}); - -const handleError = (commit, e) => { - commit(ACTIONS.TOGGLE_LOADING); - - if (e instanceof SubmissionError) { - commit(ACTIONS.SET_VIOLATIONS, e.errors); - // eslint-disable-next-line - commit(ACTIONS.SET_ERROR, e.errors._error); - - return Promise.reject(e); - } - - // eslint-disable-next-line - commit(ACTIONS.SET_ERROR, e.message); - - return Promise.reject(e); -}; - -export const ACTIONS = { - ADD: 'ADD', - RESET_CREATE: 'RESET_CREATE', - RESET_DELETE: 'RESET_DELETE', - RESET_LIST: 'RESET_LIST', - RESET_SHOW: 'RESET_SHOW', - RESET_UPDATE: 'RESET_UPDATE', - SET_CREATED: 'SET_CREATED', - SET_DELETED: 'SET_DELETED', - SET_ERROR: 'SET_ERROR', - SET_SELECT_ITEMS: 'SET_SELECT_ITEMS', - SET_TOTAL_ITEMS: 'SET_TOTAL_ITEMS', - SET_UPDATED: 'SET_UPDATED', - SET_VIEW: 'SET_VIEW', - SET_VIOLATIONS: 'SET_VIOLATIONS', - TOGGLE_LOADING: 'TOGGLE_LOADING' -}; - -export default function makeCrudModule({ - normalizeRelations = x => x, - resolveRelations = x => x, - service -} = {}) { - return { - actions: { - create: ({ commit }, values) => { - commit(ACTIONS.SET_ERROR, ''); - commit(ACTIONS.TOGGLE_LOADING); - - return service - .create(values) - .then(response => response.json()) - .then(data => { - commit(ACTIONS.TOGGLE_LOADING); - commit(ACTIONS.ADD, data); - commit(ACTIONS.SET_CREATED, data); - }) - .catch(e => handleError(commit, e)); - }, - del: ({ commit }, item) => { - commit(ACTIONS.TOGGLE_LOADING); - - return service - .del(item) - .then(() => { - commit(ACTIONS.TOGGLE_LOADING); - commit(ACTIONS.SET_DELETED, item); - }) - .catch(e => handleError(commit, e)); - }, - fetchAll: ({ commit, state }, params) => { - if (!service) throw new Error('No service specified!'); - - commit(ACTIONS.TOGGLE_LOADING); - - return service - .findAll({ params }) - .then(response => response.json()) - .then(retrieved => { - commit(ACTIONS.TOGGLE_LOADING); - - commit( - ACTIONS.SET_TOTAL_ITEMS, - retrieved['{{{hydraPrefix}}}totalItems'] - ); - commit(ACTIONS.SET_VIEW, retrieved['{{{hydraPrefix}}}view']); - - if (true === state.resetList) { - commit(ACTIONS.RESET_LIST); - } - - retrieved['{{{hydraPrefix}}}member'].forEach(item => { - commit(ACTIONS.ADD, normalizeRelations(item)); - }); - }) - .catch(e => handleError(commit, e)); - }, - fetchSelectItems: ( - { commit }, - { params = { properties: ['@id', 'name'] } } = {} - ) => { - commit(ACTIONS.TOGGLE_LOADING); - - if (!service) throw new Error('No service specified!'); - - return service - .findAll({ params }) - .then(response => response.json()) - .then(retrieved => { - commit( - ACTIONS.SET_SELECT_ITEMS, - retrieved['{{{hydraPrefix}}}member'] - ); - }) - .catch(e => handleError(commit, e)); - }, - load: ({ commit }, id) => { - if (!service) throw new Error('No service specified!'); - - commit(ACTIONS.TOGGLE_LOADING); - return service - .find(id) - .then(response => response.json()) - .then(item => { - commit(ACTIONS.TOGGLE_LOADING); - commit(ACTIONS.ADD, normalizeRelations(item)); - }) - .catch(e => handleError(commit, e)); - }, - resetCreate: ({ commit }) => { - commit(ACTIONS.RESET_CREATE); - }, - resetDelete: ({ commit }) => { - commit(ACTIONS.RESET_DELETE); - }, - resetShow: ({ commit }) => { - commit(ACTIONS.RESET_SHOW); - }, - resetUpdate: ({ commit }) => { - commit(ACTIONS.RESET_UPDATE); - }, - update: ({ commit }, item) => { - commit(ACTIONS.SET_ERROR, ''); - commit(ACTIONS.TOGGLE_LOADING); - - return service - .update(item) - .then(response => response.json()) - .then(data => { - commit(ACTIONS.TOGGLE_LOADING); - commit(ACTIONS.SET_UPDATED, data); - }) - .catch(e => handleError(commit, e)); - } - }, - getters: { - find: state => id => { - return resolveRelations(state.byId[id]); - }, - getField, - list: (state, getters) => { - return state.allIds.map(id => getters.find(id)); - } - }, - mutations: { - updateField, - [ACTIONS.ADD]: (state, item) => { - Vue.set(state.byId, item['@id'], item); - Vue.set(state, 'isLoading', false); - if (state.allIds.includes(item['@id'])) return; - state.allIds.push(item['@id']); - }, - [ACTIONS.RESET_CREATE]: state => { - Object.assign(state, { - isLoading: false, - error: '', - created: null, - violations: null - }); - }, - [ACTIONS.RESET_DELETE]: state => { - Object.assign(state, { - isLoading: false, - error: '', - deleted: null - }); - }, - [ACTIONS.RESET_LIST]: state => { - Object.assign(state, { - allIds: [], - byId: {}, - error: '', - isLoading: false, - resetList: false - }); - }, - [ACTIONS.RESET_SHOW]: state => { - Object.assign(state, { - error: '', - isLoading: false - }); - }, - [ACTIONS.RESET_UPDATE]: state => { - Object.assign(state, { - error: '', - isLoading: false, - updated: null, - violations: null - }); - }, - [ACTIONS.SET_CREATED]: (state, created) => { - Object.assign(state, { created }); - }, - [ACTIONS.SET_DELETED]: (state, deleted) => { - if (!state.allIds.includes(deleted['@id'])) return; - Object.assign(state, { - allIds: remove(state.allIds, item => item['@id'] === deleted['@id']), - byId: remove(state.byId, id => id === deleted['@id']), - deleted - }); - }, - [ACTIONS.SET_ERROR]: (state, error) => { - Object.assign(state, { error, isLoading: false }); - }, - [ACTIONS.SET_SELECT_ITEMS]: (state, selectItems) => { - Object.assign(state, { - error: '', - isLoading: false, - selectItems - }); - }, - [ACTIONS.SET_TOTAL_ITEMS]: (state, totalItems) => { - Object.assign(state, { totalItems }); - }, - [ACTIONS.SET_UPDATED]: (state, updated) => { - Object.assign(state, { - byId: { - [updated['@id']]: updated - }, - updated - }); - }, - [ACTIONS.SET_VIEW]: (state, view) => { - Object.assign(state, { view }); - }, - [ACTIONS.SET_VIOLATIONS]: (state, violations) => { - Object.assign(state, { violations }); - }, - [ACTIONS.TOGGLE_LOADING]: state => { - Object.assign(state, { error: '', isLoading: !state.isLoading }); - } - }, - namespaced: true, - state: initialState - }; -} diff --git a/templates/nuxt/store/foo.js b/templates/nuxt/store/foo.js deleted file mode 100644 index ef776547..00000000 --- a/templates/nuxt/store/foo.js +++ /dev/null @@ -1,6 +0,0 @@ -import {{{titleUcFirst}}}Service from '../services/{{{lc}}}' -import makeCrudModule from './crud' - -export default makeCrudModule({ - service: {{{titleUcFirst}}}Service -}) diff --git a/templates/nuxt/store/notifications.js b/templates/nuxt/store/notifications.js deleted file mode 100644 index fb70a236..00000000 --- a/templates/nuxt/store/notifications.js +++ /dev/null @@ -1,18 +0,0 @@ -import {getField, updateField} from 'vuex-map-fields'; - -export default { - namespaced: true, - state: () => ({ - show: false, - color: 'error', - text: 'An error occurred', - subText: '', - timeout: 6000 - }), - getters: { - getField - }, - mutations: { - updateField - } -}; diff --git a/templates/nuxt/stores/foo/create.ts b/templates/nuxt/stores/foo/create.ts new file mode 100644 index 00000000..c79cd8cd --- /dev/null +++ b/templates/nuxt/stores/foo/create.ts @@ -0,0 +1,48 @@ +import { defineStore } from "pinia"; +import { {{titleUcFirst}} } from "~~/types/{{lc}}"; +import type { SubmissionErrors } from "~~/types/error"; +import { CreateItemData } from "~~/types/api"; + +interface State { + created?: {{titleUcFirst}}; + isLoading: boolean; + error?: string; + violations?: SubmissionErrors; +} + +export const use{{titleUcFirst}}CreateStore = defineStore("{{lc}}Create", { + state: (): State => ({ + created: undefined, + isLoading: false, + error: undefined, + violations: undefined, + }), + + actions: { + setData({ created, isLoading, error, violations }: CreateItemData<{{titleUcFirst}}>) { + this.setCreated(created.value); + this.setLoading(isLoading.value); + this.setViolations(violations.value); + + if (error.value instanceof Error) { + this.setError(error.value?.message); + } + }, + + setCreated(created?: {{titleUcFirst}}) { + this.created = created; + }, + + setLoading(isLoading: boolean) { + this.isLoading = isLoading; + }, + + setError(error: string | undefined) { + this.error = error; + }, + + setViolations(violations: SubmissionErrors | undefined) { + this.violations = violations; + }, + }, +}); diff --git a/templates/nuxt/stores/foo/delete.ts b/templates/nuxt/stores/foo/delete.ts new file mode 100644 index 00000000..2fff7c1d --- /dev/null +++ b/templates/nuxt/stores/foo/delete.ts @@ -0,0 +1,36 @@ +import { defineStore } from "pinia"; +import type { {{titleUcFirst}} } from "~~/types/{{lc}}"; + +interface State { + deleted?: {{titleUcFirst}}; + mercureDeleted?: {{titleUcFirst}}; + isLoading: boolean; + error?: string; +} + +export const use{{titleUcFirst}}DeleteStore = defineStore("{{lc}}Delete", { + state: (): State => ({ + deleted: undefined, + mercureDeleted: undefined, + isLoading: false, + error: undefined, + }), + + actions: { + setLoading(isLoading: boolean) { + this.isLoading = isLoading; + }, + + setDeleted(deleted: {{titleUcFirst}}) { + this.deleted = deleted; + }, + + setMercureDeleted(mercureDeleted: {{titleUcFirst}} | undefined) { + this.mercureDeleted = mercureDeleted; + }, + + setError(error: string) { + this.error = error; + }, + }, +}); diff --git a/templates/nuxt/stores/foo/list.ts b/templates/nuxt/stores/foo/list.ts new file mode 100644 index 00000000..9bcfdf93 --- /dev/null +++ b/templates/nuxt/stores/foo/list.ts @@ -0,0 +1,71 @@ +import { defineStore } from "pinia"; +import { {{titleUcFirst}} } from "~~/types/{{lc}}"; +import { View } from "~~/types/view"; +import { FetchAllData } from "~~/types/api"; + +interface State { + items: {{titleUcFirst}}[]; + hubUrl?: URL; + isLoading: boolean; + view?: View; + error?: string; +} + +export const use{{titleUcFirst}}ListStore = defineStore("{{lc}}List", { + state: (): State => ({ + items: [], + isLoading: false, + error: undefined, + hubUrl: undefined, + view: undefined, + }), + + actions: { + setData({ items, view, isLoading, error, hubUrl }: FetchAllData<{{titleUcFirst}}>) { + this.setItems(items.value); + this.setLoading(isLoading.value); + if (hubUrl) this.setHubUrl(hubUrl.value); + if (view) this.setView(view.value); + + if (error.value instanceof Error) { + this.setError(error.value?.message); + } + }, + + setLoading(isLoading: boolean) { + this.isLoading = isLoading; + }, + + setItems(items: {{titleUcFirst}}[]) { + this.items = items; + }, + + setHubUrl(hubUrl?: URL) { + this.hubUrl = hubUrl; + }, + + setView(view?: View) { + this.view = view; + }, + + setError(error?: string) { + this.error = error; + }, + + updateItem(updatedItem: {{titleUcFirst}}) { + const item: {{titleUcFirst}} | undefined = this.items.find( + (i) => i["@id"] === updatedItem["@id"] + ); + + if (!item) return; + + Object.assign(item, updatedItem); + }, + + deleteItem(deletedItem: {{titleUcFirst}}) { + this.items = this.items.filter((item) => { + return item["@id"] !== deletedItem["@id"]; + }); + }, + }, +}); diff --git a/templates/nuxt/stores/foo/show.ts b/templates/nuxt/stores/foo/show.ts new file mode 100644 index 00000000..68859aa9 --- /dev/null +++ b/templates/nuxt/stores/foo/show.ts @@ -0,0 +1,46 @@ +import { defineStore } from "pinia"; +import type { {{titleUcFirst}} } from "~~/types/{{lc}}"; +import { FetchItemData } from "~~/types/api"; +interface State { + retrieved?: {{titleUcFirst}}; + isLoading: boolean; + error?: string; + hubUrl?: URL; +} + +export const use{{titleUcFirst}}ShowStore = defineStore("{{lc}}Show", { + state: (): State => ({ + retrieved: undefined, + isLoading: false, + error: "", + hubUrl: undefined, + }), + + actions: { + setData({ retrieved, isLoading, error, hubUrl }: FetchItemData<{{titleUcFirst}}>) { + this.setRetrieved(retrieved.value); + this.setLoading(isLoading.value); + this.setHubUrl(hubUrl.value); + + if (error.value instanceof Error) { + this.setError(error.value?.message); + } + }, + + setLoading(isLoading: boolean) { + this.isLoading = isLoading; + }, + + setRetrieved(retrieved?: {{titleUcFirst}}) { + this.retrieved = retrieved; + }, + + setHubUrl(hubUrl?: URL) { + this.hubUrl = hubUrl; + }, + + setError(error?: string) { + this.error = error; + }, + }, +}); diff --git a/templates/nuxt/stores/foo/update.ts b/templates/nuxt/stores/foo/update.ts new file mode 100644 index 00000000..0e10f625 --- /dev/null +++ b/templates/nuxt/stores/foo/update.ts @@ -0,0 +1,75 @@ +import { defineStore } from "pinia"; +import type { {{titleUcFirst}} } from "~~/types/{{lc}}"; +import type { SubmissionErrors } from "~~/types/error"; +import { FetchItemData, UpdateItemData } from "~~/types/api"; + +interface State { + updated?: {{titleUcFirst}}; + retrieved?: {{titleUcFirst}}; + isLoading: boolean; + error?: string; + hubUrl?: URL; + violations?: SubmissionErrors; +} + +export const use{{titleUcFirst}}UpdateStore = defineStore("{{lc}}Update", { + state: (): State => ({ + updated: undefined, + retrieved: undefined, + isLoading: false, + error: undefined, + hubUrl: undefined, + violations: undefined, + }), + + actions: { + setData({ retrieved, isLoading, error, hubUrl }: FetchItemData<{{titleUcFirst}}>) { + this.setRetrieved(retrieved.value); + this.setLoading(isLoading.value); + this.setHubUrl(hubUrl.value); + + if (error.value instanceof Error) { + this.setError(error.value?.message); + } + }, + + setUpdateData({ + updated, + isLoading, + error, + violations, + }: UpdateItemData<{{titleUcFirst}}>) { + this.setUpdated(updated.value); + this.setLoading(isLoading.value); + this.setViolations(violations.value); + + if (error.value instanceof Error) { + this.setError(error.value?.message); + } + }, + + setRetrieved(retrieved?: {{titleUcFirst}}) { + this.retrieved = retrieved; + }, + + setUpdated(updated?: {{titleUcFirst}}) { + this.updated = updated; + }, + + setHubUrl(hubUrl?: URL) { + this.hubUrl = hubUrl; + }, + + setLoading(isLoading: boolean) { + this.isLoading = isLoading; + }, + + setError(error?: string) { + this.error = error; + }, + + setViolations(violations?: SubmissionErrors) { + this.violations = violations; + }, + }, +}); diff --git a/templates/nuxt/types/api.ts b/templates/nuxt/types/api.ts new file mode 100644 index 00000000..d13f68f1 --- /dev/null +++ b/templates/nuxt/types/api.ts @@ -0,0 +1,32 @@ +import { Ref } from "vue"; +import { SubmissionErrors } from "./error"; +import { View } from "./view"; + +export interface FetchAllData { + items: Ref; + view: Ref; + isLoading: Ref; + error: Ref; + hubUrl: Ref; +} + +export interface FetchItemData { + retrieved: Ref; + isLoading: Ref; + error: Ref; + hubUrl: Ref; +} + +export interface CreateItemData { + created: Ref; + isLoading: Ref; + error: Ref; + violations: Ref; +} + +export interface UpdateItemData { + updated: Ref; + isLoading: Ref; + error: Ref; + violations: Ref; +} diff --git a/templates/nuxt/utils/resource.ts b/templates/nuxt/utils/resource.ts new file mode 100644 index 00000000..e99e7eed --- /dev/null +++ b/templates/nuxt/utils/resource.ts @@ -0,0 +1,11 @@ +export const getIdFromIri = (iri?: string): string => { + if (!iri) return ""; + + const id = iri.split("/").pop(); + + if (!id) { + return ""; + } + + return id; +} diff --git a/templates/quasar/components/common/CommonFormRepeater.vue b/templates/quasar/components/common/CommonFormRepeater.vue index ab6f6db4..19ca77f6 100644 --- a/templates/quasar/components/common/CommonFormRepeater.vue +++ b/templates/quasar/components/common/CommonFormRepeater.vue @@ -66,7 +66,7 @@ function removeField(index: number) { function emitUpdate() { emit( 'update', - fields.value.filter((review) => review.length) + fields.value.filter((field) => field.length) ); } diff --git a/templates/quasar/components/foo/FooForm.vue b/templates/quasar/components/foo/FooForm.vue index b2d98f79..963da8c5 100644 --- a/templates/quasar/components/foo/FooForm.vue +++ b/templates/quasar/components/foo/FooForm.vue @@ -7,7 +7,7 @@ :values="item.{{name}}" :label="$t('{{../lc}}.{{name}}')" class="col-12 col-md-8" - @update="(values: any) => (item.{{name}} = values)" + @update="(values: any[]) => (item.{{name}} = values)" /> {{else}} review.length) + fields.value.filter((field) => field.length) ); } diff --git a/testapp.sh b/testapp.sh index 62c52119..47459444 100755 --- a/testapp.sh +++ b/testapp.sh @@ -34,12 +34,26 @@ if [ "$1" = "react" ]; then fi if [ "$1" = "nuxt" ]; then - yarn create nuxt-app --answers "'{\"name\":\"nuxt\",\"language\":\"js\",\"pm\":\"yarn\",\"ui\":\"vuetify\",\"template\":\"html\",\"features\":[],\"linter\":[],\"test\":\"none\",\"mode\":\"spa\",\"target\":\"static\",\"devTools\":[],\"vcs\":\"none\"}'" ./tmp/app/nuxt - yarn --cwd ./tmp/app/nuxt add moment lodash vuelidate vuex-map-fields + npx nuxi init ./tmp/app/nuxt + + rm ./tmp/app/nuxt/app.vue + rm ./tmp/app/nuxt/nuxt.config.ts + + cp ./templates/nuxt/nuxt.config.ts ./tmp/app/nuxt + + yarn --cwd ./tmp/app/nuxt add dayjs @pinia/nuxt qs @types/qs cp -R ./tmp/nuxt/* ./tmp/app/nuxt - NUXT_TELEMETRY_DISABLED=1 yarn --cwd ./tmp/app/nuxt generate - start-server-and-test 'yarn --cwd ./tmp/app/nuxt start --hostname 127.0.0.1' http://127.0.0.1:3000/books/ 'yarn playwright test' + + # Tailwind + yarn --cwd ./tmp/app/nuxt add tailwindcss postcss autoprefixer + yarn --cwd ./tmp/app/nuxt tailwindcss init -p + cp ./templates/common/tailwind.config.js ./tmp/app/nuxt + cp ./templates/common/style.css ./tmp/app/nuxt/assets/css + + yarn --cwd ./tmp/app/nuxt generate + + start-server-and-test 'yarn --cwd ./tmp/app/nuxt preview' http://127.0.0.1:3000/books/ 'yarn playwright test' fi if [ "$1" = "vue" ]; then diff --git a/tests/show.spec.ts b/tests/show.spec.ts index 99e0c6b1..b269d707 100644 --- a/tests/show.spec.ts +++ b/tests/show.spec.ts @@ -9,6 +9,8 @@ test('resource show', async ({ page, within, queries: { getAllByRole, getByRole, await expect(queryByText('Loading...')).not.toBeVisible(); + await expect(queryByRole('heading', { level: 1 })).toHaveText('Book List'); + const listRows = getAllByRole('row'); const { getAllByRole: getAllByRoleWithinListRow } = within(listRows.nth(3));