diff --git a/generators/testdata/consts.go b/generators/testdata/consts.go
index bee57ea..bc48f10 100644
--- a/generators/testdata/consts.go
+++ b/generators/testdata/consts.go
@@ -16,25 +16,31 @@ const (
PackageVTUpdated = "vt-updated"
PackageDBTest = "test"
- PrefixAll = "all"
- PrefixEntity = "entities"
+ PrefixAll = "all"
+ PrefixEntity = "entities"
+ PrefixComposition = "composition"
+ PrefixVue3 = "vue3"
)
var (
PathActual = filepath.Join(DirParent, DirTestdata, "actual")
PathExpected = filepath.Join(DirParent, DirTestdata, "expected")
- PathActualMFD = filepath.Join(PathActual, FilenameMFD)
- PathExpectedMFD = filepath.Join(PathExpected, FilenameMFD)
- PathActualDB = filepath.Join(PathActual, PackageDB)
- PathExpectedDB = filepath.Join(PathExpected, PackageDB)
- PathActualVT = filepath.Join(PathActual, PackageVT)
- PathUpdatedVT = filepath.Join(PathExpected, PackageVTUpdated)
- PathExpectedVT = filepath.Join(PathExpected, PackageVT)
- PathActualVTTemplateAll = filepath.Join(PathActual, PackageVTTemplate, PrefixAll)
- PathExpectedVTTemplateAll = filepath.Join(PathExpected, PackageVTTemplate, PrefixAll)
- PathActualVTTemplateEntity = filepath.Join(PathActual, PackageVTTemplate, PrefixEntity)
- PathExpectedVTTemplateEntity = filepath.Join(PathExpected, PackageVTTemplate, PrefixEntity)
- PathActualDBTest = filepath.Join(PathActual, PackageDB, PackageDBTest)
- PathExpectedDBTest = filepath.Join(PathExpected, PackageDB, PackageDBTest)
+ PathActualMFD = filepath.Join(PathActual, FilenameMFD)
+ PathExpectedMFD = filepath.Join(PathExpected, FilenameMFD)
+ PathActualDB = filepath.Join(PathActual, PackageDB)
+ PathExpectedDB = filepath.Join(PathExpected, PackageDB)
+ PathActualVT = filepath.Join(PathActual, PackageVT)
+ PathUpdatedVT = filepath.Join(PathExpected, PackageVTUpdated)
+ PathExpectedVT = filepath.Join(PathExpected, PackageVT)
+ PathActualVTTemplateAll = filepath.Join(PathActual, PackageVTTemplate, PrefixAll)
+ PathExpectedVTTemplateAll = filepath.Join(PathExpected, PackageVTTemplate, PrefixAll)
+ PathActualVTTemplateEntity = filepath.Join(PathActual, PackageVTTemplate, PrefixEntity)
+ PathExpectedVTTemplateEntity = filepath.Join(PathExpected, PackageVTTemplate, PrefixEntity)
+ PathActualVTTemplateComposition = filepath.Join(PathActual, PackageVTTemplate, PrefixComposition)
+ PathExpectedVTTemplateComposition = filepath.Join(PathExpected, PackageVTTemplate, PrefixComposition)
+ PathActualVTTemplateVue3 = filepath.Join(PathActual, PackageVTTemplate, PrefixVue3)
+ PathExpectedVTTemplateVue3 = filepath.Join(PathExpected, PackageVTTemplate, PrefixVue3)
+ PathActualDBTest = filepath.Join(PathActual, PackageDB, PackageDBTest)
+ PathExpectedDBTest = filepath.Join(PathExpected, PackageDB, PackageDBTest)
)
diff --git a/generators/testdata/expected/media.vt.xml b/generators/testdata/expected/media.vt.xml
new file mode 100644
index 0000000..c81c585
--- /dev/null
+++ b/generators/testdata/expected/media.vt.xml
@@ -0,0 +1,23 @@
+
+ media
+
+
+ pages
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/media.xml b/generators/testdata/expected/media.xml
new file mode 100644
index 0000000..d362400
--- /dev/null
+++ b/generators/testdata/expected/media.xml
@@ -0,0 +1,18 @@
+
+ media
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/all/src/pages/Entity/Category/List.vue b/generators/testdata/expected/vt-template/all/src/pages/Entity/Category/List.vue
index f2e6d2a..f10db30 100644
--- a/generators/testdata/expected/vt-template/all/src/pages/Entity/Category/List.vue
+++ b/generators/testdata/expected/vt-template/all/src/pages/Entity/Category/List.vue
@@ -69,7 +69,7 @@
$t('category.list.filter.quickFilterPlaceholder')
"
hide-details
- @keyup.enter.native="submitFilters()"
+ @keyup.enter="submitFilters()"
/>
+
+
+
+
+
+
+ {{ model.title || "..." }}
+
+
+
+
+
+
+ arrow_back
+
+
+ {{ t("common.form.cancelButtonLabel") }}
+
+
+
+
+ delete
+
+
+
+
+
+
+ Основные
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ done
+
+ {{ t("common.form.saveAndCloseButtonLabel") }}
+
+
+
+ {{ t("common.form.saveButtonLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/composition/src/pages/Entity/Category/List.vue b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Category/List.vue
new file mode 100644
index 0000000..e990d6c
--- /dev/null
+++ b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Category/List.vue
@@ -0,0 +1,239 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t("category.list.title") }}
+
+
+ {{ pagination.totalItems }}
+
+
+
+
+
+
+ add
+ {{ t("common.list.addNewLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+ {{ item.orderNumber }}
+
+
+
+
+
+
+
+
+
+
+ delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/composition/src/pages/Entity/Category/components/MultiListFilters.vue b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Category/components/MultiListFilters.vue
new file mode 100644
index 0000000..a6fedaf
--- /dev/null
+++ b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Category/components/MultiListFilters.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/composition/src/pages/Entity/Category/en.json b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Category/en.json
new file mode 100644
index 0000000..33b0135
--- /dev/null
+++ b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Category/en.json
@@ -0,0 +1,30 @@
+{
+ "breadcrumbs": {
+ "categoryAdd": "Add",
+ "categoryEdit": "Edit",
+ "categoryList": "Categories"
+ },
+ "category": {
+ "form": {
+ "orderNumberLabel": "Order Number",
+ "statusIdLabel": "Status",
+ "titleLabel": "Title"
+ },
+ "list": {
+ "title": "Categories",
+ "filter": {
+ "ids": "Ids",
+ "orderNumber": "Order Number",
+ "quickFilterPlaceholder": "",
+ "statusId": "Status",
+ "title": "Title"
+ },
+ "headers": {
+ "actions": "Actions",
+ "orderNumber": "Order Number",
+ "status": "Status",
+ "title": "Title"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/generators/testdata/expected/vt-template/composition/src/pages/Entity/News/Form.vue b/generators/testdata/expected/vt-template/composition/src/pages/Entity/News/Form.vue
new file mode 100644
index 0000000..ff0c74e
--- /dev/null
+++ b/generators/testdata/expected/vt-template/composition/src/pages/Entity/News/Form.vue
@@ -0,0 +1,282 @@
+
+
+
+
+
+
+
+ {{ model.title || "..." }}
+
+
+
+
+
+
+ arrow_back
+
+
+ {{ t("common.form.cancelButtonLabel") }}
+
+
+
+
+ delete
+
+
+
+
+
+
+ Основные
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ done
+
+ {{ t("common.form.saveAndCloseButtonLabel") }}
+
+
+
+ {{ t("common.form.saveButtonLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/composition/src/pages/Entity/News/List.vue b/generators/testdata/expected/vt-template/composition/src/pages/Entity/News/List.vue
new file mode 100644
index 0000000..07b75fa
--- /dev/null
+++ b/generators/testdata/expected/vt-template/composition/src/pages/Entity/News/List.vue
@@ -0,0 +1,270 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t("news.list.title") }}
+
+
+ {{ pagination.totalItems }}
+
+
+
+
+
+
+ add
+ {{ t("common.list.addNewLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+ {{ item.preview }}
+
+
+ {{ item.content }}
+
+
+ {{ item.category | getField("title") }}
+
+
+ {{ item.country | getField("title") }}
+
+
+ {{ item.region | getField("title") }}
+
+
+
+
+
+
+
+
+
+
+ delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/composition/src/pages/Entity/News/components/MultiListFilters.vue b/generators/testdata/expected/vt-template/composition/src/pages/Entity/News/components/MultiListFilters.vue
new file mode 100644
index 0000000..7f1ec20
--- /dev/null
+++ b/generators/testdata/expected/vt-template/composition/src/pages/Entity/News/components/MultiListFilters.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/composition/src/pages/Entity/News/en.json b/generators/testdata/expected/vt-template/composition/src/pages/Entity/News/en.json
new file mode 100644
index 0000000..3ee7e56
--- /dev/null
+++ b/generators/testdata/expected/vt-template/composition/src/pages/Entity/News/en.json
@@ -0,0 +1,48 @@
+{
+ "breadcrumbs": {
+ "newsAdd": "Add",
+ "newsEdit": "Edit",
+ "newsList": "News"
+ },
+ "news": {
+ "form": {
+ "categoryIdLabel": "Category",
+ "cityIdLabel": "City",
+ "contentLabel": "Content",
+ "countryIdLabel": "Country",
+ "previewLabel": "Preview",
+ "publishedAtLabel": "Published At",
+ "regionIdLabel": "Region",
+ "statusIdLabel": "Status",
+ "tagIdsLabel": "Tags",
+ "titleLabel": "Title"
+ },
+ "list": {
+ "title": "News",
+ "filter": {
+ "categoryId": "Category",
+ "cityId": "City",
+ "content": "Content",
+ "countryId": "Country",
+ "createdAt": "Created at",
+ "ids": "Ids",
+ "preview": "Preview",
+ "publishedAt": "Published At",
+ "quickFilterPlaceholder": "",
+ "regionId": "Region",
+ "statusId": "Status",
+ "title": "Title"
+ },
+ "headers": {
+ "actions": "Actions",
+ "category": "Category",
+ "content": "Content",
+ "country": "Country",
+ "preview": "Preview",
+ "region": "Region",
+ "status": "Status",
+ "title": "Title"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/generators/testdata/expected/vt-template/composition/src/pages/Entity/Tag/Form.vue b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Tag/Form.vue
new file mode 100644
index 0000000..6630576
--- /dev/null
+++ b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Tag/Form.vue
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+ {{ model.title || "..." }}
+
+
+
+
+
+
+ arrow_back
+
+
+ {{ t("common.form.cancelButtonLabel") }}
+
+
+
+
+ delete
+
+
+
+
+
+
+ Основные
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ done
+
+ {{ t("common.form.saveAndCloseButtonLabel") }}
+
+
+
+ {{ t("common.form.saveButtonLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/composition/src/pages/Entity/Tag/List.vue b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Tag/List.vue
new file mode 100644
index 0000000..a781213
--- /dev/null
+++ b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Tag/List.vue
@@ -0,0 +1,232 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t("tag.list.title") }}
+
+
+ {{ pagination.totalItems }}
+
+
+
+
+
+
+ add
+ {{ t("common.list.addNewLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+ delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/composition/src/pages/Entity/Tag/components/MultiListFilters.vue b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Tag/components/MultiListFilters.vue
new file mode 100644
index 0000000..523e594
--- /dev/null
+++ b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Tag/components/MultiListFilters.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/composition/src/pages/Entity/Tag/en.json b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Tag/en.json
new file mode 100644
index 0000000..d408aad
--- /dev/null
+++ b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Tag/en.json
@@ -0,0 +1,27 @@
+{
+ "breadcrumbs": {
+ "tagAdd": "Add",
+ "tagEdit": "Edit",
+ "tagList": "Tags"
+ },
+ "tag": {
+ "form": {
+ "statusIdLabel": "Status",
+ "titleLabel": "Title"
+ },
+ "list": {
+ "title": "Tags",
+ "filter": {
+ "ids": "Ids",
+ "quickFilterPlaceholder": "",
+ "statusId": "Status",
+ "title": "Title"
+ },
+ "headers": {
+ "actions": "Actions",
+ "status": "Status",
+ "title": "Title"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/generators/testdata/expected/vt-template/composition/src/pages/Entity/routes.ts b/generators/testdata/expected/vt-template/composition/src/pages/Entity/routes.ts
new file mode 100644
index 0000000..198b28f
--- /dev/null
+++ b/generators/testdata/expected/vt-template/composition/src/pages/Entity/routes.ts
@@ -0,0 +1,344 @@
+/* eslint-disable */
+export default [
+ /* Category */
+ {
+ name: "categoryList",
+ path: "/categories",
+ component: () =>
+ import("@/pages/Entity/Category/List.vue"),
+ meta: {
+ title: "CategoryList",
+ breadcrumbs: ["dashboard", "categoryList"]
+ }
+ },
+ {
+ name: "categoryEdit",
+ path: "/categories/:id/edit",
+ component: () =>
+ import("@/pages/Entity/Category/Form.vue"),
+ meta: {
+ title: "CategoryEdit",
+ breadcrumbs: ["dashboard", "categoryList", "categoryEdit"]
+ }
+ },
+ {
+ name: "categoryAdd",
+ path: "/categories/add",
+ component: () =>
+ import("@/pages/Entity/Category/Form.vue"),
+ meta: {
+ title: "CategoryAdd",
+ breadcrumbs: ["dashboard", "categoryList", "categoryAdd"]
+ }
+ },
+ /* News */
+ {
+ name: "newsList",
+ path: "/news",
+ component: () =>
+ import("@/pages/Entity/News/List.vue"),
+ meta: {
+ title: "NewsList",
+ breadcrumbs: ["dashboard", "newsList"]
+ }
+ },
+ {
+ name: "newsEdit",
+ path: "/news/:id/edit",
+ component: () =>
+ import("@/pages/Entity/News/Form.vue"),
+ meta: {
+ title: "NewsEdit",
+ breadcrumbs: ["dashboard", "newsList", "newsEdit"]
+ }
+ },
+ {
+ name: "newsAdd",
+ path: "/news/add",
+ component: () =>
+ import("@/pages/Entity/News/Form.vue"),
+ meta: {
+ title: "NewsAdd",
+ breadcrumbs: ["dashboard", "newsList", "newsAdd"]
+ }
+ },
+ /* Tag */
+ {
+ name: "tagList",
+ path: "/tags",
+ component: () =>
+ import("@/pages/Entity/Tag/List.vue"),
+ meta: {
+ title: "TagList",
+ breadcrumbs: ["dashboard", "tagList"]
+ }
+ },
+ {
+ name: "tagEdit",
+ path: "/tags/:id/edit",
+ component: () =>
+ import("@/pages/Entity/Tag/Form.vue"),
+ meta: {
+ title: "TagEdit",
+ breadcrumbs: ["dashboard", "tagList", "tagEdit"]
+ }
+ },
+ {
+ name: "tagAdd",
+ path: "/tags/add",
+ component: () =>
+ import("@/pages/Entity/Tag/Form.vue"),
+ meta: {
+ title: "TagAdd",
+ breadcrumbs: ["dashboard", "tagList", "tagAdd"]
+ }
+ },
+ /* City */
+ {
+ name: "cityList",
+ path: "/cities",
+ component: () =>
+ import("@/pages/Entity/City/List.vue"),
+ meta: {
+ title: "CityList",
+ breadcrumbs: ["dashboard", "cityList"]
+ }
+ },
+ {
+ name: "cityEdit",
+ path: "/cities/:id/edit",
+ component: () =>
+ import("@/pages/Entity/City/Form.vue"),
+ meta: {
+ title: "CityEdit",
+ breadcrumbs: ["dashboard", "cityList", "cityEdit"]
+ }
+ },
+ {
+ name: "cityAdd",
+ path: "/cities/add",
+ component: () =>
+ import("@/pages/Entity/City/Form.vue"),
+ meta: {
+ title: "CityAdd",
+ breadcrumbs: ["dashboard", "cityList", "cityAdd"]
+ }
+ },
+ /* Country */
+ {
+ name: "countryList",
+ path: "/countries",
+ component: () =>
+ import("@/pages/Entity/Country/List.vue"),
+ meta: {
+ title: "CountryList",
+ breadcrumbs: ["dashboard", "countryList"]
+ }
+ },
+ {
+ name: "countryEdit",
+ path: "/countries/:id/edit",
+ component: () =>
+ import("@/pages/Entity/Country/Form.vue"),
+ meta: {
+ title: "CountryEdit",
+ breadcrumbs: ["dashboard", "countryList", "countryEdit"]
+ }
+ },
+ {
+ name: "countryAdd",
+ path: "/countries/add",
+ component: () =>
+ import("@/pages/Entity/Country/Form.vue"),
+ meta: {
+ title: "CountryAdd",
+ breadcrumbs: ["dashboard", "countryList", "countryAdd"]
+ }
+ },
+ /* Region */
+ {
+ name: "regionList",
+ path: "/regions",
+ component: () =>
+ import("@/pages/Entity/Region/List.vue"),
+ meta: {
+ title: "RegionList",
+ breadcrumbs: ["dashboard", "regionList"]
+ }
+ },
+ {
+ name: "regionEdit",
+ path: "/regions/:id/edit",
+ component: () =>
+ import("@/pages/Entity/Region/Form.vue"),
+ meta: {
+ title: "RegionEdit",
+ breadcrumbs: ["dashboard", "regionList", "regionEdit"]
+ }
+ },
+ {
+ name: "regionAdd",
+ path: "/regions/add",
+ component: () =>
+ import("@/pages/Entity/Region/Form.vue"),
+ meta: {
+ title: "RegionAdd",
+ breadcrumbs: ["dashboard", "regionList", "regionAdd"]
+ }
+ },
+ /* EncryptionKey */
+ {
+ name: "encryptionKeyList",
+ path: "/encryption-keys",
+ component: () =>
+ import("@/pages/Entity/EncryptionKey/List.vue"),
+ meta: {
+ title: "EncryptionKeyList",
+ breadcrumbs: ["dashboard", "encryptionKeyList"]
+ }
+ },
+ {
+ name: "encryptionKeyEdit",
+ path: "/encryption-keys/:id/edit",
+ component: () =>
+ import("@/pages/Entity/EncryptionKey/Form.vue"),
+ meta: {
+ title: "EncryptionKeyEdit",
+ breadcrumbs: ["dashboard", "encryptionKeyList", "encryptionKeyEdit"]
+ }
+ },
+ {
+ name: "encryptionKeyAdd",
+ path: "/encryption-keys/add",
+ component: () =>
+ import("@/pages/Entity/EncryptionKey/Form.vue"),
+ meta: {
+ title: "EncryptionKeyAdd",
+ breadcrumbs: ["dashboard", "encryptionKeyList", "encryptionKeyAdd"]
+ }
+ },
+ /* LoginCode */
+ {
+ name: "loginCodeList",
+ path: "/login-codes",
+ component: () =>
+ import("@/pages/Entity/LoginCode/List.vue"),
+ meta: {
+ title: "LoginCodeList",
+ breadcrumbs: ["dashboard", "loginCodeList"]
+ }
+ },
+ {
+ name: "loginCodeEdit",
+ path: "/login-codes/:id/edit",
+ component: () =>
+ import("@/pages/Entity/LoginCode/Form.vue"),
+ meta: {
+ title: "LoginCodeEdit",
+ breadcrumbs: ["dashboard", "loginCodeList", "loginCodeEdit"]
+ }
+ },
+ {
+ name: "loginCodeAdd",
+ path: "/login-codes/add",
+ component: () =>
+ import("@/pages/Entity/LoginCode/Form.vue"),
+ meta: {
+ title: "LoginCodeAdd",
+ breadcrumbs: ["dashboard", "loginCodeList", "loginCodeAdd"]
+ }
+ },
+ /* SiteUser */
+ {
+ name: "siteUserList",
+ path: "/site-users",
+ component: () =>
+ import("@/pages/Entity/SiteUser/List.vue"),
+ meta: {
+ title: "SiteUserList",
+ breadcrumbs: ["dashboard", "siteUserList"]
+ }
+ },
+ {
+ name: "siteUserEdit",
+ path: "/site-users/:id/edit",
+ component: () =>
+ import("@/pages/Entity/SiteUser/Form.vue"),
+ meta: {
+ title: "SiteUserEdit",
+ breadcrumbs: ["dashboard", "siteUserList", "siteUserEdit"]
+ }
+ },
+ {
+ name: "siteUserAdd",
+ path: "/site-users/add",
+ component: () =>
+ import("@/pages/Entity/SiteUser/Form.vue"),
+ meta: {
+ title: "SiteUserAdd",
+ breadcrumbs: ["dashboard", "siteUserList", "siteUserAdd"]
+ }
+ },
+ /* VfsFile */
+ {
+ name: "vfsFileList",
+ path: "/vfs-files",
+ component: () =>
+ import("@/pages/Entity/VfsFile/List.vue"),
+ meta: {
+ title: "VfsFileList",
+ breadcrumbs: ["dashboard", "vfsFileList"]
+ }
+ },
+ {
+ name: "vfsFileEdit",
+ path: "/vfs-files/:id/edit",
+ component: () =>
+ import("@/pages/Entity/VfsFile/Form.vue"),
+ meta: {
+ title: "VfsFileEdit",
+ breadcrumbs: ["dashboard", "vfsFileList", "vfsFileEdit"]
+ }
+ },
+ {
+ name: "vfsFileAdd",
+ path: "/vfs-files/add",
+ component: () =>
+ import("@/pages/Entity/VfsFile/Form.vue"),
+ meta: {
+ title: "VfsFileAdd",
+ breadcrumbs: ["dashboard", "vfsFileList", "vfsFileAdd"]
+ }
+ },
+ /* VfsFolder */
+ {
+ name: "vfsFolderList",
+ path: "/vfs-folders",
+ component: () =>
+ import("@/pages/Entity/VfsFolder/List.vue"),
+ meta: {
+ title: "VfsFolderList",
+ breadcrumbs: ["dashboard", "vfsFolderList"]
+ }
+ },
+ {
+ name: "vfsFolderEdit",
+ path: "/vfs-folders/:id/edit",
+ component: () =>
+ import("@/pages/Entity/VfsFolder/Form.vue"),
+ meta: {
+ title: "VfsFolderEdit",
+ breadcrumbs: ["dashboard", "vfsFolderList", "vfsFolderEdit"]
+ }
+ },
+ {
+ name: "vfsFolderAdd",
+ path: "/vfs-folders/add",
+ component: () =>
+ import("@/pages/Entity/VfsFolder/Form.vue"),
+ meta: {
+ title: "VfsFolderAdd",
+ breadcrumbs: ["dashboard", "vfsFolderList", "vfsFolderAdd"]
+ }
+ },
+];
diff --git a/generators/testdata/expected/vt-template/entities/src/pages/Entity/Category/List.vue b/generators/testdata/expected/vt-template/entities/src/pages/Entity/Category/List.vue
index f2e6d2a..f10db30 100644
--- a/generators/testdata/expected/vt-template/entities/src/pages/Entity/Category/List.vue
+++ b/generators/testdata/expected/vt-template/entities/src/pages/Entity/Category/List.vue
@@ -69,7 +69,7 @@
$t('category.list.filter.quickFilterPlaceholder')
"
hide-details
- @keyup.enter.native="submitFilters()"
+ @keyup.enter="submitFilters()"
/>
+
+
+
+
+
+
+ {{ model.title || "..." }}
+
+
+
+
+
+
+ mdi-arrow-left
+
+
+ {{ t("common.form.cancelButtonLabel") }}
+
+
+
+
+ mdi-delete
+
+
+
+
+
+
+
+ Основные
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-check
+ {{ t("common.form.saveAndCloseButtonLabel") }}
+
+
+
+ {{ t("common.form.saveButtonLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Category/List.vue b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Category/List.vue
new file mode 100644
index 0000000..0d34323
--- /dev/null
+++ b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Category/List.vue
@@ -0,0 +1,243 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t("category.list.title") }}
+
+
+ {{ pagination.totalItems }}
+
+
+
+
+
+
+ mdi-plus
+ {{ t("common.list.addNewLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+ {{ item.orderNumber }}
+
+
+
+
+
+
+
+
+
+
+ mdi-delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Category/components/MultiListFilters.vue b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Category/components/MultiListFilters.vue
new file mode 100644
index 0000000..a6fedaf
--- /dev/null
+++ b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Category/components/MultiListFilters.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Category/en.json b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Category/en.json
new file mode 100644
index 0000000..33b0135
--- /dev/null
+++ b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Category/en.json
@@ -0,0 +1,30 @@
+{
+ "breadcrumbs": {
+ "categoryAdd": "Add",
+ "categoryEdit": "Edit",
+ "categoryList": "Categories"
+ },
+ "category": {
+ "form": {
+ "orderNumberLabel": "Order Number",
+ "statusIdLabel": "Status",
+ "titleLabel": "Title"
+ },
+ "list": {
+ "title": "Categories",
+ "filter": {
+ "ids": "Ids",
+ "orderNumber": "Order Number",
+ "quickFilterPlaceholder": "",
+ "statusId": "Status",
+ "title": "Title"
+ },
+ "headers": {
+ "actions": "Actions",
+ "orderNumber": "Order Number",
+ "status": "Status",
+ "title": "Title"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/generators/testdata/expected/vt-template/vue3/src/pages/Entity/News/Form.vue b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/News/Form.vue
new file mode 100644
index 0000000..16f3dec
--- /dev/null
+++ b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/News/Form.vue
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+
+ {{ model.title || "..." }}
+
+
+
+
+
+
+ mdi-arrow-left
+
+
+ {{ t("common.form.cancelButtonLabel") }}
+
+
+
+
+ mdi-delete
+
+
+
+
+
+
+
+ Основные
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-check
+ {{ t("common.form.saveAndCloseButtonLabel") }}
+
+
+
+ {{ t("common.form.saveButtonLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/vue3/src/pages/Entity/News/List.vue b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/News/List.vue
new file mode 100644
index 0000000..177ee8e
--- /dev/null
+++ b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/News/List.vue
@@ -0,0 +1,278 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t("news.list.title") }}
+
+
+ {{ pagination.totalItems }}
+
+
+
+
+
+
+ mdi-plus
+ {{ t("common.list.addNewLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+ {{ item.preview }}
+
+
+ {{ item.content }}
+
+
+ {{ item.category?.title }}
+
+
+ {{ item.country?.title }}
+
+
+ {{ item.region?.title }}
+
+
+
+
+
+
+
+
+
+
+ mdi-delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/vue3/src/pages/Entity/News/components/MultiListFilters.vue b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/News/components/MultiListFilters.vue
new file mode 100644
index 0000000..7f1ec20
--- /dev/null
+++ b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/News/components/MultiListFilters.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/vue3/src/pages/Entity/News/en.json b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/News/en.json
new file mode 100644
index 0000000..3ee7e56
--- /dev/null
+++ b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/News/en.json
@@ -0,0 +1,48 @@
+{
+ "breadcrumbs": {
+ "newsAdd": "Add",
+ "newsEdit": "Edit",
+ "newsList": "News"
+ },
+ "news": {
+ "form": {
+ "categoryIdLabel": "Category",
+ "cityIdLabel": "City",
+ "contentLabel": "Content",
+ "countryIdLabel": "Country",
+ "previewLabel": "Preview",
+ "publishedAtLabel": "Published At",
+ "regionIdLabel": "Region",
+ "statusIdLabel": "Status",
+ "tagIdsLabel": "Tags",
+ "titleLabel": "Title"
+ },
+ "list": {
+ "title": "News",
+ "filter": {
+ "categoryId": "Category",
+ "cityId": "City",
+ "content": "Content",
+ "countryId": "Country",
+ "createdAt": "Created at",
+ "ids": "Ids",
+ "preview": "Preview",
+ "publishedAt": "Published At",
+ "quickFilterPlaceholder": "",
+ "regionId": "Region",
+ "statusId": "Status",
+ "title": "Title"
+ },
+ "headers": {
+ "actions": "Actions",
+ "category": "Category",
+ "content": "Content",
+ "country": "Country",
+ "preview": "Preview",
+ "region": "Region",
+ "status": "Status",
+ "title": "Title"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Tag/Form.vue b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Tag/Form.vue
new file mode 100644
index 0000000..1c5c3e2
--- /dev/null
+++ b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Tag/Form.vue
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+ {{ model.title || "..." }}
+
+
+
+
+
+
+ mdi-arrow-left
+
+
+ {{ t("common.form.cancelButtonLabel") }}
+
+
+
+
+ mdi-delete
+
+
+
+
+
+
+
+ Основные
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-check
+ {{ t("common.form.saveAndCloseButtonLabel") }}
+
+
+
+ {{ t("common.form.saveButtonLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Tag/List.vue b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Tag/List.vue
new file mode 100644
index 0000000..17999e3
--- /dev/null
+++ b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Tag/List.vue
@@ -0,0 +1,235 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t("tag.list.title") }}
+
+
+ {{ pagination.totalItems }}
+
+
+
+
+
+
+ mdi-plus
+ {{ t("common.list.addNewLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+ mdi-delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Tag/components/MultiListFilters.vue b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Tag/components/MultiListFilters.vue
new file mode 100644
index 0000000..523e594
--- /dev/null
+++ b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Tag/components/MultiListFilters.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
diff --git a/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Tag/en.json b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Tag/en.json
new file mode 100644
index 0000000..d408aad
--- /dev/null
+++ b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/Tag/en.json
@@ -0,0 +1,27 @@
+{
+ "breadcrumbs": {
+ "tagAdd": "Add",
+ "tagEdit": "Edit",
+ "tagList": "Tags"
+ },
+ "tag": {
+ "form": {
+ "statusIdLabel": "Status",
+ "titleLabel": "Title"
+ },
+ "list": {
+ "title": "Tags",
+ "filter": {
+ "ids": "Ids",
+ "quickFilterPlaceholder": "",
+ "statusId": "Status",
+ "title": "Title"
+ },
+ "headers": {
+ "actions": "Actions",
+ "status": "Status",
+ "title": "Title"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/generators/testdata/expected/vt-template/vue3/src/pages/Entity/routes.ts b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/routes.ts
new file mode 100644
index 0000000..198b28f
--- /dev/null
+++ b/generators/testdata/expected/vt-template/vue3/src/pages/Entity/routes.ts
@@ -0,0 +1,344 @@
+/* eslint-disable */
+export default [
+ /* Category */
+ {
+ name: "categoryList",
+ path: "/categories",
+ component: () =>
+ import("@/pages/Entity/Category/List.vue"),
+ meta: {
+ title: "CategoryList",
+ breadcrumbs: ["dashboard", "categoryList"]
+ }
+ },
+ {
+ name: "categoryEdit",
+ path: "/categories/:id/edit",
+ component: () =>
+ import("@/pages/Entity/Category/Form.vue"),
+ meta: {
+ title: "CategoryEdit",
+ breadcrumbs: ["dashboard", "categoryList", "categoryEdit"]
+ }
+ },
+ {
+ name: "categoryAdd",
+ path: "/categories/add",
+ component: () =>
+ import("@/pages/Entity/Category/Form.vue"),
+ meta: {
+ title: "CategoryAdd",
+ breadcrumbs: ["dashboard", "categoryList", "categoryAdd"]
+ }
+ },
+ /* News */
+ {
+ name: "newsList",
+ path: "/news",
+ component: () =>
+ import("@/pages/Entity/News/List.vue"),
+ meta: {
+ title: "NewsList",
+ breadcrumbs: ["dashboard", "newsList"]
+ }
+ },
+ {
+ name: "newsEdit",
+ path: "/news/:id/edit",
+ component: () =>
+ import("@/pages/Entity/News/Form.vue"),
+ meta: {
+ title: "NewsEdit",
+ breadcrumbs: ["dashboard", "newsList", "newsEdit"]
+ }
+ },
+ {
+ name: "newsAdd",
+ path: "/news/add",
+ component: () =>
+ import("@/pages/Entity/News/Form.vue"),
+ meta: {
+ title: "NewsAdd",
+ breadcrumbs: ["dashboard", "newsList", "newsAdd"]
+ }
+ },
+ /* Tag */
+ {
+ name: "tagList",
+ path: "/tags",
+ component: () =>
+ import("@/pages/Entity/Tag/List.vue"),
+ meta: {
+ title: "TagList",
+ breadcrumbs: ["dashboard", "tagList"]
+ }
+ },
+ {
+ name: "tagEdit",
+ path: "/tags/:id/edit",
+ component: () =>
+ import("@/pages/Entity/Tag/Form.vue"),
+ meta: {
+ title: "TagEdit",
+ breadcrumbs: ["dashboard", "tagList", "tagEdit"]
+ }
+ },
+ {
+ name: "tagAdd",
+ path: "/tags/add",
+ component: () =>
+ import("@/pages/Entity/Tag/Form.vue"),
+ meta: {
+ title: "TagAdd",
+ breadcrumbs: ["dashboard", "tagList", "tagAdd"]
+ }
+ },
+ /* City */
+ {
+ name: "cityList",
+ path: "/cities",
+ component: () =>
+ import("@/pages/Entity/City/List.vue"),
+ meta: {
+ title: "CityList",
+ breadcrumbs: ["dashboard", "cityList"]
+ }
+ },
+ {
+ name: "cityEdit",
+ path: "/cities/:id/edit",
+ component: () =>
+ import("@/pages/Entity/City/Form.vue"),
+ meta: {
+ title: "CityEdit",
+ breadcrumbs: ["dashboard", "cityList", "cityEdit"]
+ }
+ },
+ {
+ name: "cityAdd",
+ path: "/cities/add",
+ component: () =>
+ import("@/pages/Entity/City/Form.vue"),
+ meta: {
+ title: "CityAdd",
+ breadcrumbs: ["dashboard", "cityList", "cityAdd"]
+ }
+ },
+ /* Country */
+ {
+ name: "countryList",
+ path: "/countries",
+ component: () =>
+ import("@/pages/Entity/Country/List.vue"),
+ meta: {
+ title: "CountryList",
+ breadcrumbs: ["dashboard", "countryList"]
+ }
+ },
+ {
+ name: "countryEdit",
+ path: "/countries/:id/edit",
+ component: () =>
+ import("@/pages/Entity/Country/Form.vue"),
+ meta: {
+ title: "CountryEdit",
+ breadcrumbs: ["dashboard", "countryList", "countryEdit"]
+ }
+ },
+ {
+ name: "countryAdd",
+ path: "/countries/add",
+ component: () =>
+ import("@/pages/Entity/Country/Form.vue"),
+ meta: {
+ title: "CountryAdd",
+ breadcrumbs: ["dashboard", "countryList", "countryAdd"]
+ }
+ },
+ /* Region */
+ {
+ name: "regionList",
+ path: "/regions",
+ component: () =>
+ import("@/pages/Entity/Region/List.vue"),
+ meta: {
+ title: "RegionList",
+ breadcrumbs: ["dashboard", "regionList"]
+ }
+ },
+ {
+ name: "regionEdit",
+ path: "/regions/:id/edit",
+ component: () =>
+ import("@/pages/Entity/Region/Form.vue"),
+ meta: {
+ title: "RegionEdit",
+ breadcrumbs: ["dashboard", "regionList", "regionEdit"]
+ }
+ },
+ {
+ name: "regionAdd",
+ path: "/regions/add",
+ component: () =>
+ import("@/pages/Entity/Region/Form.vue"),
+ meta: {
+ title: "RegionAdd",
+ breadcrumbs: ["dashboard", "regionList", "regionAdd"]
+ }
+ },
+ /* EncryptionKey */
+ {
+ name: "encryptionKeyList",
+ path: "/encryption-keys",
+ component: () =>
+ import("@/pages/Entity/EncryptionKey/List.vue"),
+ meta: {
+ title: "EncryptionKeyList",
+ breadcrumbs: ["dashboard", "encryptionKeyList"]
+ }
+ },
+ {
+ name: "encryptionKeyEdit",
+ path: "/encryption-keys/:id/edit",
+ component: () =>
+ import("@/pages/Entity/EncryptionKey/Form.vue"),
+ meta: {
+ title: "EncryptionKeyEdit",
+ breadcrumbs: ["dashboard", "encryptionKeyList", "encryptionKeyEdit"]
+ }
+ },
+ {
+ name: "encryptionKeyAdd",
+ path: "/encryption-keys/add",
+ component: () =>
+ import("@/pages/Entity/EncryptionKey/Form.vue"),
+ meta: {
+ title: "EncryptionKeyAdd",
+ breadcrumbs: ["dashboard", "encryptionKeyList", "encryptionKeyAdd"]
+ }
+ },
+ /* LoginCode */
+ {
+ name: "loginCodeList",
+ path: "/login-codes",
+ component: () =>
+ import("@/pages/Entity/LoginCode/List.vue"),
+ meta: {
+ title: "LoginCodeList",
+ breadcrumbs: ["dashboard", "loginCodeList"]
+ }
+ },
+ {
+ name: "loginCodeEdit",
+ path: "/login-codes/:id/edit",
+ component: () =>
+ import("@/pages/Entity/LoginCode/Form.vue"),
+ meta: {
+ title: "LoginCodeEdit",
+ breadcrumbs: ["dashboard", "loginCodeList", "loginCodeEdit"]
+ }
+ },
+ {
+ name: "loginCodeAdd",
+ path: "/login-codes/add",
+ component: () =>
+ import("@/pages/Entity/LoginCode/Form.vue"),
+ meta: {
+ title: "LoginCodeAdd",
+ breadcrumbs: ["dashboard", "loginCodeList", "loginCodeAdd"]
+ }
+ },
+ /* SiteUser */
+ {
+ name: "siteUserList",
+ path: "/site-users",
+ component: () =>
+ import("@/pages/Entity/SiteUser/List.vue"),
+ meta: {
+ title: "SiteUserList",
+ breadcrumbs: ["dashboard", "siteUserList"]
+ }
+ },
+ {
+ name: "siteUserEdit",
+ path: "/site-users/:id/edit",
+ component: () =>
+ import("@/pages/Entity/SiteUser/Form.vue"),
+ meta: {
+ title: "SiteUserEdit",
+ breadcrumbs: ["dashboard", "siteUserList", "siteUserEdit"]
+ }
+ },
+ {
+ name: "siteUserAdd",
+ path: "/site-users/add",
+ component: () =>
+ import("@/pages/Entity/SiteUser/Form.vue"),
+ meta: {
+ title: "SiteUserAdd",
+ breadcrumbs: ["dashboard", "siteUserList", "siteUserAdd"]
+ }
+ },
+ /* VfsFile */
+ {
+ name: "vfsFileList",
+ path: "/vfs-files",
+ component: () =>
+ import("@/pages/Entity/VfsFile/List.vue"),
+ meta: {
+ title: "VfsFileList",
+ breadcrumbs: ["dashboard", "vfsFileList"]
+ }
+ },
+ {
+ name: "vfsFileEdit",
+ path: "/vfs-files/:id/edit",
+ component: () =>
+ import("@/pages/Entity/VfsFile/Form.vue"),
+ meta: {
+ title: "VfsFileEdit",
+ breadcrumbs: ["dashboard", "vfsFileList", "vfsFileEdit"]
+ }
+ },
+ {
+ name: "vfsFileAdd",
+ path: "/vfs-files/add",
+ component: () =>
+ import("@/pages/Entity/VfsFile/Form.vue"),
+ meta: {
+ title: "VfsFileAdd",
+ breadcrumbs: ["dashboard", "vfsFileList", "vfsFileAdd"]
+ }
+ },
+ /* VfsFolder */
+ {
+ name: "vfsFolderList",
+ path: "/vfs-folders",
+ component: () =>
+ import("@/pages/Entity/VfsFolder/List.vue"),
+ meta: {
+ title: "VfsFolderList",
+ breadcrumbs: ["dashboard", "vfsFolderList"]
+ }
+ },
+ {
+ name: "vfsFolderEdit",
+ path: "/vfs-folders/:id/edit",
+ component: () =>
+ import("@/pages/Entity/VfsFolder/Form.vue"),
+ meta: {
+ title: "VfsFolderEdit",
+ breadcrumbs: ["dashboard", "vfsFolderList", "vfsFolderEdit"]
+ }
+ },
+ {
+ name: "vfsFolderAdd",
+ path: "/vfs-folders/add",
+ component: () =>
+ import("@/pages/Entity/VfsFolder/Form.vue"),
+ meta: {
+ title: "VfsFolderAdd",
+ breadcrumbs: ["dashboard", "vfsFolderList", "vfsFolderAdd"]
+ }
+ },
+];
diff --git a/generators/vt-template/README.md b/generators/vt-template/README.md
index e0d7255..66bbb76 100644
--- a/generators/vt-template/README.md
+++ b/generators/vt-template/README.md
@@ -25,10 +25,20 @@ Flags:
-o, --output string output dir path
-m, --mfd string mfd file path
-n, --namespaces strings namespaces to generate. separate by comma
+ -e, --entities strings entities to generate, must be in vt.xml file. separate by comma
-h, --help help for template
```
+#### Шаблоны
+
+Встроенные шаблоны существуют в трёх вариантах:
+- **`vue2`** (по умолчанию) — class-based компоненты на `vue-property-decorator` + `mobx-vue`, Vuetify 2.
+- **`composition`** — Vue 2 + Composition API: компоненты через `defineComponent`/`setup` и composables (`useEntityList`, `useEntityForm`, `useI18n`), которые должны существовать во фронтенд-проекте (`@/composables/...`); в `routes.ts` добавляется поле `meta.title`. Vuetify 2.
+- **`vue3`** — Vue 3 + Vuetify 3: те же composables плюс `useErrorMessage`, компоненты обёрток `vt-row`/`vt-col`/`vt-btn`/`vt-icon`/`vt-card`/`vt-hover`/`vt-data-table`, `$vuetify.display` вместо `$vuetify.breakpoint`, `v-tabs-window`/`v-tabs-window-item` вместо `v-tabs-items`/`v-tab-item`, mdi-иконки. `routes.ts` и `MultiListFilters.vue` совпадают с вариантом `composition`.
+
+Выбор варианта задаётся на уровне проекта в mfd-файле. В корневом `` добавьте элемент `vue3`. Допустимые значения — `vue2`, `composition`, `vue3`; если элемент отсутствует, пуст или содержит неизвестное значение, используется `vue2`.
+
#### MODE
Значение Mode vt-сущности в vt.xml определяет какие файлы будут сгенерированы.
@@ -36,6 +46,12 @@ Flags:
- "ReadOnlyWithTemplates" - все файлы в read-only режиме, Form.vue генерироваться не будет
- "None" - файлы генерироваться не будут
+#### Частичная генерация
+
+Флаги `-n --namespaces` и `-e --entities` ограничивают набор сущностей, для которых генерируются файлы. Если ни один из них не указан, обрабатываются все неймспейсы и сущности.
+
+При частичной генерации `routes.ts` обновляется точечно: перезаписываются только блоки указанных сущностей (по маркеру `/* EntityName */`), остальная часть файла сохраняется. Если целевой сущности в файле ещё нет — её блок дописывается в конец. При полной генерации (без `-n`/`-e`) `routes.ts` перезаписывается целиком.
+
#### Особенности работы с существующими моделями
-Все файлы будут перезаписаны при каждой генерации.
+Файлы сущностей (`List.vue`, `Form.vue`, `MultiListFilters.vue`, переводы) перезаписываются при каждой генерации. Исключение — `routes.ts` при частичной генерации (см. выше).
diff --git a/generators/vt-template/generator.go b/generators/vt-template/generator.go
index 7375f6f..2062345 100644
--- a/generators/vt-template/generator.go
+++ b/generators/vt-template/generator.go
@@ -4,7 +4,9 @@ import (
"bytes"
"fmt"
"html/template"
+ "os"
"path"
+ "strings"
"github.com/vmkteam/mfd-generator/mfd"
@@ -17,6 +19,7 @@ const (
nsFlag = "namespaces"
entitiesFlag = "entities"
+ entityDir = "src/pages/Entity"
routesTemplateFlag = "routes-tmpl"
listTemplateFlag = "list-tmpl"
filterTemplateFlag = "filter-tmpl"
@@ -93,7 +96,7 @@ func (g *Generator) ReadFlags(command *cobra.Command) error {
if g.options.FiltersTemplatePath, err = flags.GetString(filterTemplateFlag); err != nil {
return err
}
- if g.options.FiltersTemplatePath, err = flags.GetString(formTemplateFlag); err != nil {
+ if g.options.FormTemplatePath, err = flags.GetString(formTemplateFlag); err != nil {
return err
}
@@ -119,29 +122,16 @@ func (g *Generator) Generate() error {
g.options.Namespaces = project.NamespaceNames
}
- // loading templates
- routesTemplate, err := mfd.LoadTemplate(g.options.RoutesTemplatePath, routesDefaultTemplate)
- if err != nil {
- return fmt.Errorf("load routes template, err=%w", err)
- }
-
- listTemplate, err := mfd.LoadTemplate(g.options.ListTemplatePath, listDefaultTemplate)
- if err != nil {
- return fmt.Errorf("load list template, err=%w", err)
- }
+ // selecting built-in templates set based on the project-level VTTemplate setting
+ vtTemplate := project.VTTemplateName()
- filterTemplate, err := mfd.LoadTemplate(g.options.FiltersTemplatePath, filterDefaultTemplate)
+ templates, err := g.loadTemplates(vtTemplate)
if err != nil {
- return fmt.Errorf("load filter template, err=%w", err)
- }
-
- formTemplate, err := mfd.LoadTemplate(g.options.ListTemplatePath, formDefaultTemplate)
- if err != nil {
- return fmt.Errorf("load form template, err=%w", err)
+ return err
}
// generating routes for all namespaces
- if _, err := g.SaveRoutes(project.VTNamespaces, routesTemplate); err != nil {
+ if _, err := g.SaveRoutes(project, templates.routes); err != nil {
return fmt.Errorf("generate routes, err=%w", err)
}
@@ -172,17 +162,19 @@ func (g *Generator) Generate() error {
continue
}
- if err := g.SaveEntity(*entity, "List.vue", listTemplate); err != nil {
+ packed := PackEntity(*entity, vtTemplate)
+
+ if err := g.SaveEntity(packed, "List.vue", templates.list); err != nil {
return fmt.Errorf("generate entity %s list, err=%w", entity.Name, err)
}
- if err := g.SaveEntity(*entity, "components/MultiListFilters.vue", filterTemplate); err != nil {
+ if err := g.SaveEntity(packed, "components/MultiListFilters.vue", templates.filter); err != nil {
return fmt.Errorf("generate entity %s filters, err=%w", entity.Name, err)
}
// do not generate form on
if entity.Mode != mfd.ModeReadOnlyWithTemplates {
- if err := g.SaveEntity(*entity, "Form.vue", formTemplate); err != nil {
+ if err := g.SaveEntity(packed, "Form.vue", templates.form); err != nil {
return fmt.Errorf("generate entity %s form, err=%w", entity.Name, err)
}
}
@@ -199,45 +191,239 @@ func (g *Generator) Generate() error {
return mfd.SaveMFD(g.options.MFDPath, project)
}
-// SaveEntity saves vt entity to template with special delims
-func (g *Generator) SaveEntity(entity mfd.VTEntity, output, tmpl string) error {
- parsed, err := template.New("base").
+type vtTemplates struct {
+ routes, list, filter, form *template.Template
+}
+
+func (g *Generator) loadTemplates(vtTemplate string) (vtTemplates, error) {
+ def := defaultTemplates(vtTemplate)
+
+ var (
+ t vtTemplates
+ body string
+ err error
+ )
+
+ if body, err = mfd.LoadTemplate(g.options.RoutesTemplatePath, def.routes); err != nil {
+ return t, fmt.Errorf("load routes template, err=%w", err)
+ }
+ if t.routes, err = parseRoutesTemplate(body); err != nil {
+ return t, fmt.Errorf("parse routes template, err=%w", err)
+ }
+
+ for _, tt := range []struct {
+ name string
+ path string
+ def string
+ target **template.Template
+ }{
+ {"list", g.options.ListTemplatePath, def.list, &t.list},
+ {"filter", g.options.FiltersTemplatePath, def.filter, &t.filter},
+ {"form", g.options.FormTemplatePath, def.form, &t.form},
+ } {
+ if body, err = mfd.LoadTemplate(tt.path, tt.def); err != nil {
+ return t, fmt.Errorf("load %s template, err=%w", tt.name, err)
+ }
+ if *tt.target, err = parseEntityTemplate(body); err != nil {
+ return t, fmt.Errorf("parse %s template, err=%w", tt.name, err)
+ }
+ }
+
+ return t, nil
+}
+
+type vtTemplateBodies struct {
+ routes, list, filter, form string
+}
+
+// defaultTemplates returns the built-in templates set used as fallback when no
+// custom template path is provided.
+func defaultTemplates(vtTemplate string) vtTemplateBodies {
+ switch vtTemplate {
+ case mfd.VTTemplateComposition:
+ return vtTemplateBodies{routesCompositionTemplate, listCompositionTemplate, filterCompositionTemplate, formCompositionTemplate}
+ case mfd.VTTemplateVue3:
+ return vtTemplateBodies{routesVue3Template, listVue3Template, filterVue3Template, formVue3Template}
+ }
+
+ return vtTemplateBodies{routesVue2Template, listVue2Template, filterVue2Template, formVue2Template}
+}
+
+// parseEntityTemplate parses a vt entity template. Entity templates use custom
+// delims so that vue interpolation ({{ ... }}) passes through untouched.
+func parseEntityTemplate(body string) (*template.Template, error) {
+ return template.New("base").
Delims("[[", "]]").
Funcs(mfd.TemplateFunctions).
- Parse(tmpl)
- if err != nil {
- return fmt.Errorf("parsing template, err=%w", err)
+ Parse(body)
+}
+
+// parseRoutesTemplate parses the routes template, which keeps the default delims.
+func parseRoutesTemplate(body string) (*template.Template, error) {
+ return template.New("base").Funcs(mfd.TemplateFunctions).Parse(body)
+}
+
+func (g *Generator) getTargetEntities(project *mfd.Project) []string {
+ var targetEntities []string
+ nsList := g.options.Namespaces
+ if len(nsList) == 0 {
+ nsList = project.NamespaceNames
+ }
+ for _, nsName := range nsList {
+ ns := project.VTNamespace(nsName)
+ if ns == nil {
+ continue
+ }
+ entityNames := ns.VTEntityNames()
+ if len(g.options.Entities) != 0 {
+ entityNames = g.options.Entities
+ }
+
+ for _, eName := range entityNames {
+ if entity := ns.VTEntity(eName); entity != nil {
+ targetEntities = append(targetEntities, entity.Name)
+ }
+ }
}
- packed := PackEntity(entity)
+ return targetEntities
+}
+// SaveEntity renders packed vt entity data with an already parsed template and
+// saves it under the entity directory.
+func (g *Generator) SaveEntity(packed EntityData, output string, tmpl *template.Template) error {
var buffer bytes.Buffer
- if err := parsed.ExecuteTemplate(&buffer, "base", packed); err != nil {
+ if err := tmpl.ExecuteTemplate(&buffer, "base", packed); err != nil {
return fmt.Errorf("processing model template, err=%w", err)
}
- _, err = mfd.Save(buffer.Bytes(), path.Join(g.options.Output, "src/pages/Entity", entity.Name, output))
+ _, err := mfd.Save(buffer.Bytes(), path.Join(g.options.Output, entityDir, packed.Name, output))
return err
}
// SaveRoutes saves all vt namespaces to routes file
-func (g *Generator) SaveRoutes(namespaces []*mfd.VTNamespace, tmpl string) (bool, error) {
- parsed, err := template.New("base").Funcs(mfd.TemplateFunctions).Parse(tmpl)
- if err != nil {
- return false, fmt.Errorf("parsing template, err=%w", err)
+func (g *Generator) SaveRoutes(project *mfd.Project, tmpl *template.Template) (bool, error) {
+ var targetEntities []string
+ isPartial := len(g.options.Namespaces) > 0 || len(g.options.Entities) > 0
+
+ if isPartial {
+ targetEntities = g.getTargetEntities(project)
}
- pack, err := PackRoutesNamespace(namespaces)
+ pack, err := PackRoutesNamespace(project.VTNamespaces)
if err != nil {
return false, fmt.Errorf("packing data, err=%w", err)
}
var buffer bytes.Buffer
- if err := parsed.ExecuteTemplate(&buffer, "base", pack); err != nil {
+ if err := tmpl.ExecuteTemplate(&buffer, "base", pack); err != nil {
return false, fmt.Errorf("processing model template, err=%w", err)
}
- return mfd.Save(buffer.Bytes(), path.Join(g.options.Output, "src/pages/Entity/routes.ts"))
+ routesPath := path.Join(g.options.Output, entityDir, "routes.ts")
+
+ // base flow when generate all routes
+ if len(targetEntities) == 0 {
+ return mfd.Save(buffer.Bytes(), routesPath)
+ }
+
+ existingData, err := os.ReadFile(routesPath)
+ if err != nil {
+ return mfd.Save(buffer.Bytes(), routesPath)
+ }
+
+ newStr := buffer.String()
+ existingStr := string(existingData)
+
+ for _, entityName := range targetEntities {
+ // get new code block
+ newBlock := extractEntityBlock(newStr, entityName)
+ if len(newBlock) == 0 {
+ continue
+ }
+ // insert new code with flow
+ existingStr = injectEntityBlock(existingStr, entityName, newBlock)
+ }
+
+ return mfd.Save([]byte(existingStr), routesPath)
+}
+
+// extractEntityBlock get block code from content by entity name
+func extractEntityBlock(content string, entityName string) []string {
+ var block []string
+ inBlock := false
+ marker := fmt.Sprintf("/* %s */", entityName)
+
+ lines := strings.Split(content, "\n")
+ for _, line := range lines {
+ trimmed := strings.TrimSpace(line)
+
+ if inBlock {
+ if strings.HasPrefix(trimmed, "/*") || trimmed == "];" {
+ break
+ }
+ block = append(block, strings.TrimPrefix(line, " "))
+ } else if trimmed == marker {
+ inBlock = true
+ }
+ }
+
+ return block
+}
+
+// injectEntityBlock update or concat body with new blocks
+func injectEntityBlock(existingContent string, entityName string, newBlock []string) string {
+ var result []string
+ inBlock := false
+ found := false
+ marker := fmt.Sprintf("/* %s */", entityName)
+
+ lines := strings.Split(existingContent, "\n")
+ for _, line := range lines {
+ trimmed := strings.TrimSpace(line)
+
+ if inBlock {
+ if strings.HasPrefix(trimmed, "/*") || trimmed == "];" {
+ inBlock = false
+ } else {
+ continue
+ }
+ }
+
+ // Case 1 - when found
+ if !inBlock && trimmed == marker {
+ inBlock = true
+ found = true
+
+ result = append(result, " "+marker)
+ for _, bLine := range newBlock {
+ result = append(result, " "+bLine)
+ }
+ continue
+ }
+
+ // Case 2 - not found, create new
+ if !found && trimmed == "];" {
+ if len(result) > 0 {
+ lastIdx := len(result) - 1
+ if strings.HasSuffix(strings.TrimSpace(result[lastIdx]), "}") {
+ result[lastIdx] += ","
+ }
+ }
+
+ result = append(result, " "+marker)
+ for _, bLine := range newBlock {
+ result = append(result, " "+bLine)
+ }
+ found = true
+ }
+
+ if !inBlock {
+ result = append(result, line)
+ }
+ }
+
+ return strings.Join(result, "\n")
}
func (g *Generator) SaveLang(entity *mfd.TranslationEntity, lang string) error {
@@ -245,7 +431,7 @@ func (g *Generator) SaveLang(entity *mfd.TranslationEntity, lang string) error {
return nil
}
- output := path.Join(g.options.Output, "src/pages/Entity", entity.Name, lang+".json")
+ output := path.Join(g.options.Output, entityDir, entity.Name, lang+".json")
if err := mfd.MarshalJSONToFile(output, entity.ToJSONMap()); err != nil {
return fmt.Errorf("save translation lang %s, err=%w", lang, err)
}
diff --git a/generators/vt-template/generator_test.go b/generators/vt-template/generator_test.go
index 3d5c8de..4472c94 100644
--- a/generators/vt-template/generator_test.go
+++ b/generators/vt-template/generator_test.go
@@ -1,61 +1,82 @@
package vttmpl
import (
+ "bytes"
"os"
"path/filepath"
+ "reflect"
"strings"
"testing"
"github.com/vmkteam/mfd-generator/generators/testdata"
+ "github.com/vmkteam/mfd-generator/mfd"
. "github.com/smartystreets/goconvey/convey"
)
+// portalExpectedFiles is the file set generated for the portal namespace of the
+// newsportal fixture, shared by every template set check.
+var portalExpectedFiles = map[string]struct{}{
+ filepath.Join("Category", "List.vue"): {},
+ filepath.Join("Category", "Form.vue"): {},
+ filepath.Join("Category", "en.json"): {},
+ filepath.Join("Category", "components", "MultiListFilters.vue"): {},
+ filepath.Join("News", "List.vue"): {},
+ filepath.Join("News", "Form.vue"): {},
+ filepath.Join("News", "en.json"): {},
+ filepath.Join("News", "components", "MultiListFilters.vue"): {},
+ filepath.Join("Tag", "List.vue"): {},
+ filepath.Join("Tag", "Form.vue"): {},
+ filepath.Join("Tag", "en.json"): {},
+ filepath.Join("Tag", "components", "MultiListFilters.vue"): {},
+ "routes.ts": {},
+}
+
+// entityFilePrefix mirrors the layout the generator writes into the output dir
+var entityFilePrefix = filepath.Join("src", "pages", "Entity")
+
+// compareGenerated asserts that every file of the set is byte equal in the
+// actual and the expected output dirs.
+func compareGenerated(t *testing.T, actualPath, expectedPath string, files map[string]struct{}) {
+ t.Helper()
+
+ for f := range files {
+ filenameWithFullPath := filepath.Join(actualPath, entityFilePrefix, f)
+ t.Logf("Check %s file", filenameWithFullPath)
+ content, err := os.ReadFile(filenameWithFullPath)
+ So(err, ShouldBeNil)
+ expectedContent, err := os.ReadFile(filepath.Join(expectedPath, entityFilePrefix, f))
+ So(err, ShouldBeNil)
+ So(string(content), ShouldResemble, string(expectedContent))
+ }
+}
+
func TestGenerator_Generate(t *testing.T) {
+ // clean up leftover output from previous runs so partial routes injection starts fresh
+ _ = os.RemoveAll(testdata.PathActualVTTemplateAll)
+ _ = os.RemoveAll(testdata.PathActualVTTemplateEntity)
+
Convey("TestGenerator_Generate", t, func() {
generator := New()
generator.options.Output = testdata.PathActualVTTemplateAll
generator.options.MFDPath = testdata.PathExpectedMFD
generator.options.Namespaces = []string{"portal"}
+ // expected testdata is generated with default vue2 class-based templates
+ // (project mfd has no VTTemplate setting)
Convey("Check correct generate", func() {
t.Log("Generate vt-template")
So(generator.Generate(), ShouldBeNil)
})
- filePrefix := filepath.Join("src", "pages", "Entity")
-
Convey("Check generated files", func() {
- expectedFilenames := map[string]struct{}{
- filepath.Join("Category", "List.vue"): {},
- filepath.Join("Category", "Form.vue"): {},
- filepath.Join("Category", "en.json"): {},
- filepath.Join("Category", "components", "MultiListFilters.vue"): {},
- filepath.Join("News", "List.vue"): {},
- filepath.Join("News", "Form.vue"): {},
- filepath.Join("News", "en.json"): {},
- filepath.Join("News", "components", "MultiListFilters.vue"): {},
- filepath.Join("Tag", "List.vue"): {},
- filepath.Join("Tag", "Form.vue"): {},
- filepath.Join("Tag", "en.json"): {},
- filepath.Join("Tag", "components", "MultiListFilters.vue"): {},
- "routes.ts": {},
- }
-
- for f := range expectedFilenames {
- filenameWithFullPath := filepath.Join(testdata.PathActualVTTemplateAll, filePrefix, f)
- t.Logf("Check %s file", filenameWithFullPath)
- content, err := os.ReadFile(filenameWithFullPath)
- So(err, ShouldBeNil)
- expectedContent, err := os.ReadFile(filepath.Join(testdata.PathExpectedVTTemplateAll, filePrefix, f))
- So(err, ShouldBeNil)
- So(string(content), ShouldResemble, string(expectedContent))
- }
+ compareGenerated(t, testdata.PathActualVTTemplateAll, testdata.PathExpectedVTTemplateAll, portalExpectedFiles)
})
Convey("Check correct generate with entities", func() {
generator.options.Output = filepath.Join(testdata.PathActual, "vt-template", "entities")
+ // two Entities
generator.options.Entities = []string{"Category", "Tag"}
t.Log("Generate vt-template with entities")
@@ -77,15 +98,7 @@ func TestGenerator_Generate(t *testing.T) {
}
Convey("Check content", func() {
- for f := range expectedFilenames {
- filenameWithFullPath := filepath.Join(testdata.PathActualVTTemplateEntity, filePrefix, f)
- t.Logf("Check %s file", filenameWithFullPath)
- content, err := os.ReadFile(filenameWithFullPath)
- So(err, ShouldBeNil)
- expectedContent, err := os.ReadFile(filepath.Join(testdata.PathExpectedVTTemplateEntity, filePrefix, f))
- So(err, ShouldBeNil)
- So(string(content), ShouldResemble, string(expectedContent))
- }
+ compareGenerated(t, testdata.PathActualVTTemplateEntity, testdata.PathExpectedVTTemplateEntity, expectedFilenames)
})
Convey("Check filenames", func() {
@@ -93,7 +106,7 @@ func TestGenerator_Generate(t *testing.T) {
So(err, ShouldBeNil)
for _, a := range actualFiles {
- shortPath := strings.ReplaceAll(a, filepath.Join(testdata.PathExpectedVTTemplateEntity, filePrefix)+string(os.PathSeparator), "")
+ shortPath := strings.ReplaceAll(a, filepath.Join(testdata.PathExpectedVTTemplateEntity, entityFilePrefix)+string(os.PathSeparator), "")
t.Logf("Check %s filename", shortPath)
_, ok := expectedFilenames[shortPath]
So(ok, ShouldBeTrue)
@@ -103,6 +116,184 @@ func TestGenerator_Generate(t *testing.T) {
})
}
+// buildVTTemplateMFD writes a copy of the expected mfd with the project-level
+// VTTemplate setting next to the original one, so translation and namespace
+// files are still resolved, and returns its path.
+func buildVTTemplateMFD(t *testing.T, vtTemplate string) string {
+ t.Helper()
+
+ project, err := mfd.LoadProject(testdata.PathExpectedMFD, false, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ project.VTTemplate = vtTemplate
+
+ return saveTempMFD(t, filepath.Join(filepath.Dir(testdata.PathExpectedMFD), "newsportal."+vtTemplate+".mfd"), project)
+}
+
+// saveTempMFD writes project to mfdPath and removes it when the test ends. The
+// path has to sit next to the fixture so namespace and translation files, which
+// LoadProject resolves relative to the mfd, are still found.
+func saveTempMFD(t *testing.T, mfdPath string, project *mfd.Project) string {
+ t.Helper()
+
+ if err := mfd.SaveMFD(mfdPath, project); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { _ = os.Remove(mfdPath) })
+
+ return mfdPath
+}
+
+// checkVTTemplateSet generates the portal namespace with the given VTTemplate
+// set and compares every generated file with the expected testdata.
+func checkVTTemplateSet(t *testing.T, vtTemplate, actualPath, expectedPath string) {
+ t.Helper()
+
+ // clean up leftover output from previous runs so partial routes injection starts fresh
+ _ = os.RemoveAll(actualPath)
+
+ Convey("TestGenerator_Generate"+vtTemplate, t, func() {
+ generator := New()
+
+ generator.options.Output = actualPath
+ // template set is selected via the project-level VTTemplate setting
+ generator.options.MFDPath = buildVTTemplateMFD(t, vtTemplate)
+ generator.options.Namespaces = []string{"portal"}
+
+ Convey("Check correct generate with "+vtTemplate+" templates", func() {
+ t.Logf("Generate vt-template with %s templates", vtTemplate)
+ So(generator.Generate(), ShouldBeNil)
+ })
+
+ Convey("Check generated "+vtTemplate+" files", func() {
+ compareGenerated(t, actualPath, expectedPath, portalExpectedFiles)
+ })
+ })
+}
+
+func TestGenerator_GenerateComposition(t *testing.T) {
+ checkVTTemplateSet(t, mfd.VTTemplateComposition, testdata.PathActualVTTemplateComposition, testdata.PathExpectedVTTemplateComposition)
+}
+
+func TestGenerator_GenerateVue3(t *testing.T) {
+ checkVTTemplateSet(t, mfd.VTTemplateVue3, testdata.PathActualVTTemplateVue3, testdata.PathExpectedVTTemplateVue3)
+}
+
+func TestGenerator_UnknownVTTemplate(t *testing.T) {
+ // expected vue2 testdata is generated from the mfd without VTTemplate, so an
+ // unknown value must produce exactly the same files
+ checkVTTemplateSet(t, "vue4", filepath.Join(testdata.PathActual, "vt-template", "unknown"), testdata.PathExpectedVTTemplateAll)
+}
+
+func TestGenerator_ModelAccessPrefix(t *testing.T) {
+ dir := filepath.Dir(testdata.PathExpectedMFD)
+
+ // buildMediaMFD writes a temp mfd next to the media/vfs namespace files so
+ // LoadProject can resolve them; VTTemplate drives the access prefix.
+ buildMediaMFD := func(vtTemplate string) string {
+ p := mfd.NewProject("media.mfd", mfd.GoPG10)
+ p.NamespaceNames = []string{"media", "vfs"}
+ p.VTTemplate = vtTemplate
+
+ return saveTempMFD(t, filepath.Join(dir, "media."+vtTemplate+".gen.mfd"), p)
+ }
+
+ generateForm := func(vtTemplate string) (string, error) {
+ out := filepath.Join(testdata.PathActual, "vt-template", "media-"+vtTemplate)
+ _ = os.RemoveAll(out)
+
+ generator := New()
+ generator.options.Output = out
+ generator.options.MFDPath = buildMediaMFD(vtTemplate)
+ generator.options.Namespaces = []string{"media"}
+ if err := generator.Generate(); err != nil {
+ return "", err
+ }
+
+ content, err := os.ReadFile(filepath.Join(out, "src", "pages", "Entity", "Page", "Form.vue"))
+ return string(content), err
+ }
+
+ Convey("model access prefix depends on VTTemplate", t, func() {
+ Convey("vue2 class-based templates use store.model.", func() {
+ form, err := generateForm(mfd.VTTemplateVue2)
+ So(err, ShouldBeNil)
+ So(form, ShouldContainSubstring, `:value-for-transliterating="store.model.title"`)
+ So(form, ShouldContainSubstring, `:file="store.model.`)
+ So(form, ShouldNotContainSubstring, `:value-for-transliterating="model.`)
+ })
+
+ Convey("composition templates use model.", func() {
+ form, err := generateForm(mfd.VTTemplateComposition)
+ So(err, ShouldBeNil)
+ So(form, ShouldContainSubstring, `:value-for-transliterating="model.title"`)
+ So(form, ShouldContainSubstring, `:file="model.`)
+ So(form, ShouldNotContainSubstring, `store.model.`)
+ })
+
+ Convey("vue3 templates use model. and the vuetify 3 display api", func() {
+ form, err := generateForm(mfd.VTTemplateVue3)
+ So(err, ShouldBeNil)
+ So(form, ShouldContainSubstring, `:value-for-transliterating="model.title"`)
+ So(form, ShouldContainSubstring, `:file="model.`)
+ So(form, ShouldNotContainSubstring, `store.model.`)
+ So(form, ShouldNotContainSubstring, `$vuetify.breakpoint.`)
+ })
+ })
+}
+
+// TestVue3ListValues covers the vue3 list cell expressions: Vue 3 has no
+// filters, so the tableDate helper is called directly (and imported/returned
+// only when it is really used) and a foreign key field is read from the nested
+// object. The newsportal testdata has no date column in a list, so the template
+// is rendered here directly.
+func TestVue3ListValues(t *testing.T) {
+ render := func(columns []AttributeData) string {
+ parsed, err := parseEntityTemplate(listVue3Template)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var buf bytes.Buffer
+ data := EntityData{
+ Name: "News",
+ JSName: "news",
+ PKs: []PKPair{{JSName: "id"}},
+ ListColumns: columns,
+ }
+ if err := parsed.ExecuteTemplate(&buf, "base", data); err != nil {
+ t.Fatal(err)
+ }
+
+ return buf.String()
+ }
+
+ Convey("vue3 list renders cell values without vue 2 filters", t, func() {
+ Convey("date column calls tableDate and imports it", func() {
+ out := render([]AttributeData{
+ {JSName: "createdAt", HasPipe: true, Pipe: "tableDate", IsTableDate: true, Value: "tableDate(item.createdAt)", IsSortable: true},
+ })
+
+ So(out, ShouldContainSubstring, "{{ tableDate(item.createdAt) }}")
+ So(out, ShouldContainSubstring, "import { tableDate } from '@/helpers/date';")
+ So(out, ShouldContainSubstring, "\n tableDate,\n")
+ So(out, ShouldNotContainSubstring, "|")
+ })
+
+ Convey("fk column reads the nested field, no tableDate import", func() {
+ out := render([]AttributeData{
+ {JSName: "category", HasPipe: true, Pipe: `getField("title")`, Value: "item.category?.title"},
+ })
+
+ So(out, ShouldContainSubstring, "{{ item.category?.title }}")
+ So(out, ShouldNotContainSubstring, "getField")
+ So(out, ShouldNotContainSubstring, "@/helpers/date")
+ So(out, ShouldNotContainSubstring, "tableDate")
+ })
+ })
+}
+
func fullFilesPaths(path string) ([]string, error) {
files, err := os.ReadDir(path)
if err != nil {
@@ -124,3 +315,244 @@ func fullFilesPaths(path string) ([]string, error) {
return filePaths, nil
}
+
+func TestManualGenerate(t *testing.T) {
+ t.Skip()
+ generator := New()
+
+ generator.options.Output = ""
+ generator.options.MFDPath = ""
+ generator.options.Namespaces = []string{"article", "catalogue"}
+ generator.options.Entities = []string{"tag", "category"}
+
+ err := generator.Generate()
+ if err != nil {
+ t.Error(err)
+ }
+}
+
+func Test_extractEntityBlock(t *testing.T) {
+ type args struct {
+ content string
+ entityName string
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ }{
+ {
+ name: "middle body contains",
+ args: args{
+ entityName: "Category",
+ content: `export default [
+
+ /* News */
+ {
+ name: "newsList",
+ path: "/news",
+ },
+ /* Category */
+ {
+ name: "categoryList",
+ path: "/category",
+ },
+ /* Tag */
+ {
+ name: "tagList",
+ },
+];`,
+ },
+ want: []string{
+ `{`,
+ ` name: "categoryList",`,
+ ` path: "/category",`,
+ `},`,
+ },
+ },
+ {
+ name: "last body",
+ args: args{
+ entityName: "Tag",
+ content: `export default [
+ /* Category */
+ {
+ name: "categoryList",
+ },
+ /* Tag */
+ {
+ name: "tagList",
+ path: "/tags",
+ }
+];`,
+ },
+ want: []string{
+ `{`,
+ ` name: "tagList",`,
+ ` path: "/tags",`,
+ `}`,
+ },
+ },
+ {
+ name: "now found",
+ args: args{
+ entityName: "News",
+ content: `export default [
+ /* Category */
+ {
+ name: "categoryList",
+ },
+];`,
+ },
+ want: nil,
+ },
+ {
+ name: "empty content",
+ args: args{
+ entityName: "Category",
+ content: ``,
+ },
+ want: nil,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := extractEntityBlock(tt.args.content, tt.args.entityName)
+
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("extractEntityBlock() =\n%#v\nwant\n%#v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_injectEntityBlock(t *testing.T) {
+ sampleNewBlock := []string{
+ `{`,
+ ` name: "tagList",`,
+ ` path: "/tags",`,
+ `},`,
+ }
+
+ type args struct {
+ existingContent string
+ entityName string
+ newBlock []string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "update middle body",
+ args: args{
+ entityName: "Tag",
+ newBlock: sampleNewBlock,
+ existingContent: `export default [
+ /* Category */
+ {
+ name: "categoryList",
+ },
+ /* Tag */
+ {
+ name: "oldTagList",
+ },
+ /* News */
+ {
+ name: "newsList",
+ }
+];`,
+ },
+ want: `export default [
+ /* Category */
+ {
+ name: "categoryList",
+ },
+ /* Tag */
+ {
+ name: "tagList",
+ path: "/tags",
+ },
+ /* News */
+ {
+ name: "newsList",
+ }
+];`,
+ },
+ {
+ name: "insert new data variant 1",
+ args: args{
+ entityName: "Tag",
+ newBlock: sampleNewBlock,
+ existingContent: `export default [
+ /* Category */
+ {
+ name: "categoryList",
+ },
+];`,
+ },
+ want: `export default [
+ /* Category */
+ {
+ name: "categoryList",
+ },
+ /* Tag */
+ {
+ name: "tagList",
+ path: "/tags",
+ },
+];`,
+ },
+ {
+ name: "insert new data variant 2",
+ args: args{
+ entityName: "Tag",
+ newBlock: sampleNewBlock,
+ existingContent: `export default [
+ /* Category */
+ {
+ name: "categoryList"
+ }
+];`,
+ },
+ want: `export default [
+ /* Category */
+ {
+ name: "categoryList"
+ },
+ /* Tag */
+ {
+ name: "tagList",
+ path: "/tags",
+ },
+];`,
+ },
+ {
+ name: "insert new data variant 2",
+ args: args{
+ entityName: "Tag",
+ newBlock: sampleNewBlock,
+ existingContent: `export default [
+];`,
+ },
+ want: `export default [
+ /* Tag */
+ {
+ name: "tagList",
+ path: "/tags",
+ },
+];`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := injectEntityBlock(tt.args.existingContent, tt.args.entityName, tt.args.newBlock)
+ if got != tt.want {
+ t.Errorf("injectEntityBlock() failed.\n\n GOT \n%s\n\n WANT \n%s\n", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/generators/vt-template/template.go b/generators/vt-template/template.go
index 10912aa..e474c6b 100644
--- a/generators/vt-template/template.go
+++ b/generators/vt-template/template.go
@@ -1,6 +1,6 @@
package vttmpl
-const routesDefaultTemplate = `/* eslint-disable */
+const routesVue2Template = `/* eslint-disable */
export default [{{range $model := .Entities}}
/* {{.Name}} */
{
@@ -33,8 +33,7 @@ export default [{{range $model := .Entities}}
];
`
-// fuck backtick js
-const listDefaultTemplate = `
+const listVue2Template = `
$t('[[.JSName]].list.filter.quickFilterPlaceholder')
"
hide-details
- @keyup.enter.native="submitFilters()"
+ @keyup.enter="submitFilters()"
/>
`
-const filterDefaultTemplate = `
+const filterVue2Template = `
`
-const formDefaultTemplate = `
+const formVue2Template = `
+ import("@/pages/Entity/{{.Name}}/List.vue"),
+ meta: {
+ title: "{{.Name}}List",
+ breadcrumbs: ["dashboard", "{{.JSName}}List"]
+ }
+ },
+ {{if not .ReadOnly}}{
+ name: "{{.JSName}}Edit",
+ path: "/{{.TerminalPath}}/:id/edit",
+ component: () =>
+ import("@/pages/Entity/{{.Name}}/Form.vue"),
+ meta: {
+ title: "{{.Name}}Edit",
+ breadcrumbs: ["dashboard", "{{.JSName}}List", "{{.JSName}}Edit"]
+ }
+ },
+ {
+ name: "{{.JSName}}Add",
+ path: "/{{.TerminalPath}}/add",
+ component: () =>
+ import("@/pages/Entity/{{.Name}}/Form.vue"),
+ meta: {
+ title: "{{.Name}}Add",
+ breadcrumbs: ["dashboard", "{{.JSName}}List", "{{.JSName}}Add"]
+ }
+ },{{end}}{{end}}
+];
+`
+
+const listCompositionTemplate = `
+
+
+
+
+
+
+
+
+
+ {{ t("[[.JSName]].list.title") }}
+
+
+ {{ pagination.totalItems }}
+
+
+
+
+
+ [[if not .ReadOnly]]
+ add
+ {{ t("common.list.addNewLabel") }}
+ [[end]]
+
+
+
+
+
+ [[raw ""]]
+
+
+
+ [[raw ""]]
+
+ [[if .HasQuickFilter]]
+
+
+
+
+
+ [[end]]
+
+
+
+
+
+
+ [[raw ""]]
+ [[range .ListColumns]][[if eq .JSName "statusId"]]
+
+
+
+
+ [[else]]
+ [[raw "<"]]template #item.[[.JSName]]="{ item }">[[if .IsBool]]
+ [[else]][[if .EditLink]]
+ [[end]]
+ [[if .EditLink]] [[end]]{{ item.[[.JSName]][[if .HasPipe]] | [[.Pipe]][[end]] }}[[end]][[if .EditLink]]
+ [[end]]
+ [[end]][[end]][[if not $.ReadOnly]]
+
+
+
+
+ delete
+
+
+
+ [[end]]
+
+
+
+
+
+
+
+
+
+[[raw "<"]]script lang="ts">
+import { computed, defineComponent } from 'vue';
+import { [[.Name]]Summary, [[.Name]]Search } from '@/services/api/factory';
+import { useEntityList } from '@/composables/useEntityList';
+import { useI18n } from '@/composables/useI18n';
+
+import MultiFilters from './components/MultiListFilters.vue';
+
+export default defineComponent({
+ // eslint-disable-next-line vue/multi-word-component-names
+ name: 'List',
+ components: { MultiFilters },
+
+ setup () {
+ const { t } = useI18n();
+
+ const {
+ selected,
+ pagination,
+ filters,
+ activeFilters,
+ list,
+ isLoading,
+ vuetifyTableOptions,
+ deleteItem,
+ submitFilters,
+ setCompactPagination,
+ setPagination,
+ updateFilters
+ } = useEntityList([[.Name]]Summary, [[.Name]]Search);
+
+ const headers = computed(() => [
+ [[range $i, $e := .ListColumns]][[if ne $i 0]]
+ },
+ [[end]]{[[if eq .JSName "statusId"]]
+ text: t('[[$.JSName]].list.headers.status'),
+ value: 'status',
+ sortable: false
+ [[- else ]]
+ text: t('[[$.JSName]].list.headers.[[.JSName]]'),
+ value: '[[.JSName]]'[[if eq $i 0]],
+ align: 'left'[[end]][[if not .IsSortable]],
+ sortable: false[[end]][[end]][[end]]
+ [[- if $.ReadOnly ]]
+ }
+ [[- else ]]
+ },
+ {
+ text: t('[[$.JSName]].list.headers.actions'),
+ value: 'id',
+ sortable: false
+ }[[end]]
+ ]);
+
+ return {
+ t,
+ selected,
+ pagination,
+ filters,
+ activeFilters,
+ list,
+ isLoading,
+ vuetifyTableOptions,
+ deleteItem,
+ submitFilters,
+ setCompactPagination,
+ setPagination,
+ updateFilters,
+ headers
+ };
+ }
+});
+
+
+
+`
+
+const filterCompositionTemplate = `
+
+
+
+
+`
+
+const formCompositionTemplate = `
+
+
+
+
+
+
+ {{ model.[[.TitleField]] || "..." }}
+
+
+
+
+
+
+ arrow_back
+
+
+ {{ t("common.form.cancelButtonLabel") }}
+
+
+
+
+ delete
+
+
+
+
+
+
+ Основные
+
+
+
+
+
+
+
+ [[raw ""]]
+ [[range .FormColumns]][[if .IsCheckBox]]
+
+ [[raw "
+
+
+ [[else]][[raw "[[end]][[end]][[raw ""]]
+
+
+
+
+
+
+
+
+
+
+ done
+
+ {{ t("common.form.saveAndCloseButtonLabel") }}
+
+
+
+ {{ t("common.form.saveButtonLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+[[raw "
+
+
+`
diff --git a/generators/vt-template/template_vue3.go b/generators/vt-template/template_vue3.go
new file mode 100644
index 0000000..8d2ba9f
--- /dev/null
+++ b/generators/vt-template/template_vue3.go
@@ -0,0 +1,451 @@
+package vttmpl
+
+// vue3 template set: Vue 3 + Vuetify 3 components built with defineComponent/setup
+// and the same composables as the composition set.
+
+// routesVue3Template repeats the composition routes as is
+const routesVue3Template = routesCompositionTemplate
+
+// filterVue3Template repeats the composition multi filter as is
+const filterVue3Template = filterCompositionTemplate
+
+const listVue3Template = `
+
+
+
+
+
+
+
+
+
+ {{ t("[[.JSName]].list.title") }}
+
+
+ {{ pagination.totalItems }}
+
+
+
+
+
+ [[if not .ReadOnly]]
+ mdi-plus
+ {{ t("common.list.addNewLabel") }}
+ [[end]]
+
+
+
+
+
+ [[raw ""]]
+
+
+
+ [[raw ""]]
+
+ [[if .HasQuickFilter]]
+
+
+
+
+
+ [[end]]
+
+
+
+
+
+
+ [[raw ""]]
+ [[range .ListColumns]][[if eq .JSName "statusId"]]
+
+
+
+
+ [[else]]
+ [[raw "<"]]template #item.[[.JSName]]="{ item }">[[if .IsBool]]
+ [[else]][[if .EditLink]]
+ [[end]]
+ [[if .EditLink]] [[end]]{{ [[.Value]] }}[[end]][[if .EditLink]]
+ [[end]]
+ [[end]][[end]][[if not $.ReadOnly]]
+
+
+
+
+ mdi-delete
+
+
+
+ [[end]]
+
+
+
+
+
+
+
+
+
+[[raw "<"]]script lang="ts">
+import { useEntityList } from '@/composables/useEntityList';
+import { useI18n } from '@/composables/useI18n';[[if .UsesTableDate]]
+import { tableDate } from '@/helpers/date';[[end]]
+import { [[.Name]], [[.Name]]Search } from '@/services/api/factory';
+import { computed, defineComponent } from 'vue';
+
+import MultiFilters from './components/MultiListFilters.vue';
+
+export default defineComponent({
+ name: 'List',
+
+ components: { MultiFilters },
+
+ setup () {
+ const { t } = useI18n();
+
+ const {
+ list,
+ filters,
+ selected,
+ isLoading,
+ pagination,
+ activeFilters,
+ vuetifyTableOptions,
+
+ deleteItem,
+ submitFilters,
+ setPagination,
+ updateFilters,
+ setCompactPagination
+ } = useEntityList([[.Name]], [[.Name]]Search);
+
+ const headers = computed(() => [
+ [[range $i, $e := .ListColumns]][[if ne $i 0]]
+ },
+ [[end]]{[[if eq .JSName "statusId"]]
+ text: t('[[$.JSName]].list.headers.status'),
+ align: 'left',
+ value: 'status',
+ sortable: false
+ [[- else ]]
+ text: t('[[$.JSName]].list.headers.[[.JSName]]'),
+ align: 'left',
+ value: '[[.JSName]]'[[if not .IsSortable]],
+ sortable: false[[end]][[end]][[end]]
+ [[- if $.ReadOnly ]]
+ }
+ [[- else ]]
+ },
+ {
+ text: t('[[$.JSName]].list.headers.actions'),
+ value: 'id',
+ align: 'right',
+ sortable: false
+ }[[end]]
+ ]);
+
+ return {
+ t,
+ list,
+ headers,
+ filters,
+ selected,
+ isLoading,
+ pagination,
+ activeFilters,
+ vuetifyTableOptions,
+[[if .UsesTableDate]]
+ tableDate,[[end]]
+ deleteItem,
+ submitFilters,
+ setPagination,
+ updateFilters,
+ setCompactPagination
+ };
+ }
+});
+
+
+
+`
+
+const formVue3Template = `
+
+
+
+
+
+
+ {{ model.[[.TitleField]] || "..." }}
+
+
+
+
+
+
+ mdi-arrow-left
+
+
+ {{ t("common.form.cancelButtonLabel") }}
+
+
+
+
+ mdi-delete
+
+
+
+
+
+
+
+ Основные
+
+
+
+
+
+
+
+ [[raw ""]][[range .FormColumns]]
+ [[if .IsCheckBox]]
+
+ [[raw "
+
+ [[else]][[raw "[[end]][[end]]
+ [[raw ""]]
+
+
+
+
+
+
+
+
+
+ mdi-check
+ {{ t("common.form.saveAndCloseButtonLabel") }}
+
+
+
+ {{ t("common.form.saveButtonLabel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+[[raw "
+
+
+`
diff --git a/generators/vt-template/vt_template.go b/generators/vt-template/vt_template.go
index cd51ad4..85d3cb7 100644
--- a/generators/vt-template/vt_template.go
+++ b/generators/vt-template/vt_template.go
@@ -25,8 +25,19 @@ type EntityData struct {
FormColumns []InputData
}
-// PackEntity packs mfd vt entity to template data
-func PackEntity(vtEntity mfd.VTEntity) EntityData {
+func (e EntityData) UsesTableDate() bool {
+ for _, column := range e.ListColumns {
+ if column.IsTableDate {
+ return true
+ }
+ }
+
+ return false
+}
+
+// PackEntity packs mfd vt entity to template data.
+// vtTemplate is the normalized project-level template set name
+func PackEntity(vtEntity mfd.VTEntity, vtTemplate string) EntityData {
pks := vtEntity.Entity.PKs()
pkPairs := make([]PKPair, len(pks))
for i := range pks {
@@ -50,16 +61,18 @@ func PackEntity(vtEntity mfd.VTEntity) EntityData {
tmpl.ListColumns = append(tmpl.ListColumns, PackAttribute(vtEntity, *attr))
}
if attr.Search != mfd.TypeHTMLNone && attr.Search != "" {
- tmpl.FilterColumns = append(tmpl.FilterColumns, PackInput(*attr, vtEntity, true))
+ tmpl.FilterColumns = append(tmpl.FilterColumns, PackInput(*attr, vtEntity, true, vtTemplate))
}
if attr.Form != mfd.TypeHTMLNone && attr.Form != "" {
- tmpl.FormColumns = append(tmpl.FormColumns, PackInput(*attr, vtEntity, false))
+ tmpl.FormColumns = append(tmpl.FormColumns, PackInput(*attr, vtEntity, false, vtTemplate))
}
}
return tmpl
}
+const tableDatePipe = "tableDate"
+
// AttributeData stores attribute info
type AttributeData struct {
JSName string
@@ -70,6 +83,13 @@ type AttributeData struct {
HasPipe bool
Pipe template.HTML
+
+ // IsTableDate reports that the column is rendered through the tableDate helper.
+ IsTableDate bool
+
+ // Value is the ready to use cell expression for templates without Vue 2
+ // filters (vue3): `item.createdAt`, `tableDate(item.createdAt)`, `item.category?.title`
+ Value template.HTML
}
// PackAttribute packs mfd tmpl attribute to template data
@@ -78,16 +98,26 @@ func PackAttribute(vtEntity mfd.VTEntity, tmpl mfd.TmplAttribute) AttributeData
boolType := false
isSortable := true
+ jsName := mfd.VarName(tmpl.Name)
pipe := ""
+ value := template.HTML("item." + jsName)
+ isTableDate := false
+
if tmpl.VTAttribute != nil {
attr := tmpl.VTAttribute.Attribute
if attr.IsDateTime() {
- pipe = "tableDate"
+ pipe = tableDatePipe
+ value = template.HTML(fmt.Sprintf("%s(item.%s)", tableDatePipe, jsName))
+ isTableDate = true
}
if attr.ForeignKey != "" {
- pipe = fmt.Sprintf(`getField("%s")`, mfd.VarName(tmpl.FKOpts))
+ fkField := mfd.VarName(tmpl.FKOpts)
+ pipe = fmt.Sprintf(`getField("%s")`, fkField)
+ // vue3 has no filters, so the nested field is read directly
+ value = template.HTML(fmt.Sprintf("item.%s?.%s", jsName, fkField))
isSortable = false
+ isTableDate = false
}
if attr.IsBool() || tmpl.Search == mfd.TypeHTMLCheckbox {
boolType = true
@@ -96,12 +126,14 @@ func PackAttribute(vtEntity mfd.VTEntity, tmpl mfd.TmplAttribute) AttributeData
}
return AttributeData{
- JSName: mfd.VarName(tmpl.Name),
- EditLink: vtEntity.Mode == mfd.ModeFull && (lowerName == "title" || lowerName == "name"),
- IsBool: tmpl.List && boolType,
- IsSortable: isSortable,
- HasPipe: pipe != "",
- Pipe: template.HTML(pipe),
+ JSName: jsName,
+ EditLink: vtEntity.Mode == mfd.ModeFull && (lowerName == "title" || lowerName == "name"),
+ IsBool: tmpl.List && boolType,
+ IsSortable: isSortable,
+ HasPipe: pipe != "",
+ Pipe: template.HTML(pipe),
+ IsTableDate: isTableDate,
+ Value: value,
}
}
@@ -123,8 +155,45 @@ type InputData struct {
Params []template.HTML
}
-// PackInput packs mfd tmpl attribute to template input data
-func PackInput(tmpl mfd.TmplAttribute, vtEntity mfd.VTEntity, isSearch bool) InputData {
+// IsDefaultComponent reports that Component is the fallback text field, so vue3
+// templates may skip the explicit component attribute.
+func (i InputData) IsDefaultComponent() bool {
+ return i.Component == defaultInputComponent
+}
+
+// vtDialect holds the js and vuetify api differences between the built-in
+// template sets that leak into the packed data.
+type vtDialect struct {
+ // modelPrefix is how form templates reach the edited model: the mobx store
+ // in vue2, the model destructured from useEntityForm in the other sets.
+ modelPrefix template.HTML
+ // smAndUp is the "small and up" breakpoint expression, renamed in vuetify 3.
+ smAndUp template.HTML
+}
+
+var vtDialects = map[string]vtDialect{
+ mfd.VTTemplateVue2: {modelPrefix: "store.model.", smAndUp: "$vuetify.breakpoint.smAndUp"},
+ mfd.VTTemplateComposition: {modelPrefix: "model.", smAndUp: "$vuetify.breakpoint.smAndUp"},
+ mfd.VTTemplateVue3: {modelPrefix: "model.", smAndUp: "$vuetify.display.smAndUp"},
+}
+
+// dialect returns the dialect of a normalized template set name, falling back
+// to vue2 the same way mfd.Project.VTTemplateName does.
+func dialect(vtTemplate string) vtDialect {
+ if d, ok := vtDialects[vtTemplate]; ok {
+ return d
+ }
+
+ return vtDialects[mfd.VTTemplateVue2]
+}
+
+// PackInput packs mfd tmpl attribute to template input data.
+// vtTemplate is the normalized project-level template set name, it selects the
+// dialect the packed params are rendered in.
+func PackInput(tmpl mfd.TmplAttribute, vtEntity mfd.VTEntity, isSearch bool, vtTemplate string) InputData {
+ d := dialect(vtTemplate)
+ modelPrefix, smAndUp := d.modelPrefix, d.smAndUp
+
inp := InputData{
JSName: mfd.VarName(tmpl.Name),
Component: filterComponent(tmpl.Search, isSearch),
@@ -139,7 +208,7 @@ func PackInput(tmpl mfd.TmplAttribute, vtEntity mfd.VTEntity, isSearch bool) Inp
inp.Component = "vt-status-select"
if !isSearch {
- inp.Params = append(inp.Params, `compact`, `:row="$vuetify.breakpoint.smAndUp"`)
+ inp.Params = append(inp.Params, `compact`, `:row="`+smAndUp+`"`)
}
}
@@ -164,7 +233,7 @@ func PackInput(tmpl mfd.TmplAttribute, vtEntity mfd.VTEntity, isSearch bool) Inp
trasliteratingValue := template.HTML(mfd.VarName(title.Name))
inp.Component = "vt-transliterator"
- inp.Params = append(inp.Params, `:value-for-transliterating="store.model.`+trasliteratingValue+`"`)
+ inp.Params = append(inp.Params, `:value-for-transliterating="`+modelPrefix+trasliteratingValue+`"`)
}
}
@@ -178,8 +247,8 @@ func PackInput(tmpl mfd.TmplAttribute, vtEntity mfd.VTEntity, isSearch bool) Inp
inp.IsFK = false
inp.FKJSName = mfd.VarName(mfd.FKName(tmpl.AttrName))
inp.FKJSSearch = mfd.VarName(tmpl.FKOpts)
- inp.Params = append(inp.Params, `:file="store.model.`+template.HTML(inp.FKJSName)+`"
- @input:file="file => store.model.`+template.HTML(inp.FKJSName)+` = file"`)
+ inp.Params = append(inp.Params, `:file="`+modelPrefix+template.HTML(inp.FKJSName)+`"
+ @input:file="file => `+modelPrefix+template.HTML(inp.FKJSName)+` = file"`)
} else if attr.ForeignKey != "" {
inp.Component = "vt-entity-autocomplete"
inp.IsFK = true
@@ -199,8 +268,11 @@ func PackInput(tmpl mfd.TmplAttribute, vtEntity mfd.VTEntity, isSearch bool) Inp
return inp
}
+// defaultInputComponent is the fallback input component
+const defaultInputComponent = "v-text-field"
+
func filterComponent(input string, isSearch bool) string {
- defaultComponent := "v-text-field"
+ defaultComponent := defaultInputComponent
switch input {
case mfd.TypeHTMLInput:
return defaultComponent
diff --git a/generators/xml/README.md b/generators/xml/README.md
index b7e7d29..c164ad1 100644
--- a/generators/xml/README.md
+++ b/generators/xml/README.md
@@ -53,6 +53,7 @@ Flags:
news,categories,tags
8
+ vue3
```
@@ -63,6 +64,7 @@ Flags:
- импорты (`"github.com/go-pg/pg"` vs `"github.com/go-pg/pg/v9"` vs `"github.com/go-pg/pg/v10"`)
- аннотации к структурам (`sql:"title"` vs `pg:"title"`)
- функции (`pg.F` и `pg.Q` vs `pg.Ident` и `pg.SafeQuery`)
+**VTTemplate** - Опциональное поле, задаёт набор vt-шаблонов в генераторе [template](/generators/vt-template). `vue2` (по умолчанию) — class-based компоненты (`vue-property-decorator` + `mobx-vue`); `composition` — Vue 2 Composition API (`defineComponent`/`setup` + composables); `vue3` — Vue 3 + Vuetify 3. Если элемент отсутствует, пуст или содержит неизвестное значение, используется `vue2`.
#### Namespace файл и сущности
Файл с неймспейсом, содержит все входящие в него сущности. Сущности будут сгруппированы в файлы по неймспейсам и в дальнейшей генерации
diff --git a/mfd/model.go b/mfd/model.go
index 3957846..3751bfc 100644
--- a/mfd/model.go
+++ b/mfd/model.go
@@ -134,6 +134,7 @@ type Project struct {
NamespaceNames []string `xml:"PackageNames>string" json:"-"`
Languages []string `xml:"Languages>string" json:"languages"`
GoPGVer int `xml:"GoPGVer" json:"goPGVer"`
+ VTTemplate string `xml:"VTTemplate,omitempty" json:"vtTemplate,omitempty"`
CustomTypes CustomTypes `xml:"CustomTypes>CustomType,omitempty" json:"customTypes,omitempty"`
Dictionary *Dictionary `xml:"Dictionary" json:"dict,omitempty"`
TableMapping TableMapping `xml:"TableMapping" json:"tableMapping,omitempty"`
@@ -171,6 +172,15 @@ func (tm *TableMapping) Packages() map[string]string {
return packages
}
+const (
+ // VTTemplateVue2 - class-based components on vue-property-decorator + mobx-vue. Default.
+ VTTemplateVue2 = "vue2"
+ // VTTemplateComposition - Vue 2 Composition API components (defineComponent/setup + composables).
+ VTTemplateComposition = "composition"
+ // VTTemplateVue3 - Vue 3 + Vuetify 3 components (defineComponent/setup + composables).
+ VTTemplateVue3 = "vue3"
+)
+
func NewProject(name string, goPGVer int) *Project {
return &Project{
Name: name,
@@ -184,6 +194,15 @@ func NewProject(name string, goPGVer int) *Project {
}
}
+func (p *Project) VTTemplateName() string {
+ switch name := strings.ToLower(strings.TrimSpace(p.VTTemplate)); name {
+ case VTTemplateComposition, VTTemplateVue3:
+ return name
+ }
+
+ return VTTemplateVue2
+}
+
// Namespace returns mfd.Namespace by its name
func (p *Project) Namespace(namespace string) *Namespace {
for _, ns := range p.Namespaces {