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 @@ + + + + + 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 @@ + + + + + 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..c9dc873 --- /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..71dec4b --- /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/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 = `[[end]][[end]][[if not $.ReadOnly]] + [[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 = ` + +[[raw " +import { useEntityForm } from '@/composables/useEntityForm'; +import { useI18n } from '@/composables/useI18n' +import { [[.Name]] as Model } from '@/services/api/factory'; +import { defineComponent } from 'vue'; + +export default defineComponent({ + // eslint-disable-next-line vue/match-component-file-name + name: '[[.Name]]Form', + + setup () { + const { t } = useI18n(); + + const { + tab, + form, + model, + errors, + isLoading, + isChanged, + i18nFieldError, + tabsHasError, + + onSave, + onDelete, + navigateBack, + onSaveAndBack + } = useEntityForm({ Model} ); + + const getErrorMessage = (errorKey: string | null): string => { + const errorMessage = i18nFieldError(errorKey); + return errorMessage ? t(errorMessage) : ''; + }; + + return { + t, + tab, + form, + model, + errors, + isLoading, + isChanged, + i18nFieldError, + tabsHasError, + getErrorMessage, + + onSave, + onDelete, + navigateBack, + onSaveAndBack + }; + } +}); + + + +` 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"`