From 21560363e2c0ea69d0adc4bb3e32c5082b935827 Mon Sep 17 00:00:00 2001 From: Valentin Date: Mon, 30 Jan 2023 16:04:12 +0100 Subject: [PATCH] feat: migrate templates from Vuetify 2 to Vuetify 3 (#361) --- src/generators/QuasarGenerator.js | 2 +- src/generators/VueBaseGenerator.js | 333 -------------- src/generators/VueBaseGenerator.test.js | 62 --- src/generators/VuetifyGenerator.js | 416 ++++++++++++++---- src/generators/VuetifyGenerator.test.js | 99 +++-- templates/nuxt/components/foo/FooCreate.vue | 5 + templates/nuxt/components/foo/FooUpdate.vue | 1 + templates/quasar/components/foo/FooCreate.vue | 5 + templates/vue-common/error/SubmissionError.js | 10 - templates/vue-common/mixins/CreateMixin.js | 41 -- templates/vue-common/mixins/ListMixin.js | 89 ---- .../vue-common/mixins/NotificationMixin.js | 37 -- templates/vue-common/mixins/ShowMixin.js | 42 -- templates/vue-common/mixins/UpdateMixin.js | 79 ---- templates/vue-common/services/api.js | 24 - templates/vue-common/services/foo.js | 3 - templates/vue-common/store/modules/crud.js | 273 ------------ .../vue-common/store/modules/notifications.js | 18 - templates/vue-common/utils/dates.js | 9 - templates/vue-common/utils/fetch.js | 83 ---- templates/vue-common/utils/hydra.js | 19 - templates/vue-common/validators/date.js | 7 - templates/vue/components/foo/FooCreate.vue | 5 + templates/vue/components/foo/FooUpdate.vue | 1 + templates/vuetify/components/ActionCell.vue | 45 -- templates/vuetify/components/Breadcrumb.vue | 40 -- .../vuetify/components/ConfirmDelete.vue | 45 -- templates/vuetify/components/DataFilter.vue | 40 -- templates/vuetify/components/InputDate.vue | 52 --- templates/vuetify/components/Loading.vue | 18 - templates/vuetify/components/Snackbar.vue | 32 -- templates/vuetify/components/Toolbar.vue | 119 ----- .../vuetify/components/common/ActionCell.vue | 74 ++++ .../vuetify/components/common/Breadcrumb.vue | 24 + .../components/common/ConfirmDelete.vue | 44 ++ .../vuetify/components/common/DataFilter.vue | 38 ++ .../components/common/FormRepeater.vue | 68 +++ .../vuetify/components/common/Loading.vue | 11 + .../vuetify/components/common/Toolbar.vue | 68 +++ templates/vuetify/components/foo/Filter.vue | 139 ------ .../vuetify/components/foo/FooCreate.vue | 43 ++ .../vuetify/components/foo/FooFilter.vue | 26 ++ templates/vuetify/components/foo/FooForm.vue | 105 +++++ templates/vuetify/components/foo/FooList.vue | 253 +++++++++++ templates/vuetify/components/foo/FooShow.vue | 161 +++++++ .../vuetify/components/foo/FooUpdate.vue | 93 ++++ templates/vuetify/components/foo/Form.vue | 223 ---------- templates/vuetify/components/foo/Layout.vue | 9 - templates/vuetify/composables/breadcrumb.ts | 9 + templates/vuetify/locales/en-US/foo.ts | 5 + templates/vuetify/locales/en-US/index.ts | 5 + templates/vuetify/locales/en.js | 5 - templates/vuetify/locales/index.ts | 5 + templates/vuetify/plugins/i18n.ts | 8 + templates/vuetify/router/foo.js | 28 -- templates/vuetify/router/foo.ts | 48 ++ templates/vuetify/store/foo/create.ts | 68 +++ templates/vuetify/store/foo/delete.ts | 61 +++ templates/vuetify/store/foo/list.ts | 88 ++++ templates/vuetify/store/foo/show.ts | 61 +++ templates/vuetify/store/foo/update.ts | 117 +++++ templates/vuetify/types/breadcrumb.ts | 3 + templates/vuetify/types/list.ts | 18 + templates/vuetify/views/foo/Create.vue | 45 -- templates/vuetify/views/foo/List.vue | 138 ------ templates/vuetify/views/foo/Show.vue | 100 ----- templates/vuetify/views/foo/Update.vue | 61 --- templates/vuetify/views/foo/ViewCreate.vue | 11 + templates/vuetify/views/foo/ViewList.vue | 11 + templates/vuetify/views/foo/ViewShow.vue | 11 + templates/vuetify/views/foo/ViewUpdate.vue | 11 + 71 files changed, 1973 insertions(+), 2377 deletions(-) delete mode 100644 src/generators/VueBaseGenerator.js delete mode 100644 src/generators/VueBaseGenerator.test.js delete mode 100644 templates/vue-common/error/SubmissionError.js delete mode 100644 templates/vue-common/mixins/CreateMixin.js delete mode 100644 templates/vue-common/mixins/ListMixin.js delete mode 100644 templates/vue-common/mixins/NotificationMixin.js delete mode 100644 templates/vue-common/mixins/ShowMixin.js delete mode 100644 templates/vue-common/mixins/UpdateMixin.js delete mode 100644 templates/vue-common/services/api.js delete mode 100644 templates/vue-common/services/foo.js delete mode 100644 templates/vue-common/store/modules/crud.js delete mode 100644 templates/vue-common/store/modules/notifications.js delete mode 100644 templates/vue-common/utils/dates.js delete mode 100644 templates/vue-common/utils/fetch.js delete mode 100644 templates/vue-common/utils/hydra.js delete mode 100644 templates/vue-common/validators/date.js delete mode 100644 templates/vuetify/components/ActionCell.vue delete mode 100644 templates/vuetify/components/Breadcrumb.vue delete mode 100644 templates/vuetify/components/ConfirmDelete.vue delete mode 100644 templates/vuetify/components/DataFilter.vue delete mode 100644 templates/vuetify/components/InputDate.vue delete mode 100644 templates/vuetify/components/Loading.vue delete mode 100644 templates/vuetify/components/Snackbar.vue delete mode 100644 templates/vuetify/components/Toolbar.vue create mode 100644 templates/vuetify/components/common/ActionCell.vue create mode 100644 templates/vuetify/components/common/Breadcrumb.vue create mode 100644 templates/vuetify/components/common/ConfirmDelete.vue create mode 100644 templates/vuetify/components/common/DataFilter.vue create mode 100644 templates/vuetify/components/common/FormRepeater.vue create mode 100644 templates/vuetify/components/common/Loading.vue create mode 100644 templates/vuetify/components/common/Toolbar.vue delete mode 100644 templates/vuetify/components/foo/Filter.vue create mode 100644 templates/vuetify/components/foo/FooCreate.vue create mode 100644 templates/vuetify/components/foo/FooFilter.vue create mode 100644 templates/vuetify/components/foo/FooForm.vue create mode 100644 templates/vuetify/components/foo/FooList.vue create mode 100644 templates/vuetify/components/foo/FooShow.vue create mode 100644 templates/vuetify/components/foo/FooUpdate.vue delete mode 100644 templates/vuetify/components/foo/Form.vue delete mode 100644 templates/vuetify/components/foo/Layout.vue create mode 100644 templates/vuetify/composables/breadcrumb.ts create mode 100644 templates/vuetify/locales/en-US/foo.ts create mode 100644 templates/vuetify/locales/en-US/index.ts delete mode 100644 templates/vuetify/locales/en.js create mode 100644 templates/vuetify/locales/index.ts create mode 100644 templates/vuetify/plugins/i18n.ts delete mode 100644 templates/vuetify/router/foo.js create mode 100644 templates/vuetify/router/foo.ts create mode 100644 templates/vuetify/store/foo/create.ts create mode 100644 templates/vuetify/store/foo/delete.ts create mode 100644 templates/vuetify/store/foo/list.ts create mode 100644 templates/vuetify/store/foo/show.ts create mode 100644 templates/vuetify/store/foo/update.ts create mode 100644 templates/vuetify/types/breadcrumb.ts create mode 100644 templates/vuetify/types/list.ts delete mode 100644 templates/vuetify/views/foo/Create.vue delete mode 100644 templates/vuetify/views/foo/List.vue delete mode 100644 templates/vuetify/views/foo/Show.vue delete mode 100644 templates/vuetify/views/foo/Update.vue create mode 100644 templates/vuetify/views/foo/ViewCreate.vue create mode 100644 templates/vuetify/views/foo/ViewList.vue create mode 100644 templates/vuetify/views/foo/ViewShow.vue create mode 100644 templates/vuetify/views/foo/ViewUpdate.vue diff --git a/src/generators/QuasarGenerator.js b/src/generators/QuasarGenerator.js index 756876bc..52c3e942 100644 --- a/src/generators/QuasarGenerator.js +++ b/src/generators/QuasarGenerator.js @@ -144,7 +144,7 @@ export default { params.forEach((p) => { if (p.variable.startsWith("exists[")) { result.push(p); - return; // removed for the moment, it can help to add null option to select + return; } if (p.variable.startsWith("order[")) { result.push(p); diff --git a/src/generators/VueBaseGenerator.js b/src/generators/VueBaseGenerator.js deleted file mode 100644 index 9e9321e6..00000000 --- a/src/generators/VueBaseGenerator.js +++ /dev/null @@ -1,333 +0,0 @@ -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 { sprintf } from "sprintf-js"; -import BaseGenerator from "./BaseGenerator.js"; - -export default class extends BaseGenerator { - constructor(params) { - super(params); - - this.registerTemplates("vue-common/", [ - // error - "error/SubmissionError.js", - - // mixins - "mixins/CreateMixin.js", - "mixins/ListMixin.js", - "mixins/NotificationMixin.js", - "mixins/ShowMixin.js", - "mixins/UpdateMixin.js", - - // services - "services/api.js", - "services/foo.js", - - // modules - "store/modules/crud.js", - "store/modules/notifications.js", - - // utils - "utils/dates.js", - "utils/fetch.js", - "utils/hydra.js", - - // validators - "validators/date.js", - ]); - - handlebars.registerHelper("compare", hbh_comparison.compare); - handlebars.registerHelper("ifEven", hbh_comparison.ifEven); - handlebars.registerHelper("ifOdd", hbh_comparison.ifOdd); - handlebars.registerHelper("isArray", hbh_array.isArray); - handlebars.registerHelper("inArray", hbh_array.inArray); - handlebars.registerHelper("forEach", hbh_array.forEach); - handlebars.registerHelper("lowercase", hbh_string.lowercase); - - this.registerSwitchHelper(); - } - - registerSwitchHelper() { - /* - https://github.com/wycats/handlebars.js/issues/927#issuecomment-318640459 - - {{#switch state}} - {{#case "page1" "page2"}}page 1 or 2{{/case}} - {{#case "page3"}}page3{{/case}} - {{#case "page4"}}page4{{/case}} - {{#case "page5"}} - {{#switch s}} - {{#case "3"}}s = 3{{/case}} - {{#case "2"}}s = 2{{/case}} - {{#case "1"}}s = 1{{/case}} - {{#default}}unknown{{/default}} - {{/switch}} - {{/case}} - {{#default}}page0{{/default}} - {{/switch}} - */ - handlebars.__switch_stack__ = []; - - handlebars.registerHelper("switch", function (value, options) { - handlebars.__switch_stack__.push({ - switch_match: false, - switch_value: value, - }); - let html = options.fn(this); - handlebars.__switch_stack__.pop(); - return html; - }); - handlebars.registerHelper("case", function (value, options) { - let args = Array.from(arguments); - options = args.pop(); - let caseValues = args; - let stack = - handlebars.__switch_stack__[handlebars.__switch_stack__.length - 1]; - - if (stack.switch_match || caseValues.indexOf(stack.switch_value) === -1) { - return ""; - } else { - stack.switch_match = true; - return options.fn(this); - } - }); - handlebars.registerHelper("default", function (options) { - let stack = - handlebars.__switch_stack__[handlebars.__switch_stack__.length - 1]; - if (!stack.switch_match) { - return options.fn(this); - } - }); - } - - getContextForResource(resource, params) { - const lc = resource.title.toLowerCase(); - const titleUcFirst = - resource.title.charAt(0).toUpperCase() + resource.title.slice(1); - const fields = this.parseFields(resource); - - const formFields = this.buildFields(resource.writableFields); - - const dateTypes = ["time", "date", "dateTime"]; - const formContainsDate = formFields.some((e) => dateTypes.includes(e.type)); - - const parameters = []; - params.forEach((p) => { - const param = fields.find((field) => field.name === p.variable); - if (!param) { - p.name = p.variable; - parameters.push(p); - } else { - param.multiple = p.multiple; - parameters.push(param); - } - }); - - const paramsHaveRefs = parameters.some( - (e) => e.type === "text" && e.reference - ); - - const labels = this.commonLabelTexts(); - - return { - title: resource.title, - name: resource.name, - lc, - uc: resource.title.toUpperCase(), - fields, - dateTypes, - paramsHaveRefs, - parameters, - formFields, - formContainsDate, - hydraPrefix: this.hydraPrefix, - titleUcFirst, - labels, - }; - } - - generate(api, resource, dir) { - return resource.getParameters().then((params) => { - params = params.map((param) => ({ - ...param, - ...this.getHtmlInputTypeFromField(param), - })); - - params = this.cleanupParams(params); - - this.generateFiles(api, resource, dir, params); - }); - } - - // eslint-disable-next-line no-unused-vars - generateFiles(api, resource, dir, params) { - // Create directories - // These directories may already exist - [ - `${dir}/config`, - `${dir}/error`, - `${dir}/mixins`, - `${dir}/router`, - `${dir}/services`, - `${dir}/store/modules`, - `${dir}/utils`, - `${dir}/validators`, - ].forEach((dir) => this.createDir(dir, false)); - - // error - this.createFile( - "error/SubmissionError.js", - `${dir}/error/SubmissionError.js`, - {}, - false - ); - - // mixins - [ - "mixins/Create%s.js", - "mixins/List%s.js", - "mixins/Notification%s.js", - "mixins/Show%s.js", - "mixins/Update%s.js", - ].forEach((pattern) => - this.createFile( - sprintf(`${pattern}`, "Mixin"), - sprintf(`${dir}/${pattern}`, "Mixin"), - {}, - false - ) - ); - - // stores - ["crud.js", "notifications.js"].forEach((file) => - this.createFile( - `store/modules/${file}`, - `${dir}/store/modules/${file}`, - { hydraPrefix: this.hydraPrefix }, - false - ) - ); - - // services - this.createFile("services/api.js", `${dir}/services/api.js`, {}, false); - this.createFileFromPattern( - "services/%s.js", - dir, - [resource.title.toLowerCase()], - { name: resource.name } - ); - - // 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) - ); - - this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.js`); - } - - cleanupParams(params) { - const stats = {}; - const result = []; - - params.forEach((p) => { - let key = p.variable.endsWith("[]") - ? p.variable.slice(0, -2) - : p.variable; - if (!stats[key]) { - stats[key] = 0; - } - stats[key] += 1; - }); - - params.forEach((p) => { - if (p.variable.endsWith("[exists]")) { - return; // removed for the moment, it can help to add null option to select - } - if (p.variable.startsWith("order[")) { - return; // removed for the moment, it can help to sorting data - } - if (!stats[p.variable] && p.variable.endsWith("[]")) { - if (stats[p.variable.slice(0, -2)] === 1) { - result.push(p); - } - } else { - if (stats[p.variable] === 2) { - p.multiple = true; - } - result.push(p); - } - }); - - return result; - } - - contextLabelTexts(formFields, fields) { - let texts = []; - formFields.forEach((x) => texts.push(x.name)); // forms - fields.forEach((x) => texts.push(x.name)); // for show, too - return [...new Set(texts)]; - } - - commonLabelTexts() { - return { - submit: "Submit", - reset: "Reset", - delete: "Delete", - edit: "Edit", - confirmDelete: "Are you sure you want to delete this item?", - noresults: "No results", - close: "Close", - cancel: "Cancel", - updated: "Updated", - field: "Field", - value: "Value", - filters: "Filters", - filter: "Filter", - unavail: "Data unavailable", - loading: "Loading...", - deleted: "Deleted", - numValidation: "Please, insert a value bigger than zero!", - stringValidation: "Please type something", - required: "Field is required", - recPerPage: "Records per page:", - }; - } - - 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, - isRelations: isEmbeddeds || isReferences, - }, - }; - }, {}); - - return Object.values(fields); - } -} diff --git a/src/generators/VueBaseGenerator.test.js b/src/generators/VueBaseGenerator.test.js deleted file mode 100644 index 04e9d6ad..00000000 --- a/src/generators/VueBaseGenerator.test.js +++ /dev/null @@ -1,62 +0,0 @@ -import { Api, Resource, Field } from "@api-platform/api-doc-parser"; -import path from "path"; -import { fileURLToPath } from "url"; -import fs from "fs"; -import tmp from "tmp"; -import VueBaseGenerator from "./VueBaseGenerator.js"; - -const dirname = path.dirname(fileURLToPath(import.meta.url)); - -test("Test VueBaseGenerator", () => { - const generator = new VueBaseGenerator({ - hydraPrefix: "hydra:", - templateDirectory: `${dirname}/../../templates`, - }); - const tmpobj = tmp.dirSync({ unsafeCleanup: true }); - - 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("abc", "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], - }); - generator.generate(api, resource, tmpobj.name).then(() => { - expect(fs.existsSync(tmpobj.name + "/mixins/CreateMixin.js")).toBe(true); - expect(fs.existsSync(tmpobj.name + "/mixins/ListMixin.js")).toBe(true); - expect(fs.existsSync(tmpobj.name + "/mixins/NotificationMixin.js")).toBe( - true - ); - expect(fs.existsSync(tmpobj.name + "/mixins/ShowMixin.js")).toBe(true); - expect(fs.existsSync(tmpobj.name + "/mixins/UpdateMixin.js")).toBe(true); - - expect(fs.existsSync(tmpobj.name + "/error/SubmissionError.js")).toBe(true); - - expect(fs.existsSync(tmpobj.name + "/store/modules/crud.js")).toBe(true); - expect(fs.existsSync(tmpobj.name + "/store/modules/notifications.js")).toBe( - true - ); - - expect(fs.existsSync(tmpobj.name + "/utils/dates.js")).toBe(true); - expect(fs.existsSync(tmpobj.name + "/utils/fetch.js")).toBe(true); - expect(fs.existsSync(tmpobj.name + "/utils/hydra.js")).toBe(true); - - tmpobj.removeCallback(); - }); -}); diff --git a/src/generators/VuetifyGenerator.js b/src/generators/VuetifyGenerator.js index 3bb80012..44d697dc 100644 --- a/src/generators/VuetifyGenerator.js +++ b/src/generators/VuetifyGenerator.js @@ -1,36 +1,88 @@ import chalk from "chalk"; -import BaseVueGenerator from "./VueBaseGenerator.js"; +import handlebars from "handlebars"; +import hbh_comparison from "handlebars-helpers/lib/comparison.js"; +import hbh_string from "handlebars-helpers/lib/string.js"; +import BaseGenerator from "./BaseGenerator.js"; -export default class extends BaseVueGenerator { +export default class extends BaseGenerator { constructor(params) { super(params); - this.registerTemplates(`vuetify/`, [ + this.registerTemplates("common/", [ + // types + "types/collection.ts", + "types/error.ts", + "types/foo.ts", + "types/item.ts", + "types/view.ts", + + // utils + "utils/api.ts", + "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("vuetify/", [ // components - "components/ActionCell.vue", - "components/Breadcrumb.vue", - "components/ConfirmDelete.vue", - "components/DataFilter.vue", - "components/InputDate.vue", - "components/Loading.vue", - "components/Snackbar.vue", - "components/Toolbar.vue", - "components/foo/Filter.vue", - "components/foo/Form.vue", - "components/foo/Layout.vue", + "components/foo/FooCreate.vue", + "components/foo/FooFilter.vue", + "components/foo/FooForm.vue", + "components/foo/FooList.vue", + "components/foo/FooShow.vue", + "components/foo/FooUpdate.vue", + + // common components + "components/common/ActionCell.vue", + "components/common/Breadcrumb.vue", + "components/common/ConfirmDelete.vue", + "components/common/DataFilter.vue", + "components/common/FormRepeater.vue", + "components/common/Loading.vue", + "components/common/Toolbar.vue", + + // composables + "composables/breadcrumb.ts", // locales - "locales/en.js", + "locales/en-US/foo.ts", + "locales/en-US/index.ts", + "locales/index.ts", + + // plugins + "plugins/i18n.ts", // routes - "router/foo.js", + "router/foo.ts", + + // store + "store/foo/create.ts", + "store/foo/delete.ts", + "store/foo/list.ts", + "store/foo/show.ts", + "store/foo/update.ts", + + // types + "types/breadcrumb.ts", + "types/list.ts", // views - "views/foo/Create.vue", - "views/foo/List.vue", - "views/foo/Show.vue", - "views/foo/Update.vue", + "views/foo/ViewCreate.vue", + "views/foo/ViewList.vue", + "views/foo/ViewShow.vue", + "views/foo/ViewUpdate.vue", ]); + + handlebars.registerHelper("compare", hbh_comparison.compare); + handlebars.registerHelper("lowercase", hbh_string.lowercase); + handlebars.registerHelper("capitalize", hbh_string.capitalize); } help(resource) { @@ -44,88 +96,298 @@ export default class extends BaseVueGenerator { ); console.log( chalk.green(` -// Register the routes in you router -// src/router/index.js +// Import routes in src/router/index.ts import ${titleLc}Routes from './${titleLc}'; -// Add routes to VueRouter -export default new VueRouter({ +const routes = [ // ... - routes: [ - ${titleLc}Routes, - ] -}); + ...${titleLc}Routes, +]; -// Register the modules in the store -// src/store/index.js -import ${titleLc}Service from '../services/${titleLc}'; -import makeCrudModule from './modules/crud'; +// import translations in src/locales/en-US/index.ts +import ${titleLc} from './${titleLc}'; -export const store = new Vuex.Store({ +export default { // ... - modules: { - // other modules - ${titleLc}: makeCrudModule({ - service: ${titleLc}Service - }) - } -}); + ${titleLc}, +} `) ); } - generateFiles(api, resource, dir, params) { - super.generateFiles(api, resource, dir, params); + generate(api, resource, dir) { + return resource.getParameters().then((params) => { + params = params.map((param) => ({ + ...param, + ...this.getHtmlInputTypeFromField(param), + })); + + params = this.cleanupParams(params); + + this.generateFiles(api, resource, dir, params); + }); + } + + cleanupParams(params) { + const stats = {}; + const result = []; + + params.forEach((p) => { + let key = p.variable.endsWith("[]") + ? p.variable.slice(0, -2) + : p.variable; + if (!stats[key]) { + stats[key] = 0; + } + stats[key] += 1; + }); + + params.forEach((p) => { + if (p.variable.startsWith("exists[")) { + return; // removed for the moment, it can help to add null option to select + } + if (p.variable.startsWith("order[")) { + result.push(p); + return; + } + if (!stats[p.variable] && p.variable.endsWith("[]")) { + if (stats[p.variable.slice(0, -2)] === 1) { + result.push(p); + } + } else { + if (stats[p.variable] === 2) { + p.multiple = true; + } + result.push(p); + } + }); + + return result; + } + + getContextForResource(resource, params) { + const lc = resource.title.toLowerCase(); + const titleUcFirst = + resource.title.charAt(0).toUpperCase() + resource.title.slice(1); + const fields = this.parseFields(resource); + const formFields = this.buildFields(fields); + const hasIsRelations = fields.some((field) => field.isRelations); + const hasDateField = fields.some((field) => field.type === "dateTime"); + + const parameters = []; + params.forEach((p) => { + const paramIndex = fields.findIndex((field) => field.name === p.variable); + if (paramIndex === -1) { + if (p.variable.startsWith("order[")) { + let v = p.variable.slice(6, -1); + let found = fields.findIndex((field) => field.name === v); + if (found !== -1) { + fields[found].sortable = true; + } + return; + } + } else { + const param = fields[paramIndex]; + param.multiple = p.multiple; + parameters.push(param); + } + }); + + const labels = this.commonLabelTexts(); + + return { + title: resource.title, + titleUcFirst, + name: resource.name, + lc, + fields, + formFields, + hasIsRelations, + hasDateField, + parameters, + hydraPrefix: this.hydraPrefix, + labels, + }; + } - const context = super.getContextForResource(resource, params); - const lc = context.lc; + generateFiles(api, resource, dir, params) { + const context = this.getContextForResource(resource, params); + const { lc, titleUcFirst, labels, formFields, fields } = context; // Create directories // These directories may already exist - this.createDir(`${dir}/router`, false); - this.createDir(`${dir}/locales`, false); + [ + `${dir}/components/${lc}`, + `${dir}/components/common`, + `${dir}/composables`, + `${dir}/locales`, + `${dir}/locales/en-US`, + `${dir}/plugins`, + `${dir}/router`, + `${dir}/store/${lc}`, + `${dir}/types`, + `${dir}/utils`, + `${dir}/views/${lc}`, + ].forEach((dir) => this.createDir(dir, false)); - [`${dir}/components/${lc}`, `${dir}/views/${lc}`].forEach((dir) => - this.createDir(dir) - ); + [ + // common components + "components/common/ActionCell.vue", + "components/common/Breadcrumb.vue", + "components/common/ConfirmDelete.vue", + "components/common/DataFilter.vue", + "components/common/FormRepeater.vue", + "components/common/Loading.vue", + "components/common/Toolbar.vue", + + // composables + "composables/breadcrumb.ts", + "composables/mercureItem.ts", + "composables/mercureList.ts", - this.createFile("locales/en.js", `${dir}/locales/en.js`, context, false); + // locales + "locales/index.ts", + + // plugins + "plugins/i18n.ts", + + // types + "types/breadcrumb.ts", + "types/collection.ts", + "types/error.ts", + "types/item.ts", + "types/list.ts", + "types/view.ts", + + // utils + "utils/api.ts", + "utils/date.ts", + "utils/error.ts", + "utils/mercure.ts", + ].forEach((common) => + this.createFile(common, `${dir}/${common}`, context, false) + ); [ // components - "components/%s/Filter.vue", - "components/%s/Form.vue", - "components/%s/Layout.vue", + "components/%s/%sCreate.vue", + "components/%s/%sFilter.vue", + "components/%s/%sForm.vue", + "components/%s/%sList.vue", + "components/%s/%sShow.vue", + "components/%s/%sUpdate.vue", // routes - "router/%s.js", + "router/%s.ts", + + // store + "store/%s/create.ts", + "store/%s/delete.ts", + "store/%s/list.ts", + "store/%s/show.ts", + "store/%s/update.ts", // views - "views/%s/Create.vue", - "views/%s/List.vue", - "views/%s/Show.vue", - "views/%s/Update.vue", - ].forEach((pattern) => - this.createFileFromPattern(pattern, dir, [lc], context) + "views/%s/ViewCreate.vue", + "views/%s/ViewList.vue", + "views/%s/ViewShow.vue", + "views/%s/ViewUpdate.vue", + + // types + "types/%s.ts", + ].forEach((pattern) => { + if ( + pattern === "components/%s/%sFilter.vue" && + !context.parameters.length + ) { + return; + } + this.createFileFromPattern(pattern, dir, [lc, titleUcFirst], context); + }); + + // config + this.createConfigFile(`${dir}/utils/config.ts`, { + entrypoint: api.entrypoint, + }); + + this.createFile( + "locales/en-US/index.ts", + `${dir}/locales/en-US/index.ts`, + { labels }, + false ); - // components - [ - "ActionCell.vue", - "Breadcrumb.vue", - "ConfirmDelete.vue", - "DataFilter.vue", - "InputDate.vue", - "Loading.vue", - "Snackbar.vue", - "Toolbar.vue", - ].forEach((file) => - this.createFile( - `components/${file}`, - `${dir}/components/${file}`, - context, - false - ) + const contextLabels = { + labels: this.contextLabelTexts(formFields, fields), + }; + + this.createFile( + "locales/en-US/foo.ts", + `${dir}/locales/en-US/${lc}.ts`, + contextLabels, + false ); } + + parseFields(resource) { + const fields = [ + ...resource.writableFields, + ...resource.readableFields, + ].reduce((list, field) => { + if (list[field.name]) { + return list; + } + + const isReferences = field.reference && field.maxCardinality !== 1; + const isEmbeddeds = 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); + } + + contextLabelTexts(formFields, fields) { + let texts = []; + formFields.forEach((x) => texts.push(x.name)); // forms + fields.forEach((x) => texts.push(x.name)); // for show, too + return [...new Set(texts)]; + } + + commonLabelTexts() { + return { + home: "Home", + submit: "Submit", + reset: "Reset", + add: "Add", + delete: "Delete", + edit: "Edit", + show: "Show", + cancel: "Cancel", + updated: "Updated", + filters: "Filters", + filter: "Filter", + actions: "Actions", + id: "Id", + itemCreated: "{0} created", + itemUpdated: "{0} updated", + itemDeleted: "{0} deleted", + itemDeletedByAnotherUser: "{0} deleted by another user", + field: "Field", + value: "Value", + itemNotFound: "No item found. Please reload", + confirmDelete: "Are you sure you want to delete this item?", + loading: "Loading...", + }; + } } diff --git a/src/generators/VuetifyGenerator.test.js b/src/generators/VuetifyGenerator.test.js index 2f74ceba..1843fa70 100644 --- a/src/generators/VuetifyGenerator.test.js +++ b/src/generators/VuetifyGenerator.test.js @@ -7,7 +7,7 @@ import VuetifyGenerator from "./VuetifyGenerator.js"; const dirname = path.dirname(fileURLToPath(import.meta.url)); -test("Generate a Vuetify app", () => { +test("Generate a Vuetify app", async () => { const generator = new VuetifyGenerator({ hydraPrefix: "hydra:", templateDirectory: `${dirname}/../../templates`, @@ -21,6 +21,7 @@ test("Generate a Vuetify app", () => { reference: null, required: true, description: "An URL", + type: "string", }), ]; const resource = new Resource("abc", "http://example.com/foos", { @@ -37,36 +38,72 @@ test("Generate a Vuetify app", () => { title: "My API", resources: [resource], }); - generator.generate(api, resource, tmpobj.name).then(() => { - [ - "/components/ActionCell.vue", - "/components/Breadcrumb.vue", - "/components/ConfirmDelete.vue", - "/components/DataFilter.vue", - "/components/foo/Filter.vue", - "/components/foo/Form.vue", - "/components/foo/Layout.vue", - "/components/InputDate.vue", - "/components/Loading.vue", - "/components/Snackbar.vue", - "/components/Toolbar.vue", - "/config/entrypoint.js", - "/error/SubmissionError.js", - "/locales/en.js", - "/router/foo.js", - "/services/api.js", - "/services/foo.js", - "/utils/dates.js", - "/utils/fetch.js", - "/utils/hydra.js", - "/views/foo/Create.vue", - "/views/foo/List.vue", - "/views/foo/Show.vue", - "/views/foo/Update.vue", - ].forEach((file) => { - expect(fs.existsSync(tmpobj.name + file)).toBe(true); - }); - tmpobj.removeCallback(); + await generator.generate(api, resource, tmpobj.name, []); + + // common components + [ + "ActionCell", + "Breadcrumb", + "ConfirmDelete", + "DataFilter", + "FormRepeater", + "Loading", + "Toolbar", + ].forEach((name) => { + expect(fs.existsSync(`${tmpobj.name}/components/common/${name}.vue`)).toBe( + true + ); + }); + + // components + ["Create", "Form", "List", "Show", "Update"].forEach((name) => { + expect(fs.existsSync(`${tmpobj.name}/components/foo/Foo${name}.vue`)).toBe( + true + ); + }); + + // composables + expect(fs.existsSync(`${tmpobj.name}/composables/mercureItem.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/composables/mercureList.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/composables/breadcrumb.ts`)).toBe(true); + + // locales + expect(fs.existsSync(`${tmpobj.name}/locales/en-US/foo.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/locales/en-US/index.ts`)).toBe(true); + expect(fs.existsSync(`${tmpobj.name}/locales/index.ts`)).toBe(true); + + // plugins + expect(fs.existsSync(`${tmpobj.name}/plugins/i18n.ts`)).toBe(true); + + // router + expect(fs.existsSync(`${tmpobj.name}/router/foo.ts`)).toBe(true); + + // stores + ["create", "delete", "list", "show", "update"].forEach((name) => { + expect(fs.existsSync(`${tmpobj.name}/store/foo/${name}.ts`)).toBe(true); }); + + // types + ["breadcrumb", "collection", "error", "foo", "item", "list", "view"].forEach( + (name) => { + expect(fs.existsSync(`${tmpobj.name}/types/${name}.ts`)).toBe(true); + } + ); + + // utils + expect(fs.existsSync(`${tmpobj.name}/utils/api.ts`)).toBe(true); + 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); + + // views + ["Create", "List", "Show", "Update"].forEach((name) => { + expect(fs.existsSync(`${tmpobj.name}/views/foo/View${name}.vue`)).toBe( + true + ); + }); + + tmpobj.removeCallback(); }); diff --git a/templates/nuxt/components/foo/FooCreate.vue b/templates/nuxt/components/foo/FooCreate.vue index 2f3f9ef7..0f9c27f1 100644 --- a/templates/nuxt/components/foo/FooCreate.vue +++ b/templates/nuxt/components/foo/FooCreate.vue @@ -25,6 +25,7 @@ diff --git a/templates/nuxt/components/foo/FooUpdate.vue b/templates/nuxt/components/foo/FooUpdate.vue index 94e66c54..191860f7 100644 --- a/templates/nuxt/components/foo/FooUpdate.vue +++ b/templates/nuxt/components/foo/FooUpdate.vue @@ -132,5 +132,6 @@ async function deleteItem() { onBeforeUnmount(() => { {{lc}}UpdateStore.$reset(); {{lc}}CreateStore.$reset(); + {{lc}}DeleteStore.$reset(); }); diff --git a/templates/quasar/components/foo/FooCreate.vue b/templates/quasar/components/foo/FooCreate.vue index e782e53a..6613d96f 100644 --- a/templates/quasar/components/foo/FooCreate.vue +++ b/templates/quasar/components/foo/FooCreate.vue @@ -11,6 +11,7 @@ diff --git a/templates/vue-common/error/SubmissionError.js b/templates/vue-common/error/SubmissionError.js deleted file mode 100644 index fec731e6..00000000 --- a/templates/vue-common/error/SubmissionError.js +++ /dev/null @@ -1,10 +0,0 @@ -export default class SubmissionError extends Error { - constructor (errors) { - super('Submit Validation Failed'); - this.errors = errors; - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - - return this; - } -} diff --git a/templates/vue-common/mixins/CreateMixin.js b/templates/vue-common/mixins/CreateMixin.js deleted file mode 100644 index c263c106..00000000 --- a/templates/vue-common/mixins/CreateMixin.js +++ /dev/null @@ -1,41 +0,0 @@ -import NotificationMixin from './NotificationMixin'; -import { formatDateTime } from '../utils/dates'; - -export default { - mixins: [NotificationMixin], - methods: { - formatDateTime, - onCreated(item) { - this.showMessage(`${item['@id']} created`); - - this.$router.push({ - name: `${this.$options.servicePrefix}Update`, - params: { id: item['@id'] } - }); - }, - 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/vue-common/mixins/ListMixin.js b/templates/vue-common/mixins/ListMixin.js deleted file mode 100644 index 7515e94b..00000000 --- a/templates/vue-common/mixins/ListMixin.js +++ /dev/null @@ -1,89 +0,0 @@ -import isEmpty from 'lodash/isEmpty'; -import { formatDateTime } from '../utils/dates'; -import NotificationMixin from './NotificationMixin'; - -export default { - mixins: [NotificationMixin], - - 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; - - this.getPage(params).then(() => { - this.options.sortBy = sortBy; - this.options.sortDesc = sortDesc; - this.options.itemsPerPage = itemsPerPage; - this.options.totalItems = totalItems; - }); - }, - - onSendFilter() { - this.resetList = true; - this.onUpdateOptions(this.options); - }, - - resetFilter() { - this.filters = {}; - }, - - addHandler() { - this.$router.push({ name: `${this.$options.servicePrefix}Create` }); - }, - - showHandler(item) { - this.$router.push({ - name: `${this.$options.servicePrefix}Show`, - params: { id: item['@id'] } - }); - }, - - editHandler(item) { - this.$router.push({ - name: `${this.$options.servicePrefix}Update`, - params: { id: item['@id'] } - }); - }, - - deleteHandler(item) { - this.deleteItem(item).then(() => this.onUpdateOptions(this.options)); - }, - formatDateTime - } -}; diff --git a/templates/vue-common/mixins/NotificationMixin.js b/templates/vue-common/mixins/NotificationMixin.js deleted file mode 100644 index 9ad61347..00000000 --- a/templates/vue-common/mixins/NotificationMixin.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/vue-common/mixins/ShowMixin.js b/templates/vue-common/mixins/ShowMixin.js deleted file mode 100644 index 8b2fb768..00000000 --- a/templates/vue-common/mixins/ShowMixin.js +++ /dev/null @@ -1,42 +0,0 @@ -import NotificationMixin from './NotificationMixin'; -import { formatDateTime } from '../utils/dates'; - -export default { - mixins: [NotificationMixin], - created() { - this.retrieve(decodeURIComponent(this.$route.params.id)); - }, - computed: { - item() { - return this.find(decodeURIComponent(this.$route.params.id)); - } - }, - methods: { - del() { - this.deleteItem(this.item).then(() => { - this.showMessage(`${this.item['@id']} deleted.`); - this.$router - .push({ name: `${this.$options.servicePrefix}List` }) - .catch(() => {}); - }); - }, - formatDateTime, - editHandler() { - this.$router.push({ - name: `${this.$options.servicePrefix}Update`, - params: { id: this.item['@id'] } - }); - } - }, - watch: { - error(message) { - message && this.showError(message); - }, - deleteError(message) { - message && this.showError(message); - } - }, - beforeDestroy() { - this.reset(); - } -}; diff --git a/templates/vue-common/mixins/UpdateMixin.js b/templates/vue-common/mixins/UpdateMixin.js deleted file mode 100644 index 0ab0493c..00000000 --- a/templates/vue-common/mixins/UpdateMixin.js +++ /dev/null @@ -1,79 +0,0 @@ -import NotificationMixin from './NotificationMixin'; -import { formatDateTime } from '../utils/dates'; - -export default { - mixins: [NotificationMixin], - data() { - return { - item: {} - }; - }, - created() { - this.retrieve(decodeURIComponent(this.$route.params.id)); - }, - beforeDestroy() { - this.reset(); - }, - computed: { - retrieved() { - return this.find(decodeURIComponent(this.$route.params.id)); - } - }, - methods: { - del() { - this.deleteItem(this.retrieved).then(() => { - this.showMessage(`${this.item['@id']} deleted.`); - this.$router - .push({ name: `${this.$options.servicePrefix}List` }) - .catch(() => {}); - }); - }, - 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({ name: `${this.$options.servicePrefix}List` }) - .catch(() => {}); - }, - - 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/vue-common/services/api.js b/templates/vue-common/services/api.js deleted file mode 100644 index 69576af7..00000000 --- a/templates/vue-common/services/api.js +++ /dev/null @@ -1,24 +0,0 @@ -import fetch from '../utils/fetch'; - -export default function makeService(endpoint) { - return { - find(id) { - return fetch(`${id}`); - }, - findAll(params) { - return fetch(endpoint, params); - }, - create(payload) { - return fetch(endpoint, { method: 'POST', body: JSON.stringify(payload) }); - }, - del(item) { - return fetch(item['@id'], { method: 'DELETE' }); - }, - update(payload) { - return fetch(payload['@id'], { - method: 'PUT', - body: JSON.stringify(payload) - }); - } - }; -} diff --git a/templates/vue-common/services/foo.js b/templates/vue-common/services/foo.js deleted file mode 100644 index b9b02ffe..00000000 --- a/templates/vue-common/services/foo.js +++ /dev/null @@ -1,3 +0,0 @@ -import makeService from './api'; - -export default makeService('{{{name}}}'); diff --git a/templates/vue-common/store/modules/crud.js b/templates/vue-common/store/modules/crud.js deleted file mode 100644 index f5de0039..00000000 --- a/templates/vue-common/store/modules/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/vue-common/store/modules/notifications.js b/templates/vue-common/store/modules/notifications.js deleted file mode 100644 index fb70a236..00000000 --- a/templates/vue-common/store/modules/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/vue-common/utils/dates.js b/templates/vue-common/utils/dates.js deleted file mode 100644 index c379e8da..00000000 --- a/templates/vue-common/utils/dates.js +++ /dev/null @@ -1,9 +0,0 @@ -import moment from 'moment'; - -const formatDateTime = function(date) { - if (!date) return null; - - return moment(date).format('DD/MM/YYYY'); -}; - -export { formatDateTime }; diff --git a/templates/vue-common/utils/fetch.js b/templates/vue-common/utils/fetch.js deleted file mode 100644 index 2d65a672..00000000 --- a/templates/vue-common/utils/fetch.js +++ /dev/null @@ -1,83 +0,0 @@ -import isObject from 'lodash/isObject'; -import { ENTRYPOINT } from '../config/entrypoint'; -import SubmissionError from '../error/SubmissionError'; -import { normalize } from './hydra'; - -const MIME_TYPE = 'application/ld+json'; - -const makeParamArray = (key, arr) => - arr.map(val => `${key}[]=${val}`).join('&'); - -export const getPath = (iri, pathTemplate) => { - if (!iri) { - return ''; - } - - const resourceId = iri.split('/').slice(-1)[0]; - - return pathTemplate.replace('[id]', resourceId); -} - -export default function(id, options = {}) { - if ('undefined' === typeof options.headers) { - options.headers = {}; - } - - if (!options.headers.hasOwnProperty('Accept')) { - options.headers = {...options.headers, 'Accept': MIME_TYPE}; - } - - if ( - undefined !== options.body && - !(options.body instanceof FormData) && - !options.headers.hasOwnProperty('Content-Type') - ) { - options.headers = {...options.headers, 'Content-Type': MIME_TYPE}; - } - - if (options.params) { - const params = normalize(options.params); - let queryString = Object.keys(params) - .map(key => - Array.isArray(params[key]) - ? makeParamArray(key, params[key]) - : `${key}=${params[key]}` - ) - .join('&'); - id = `${id}?${queryString}`; - } - - const entryPoint = ENTRYPOINT + (ENTRYPOINT.endsWith('/') ? '' : '/'); - - const payload = options.body && JSON.parse(options.body); - if (isObject(payload) && payload['@id']) - options.body = JSON.stringify(normalize(payload)); - - return global.fetch(new URL(id, entryPoint), options).then(response => { - if (response.ok) return response; - - return response.json().then( - json => { - const error = - json['hydra:description'] || - json['hydra:title'] || - 'An error occurred.'; - - if (!json.violations) throw Error(error); - - let errors = { _error: error }; - json.violations.forEach(violation => - errors[violation.propertyPath] - ? (errors[violation.propertyPath] += - '\n' + errors[violation.propertyPath]) - : (errors[violation.propertyPath] = violation.message) - ); - - throw new SubmissionError(errors); - }, - () => { - throw new Error(response.statusText || 'An error occurred.'); - } - ); - }); -} diff --git a/templates/vue-common/utils/hydra.js b/templates/vue-common/utils/hydra.js deleted file mode 100644 index 6d23ee75..00000000 --- a/templates/vue-common/utils/hydra.js +++ /dev/null @@ -1,19 +0,0 @@ -import get from 'lodash/get'; -import has from 'lodash/has'; -import mapValues from 'lodash/mapValues'; - -export function normalize(data) { - if (has(data, 'hydra:member')) { - // Normalize items in collections - data['hydra:member'] = data['hydra:member'].map(item => normalize(item)); - - return data; - } - - // Flatten nested documents - return mapValues(data, value => - Array.isArray(value) - ? value.map(v => get(v, '@id', v)) - : get(value, '@id', value) - ); -} diff --git a/templates/vue-common/validators/date.js b/templates/vue-common/validators/date.js deleted file mode 100644 index d2a12c7e..00000000 --- a/templates/vue-common/validators/date.js +++ /dev/null @@ -1,7 +0,0 @@ -import moment from 'moment'; - -const date = function(value) { - return moment(value).isValid(); -}; - -export { date }; diff --git a/templates/vue/components/foo/FooCreate.vue b/templates/vue/components/foo/FooCreate.vue index cbaaeba0..710072b2 100644 --- a/templates/vue/components/foo/FooCreate.vue +++ b/templates/vue/components/foo/FooCreate.vue @@ -29,6 +29,7 @@ diff --git a/templates/vue/components/foo/FooUpdate.vue b/templates/vue/components/foo/FooUpdate.vue index bc4240a6..8f060aa4 100644 --- a/templates/vue/components/foo/FooUpdate.vue +++ b/templates/vue/components/foo/FooUpdate.vue @@ -107,5 +107,6 @@ async function deleteItem() { onBeforeUnmount(() => { {{lc}}UpdateStore.$reset(); {{lc}}CreateStore.$reset(); + {{lc}}DeleteStore.$reset(); }); diff --git a/templates/vuetify/components/ActionCell.vue b/templates/vuetify/components/ActionCell.vue deleted file mode 100644 index 26aaba9f..00000000 --- a/templates/vuetify/components/ActionCell.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/templates/vuetify/components/Breadcrumb.vue b/templates/vuetify/components/Breadcrumb.vue deleted file mode 100644 index 321feb97..00000000 --- a/templates/vuetify/components/Breadcrumb.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - diff --git a/templates/vuetify/components/ConfirmDelete.vue b/templates/vuetify/components/ConfirmDelete.vue deleted file mode 100644 index 54720718..00000000 --- a/templates/vuetify/components/ConfirmDelete.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/templates/vuetify/components/DataFilter.vue b/templates/vuetify/components/DataFilter.vue deleted file mode 100644 index 097409a4..00000000 --- a/templates/vuetify/components/DataFilter.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - diff --git a/templates/vuetify/components/InputDate.vue b/templates/vuetify/components/InputDate.vue deleted file mode 100644 index a79af800..00000000 --- a/templates/vuetify/components/InputDate.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - diff --git a/templates/vuetify/components/Loading.vue b/templates/vuetify/components/Loading.vue deleted file mode 100644 index bceef719..00000000 --- a/templates/vuetify/components/Loading.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/templates/vuetify/components/Snackbar.vue b/templates/vuetify/components/Snackbar.vue deleted file mode 100644 index cda93f16..00000000 --- a/templates/vuetify/components/Snackbar.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - diff --git a/templates/vuetify/components/Toolbar.vue b/templates/vuetify/components/Toolbar.vue deleted file mode 100644 index e5441407..00000000 --- a/templates/vuetify/components/Toolbar.vue +++ /dev/null @@ -1,119 +0,0 @@ - - - diff --git a/templates/vuetify/components/common/ActionCell.vue b/templates/vuetify/components/common/ActionCell.vue new file mode 100644 index 00000000..a6fcf2d2 --- /dev/null +++ b/templates/vuetify/components/common/ActionCell.vue @@ -0,0 +1,74 @@ + + + diff --git a/templates/vuetify/components/common/Breadcrumb.vue b/templates/vuetify/components/common/Breadcrumb.vue new file mode 100644 index 00000000..82ff7b95 --- /dev/null +++ b/templates/vuetify/components/common/Breadcrumb.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/templates/vuetify/components/common/ConfirmDelete.vue b/templates/vuetify/components/common/ConfirmDelete.vue new file mode 100644 index 00000000..b5f4c47e --- /dev/null +++ b/templates/vuetify/components/common/ConfirmDelete.vue @@ -0,0 +1,44 @@ + + + diff --git a/templates/vuetify/components/common/DataFilter.vue b/templates/vuetify/components/common/DataFilter.vue new file mode 100644 index 00000000..38e1c208 --- /dev/null +++ b/templates/vuetify/components/common/DataFilter.vue @@ -0,0 +1,38 @@ + + + diff --git a/templates/vuetify/components/common/FormRepeater.vue b/templates/vuetify/components/common/FormRepeater.vue new file mode 100644 index 00000000..b7fa8bbc --- /dev/null +++ b/templates/vuetify/components/common/FormRepeater.vue @@ -0,0 +1,68 @@ + + + diff --git a/templates/vuetify/components/common/Loading.vue b/templates/vuetify/components/common/Loading.vue new file mode 100644 index 00000000..b6e269c2 --- /dev/null +++ b/templates/vuetify/components/common/Loading.vue @@ -0,0 +1,11 @@ + + + diff --git a/templates/vuetify/components/common/Toolbar.vue b/templates/vuetify/components/common/Toolbar.vue new file mode 100644 index 00000000..db5eebe6 --- /dev/null +++ b/templates/vuetify/components/common/Toolbar.vue @@ -0,0 +1,68 @@ + + + diff --git a/templates/vuetify/components/foo/Filter.vue b/templates/vuetify/components/foo/Filter.vue deleted file mode 100644 index ae434575..00000000 --- a/templates/vuetify/components/foo/Filter.vue +++ /dev/null @@ -1,139 +0,0 @@ - - - diff --git a/templates/vuetify/components/foo/FooCreate.vue b/templates/vuetify/components/foo/FooCreate.vue new file mode 100644 index 00000000..e40467fd --- /dev/null +++ b/templates/vuetify/components/foo/FooCreate.vue @@ -0,0 +1,43 @@ + + + diff --git a/templates/vuetify/components/foo/FooFilter.vue b/templates/vuetify/components/foo/FooFilter.vue new file mode 100644 index 00000000..9ed0201f --- /dev/null +++ b/templates/vuetify/components/foo/FooFilter.vue @@ -0,0 +1,26 @@ + + + diff --git a/templates/vuetify/components/foo/FooForm.vue b/templates/vuetify/components/foo/FooForm.vue new file mode 100644 index 00000000..f85d223b --- /dev/null +++ b/templates/vuetify/components/foo/FooForm.vue @@ -0,0 +1,105 @@ + + + diff --git a/templates/vuetify/components/foo/FooList.vue b/templates/vuetify/components/foo/FooList.vue new file mode 100644 index 00000000..9b21ac37 --- /dev/null +++ b/templates/vuetify/components/foo/FooList.vue @@ -0,0 +1,253 @@ + + + diff --git a/templates/vuetify/components/foo/FooShow.vue b/templates/vuetify/components/foo/FooShow.vue new file mode 100644 index 00000000..d075e646 --- /dev/null +++ b/templates/vuetify/components/foo/FooShow.vue @@ -0,0 +1,161 @@ + + + diff --git a/templates/vuetify/components/foo/FooUpdate.vue b/templates/vuetify/components/foo/FooUpdate.vue new file mode 100644 index 00000000..fd61ff02 --- /dev/null +++ b/templates/vuetify/components/foo/FooUpdate.vue @@ -0,0 +1,93 @@ + + + diff --git a/templates/vuetify/components/foo/Form.vue b/templates/vuetify/components/foo/Form.vue deleted file mode 100644 index c1dcb727..00000000 --- a/templates/vuetify/components/foo/Form.vue +++ /dev/null @@ -1,223 +0,0 @@ - - - diff --git a/templates/vuetify/components/foo/Layout.vue b/templates/vuetify/components/foo/Layout.vue deleted file mode 100644 index 760a811f..00000000 --- a/templates/vuetify/components/foo/Layout.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/templates/vuetify/composables/breadcrumb.ts b/templates/vuetify/composables/breadcrumb.ts new file mode 100644 index 00000000..eecc1237 --- /dev/null +++ b/templates/vuetify/composables/breadcrumb.ts @@ -0,0 +1,9 @@ +import { useRoute } from "vue-router"; +import type { BreadcrumbValue } from "@/types/breadcrumb"; + +export function useBreadcrumb() { + const route = useRoute(); + const breadcrumb = route.meta.breadcrumb as BreadcrumbValue[]; + + return breadcrumb; +} diff --git a/templates/vuetify/locales/en-US/foo.ts b/templates/vuetify/locales/en-US/foo.ts new file mode 100644 index 00000000..2e380185 --- /dev/null +++ b/templates/vuetify/locales/en-US/foo.ts @@ -0,0 +1,5 @@ +export default { + {{#each labels}} + {{this}}: '{{capitalize this}}', + {{/each }} +}; diff --git a/templates/vuetify/locales/en-US/index.ts b/templates/vuetify/locales/en-US/index.ts new file mode 100644 index 00000000..68e60200 --- /dev/null +++ b/templates/vuetify/locales/en-US/index.ts @@ -0,0 +1,5 @@ +export default { + {{#each labels}} + {{@key}}: '{{this}}', + {{/each }} +}; diff --git a/templates/vuetify/locales/en.js b/templates/vuetify/locales/en.js deleted file mode 100644 index b3233096..00000000 --- a/templates/vuetify/locales/en.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - {{#each labels as |label|}} - '{{{label}}}': '{{{label}}}', - {{/each }} -}; diff --git a/templates/vuetify/locales/index.ts b/templates/vuetify/locales/index.ts new file mode 100644 index 00000000..4be06fa7 --- /dev/null +++ b/templates/vuetify/locales/index.ts @@ -0,0 +1,5 @@ +import enUS from "./en-US"; + +export default { + "en-US": enUS, +}; diff --git a/templates/vuetify/plugins/i18n.ts b/templates/vuetify/plugins/i18n.ts new file mode 100644 index 00000000..5fd1823f --- /dev/null +++ b/templates/vuetify/plugins/i18n.ts @@ -0,0 +1,8 @@ +import { createI18n } from "vue-i18n"; +import messages from "@/locales"; + +export default createI18n({ + locale: "en-US", + legacy: false, + messages, +}); diff --git a/templates/vuetify/router/foo.js b/templates/vuetify/router/foo.js deleted file mode 100644 index 2267a4b6..00000000 --- a/templates/vuetify/router/foo.js +++ /dev/null @@ -1,28 +0,0 @@ -export default { - path: '/{{{name}}}', - name: '{{{name}}}', - component: () => import('../components/{{{lc}}}/Layout'), - redirect: { name: '{{{titleUcFirst}}}List' }, - children: [ - { - name: '{{{titleUcFirst}}}List', - path: '', - component: () => import('../views/{{{lc}}}/List') - }, - { - name: '{{{titleUcFirst}}}Create', - path: 'new', - component: () => import('../views/{{{lc}}}/Create') - }, - { - name: '{{{titleUcFirst}}}Update', - path: ':id/edit', - component: () => import('../views/{{{lc}}}/Update') - }, - { - name: '{{{titleUcFirst}}}Show', - path: ':id', - component: () => import('../views/{{{lc}}}/Show') - } - ] -}; diff --git a/templates/vuetify/router/foo.ts b/templates/vuetify/router/foo.ts new file mode 100644 index 00000000..34db0075 --- /dev/null +++ b/templates/vuetify/router/foo.ts @@ -0,0 +1,48 @@ +const names = { + list: "{{titleUcFirst}}List", + create: "{{titleUcFirst}}Create", + update: "{{titleUcFirst}}Update", + show: "{{titleUcFirst}}Show", +}; + +const breadcrumbs = { + list: { title: names.list, to: { name: names.list } }, + create: { title: names.create, to: { name: names.create } }, + update: { title: names.update, to: { name: names.update } }, + show: { title: names.show, to: { name: names.show } }, +}; + +export default [ + { + name: names.list, + path: "/{{name}}", + component: () => import("@/views/{{lc}}/ViewList.vue"), + meta: { + breadcrumb: [breadcrumbs.list], + }, + }, + { + name: names.create, + path: "/{{name}}/create", + component: () => import("@/views/{{lc}}/ViewCreate.vue"), + meta: { + breadcrumb: [breadcrumbs.list, breadcrumbs.create], + }, + }, + { + name: names.update, + path: "/{{name}}/edit/:id", + component: () => import("@/views/{{lc}}/ViewUpdate.vue"), + meta: { + breadcrumb: [breadcrumbs.list, breadcrumbs.update], + }, + }, + { + name: names.show, + path: "/{{name}}/show/:id", + component: () => import("@/views/{{lc}}/ViewShow.vue"), + meta: { + breadcrumb: [breadcrumbs.list, breadcrumbs.show], + }, + }, +]; diff --git a/templates/vuetify/store/foo/create.ts b/templates/vuetify/store/foo/create.ts new file mode 100644 index 00000000..f0049477 --- /dev/null +++ b/templates/vuetify/store/foo/create.ts @@ -0,0 +1,68 @@ +import { defineStore } from "pinia"; +import { SubmissionError } from "@/utils/error"; +import api from "@/utils/api"; +import type { {{titleUcFirst}} } from "@/types/{{lc}}"; +import type { SubmissionErrors } from "@/types/error"; + +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: { + async create(payload: {{titleUcFirst}}) { + this.setError(undefined); + this.setViolations(undefined); + this.toggleLoading(); + + try { + const response = await api("{{name}}", { + method: "POST", + body: JSON.stringify(payload), + }); + const data: {{titleUcFirst}} = await response.json(); + + this.toggleLoading(); + this.setCreated(data); + } catch (error) { + this.toggleLoading(); + + if (error instanceof SubmissionError) { + this.setViolations(error.errors); + this.setError(error.errors._error); + return; + } + + if (error instanceof Error) { + this.setError(error.message); + } + } + }, + + setCreated(created: {{titleUcFirst}}) { + this.created = created; + }, + + toggleLoading() { + this.isLoading = !this.isLoading; + }, + + setError(error: string | undefined) { + this.error = error; + }, + + setViolations(violations: SubmissionErrors | undefined) { + this.violations = violations; + }, + }, +}); diff --git a/templates/vuetify/store/foo/delete.ts b/templates/vuetify/store/foo/delete.ts new file mode 100644 index 00000000..ec8addcd --- /dev/null +++ b/templates/vuetify/store/foo/delete.ts @@ -0,0 +1,61 @@ +import { defineStore } from "pinia"; +import api from "@/utils/api"; +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: { + async deleteItem(item: {{titleUcFirst}}) { + this.setError(""); + this.toggleLoading(); + + if (!item?.["@id"]) { + this.setError("No {{lc}} found. Please reload"); + return; + } + + try { + await api(item["@id"], { method: "DELETE" }); + + this.toggleLoading(); + this.setDeleted(item); + this.setMercureDeleted(undefined); + } catch (error) { + this.toggleLoading(); + + if (error instanceof Error) { + this.setError(error.message); + } + } + }, + + toggleLoading() { + this.isLoading = !this.isLoading; + }, + + setDeleted(deleted: {{titleUcFirst}}) { + this.deleted = deleted; + }, + + setMercureDeleted(mercureDeleted: {{titleUcFirst}} | undefined) { + this.mercureDeleted = mercureDeleted; + }, + + setError(error: string) { + this.error = error; + }, + }, +}); diff --git a/templates/vuetify/store/foo/list.ts b/templates/vuetify/store/foo/list.ts new file mode 100644 index 00000000..eb5d6461 --- /dev/null +++ b/templates/vuetify/store/foo/list.ts @@ -0,0 +1,88 @@ +import { defineStore } from "pinia"; +import api from "@/utils/api"; +import { extractHubURL } from "@/utils/mercure"; +import type { {{titleUcFirst}} } from "@/types/{{lc}}"; +import type { PagedCollection } from "@/types/collection"; +import type { ListParams } from "@/types/list"; + +interface State { + items: {{titleUcFirst}}[]; + totalItems: number; + isLoading: boolean; + error?: string; + hubUrl?: URL; +} + +export const use{{titleUcFirst}}ListStore = defineStore("{{lc}}List", { + state: (): State => ({ + items: [], + totalItems: 0, + isLoading: false, + error: undefined, + hubUrl: undefined, + }), + + actions: { + async getItems(params: ListParams) { + this.setError(""); + this.toggleLoading(); + + try { + const response = await api("{{name}}", { params }); + const data: PagedCollection<{{titleUcFirst}}> = await response.json(); + const hubUrl = extractHubURL(response); + + this.toggleLoading(); + + this.setItems(data["hydra:member"]); + this.setTotalItems(data["hydra:totalItems"] ?? 0); + + if (hubUrl) { + this.setHubUrl(hubUrl); + } + } catch (error) { + this.toggleLoading(); + + if (error instanceof Error) { + this.setError(error.message); + } + } + }, + + toggleLoading() { + this.isLoading = !this.isLoading; + }, + + setItems(items: {{titleUcFirst}}[]) { + this.items = items; + }, + + setTotalItems(totalItems: number) { + this.totalItems = totalItems; + }, + + setHubUrl(hubUrl: URL) { + this.hubUrl = hubUrl; + }, + + 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/vuetify/store/foo/show.ts b/templates/vuetify/store/foo/show.ts new file mode 100644 index 00000000..5d905004 --- /dev/null +++ b/templates/vuetify/store/foo/show.ts @@ -0,0 +1,61 @@ +import { defineStore } from "pinia"; +import api from "@/utils/api"; +import { extractHubURL } from "@/utils/mercure"; +import type { {{titleUcFirst}} } from "@/types/{{lc}}"; + +interface State { + retrieved?: {{titleUcFirst}}; + hubUrl?: URL; + isLoading: boolean; + error?: string; +} + +export const use{{titleUcFirst}}ShowStore = defineStore("{{lc}}Show", { + state: (): State => ({ + retrieved: undefined, + hubUrl: undefined, + isLoading: false, + error: undefined, + }), + + actions: { + async retrieve(id: string) { + this.toggleLoading(); + + try { + const response = await api(id); + const data: {{titleUcFirst}} = await response.json(); + const hubUrl = extractHubURL(response); + + this.toggleLoading(); + this.setRetrieved(data); + + if (hubUrl) { + this.setHubUrl(hubUrl); + } + } catch (error) { + this.toggleLoading(); + + if (error instanceof Error) { + this.setError(error.message); + } + } + }, + + toggleLoading() { + this.isLoading = !this.isLoading; + }, + + setRetrieved(retrieved: {{titleUcFirst}}) { + this.retrieved = retrieved; + }, + + setHubUrl(hubUrl: URL) { + this.hubUrl = hubUrl; + }, + + setError(error: string) { + this.error = error; + }, + }, +}); diff --git a/templates/vuetify/store/foo/update.ts b/templates/vuetify/store/foo/update.ts new file mode 100644 index 00000000..42912fa9 --- /dev/null +++ b/templates/vuetify/store/foo/update.ts @@ -0,0 +1,117 @@ +import { defineStore } from "pinia"; +import { SubmissionError } from "@/utils/error"; +import api from "@/utils/api"; +import { extractHubURL } from "@/utils/mercure"; +import type { {{titleUcFirst}} } from "@/types/{{lc}}"; +import type { SubmissionErrors } from "@/types/error"; + +interface State { + retrieved?: {{titleUcFirst}}; + updated?: {{titleUcFirst}}; + hubUrl?: URL; + isLoading: boolean; + error?: string; + violations?: SubmissionErrors; +} + +export const use{{titleUcFirst}}UpdateStore = defineStore("{{lc}}Update", { + state: (): State => ({ + retrieved: undefined, + updated: undefined, + hubUrl: undefined, + isLoading: false, + error: undefined, + violations: undefined, + }), + + actions: { + async retrieve(id: string) { + this.toggleLoading(); + + try { + const response = await api(id); + const data: {{titleUcFirst}} = await response.json(); + const hubUrl = extractHubURL(response); + + this.toggleLoading(); + this.setRetrieved(data); + + if (hubUrl) { + this.setHubUrl(hubUrl); + } + } catch (error) { + this.toggleLoading(); + + if (error instanceof Error) { + this.setError(error.message); + } + } + }, + + async update(payload: {{titleUcFirst}}) { + this.setError(undefined); + this.toggleLoading(); + + if (!this.retrieved) { + this.setError("No {{lc}} found. Please reload"); + return; + } + + try { + const response = await api( + this.retrieved["@id"] ?? payload["@id"] ?? "", + { + method: "PUT", + headers: new Headers({ "Content-Type": "application/ld+json" }), + body: JSON.stringify(payload), + } + ); + const data: {{titleUcFirst}} = await response.json(); + + this.toggleLoading(); + this.setUpdated(data); + } catch (error) { + this.toggleLoading(); + + if (error instanceof SubmissionError) { + this.setViolations(error.errors); + this.setError(error.errors._error); + return; + } + + if (error instanceof Error) { + this.setError(error.message); + } + } + }, + + setRetrieved(retrieved: {{titleUcFirst}}) { + this.retrieved = retrieved; + }, + + setUpdated(updated: {{titleUcFirst}}) { + this.updated = updated; + }, + + setHubUrl(hubUrl: URL) { + this.hubUrl = hubUrl; + }, + + toggleLoading() { + this.isLoading = !this.isLoading; + }, + + setError(error?: string) { + this.error = error; + }, + + setViolations(violations?: SubmissionErrors) { + this.violations = violations; + }, + + resetErrors() { + this.setError(undefined); + this.setViolations(undefined); + }, + }, +}); diff --git a/templates/vuetify/types/breadcrumb.ts b/templates/vuetify/types/breadcrumb.ts new file mode 100644 index 00000000..422b8b21 --- /dev/null +++ b/templates/vuetify/types/breadcrumb.ts @@ -0,0 +1,3 @@ +export interface BreadcrumbValue { + name: string; +} diff --git a/templates/vuetify/types/list.ts b/templates/vuetify/types/list.ts new file mode 100644 index 00000000..5cd59bcf --- /dev/null +++ b/templates/vuetify/types/list.ts @@ -0,0 +1,18 @@ +export interface Filters { + [key: string]: string; +} + +export interface Order { + [key: string]: "asc" | "desc"; +} + +export interface VuetifyOrder { + key: string; + order: string; +} + +export interface ListParams { + page: string; + filters?: Filters; + order?: Order; +} diff --git a/templates/vuetify/views/foo/Create.vue b/templates/vuetify/views/foo/Create.vue deleted file mode 100644 index c31fbf59..00000000 --- a/templates/vuetify/views/foo/Create.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/templates/vuetify/views/foo/List.vue b/templates/vuetify/views/foo/List.vue deleted file mode 100644 index d02413a4..00000000 --- a/templates/vuetify/views/foo/List.vue +++ /dev/null @@ -1,138 +0,0 @@ - - - diff --git a/templates/vuetify/views/foo/Show.vue b/templates/vuetify/views/foo/Show.vue deleted file mode 100644 index 786bc7b4..00000000 --- a/templates/vuetify/views/foo/Show.vue +++ /dev/null @@ -1,100 +0,0 @@ - - - diff --git a/templates/vuetify/views/foo/Update.vue b/templates/vuetify/views/foo/Update.vue deleted file mode 100644 index ed196c2d..00000000 --- a/templates/vuetify/views/foo/Update.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - diff --git a/templates/vuetify/views/foo/ViewCreate.vue b/templates/vuetify/views/foo/ViewCreate.vue new file mode 100644 index 00000000..039abfcd --- /dev/null +++ b/templates/vuetify/views/foo/ViewCreate.vue @@ -0,0 +1,11 @@ + + + diff --git a/templates/vuetify/views/foo/ViewList.vue b/templates/vuetify/views/foo/ViewList.vue new file mode 100644 index 00000000..94573647 --- /dev/null +++ b/templates/vuetify/views/foo/ViewList.vue @@ -0,0 +1,11 @@ + + + diff --git a/templates/vuetify/views/foo/ViewShow.vue b/templates/vuetify/views/foo/ViewShow.vue new file mode 100644 index 00000000..1fea4427 --- /dev/null +++ b/templates/vuetify/views/foo/ViewShow.vue @@ -0,0 +1,11 @@ + + + diff --git a/templates/vuetify/views/foo/ViewUpdate.vue b/templates/vuetify/views/foo/ViewUpdate.vue new file mode 100644 index 00000000..2c3c2dc0 --- /dev/null +++ b/templates/vuetify/views/foo/ViewUpdate.vue @@ -0,0 +1,11 @@ + + +