Skip to content

Migrate templates from Nuxt 2 to Nuxt 3 #356

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
111c3fe
Add FooCreate
ValentinCrochemore Dec 20, 2022
b9fbd60
Add FooForm
ValentinCrochemore Dec 20, 2022
03027c5
Add FooList
ValentinCrochemore Dec 20, 2022
706abcf
Add FooShow
ValentinCrochemore Dec 20, 2022
b2e15ab
Add FooUpdate
ValentinCrochemore Dec 20, 2022
84ca6d4
Add FormRepeater
ValentinCrochemore Dec 20, 2022
ec61bfd
Add Mercure composables
ValentinCrochemore Dec 20, 2022
ddc8908
Add pages
ValentinCrochemore Dec 20, 2022
fa9d1bb
Add stores
ValentinCrochemore Dec 20, 2022
8664c4b
Add types
ValentinCrochemore Dec 20, 2022
3d15a3d
Add utils
ValentinCrochemore Dec 20, 2022
50c49c7
Delete useless files
ValentinCrochemore Dec 20, 2022
a3924b2
Fix some bugs
ValentinCrochemore Dec 22, 2022
0721a94
Add files to generate
ValentinCrochemore Dec 22, 2022
f0a04a7
Import defineStore as it is not auto imported
ValentinCrochemore Dec 22, 2022
f5b79e4
Update Nuxt Generator
ValentinCrochemore Dec 22, 2022
f72548c
Update Nuxt generator test
ValentinCrochemore Dec 22, 2022
997a701
Remove lodash
ValentinCrochemore Dec 22, 2022
2aca9ab
Change link label
ValentinCrochemore Dec 22, 2022
d6cf383
Add a nuxt.config.ts for e2e test
ValentinCrochemore Dec 22, 2022
605e98f
Update command for e2e test
ValentinCrochemore Dec 22, 2022
4d5280b
Change update & create functions names
ValentinCrochemore Dec 28, 2022
66785d3
Rename relations variables
ValentinCrochemore Dec 28, 2022
7ee76ee
Merge branch 'main' into feat/update-nuxt-templates
ValentinCrochemore Jan 6, 2023
fb47b0e
Update e2e script
ValentinCrochemore Jan 6, 2023
24e6ca2
Fix router paths
ValentinCrochemore Jan 6, 2023
7540e39
Fix tailwind config for e2e tests
ValentinCrochemore Jan 6, 2023
ee165af
Fix e2e test
ValentinCrochemore Jan 6, 2023
c868a15
Use files from common & vue-common
ValentinCrochemore Jan 6, 2023
59e915d
Fixes
ValentinCrochemore Jan 6, 2023
497e017
Rewrite test so errors are more easy to locate
ValentinCrochemore Jan 6, 2023
9121963
Add qs in e2e test
ValentinCrochemore Jan 6, 2023
9f53b0d
Remove tailwinf files from generator
ValentinCrochemore Jan 6, 2023
5400b4a
fixes
alanpoulain Jan 10, 2023
4017db4
Manage api calls with useFetch instead of native fetch
ValentinCrochemore Jan 13, 2023
803fd74
Fix access properties of values if null
ValentinCrochemore Jan 16, 2023
684f84c
Fix pagination
ValentinCrochemore Jan 16, 2023
a099ded
Fix reload on show page
ValentinCrochemore Jan 16, 2023
97208c0
Fix reload update page
ValentinCrochemore Jan 17, 2023
ea63439
fixes
alanpoulain Jan 17, 2023
aadf191
Reset stores when leaving FooList
ValentinCrochemore Jan 17, 2023
3cdd33f
Remove FetchError
ValentinCrochemore Jan 18, 2023
fcb1ffd
Updates for Nuxt preview mode
ValentinCrochemore Jan 20, 2023
26e0d9a
fix: use params instead of query to solve SSR issues
alanpoulain Jan 20, 2023
4c282bf
Add util function to get the id instead of accessing it directly
ValentinCrochemore Jan 24, 2023
92c620b
fix: minor fixes
alanpoulain Jan 24, 2023
8198f96
fix: disable SSR for now
alanpoulain Jan 25, 2023
bf563fe
fix: show test
alanpoulain Jan 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
299 changes: 181 additions & 118 deletions src/generators/NuxtGenerator.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
135 changes: 76 additions & 59 deletions src/generators/NuxtGenerator.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
8 changes: 7 additions & 1 deletion templates/common/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -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: [],
}
};
16 changes: 8 additions & 8 deletions templates/common/types/collection.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { View } from './view';
import type { View } from "./view";

export interface PagedCollection<T> {
'@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;
}
2 changes: 1 addition & 1 deletion templates/common/types/item.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export interface Item {
'@id'?: string;
"@id"?: string;
}
10 changes: 5 additions & 5 deletions templates/common/types/view.ts
Original file line number Diff line number Diff line change
@@ -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;
}
70 changes: 0 additions & 70 deletions templates/nuxt/components/ActionCell.vue

This file was deleted.

42 changes: 0 additions & 42 deletions templates/nuxt/components/Alert.vue

This file was deleted.

45 changes: 0 additions & 45 deletions templates/nuxt/components/ConfirmDelete.vue

This file was deleted.

58 changes: 0 additions & 58 deletions templates/nuxt/components/DataFilter.vue

