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 + + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 + + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 = `