diff --git a/modules/auth/i18n/auth.pot b/modules/auth/i18n/auth.pot index 6f7c0e635..01021548f 100644 --- a/modules/auth/i18n/auth.pot +++ b/modules/auth/i18n/auth.pot @@ -1019,42 +1019,47 @@ msgctxt "web/components/preferences/PreferencesDialog" msgid "Using current session language" msgstr "" -#: web/menu/menus.ts:34 +#: web/menu/menus.ts:40 msgctxt "web/menu/menus" msgid "Access Control" msgstr "" -#: web/menu/menus.ts:50 +#: web/menu/menus.ts:56 msgctxt "web/menu/menus" msgid "Access Rules" msgstr "" -#: web/menu/menus.ts:59 +#: web/menu/menus.ts:65 msgctxt "web/menu/menus" msgid "Field Rules" msgstr "" -#: web/menu/menus.ts:64 +#: web/menu/menus.ts:70 msgctxt "web/menu/menus" msgid "Method Access" msgstr "" -#: web/menu/menus.ts:54 +#: web/menu/menus.ts:30 +msgctxt "web/menu/menus" +msgid "Record Properties" +msgstr "" + +#: web/menu/menus.ts:60 msgctxt "web/menu/menus" msgid "Record Rules" msgstr "" -#: web/menu/menus.ts:44 +#: web/menu/menus.ts:50 msgctxt "web/menu/menus" msgid "Role List" msgstr "" -#: web/menu/menus.ts:76 +#: web/menu/menus.ts:82 msgctxt "web/menu/menus" msgid "Session List" msgstr "" -#: web/menu/menus.ts:81 +#: web/menu/menus.ts:87 msgctxt "web/menu/menus" msgid "Token List" msgstr "" @@ -1064,12 +1069,12 @@ msgctxt "web/menu/menus" msgid "Translations" msgstr "" -#: web/menu/menus.ts:69 +#: web/menu/menus.ts:75 msgctxt "web/menu/menus" msgid "UI Resource Grants" msgstr "" -#: web/menu/menus.ts:39 +#: web/menu/menus.ts:45 msgctxt "web/menu/menus" msgid "User List" msgstr "" @@ -1435,6 +1440,11 @@ msgctxt "web/route/routes" msgid "Method Access List" msgstr "" +#: web/route/routes.ts:379 +msgctxt "web/route/routes" +msgid "Record Properties" +msgstr "" + #: web/route/routes.ts:212 msgctxt "web/route/routes" msgid "Record Rule Details" @@ -1445,7 +1455,7 @@ msgctxt "web/route/routes" msgid "Record Rule List" msgstr "" -#: web/route/routes.ts:383 +#: web/route/routes.ts:392 msgctxt "web/route/routes" msgid "Register" msgstr "" diff --git a/modules/auth/i18n/zh_CN.po b/modules/auth/i18n/zh_CN.po index f2aee907a..26571665e 100644 --- a/modules/auth/i18n/zh_CN.po +++ b/modules/auth/i18n/zh_CN.po @@ -991,42 +991,47 @@ msgctxt "web/components/preferences/PreferencesDialog" msgid "Using current session language" msgstr "当前使用会话语言" -#: web/menu/menus.ts:34 +#: web/menu/menus.ts:40 msgctxt "web/menu/menus" msgid "Access Control" msgstr "访问控制" -#: web/menu/menus.ts:50 +#: web/menu/menus.ts:56 msgctxt "web/menu/menus" msgid "Access Rules" msgstr "授权规则" -#: web/menu/menus.ts:59 +#: web/menu/menus.ts:65 msgctxt "web/menu/menus" msgid "Field Rules" msgstr "字段规则" -#: web/menu/menus.ts:64 +#: web/menu/menus.ts:70 msgctxt "web/menu/menus" msgid "Method Access" msgstr "方法访问" -#: web/menu/menus.ts:54 +#: web/menu/menus.ts:30 +msgctxt "web/menu/menus" +msgid "Record Properties" +msgstr "记录属性" + +#: web/menu/menus.ts:60 msgctxt "web/menu/menus" msgid "Record Rules" msgstr "记录规则" -#: web/menu/menus.ts:44 +#: web/menu/menus.ts:50 msgctxt "web/menu/menus" msgid "Role List" msgstr "角色列表" -#: web/menu/menus.ts:76 +#: web/menu/menus.ts:82 msgctxt "web/menu/menus" msgid "Session List" msgstr "会话列表" -#: web/menu/menus.ts:81 +#: web/menu/menus.ts:87 msgctxt "web/menu/menus" msgid "Token List" msgstr "令牌列表" @@ -1036,12 +1041,12 @@ msgctxt "web/menu/menus" msgid "Translations" msgstr "翻译" -#: web/menu/menus.ts:69 +#: web/menu/menus.ts:75 msgctxt "web/menu/menus" msgid "UI Resource Grants" msgstr "UI 资源授权" -#: web/menu/menus.ts:39 +#: web/menu/menus.ts:45 msgctxt "web/menu/menus" msgid "User List" msgstr "用户列表" @@ -1401,6 +1406,11 @@ msgctxt "web/route/routes" msgid "Method Access List" msgstr "方法访问列表" +#: web/route/routes.ts:379 +msgctxt "web/route/routes" +msgid "Record Properties" +msgstr "记录属性" + #: web/route/routes.ts:212 msgctxt "web/route/routes" msgid "Record Rule Details" @@ -1411,7 +1421,7 @@ msgctxt "web/route/routes" msgid "Record Rule List" msgstr "记录规则列表" -#: web/route/routes.ts:383 +#: web/route/routes.ts:392 msgctxt "web/route/routes" msgid "Register" msgstr "注册" diff --git a/modules/auth/web/menu/menus.ts b/modules/auth/web/menu/menus.ts index c5b05e843..acd21678d 100644 --- a/modules/auth/web/menu/menus.ts +++ b/modules/auth/web/menu/menus.ts @@ -26,6 +26,12 @@ export const authSettingsAppMenu: MenuItem = defineMenu('auth.menu.settings_app' sequence: 10, defaultRoles: ['sys.admin'], }), + defineMenu('auth.menu.property_definitions', { + title: _lt('Record Properties'), + path: '/web/property-definitions/auth', + sequence: 20, + defaultRoles: ['sys.admin'], + }), ], }); diff --git a/modules/auth/web/route/routes.ts b/modules/auth/web/route/routes.ts index 002e9be49..56bd20f7a 100644 --- a/modules/auth/web/route/routes.ts +++ b/modules/auth/web/route/routes.ts @@ -4,7 +4,7 @@ import { RouteRecordRaw } from 'vue-router'; import { defineRoute } from '@/core/web/resource'; import { createTranslate } from '@/web/web/i18n'; -import { terminologyListComponent } from '@/web/web/route'; +import { propertyDefinitionListComponent, terminologyListComponent } from '@/web/web/route'; const { _lt } = createTranslate('auth', { scope: 'web/route/routes' }); @@ -374,6 +374,15 @@ export const appRoutes: RouteRecordRaw[] = [ defaultRoles: ['sys.admin'], meta: { requiresAuth: true, terminologyApplication: 'auth' }, }), + defineRoute('auth.route.property_definition_list', { + sequence: 50, + title: _lt('Record Properties'), + path: 'web/property-definitions/auth', + name: 'PropertyDefinitionList_auth', + component: propertyDefinitionListComponent, + defaultRoles: ['sys.admin'], + meta: { requiresAuth: true, propertyDefinitionApplication: 'auth' }, + }), ]; // Add the registration route when self-service registration is enabled. diff --git a/modules/base/i18n/base.pot b/modules/base/i18n/base.pot index 22a541337..3a0084ca2 100644 --- a/modules/base/i18n/base.pot +++ b/modules/base/i18n/base.pot @@ -1031,62 +1031,67 @@ msgctxt "service/models/uom" msgid "Rounding must be greater than 0" msgstr "" -#: web/menu/menus.ts:34 +#: web/menu/menus.ts:40 msgctxt "web/menu/menus" msgid "Address Management" msgstr "" -#: web/menu/menus.ts:35 +#: web/menu/menus.ts:41 msgctxt "web/menu/menus" msgid "Bank Management" msgstr "" -#: web/menu/menus.ts:38 +#: web/menu/menus.ts:44 msgctxt "web/menu/menus" msgid "City Management" msgstr "" -#: web/menu/menus.ts:33 +#: web/menu/menus.ts:39 msgctxt "web/menu/menus" msgid "Company Management" msgstr "" -#: web/menu/menus.ts:36 +#: web/menu/menus.ts:42 msgctxt "web/menu/menus" msgid "Country Management" msgstr "" -#: web/menu/menus.ts:39 +#: web/menu/menus.ts:45 msgctxt "web/menu/menus" msgid "Currency Management" msgstr "" -#: web/menu/menus.ts:40 +#: web/menu/menus.ts:46 msgctxt "web/menu/menus" msgid "Exchange Rate Management" msgstr "" -#: web/menu/menus.ts:41 +#: web/menu/menus.ts:47 msgctxt "web/menu/menus" msgid "Language Management" msgstr "" -#: web/menu/menus.ts:29 +#: web/menu/menus.ts:35 msgctxt "web/menu/menus" msgid "Master Data" msgstr "" -#: web/menu/menus.ts:43 +#: web/menu/menus.ts:25 +msgctxt "web/menu/menus" +msgid "Record Properties" +msgstr "" + +#: web/menu/menus.ts:49 msgctxt "web/menu/menus" msgid "Sequence Idempotency Record" msgstr "" -#: web/menu/menus.ts:42 +#: web/menu/menus.ts:48 msgctxt "web/menu/menus" msgid "Sequence Management" msgstr "" -#: web/menu/menus.ts:37 +#: web/menu/menus.ts:43 msgctxt "web/menu/menus" msgid "State Management" msgstr "" @@ -1096,12 +1101,12 @@ msgctxt "web/menu/menus" msgid "Translations" msgstr "" -#: web/menu/menus.ts:45 +#: web/menu/menus.ts:51 msgctxt "web/menu/menus" msgid "Unit of Measure" msgstr "" -#: web/menu/menus.ts:44 +#: web/menu/menus.ts:50 msgctxt "web/menu/menus" msgid "Unit of Measure Category" msgstr "" @@ -1326,6 +1331,11 @@ msgctxt "web/route/routes" msgid "Language List" msgstr "" +#: web/route/routes.ts:441 +msgctxt "web/route/routes" +msgid "Record Properties" +msgstr "" + #: web/route/routes.ts:263 msgctxt "web/route/routes" msgid "Sequence Details" diff --git a/modules/base/i18n/zh_CN.po b/modules/base/i18n/zh_CN.po index 127c92b98..fa3b99db9 100644 --- a/modules/base/i18n/zh_CN.po +++ b/modules/base/i18n/zh_CN.po @@ -1021,62 +1021,67 @@ msgctxt "service/models/uom" msgid "Rounding must be greater than 0" msgstr "Rounding 必须大于 0" -#: web/menu/menus.ts:34 +#: web/menu/menus.ts:40 msgctxt "web/menu/menus" msgid "Address Management" msgstr "地址管理" -#: web/menu/menus.ts:35 +#: web/menu/menus.ts:41 msgctxt "web/menu/menus" msgid "Bank Management" msgstr "银行管理" -#: web/menu/menus.ts:38 +#: web/menu/menus.ts:44 msgctxt "web/menu/menus" msgid "City Management" msgstr "城市管理" -#: web/menu/menus.ts:33 +#: web/menu/menus.ts:39 msgctxt "web/menu/menus" msgid "Company Management" msgstr "公司管理" -#: web/menu/menus.ts:36 +#: web/menu/menus.ts:42 msgctxt "web/menu/menus" msgid "Country Management" msgstr "国家/地区管理" -#: web/menu/menus.ts:39 +#: web/menu/menus.ts:45 msgctxt "web/menu/menus" msgid "Currency Management" msgstr "币种管理" -#: web/menu/menus.ts:40 +#: web/menu/menus.ts:46 msgctxt "web/menu/menus" msgid "Exchange Rate Management" msgstr "汇率管理" -#: web/menu/menus.ts:41 +#: web/menu/menus.ts:47 msgctxt "web/menu/menus" msgid "Language Management" msgstr "语言管理" -#: web/menu/menus.ts:29 +#: web/menu/menus.ts:35 msgctxt "web/menu/menus" msgid "Master Data" msgstr "基础数据" -#: web/menu/menus.ts:43 +#: web/menu/menus.ts:25 +msgctxt "web/menu/menus" +msgid "Record Properties" +msgstr "记录属性" + +#: web/menu/menus.ts:49 msgctxt "web/menu/menus" msgid "Sequence Idempotency Record" msgstr "序列幂等记录" -#: web/menu/menus.ts:42 +#: web/menu/menus.ts:48 msgctxt "web/menu/menus" msgid "Sequence Management" msgstr "序列管理" -#: web/menu/menus.ts:37 +#: web/menu/menus.ts:43 msgctxt "web/menu/menus" msgid "State Management" msgstr "州/省管理" @@ -1086,12 +1091,12 @@ msgctxt "web/menu/menus" msgid "Translations" msgstr "翻译" -#: web/menu/menus.ts:45 +#: web/menu/menus.ts:51 msgctxt "web/menu/menus" msgid "Unit of Measure" msgstr "计量单位" -#: web/menu/menus.ts:44 +#: web/menu/menus.ts:50 msgctxt "web/menu/menus" msgid "Unit of Measure Category" msgstr "计量单位类别" @@ -1316,6 +1321,11 @@ msgctxt "web/route/routes" msgid "Language List" msgstr "语言列表" +#: web/route/routes.ts:441 +msgctxt "web/route/routes" +msgid "Record Properties" +msgstr "记录属性" + #: web/route/routes.ts:263 msgctxt "web/route/routes" msgid "Sequence Details" diff --git a/modules/base/web/menu/menus.ts b/modules/base/web/menu/menus.ts index 24765cf49..aeca1dc5c 100644 --- a/modules/base/web/menu/menus.ts +++ b/modules/base/web/menu/menus.ts @@ -21,6 +21,12 @@ export const baseSettingsAppMenu: MenuItem = defineMenu('base.menu.settings_app' sequence: 10, defaultRoles: ['sys.admin'], }), + defineMenu('base.menu.property_definitions', { + title: _lt('Record Properties'), + path: '/web/property-definitions/base', + sequence: 20, + defaultRoles: ['sys.admin'], + }), ], }); diff --git a/modules/base/web/route/routes.ts b/modules/base/web/route/routes.ts index 33c675e49..0800ad7f5 100644 --- a/modules/base/web/route/routes.ts +++ b/modules/base/web/route/routes.ts @@ -4,7 +4,7 @@ import { RouteRecordRaw } from 'vue-router'; import { defineRoute } from '@/core/web/resource'; import { createTranslate } from '@/web/web/i18n'; -import { terminologyListComponent } from '@/web/web/route'; +import { propertyDefinitionListComponent, terminologyListComponent } from '@/web/web/route'; const { _lt } = createTranslate('base', { scope: 'web/route/routes' }); @@ -436,4 +436,13 @@ export const baseRoutes: RouteRecordRaw[] = [ defaultRoles: ['sys.admin'], meta: { requiresAuth: true, terminologyApplication: 'base' }, }), + defineRoute('base.route.property_definition_list', { + sequence: 50, + title: _lt('Record Properties'), + path: 'web/property-definitions/base', + name: 'PropertyDefinitionList_base', + component: propertyDefinitionListComponent, + defaultRoles: ['sys.admin'], + meta: { requiresAuth: true, propertyDefinitionApplication: 'base' }, + }), ]; diff --git a/modules/meta/i18n/meta.pot b/modules/meta/i18n/meta.pot index 761abcf32..b9d4d8bff 100644 --- a/modules/meta/i18n/meta.pot +++ b/modules/meta/i18n/meta.pot @@ -1155,26 +1155,31 @@ msgctxt "service/models/ui_resource" msgid "Cycle detected: %s cannot be assigned under one of its descendants" msgstr "" -#: web/menu/menus.ts:34 +#: web/menu/menus.ts:40 msgctxt "web/menu/menus" msgid "Module Board" msgstr "" -#: web/menu/menus.ts:40 +#: web/menu/menus.ts:46 msgctxt "web/menu/menus" msgid "Module List" msgstr "" -#: web/menu/menus.ts:29 +#: web/menu/menus.ts:35 msgctxt "web/menu/menus" msgid "Module Management" msgstr "" -#: web/menu/menus.ts:46 +#: web/menu/menus.ts:52 msgctxt "web/menu/menus" msgid "Operation History" msgstr "" +#: web/menu/menus.ts:25 +msgctxt "web/menu/menus" +msgid "Record Properties" +msgstr "" + #: web/menu/menus.ts:19 msgctxt "web/menu/menus" msgid "Translations" @@ -1220,6 +1225,11 @@ msgctxt "web/route/routes" msgid "Operation History" msgstr "" +#: web/route/routes.ts:66 +msgctxt "web/route/routes" +msgid "Record Properties" +msgstr "" + #: web/route/routes.ts:57 msgctxt "web/route/routes" msgid "Translations" diff --git a/modules/meta/i18n/zh_CN.po b/modules/meta/i18n/zh_CN.po index 25ba005ed..9c31e30ea 100644 --- a/modules/meta/i18n/zh_CN.po +++ b/modules/meta/i18n/zh_CN.po @@ -1146,26 +1146,31 @@ msgctxt "service/models/ui_resource" msgid "Cycle detected: %s cannot be assigned under one of its descendants" msgstr "检测到循环:%s 不能挂到其子孙节点下" -#: web/menu/menus.ts:34 +#: web/menu/menus.ts:40 msgctxt "web/menu/menus" msgid "Module Board" msgstr "模块看板" -#: web/menu/menus.ts:40 +#: web/menu/menus.ts:46 msgctxt "web/menu/menus" msgid "Module List" msgstr "模块列表" -#: web/menu/menus.ts:29 +#: web/menu/menus.ts:35 msgctxt "web/menu/menus" msgid "Module Management" msgstr "模块管理" -#: web/menu/menus.ts:46 +#: web/menu/menus.ts:52 msgctxt "web/menu/menus" msgid "Operation History" msgstr "操作历史" +#: web/menu/menus.ts:25 +msgctxt "web/menu/menus" +msgid "Record Properties" +msgstr "记录属性" + #: web/menu/menus.ts:19 msgctxt "web/menu/menus" msgid "Translations" @@ -1211,6 +1216,11 @@ msgctxt "web/route/routes" msgid "Operation History" msgstr "操作历史" +#: web/route/routes.ts:66 +msgctxt "web/route/routes" +msgid "Record Properties" +msgstr "记录属性" + #: web/route/routes.ts:57 msgctxt "web/route/routes" msgid "Translations" diff --git a/modules/meta/web/menu/menus.ts b/modules/meta/web/menu/menus.ts index b1f48ffb3..fe4b0b6d8 100644 --- a/modules/meta/web/menu/menus.ts +++ b/modules/meta/web/menu/menus.ts @@ -21,6 +21,12 @@ export const metaSettingsAppMenu: MenuItem = defineMenu('meta.menu.settings_app' sequence: 10, defaultRoles: ['sys.admin'], }), + defineMenu('meta.menu.property_definitions', { + title: _lt('Record Properties'), + path: '/web/property-definitions/meta', + sequence: 20, + defaultRoles: ['sys.admin'], + }), ], }); diff --git a/modules/meta/web/route/routes.ts b/modules/meta/web/route/routes.ts index b689b8995..4dfd110b3 100644 --- a/modules/meta/web/route/routes.ts +++ b/modules/meta/web/route/routes.ts @@ -4,7 +4,7 @@ import { RouteRecordRaw } from 'vue-router'; import { defineRoute } from '@/core/web/resource'; import { createTranslate } from '@/web/web/i18n'; -import { terminologyListComponent } from '@/web/web/route'; +import { propertyDefinitionListComponent, terminologyListComponent } from '@/web/web/route'; const { _lt } = createTranslate('meta', { scope: 'web/route/routes' }); @@ -61,4 +61,13 @@ export const metaRoutes: RouteRecordRaw[] = [ defaultRoles: ['sys.admin'], meta: { requiresAuth: true, terminologyApplication: 'meta' }, }), + defineRoute('meta.route.property_definition_list', { + sequence: 50, + title: _lt('Record Properties'), + path: 'web/property-definitions/meta', + name: 'PropertyDefinitionList_meta', + component: propertyDefinitionListComponent, + defaultRoles: ['sys.admin'], + meta: { requiresAuth: true, propertyDefinitionApplication: 'meta' }, + }), ]; diff --git a/modules/partner/i18n/partner.pot b/modules/partner/i18n/partner.pot index 11f7e19e0..382c521d5 100644 --- a/modules/partner/i18n/partner.pot +++ b/modules/partner/i18n/partner.pot @@ -281,6 +281,11 @@ msgctxt "web/menu/menus" msgid "Partner Management" msgstr "" +#: web/menu/menus.ts:27 +msgctxt "web/menu/menus" +msgid "Record Properties" +msgstr "" + #: web/menu/menus.ts:21 msgctxt "web/menu/menus" msgid "Translations" @@ -316,6 +321,11 @@ msgctxt "web/route/routes" msgid "Partner List" msgstr "" +#: web/route/routes.ts:59 +msgctxt "web/route/routes" +msgid "Record Properties" +msgstr "" + #: web/route/routes.ts:50 msgctxt "web/route/routes" msgid "Translations" diff --git a/modules/partner/i18n/zh_CN.po b/modules/partner/i18n/zh_CN.po index 5a0b5f5f7..619ba68d0 100644 --- a/modules/partner/i18n/zh_CN.po +++ b/modules/partner/i18n/zh_CN.po @@ -271,6 +271,11 @@ msgctxt "web/menu/menus" msgid "Partner Management" msgstr "伙伴管理" +#: web/menu/menus.ts:27 +msgctxt "web/menu/menus" +msgid "Record Properties" +msgstr "记录属性" + #: web/menu/menus.ts:21 msgctxt "web/menu/menus" msgid "Translations" @@ -306,6 +311,11 @@ msgctxt "web/route/routes" msgid "Partner List" msgstr "伙伴列表" +#: web/route/routes.ts:59 +msgctxt "web/route/routes" +msgid "Record Properties" +msgstr "记录属性" + #: web/route/routes.ts:50 msgctxt "web/route/routes" msgid "Translations" diff --git a/modules/partner/web/menu/menus.ts b/modules/partner/web/menu/menus.ts index aa7d15c3c..51839d7d2 100644 --- a/modules/partner/web/menu/menus.ts +++ b/modules/partner/web/menu/menus.ts @@ -23,6 +23,12 @@ export const partnerSettingsAppMenu: MenuItem = defineMenu('partner.menu.setting sequence: 10, defaultRoles: ['sys.admin'], }), + defineMenu('partner.menu.property_definitions', { + title: _lt('Record Properties'), + path: '/web/property-definitions/partner', + sequence: 20, + defaultRoles: ['sys.admin'], + }), ], }); diff --git a/modules/partner/web/route/routes.ts b/modules/partner/web/route/routes.ts index d86a97efc..5912f1a05 100644 --- a/modules/partner/web/route/routes.ts +++ b/modules/partner/web/route/routes.ts @@ -4,7 +4,7 @@ import type { RouteRecordRaw } from 'vue-router'; import { defineRoute } from '@/core/web/resource'; import { createTranslate } from '@/web/web/i18n'; -import { terminologyListComponent } from '@/web/web/route'; +import { propertyDefinitionListComponent, terminologyListComponent } from '@/web/web/route'; const { _lt } = createTranslate('partner', { scope: 'web/route/routes' }); @@ -54,4 +54,13 @@ export const partnerRoutes: RouteRecordRaw[] = [ defaultRoles: ['sys.admin'], meta: { requiresAuth: true, terminologyApplication: 'partner' }, }), + defineRoute('partner.route.property_definition_list', { + sequence: 50, + title: _lt('Record Properties'), + path: 'web/property-definitions/partner', + name: 'PropertyDefinitionList_partner', + component: propertyDefinitionListComponent, + defaultRoles: ['sys.admin'], + meta: { requiresAuth: true, propertyDefinitionApplication: 'partner' }, + }), ]; diff --git a/modules/sandbox/i18n/sandbox.pot b/modules/sandbox/i18n/sandbox.pot index a4155343a..ed97f97d2 100644 --- a/modules/sandbox/i18n/sandbox.pot +++ b/modules/sandbox/i18n/sandbox.pot @@ -377,19 +377,19 @@ msgctxt "sandbox.model.SandboxTag.fields" msgid "Name" msgstr "" -#: web/menu/menus.ts:26 +#: web/menu/menus.ts:27 msgctxt "web/menu/menus" -msgid "Translations" +msgid "Record Properties" msgstr "" -#: web/menu/menus.ts:20 +#: web/menu/menus.ts:21 msgctxt "web/menu/menus" -msgid "sandbox" +msgid "Translations" msgstr "" -#: web/menu/titles.ts:10 -msgctxt "web/menu/titles" -msgid "Configuration" +#: web/menu/menus.ts:15 +msgctxt "web/menu/menus" +msgid "sandbox" msgstr "" #: web/menu/titles.ts:9 @@ -397,21 +397,11 @@ msgctxt "web/menu/titles" msgid "Field Kitchen Sink" msgstr "" -#: web/menu/titles.ts:11 -msgctxt "web/menu/titles" -msgid "Record Properties" -msgstr "" - #: web/menu/titles.ts:8 msgctxt "web/menu/titles" msgid "Sandbox" msgstr "" -#: web/pages/SandboxPropertyDefinitionList.vue:11 -msgctxt "web/pages/SandboxPropertyDefinitionList" -msgid "Record Properties" -msgstr "" - #: web/pages/SandboxRecordChart.vue:12 msgctxt "web/pages/SandboxRecordChart" msgid "Sandbox Record Chart" @@ -427,11 +417,6 @@ msgctxt "web/pages/SandboxRecordList" msgid "Field Kitchen Sink" msgstr "" -#: web/route/routes.ts:157 -msgctxt "web/route/routes" -msgid "New Property Definition" -msgstr "" - #: web/route/routes.ts:83 msgctxt "web/route/routes" msgid "New Sandbox Record" @@ -442,12 +427,7 @@ msgctxt "web/route/routes" msgid "New Sandbox Tag" msgstr "" -#: web/route/routes.ts:171 -msgctxt "web/route/routes" -msgid "Property Definition" -msgstr "" - -#: web/route/routes.ts:143 +#: web/route/routes.ts:150 msgctxt "web/route/routes" msgid "Record Properties" msgstr "" @@ -477,7 +457,7 @@ msgctxt "web/route/routes" msgid "Sandbox Tag Detail" msgstr "" -#: web/route/routes.ts:185 +#: web/route/routes.ts:141 msgctxt "web/route/routes" msgid "Translations" msgstr "" @@ -532,91 +512,6 @@ msgctxt "web/views/SandboxLinesVirtTools" msgid "Short Lines stay content-sized. Open VIRT-LINES or fill rows to exercise capped table / kanban virtualize (not saved until you Save)." msgstr "" -#: web/views/SandboxPropertyDefinitionFormView.vue:21 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Container Id" -msgstr "" - -#: web/views/SandboxPropertyDefinitionFormView.vue:18 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Container Model" -msgstr "" - -#: web/views/sandbox_property_definition_actions.ts:10 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Copy Property Definition" -msgstr "" - -#: web/views/sandbox_property_definition_actions.ts:10 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Create Property Definition" -msgstr "" - -#: web/views/SandboxPropertyDefinitionFormView.vue:26 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Definition" -msgstr "" - -#: web/views/sandbox_property_definition_actions.ts:10 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Delete Property Definition" -msgstr "" - -#: web/views/sandbox_property_definition_actions.ts:10 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Edit Property Definition" -msgstr "" - -#: web/views/SandboxPropertyDefinitionFormView.vue:15 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Properties Field" -msgstr "" - -#: web/views/sandbox_property_definition_actions.ts:11 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Property Definition" -msgstr "" - -#: web/views/SandboxPropertyDefinitionFormView.vue:21 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Required" -msgstr "" - -#: web/views/SandboxPropertyDefinitionFormView.vue:9 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Scope" -msgstr "" - -#: web/views/SandboxPropertyDefinitionFormView.vue:12 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Target Model" -msgstr "" - -#: web/views/SandboxPropertyDefinitionListView.vue:11 -msgctxt "web/views/SandboxPropertyDefinitionListView" -msgid "Container Id" -msgstr "" - -#: web/views/SandboxPropertyDefinitionListView.vue:10 -msgctxt "web/views/SandboxPropertyDefinitionListView" -msgid "Container Model" -msgstr "" - -#: web/views/SandboxPropertyDefinitionListView.vue:12 -msgctxt "web/views/SandboxPropertyDefinitionListView" -msgid "Definition" -msgstr "" - -#: web/views/SandboxPropertyDefinitionListView.vue:9 -msgctxt "web/views/SandboxPropertyDefinitionListView" -msgid "Properties Field" -msgstr "" - -#: web/views/SandboxPropertyDefinitionListView.vue:8 -msgctxt "web/views/SandboxPropertyDefinitionListView" -msgid "Target Model" -msgstr "" - #: web/views/SandboxRecordChartView.vue:43 msgctxt "web/views/SandboxRecordChartView" msgid "Chart View" diff --git a/modules/sandbox/i18n/zh_CN.po b/modules/sandbox/i18n/zh_CN.po index 5500dc027..5d0ea895c 100644 --- a/modules/sandbox/i18n/zh_CN.po +++ b/modules/sandbox/i18n/zh_CN.po @@ -371,41 +371,31 @@ msgctxt "sandbox.model.SandboxTag.fields" msgid "Name" msgstr "名称" -#: web/menu/menus.ts:26 +#: web/menu/menus.ts:27 +msgctxt "web/menu/menus" +msgid "Record Properties" +msgstr "记录属性" + +#: web/menu/menus.ts:21 msgctxt "web/menu/menus" msgid "Translations" msgstr "翻译" -#: web/menu/menus.ts:20 +#: web/menu/menus.ts:15 msgctxt "web/menu/menus" msgid "sandbox" msgstr "sandbox" -#: web/menu/titles.ts:10 -msgctxt "web/menu/titles" -msgid "Configuration" -msgstr "配置" - #: web/menu/titles.ts:9 msgctxt "web/menu/titles" msgid "Field Kitchen Sink" msgstr "字段试验场" -#: web/menu/titles.ts:11 -msgctxt "web/menu/titles" -msgid "Record Properties" -msgstr "记录属性" - #: web/menu/titles.ts:8 msgctxt "web/menu/titles" msgid "Sandbox" msgstr "沙箱" -#: web/pages/SandboxPropertyDefinitionList.vue:11 -msgctxt "web/pages/SandboxPropertyDefinitionList" -msgid "Record Properties" -msgstr "记录属性" - #: web/pages/SandboxRecordChart.vue:12 msgctxt "web/pages/SandboxRecordChart" msgid "Sandbox Record Chart" @@ -421,11 +411,6 @@ msgctxt "web/pages/SandboxRecordList" msgid "Field Kitchen Sink" msgstr "字段试验场" -#: web/route/routes.ts:157 -msgctxt "web/route/routes" -msgid "New Property Definition" -msgstr "新建属性定义" - #: web/route/routes.ts:83 msgctxt "web/route/routes" msgid "New Sandbox Record" @@ -436,12 +421,7 @@ msgctxt "web/route/routes" msgid "New Sandbox Tag" msgstr "新建沙箱标签" -#: web/route/routes.ts:171 -msgctxt "web/route/routes" -msgid "Property Definition" -msgstr "属性定义" - -#: web/route/routes.ts:143 +#: web/route/routes.ts:150 msgctxt "web/route/routes" msgid "Record Properties" msgstr "记录属性" @@ -471,7 +451,7 @@ msgctxt "web/route/routes" msgid "Sandbox Tag Detail" msgstr "沙箱标签详情" -#: web/route/routes.ts:185 +#: web/route/routes.ts:141 msgctxt "web/route/routes" msgid "Translations" msgstr "翻译" @@ -526,91 +506,6 @@ msgctxt "web/views/SandboxLinesVirtTools" msgid "Short Lines stay content-sized. Open VIRT-LINES or fill rows to exercise capped table / kanban virtualize (not saved until you Save)." msgstr "短 Lines 保持内容高度。打开 VIRT-LINES 或填充行以练习封顶表格 / kanban 虚拟化(保存前不落库)。" -#: web/views/SandboxPropertyDefinitionFormView.vue:21 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Container Id" -msgstr "容器 ID" - -#: web/views/SandboxPropertyDefinitionFormView.vue:18 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Container Model" -msgstr "容器模型" - -#: web/views/sandbox_property_definition_actions.ts:10 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Copy Property Definition" -msgstr "复制属性定义" - -#: web/views/sandbox_property_definition_actions.ts:10 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Create Property Definition" -msgstr "创建属性定义" - -#: web/views/SandboxPropertyDefinitionFormView.vue:26 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Definition" -msgstr "定义" - -#: web/views/sandbox_property_definition_actions.ts:10 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Delete Property Definition" -msgstr "删除属性定义" - -#: web/views/sandbox_property_definition_actions.ts:10 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Edit Property Definition" -msgstr "编辑属性定义" - -#: web/views/SandboxPropertyDefinitionFormView.vue:15 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Properties Field" -msgstr "属性字段" - -#: web/views/sandbox_property_definition_actions.ts:11 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Property Definition" -msgstr "属性定义" - -#: web/views/SandboxPropertyDefinitionFormView.vue:21 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Required" -msgstr "必填" - -#: web/views/SandboxPropertyDefinitionFormView.vue:9 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Scope" -msgstr "作用域" - -#: web/views/SandboxPropertyDefinitionFormView.vue:12 -msgctxt "web/views/SandboxPropertyDefinitionFormView" -msgid "Target Model" -msgstr "目标模型" - -#: web/views/SandboxPropertyDefinitionListView.vue:11 -msgctxt "web/views/SandboxPropertyDefinitionListView" -msgid "Container Id" -msgstr "容器 ID" - -#: web/views/SandboxPropertyDefinitionListView.vue:10 -msgctxt "web/views/SandboxPropertyDefinitionListView" -msgid "Container Model" -msgstr "容器模型" - -#: web/views/SandboxPropertyDefinitionListView.vue:12 -msgctxt "web/views/SandboxPropertyDefinitionListView" -msgid "Definition" -msgstr "定义" - -#: web/views/SandboxPropertyDefinitionListView.vue:9 -msgctxt "web/views/SandboxPropertyDefinitionListView" -msgid "Properties Field" -msgstr "属性字段" - -#: web/views/SandboxPropertyDefinitionListView.vue:8 -msgctxt "web/views/SandboxPropertyDefinitionListView" -msgid "Target Model" -msgstr "目标模型" - #: web/views/SandboxRecordChartView.vue:43 msgctxt "web/views/SandboxRecordChartView" msgid "Chart View" @@ -964,10 +859,58 @@ msgstr "标签" #~ msgid "Per-company varchar; open Company Values from the field chrome." #~ msgstr "按公司存储的 varchar;可从字段控件打开公司值。" +#~ msgctxt "web/menu/titles" +#~ msgid "Configuration" +#~ msgstr "配置" + +#~ msgctxt "web/menu/titles" +#~ msgid "Record Properties" +#~ msgstr "记录属性" + +#~ msgctxt "web/pages/SandboxPropertyDefinitionList" +#~ msgid "Record Properties" +#~ msgstr "记录属性" + +#~ msgctxt "web/route/routes" +#~ msgid "New Property Definition" +#~ msgstr "新建属性定义" + +#~ msgctxt "web/route/routes" +#~ msgid "Property Definition" +#~ msgstr "属性定义" + +#~ msgctxt "web/views/SandboxPropertyDefinitionFormView" +#~ msgid "Container Id" +#~ msgstr "容器 ID" + +#~ msgctxt "web/views/SandboxPropertyDefinitionFormView" +#~ msgid "Container Model" +#~ msgstr "容器模型" + #~ msgctxt "web/views/SandboxPropertyDefinitionFormView" #~ msgid "Container scope" #~ msgstr "容器作用域" +#~ msgctxt "web/views/SandboxPropertyDefinitionFormView" +#~ msgid "Copy Property Definition" +#~ msgstr "复制属性定义" + +#~ msgctxt "web/views/SandboxPropertyDefinitionFormView" +#~ msgid "Create Property Definition" +#~ msgstr "创建属性定义" + +#~ msgctxt "web/views/SandboxPropertyDefinitionFormView" +#~ msgid "Definition" +#~ msgstr "定义" + +#~ msgctxt "web/views/SandboxPropertyDefinitionFormView" +#~ msgid "Delete Property Definition" +#~ msgstr "删除属性定义" + +#~ msgctxt "web/views/SandboxPropertyDefinitionFormView" +#~ msgid "Edit Property Definition" +#~ msgstr "编辑属性定义" + #~ msgctxt "web/views/SandboxPropertyDefinitionFormView" #~ msgid "Leave empty for app-level scope; set to the parent model short name for record-scoped definitions." #~ msgstr "应用级作用域请留空;记录级定义则填写父模型短名。" @@ -988,6 +931,46 @@ msgstr "标签" #~ msgid "Optional parent container for record-scoped definitions. Leave empty for app-level properties (e.g. SandboxRecord.Properties)." #~ msgstr "可选的父级容器,用于记录级定义。应用级属性(如 SandboxRecord.Properties)请留空。" +#~ msgctxt "web/views/SandboxPropertyDefinitionFormView" +#~ msgid "Properties Field" +#~ msgstr "属性字段" + +#~ msgctxt "web/views/SandboxPropertyDefinitionFormView" +#~ msgid "Property Definition" +#~ msgstr "属性定义" + +#~ msgctxt "web/views/SandboxPropertyDefinitionFormView" +#~ msgid "Required" +#~ msgstr "必填" + +#~ msgctxt "web/views/SandboxPropertyDefinitionFormView" +#~ msgid "Scope" +#~ msgstr "作用域" + +#~ msgctxt "web/views/SandboxPropertyDefinitionFormView" +#~ msgid "Target Model" +#~ msgstr "目标模型" + +#~ msgctxt "web/views/SandboxPropertyDefinitionListView" +#~ msgid "Container Id" +#~ msgstr "容器 ID" + +#~ msgctxt "web/views/SandboxPropertyDefinitionListView" +#~ msgid "Container Model" +#~ msgstr "容器模型" + +#~ msgctxt "web/views/SandboxPropertyDefinitionListView" +#~ msgid "Definition" +#~ msgstr "定义" + +#~ msgctxt "web/views/SandboxPropertyDefinitionListView" +#~ msgid "Properties Field" +#~ msgstr "属性字段" + +#~ msgctxt "web/views/SandboxPropertyDefinitionListView" +#~ msgid "Target Model" +#~ msgstr "目标模型" + #~ msgctxt "web/views/SandboxRecordFormView" #~ msgid "Add Line" #~ msgstr "添加明细" diff --git a/modules/sandbox/web/__tests__/lt_titles.test.ts b/modules/sandbox/web/__tests__/lt_titles.test.ts index 9a6a0bbc2..4b1e619e3 100644 --- a/modules/sandbox/web/__tests__/lt_titles.test.ts +++ b/modules/sandbox/web/__tests__/lt_titles.test.ts @@ -2,25 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 import { createTranslate } from '@/core/service/i18n'; -import { - sandboxConfigurationMenuTitle, - sandboxRecordPropertiesMenuTitle, - sandboxRecordsMenuTitle, - sandboxRootMenuTitle, -} from '../menu/titles'; +import { sandboxRecordsMenuTitle, sandboxRootMenuTitle } from '../menu/titles'; import { sandboxRoutes } from '../route/routes'; const listRouteTitle = createTranslate('sandbox', { scope: 'web/route/routes' })._lt('Sandbox Record List'); const recordPropertiesRouteTitle = createTranslate('sandbox', { scope: 'web/route/routes' })._lt( - 'Record Properties', + 'Record Properties' ); test('sandbox menu titles: titles.ts registers shared TermReferences', () => { expect(sandboxRootMenuTitle.src).toBe('Sandbox'); expect(sandboxRootMenuTitle.module).toBe('sandbox'); expect(sandboxRecordsMenuTitle.src).toBe('Field Kitchen Sink'); - expect(sandboxConfigurationMenuTitle.src).toBe('Configuration'); - expect(sandboxRecordPropertiesMenuTitle.src).toBe('Record Properties'); }); test('sandbox route titles: list route pins pageTitleText', () => { @@ -31,7 +24,7 @@ test('sandbox route titles: list route pins pageTitleText', () => { }); test('sandbox route titles: PropertyDefinition list pins pageTitleText', () => { - const route = sandboxRoutes.find(r => r.name === 'SandboxPropertyDefinitionList') as any; + const route = sandboxRoutes.find(r => r.name === 'PropertyDefinitionList_sandbox') as any; expect(route).toBeTruthy(); expect(route.meta?.pageTitle).toBe('Record Properties'); expect(route.meta?.pageTitleText).toEqual(recordPropertiesRouteTitle); diff --git a/modules/sandbox/web/__tests__/resourceWiring.test.ts b/modules/sandbox/web/__tests__/resourceWiring.test.ts index ed6778dbd..c995d789b 100644 --- a/modules/sandbox/web/__tests__/resourceWiring.test.ts +++ b/modules/sandbox/web/__tests__/resourceWiring.test.ts @@ -86,12 +86,14 @@ test('sandbox resource wiring: list/kanban/chart routes expose view switcher met }); }); -test('sandbox resource wiring: PropertyDefinition List→Form routes', () => { +test('sandbox resource wiring: PropertyDefinition list uses shared web host path', () => { const byName = Object.fromEntries(sandboxRoutes.map(route => [String(route.name), route])); - expect(byName.SandboxPropertyDefinitionList?.path).toBe('sandbox/configuration/record-properties'); - expect(byName.SandboxPropertyDefinitionCreate?.path).toBe('sandbox/configuration/record-properties/new'); - expect(byName.SandboxPropertyDefinitionDetail?.path).toBe('sandbox/configuration/record-properties/:id'); - expect(byName.SandboxPropertyDefinitionList?.meta).toMatchObject({ requiresAuth: true }); - expect(byName.SandboxPropertyDefinitionCreate?.meta).toMatchObject({ requiresAuth: true }); - expect(byName.SandboxPropertyDefinitionDetail?.meta).toMatchObject({ requiresAuth: true }); + expect(byName.PropertyDefinitionList_sandbox?.path).toBe('web/property-definitions/sandbox'); + expect(byName.PropertyDefinitionList_sandbox?.meta).toMatchObject({ + requiresAuth: true, + propertyDefinitionApplication: 'sandbox', + }); + expect(byName.SandboxPropertyDefinitionList).toBeUndefined(); + expect(byName.SandboxPropertyDefinitionCreate).toBeUndefined(); + expect(byName.SandboxPropertyDefinitionDetail).toBeUndefined(); }); diff --git a/modules/sandbox/web/menu/menus.ts b/modules/sandbox/web/menu/menus.ts index c6c6ec1d3..a934a6362 100644 --- a/modules/sandbox/web/menu/menus.ts +++ b/modules/sandbox/web/menu/menus.ts @@ -4,18 +4,13 @@ import type { MenuItem } from '@/core/web/menu'; import { defineMenu } from '@/core/web/resource'; import { FlaskConical } from 'lucide-vue-next'; -import { - sandboxConfigurationMenuTitle, - sandboxRecordPropertiesMenuTitle, - sandboxRecordsMenuTitle, - sandboxRootMenuTitle, -} from './titles'; +import { sandboxRecordsMenuTitle, sandboxRootMenuTitle } from './titles'; import { createTranslate } from '@/web/web/i18n'; const { _lt } = createTranslate('sandbox', { scope: 'web/menu/menus' }); -/** Settings → sandbox → Translations when sandbox is installed. */ +/** Settings → sandbox → Translations / Record Properties when sandbox is installed. */ export const sandboxSettingsAppMenu: MenuItem = defineMenu('sandbox.menu.settings_app', { title: _lt('sandbox'), sequence: 110, @@ -28,6 +23,12 @@ export const sandboxSettingsAppMenu: MenuItem = defineMenu('sandbox.menu.setting sequence: 10, defaultRoles: ['sys.admin'], }), + defineMenu('sandbox.menu.property_definitions', { + title: _lt('Record Properties'), + path: '/web/property-definitions/sandbox', + sequence: 20, + defaultRoles: ['sys.admin'], + }), ], }); @@ -43,18 +44,6 @@ export const sandboxMenus: MenuItem[] = [ path: '/sandbox/records', sequence: 10, }), - // Pathless folder: app configuration (properties schema, future settings). - defineMenu('sandbox.menu.configuration', { - title: sandboxConfigurationMenuTitle, - sequence: 90, - children: [ - defineMenu('sandbox.menu.record_properties', { - title: sandboxRecordPropertiesMenuTitle, - path: '/sandbox/configuration/record-properties', - sequence: 10, - }), - ], - }), ], }), ]; diff --git a/modules/sandbox/web/menu/titles.ts b/modules/sandbox/web/menu/titles.ts index af3f4edba..6e4f895fa 100644 --- a/modules/sandbox/web/menu/titles.ts +++ b/modules/sandbox/web/menu/titles.ts @@ -7,5 +7,3 @@ const { _lt } = createTranslate('sandbox', { scope: 'web/menu/titles' }); export const sandboxRootMenuTitle = _lt('Sandbox'); export const sandboxRecordsMenuTitle = _lt('Field Kitchen Sink'); -export const sandboxConfigurationMenuTitle = _lt('Configuration'); -export const sandboxRecordPropertiesMenuTitle = _lt('Record Properties'); diff --git a/modules/sandbox/web/pages/SandboxPropertyDefinition.vue b/modules/sandbox/web/pages/SandboxPropertyDefinition.vue deleted file mode 100644 index e11727f48..000000000 --- a/modules/sandbox/web/pages/SandboxPropertyDefinition.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - - - diff --git a/modules/sandbox/web/pages/SandboxPropertyDefinitionList.vue b/modules/sandbox/web/pages/SandboxPropertyDefinitionList.vue deleted file mode 100644 index 75d742945..000000000 --- a/modules/sandbox/web/pages/SandboxPropertyDefinitionList.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/modules/sandbox/web/pages/page_mount_coverage.test.ts b/modules/sandbox/web/pages/page_mount_coverage.test.ts index 0feb350bb..f783fa81b 100644 --- a/modules/sandbox/web/pages/page_mount_coverage.test.ts +++ b/modules/sandbox/web/pages/page_mount_coverage.test.ts @@ -8,9 +8,7 @@ import SandboxRecordList from './SandboxRecordList.vue'; import SandboxRecordKanban from './SandboxRecordKanban.vue'; import SandboxRecordChart from './SandboxRecordChart.vue'; import SandboxTag from './SandboxTag.vue'; -import SandboxPropertyDefinitionList from './SandboxPropertyDefinitionList.vue'; -import SandboxPropertyDefinition from './SandboxPropertyDefinition.vue'; -import { sandboxMenus } from '../menu/menus'; +import { sandboxMenus, sandboxSettingsAppMenu } from '../menu/menus'; const pages: Array<[string, any, string]> = [ ['SandboxRecordList', SandboxRecordList, '/sandbox/records'], @@ -18,8 +16,6 @@ const pages: Array<[string, any, string]> = [ ['SandboxRecordChart', SandboxRecordChart, '/sandbox/records/chart'], ['SandboxRecord', SandboxRecord, '/sandbox/records/1'], ['SandboxTag', SandboxTag, '/sandbox/tags/new'], - ['SandboxPropertyDefinitionList', SandboxPropertyDefinitionList, '/sandbox/configuration/record-properties'], - ['SandboxPropertyDefinition', SandboxPropertyDefinition, '/sandbox/configuration/record-properties/new'], ]; test('sandbox page mount: every ChoyPage host mounts under choysumMount', async () => { @@ -49,9 +45,15 @@ test('sandbox menus: root declares an icon and leaves stay text-only', () => { expect(sandboxMenus.length).toBeGreaterThan(0); expect(sandboxMenus[0]!.icon).toBeTruthy(); expect(sandboxMenus[0]!.children?.every(child => child.icon == null)).toBe(true); - const configuration = sandboxMenus[0]!.children?.find(child => child.id === 'sandbox.menu.configuration'); - expect(configuration?.path).toBeFalsy(); - expect(configuration?.children?.some(child => child.path === '/sandbox/configuration/record-properties')).toBe( - true, + expect(sandboxMenus[0]!.children?.some(child => child.id === 'sandbox.menu.configuration')).toBe( + false ); }); + +test('sandbox Settings menu links Record Properties to the shared web path', () => { + expect( + sandboxSettingsAppMenu.children?.some( + child => child.path === '/web/property-definitions/sandbox' + ) + ).toBe(true); +}); diff --git a/modules/sandbox/web/route/routes.ts b/modules/sandbox/web/route/routes.ts index 7f6137602..c2ea5f944 100644 --- a/modules/sandbox/web/route/routes.ts +++ b/modules/sandbox/web/route/routes.ts @@ -4,7 +4,7 @@ import type { RouteRecordRaw } from 'vue-router'; import { defineRoute } from '@/core/web/resource'; import { createTranslate } from '@/web/web/i18n'; -import { terminologyListComponent } from '@/web/web/route'; +import { propertyDefinitionListComponent, terminologyListComponent } from '@/web/web/route'; const { _lt } = createTranslate('sandbox', { scope: 'web/route/routes' }); @@ -136,50 +136,6 @@ export const sandboxRoutes: RouteRecordRaw[] = [ ], meta: { requiresAuth: true }, }), - // PropertyDefinition List → Form (Definition edited via ChoyPropertiesDefinitionField). - // Write ACL comes from platform PropertyDefinition logical packs (auth RMA/RFR). - defineRoute('sandbox.route.property_definition_list', { - sequence: 80, - title: _lt('Record Properties'), - path: 'sandbox/configuration/record-properties', - name: 'SandboxPropertyDefinitionList', - component: () => import('../pages/SandboxPropertyDefinitionList.vue'), - actions: [ - 'sandbox.action.property_definition_create', - 'sandbox.action.property_definition_edit', - 'sandbox.action.property_definition_delete', - 'sandbox.action.property_definition_copy', - ], - meta: { requiresAuth: true }, - }), - defineRoute('sandbox.route.property_definition_create', { - sequence: 81, - title: _lt('New Property Definition'), - path: 'sandbox/configuration/record-properties/new', - name: 'SandboxPropertyDefinitionCreate', - component: () => import('../pages/SandboxPropertyDefinition.vue'), - actions: [ - 'sandbox.action.property_definition_create', - 'sandbox.action.property_definition_edit', - 'sandbox.action.property_definition_delete', - 'sandbox.action.property_definition_copy', - ], - meta: { requiresAuth: true }, - }), - defineRoute('sandbox.route.property_definition_detail', { - sequence: 82, - title: _lt('Property Definition'), - path: 'sandbox/configuration/record-properties/:id', - name: 'SandboxPropertyDefinitionDetail', - component: () => import('../pages/SandboxPropertyDefinition.vue'), - actions: [ - 'sandbox.action.property_definition_create', - 'sandbox.action.property_definition_edit', - 'sandbox.action.property_definition_delete', - 'sandbox.action.property_definition_copy', - ], - meta: { requiresAuth: true }, - }), defineRoute('sandbox.route.terminology_list', { sequence: 40, title: _lt('Translations'), @@ -189,4 +145,13 @@ export const sandboxRoutes: RouteRecordRaw[] = [ defaultRoles: ['sys.admin'], meta: { requiresAuth: true, terminologyApplication: 'sandbox' }, }), + defineRoute('sandbox.route.property_definition_list', { + sequence: 50, + title: _lt('Record Properties'), + path: 'web/property-definitions/sandbox', + name: 'PropertyDefinitionList_sandbox', + component: propertyDefinitionListComponent, + defaultRoles: ['sys.admin'], + meta: { requiresAuth: true, propertyDefinitionApplication: 'sandbox' }, + }), ]; diff --git a/modules/sandbox/web/views/sandbox_property_definition_actions.ts b/modules/sandbox/web/views/sandbox_property_definition_actions.ts deleted file mode 100644 index bfa39bbe8..000000000 --- a/modules/sandbox/web/views/sandbox_property_definition_actions.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2026-present Brian Wang -// SPDX-License-Identifier: Apache-2.0 - -import { defineModelActions } from '@/core/web/resource'; -import { createTranslate } from '@/web/web/i18n'; - -const { _lt } = createTranslate('sandbox', { scope: 'web/views/SandboxPropertyDefinitionFormView' }); - -/** Standard CRUD action ids for sandbox.PropertyDefinition (C2-injected logical model). */ -export const sandboxPropertyDefinitionActions = defineModelActions('sandbox.PropertyDefinition', { - entityTitle: _lt('Property Definition'), -}); diff --git a/modules/web/i18n/web.pot b/modules/web/i18n/web.pot index 17210fa8b..2587a550a 100644 --- a/modules/web/i18n/web.pot +++ b/modules/web/i18n/web.pot @@ -848,12 +848,12 @@ msgctxt "web/components/field/ChoyOneToManyKanbanField" msgid "View record" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionField.vue:374 +#: web/components/field/ChoyPropertiesDefinitionField.vue:365 msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "%s properties" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionField.vue:376 +#: web/components/field/ChoyPropertiesDefinitionField.vue:367 msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "%s properties (%s, …)" msgstr "" @@ -863,12 +863,6 @@ msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "Add a line…" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionField.vue:16 -#: web/components/field/ChoyPropertiesDefinitionField.vue:9 -msgctxt "web/components/field/ChoyPropertiesDefinitionField" -msgid "Edit" -msgstr "" - #: web/components/field/ChoyPropertiesDefinitionField.vue:16 #: web/components/field/ChoyPropertiesDefinitionField.vue:9 msgctxt "web/components/field/ChoyPropertiesDefinitionField" @@ -881,7 +875,7 @@ msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "Name" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionField.vue:370 +#: web/components/field/ChoyPropertiesDefinitionField.vue:361 msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "No properties" msgstr "" @@ -904,9 +898,9 @@ msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "Reorder" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionField.vue:394 -#: web/components/field/ChoyPropertiesDefinitionField.vue:474 -#: web/components/field/ChoyPropertiesDefinitionField.vue:478 +#: web/components/field/ChoyPropertiesDefinitionField.vue:385 +#: web/components/field/ChoyPropertiesDefinitionField.vue:472 +#: web/components/field/ChoyPropertiesDefinitionField.vue:476 msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "This row was changed while editing; please reopen it." msgstr "" @@ -917,101 +911,137 @@ msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "Type" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionField.vue:16 -#: web/components/field/ChoyPropertiesDefinitionField.vue:9 -msgctxt "web/components/field/ChoyPropertiesDefinitionField" -msgid "View" +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:79 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Apply" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:59 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:266 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:76 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Cancel" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:41 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:71 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Close" +msgstr "" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:42 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Constraints" +msgstr "" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:163 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Default" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:88 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:265 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Discard" +msgstr "" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:68 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Edit" +msgstr "" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:101 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Edit property: %s" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:128 -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:32 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:162 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:202 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Help" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:118 -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:139 -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:23 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:16 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Identity" +msgstr "" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:161 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:191 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:214 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Label" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:8 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:154 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Name" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:183 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:331 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Name is required" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:88 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:101 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "New property" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:86 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:24 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Presentation" +msgstr "" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:99 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Property" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:86 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:99 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Property: %s" msgstr "" +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:164 #: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:46 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Readonly" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:62 -msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" -msgid "Save" -msgstr "" - -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:49 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:52 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Selection" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:35 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:34 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Translate help" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:26 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:28 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Translate label" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:12 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:157 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Type" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionSelectionEditor.vue:47 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:264 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Unsaved changes" +msgstr "" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:264 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "You have unsaved changes. Discard them?" +msgstr "" + +#: web/components/field/ChoyPropertiesDefinitionSelectionEditor.vue:49 msgctxt "web/components/field/ChoyPropertiesDefinitionSelectionEditor" msgid "Add a line…" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionSelectionEditor.vue:37 +#: web/components/field/ChoyPropertiesDefinitionSelectionEditor.vue:39 msgctxt "web/components/field/ChoyPropertiesDefinitionSelectionEditor" msgid "Delete" msgstr "" @@ -1031,7 +1061,7 @@ msgctxt "web/components/field/ChoyPropertiesDefinitionSelectionEditor" msgid "Reorder" msgstr "" -#: web/components/field/ChoyPropertiesDefinitionSelectionEditor.vue:31 +#: web/components/field/ChoyPropertiesDefinitionSelectionEditor.vue:33 msgctxt "web/components/field/ChoyPropertiesDefinitionSelectionEditor" msgid "Translate option" msgstr "" @@ -1534,7 +1564,7 @@ msgctxt "web/components/terminology/TerminologyExportMenu" msgid "No modules" msgstr "" -#: web/components/terminology/TerminologyExportMenu.vue:123 +#: web/components/terminology/TerminologyExportMenu.vue:131 msgctxt "web/components/terminology/TerminologyExportMenu" msgid "PO download failed" msgstr "" @@ -1692,12 +1722,12 @@ msgctxt "web/components/view/FormView" msgid "Reset" msgstr "" -#: web/components/view/ChoyFormView.vue:300 +#: web/components/view/ChoyFormView.vue:311 msgctxt "web/components/view/FormView" msgid "Save" msgstr "" -#: web/components/view/ChoyFormView.vue:300 +#: web/components/view/ChoyFormView.vue:311 msgctxt "web/components/view/FormView" msgid "Saving..." msgstr "" @@ -2283,22 +2313,22 @@ msgctxt "web/composables/search/useSearch" msgid "Not configured" msgstr "" -#: web/composables/useFormViewLifecycle.ts:97 +#: web/composables/useFormViewLifecycle.ts:102 msgctxt "web/composables/useFormViewLifecycle@confirmDiscardIfDirty" msgid "Cancel" msgstr "" -#: web/composables/useFormViewLifecycle.ts:96 +#: web/composables/useFormViewLifecycle.ts:101 msgctxt "web/composables/useFormViewLifecycle@confirmDiscardIfDirty" msgid "Discard" msgstr "" -#: web/composables/useFormViewLifecycle.ts:95 +#: web/composables/useFormViewLifecycle.ts:100 msgctxt "web/composables/useFormViewLifecycle@confirmDiscardIfDirty" msgid "Unsaved changes" msgstr "" -#: web/composables/useFormViewLifecycle.ts:95 +#: web/composables/useFormViewLifecycle.ts:100 msgctxt "web/composables/useFormViewLifecycle@confirmDiscardIfDirty" msgid "You have unsaved changes. Discard them?" msgstr "" @@ -2552,23 +2582,28 @@ msgctxt "web/import/ImportPanel" msgid "Upload a UTF-8 CSV. Map columns to importable fields from the catalog." msgstr "" -#: web/menu/menus.ts:18 +#: web/menu/menus.ts:50 +msgctxt "web/menu/menus" +msgid "Record Properties" +msgstr "" + +#: web/menu/menus.ts:19 msgctxt "web/menu/menus" msgid "Settings" msgstr "" -#: web/menu/menus.ts:30 -#: web/menu/menus.ts:43 +#: web/menu/menus.ts:31 +#: web/menu/menus.ts:44 msgctxt "web/menu/menus" msgid "Translations" msgstr "" -#: web/menu/menus.ts:25 +#: web/menu/menus.ts:26 msgctxt "web/menu/menus" msgid "core" msgstr "" -#: web/menu/menus.ts:38 +#: web/menu/menus.ts:39 msgctxt "web/menu/menus" msgid "web" msgstr "" @@ -2656,6 +2691,11 @@ msgctxt "web/pages/ErrorView" msgid "You do not have permission to access this page" msgstr "" +#: web/pages/property_definition_list_page.ts:38 +msgctxt "web/pages/PropertyDefinitionList" +msgid "Record Properties: %s" +msgstr "" + #: web/pages/terminology_list_page.ts:36 msgctxt "web/pages/TranslationTermList" msgid "Translations: %s" @@ -2745,6 +2785,21 @@ msgctxt "web/query/utils/filter/operators" msgid "Not in" msgstr "" +#: web/route/propertyDefinitionRoutes.ts:47 +msgctxt "web/route/propertyDefinitionRoutes" +msgid "New Property Definition" +msgstr "" + +#: web/route/propertyDefinitionRoutes.ts:62 +msgctxt "web/route/propertyDefinitionRoutes" +msgid "Property Definition" +msgstr "" + +#: web/route/propertyDefinitionRoutes.ts:31 +msgctxt "web/route/propertyDefinitionRoutes" +msgid "Record Properties" +msgstr "" + #: web/route/terminologyRoutes.ts:43 msgctxt "web/route/terminologyRoutes" msgid "Translation Details" @@ -2766,6 +2821,91 @@ msgctxt "web/stores/breadcrumbStore" msgid "Page" msgstr "" +#: web/views/PropertyDefinitionFormView.vue:21 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Container Id" +msgstr "" + +#: web/views/PropertyDefinitionFormView.vue:18 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Container Model" +msgstr "" + +#: web/views/property_definition_actions.ts:14 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Copy Property Definition" +msgstr "" + +#: web/views/property_definition_actions.ts:14 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Create Property Definition" +msgstr "" + +#: web/views/PropertyDefinitionFormView.vue:26 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Definition" +msgstr "" + +#: web/views/property_definition_actions.ts:14 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Delete Property Definition" +msgstr "" + +#: web/views/property_definition_actions.ts:14 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Edit Property Definition" +msgstr "" + +#: web/views/PropertyDefinitionFormView.vue:15 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Properties Field" +msgstr "" + +#: web/views/property_definition_actions.ts:15 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Property Definition" +msgstr "" + +#: web/views/PropertyDefinitionFormView.vue:21 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Required" +msgstr "" + +#: web/views/PropertyDefinitionFormView.vue:9 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Scope" +msgstr "" + +#: web/views/PropertyDefinitionFormView.vue:12 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Target Model" +msgstr "" + +#: web/views/PropertyDefinitionListView.vue:11 +msgctxt "web/views/PropertyDefinitionListView" +msgid "Container Id" +msgstr "" + +#: web/views/PropertyDefinitionListView.vue:10 +msgctxt "web/views/PropertyDefinitionListView" +msgid "Container Model" +msgstr "" + +#: web/views/PropertyDefinitionListView.vue:12 +msgctxt "web/views/PropertyDefinitionListView" +msgid "Definition" +msgstr "" + +#: web/views/PropertyDefinitionListView.vue:9 +msgctxt "web/views/PropertyDefinitionListView" +msgid "Properties Field" +msgstr "" + +#: web/views/PropertyDefinitionListView.vue:8 +msgctxt "web/views/PropertyDefinitionListView" +msgid "Target Model" +msgstr "" + #: web/views/TranslationTermFormView.vue:35 msgctxt "web/views/TranslationTermFormView" msgid "Edit Translation Term" diff --git a/modules/web/i18n/zh_CN.po b/modules/web/i18n/zh_CN.po index 5373a6116..8dfb8bf11 100644 --- a/modules/web/i18n/zh_CN.po +++ b/modules/web/i18n/zh_CN.po @@ -826,12 +826,12 @@ msgctxt "web/components/field/ChoyOneToManyKanbanField" msgid "View record" msgstr "查看记录" -#: web/components/field/ChoyPropertiesDefinitionField.vue:374 +#: web/components/field/ChoyPropertiesDefinitionField.vue:365 msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "%s properties" msgstr "%s 个属性" -#: web/components/field/ChoyPropertiesDefinitionField.vue:376 +#: web/components/field/ChoyPropertiesDefinitionField.vue:367 msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "%s properties (%s, …)" msgstr "%s 个属性(%s,…)" @@ -841,11 +841,6 @@ msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "Add a line…" msgstr "添加一行…" -#: web/components/field/ChoyPropertiesDefinitionField.vue:16 web/components/field/ChoyPropertiesDefinitionField.vue:9 -msgctxt "web/components/field/ChoyPropertiesDefinitionField" -msgid "Edit" -msgstr "编辑" - #: web/components/field/ChoyPropertiesDefinitionField.vue:16 web/components/field/ChoyPropertiesDefinitionField.vue:9 msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "Label" @@ -856,7 +851,7 @@ msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "Name" msgstr "名称" -#: web/components/field/ChoyPropertiesDefinitionField.vue:370 +#: web/components/field/ChoyPropertiesDefinitionField.vue:361 msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "No properties" msgstr "无属性" @@ -876,7 +871,7 @@ msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "Reorder" msgstr "调整顺序" -#: web/components/field/ChoyPropertiesDefinitionField.vue:394 web/components/field/ChoyPropertiesDefinitionField.vue:474 web/components/field/ChoyPropertiesDefinitionField.vue:478 +#: web/components/field/ChoyPropertiesDefinitionField.vue:385 web/components/field/ChoyPropertiesDefinitionField.vue:472 web/components/field/ChoyPropertiesDefinitionField.vue:476 msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "This row was changed while editing; please reopen it." msgstr "该行在编辑期间已变更,请重新打开。" @@ -886,97 +881,132 @@ msgctxt "web/components/field/ChoyPropertiesDefinitionField" msgid "Type" msgstr "类型" -#: web/components/field/ChoyPropertiesDefinitionField.vue:16 web/components/field/ChoyPropertiesDefinitionField.vue:9 -msgctxt "web/components/field/ChoyPropertiesDefinitionField" -msgid "View" -msgstr "查看" +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:79 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Apply" +msgstr "应用" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:59 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:266 web/components/field/ChoyPropertiesDefinitionItemDialog.vue:76 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Cancel" msgstr "取消" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:41 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:71 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Close" +msgstr "关闭" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:42 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Constraints" +msgstr "约束" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:163 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Default" msgstr "默认值" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:88 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:265 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Discard" +msgstr "放弃" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:68 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Edit" +msgstr "编辑" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:101 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Edit property: %s" msgstr "编辑属性:%s" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:128 web/components/field/ChoyPropertiesDefinitionItemDialog.vue:32 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:162 web/components/field/ChoyPropertiesDefinitionItemDialog.vue:202 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Help" msgstr "帮助" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:118 web/components/field/ChoyPropertiesDefinitionItemDialog.vue:139 web/components/field/ChoyPropertiesDefinitionItemDialog.vue:23 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:16 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Identity" +msgstr "标识" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:161 web/components/field/ChoyPropertiesDefinitionItemDialog.vue:191 web/components/field/ChoyPropertiesDefinitionItemDialog.vue:214 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Label" msgstr "标签" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:8 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:154 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Name" msgstr "名称" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:183 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:331 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Name is required" msgstr "名称为必填项" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:88 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:101 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "New property" msgstr "新建属性" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:86 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:24 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Presentation" +msgstr "展示" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:99 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Property" msgstr "属性" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:86 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:99 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Property: %s" msgstr "属性:%s" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:46 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:164 web/components/field/ChoyPropertiesDefinitionItemDialog.vue:46 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Readonly" msgstr "只读" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:62 -msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" -msgid "Save" -msgstr "保存" - -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:49 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:52 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Selection" msgstr "选项" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:35 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:34 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Translate help" msgstr "翻译帮助" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:26 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:28 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Translate label" msgstr "翻译标签" -#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:12 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:157 msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" msgid "Type" msgstr "类型" -#: web/components/field/ChoyPropertiesDefinitionSelectionEditor.vue:47 +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:264 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "Unsaved changes" +msgstr "未保存的更改" + +#: web/components/field/ChoyPropertiesDefinitionItemDialog.vue:264 +msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +msgid "You have unsaved changes. Discard them?" +msgstr "有未保存的更改。要放弃吗?" + +#: web/components/field/ChoyPropertiesDefinitionSelectionEditor.vue:49 msgctxt "web/components/field/ChoyPropertiesDefinitionSelectionEditor" msgid "Add a line…" msgstr "添加一行…" -#: web/components/field/ChoyPropertiesDefinitionSelectionEditor.vue:37 +#: web/components/field/ChoyPropertiesDefinitionSelectionEditor.vue:39 msgctxt "web/components/field/ChoyPropertiesDefinitionSelectionEditor" msgid "Delete" msgstr "删除" @@ -996,7 +1026,7 @@ msgctxt "web/components/field/ChoyPropertiesDefinitionSelectionEditor" msgid "Reorder" msgstr "调整顺序" -#: web/components/field/ChoyPropertiesDefinitionSelectionEditor.vue:31 +#: web/components/field/ChoyPropertiesDefinitionSelectionEditor.vue:33 msgctxt "web/components/field/ChoyPropertiesDefinitionSelectionEditor" msgid "Translate option" msgstr "翻译选项" @@ -1491,7 +1521,7 @@ msgctxt "web/components/terminology/TerminologyExportMenu" msgid "No modules" msgstr "无模块" -#: web/components/terminology/TerminologyExportMenu.vue:123 +#: web/components/terminology/TerminologyExportMenu.vue:131 msgctxt "web/components/terminology/TerminologyExportMenu" msgid "PO download failed" msgstr "PO 下载失败" @@ -1646,12 +1676,12 @@ msgctxt "web/components/view/FormView" msgid "Reset" msgstr "重置" -#: web/components/view/ChoyFormView.vue:300 +#: web/components/view/ChoyFormView.vue:311 msgctxt "web/components/view/FormView" msgid "Save" msgstr "保存" -#: web/components/view/ChoyFormView.vue:300 +#: web/components/view/ChoyFormView.vue:311 msgctxt "web/components/view/FormView" msgid "Saving..." msgstr "保存中..." @@ -2221,22 +2251,22 @@ msgctxt "web/composables/search/useSearch" msgid "Not configured" msgstr "未配置" -#: web/composables/useFormViewLifecycle.ts:97 +#: web/composables/useFormViewLifecycle.ts:102 msgctxt "web/composables/useFormViewLifecycle@confirmDiscardIfDirty" msgid "Cancel" msgstr "取消" -#: web/composables/useFormViewLifecycle.ts:96 +#: web/composables/useFormViewLifecycle.ts:101 msgctxt "web/composables/useFormViewLifecycle@confirmDiscardIfDirty" msgid "Discard" msgstr "放弃" -#: web/composables/useFormViewLifecycle.ts:95 +#: web/composables/useFormViewLifecycle.ts:100 msgctxt "web/composables/useFormViewLifecycle@confirmDiscardIfDirty" msgid "Unsaved changes" msgstr "未保存的更改" -#: web/composables/useFormViewLifecycle.ts:95 +#: web/composables/useFormViewLifecycle.ts:100 msgctxt "web/composables/useFormViewLifecycle@confirmDiscardIfDirty" msgid "You have unsaved changes. Discard them?" msgstr "有未保存的更改。要放弃吗?" @@ -2486,22 +2516,27 @@ msgctxt "web/import/ImportPanel" msgid "Upload a UTF-8 CSV. Map columns to importable fields from the catalog." msgstr "上传 UTF-8 CSV。将列映射到目录中的可导入字段。" -#: web/menu/menus.ts:18 +#: web/menu/menus.ts:50 +msgctxt "web/menu/menus" +msgid "Record Properties" +msgstr "记录属性" + +#: web/menu/menus.ts:19 msgctxt "web/menu/menus" msgid "Settings" msgstr "设置" -#: web/menu/menus.ts:30 web/menu/menus.ts:43 +#: web/menu/menus.ts:31 web/menu/menus.ts:44 msgctxt "web/menu/menus" msgid "Translations" msgstr "翻译" -#: web/menu/menus.ts:25 +#: web/menu/menus.ts:26 msgctxt "web/menu/menus" msgid "core" msgstr "core" -#: web/menu/menus.ts:38 +#: web/menu/menus.ts:39 msgctxt "web/menu/menus" msgid "web" msgstr "web" @@ -2586,6 +2621,11 @@ msgctxt "web/pages/ErrorView" msgid "You do not have permission to access this page" msgstr "您没有权限访问此页面" +#: web/pages/property_definition_list_page.ts:38 +msgctxt "web/pages/PropertyDefinitionList" +msgid "Record Properties: %s" +msgstr "记录属性:%s" + #: web/pages/terminology_list_page.ts:36 msgctxt "web/pages/TranslationTermList" msgid "Translations: %s" @@ -2661,6 +2701,21 @@ msgctxt "web/query/utils/filter/operators" msgid "Not in" msgstr "不属于" +#: web/route/propertyDefinitionRoutes.ts:47 +msgctxt "web/route/propertyDefinitionRoutes" +msgid "New Property Definition" +msgstr "新建属性定义" + +#: web/route/propertyDefinitionRoutes.ts:62 +msgctxt "web/route/propertyDefinitionRoutes" +msgid "Property Definition" +msgstr "属性定义" + +#: web/route/propertyDefinitionRoutes.ts:31 +msgctxt "web/route/propertyDefinitionRoutes" +msgid "Record Properties" +msgstr "记录属性" + #: web/route/terminologyRoutes.ts:43 msgctxt "web/route/terminologyRoutes" msgid "Translation Details" @@ -2681,6 +2736,91 @@ msgctxt "web/stores/breadcrumbStore" msgid "Page" msgstr "页面" +#: web/views/PropertyDefinitionFormView.vue:21 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Container Id" +msgstr "容器 ID" + +#: web/views/PropertyDefinitionFormView.vue:18 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Container Model" +msgstr "容器模型" + +#: web/views/property_definition_actions.ts:14 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Copy Property Definition" +msgstr "复制属性定义" + +#: web/views/property_definition_actions.ts:14 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Create Property Definition" +msgstr "创建属性定义" + +#: web/views/PropertyDefinitionFormView.vue:26 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Definition" +msgstr "定义" + +#: web/views/property_definition_actions.ts:14 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Delete Property Definition" +msgstr "删除属性定义" + +#: web/views/property_definition_actions.ts:14 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Edit Property Definition" +msgstr "编辑属性定义" + +#: web/views/PropertyDefinitionFormView.vue:15 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Properties Field" +msgstr "属性字段" + +#: web/views/property_definition_actions.ts:15 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Property Definition" +msgstr "属性定义" + +#: web/views/PropertyDefinitionFormView.vue:21 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Required" +msgstr "必填" + +#: web/views/PropertyDefinitionFormView.vue:9 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Scope" +msgstr "作用域" + +#: web/views/PropertyDefinitionFormView.vue:12 +msgctxt "web/views/PropertyDefinitionFormView" +msgid "Target Model" +msgstr "目标模型" + +#: web/views/PropertyDefinitionListView.vue:11 +msgctxt "web/views/PropertyDefinitionListView" +msgid "Container Id" +msgstr "容器 ID" + +#: web/views/PropertyDefinitionListView.vue:10 +msgctxt "web/views/PropertyDefinitionListView" +msgid "Container Model" +msgstr "容器模型" + +#: web/views/PropertyDefinitionListView.vue:12 +msgctxt "web/views/PropertyDefinitionListView" +msgid "Definition" +msgstr "定义" + +#: web/views/PropertyDefinitionListView.vue:9 +msgctxt "web/views/PropertyDefinitionListView" +msgid "Properties Field" +msgstr "属性字段" + +#: web/views/PropertyDefinitionListView.vue:8 +msgctxt "web/views/PropertyDefinitionListView" +msgid "Target Model" +msgstr "目标模型" + #: web/views/TranslationTermFormView.vue:35 msgctxt "web/views/TranslationTermFormView" msgid "Edit Translation Term" @@ -2887,14 +3027,34 @@ msgstr "词条" #~ msgid "Add property" #~ msgstr "添加属性" +#~ msgctxt "web/components/field/ChoyPropertiesDefinitionField" +#~ msgid "Edit" +#~ msgstr "编辑" + #~ msgctxt "web/components/field/ChoyPropertiesDefinitionField" #~ msgid "Translate label" #~ msgstr "翻译标签" +#~ msgctxt "web/components/field/ChoyPropertiesDefinitionField" +#~ msgid "View" +#~ msgstr "查看" + +#~ msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +#~ msgid "No" +#~ msgstr "否" + +#~ msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +#~ msgid "Save" +#~ msgstr "保存" + #~ msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" #~ msgid "Translate" #~ msgstr "翻译" +#~ msgctxt "web/components/field/ChoyPropertiesDefinitionItemDialog" +#~ msgid "Yes" +#~ msgstr "是" + #~ msgctxt "web/components/field/ChoyTextField" #~ msgid "No content" #~ msgstr "无内容" diff --git a/modules/web/web/appSetup.ts b/modules/web/web/appSetup.ts index 89238907e..e2118cf8f 100644 --- a/modules/web/web/appSetup.ts +++ b/modules/web/web/appSetup.ts @@ -10,7 +10,7 @@ import { useI18nStore } from './stores/i18nStore'; import { watch } from 'vue'; import sourceMessages from './i18n/source'; import { createAppRouter } from './router'; -import { registerTerminologyRoutes } from './route'; +import { registerPropertyDefinitionRoutes, registerTerminologyRoutes } from './route'; import { registerGlobalDirectives } from './directives'; import { setGlobalRequestContextProvider } from '@/core/rpc/context'; import { createTerminologyCatalogMerger } from './stores/i18nStore/merge'; @@ -49,6 +49,7 @@ export type SetupAppDeps = { createAppRouter?: typeof createAppRouter; createAppMenu?: typeof createAppMenu; registerTerminologyRoutes?: typeof registerTerminologyRoutes; + registerPropertyDefinitionRoutes?: typeof registerPropertyDefinitionRoutes; applyChoyThemePreference?: typeof applyChoyThemePreference; readChoyThemePreference?: typeof readChoyThemePreference; baseUrl?: string; @@ -79,6 +80,10 @@ export function setupApp(app: ChoysumWebApp, deps: SetupAppDeps = {}): void { const makeRouter = pickDep(deps.createAppRouter, createAppRouter); const makeMenu = pickDep(deps.createAppMenu, createAppMenu); const registerTerminology = pickDep(deps.registerTerminologyRoutes, registerTerminologyRoutes); + const registerPropertyDefinitions = pickDep( + deps.registerPropertyDefinitionRoutes, + registerPropertyDefinitionRoutes + ); const applyTheme = pickDep(deps.applyChoyThemePreference, applyChoyThemePreference); const readTheme = pickDep(deps.readChoyThemePreference, readChoyThemePreference); const baseUrl = pickDep(deps.baseUrl, import.meta.env?.BASE_URL ?? '/'); @@ -178,5 +183,6 @@ export function setupApp(app: ChoysumWebApp, deps: SetupAppDeps = {}): void { const router = makeRouter(baseUrl, i18n.global); registerTerminology(router); + registerPropertyDefinitions(router); app.usePlugin('router', router); } diff --git a/modules/web/web/components/field/ChoyBooleanField.vue b/modules/web/web/components/field/ChoyBooleanField.vue index dcbcc10b1..0eddd8191 100644 --- a/modules/web/web/components/field/ChoyBooleanField.vue +++ b/modules/web/web/components/field/ChoyBooleanField.vue @@ -67,7 +67,18 @@ SPDX-License-Identifier: Apache-2.0 disabled tabindex="-1" /> - {{ displayLabel(fieldValue().value) }} + + {{ checkboxLabel }} + + + {{ displayLabel(fieldValue().value) }} + diff --git a/modules/web/web/components/field/ChoyPropertiesDefinitionField.test.ts b/modules/web/web/components/field/ChoyPropertiesDefinitionField.test.ts index 43ae8ef4c..d307d0d66 100644 --- a/modules/web/web/components/field/ChoyPropertiesDefinitionField.test.ts +++ b/modules/web/web/components/field/ChoyPropertiesDefinitionField.test.ts @@ -123,9 +123,10 @@ function installItemDialogStub() { modelValue: Boolean, item: Object, readonly: Boolean, + canEdit: Boolean, title: String, }, - emits: ['update:modelValue', 'saved', 'closed'], + emits: ['update:modelValue', 'saved', 'closed', 'edit'], setup(props: any, { emit }: any) { return () => props.modelValue @@ -150,6 +151,14 @@ function installItemDialogStub() { }, 'save', ), + h( + 'button', + { + 'data-testid': 'stub-item-edit', + onClick: () => emit('edit'), + }, + 'edit', + ), h( 'button', { @@ -223,6 +232,7 @@ describe('ChoyPropertiesDefinitionField', () => { await flushPromises(); expect(displayMounted.q('[data-testid="choy-properties-definition-add"]')).toBeFalsy(); expect(displayMounted.q('[data-testid="choy-properties-definition-display"]')).toBeTruthy(); + expect(displayMounted.q('[data-testid="choy-properties-definition-remove"]')).toBeFalsy(); } finally { displayMounted.unmount(); } @@ -254,6 +264,30 @@ describe('ChoyPropertiesDefinitionField', () => { } }); + test('Remove keydown Enter does not bubble to open the row dialog', async () => { + const binding = makeBinding({ + definition: [{ name: 'a', type: 'char', string: { en_US: 'A' } }], + isEditMode: true, + }); + const mounted = mountApp(ChoyPropertiesDefinitionField as any, { + props: { binding }, + }); + try { + await flushPromises(); + const removeBtn = mounted.q( + '[data-testid="choy-properties-definition-remove"]', + ) as HTMLButtonElement; + expect(removeBtn).toBeTruthy(); + const enter = new Event('keydown', { bubbles: true, cancelable: true }); + Object.defineProperty(enter, 'key', { value: 'Enter' }); + removeBtn.dispatchEvent(enter); + await nextTick(); + expect(mounted.q('[data-testid="choy-properties-definition-item-dialog-stub"]')).toBeFalsy(); + } finally { + mounted.unmount(); + } + }); + test('shows 1-based index column for definition rows', async () => { const binding = makeBinding({ definition: [ @@ -416,9 +450,9 @@ describe('ChoyPropertiesDefinitionField', () => { }); try { await flushPromises(); - const openBtns = mounted.el.querySelectorAll('[data-testid="choy-properties-definition-open"]'); - expect(openBtns.length).toBe(2); - (openBtns[1] as HTMLElement).click(); + const rows = mounted.el.querySelectorAll('[data-testid="choy-properties-definition-row"]'); + expect(rows.length).toBe(2); + (rows[1] as HTMLElement).click(); await nextTick(); expect(mounted.q('[data-testid="choy-properties-definition-item-dialog-stub"]')).toBeTruthy(); // Remove the first row so dialogIndex 1 is now out of range. @@ -453,8 +487,8 @@ describe('ChoyPropertiesDefinitionField', () => { }); try { await flushPromises(); - const openBtns = mounted.el.querySelectorAll('[data-testid="choy-properties-definition-open"]'); - (openBtns[0] as HTMLElement).click(); + const rows = mounted.el.querySelectorAll('[data-testid="choy-properties-definition-row"]'); + (rows[0] as HTMLElement).click(); await nextTick(); expect(mounted.q('[data-testid="choy-properties-definition-item-dialog-stub"]')).toBeTruthy(); const state = mounted.setupState() as { @@ -492,8 +526,8 @@ describe('ChoyPropertiesDefinitionField', () => { }); try { await flushPromises(); - const openBtns = mounted.el.querySelectorAll('[data-testid="choy-properties-definition-open"]'); - (openBtns[0] as HTMLElement).click(); + const rows = mounted.el.querySelectorAll('[data-testid="choy-properties-definition-row"]'); + (rows[0] as HTMLElement).click(); await nextTick(); expect(mounted.q('[data-testid="choy-properties-definition-item-dialog-stub"]')).toBeTruthy(); binding.__value.value = [ @@ -583,7 +617,49 @@ describe('ChoyPropertiesDefinitionField', () => { } }); - test('openRow readonly in display mode opens dialog with View', async () => { + test('onDialogEdit promotes when canMutate; no-ops when display-only', async () => { + const binding = makeBinding({ + definition: [{ name: 'tax_id', type: 'char', string: { en_US: 'Tax' } }], + isEditMode: true, + }); + const mounted = mountApp(ChoyPropertiesDefinitionField as any, { + props: { binding }, + }); + try { + await flushPromises(); + mounted.click('[data-testid="choy-properties-definition-row"]'); + await nextTick(); + expect(mounted.q('[data-testid="choy-properties-definition-item-dialog-stub"]')).toBeTruthy(); + mounted.click('[data-testid="stub-item-edit"]'); + await nextTick(); + expect(mounted.q('[data-testid="choy-properties-definition-item-dialog-stub"]')).toBeTruthy(); + } finally { + mounted.unmount(); + } + + // Early return: canMutate is false in display mode. + const displayBinding = makeBinding({ + definition: [{ name: 'tax_id', type: 'char', string: { en_US: 'Tax' } }], + isEditMode: false, + }); + const displayMounted = mountApp(ChoyPropertiesDefinitionField as any, { + props: { binding: displayBinding }, + }); + try { + await flushPromises(); + displayMounted.click('[data-testid="choy-properties-definition-row"]'); + await nextTick(); + displayMounted.click('[data-testid="stub-item-edit"]'); + await nextTick(); + expect( + displayMounted.q('[data-testid="choy-properties-definition-item-dialog-stub"]'), + ).toBeTruthy(); + } finally { + displayMounted.unmount(); + } + }); + + test('openRow readonly in display mode has no View column and opens dialog via row click', async () => { const binding = makeBinding({ definition: [{ name: 'tax_id', type: 'char', string: { en_US: 'Tax' } }], isEditMode: false, @@ -613,6 +689,7 @@ describe('ChoyPropertiesDefinitionField', () => { }); try { await flushPromises(); + expect(mounted.q('[data-testid="choy-properties-definition-remove"]')).toBeFalsy(); // Row click opens as readonly when canMutate is false (display mode). mounted.click('[data-testid="choy-properties-definition-row"]'); await nextTick(); @@ -648,7 +725,7 @@ describe('ChoyPropertiesDefinitionField', () => { await flushPromises(); expect(mounted.el.textContent || '').toMatch(/prio/); expect(mounted.el.textContent || '').toMatch(/selection/); - mounted.click('[data-testid="choy-properties-definition-open"]'); + mounted.click('[data-testid="choy-properties-definition-row"]'); await nextTick(); expect(mounted.q('[data-testid="choy-properties-definition-item-dialog-stub"]')).toBeTruthy(); @@ -760,7 +837,7 @@ describe('ChoyPropertiesDefinitionField', () => { const mounted = mountApp(ChoyPropertiesDefinitionField as any, { props: { binding } }); try { await flushPromises(); - mounted.click('[data-testid="choy-properties-definition-open"]'); + mounted.click('[data-testid="choy-properties-definition-row"]'); await nextTick(); mounted.click('[data-testid="stub-item-save"]'); await nextTick(); @@ -783,7 +860,7 @@ describe('ChoyPropertiesDefinitionField', () => { const state = mounted.setupState() as { writeError: string }; state.writeError = 'stale'; expect(state.writeError).toBe('stale'); - mounted.click('[data-testid="choy-properties-definition-open"]'); + mounted.click('[data-testid="choy-properties-definition-row"]'); await nextTick(); expect(state.writeError).toBe(''); } finally { diff --git a/modules/web/web/components/field/ChoyPropertiesDefinitionField.vue b/modules/web/web/components/field/ChoyPropertiesDefinitionField.vue index e44937406..f32421a93 100644 --- a/modules/web/web/components/field/ChoyPropertiesDefinitionField.vue +++ b/modules/web/web/components/field/ChoyPropertiesDefinitionField.vue @@ -52,8 +52,6 @@ SPDX-License-Identifier: Apache-2.0 :name-header="_t('Name')" :type-header="_t('Type')" :label-header="_t('Label')" - :edit-text="_t('Edit')" - :view-text="_t('View')" :remove-text="_t('Remove')" :reorder-text="_t('Reorder')" :index-header="'#'" @@ -80,8 +78,6 @@ SPDX-License-Identifier: Apache-2.0 :name-header="_t('Name')" :type-header="_t('Type')" :label-header="_t('Label')" - :edit-text="_t('Edit')" - :view-text="_t('View')" :remove-text="_t('Remove')" :reorder-text="_t('Reorder')" :index-header="'#'" @@ -97,8 +93,10 @@ SPDX-License-Identifier: Apache-2.0 v-model="dialogOpen" :item="dialogItem" :readonly="dialogReadonly" + :can-edit="canMutate" :commit-error="writeError" @saved="onDialogSaved" + @edit="onDialogEdit" /> @@ -110,7 +108,7 @@ import type { WebModelStore } from '@/web/web/stores/modelStore'; import type { RuleItem } from 'async-validator'; import { computed, defineComponent, h, nextTick, ref, watch, type WritableComputedRef } from 'vue'; import { getActivePinia } from 'pinia'; -import { GripVertical } from 'lucide-vue-next'; +import { GripVertical, Trash2 } from 'lucide-vue-next'; import { useField } from '@/web/web/composables/useField'; import type { UseField } from '@/web/web/composables/useField'; import { @@ -144,8 +142,6 @@ const DefinitionLinesTable = defineComponent({ nameHeader: { type: String, required: true }, typeHeader: { type: String, required: true }, labelHeader: { type: String, required: true }, - editText: { type: String, required: true }, - viewText: { type: String, required: true }, removeText: { type: String, required: true }, reorderText: { type: String, default: 'Reorder' }, /** Index column header (O2M-style "#"). */ @@ -183,11 +179,12 @@ const DefinitionLinesTable = defineComponent({ h('th', { class: 'whitespace-nowrap px-3 py-1.5 font-medium' }, props.nameHeader), h('th', { class: 'whitespace-nowrap px-3 py-1.5 font-medium' }, props.typeHeader), h('th', { class: 'min-w-[10rem] px-3 py-1.5 font-medium' }, props.labelHeader), - h('th', { class: 'whitespace-nowrap px-3 py-1.5 font-medium' }), + // Actions (Edit/Remove) only in edit mode; display mode has no View column. + props.canMutate ? h('th', { class: 'whitespace-nowrap px-3 py-1.5 font-medium' }) : null, ]), ]); - const colCount = props.canMutate ? 6 : 5; + const colCount = props.canMutate ? 6 : 4; const dragging = handleReorder.draggingIndex.value; const over = handleReorder.overIndex.value; @@ -300,42 +297,34 @@ const DefinitionLinesTable = defineComponent({ }, item.string || '—', ), - h( - 'td', - { - class: 'whitespace-nowrap px-3 py-1.5', - onClick: (e: Event) => e.stopPropagation(), - }, - [ - h('div', { class: 'flex items-center gap-0.5' }, [ - h( - ChoyButton, - { - size: 'sm', - variant: 'ghost', - class: 'h-7 px-2', - 'data-testid': 'choy-properties-definition-open', - onClick: () => emit('open', index, !props.canMutate), - }, - () => (props.canMutate ? props.editText : props.viewText), - ), - props.canMutate - ? h( - ChoyButton, - { - size: 'sm', - variant: 'ghost', - class: - 'h-7 px-2 text-destructive hover:bg-destructive/10 hover:text-destructive', - 'data-testid': 'choy-properties-definition-remove', - onClick: () => emit('remove', index), - }, - () => props.removeText, - ) - : null, - ]), - ], - ), + props.canMutate + ? h( + 'td', + { + class: 'w-11 whitespace-nowrap px-1 py-1.5 text-center', + onClick: (e: Event) => e.stopPropagation(), + // Stop keydown so Enter/Space on Remove does not bubble to the + // row role=button handler (which would open the dialog instead). + onKeydown: (e: KeyboardEvent) => e.stopPropagation(), + }, + [ + h( + ChoyButton, + { + type: 'button', + size: 'icon', + variant: 'ghost', + class: 'size-7 text-muted-foreground hover:text-destructive', + 'aria-label': props.removeText, + title: props.removeText, + 'data-testid': 'choy-properties-definition-remove', + onClick: () => emit('remove', index), + }, + () => h(Trash2, { class: 'size-3.5', 'aria-hidden': 'true' }), + ), + ], + ) + : null, ], ), ); @@ -566,6 +555,13 @@ function openRow(index: number, asReadonly: boolean) { dialogOpen.value = true; } +/** Promote a readonly preview to edit when the parent form can mutate. */ +function onDialogEdit() { + if (!canMutate.value || !dialogOpen.value) return; + writeError.value = ''; + dialogReadonly.value = false; +} + function onDialogSaved(item: DefinitionEditorDraftItem) { if (dialogReadonly.value) return; const next = [...drafts.value]; diff --git a/modules/web/web/components/field/ChoyPropertiesDefinitionItemDialog.test.ts b/modules/web/web/components/field/ChoyPropertiesDefinitionItemDialog.test.ts index f63862f94..73d0c0f32 100644 --- a/modules/web/web/components/field/ChoyPropertiesDefinitionItemDialog.test.ts +++ b/modules/web/web/components/field/ChoyPropertiesDefinitionItemDialog.test.ts @@ -1,25 +1,24 @@ // SPDX-FileCopyrightText: 2026-present Brian Wang // SPDX-License-Identifier: Apache-2.0 -import { h } from 'vue'; +import { h, inject, provide, reactive } from 'vue'; import { createPinia, setActivePinia } from 'pinia'; +import { resolveConfirmChoy } from '@/web/web/composables/confirmChoyAction'; import { useI18nStore } from '@/web/web/stores/i18nStore'; import { flushPromises, fnRecorder, mountApp, restoreSfc, stubSfc } from '@/web/web/__tests__/mountApp'; import { ChoyDialog, ChoyDialogContent, ChoyDialogTitle } from '@/web/web/components/layout/choyDialog'; import ChoyButton from '@/web/web/components/layout/ChoyButton.vue'; -import Checkbox from '../vendor/ui/checkbox/Checkbox.vue'; -import FieldLabel from '../vendor/ui/field/FieldLabel.vue'; -import Input from '../vendor/ui/input/Input.vue'; -import Textarea from '../vendor/ui/textarea/Textarea.vue'; -import Select from '../vendor/ui/select/Select.vue'; -import SelectContent from '../vendor/ui/select/SelectContent.vue'; -import SelectItem from '../vendor/ui/select/SelectItem.vue'; -import SelectTrigger from '../vendor/ui/select/SelectTrigger.vue'; +import ChoyFormView from '@/web/web/components/view/ChoyFormView.vue'; +import ChoyVarcharField from '@/web/web/components/field/ChoyVarcharField.vue'; +import ChoyTextField from '@/web/web/components/field/ChoyTextField.vue'; +import ChoyBooleanField from '@/web/web/components/field/ChoyBooleanField.vue'; +import ChoySelectionField from '@/web/web/components/field/ChoySelectionField.vue'; import ChoyPropertyLabelTranslationsDialog from './ChoyPropertyLabelTranslationsDialog.vue'; import ChoyPropertiesDefinitionSelectionEditor from './ChoyPropertiesDefinitionSelectionEditor.vue'; import ItemDialog from './ChoyPropertiesDefinitionItemDialog.vue'; import type { DefinitionEditorDraftItem } from './propertiesDefinitionHelpers'; +import type { LocalFormStore } from '@/web/web/stores/localFormStore'; function draft(partial: Partial = {}): DefinitionEditorDraftItem { return { @@ -40,10 +39,7 @@ function draft(partial: Partial = {}): DefinitionEdit describe('ChoyPropertiesDefinitionItemDialog', () => { let pinia: ReturnType; let lastTranslateProps: Record | null = null; - let translateSavedEmit: ((next: Record) => void) | null = null; let lastSelectionProps: Record | null = null; - let selectionTranslateEmit: ((value: string) => void) | null = null; - let selectionUpdateEmit: ((rows: unknown[]) => void) | null = null; function installStubs() { stubSfc(ChoyDialog as any, { @@ -92,104 +88,123 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { ); }, }); - stubSfc(Input as any, { - name: 'Input', - props: { modelValue: { type: [String, Number], default: '' }, disabled: Boolean, id: String }, - emits: ['update:modelValue'], - setup(props: any, { emit, attrs }: any) { - return () => - h('input', { - ...attrs, - id: props.id, - disabled: props.disabled || undefined, - value: props.modelValue ?? '', - onInput: (e: Event) => emit('update:modelValue', (e.target as HTMLInputElement).value), - }); - }, - }); - stubSfc(Textarea as any, { - name: 'Textarea', - props: { modelValue: { type: String, default: '' }, disabled: Boolean, id: String }, - emits: ['update:modelValue'], - setup(props: any, { emit, attrs }: any) { - return () => - h('textarea', { - ...attrs, - id: props.id, - disabled: props.disabled || undefined, - value: props.modelValue ?? '', - onInput: (e: Event) => emit('update:modelValue', (e.target as HTMLTextAreaElement).value), - }); - }, - }); - stubSfc(FieldLabel as any, { - name: 'FieldLabel', - setup(_: any, { slots }: any) { - return () => h('label', slots.default?.()); - }, - }); - stubSfc(Checkbox as any, { - name: 'Checkbox', - props: { modelValue: { type: [Boolean, String, Number], default: false }, disabled: Boolean }, - emits: ['update:modelValue'], - setup(props: any, { emit, attrs }: any) { - return () => - h('input', { - ...attrs, - type: 'checkbox', - disabled: props.disabled || undefined, - checked: props.modelValue === true, - onChange: (e: Event) => emit('update:modelValue', (e.target as HTMLInputElement).checked), - }); + + stubSfc(ChoyFormView as any, { + name: 'ChoyFormView', + props: { + store: Object, + initialValues: Object, + viewMode: String, + embedded: Boolean, + showHeader: Boolean, + showActions: Boolean, + submitHandler: Function, + cancelHandler: Function, }, - }); - stubSfc(Select as any, { - name: 'Select', - props: { modelValue: { type: String, default: '' }, disabled: Boolean }, - emits: ['update:modelValue'], - setup(props: any, { slots, emit }: any) { + setup(props: any, { slots, attrs, expose }: any) { + const formData = reactive({ ...(props.initialValues || {}) }); + const syncStoreField = (path: string, value: unknown) => { + (formData as any)[path] = value; + const store = props.store as LocalFormStore | null | undefined; + store?.setField?.(path, value); + }; + provide('form-root', { + get mode() { + return props.viewMode === 'display' ? 'display' : 'create'; + }, + get draft() { + return formData; + }, + setField(path: string, value: unknown) { + syncStoreField(path, value); + }, + getField(path: string) { + return (formData as any)[path]; + }, + onChange() {}, + }); + expose({ + submit: () => + props.submitHandler?.({ + formData, + mode: 'create', + data: formData, + defaultSubmit: async () => null, + }), + // Mirror FormView.cancel: dirty confirm is skipped in this stub; host + // cancelHandler closes the dialog (same post-confirm contract). + cancel: async () => { + await props.cancelHandler?.(); + }, + }); return () => - h('div', { class: 'select-stub', 'data-value': props.modelValue }, [ - slots.default?.(), - h( - 'button', - { - type: 'button', - 'data-testid': 'stub-set-selection-type', - disabled: props.disabled || undefined, - onClick: () => emit('update:modelValue', 'selection'), - }, - 'selection', - ), + h('div', { ...attrs, 'data-testid': 'choy-properties-definition-item-form' }, [ + slots.default?.({ formData, viewMode: props.viewMode }), ]); }, }); - stubSfc(SelectTrigger as any, { - name: 'SelectTrigger', - setup(_: any, { attrs }: any) { - return () => h('button', { ...attrs, type: 'button' }, 'type'); - }, - }); - stubSfc(SelectContent as any, { - name: 'SelectContent', - setup(_: any, { slots }: any) { - return () => h('div', slots.default?.()); - }, - }); - stubSfc(SelectItem as any, { - name: 'SelectItem', - props: { value: String }, - setup(props: any, { slots }: any) { - return () => h('div', { 'data-value': props.value }, slots.default?.()); - }, - }); + + function fieldStub(name: string, tag: 'input' | 'textarea' | 'checkbox' | 'select') { + return { + name, + props: { + store: Object, + prop: String, + readonly: { type: Boolean, default: false }, + }, + setup(props: any, { attrs }: any) { + const form = inject('form-root'); + const testId = attrs['data-testid'] || `field-${props.prop}`; + return () => { + const mode = form?.mode; + if (mode === 'display' || props.readonly) { + const v = form?.getField?.(props.prop); + const text = v === true ? 'Yes' : v === false ? 'No' : String(v ?? ''); + return h('div', { 'data-testid': testId, 'data-readonly': '1' }, text); + } + if (tag === 'checkbox') { + return h('input', { + type: 'checkbox', + 'data-testid': testId, + checked: form?.getField?.(props.prop) === true, + onChange: (e: Event) => form?.setField?.(props.prop, (e.target as HTMLInputElement).checked), + }); + } + if (tag === 'select') { + return h('div', { 'data-testid': testId }, [ + h( + 'button', + { + type: 'button', + 'data-testid': 'stub-set-selection-type', + onClick: () => form?.setField?.(props.prop, 'selection'), + }, + String(form?.getField?.(props.prop) ?? ''), + ), + ]); + } + return h(tag, { + 'data-testid': testId, + value: String(form?.getField?.(props.prop) ?? ''), + onInput: (e: Event) => + form?.setField?.(props.prop, (e.target as HTMLInputElement | HTMLTextAreaElement).value), + }); + }; + }, + }; + } + + stubSfc(ChoyVarcharField as any, fieldStub('ChoyVarcharField', 'input') as any); + stubSfc(ChoyTextField as any, fieldStub('ChoyTextField', 'textarea') as any); + stubSfc(ChoyBooleanField as any, fieldStub('ChoyBooleanField', 'checkbox') as any); + stubSfc(ChoySelectionField as any, fieldStub('ChoySelectionField', 'select') as any); + stubSfc(ChoyPropertyLabelTranslationsDialog as any, { name: 'ChoyPropertyLabelTranslationsDialog', props: { modelValue: Boolean, labelTitle: String, translations: Object }, emits: ['update:modelValue', 'saved', 'closed'], setup(props: any, { emit }: any) { lastTranslateProps = props; - translateSavedEmit = (next: Record) => emit('saved', next); return () => props.modelValue ? h('div', { 'data-testid': 'translate-dialog-stub' }, [ @@ -230,8 +245,6 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { emits: ['update:modelValue', 'translate'], setup(props: any, { emit }: any) { lastSelectionProps = props; - selectionTranslateEmit = (value: string) => emit('translate', value); - selectionUpdateEmit = (rows: unknown[]) => emit('update:modelValue', rows); return () => h('div', { 'data-testid': 'selection-editor-stub' }, [ h( @@ -266,10 +279,7 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { setActivePinia(pinia); void useI18nStore(); lastTranslateProps = null; - translateSavedEmit = null; lastSelectionProps = null; - selectionTranslateEmit = null; - selectionUpdateEmit = null; installStubs(); }); @@ -278,19 +288,20 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { restoreSfc(ChoyDialogContent as any); restoreSfc(ChoyDialogTitle as any); restoreSfc(ChoyButton as any); - restoreSfc(Input as any); - restoreSfc(Textarea as any); - restoreSfc(FieldLabel as any); - restoreSfc(Checkbox as any); - restoreSfc(Select as any); - restoreSfc(SelectTrigger as any); - restoreSfc(SelectContent as any); - restoreSfc(SelectItem as any); + restoreSfc(ChoyFormView as any); + restoreSfc(ChoyVarcharField as any); + restoreSfc(ChoyTextField as any); + restoreSfc(ChoyBooleanField as any); + restoreSfc(ChoySelectionField as any); restoreSfc(ChoyPropertyLabelTranslationsDialog as any); restoreSfc(ChoyPropertiesDefinitionSelectionEditor as any); }); - function mountDialog(props: Record = {}, on?: Record void>) { + function mountDialog( + props: Record = {}, + on?: Record void>, + opts: { reactiveProps?: boolean } = {}, + ) { return mountApp(ItemDialog as any, { props: { modelValue: true, @@ -299,6 +310,7 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { }, on, plugins: [pinia], + reactiveProps: opts.reactiveProps === true, }); } @@ -311,6 +323,8 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { const edit = mountDialog({ title: '' }); await flushPromises(); expect(edit.el.querySelector('[data-testid="item-dialog-title"]')?.textContent || '').toMatch(/tax_id/); + expect(edit.q('[data-testid="choy-properties-definition-item-save"]')).toBeTruthy(); + expect(edit.q('[data-testid="choy-properties-definition-item-footer"]')).toBeTruthy(); edit.unmount(); const neu = mountDialog({ title: '', item: draft() }); @@ -322,12 +336,14 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { await flushPromises(); expect(ro.el.querySelector('[data-testid="item-dialog-title"]')?.textContent || '').toMatch(/Property/); expect(ro.q('[data-testid="choy-properties-definition-item-save"]')).toBeFalsy(); + expect(ro.q('[data-testid="choy-properties-definition-item-edit"]')).toBeFalsy(); + expect(ro.q('[data-testid="choy-properties-definition-item-cancel"]')).toBeTruthy(); ro.unmount(); - const roEmpty = mountDialog({ title: '', readonly: true, item: draft() }); + const roEdit = mountDialog({ title: '', readonly: true, canEdit: true }); await flushPromises(); - expect(roEmpty.el.querySelector('[data-testid="item-dialog-title"]')?.textContent || '').toMatch(/Property/); - roEmpty.unmount(); + expect(roEdit.q('[data-testid="choy-properties-definition-item-edit"]')).toBeTruthy(); + roEdit.unmount(); }); test('Name required validation shows formError', async () => { @@ -362,7 +378,7 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { } }); - test('Save emits saved and does not close itself', async () => { + test('Apply emits saved via FormView submit', async () => { const onSaved = fnRecorder(); const onUpdate = fnRecorder(); const m = mountDialog({}, { onSaved, 'onUpdate:modelValue': onUpdate }); @@ -372,7 +388,6 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { expect(onSaved.calls.length).toBe(1); expect((onSaved.calls[0][0] as DefinitionEditorDraftItem).name).toBe('tax_id'); expect(onUpdate.calls.some((c: any[]) => c[0] === false)).toBe(false); - expect(m.q('[data-testid="choy-properties-definition-item-dialog"]')).toBeTruthy(); m.unmount(); }); @@ -393,59 +408,362 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { } }); - test('Cancel closes dialog', async () => { + test('Cancel footer closes dialog', async () => { const onUpdate = fnRecorder(); - const onClosed = fnRecorder(); - const m = mountApp(ItemDialog as any, { - reactiveProps: true, + const m = mountDialog({}, { 'onUpdate:modelValue': onUpdate }); + await flushPromises(); + m.click('[data-testid="choy-properties-definition-item-cancel"]'); + await flushPromises(); + expect(onUpdate.calls.some((c: any[]) => c[0] === false)).toBe(true); + m.unmount(); + }); + + test('Cancel with selection-only edits confirms before close', async () => { + const onUpdate = fnRecorder(); + const m = mountDialog( + { + item: draft({ + name: 'prio', + type: 'selection', + string: 'P', + stringMap: { en_US: 'P' }, + selectionText: '[["low","Low"]]', + selectionLabelMaps: { low: { en_US: 'Low' } }, + }), + }, + { 'onUpdate:modelValue': onUpdate }, + ); + try { + await flushPromises(); + m.click('[data-testid="stub-selection-update"]'); + await flushPromises(); + queueMicrotask(() => resolveConfirmChoy('confirm')); + m.click('[data-testid="choy-properties-definition-item-cancel"]'); + await flushPromises(); + expect(onUpdate.calls.some((c: any[]) => c[0] === false)).toBe(true); + } finally { + m.unmount(); + } + }); + + test('Cancel with selection edits declines keeps dialog open', async () => { + const onUpdate = fnRecorder(); + const m = mountDialog( + { + item: draft({ + name: 'prio', + type: 'selection', + string: 'P', + stringMap: { en_US: 'P' }, + selectionText: '[["low","Low"]]', + selectionLabelMaps: { low: { en_US: 'Low' } }, + }), + }, + { 'onUpdate:modelValue': onUpdate }, + ); + try { + await flushPromises(); + m.click('[data-testid="stub-selection-update"]'); + await flushPromises(); + queueMicrotask(() => resolveConfirmChoy('cancel')); + m.click('[data-testid="choy-properties-definition-item-cancel"]'); + await flushPromises(); + expect(onUpdate.calls.some((c: any[]) => c[0] === false)).toBe(false); + expect(m.q('[data-testid="choy-properties-definition-item-form"]')).toBeTruthy(); + } finally { + m.unmount(); + } + }); + + test('Cancel without FormView.cancel closes directly when clean', async () => { + stubSfc(ChoyFormView as any, { + name: 'ChoyFormView', props: { - modelValue: true, - item: draft({ name: 'tax_id', string: 'Tax', stringMap: { en_US: 'Tax' } }), - 'onUpdate:modelValue': (v: boolean) => { - (m.props as any).modelValue = v; - }, - onClosed, + store: Object, + initialValues: Object, + viewMode: String, + submitHandler: Function, + cancelHandler: Function, + }, + setup(props: any, { slots, attrs, expose }: any) { + const formData = reactive({ ...(props.initialValues || {}) }); + provide('form-root', { + get mode() { + return 'create'; + }, + get draft() { + return formData; + }, + setField(path: string, value: unknown) { + (formData as any)[path] = value; + (props.store as LocalFormStore | undefined)?.setField?.(path, value); + }, + getField(path: string) { + return (formData as any)[path]; + }, + onChange() {}, + }); + expose({}); + return () => + h('div', { ...attrs, 'data-testid': 'choy-properties-definition-item-form' }, [ + slots.default?.({ formData, viewMode: props.viewMode }), + ]); }, - plugins: [pinia], }); + const onUpdate = fnRecorder(); + const m = mountDialog({}, { 'onUpdate:modelValue': onUpdate }); + try { + await flushPromises(); + m.click('[data-testid="choy-properties-definition-item-cancel"]'); + await flushPromises(); + expect(onUpdate.calls.some((c: any[]) => c[0] === false)).toBe(true); + } finally { + m.unmount(); + installStubs(); + } + }); + + test('readonly Close closes without requiring FormView.cancel', async () => { + const onUpdate = fnRecorder(); + const m = mountDialog({ readonly: true }, { 'onUpdate:modelValue': onUpdate }); await flushPromises(); m.click('[data-testid="choy-properties-definition-item-cancel"]'); await flushPromises(); - expect((m.props as any).modelValue).toBe(false); - expect(onClosed.calls.length).toBeGreaterThanOrEqual(1); - // onUpdate may be wired via props callback rather than `on` map - void onUpdate; + expect(onUpdate.calls.some((c: any[]) => c[0] === false)).toBe(true); m.unmount(); }); - test('translate label and help apply maps via onTranslateSaved', async () => { - const m = mountDialog({ - item: draft({ - name: 'tax_id', - string: 'Tax', - stringMap: { en_US: 'Tax' }, - help: 'Help EN', - helpMap: { en_US: 'Help EN' }, - }), + test('onApply/onRequestEdit early-return guards via setupState', async () => { + const onEdit = fnRecorder(); + const m = mountDialog({ readonly: true, canEdit: false }, { onEdit }); + try { + await flushPromises(); + const api = m.setupState(); + await api.onApply(); + api.onRequestEdit(); + expect(onEdit.calls.length).toBe(0); + expect(m.q('[data-testid="choy-properties-definition-item-error"]')).toBeFalsy(); + } finally { + m.unmount(); + } + }); + + test('onApply surfaces thrown submit errors as formError', async () => { + stubSfc(ChoyFormView as any, { + name: 'ChoyFormView', + props: { + store: Object, + initialValues: Object, + viewMode: String, + submitHandler: Function, + cancelHandler: Function, + }, + setup(props: any, { slots, attrs, expose }: any) { + const formData = reactive({ ...(props.initialValues || {}) }); + provide('form-root', { + get mode() { + return 'create'; + }, + get draft() { + return formData; + }, + setField() {}, + getField(path: string) { + return (formData as any)[path]; + }, + onChange() {}, + }); + expose({ + submit: async () => { + throw new Error('submit boom'); + }, + }); + return () => + h('div', { ...attrs, 'data-testid': 'choy-properties-definition-item-form' }, [ + slots.default?.({ formData, viewMode: props.viewMode }), + ]); + }, }); + const m = mountDialog({}); + try { + await flushPromises(); + m.click('[data-testid="choy-properties-definition-item-save"]'); + await flushPromises(); + expect(m.q('[data-testid="choy-properties-definition-item-error"]')?.textContent || '').toMatch( + /submit boom/, + ); + } finally { + m.unmount(); + installStubs(); + } + }); + + test('Apply falls back to onFormSubmit when FormView has no submit()', async () => { + stubSfc(ChoyFormView as any, { + name: 'ChoyFormView', + props: { + store: Object, + initialValues: Object, + viewMode: String, + submitHandler: Function, + cancelHandler: Function, + }, + setup(props: any, { slots, attrs, expose }: any) { + const formData = reactive({ ...(props.initialValues || {}) }); + provide('form-root', { + get mode() { + return 'create'; + }, + get draft() { + return formData; + }, + setField(path: string, value: unknown) { + (formData as any)[path] = value; + }, + getField(path: string) { + return (formData as any)[path]; + }, + onChange() {}, + }); + // No submit/cancel expose — exercises onApply fallback. + expose({}); + return () => + h('div', { ...attrs, 'data-testid': 'choy-properties-definition-item-form' }, [ + slots.default?.({ formData, viewMode: props.viewMode }), + ]); + }, + }); + const onSaved = fnRecorder(); + const m = mountDialog({}, { onSaved }); + try { + await flushPromises(); + m.click('[data-testid="choy-properties-definition-item-save"]'); + await flushPromises(); + expect(onSaved.calls.length).toBe(1); + } finally { + m.unmount(); + installStubs(); + } + }); + + test('submitHandler is a no-op when dialog is readonly', async () => { + const onSaved = fnRecorder(); + let submitHandler: ((ctx: any) => Promise) | undefined; + stubSfc(ChoyFormView as any, { + name: 'ChoyFormView', + props: { + store: Object, + initialValues: Object, + viewMode: String, + submitHandler: Function, + cancelHandler: Function, + }, + setup(props: any, { slots, attrs, expose }: any) { + const formData = reactive({ ...(props.initialValues || {}) }); + submitHandler = props.submitHandler; + provide('form-root', { + get mode() { + return 'display'; + }, + get draft() { + return formData; + }, + setField() {}, + getField(path: string) { + return (formData as any)[path]; + }, + onChange() {}, + }); + expose({ + submit: () => + props.submitHandler?.({ + formData, + mode: 'display', + data: formData, + defaultSubmit: async () => null, + }), + }); + return () => + h('div', { ...attrs, 'data-testid': 'choy-properties-definition-item-form' }, [ + slots.default?.({ formData, viewMode: props.viewMode }), + ]); + }, + }); + const m = mountDialog({ readonly: true }, { onSaved }); + try { + await flushPromises(); + expect(submitHandler).toBeTruthy(); + const result = await submitHandler!({ + formData: {}, + mode: 'display', + data: {}, + defaultSubmit: async () => null, + }); + expect(result).toEqual({ handled: true, skipSuccessMessage: true }); + expect(onSaved.calls.length).toBe(0); + } finally { + m.unmount(); + installStubs(); + } + }); + + test('readonly Edit emits edit when canEdit', async () => { + const onEdit = fnRecorder(); + const m = mountDialog({ readonly: true, canEdit: true }, { onEdit }); + await flushPromises(); + m.click('[data-testid="choy-properties-definition-item-edit"]'); + await flushPromises(); + expect(onEdit.calls.length).toBe(1); + m.unmount(); + }); + + test('Edit is hidden when canEdit is false', async () => { + const onEdit = fnRecorder(); + const m = mountDialog({ readonly: true, canEdit: false }, { onEdit }); await flushPromises(); + expect(m.q('[data-testid="choy-properties-definition-item-edit"]')).toBeFalsy(); + expect(onEdit.calls.length).toBe(0); + m.unmount(); + }); + test('translate label and help apply maps via onTranslateSaved', async () => { + const onSaved = fnRecorder(); + const m = mountDialog( + { + item: draft({ + name: 'tax_id', + string: 'Tax', + stringMap: { en_US: 'Tax' }, + help: 'H', + helpMap: { en_US: 'H' }, + }), + }, + { onSaved }, + ); + await flushPromises(); + // setupState unwraps refs — formStore is the LocalFormStore instance. + const store = m.setupState().formStore as LocalFormStore | null; m.click('[data-testid="choy-properties-definition-translate-string"]'); await flushPromises(); - expect(m.q('[data-testid="translate-dialog-stub"]')).toBeTruthy(); - expect(String(lastTranslateProps?.labelTitle || '')).toMatch(/tax_id/); m.click('[data-testid="stub-translate-save-string"]'); await flushPromises(); + expect(String((m.q('[data-testid="choy-properties-definition-string"]') as HTMLInputElement)?.value || '')).toBe( + 'L-en', + ); + expect(store?.getField('Label')).toBe('L-en'); m.click('[data-testid="choy-properties-definition-translate-help"]'); await flushPromises(); - expect(String(lastTranslateProps?.labelTitle || '')).toMatch(/Help/i); m.click('[data-testid="stub-translate-save-help"]'); await flushPromises(); + expect(store?.getField('Help')).toBe('H-en'); - // Re-open and save again so maps stick into local before confirm m.click('[data-testid="choy-properties-definition-item-save"]'); await flushPromises(); + expect(onSaved.calls.length).toBe(1); + const saved = onSaved.calls[0][0] as DefinitionEditorDraftItem; + expect(saved.stringMap).toEqual({ en_US: 'L-en', zh_CN: 'L-zh' }); + expect(saved.helpMap).toEqual({ en_US: 'H-en', zh_CN: 'H-zh' }); m.unmount(); }); @@ -456,8 +774,8 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { item: draft({ name: 'prio', type: 'selection', - string: 'Priority', - stringMap: { en_US: 'Priority' }, + string: 'P', + stringMap: { en_US: 'P' }, selectionText: '[["low","Low"]]', selectionLabelMaps: { low: { en_US: 'Low' } }, }), @@ -465,110 +783,25 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { { onSaved }, ); await flushPromises(); + expect(m.q('[data-testid="choy-properties-definition-section-selection"]')).toBeTruthy(); + expect(m.q('[data-testid="choy-properties-definition-selection-scroll"]')).toBeTruthy(); expect(m.q('[data-testid="selection-editor-stub"]')).toBeTruthy(); - expect(lastSelectionProps).toBeTruthy(); - m.click('[data-testid="stub-selection-update"]'); await flushPromises(); - expect(selectionUpdateEmit).toBeTruthy(); - m.click('[data-testid="stub-selection-translate"]'); await flushPromises(); - expect(m.q('[data-testid="translate-dialog-stub"]')).toBeTruthy(); - expect(selectionTranslateEmit).toBeTruthy(); + expect(lastTranslateProps).toBeTruthy(); m.click('[data-testid="stub-translate-save-selection"]'); await flushPromises(); - m.click('[data-testid="choy-properties-definition-item-save"]'); await flushPromises(); expect(onSaved.calls.length).toBe(1); const saved = onSaved.calls[0][0] as DefinitionEditorDraftItem; - expect(saved.type).toBe('selection'); - expect(saved.selectionLabelMaps.low).toBeTruthy(); + expect(saved.selectionLabelMaps.low).toEqual({ en_US: 'S-en', zh_CN: 'S-zh' }); m.unmount(); }); - test('selection translate keeps in-progress blank option rows', async () => { - const onSaved = fnRecorder(); - const m = mountDialog( - { - item: draft({ - name: 'prio', - type: 'selection', - string: 'Priority', - stringMap: { en_US: 'Priority' }, - selectionText: JSON.stringify([ - ['low', 'Low'], - ['', ''], - ]), - selectionLabelMaps: { low: { en_US: 'Low' } }, - selectionRowMapKeys: ['low', ''], - }), - }, - { onSaved }, - ); - try { - await flushPromises(); - m.click('[data-testid="stub-selection-translate"]'); - await flushPromises(); - m.click('[data-testid="stub-translate-save-selection"]'); - await flushPromises(); - const editorRows = (lastSelectionProps?.modelValue as Array<{ value: string }> | undefined) || []; - expect(editorRows.some(r => r && r.value === '')).toBe(true); - m.click('[data-testid="choy-properties-definition-item-save"]'); - await flushPromises(); - expect(onSaved.calls.length).toBe(1); - const saved = onSaved.calls[0][0] as DefinitionEditorDraftItem; - const rows = JSON.parse(String(saved.selectionText || '[]')) as unknown[]; - expect(rows.some(r => Array.isArray(r) && r[0] === '')).toBe(true); - expect(saved.selectionLabelMaps.low).toBeTruthy(); - } finally { - m.unmount(); - } - }); - - test('selection translate keeps newly saved label (not stale pending row label)', async () => { - const onSaved = fnRecorder(); - const m = mountDialog( - { - item: draft({ - name: 'prio', - type: 'selection', - string: 'Priority', - stringMap: { en_US: 'Priority' }, - selectionText: JSON.stringify([ - ['low', 'Low'], - ['', ''], - ]), - selectionLabelMaps: { low: { en_US: 'Low' } }, - selectionRowMapKeys: ['low', ''], - }), - }, - { onSaved }, - ); - try { - await flushPromises(); - m.click('[data-testid="stub-selection-translate"]'); - await flushPromises(); - m.click('[data-testid="stub-translate-save-selection"]'); - await flushPromises(); - const editorRows = - (lastSelectionProps?.modelValue as Array<{ value: string; label: string }> | undefined) || []; - const low = editorRows.find(r => r && r.value === 'low'); - expect(low?.label).toBe('S-en'); - m.click('[data-testid="choy-properties-definition-item-save"]'); - await flushPromises(); - expect(onSaved.calls.length).toBe(1); - const saved = onSaved.calls[0][0] as DefinitionEditorDraftItem; - expect(saved.selectionLabelMaps.low).toEqual({ en_US: 'S-en', zh_CN: 'S-zh' }); - const rows = JSON.parse(String(saved.selectionText || '[]')) as Array<[string, string]>; - expect(rows.find(r => r[0] === 'low')?.[1]).toBe('S-en'); - } finally { - m.unmount(); - } - }); - - test('readonly skips translate controls and selection mutate', async () => { + test('readonly uses FormView display mode (not edit inputs) and skips translate', async () => { const m = mountDialog({ readonly: true, item: draft({ @@ -579,12 +812,12 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { }), }); await flushPromises(); + expect(m.q('[data-testid="choy-properties-definition-item-display"]')).toBeTruthy(); + expect(m.q('[data-testid="choy-properties-definition-name"]')?.getAttribute('data-readonly')).toBe('1'); + expect(m.q('input')).toBeFalsy(); expect(m.q('[data-testid="choy-properties-definition-translate-string"]')).toBeFalsy(); - expect(m.q('[data-testid="choy-properties-definition-translate-help"]')).toBeFalsy(); expect(m.q('[data-testid="selection-editor-stub"]')).toBeTruthy(); - // Selection update should be ignored when readonly - m.click('[data-testid="stub-selection-update"]'); - await flushPromises(); + expect(lastSelectionProps?.readonly).toBe(true); m.unmount(); }); @@ -599,7 +832,6 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { }); test('editorLang falls back without pinia', async () => { - // Unset pinia after mount setup path uses TRANSLATED_BASE_LANG when no pinia setActivePinia(undefined as any); const onSaved = fnRecorder(); const m = mountApp(ItemDialog as any, { @@ -632,12 +864,27 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { m.unmount(); }); + test('closing modelValue clears form and emits closed', async () => { + const onClosed = fnRecorder(); + const m = mountDialog({}, { onClosed }, { reactiveProps: true }); + try { + await flushPromises(); + expect(m.q('[data-testid="choy-properties-definition-item-form"]')).toBeTruthy(); + (m.props as { modelValue: boolean }).modelValue = false; + await flushPromises(); + expect(onClosed.calls.length).toBe(1); + expect(m.q('[data-testid="choy-properties-definition-item-form"]')).toBeFalsy(); + } finally { + m.unmount(); + } + }); + test('openSelectionTranslate ignores empty value', async () => { stubSfc(ChoyPropertiesDefinitionSelectionEditor as any, { name: 'ChoyPropertiesDefinitionSelectionEditor', props: { modelValue: { type: Array, default: () => [] }, readonly: Boolean }, emits: ['update:modelValue', 'translate'], - setup(_: any, { emit }: any) { + setup(_props: any, { emit }: any) { return () => h('div', { 'data-testid': 'selection-editor-stub' }, [ h( @@ -647,7 +894,7 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { 'data-testid': 'stub-selection-translate-empty', onClick: () => emit('translate', ' '), }, - 'empty', + 'translate-empty', ), ]); }, @@ -656,6 +903,8 @@ describe('ChoyPropertiesDefinitionItemDialog', () => { item: draft({ name: 'prio', type: 'selection', + string: 'P', + stringMap: { en_US: 'P' }, selectionText: '[["low","Low"]]', selectionLabelMaps: { low: { en_US: 'Low' } }, }), diff --git a/modules/web/web/components/field/ChoyPropertiesDefinitionItemDialog.vue b/modules/web/web/components/field/ChoyPropertiesDefinitionItemDialog.vue index 8e45b273d..753b07cd7 100644 --- a/modules/web/web/components/field/ChoyPropertiesDefinitionItemDialog.vue +++ b/modules/web/web/components/field/ChoyPropertiesDefinitionItemDialog.vue @@ -6,137 +6,226 @@ SPDX-License-Identifier: Apache-2.0