This file was deleted.

50 changes: 0 additions & 50 deletions templates/nuxt/components/InputDate.vue

This file was deleted.

18 changes: 0 additions & 18 deletions templates/nuxt/components/Loading.vue

This file was deleted.

123 changes: 0 additions & 123 deletions templates/nuxt/components/Toolbar.vue

This file was deleted.

67 changes: 67 additions & 0 deletions templates/nuxt/components/common/FormRepeater.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<template>
<button
type="button"
class="my-2 px-6 py-2 border-2 border-green-500 text-green-500 text-xs rounded-full hover:text-white hover:bg-green-500"
@click="addField"
>
Add
</button>

<div v-for="(field, index) in fields" :key="index" class="flex gap-2 mb-3">
<input
v-model="fields[index]"
placeholder="Relation IRI"
class="grow px-3 py-1.5 border rounded"
@input="updateField(index, ($event?.target as HTMLInputElement)?.value)"
/>

<button
type="button"
class="px-6 py-2 border-2 border-gray-800 font-medium text-xs uppercase rounded hover:text-white hover:bg-gray-800"
@click="removeField(index)"
>
Remove
</button>
</div>
</template>

<script setup lang="ts">
import { type Ref } from "vue";
const props = defineProps<{
values?: string[];
}>();
const emit = defineEmits<{
(e: "update", values: string[]): void;
}>();
let fields: Ref<string[]> = ref([]);
if (props.values) {
fields.value.push(...props.values);
}
function addField() {
fields.value.push("");
}
function updateField(index: number, value: string) {
fields.value.splice(index, 1, value.trim());
emitUpdate();
}
function removeField(index: number) {
fields.value.splice(index, 1);
emitUpdate();
}
function emitUpdate() {
emit(
"update",
fields.value.filter((field) => field.length)
);
}
</script>
136 changes: 0 additions & 136 deletions templates/nuxt/components/foo/Filter.vue

This file was deleted.

52 changes: 52 additions & 0 deletions templates/nuxt/components/foo/FooCreate.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<template>
<nuxt-link :to="{ name: '{{lc}}s' }" class="text-blue-600 hover:text-blue-800">
&lt; Back to list
</nuxt-link>

<h1 class="text-3xl my-4">Create {{titleUcFirst}}</h1>

<div
v-if="isLoading"
class="bg-blue-100 rounded py-4 px-4 text-blue-700 text-sm"
role="status"
>
Loading...
</div>

<div
v-if="error"
class="bg-red-100 rounded py-4 px-4 my-2 text-red-700 text-sm"
role="alert"
>
\{{ error }}
</div>

<Form :errors="violations" @submit="create" />
</template>

