diff --git a/generators/testdata/consts.go b/generators/testdata/consts.go
index bee57ea..27ff1eb 100644
--- a/generators/testdata/consts.go
+++ b/generators/testdata/consts.go
@@ -16,25 +16,28 @@ const (
PackageVTUpdated = "vt-updated"
PackageDBTest = "test"
- PrefixAll = "all"
- PrefixEntity = "entities"
+ PrefixAll = "all"
+ PrefixEntity = "entities"
+ PrefixComposition = "composition"
)
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)
+ 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/composition/src/pages/Entity/Category/Form.vue b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Category/Form.vue
new file mode 100644
index 0000000..ab8d2d8
--- /dev/null
+++ b/generators/testdata/expected/vt-template/composition/src/pages/Entity/Category/Form.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+ {{ 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..261806a
--- /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..c9dc873
--- /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..71dec4b
--- /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/vt-template/README.md b/generators/vt-template/README.md
index e0d7255..230b670 100644
--- a/generators/vt-template/README.md
+++ b/generators/vt-template/README.md
@@ -25,10 +25,19 @@ 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
```
+#### Шаблоны
+
+Встроенные шаблоны существуют в двух вариантах:
+- **default** (по умолчанию) — class-based компоненты на `vue-property-decorator` + `mobx-vue`.
+- **Composition API** — генерирует компоненты через `defineComponent`/`setup` и подключает composables (`useEntityList`, `useEntityForm`, `useI18n`), которые должны существовать во фронтенд-проекте (`@/composables/...`); в `routes.ts` добавляется поле `meta.title`.
+
+Выбор варианта задаётся на уровне проекта в mfd-файле . В корневом `` добавьте элемент `true` — при его наличии используются Composition API шаблоны. Если элемент отсутствует или `false`, используются default class-based шаблоны.
+
#### MODE
Значение Mode vt-сущности в vt.xml определяет какие файлы будут сгенерированы.
@@ -36,6 +45,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..8149a88 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"
@@ -93,7 +95,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 +121,33 @@ func (g *Generator) Generate() error {
g.options.Namespaces = project.NamespaceNames
}
+ // selecting built-in templates set based on the project-level setting:
+ // composition (VTComposition=true) or default class-based templates
+ defRoutes, defList, defFilter, defForm := g.defaultTemplates(project.VTComposition)
+
// loading templates
- routesTemplate, err := mfd.LoadTemplate(g.options.RoutesTemplatePath, routesDefaultTemplate)
+ routesTemplate, err := mfd.LoadTemplate(g.options.RoutesTemplatePath, defRoutes)
if err != nil {
return fmt.Errorf("load routes template, err=%w", err)
}
- listTemplate, err := mfd.LoadTemplate(g.options.ListTemplatePath, listDefaultTemplate)
+ listTemplate, err := mfd.LoadTemplate(g.options.ListTemplatePath, defList)
if err != nil {
return fmt.Errorf("load list template, err=%w", err)
}
- filterTemplate, err := mfd.LoadTemplate(g.options.FiltersTemplatePath, filterDefaultTemplate)
+ filterTemplate, err := mfd.LoadTemplate(g.options.FiltersTemplatePath, defFilter)
if err != nil {
return fmt.Errorf("load filter template, err=%w", err)
}
- formTemplate, err := mfd.LoadTemplate(g.options.ListTemplatePath, formDefaultTemplate)
+ formTemplate, err := mfd.LoadTemplate(g.options.FormTemplatePath, defForm)
if err != nil {
return fmt.Errorf("load form template, err=%w", err)
}
// generating routes for all namespaces
- if _, err := g.SaveRoutes(project.VTNamespaces, routesTemplate); err != nil {
+ if _, err := g.SaveRoutes(project, routesTemplate); err != nil {
return fmt.Errorf("generate routes, err=%w", err)
}
@@ -172,17 +178,17 @@ func (g *Generator) Generate() error {
continue
}
- if err := g.SaveEntity(*entity, "List.vue", listTemplate); err != nil {
+ if err := g.SaveEntity(*entity, "List.vue", listTemplate, project.VTComposition); 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(*entity, "components/MultiListFilters.vue", filterTemplate, project.VTComposition); 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(*entity, "Form.vue", formTemplate, project.VTComposition); err != nil {
return fmt.Errorf("generate entity %s form, err=%w", entity.Name, err)
}
}
@@ -199,8 +205,46 @@ func (g *Generator) Generate() error {
return mfd.SaveMFD(g.options.MFDPath, project)
}
+// defaultTemplates returns the built-in templates set used as fallback when no
+// custom template path is provided. When composition is true (driven by the
+// project-level VTComposition setting) it returns the Vue Composition API
+// templates, otherwise the default class-based ones.
+func (g *Generator) defaultTemplates(composition bool) (routes, list, filter, form string) {
+ if composition {
+ return routesCompositionTemplate, listCompositionTemplate, filterCompositionTemplate, formCompositionTemplate
+ }
+
+ return routesDefaultTemplate, listDefaultTemplate, filterDefaultTemplate, formDefaultTemplate
+}
+
+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)
+ }
+ }
+ }
+
+ return targetEntities
+}
+
// SaveEntity saves vt entity to template with special delims
-func (g *Generator) SaveEntity(entity mfd.VTEntity, output, tmpl string) error {
+func (g *Generator) SaveEntity(entity mfd.VTEntity, output, tmpl string, composition bool) error {
parsed, err := template.New("base").
Delims("[[", "]]").
Funcs(mfd.TemplateFunctions).
@@ -209,7 +253,7 @@ func (g *Generator) SaveEntity(entity mfd.VTEntity, output, tmpl string) error {
return fmt.Errorf("parsing template, err=%w", err)
}
- packed := PackEntity(entity)
+ packed := PackEntity(entity, composition)
var buffer bytes.Buffer
if err := parsed.ExecuteTemplate(&buffer, "base", packed); err != nil {
@@ -221,13 +265,20 @@ func (g *Generator) SaveEntity(entity mfd.VTEntity, output, tmpl string) error {
}
// SaveRoutes saves all vt namespaces to routes file
-func (g *Generator) SaveRoutes(namespaces []*mfd.VTNamespace, tmpl string) (bool, error) {
+func (g *Generator) SaveRoutes(project *mfd.Project, tmpl string) (bool, error) {
+ var targetEntities []string
+ isPartial := len(g.options.Namespaces) > 0 || len(g.options.Entities) > 0
+
+ if isPartial {
+ targetEntities = g.getTargetEntities(project)
+ }
+
parsed, err := template.New("base").Funcs(mfd.TemplateFunctions).Parse(tmpl)
if err != nil {
return false, fmt.Errorf("parsing template, err=%w", err)
}
- pack, err := PackRoutesNamespace(namespaces)
+ pack, err := PackRoutesNamespace(project.VTNamespaces)
if err != nil {
return false, fmt.Errorf("packing data, err=%w", err)
}
@@ -237,7 +288,110 @@ func (g *Generator) SaveRoutes(namespaces []*mfd.VTNamespace, tmpl string) (bool
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, "src/pages/Entity/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 {
diff --git a/generators/vt-template/generator_test.go b/generators/vt-template/generator_test.go
index 3d5c8de..a132540 100644
--- a/generators/vt-template/generator_test.go
+++ b/generators/vt-template/generator_test.go
@@ -3,21 +3,29 @@ package vttmpl
import (
"os"
"path/filepath"
+ "reflect"
"strings"
"testing"
"github.com/vmkteam/mfd-generator/generators/testdata"
+ "github.com/vmkteam/mfd-generator/mfd"
. "github.com/smartystreets/goconvey/convey"
)
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 class-based templates
+ // (project mfd has no VTComposition setting)
Convey("Check correct generate", func() {
t.Log("Generate vt-template")
@@ -56,6 +64,7 @@ func TestGenerator_Generate(t *testing.T) {
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")
@@ -103,6 +112,127 @@ func TestGenerator_Generate(t *testing.T) {
})
}
+func TestGenerator_GenerateComposition(t *testing.T) {
+ // clean up leftover output from previous runs so partial routes injection starts fresh
+ _ = os.RemoveAll(testdata.PathActualVTTemplateComposition)
+
+ // build a temp mfd with the project-level VTComposition setting enabled,
+ // placed next to the translation files so they still resolve
+ project, err := mfd.LoadProject(testdata.PathExpectedMFD, false, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ project.VTComposition = true
+ compositionMFD := filepath.Join(filepath.Dir(testdata.PathExpectedMFD), "newsportal.composition.mfd")
+ if err := mfd.SaveMFD(compositionMFD, project); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { _ = os.Remove(compositionMFD) })
+
+ Convey("TestGenerator_GenerateComposition", t, func() {
+ generator := New()
+
+ generator.options.Output = testdata.PathActualVTTemplateComposition
+ // composition templates are selected via the project-level VTComposition setting
+ generator.options.MFDPath = compositionMFD
+ generator.options.Namespaces = []string{"portal"}
+
+ Convey("Check correct generate with composition templates", func() {
+ t.Log("Generate vt-template with composition templates")
+ So(generator.Generate(), ShouldBeNil)
+ })
+
+ filePrefix := filepath.Join("src", "pages", "Entity")
+
+ Convey("Check generated composition 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.PathActualVTTemplateComposition, filePrefix, f)
+ t.Logf("Check %s file", filenameWithFullPath)
+ content, err := os.ReadFile(filenameWithFullPath)
+ So(err, ShouldBeNil)
+ expectedContent, err := os.ReadFile(filepath.Join(testdata.PathExpectedVTTemplateComposition, filePrefix, f))
+ So(err, ShouldBeNil)
+ So(string(content), ShouldResemble, string(expectedContent))
+ }
+ })
+ })
+}
+
+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; VTComposition drives the access prefix.
+ buildMediaMFD := func(composition bool) string {
+ p := mfd.NewProject("media.mfd", mfd.GoPG10)
+ p.NamespaceNames = []string{"media", "vfs"}
+ p.VTComposition = composition
+ name := "media.default.gen.mfd"
+ if composition {
+ name = "media.composition.gen.mfd"
+ }
+ mfdPath := filepath.Join(dir, name)
+ if err := mfd.SaveMFD(mfdPath, p); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { _ = os.Remove(mfdPath) })
+ return mfdPath
+ }
+
+ generateForm := func(composition bool) (string, error) {
+ out := filepath.Join(testdata.PathActual, "vt-template", "media")
+ if composition {
+ out += "-composition"
+ }
+ _ = os.RemoveAll(out)
+
+ generator := New()
+ generator.options.Output = out
+ generator.options.MFDPath = buildMediaMFD(composition)
+ 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 VTComposition", t, func() {
+ Convey("default class-based templates use store.model.", func() {
+ form, err := generateForm(false)
+ 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(true)
+ So(err, ShouldBeNil)
+ So(form, ShouldContainSubstring, `:value-for-transliterating="model.title"`)
+ So(form, ShouldContainSubstring, `:file="model.`)
+ So(form, ShouldNotContainSubstring, `store.model.`)
+ })
+ })
+}
+
func fullFilesPaths(path string) ([]string, error) {
files, err := os.ReadDir(path)
if err != nil {
@@ -124,3 +254,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_composition.go b/generators/vt-template/template_composition.go
new file mode 100644
index 0000000..442d4bc
--- /dev/null
+++ b/generators/vt-template/template_composition.go
@@ -0,0 +1,575 @@
+package vttmpl
+
+const routesCompositionTemplate = `/* eslint-disable */
+export default [{{range $model := .Entities}}
+ /* {{.Name}} */
+ {
+ name: "{{.JSName}}List",
+ path: "/{{.TerminalPath}}",
+ component: () =>
+ 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}}
+];
+`
+
+// fuck backtick js
+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/vt_template.go b/generators/vt-template/vt_template.go
index cd51ad4..8c85264 100644
--- a/generators/vt-template/vt_template.go
+++ b/generators/vt-template/vt_template.go
@@ -25,8 +25,9 @@ type EntityData struct {
FormColumns []InputData
}
-// PackEntity packs mfd vt entity to template data
-func PackEntity(vtEntity mfd.VTEntity) EntityData {
+// PackEntity packs mfd vt entity to template data.
+// composition is forwarded to PackInput to select the model access prefix.
+func PackEntity(vtEntity mfd.VTEntity, composition bool) EntityData {
pks := vtEntity.Entity.PKs()
pkPairs := make([]PKPair, len(pks))
for i := range pks {
@@ -50,10 +51,10 @@ 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, composition))
}
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, composition))
}
}
@@ -123,8 +124,16 @@ 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 {
+// PackInput packs mfd tmpl attribute to template input data.
+// composition selects the model access prefix: "model." for Composition API
+// templates (destructured from useEntityForm) or "store.model." for the default
+// class-based templates (mobx store).
+func PackInput(tmpl mfd.TmplAttribute, vtEntity mfd.VTEntity, isSearch, composition bool) InputData {
+ modelPrefix := template.HTML("store.model.")
+ if composition {
+ modelPrefix = "model."
+ }
+
inp := InputData{
JSName: mfd.VarName(tmpl.Name),
Component: filterComponent(tmpl.Search, isSearch),
@@ -164,7 +173,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 +187,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
diff --git a/generators/xml/README.md b/generators/xml/README.md
index b7e7d29..a1e9f4c 100644
--- a/generators/xml/README.md
+++ b/generators/xml/README.md
@@ -53,6 +53,7 @@ Flags:
news,categories,tags
8
+ true
```
@@ -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`)
+**VTComposition** - Опциональный флаг, задаёт стиль vt-шаблонов в генераторе [template](/generators/vt-template). `true` — компоненты на Vue Composition API (`defineComponent`/`setup` + composables). Если элемент отсутствует или `false` — используются default class-based шаблоны (`vue-property-decorator` + `mobx-vue`).
#### Namespace файл и сущности
Файл с неймспейсом, содержит все входящие в него сущности. Сущности будут сгруппированы в файлы по неймспейсам и в дальнейшей генерации
diff --git a/mfd/model.go b/mfd/model.go
index 3957846..66f11ed 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"`
+ VTComposition bool `xml:"VTComposition,omitempty" json:"vtComposition,omitempty"`
CustomTypes CustomTypes `xml:"CustomTypes>CustomType,omitempty" json:"customTypes,omitempty"`
Dictionary *Dictionary `xml:"Dictionary" json:"dict,omitempty"`
TableMapping TableMapping `xml:"TableMapping" json:"tableMapping,omitempty"`