<script lang="ts" setup>
import { storeToRefs } from "pinia";
import Form from "~~/components/{{lc}}/{{titleUcFirst}}Form.vue";
import { use{{titleUcFirst}}CreateStore } from "~~/stores/{{lc}}/create";
import { useCreateItem } from "~~/composables/api";
import { getIdFromIri } from "~~/utils/resource";
import type { {{titleUcFirst}} } from "~~/types/{{lc}}";
const {{lc}}CreateStore = use{{titleUcFirst}}CreateStore();
const { created, isLoading, violations, error } = storeToRefs({{lc}}CreateStore);
async function create(item: {{titleUcFirst}}) {
const data = await useCreateItem<{{titleUcFirst}}>("{{name}}", item);
{{lc}}CreateStore.setData(data);
if (!created?.value?.["@id"]) {
{{lc}}CreateStore.setError("Missing item id. Please reload");
return;
}
navigateTo({
name: "{{lc}}s-id-edit",
params: { id: getIdFromIri(created?.value?.["@id"]) },
});
}
</script>
97 changes: 97 additions & 0 deletions templates/nuxt/components/foo/FooForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<template>
<form class="py-4" @submit.prevent="emitSubmit">
{{#forEach formFields}}
<div class="mb-2">
<label
for="{{../lc}}_{{name}}"
class="text-gray-700 block text-sm font-bold capitalize"
>
{{name}}
</label>
{{#if isRelations}}
<FormRepeater
:values="item.{{name}}"
@update="(values: any[]) => (item.{{name}} = values)"
/>
{{else}}
<input
id="{{../lc}}_{{name}}"
v-model="item.{{name}}"
:class="[
'mt-1 w-full px-3 py-2 border rounded',
violations?.{{name}} ? 'border-red-500' : 'border-gray-300',
]"
{{#compare type "==" "dateTime" }}
type="date"
{{/compare}}
{{#compare type "!=" "dateTime" }}
type="{{type}}"
{{/compare}}
{{#if step}}
step="{{step}}"
{{/if}}
{{#if required}}
required
{{/if}}
placeholder="{{description}}"
/>
{{/if}}
<div
v-if="violations?.{{name}}"
class="bg-red-100 rounded py-4 px-4 my-2 text-red-700 text-sm"
>
\{{ violations.{{name}} }}
</div>
</div>
{{/forEach}}

<button
type="submit"
class="px-6 py-2 bg-green-500 text-white font-medium rounded shadow-md hover:bg-green-600"
>
Submit
</button>
</form>
</template>

<script lang="ts" setup>
import { Ref } from "vue";
{{#if hasIsRelations}}
import FormRepeater from "~~/components/common/FormRepeater.vue";
{{/if}}
import type { {{titleUcFirst}} } from "~~/types/{{lc}}";
import type { SubmissionErrors } from "~~/types/error";
const props = defineProps<{
values?: {{titleUcFirst}};
errors?: SubmissionErrors;
}>();
const violations = toRef(props, "errors");
let item: Ref<{{titleUcFirst}}> = ref({});
if (props.values) {
item.value = {
...props.values,
{{#each fields}}
{{#compare type "==" "dateTime" }}
publicationDate: formatDateInput(props.values.publicationDate),
{{/compare}}
{{#if isEmbeddeds}}
{{name}}: props.values.{{name}}?.map((item: any) => item["@id"] ?? "") ?? [],
{{else if embedded}}
{{name}}: props.values.{{name}}?.["@id"],
{{/if}}
{{/each}}
};
}
let emit = defineEmits<{
(e: "submit", item: {{titleUcFirst}}): void;
}>();
function emitSubmit() {
emit("submit", item.value);
}
</script>
281 changes: 281 additions & 0 deletions templates/nuxt/components/foo/FooList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
<template>
<div class="flex items-center justify-between">
<h1 class="text-3xl my-4">{{titleUcFirst}} List</h1>

<nuxt-link
:to="{ name: '{{lc}}s-create' }"
class="px-6 py-2 bg-green-600 text-white text-xs rounded shadow-md hover:bg-green-700"
>
Create
</nuxt-link>
</div>

<div
v-if="isLoading"
class="bg-blue-100 rounded py-4 px-4 text-blue-700 text-sm"
role="status"
>
Loading...
</div>

<div
v-if="error"
class="bg-red-100 rounded py-4 px-4 my-2 text-red-700 text-sm"
role="alert"
>
\{{ error }}
</div>

<div
v-if="deletedItem || mercureDeletedItem"
class="bg-green-100 rounded py-4 px-4 my-2 text-green-700 text-sm"
role="status"
>
<template v-if="deletedItem">\{{ deletedItem["@id"] }} deleted.</template>
<template v-else-if="mercureDeletedItem">
\{{ mercureDeletedItem["@id"] }} deleted by another user.
</template>
</div>

<div v-if="!isLoading" class="overflow-x-auto">
<table class="min-w-full">
<thead class="border-b">
<tr>
<th class="text-sm font-medium px-6 py-4 text-left capitalize">
id
</th>
{{#each fields}}
<th class="text-sm font-medium px-6 py-4 text-left capitalize">
{{name}}
</th>
{{/each }}
<th
colspan="2"
class="text-sm font-medium px-6 py-4 text-left capitalize"
>
Actions
</th>
</tr>
</thead>

<tbody>
<tr v-for="item in items" :key="item['@id']" class="border-b">
<td class="px-6 py-4 text-sm">
<nuxt-link
:to="{ name: '{{lc}}s-id', params: { id: getIdFromIri(item['@id']) } }"
class="text-blue-600 hover:text-blue-800"
>
\{{ item["@id"] }}
</nuxt-link>
</td>
{{#each fields}}
<td class="px-6 py-4 text-sm">
{{#if isReferences}}
<template v-if="router.hasRoute('{{reference.name}}-id')">
<nuxt-link
v-for="{{lowercase reference.title}} in item.{{reference.name}}"
:key="{{lowercase reference.title}}"
:to="{ name: '{{lowercase reference.title}}s-id', params: { id: {{lowercase reference.title}} } }"
class="text-blue-600 hover:text-blue-800"
>
\{{ {{lowercase reference.title}} }}

<br />
</nuxt-link>
</template>

<template v-else>
<p
v-for="{{lowercase reference.title}} in item.{{reference.name}}"
:key="{{lowercase reference.title}}"
>
\{{ {{lowercase reference.title}} }}
</p>
</template>
{{else if reference}}
<nuxt-link
v-if="router.hasRoute('{{reference.name}}-id')"
:to="{ name: '{{lowercase reference.title}}s-id', params: { id: item.{{lowercase reference.title}} } }"
class="text-blue-600 hover:text-blue-800"
>
\{{ item.{{lowercase reference.title}} }}
</nuxt-link>

<p v-else>
\{{ item.{{lowercase reference.title}} }}
</p>
{{else if isEmbeddeds}}
<template v-if="router.hasRoute('{{embedded.name}}-id')">
<nuxt-link
v-for="{{lowercase embedded.title}} in item.{{embedded.name}}"
:key="{{lowercase embedded.title}}['@id']"
:to="{ name: '{{lowercase embedded.title}}s-id', params: { id: getIdFromIri({{lowercase embedded.title}}['@id']) } }"
class="text-blue-600 hover:text-blue-800"
>
\{{ {{lowercase embedded.title}}["@id"] }}

<br />
</nuxt-link>
</template>

<template v-else>
<p
v-for="{{lowercase embedded.title}} in item.{{embedded.name}}"
:key="{{lowercase embedded.title}}['@id']"
>
\{{ {{lowercase embedded.title}}["@id"] }}
</p>
</template>
{{else if embedded}}
<nuxt-link
v-if="router.hasRoute('{{embedded.name}}-id')"
:to="{ name: '{{lowercase embedded.title}}s-id', params: { id: getIdFromIri(item.{{lowercase embedded.title}}['@id']) } }"
class="text-blue-600 hover:text-blue-800"
>
\{{ item.{{lowercase embedded.title}}["@id"] }}
</nuxt-link>

<p v-else>
\{{ item.{{lowercase embedded.title}}["@id"] }}
</p>
{{else if (compare type "==" "dateTime") }}
\{{ formatDateTime(item.{{name}}) }}
{{else}}
\{{ item.{{name}} }}
{{/if}}
</td>
{{/each}}
<td class="px-6 py-4 text-sm">
<nuxt-link
:to="{ name: '{{lc}}s-id', params: { id: getIdFromIri(item['@id']) } }"
class="px-6 py-2 bg-blue-600 text-white text-xs rounded shadow-md hover:bg-blue-700"
>
Show
</nuxt-link>
</td>
<td class="px-6 py-4 text-sm">
<nuxt-link
:to="{ name: '{{lc}}s-id-edit', params: { id: getIdFromIri(item['@id']) } }"
class="px-6 py-2 bg-green-600 text-white text-xs rounded shadow-md hover:bg-green-700"
>
Edit
</nuxt-link>
</td>
</tr>
</tbody>
</table>
</div>

<div v-if="view" class="flex justify-center">
<nav aria-label="Page navigation">
<ul class="flex list-style-none">
<li :class="{ disabled: !pagination.previous }">
<nuxt-link
:to="{
name: '{{lc}}s-page-page',
params: { page: pagination.first },
}"
aria-label="First page"
:class="
!pagination.previous
? 'text-gray-500 pointer-events-none'
: 'text-gray-800 hover:bg-gray-200'
"
class="block py-2 px-3 rounded"
>
<span aria-hidden="true">&lArr;</span> First
</nuxt-link>
</li>

<li :class="{ disabled: !pagination.previous }">
<nuxt-link
:to="{
name: '{{lc}}s-page-page',
params: { page: pagination.previous ?? pagination.first },
}"
:class="
!pagination.previous
? 'text-gray-500 pointer-events-none'
: 'text-gray-800 hover:bg-gray-200'
"
class="block py-2 px-3 rounded"
aria-label="Previous page"
>
<span aria-hidden="true">&larr;</span> Previous
</nuxt-link>
</li>

<li :class="{ disabled: !pagination.next }">
<nuxt-link
:to="{
name: '{{lc}}s-page-page',
params: { page: pagination.next ?? pagination.last },
}"
:class="
!pagination.next
? 'text-gray-500 pointer-events-none'
: 'text-gray-800 hover:bg-gray-200'
"
class="block py-2 px-3 rounded"
aria-label="Next page"
>
Next <span aria-hidden="true">&rarr;</span>
</nuxt-link>
</li>

<li :class="{ disabled: !pagination.next }">
<nuxt-link
:to="{ name: '{{lc}}s-page-page', params: { page: pagination.last } }"
:class="
!pagination.next
? 'text-gray-500 pointer-events-none'
: 'text-gray-800 hover:bg-gray-200'
"
class="block py-2 px-3 rounded"
aria-label="Last page"
>
Last <span aria-hidden="true">&rArr;</span>
</nuxt-link>
</li>
</ul>
</nav>
</div>
</template>

<script lang="ts" setup>
import { storeToRefs } from "pinia";
import { useMercureList } from "~~/composables/mercureList";
import { use{{titleUcFirst}}DeleteStore } from "~~/stores/{{lc}}/delete";
import { use{{titleUcFirst}}ListStore } from "~~/stores/{{lc}}/list";
import { useFetchList } from "~~/composables/api";
import { getIdFromIri } from "~~/utils/resource";
import type { {{titleUcFirst}} } from "~~/types/{{lc}}";
{{#if hasRelations}}
const router = useRouter();
{{/if}}
const {{lc}}DeleteStore = use{{titleUcFirst}}DeleteStore();
const { deleted: deletedItem, mercureDeleted: mercureDeletedItem } =
storeToRefs({{lc}}DeleteStore);
const {{lc}}ListStore = use{{titleUcFirst}}ListStore();
const { items, view, error, isLoading, hubUrl } = await useFetchList<{{titleUcFirst}}>(
"{{name}}"
);
{{lc}}ListStore.setData({ items, view, error, isLoading, hubUrl });
const pagination = {
first: view.value?.["hydra:first"]?.slice(-1),
previous: view.value?.["hydra:previous"]?.slice(-1),
next: view.value?.["hydra:next"]?.slice(-1),
last: view.value?.["hydra:last"]?.slice(-1),
};
useMercureList({ store: {{lc}}ListStore, deleteStore: {{lc}}DeleteStore });
onBeforeUnmount(() => {
{{lc}}ListStore.$reset();
{{lc}}DeleteStore.$reset();
});
</script>
206 changes: 206 additions & 0 deletions templates/nuxt/components/foo/FooShow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<template>
<div class="flex items-center justify-between">
<nuxt-link
:to="{ name: '{{lc}}s' }"
class="text-blue-600 hover:text-blue-800"
>
&lt; Back to list
</nuxt-link>

<div>
<nuxt-link
v-if="item"
:to="{ name: '{{lc}}s-id-edit', params: { id: getIdFromIri(item['@id']) } }"
class="px-6 py-2 mr-2 bg-green-600 text-white text-xs rounded shadow-md hover:bg-green-700"
>
Edit
</nuxt-link>
<button
class="px-6 py-2 bg-red-600 text-white text-xs rounded shadow-md hover:bg-red-700"
@click="deleteItem"
>
Delete
</button>
</div>
</div>

<h1 class="text-3xl my-4">Show {{titleUcFirst}} \{{ item?.["@id"] }}</h1>

<div
v-if="isLoading"
class="bg-blue-100 rounded py-4 px-4 text-blue-700 text-sm"
role="status"
>
Loading...
</div>

<div
v-if="error || deleteError"
class="bg-red-100 rounded py-4 px-4 my-2 text-red-700 text-sm"
role="alert"
>
\{{ error || deleteError }}
</div>

<div v-if="item" class="overflow-x-auto">
<table class="min-w-full">
<thead class="border-b">
<tr>
<th scope="col" class="text-sm font-medium px-6 py-4 text-left">
Field
</th>
<th scope="col" class="text-sm font-medium px-6 py-4 text-left">
Value
</th>
</tr>
</thead>
<tbody>
{{#each fields}}
<tr class="border-b">
<th
class="text-sm font-medium px-6 py-4 text-left capitalize"
scope="row"
>
{{name}}
</th>
<td class="px-6 py-4 whitespace-nowrap text-sm">
{{#if isReferences}}
<template v-if="router.hasRoute('{{reference.name}}-id')">
<nuxt-link
v-for="{{lowercase reference.title}} in item.{{reference.name}}"
:key="{{lowercase reference.title}}"
:to="{ name: '{{lowercase reference.title}}s-id', params: { id: {{lowercase reference.title}} } }"
class="text-blue-600 hover:text-blue-800"
>
\{{ {{lowercase reference.title}} }}

<br />
</nuxt-link>
</template>

<template v-else>
<p
v-for="{{lowercase reference.title}} in item.{{reference.name}}"
:key="{{lowercase reference.title}}"
>
\{{ {{lowercase reference.title}} }}
</p>
</template>
{{else if reference}}
<nuxt-link
v-if="router.hasRoute('{{reference.name}}-id')"
:to="{ name: '{{lowercase reference.title}}s-id', params: { id: item.{{lowercase reference.title}} } }"
class="text-blue-600 hover:text-blue-800"
>
\{{ item.{{lowercase reference.title}} }}
</nuxt-link>

<p v-else>
\{{ item.{{lowercase reference.title}} }}
</p>
{{else if isEmbeddeds}}
<template v-if="router.hasRoute('{{embedded.name}}-id')">
<nuxt-link
v-for="{{lowercase embedded.title}} in item.{{embedded.name}}"
:key="{{lowercase embedded.title}}['@id']"
:to="{ name: '{{lowercase embedded.title}}s-id', params: { id: getIdFromIri({{lowercase embedded.title}}['@id']) } }"
class="text-blue-600 hover:text-blue-800"
>
\{{ {{lowercase embedded.title}}["@id"] }}

<br />
</nuxt-link>
</template>

<template v-else>
<p
v-for="{{lowercase embedded.title}} in item.{{embedded.name}}"
:key="{{lowercase embedded.title}}['@id']"
>
\{{ {{lowercase embedded.title}}["@id"] }}
</p>
</template>
{{else if embedded}}
<nuxt-link
v-if="router.hasRoute('{{embedded.name}}-id')"
:to="{ name: '{{lowercase embedded.title}}s-id', params: { id: getIdFromIri(item.{{lowercase embedded.title}}['@id']) } }"
class="text-blue-600 hover:text-blue-800"
>
\{{ item.{{lowercase embedded.title}}["@id"] }}
</nuxt-link>

<p v-else>
\{{ item.{{lowercase embedded.title}}["@id"] }}
</p>
{{else if (compare type "==" "dateTime") }}
\{{ formatDateTime(item.{{name}}) }}
{{else}}
\{{ item.{{name}} }}
{{/if}}
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</template>

<script lang="ts" setup>
import { storeToRefs } from "pinia";
import { use{{titleUcFirst}}ShowStore } from "~~/stores/{{lc}}/show";
import { use{{titleUcFirst}}DeleteStore } from "~~/stores/{{lc}}/delete";
import { useMercureItem } from "~~/composables/mercureItem";
import { useFetchItem } from "~~/composables/api";
import { getIdFromIri } from "~~/utils/resource";
import type { {{titleUcFirst}} } from "~~/types/{{lc}}";
const route = useRoute();
const router = useRouter();
const {{lc}}DeleteStore = use{{titleUcFirst}}DeleteStore();
const { error: deleteError, deleted } = storeToRefs({{lc}}DeleteStore);
const {{lc}}ShowStore = use{{titleUcFirst}}ShowStore();
useMercureItem({
store: {{lc}}ShowStore,
deleteStore: {{lc}}DeleteStore,
redirectRouteName: "{{lc}}s",
});
const id = decodeURIComponent(route.params.id as string);
const {
retrieved: item,
isLoading,
error,
hubUrl,
} = await useFetchItem<{{titleUcFirst}}>(`{{name}}/${id}`);
{{lc}}ShowStore.setData({ retrieved: item, isLoading, error, hubUrl });
async function deleteItem() {
if (!item?.value) {
{{lc}}DeleteStore.setError("No item found. Please reload");
return;
}
if (window.confirm("Are you sure you want to delete this {{lc}}?")) {
const { error } = await useDeleteItem(item.value);
if (error.value) {
{{lc}}DeleteStore.setError(error.value);
return;
}
{{lc}}DeleteStore.setDeleted(item.value);
{{lc}}DeleteStore.setMercureDeleted(undefined);
if (deleted) {
router.push({ name: "{{lc}}s" });
}
}
}
onBeforeUnmount(() => {
{{lc}}ShowStore.$reset();
});
</script>
136 changes: 136 additions & 0 deletions templates/nuxt/components/foo/FooUpdate.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<template>
<div class="flex items-center justify-between">
<nuxt-link
:to="{ name: '{{lc}}s' }"
class="text-blue-600 hover:text-blue-800"
>
&lt; Back to list
</nuxt-link>

<button
class="px-6 py-2 bg-red-600 text-white text-xs rounded shadow-md hover:bg-red-700"
@click="deleteItem"
>
Delete
</button>
</div>

<h1 class="text-3xl my-4">Edit {{titleUcFirst}} \{{ item?.["@id"] }}</h1>

<div
v-if="isLoading || deleteLoading"
class="bg-blue-100 rounded py-4 px-4 text-blue-700 text-sm"
role="status"
>
Loading...
</div>

<div
v-if="error || deleteError"
class="bg-red-100 rounded py-4 px-4 my-2 text-red-700 text-sm"
role="alert"
>
\{{ error || deleteError }}
</div>

<div
v-if="created || updated"
class="bg-green-100 rounded py-4 px-4 my-2 text-green-700 text-sm"
role="status"
>
<template v-if="updated">\{{ updated["@id"] }} updated.</template>
<template v-else-if="created">\{{ created["@id"] }} created.</template>
</div>

<Form :values="item" :errors="violations" @submit="update" />
</template>

<script lang="ts" setup>
import { Ref } from "vue";
import { storeToRefs } from "pinia";
import Form from "~~/components/{{lc}}/{{titleUcFirst}}Form.vue";
import { use{{titleUcFirst}}UpdateStore } from "~~/stores/{{lc}}/update";
import { use{{titleUcFirst}}CreateStore } from "~~/stores/{{lc}}/create";
import { use{{titleUcFirst}}DeleteStore } from "~~/stores/{{lc}}/delete";
import { useMercureItem } from "~~/composables/mercureItem";
import { useFetchItem, useUpdateItem } from "~~/composables/api";
import { SubmissionErrors } from "~~/types/error";
import type { {{titleUcFirst}} } from "~~/types/{{lc}}";
const route = useRoute();
const router = useRouter();
const {{lc}}CreateStore = use{{titleUcFirst}}CreateStore();
const { created } = storeToRefs({{lc}}CreateStore);
const {{lc}}DeleteStore = use{{titleUcFirst}}DeleteStore();
const { error: deleteError, deleted, isLoading: deleteLoading } =
storeToRefs({{lc}}DeleteStore);
const {{lc}}UpdateStore = use{{titleUcFirst}}UpdateStore();
useMercureItem({
store: {{lc}}UpdateStore,
deleteStore: {{lc}}DeleteStore,
redirectRouteName: "{{lc}}s",
});
const id = decodeURIComponent(route.params.id as string);
let updated: Ref<{{titleUcFirst}} | undefined> = ref(undefined);
let violations: Ref<SubmissionErrors | undefined> = ref(undefined);
let {
retrieved: item,
error,
isLoading,
hubUrl,
} = await useFetchItem<{{titleUcFirst}}>(`{{name}}/${id}`);
{{lc}}UpdateStore.setData({
retrieved: item,
error,
isLoading,
hubUrl,
});
async function update(payload: {{titleUcFirst}}) {
if (!item?.value) {
{{lc}}UpdateStore.setError("No item found. Please reload");
return;
}
const data = await useUpdateItem<{{titleUcFirst}}>(item.value, payload);
updated.value = data.updated.value;
violations.value = data.violations.value;
isLoading.value = data.isLoading.value;
error.value = data.error.value;
{{lc}}UpdateStore.setUpdateData(data);
}
async function deleteItem() {
if (!item?.value) {
{{lc}}DeleteStore.setError("No item found. Please reload");
return;
}
if (window.confirm("Are you sure you want to delete this {{lc}}?")) {
const { isLoading, error } = await useDeleteItem(item.value);
if (error.value) {
{{lc}}DeleteStore.setError(error.value);
return;
}
{{lc}}DeleteStore.setLoading(Boolean(isLoading?.value));
{{lc}}DeleteStore.setDeleted(item.value);
{{lc}}DeleteStore.setMercureDeleted(undefined);
if (deleted) {
router.push({ name: "{{lc}}s" });
}
}
}
onBeforeUnmount(() => {
{{lc}}UpdateStore.$reset();
{{lc}}CreateStore.$reset();
});
</script>
221 changes: 0 additions & 221 deletions templates/nuxt/components/foo/Form.vue

This file was deleted.

181 changes: 181 additions & 0 deletions templates/nuxt/composables/api.ts
Original file line number Diff line number Diff line change
@@ -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<T>(path: string, options: UseFetchOptions<T>) {
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<T>(
resource: string
): Promise<FetchAllData<T>> {
const route = useRoute();

const items: Ref<T[]> = ref([]);
const view: Ref<View | undefined> = ref(undefined);
const hubUrl: Ref<URL | undefined> = ref(undefined);

const page = ref(route.params.page);

const { data, pending, error } = await useApi<T>(resource, {
params: { page },

onResponse({ response }) {
hubUrl.value = extractHubURL(response);
},
});

const value = data.value as PagedCollection<T>;
items.value = value["hydra:member"];
view.value = value["hydra:view"];

return {
items,
view,
isLoading: pending,
error,
hubUrl,
};
}

export async function useFetchItem<T>(path: string): Promise<FetchItemData<T>> {
const retrieved: Ref<T | undefined> = ref(undefined);
const hubUrl: Ref<URL | undefined> = ref(undefined);

const { data, pending, error } = await useApi<T>(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<T>(resource: string, payload: Item) {
const created: Ref<T | undefined> = ref(undefined);
const violations: Ref<SubmissionErrors | undefined> = 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<T>(item: Item, payload: Item) {
const updated: Ref<T | undefined> = ref(undefined);
const violations: Ref<SubmissionErrors | undefined> = 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<string | undefined> = 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,
};
}
39 changes: 0 additions & 39 deletions templates/nuxt/mixins/create.js

This file was deleted.

78 changes: 0 additions & 78 deletions templates/nuxt/mixins/list.js

This file was deleted.

37 changes: 0 additions & 37 deletions templates/nuxt/mixins/notification.js

This file was deleted.

37 changes: 0 additions & 37 deletions templates/nuxt/mixins/show.js

This file was deleted.

75 changes: 0 additions & 75 deletions templates/nuxt/mixins/update.js

This file was deleted.

21 changes: 21 additions & 0 deletions templates/nuxt/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -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'
}
}
})
9 changes: 9 additions & 0 deletions templates/nuxt/pages/foos/[id]/edit.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<div class="container mx-auto px-4 max-w-2xl mt-4">
<Update />
</div>
</template>

<script lang="ts" setup>
import Update from "~~/components/{{lc}}/{{titleUcFirst}}Update.vue";
</script>
9 changes: 9 additions & 0 deletions templates/nuxt/pages/foos/[id]/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<div class="container mx-auto px-4 max-w-2xl mt-4">
<Show />
</div>
</template>

<script lang="ts" setup>
import Show from "~~/components/{{lc}}/{{titleUcFirst}}Show.vue";
</script>
66 changes: 0 additions & 66 deletions templates/nuxt/pages/foos/_id/edit.vue

This file was deleted.

77 changes: 0 additions & 77 deletions templates/nuxt/pages/foos/_id/index.vue

This file was deleted.

45 changes: 4 additions & 41 deletions templates/nuxt/pages/foos/create.vue
Original file line number Diff line number Diff line change
@@ -1,46 +1,9 @@
<template>
<div>
<Toolbar :list-href="`/${$options.servicePrefix}`" :handle-submit="onSendForm" :handle-reset="resetForm">
<template #left>
<h1>
Create {{{titleUcFirst}}}
</h1>
</template>
</Toolbar>
<{{{titleUcFirst}}}Form ref="createForm" :values="item" :errors="violations" />
<Loading :visible="isLoading" />
<div class="container mx-auto px-4 max-w-2xl mt-4">
<Create />
</div>
</template>

<script>
import { mapActions } from 'vuex';
import { createHelpers } from 'vuex-map-fields';
import create from '../../mixins/create';
const servicePrefix = '{{{lc}}}s';
const { mapFields } = createHelpers({
getterType: '{{{lc}}}/getField',
mutationType: '{{{lc}}}/updateField'
});
export default {
servicePrefix,
pathTemplate: `/${servicePrefix}/[id]`,
mixins: [create],
components: {
Loading: () => import('../../components/Loading'),
Toolbar: () => import('../../components/Toolbar'),
{{{titleUcFirst}}}Form: () => import('../../components/{{{lc}}}/Form')
},
data: () => ({
item: {}
}),
computed: {
...mapFields(['error', 'isLoading', 'created', 'violations'])
},
methods: {
...mapActions('{{{lc}}}', ['create', 'reset'])
}
};
<script lang="ts" setup>
import Create from "~~/components/{{lc}}/{{titleUcFirst}}Create.vue";
</script>
181 changes: 4 additions & 177 deletions templates/nuxt/pages/foos/index.vue
Original file line number Diff line number Diff line change
@@ -1,182 +1,9 @@
<template>
<div class="{{{lc}}}-list">
<Toolbar>
<template #left>
<h1>
{{{titleUcFirst}}} List
</h1>
</template>
</Toolbar>
<v-container grid-list-xl fluid>
<v-layout row wrap>
<v-flex lg12>
<v-data-table
v-model="selected"
:headers="headers"
:items="items"
:items-per-page.sync="options.itemsPerPage"
:loading="isLoading"
loading-text="Loading..."
:options.sync="options"
:server-items-length="totalItems"
class="elevation-1"
item-key="@id"
show-select
@update:options="onUpdateOptions"
:footer-props="{
showFirstLastPage: true
}"
>
<template v-slot:top>
<v-toolbar flat color="white">
<v-toolbar-title>{{{titleUcFirst}}}</v-toolbar-title>

<v-spacer></v-spacer>

{{#if parameters.length}}
<DataFilter :handle-filter="onSendFilter" :handle-reset="resetFilter">
<{{{titleUcFirst}}}FilterForm
ref="filterForm"
:values="filters"
slot="filter"
/>
</DataFilter>
{{/if}}

<v-btn
color="primary"
dark
class="ml-2"
:href="`/${$options.servicePrefix}/create`"
>
Create
</v-btn>
</v-toolbar>
</template>

<template slot="item.@id" slot-scope="{ item }">
<nuxt-link :to="getPath(item['@id'], '/{{{lc}}}s/[id]')">
\{{ item['@id'] }}
</nuxt-link>
</template>
{{#forEach fields~}}
{{#switch type~}}
{{#case "dateTime"}}
<template slot="item.{{{name}}}" slot-scope="{ item }">
\{{ formatDateTime(item['{{{name}}}'], 'long') }}
</template>
{{/case~}}
{{#case "date"}}
<template slot="item.{{{name}}}" slot-scope="{ item }">
\{{ formatDateTime(item['{{{name}}}'], 'short') }}
</template>
{{/case~}}
{{#case "number"}}
<template slot="item.{{{name}}}" slot-scope="{ item }">
\{{ $t(item['{{{name}}}']) }}
</template>
{{/case~}}
{{#default}}
{{#if reference}}
<template slot="item.{{{name}}}" slot-scope="{ item }">
{{#if maxCardinality }}
<nuxt-link :to="getPath(item['{{{name}}}'], '/{{{lowercase reference.title}}}s/[id]')">
\{{ item['{{{name}}}'] }}
</nuxt-link>
{{else}}
<ul>
<li v-for="_item in item['{{{name}}}']" :key="_item">
<nuxt-link :to="getPath(_item, '/{{{lowercase reference.title}}}s/[id]')">
\{{ _item }}
</nuxt-link>
</li>
</ul>
{{/if}}
</template>
{{/if~}}
{{#if embedded}}
<template slot="item.{{{name}}}" slot-scope="{ item }">
{{#if maxCardinality }}
<nuxt-link :to="getPath(item['{{{name}}}']['@id'], '/{{{lowercase embedded.title}}}s/[id]')">
\{{ item['{{{name}}}']['@id'] }}
</nuxt-link>
{{else}}
<ul>
<li v-for="_item in item['{{{name}}}']" :key="_item['@id']">
<nuxt-link :to="getPath(_item['@id'], '/{{{lowercase embedded.title}}}s/[id]')">
\{{ _item['@id'] }}
</nuxt-link>
</li>
</ul>
{{/if}}
</template>
{{/if~}}
{{/default~}}
{{/switch}}
{{/forEach }}

<ActionCell
slot="item.action"
slot-scope="props"
:show-href="getPath(props.item['@id'], '/{{{lc}}}s/[id]')"
:edit-href="getPath(props.item['@id'], '/{{{lc}}}s/[id]/edit')"
:handle-delete="() => deleteHandler(props.item)"
></ActionCell>
</v-data-table>
</v-flex>
</v-layout>
</v-container>
<div class="p-4">
<List />
</div>
</template>

<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import list from '../../mixins/list';
import { getPath } from '../../utils/fetch';

export default {
servicePrefix: '{{{lc}}}s',
mixins: [list],
components: {
Toolbar: () => import('../../components/Toolbar'),
ActionCell: () => import('../../components/ActionCell'),
{{{titleUcFirst}}}FilterForm: () => import('../../components/{{{lc}}}/Filter'),
DataFilter: () => import('../../components/DataFilter')
},
data: () => ({
headers: [
{ text: 'id', value: '@id' },
{{#forEach fields}}
{ text: '{{{name}}}', value: '{{{name}}}' },
{{/forEach}}
{
text: 'Actions',
value: 'action',
sortable: false
}
],
selected: []
}),
computed: {
...mapGetters('{{{lc}}}', {
items: 'list'
}),
...mapFields('{{{lc}}}', {
deletedItem: 'deleted',
error: 'error',
isLoading: 'isLoading',
resetList: 'resetList',
totalItems: 'totalItems',
view: 'view'
})
},
methods: {
...mapActions('{{{lc}}}', {
fetchAll: 'fetchAll',
deleteItem: 'del'
}),
getPath
}
};
<script lang="ts" setup>
import List from "~~/components/{{lc}}/{{titleUcFirst}}List.vue";
</script>
9 changes: 9 additions & 0 deletions templates/nuxt/pages/foos/page/[page].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<div class="p-4">
<List />
</div>
</template>

<script lang="ts" setup>
import List from "~~/components/{{lc}}/{{titleUcFirst}}List.vue";
</script>
5 changes: 5 additions & 0 deletions templates/nuxt/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div>
<NuxtWelcome />
</div>
</template>
273 changes: 0 additions & 273 deletions templates/nuxt/store/crud.js

This file was deleted.

6 changes: 0 additions & 6 deletions templates/nuxt/store/foo.js

This file was deleted.

Loading