@object-ui/app-shell 11.3.0 → 11.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +522 -0
  2. package/README.md +23 -0
  3. package/dist/console/ConsoleShell.js +17 -2
  4. package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
  5. package/dist/console/home/CloudOnboardingNext.js +14 -4
  6. package/dist/console/home/HomePage.js +34 -7
  7. package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
  8. package/dist/console/organizations/OrganizationsPage.js +16 -7
  9. package/dist/hooks/useConsoleActionRuntime.js +32 -3
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.js +3 -0
  12. package/dist/preview/DraftChangesPanel.d.ts +3 -1
  13. package/dist/preview/DraftChangesPanel.js +6 -5
  14. package/dist/utils/deriveRelatedLists.d.ts +20 -5
  15. package/dist/utils/deriveRelatedLists.js +31 -13
  16. package/dist/utils/resolveViewId.d.ts +23 -0
  17. package/dist/utils/resolveViewId.js +37 -0
  18. package/dist/utils/warnSuppressedListNav.d.ts +10 -0
  19. package/dist/utils/warnSuppressedListNav.js +40 -0
  20. package/dist/views/InterfaceListPage.js +6 -4
  21. package/dist/views/ObjectView.js +61 -10
  22. package/dist/views/RecordDetailView.js +131 -104
  23. package/dist/views/RecordFormPage.js +7 -1
  24. package/dist/views/RelatedRecordActionsBridge.d.ts +24 -0
  25. package/dist/views/RelatedRecordActionsBridge.js +114 -0
  26. package/dist/views/metadata-admin/PackagesPage.js +18 -7
  27. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
  28. package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
  29. package/dist/views/metadata-admin/i18n.d.ts +12 -21
  30. package/dist/views/metadata-admin/i18n.js +343 -2
  31. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +25 -11
  32. package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
  33. package/dist/views/metadata-admin/permission-slice.js +70 -0
  34. package/dist/views/metadata-admin/previews/AppNavCanvas.js +11 -7
  35. package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
  36. package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
  37. package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
  38. package/dist/views/studio-design/BuilderLanding.js +133 -0
  39. package/dist/views/studio-design/ObjectFormDesigner.d.ts +31 -0
  40. package/dist/views/studio-design/ObjectFormDesigner.js +226 -0
  41. package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
  42. package/dist/views/studio-design/ObjectSettingsPanel.js +45 -0
  43. package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
  44. package/dist/views/studio-design/ObjectValidationsPanel.js +78 -0
  45. package/dist/views/studio-design/StudioDesignSurface.js +793 -146
  46. package/dist/views/studio-design/metadataError.d.ts +23 -0
  47. package/dist/views/studio-design/metadataError.js +44 -0
  48. package/dist/views/studio-design/packages-io.d.ts +27 -0
  49. package/dist/views/studio-design/packages-io.js +61 -0
  50. package/package.json +42 -39
@@ -1,4 +1,26 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ /**
3
+ * Metadata admin i18n bundle (Phase 3f).
4
+ *
5
+ * Lightweight static label table for the 27 built-in metadata types,
6
+ * plus a tiny `t()` helper for engine UI strings.
7
+ *
8
+ * Why not i18next? The engine already consumes `label` from the
9
+ * server's `/meta/types` response (which is sourced from
10
+ * `DEFAULT_METADATA_TYPE_REGISTRY`). This bundle exists as a fallback
11
+ * for environments without translation bundles configured, and as the
12
+ * single source of truth for Chinese labels until the platform's
13
+ * `setup.translation.ts` ships zh-CN coverage.
14
+ *
15
+ * Usage:
16
+ * import { translateMetadataType, t } from './i18n';
17
+ * translateMetadataType('view', 'zh-CN') // → '视图'
18
+ * t('engine.directory.title', 'zh-CN') // → '元数据'
19
+ *
20
+ * The DirectoryPage / PageShell call these to localise headings when
21
+ * the consumer hasn't wired the global i18n provider.
22
+ */
23
+ import { useObjectTranslation } from '@object-ui/i18n';
2
24
  const TYPE_LABELS_EN = {
3
25
  // Data
4
26
  object: 'Object',
@@ -547,9 +569,10 @@ const ENGINE_STRINGS_EN = {
547
569
  'engine.packages.col.version': 'Version',
548
570
  'engine.packages.col.scope': 'Scope',
549
571
  'engine.packages.col.status': 'Status',
550
- 'engine.packages.scope.project': 'Project',
572
+ 'engine.packages.scope.project': 'Read-only · code',
551
573
  'engine.packages.scope.system': 'System',
552
574
  'engine.packages.scope.cloud': 'Cloud',
575
+ 'engine.packages.scope.writable': 'Writable',
553
576
  'engine.packages.status.enabled': 'Enabled',
554
577
  'engine.packages.status.disabled': 'Disabled',
555
578
  'engine.packages.import.invalidJson': 'Selected file is not valid JSON.',
@@ -805,6 +828,157 @@ const ENGINE_STRINGS_EN = {
805
828
  // AI assistant entry points
806
829
  'designer.canvas.askAi': 'Ask AI',
807
830
  'designer.canvas.askAiGenerate': 'Generate fields with AI',
831
+ // ── StudioDesignSurface (ADR-0080 WYSIWYG design surface) ──────────────
832
+ // Shared chrome
833
+ 'engine.studio.cancel': 'Cancel',
834
+ 'engine.studio.create': 'Create',
835
+ 'engine.studio.createDraft': 'Create (save as draft)',
836
+ 'engine.studio.saveDraft': 'Save draft',
837
+ 'engine.studio.publish': 'Publish',
838
+ 'engine.studio.loading': 'Loading…',
839
+ 'engine.studio.loadFailed': 'Failed to load',
840
+ 'engine.studio.unpublishedDraft': 'Unpublished draft',
841
+ 'engine.studio.unpublished': 'Unpublished',
842
+ 'engine.studio.new': 'New',
843
+ 'engine.studio.edit': 'Edit',
844
+ 'engine.studio.done': 'Done',
845
+ 'engine.studio.close': 'Close',
846
+ 'engine.studio.deselect': 'Clear selection',
847
+ 'engine.studio.home': 'Back to home',
848
+ // Pillar tab labels
849
+ 'engine.studio.pillar.data': 'Data',
850
+ 'engine.studio.pillar.automations': 'Automations',
851
+ 'engine.studio.pillar.interfaces': 'Interfaces',
852
+ 'engine.studio.pillar.access': 'Access',
853
+ // Header · package-level publish + app bridge
854
+ 'engine.studio.changes': 'Changes',
855
+ 'engine.studio.publishTitle': 'Confirm and publish all pending drafts at once (whole package · one atomic release)',
856
+ 'engine.studio.publishNoneTitle': 'No drafts pending publish',
857
+ 'engine.studio.publishedAll': 'Published all drafts in this package (one atomic release)',
858
+ 'engine.studio.app.open': 'Open app',
859
+ 'engine.studio.app.openTitle': 'Open app “{label}” (the published front-end)',
860
+ 'engine.studio.app.create': 'Create app',
861
+ 'engine.studio.app.noneTitle': 'This package has no app (front-end) yet — create one',
862
+ 'engine.studio.app.willOpenAfterPublish': 'After publishing, this becomes “Open app”',
863
+ 'engine.studio.app.pending': 'App “{label}” pending publish',
864
+ 'engine.studio.app.namePlaceholder': 'App name (e.g. Order Center)',
865
+ 'engine.studio.app.idPlaceholder': 'Identifier (e.g. orders_app)',
866
+ 'engine.studio.app.savedDraft': 'App “{label}” saved as draft — open it after publishing',
867
+ // Package switcher
868
+ 'engine.studio.pkg.switchTitle': 'Switch / create package',
869
+ 'engine.studio.pkg.readonly': 'Read-only',
870
+ 'engine.studio.pkg.writable': 'Writable',
871
+ 'engine.studio.pkg.heading': 'Packages (apps)',
872
+ 'engine.studio.pkg.none': 'No app packages yet',
873
+ 'engine.studio.pkg.namePlaceholder': 'Name (e.g. Repair Center)',
874
+ 'engine.studio.pkg.idPlaceholder': 'Package ID (e.g. com.example.repairs)',
875
+ 'engine.studio.pkg.createWritable': 'Create writable package',
876
+ 'engine.studio.pkg.new': 'New package (writable base)',
877
+ 'engine.studio.pkg.created': 'Package {name} created (writable)',
878
+ // Nav item inspector (Interfaces pillar)
879
+ 'engine.studio.nav.selectItem': 'Select a menu item on the left.',
880
+ 'engine.studio.nav.label': 'Label',
881
+ 'engine.studio.nav.labelPlaceholder': 'e.g. Positions',
882
+ 'engine.studio.nav.linkObject': 'Link to object',
883
+ 'engine.studio.nav.chooseObject': '— Choose object —',
884
+ 'engine.studio.nav.boundHint': 'This menu item opens that object’s record list.',
885
+ 'engine.studio.nav.unboundHint': 'Choose an object; the menu item will open its record list.',
886
+ 'engine.studio.nav.noObjects': 'This package has no objects yet — create one in the Data pillar first.',
887
+ // Interfaces pillar
888
+ 'engine.studio.if.pickLeft': 'Select a menu item on the left',
889
+ 'engine.studio.if.navHeading': '{app} · Navigation',
890
+ 'engine.studio.if.editNavTitle': 'Edit navigation (drag to reorder / rename / add-remove)',
891
+ 'engine.studio.if.doneEditTitle': 'Done editing',
892
+ 'engine.studio.if.noApp': 'This package has no app yet.',
893
+ 'engine.studio.if.noNavItems': 'No nav items yet — (click “Edit” above to add)',
894
+ 'engine.studio.if.previewIsRuntime': 'Preview = runtime · same renderer',
895
+ 'engine.studio.if.noAppTitle': 'This package has no app yet',
896
+ 'engine.studio.if.noAppHint': 'Create an app to design its navigation and interfaces.',
897
+ 'engine.studio.if.readonlyPreview': '{type} shows a read-only preview for now; design support is in progress.',
898
+ 'engine.studio.if.editHint': 'Click a block → edit on the right · then “Save draft” → “Publish”',
899
+ 'engine.studio.if.objectHintPre': 'Runtime list preview · edit fields / structure in the ',
900
+ 'engine.studio.if.objectHintPost': ' pillar',
901
+ 'engine.studio.inspector.props': 'Properties',
902
+ 'engine.studio.inspector.emptyLine1': 'Click a block on the canvas,',
903
+ 'engine.studio.inspector.emptyLine2': 'and edit its properties right here.',
904
+ // Data pillar
905
+ 'engine.studio.data.newFieldLabel': 'New field',
906
+ 'engine.studio.data.nameFieldLabel': 'Name',
907
+ 'engine.studio.data.idExists': 'Identifier “{name}” already exists',
908
+ 'engine.studio.data.fieldCount': '{count} fields',
909
+ 'engine.studio.data.pickObject': 'Select an object',
910
+ 'engine.studio.data.objects': 'Objects',
911
+ 'engine.studio.data.searchObjects': 'Search objects…',
912
+ 'engine.studio.data.noObjects': 'No objects yet — create one below to start',
913
+ 'engine.studio.data.labelPlaceholder': 'Display name (e.g. Repair Ticket)',
914
+ 'engine.studio.data.idPlaceholder': 'Identifier (e.g. repair_ticket)',
915
+ 'engine.studio.data.newObject': 'New object',
916
+ 'engine.studio.data.firstObjectTitle': 'Start with your first object',
917
+ 'engine.studio.data.firstObjectHint': 'Objects are your app’s data foundation (e.g. “Orders”, “Customers”). Enter a display name and identifier at the bottom-left to create one; then design its fields, forms, and automations, and publish once at the end.',
918
+ 'engine.studio.data.tab.records': 'Records',
919
+ 'engine.studio.data.tab.form': 'Form',
920
+ 'engine.studio.data.tab.rules': 'Validations',
921
+ 'engine.studio.data.tab.settings': 'Settings',
922
+ 'engine.studio.data.badge.grid': 'Runtime list · same renderer',
923
+ 'engine.studio.data.badge.rules': 'Validation rules · draft',
924
+ 'engine.studio.data.badge.settings': 'Object settings · draft',
925
+ 'engine.studio.data.badge.formLayout': 'Form design · draft',
926
+ 'engine.studio.data.badge.formPreview': 'Runtime form · published definition',
927
+ 'engine.studio.data.addFieldTitle': 'Add a field (then set its type and properties on the right)',
928
+ 'engine.studio.data.addField': 'Add field',
929
+ 'engine.studio.data.editFieldProps': 'Edit field properties',
930
+ 'engine.studio.data.draftObjectTitle': 'Unpublished new object',
931
+ 'engine.studio.data.draftObjectHint': 'The Records grid queries real data, but this object has no table until it’s published. Design fields and groups in “Form · Layout” first, then click “Publish” in the top bar — after publishing, this becomes its live data grid.',
932
+ 'engine.studio.data.goDesignFields': 'Go to “Form · Layout” to design fields',
933
+ 'engine.studio.data.gridHint': 'Column “+” adds a field · pencil edits properties · drag a header to reorder · then “Save draft” → “Publish”',
934
+ 'engine.studio.data.form.layout': 'Layout',
935
+ 'engine.studio.data.form.preview': 'Preview',
936
+ 'engine.studio.data.form.layoutBadge': 'Layout designer · draft (includes unpublished changes)',
937
+ 'engine.studio.data.form.previewBadge': 'Runtime form · published definition',
938
+ 'engine.studio.data.form.previewWarn': 'You have unpublished changes — this preview shows the pre-publish (published) state; confirm the draft in “Layout”, and to see the post-publish result, click “Publish” in the top bar first.',
939
+ 'engine.studio.data.form.noPublishedTitle': 'No published definition yet',
940
+ 'engine.studio.data.form.noPublishedHint': '“Preview” renders the published runtime form, but this object isn’t published yet. Confirm the draft in “Layout”, then click “Publish” in the top bar to preview.',
941
+ 'engine.studio.data.formHint': 'Click any field → edit properties on the right · “Add field” adds a field · then “Save draft” → “Publish”',
942
+ 'engine.studio.data.fieldProps': 'Field properties',
943
+ // Automations pillar
944
+ 'engine.studio.auto.nodeStart': 'Start',
945
+ 'engine.studio.auto.nodeEnd': 'End',
946
+ 'engine.studio.auto.savedDraft': 'Automation “{label}” saved as draft',
947
+ 'engine.studio.auto.defaultOff': 'Off by default · review before enabling',
948
+ 'engine.studio.auto.heading': 'Automations · flow',
949
+ 'engine.studio.auto.newTitle': 'New automation',
950
+ 'engine.studio.auto.none': 'No automations yet — click “New” to start',
951
+ 'engine.studio.auto.namePlaceholder': 'Name (e.g. Offer Notice)',
952
+ 'engine.studio.auto.idPlaceholder': 'Identifier (e.g. offer_notice)',
953
+ 'engine.studio.auto.canvasHint': 'Visual orchestration · click a node to configure',
954
+ 'engine.studio.auto.pick': 'Select an automation',
955
+ 'engine.studio.auto.editHint': 'Click a node → configure on the right · then “Save draft” → “Publish”',
956
+ 'engine.studio.auto.config': 'Configuration',
957
+ 'engine.studio.auto.emptyLine1': 'Click a node on the canvas,',
958
+ 'engine.studio.auto.emptyLine2': 'and its configuration appears here.',
959
+ // Access pillar
960
+ 'engine.studio.access.created': 'Permission set “{label}” created',
961
+ 'engine.studio.access.title': 'Permission matrix',
962
+ 'engine.studio.access.subtitle': 'Objects × CRUD · field-level R/W',
963
+ 'engine.studio.access.bannerTitle': 'This matrix lists only the objects this package declares, and “Save” merges just that slice — grants contributed by other packages are preserved. Edits are saved as package drafts and go live when you Publish the package (top bar), exactly like Data and Interfaces.',
964
+ 'engine.studio.access.banner': 'This package’s objects · saved as draft',
965
+ 'engine.studio.access.heading': 'Permission sets / Profiles',
966
+ 'engine.studio.access.search': 'Search permissions…',
967
+ 'engine.studio.access.none': 'No permission sets yet — create one below',
968
+ 'engine.studio.access.labelPlaceholder': 'Display name (e.g. Sales permissions)',
969
+ 'engine.studio.access.idPlaceholder': 'Identifier (e.g. sales_perms)',
970
+ 'engine.studio.access.new': 'New permission set',
971
+ 'engine.studio.access.emptyMain': 'Create a permission set to start configuring',
972
+ 'engine.studio.access.pick': 'Select a permission set',
973
+ // ── AppNavCanvas (nav-tree editor, used by Studio + AppPreview) ─────────
974
+ 'engine.appNav.heading': 'Navigation',
975
+ 'engine.appNav.addItem': 'Add nav item',
976
+ 'engine.appNav.removeItem': 'Remove nav item',
977
+ 'engine.appNav.empty': 'Empty — click “Add nav item” to start',
978
+ 'engine.appNav.emptyReadonly': 'No top-level nav items yet',
979
+ 'engine.appNav.newItem': 'New item',
980
+ 'engine.appNav.itemOne': 'item',
981
+ 'engine.appNav.itemOther': 'items',
808
982
  };
809
983
  const ENGINE_STRINGS_ZH = {
810
984
  'engine.package.writableRequired': '请先选择或新建一个可写的基座(package)——只读的代码包中无法新建该项。',
@@ -1241,9 +1415,10 @@ const ENGINE_STRINGS_ZH = {
1241
1415
  'engine.packages.col.version': '版本',
1242
1416
  'engine.packages.col.scope': '范围',
1243
1417
  'engine.packages.col.status': '状态',
1244
- 'engine.packages.scope.project': '项目',
1418
+ 'engine.packages.scope.project': '只读 · 代码包',
1245
1419
  'engine.packages.scope.system': '系统',
1246
1420
  'engine.packages.scope.cloud': '云端',
1421
+ 'engine.packages.scope.writable': '可写',
1247
1422
  'engine.packages.status.enabled': '已启用',
1248
1423
  'engine.packages.status.disabled': '已禁用',
1249
1424
  'engine.packages.import.invalidJson': '所选文件不是有效 JSON。',
@@ -1498,6 +1673,157 @@ const ENGINE_STRINGS_ZH = {
1498
1673
  // AI assistant entry points
1499
1674
  'designer.canvas.askAi': '问 AI',
1500
1675
  'designer.canvas.askAiGenerate': '用 AI 生成字段',
1676
+ // ── StudioDesignSurface (ADR-0080 WYSIWYG design surface) ──────────────
1677
+ // Shared chrome
1678
+ 'engine.studio.cancel': '取消',
1679
+ 'engine.studio.create': '创建',
1680
+ 'engine.studio.createDraft': '创建(存为草稿)',
1681
+ 'engine.studio.saveDraft': '保存草稿',
1682
+ 'engine.studio.publish': '发布',
1683
+ 'engine.studio.loading': '加载中…',
1684
+ 'engine.studio.loadFailed': '加载失败',
1685
+ 'engine.studio.unpublishedDraft': '未发布草稿',
1686
+ 'engine.studio.unpublished': '未发布',
1687
+ 'engine.studio.new': '新建',
1688
+ 'engine.studio.edit': '编辑',
1689
+ 'engine.studio.done': '完成',
1690
+ 'engine.studio.close': '关闭',
1691
+ 'engine.studio.deselect': '取消选择',
1692
+ 'engine.studio.home': '返回主页',
1693
+ // Pillar tab labels
1694
+ 'engine.studio.pillar.data': '数据',
1695
+ 'engine.studio.pillar.automations': '自动化',
1696
+ 'engine.studio.pillar.interfaces': '界面',
1697
+ 'engine.studio.pillar.access': '权限',
1698
+ // Header · package-level publish + app bridge
1699
+ 'engine.studio.changes': '变更',
1700
+ 'engine.studio.publishTitle': '一次性确认并发布全部待发布草稿(整包 · 一次原子发布)',
1701
+ 'engine.studio.publishNoneTitle': '没有待发布的草稿',
1702
+ 'engine.studio.publishedAll': '已发布本软件包的全部草稿(一次原子发布)',
1703
+ 'engine.studio.app.open': '打开应用',
1704
+ 'engine.studio.app.openTitle': '打开应用「{label}」(发布后的前端界面)',
1705
+ 'engine.studio.app.create': '创建应用',
1706
+ 'engine.studio.app.noneTitle': '这个软件包还没有应用(前端界面)— 创建一个',
1707
+ 'engine.studio.app.willOpenAfterPublish': '发布后这里会变成「打开应用」',
1708
+ 'engine.studio.app.pending': '应用「{label}」待发布',
1709
+ 'engine.studio.app.namePlaceholder': '应用名称(如:订单中心)',
1710
+ 'engine.studio.app.idPlaceholder': '标识符(如:orders_app)',
1711
+ 'engine.studio.app.savedDraft': '应用「{label}」已存为草稿 — 发布后即可打开',
1712
+ // Package switcher
1713
+ 'engine.studio.pkg.switchTitle': '切换 / 新建软件包',
1714
+ 'engine.studio.pkg.readonly': '只读',
1715
+ 'engine.studio.pkg.writable': '可写',
1716
+ 'engine.studio.pkg.heading': '软件包(应用)',
1717
+ 'engine.studio.pkg.none': '暂无应用软件包',
1718
+ 'engine.studio.pkg.namePlaceholder': '名称(如:维修中心)',
1719
+ 'engine.studio.pkg.idPlaceholder': '包 ID(如:com.example.repairs)',
1720
+ 'engine.studio.pkg.createWritable': '创建可写软件包',
1721
+ 'engine.studio.pkg.new': '新建软件包(可写 base)',
1722
+ 'engine.studio.pkg.created': '软件包 {name} 已创建(可写)',
1723
+ // Nav item inspector (Interfaces pillar)
1724
+ 'engine.studio.nav.selectItem': '在左侧选择一个菜单项。',
1725
+ 'engine.studio.nav.label': '标签',
1726
+ 'engine.studio.nav.labelPlaceholder': '如:职位',
1727
+ 'engine.studio.nav.linkObject': '链接到对象',
1728
+ 'engine.studio.nav.chooseObject': '— 选择对象 —',
1729
+ 'engine.studio.nav.boundHint': '这个菜单项会打开该对象的记录列表。',
1730
+ 'engine.studio.nav.unboundHint': '选择一个对象,菜单项将打开它的记录列表。',
1731
+ 'engine.studio.nav.noObjects': '这个软件包还没有对象 — 先到 Data 支柱创建。',
1732
+ // Interfaces pillar
1733
+ 'engine.studio.if.pickLeft': '从左侧选择一个菜单项',
1734
+ 'engine.studio.if.navHeading': '{app} · 导航',
1735
+ 'engine.studio.if.editNavTitle': '编辑导航(拖拽排序 / 重命名 / 增删)',
1736
+ 'engine.studio.if.doneEditTitle': '完成编辑',
1737
+ 'engine.studio.if.noApp': '这个软件包还没有应用。',
1738
+ 'engine.studio.if.noNavItems': '还没有导航项 —(点上方「编辑」添加)',
1739
+ 'engine.studio.if.previewIsRuntime': '预览即运行 · 同一渲染器',
1740
+ 'engine.studio.if.noAppTitle': '这个软件包还没有应用',
1741
+ 'engine.studio.if.noAppHint': '创建一个应用来设计它的导航与界面。',
1742
+ 'engine.studio.if.readonlyPreview': '{type} 暂用只读预览,设计能力建设中。',
1743
+ 'engine.studio.if.editHint': '点选积木 → 右侧直接改 · 改完「保存草稿」→「发布」',
1744
+ 'engine.studio.if.objectHintPre': '运行态列表预览 · 改字段 / 结构请到 ',
1745
+ 'engine.studio.if.objectHintPost': ' 支柱',
1746
+ 'engine.studio.inspector.props': '属性',
1747
+ 'engine.studio.inspector.emptyLine1': '在画布里点选一个积木,',
1748
+ 'engine.studio.inspector.emptyLine2': '它的属性会在这里直接编辑。',
1749
+ // Data pillar
1750
+ 'engine.studio.data.newFieldLabel': '新字段',
1751
+ 'engine.studio.data.nameFieldLabel': '名称',
1752
+ 'engine.studio.data.idExists': '标识符 "{name}" 已存在',
1753
+ 'engine.studio.data.fieldCount': '{count} 字段',
1754
+ 'engine.studio.data.pickObject': '选择一个对象',
1755
+ 'engine.studio.data.objects': '对象',
1756
+ 'engine.studio.data.searchObjects': '搜索对象…',
1757
+ 'engine.studio.data.noObjects': '还没有对象 — 在下方新建一个开始',
1758
+ 'engine.studio.data.labelPlaceholder': '显示名(如:报修工单)',
1759
+ 'engine.studio.data.idPlaceholder': '标识符(如:repair_ticket)',
1760
+ 'engine.studio.data.newObject': '新建对象',
1761
+ 'engine.studio.data.firstObjectTitle': '从第一个对象开始',
1762
+ 'engine.studio.data.firstObjectHint': '对象是应用的数据基座(如「订单」「客户」)。在左下角输入显示名与标识符即可创建;之后为它设计字段、表单与自动化,最后一次发布。',
1763
+ 'engine.studio.data.tab.records': '记录',
1764
+ 'engine.studio.data.tab.form': '表单',
1765
+ 'engine.studio.data.tab.rules': '验证',
1766
+ 'engine.studio.data.tab.settings': '设置',
1767
+ 'engine.studio.data.badge.grid': '运行态列表 · 同一渲染器',
1768
+ 'engine.studio.data.badge.rules': '验证规则 · 草稿',
1769
+ 'engine.studio.data.badge.settings': '对象设置 · 草稿',
1770
+ 'engine.studio.data.badge.formLayout': '表单设计 · 草稿',
1771
+ 'engine.studio.data.badge.formPreview': '运行态表单 · 已发布定义',
1772
+ 'engine.studio.data.addFieldTitle': '添加一个字段(随后在右侧设置类型与属性)',
1773
+ 'engine.studio.data.addField': '添加字段',
1774
+ 'engine.studio.data.editFieldProps': '编辑字段属性',
1775
+ 'engine.studio.data.draftObjectTitle': '未发布的新对象',
1776
+ 'engine.studio.data.draftObjectHint': '「记录」网格查询真实数据,而这个对象发布前还没有数据表。请先在「表单 · 布局」里设计字段与分组,然后点顶栏「发布」— 发布后这里就是它的实时数据网格。',
1777
+ 'engine.studio.data.goDesignFields': '去「表单 · 布局」设计字段',
1778
+ 'engine.studio.data.gridHint': '列头「+」加字段 · 笔形改属性 · 拖列头重排 · 改完「保存草稿」→「发布」',
1779
+ 'engine.studio.data.form.layout': '布局',
1780
+ 'engine.studio.data.form.preview': '预览',
1781
+ 'engine.studio.data.form.layoutBadge': '布局设计器 · 草稿(含未发布改动)',
1782
+ 'engine.studio.data.form.previewBadge': '运行态表单 · 已发布定义',
1783
+ 'engine.studio.data.form.previewWarn': '有未发布改动 — 此预览为发布前(已发布)的效果;草稿确认用「布局」,看发布后效果请先点顶栏「发布」',
1784
+ 'engine.studio.data.form.noPublishedTitle': '尚无已发布定义',
1785
+ 'engine.studio.data.form.noPublishedHint': '「预览」渲染已发布的运行态表单,而这个对象还未发布。在「布局」里确认草稿,点顶栏「发布」后即可预览。',
1786
+ 'engine.studio.data.formHint': '点选任意字段 → 右侧改属性 · 「添加字段」加字段 · 改完「保存草稿」→「发布」',
1787
+ 'engine.studio.data.fieldProps': '字段属性',
1788
+ // Automations pillar
1789
+ 'engine.studio.auto.nodeStart': '开始',
1790
+ 'engine.studio.auto.nodeEnd': '结束',
1791
+ 'engine.studio.auto.savedDraft': '自动化「{label}」已存为草稿',
1792
+ 'engine.studio.auto.defaultOff': '默认 OFF · 审阅后再启用',
1793
+ 'engine.studio.auto.heading': '自动化 · flow',
1794
+ 'engine.studio.auto.newTitle': '新建自动化',
1795
+ 'engine.studio.auto.none': '还没有自动化 — 点「新建」开始',
1796
+ 'engine.studio.auto.namePlaceholder': '名称(如:录用通知)',
1797
+ 'engine.studio.auto.idPlaceholder': '标识符(如:offer_notice)',
1798
+ 'engine.studio.auto.canvasHint': '可视化编排 · 点选节点配置',
1799
+ 'engine.studio.auto.pick': '选择一个自动化',
1800
+ 'engine.studio.auto.editHint': '点选节点 → 右侧配置 · 改完「保存草稿」→「发布」',
1801
+ 'engine.studio.auto.config': '配置',
1802
+ 'engine.studio.auto.emptyLine1': '在画布里点选一个节点,',
1803
+ 'engine.studio.auto.emptyLine2': '它的配置会在这里显示。',
1804
+ // Access pillar
1805
+ 'engine.studio.access.created': '权限集「{label}」已创建',
1806
+ 'engine.studio.access.title': '权限矩阵',
1807
+ 'engine.studio.access.subtitle': '对象 × CRUD · 字段级读写',
1808
+ 'engine.studio.access.bannerTitle': '此矩阵仅列出本包声明的对象,「Save」只合并本包切片 —— 其他包贡献的授权原样保留。编辑保存为软件包草稿,点击顶栏「发布」后随整个包一起生效(与数据、界面一致)。',
1809
+ 'engine.studio.access.banner': '仅本包对象 · 保存为草稿',
1810
+ 'engine.studio.access.heading': '权限集 / Profile',
1811
+ 'engine.studio.access.search': '搜索权限…',
1812
+ 'engine.studio.access.none': '还没有权限集 — 在下方新建一个',
1813
+ 'engine.studio.access.labelPlaceholder': '显示名(如:销售权限)',
1814
+ 'engine.studio.access.idPlaceholder': '标识符(如:sales_perms)',
1815
+ 'engine.studio.access.new': '新建权限集',
1816
+ 'engine.studio.access.emptyMain': '新建一个权限集开始配置',
1817
+ 'engine.studio.access.pick': '选择一个权限集',
1818
+ // ── AppNavCanvas (nav-tree editor, used by Studio + AppPreview) ─────────
1819
+ 'engine.appNav.heading': '导航',
1820
+ 'engine.appNav.addItem': '添加导航项',
1821
+ 'engine.appNav.removeItem': '删除导航项',
1822
+ 'engine.appNav.empty': '还没有导航项 — 点「添加导航项」开始',
1823
+ 'engine.appNav.emptyReadonly': '还没有顶层导航项',
1824
+ 'engine.appNav.newItem': '新菜单项',
1825
+ 'engine.appNav.itemOne': '项',
1826
+ 'engine.appNav.itemOther': '项',
1501
1827
  };
1502
1828
  function pickTable(locale) {
1503
1829
  const lower = (locale ?? '').toLowerCase();
@@ -1569,3 +1895,18 @@ export function detectLocale() {
1569
1895
  return 'zh-CN';
1570
1896
  return 'en-US';
1571
1897
  }
1898
+ /**
1899
+ * React hook — the Studio/metadata-admin locale derived from the app's ACTIVE
1900
+ * i18n language, collapsed to one of the two locales this catalog ships.
1901
+ *
1902
+ * Prefer this over {@link detectLocale} in components: `detectLocale()` reads
1903
+ * `navigator.language`, which never changes when the user switches languages
1904
+ * in-app via the LocaleSwitcher (that only moves the i18next instance). This
1905
+ * follows the live `useObjectTranslation().language`, so the Studio pillars
1906
+ * re-render in lock-step with the console locale — the same source the rest of
1907
+ * the app (Home, forms, list views) renders from.
1908
+ */
1909
+ export function useMetadataLocale() {
1910
+ const { language } = useObjectTranslation();
1911
+ return /^zh/i.test(language) ? 'zh-CN' : 'en-US';
1912
+ }
@@ -340,19 +340,33 @@ function useObjectOptions() {
340
340
  const [opts, setOpts] = React.useState([]);
341
341
  React.useEffect(() => {
342
342
  let cancelled = false;
343
- client
344
- .list('object')
345
- .then((items) => {
343
+ Promise.all([
344
+ client.list('object'),
345
+ // Draft objects are not yet published, so `list('object')` can't see
346
+ // them. Include them so a lookup can target a SIBLING object being
347
+ // designed in the same authoring pass (before the package's first
348
+ // publish) instead of forcing the author to type an API name blind.
349
+ client.listDrafts({ type: 'object' }).catch(() => []),
350
+ ])
351
+ .then(([published, drafts]) => {
346
352
  if (cancelled)
347
353
  return;
348
- const mapped = items
349
- .filter((i) => typeof i?.name === 'string' && i.name)
350
- .map((i) => ({
351
- value: i.name,
352
- label: i.label ? `${i.label} (${i.name})` : i.name,
353
- }))
354
- .sort((a, b) => a.value.localeCompare(b.value));
355
- setOpts(mapped);
354
+ const byName = new Map();
355
+ for (const i of published ?? []) {
356
+ if (typeof i?.name === 'string' && i.name && !byName.has(i.name)) {
357
+ byName.set(i.name, {
358
+ value: i.name,
359
+ label: i.label ? `${i.label} (${i.name})` : i.name,
360
+ });
361
+ }
362
+ }
363
+ for (const d of drafts ?? []) {
364
+ const name = d.name;
365
+ if (typeof name === 'string' && name && !byName.has(name)) {
366
+ byName.set(name, { value: name, label: `${name} (草稿)` });
367
+ }
368
+ }
369
+ setOpts([...byName.values()].sort((a, b) => a.value.localeCompare(b.value)));
356
370
  })
357
371
  .catch(() => {
358
372
  // Empty list — picker falls back to free-text. No banner needed.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Package-scoped slice merge for the permission matrix (ADR-0086 P0).
3
+ *
4
+ * A Permission Set / Profile is a single metadata record whose `objects` and
5
+ * `fields` maps accumulate authorization rows contributed by many packages.
6
+ * When the Access matrix is opened inside a package context it must:
7
+ *
8
+ * 1. show only the objects that package declares (scope), and
9
+ * 2. on Save write back ONLY that slice — leaving every row contributed by
10
+ * other packages byte-for-byte intact.
11
+ *
12
+ * Overwriting the whole record (the pre-P0 behavior) silently drops the rows
13
+ * other packages contributed. {@link mergePermissionSlice} rebuilds the record
14
+ * from a freshly-read base, keeping the set-level identity and every
15
+ * out-of-scope row, and overlaying only the in-scope rows the user edited.
16
+ */
17
+ export interface ObjectPerm {
18
+ allowCreate?: boolean;
19
+ allowRead?: boolean;
20
+ allowEdit?: boolean;
21
+ allowDelete?: boolean;
22
+ allowTransfer?: boolean;
23
+ allowRestore?: boolean;
24
+ allowPurge?: boolean;
25
+ viewAllRecords?: boolean;
26
+ modifyAllRecords?: boolean;
27
+ }
28
+ export interface FieldPerm {
29
+ readable?: boolean;
30
+ editable?: boolean;
31
+ }
32
+ export interface PermissionSetDraft {
33
+ name: string;
34
+ label?: string;
35
+ isProfile?: boolean;
36
+ objects: Record<string, ObjectPerm>;
37
+ fields?: Record<string, FieldPerm>;
38
+ systemPermissions?: string[];
39
+ tabPermissions?: Record<string, 'visible' | 'hidden' | 'default_on' | 'default_off'>;
40
+ [extra: string]: unknown;
41
+ }
42
+ /**
43
+ * Object name embedded in a `${object}.${field}` field-permission key. Object
44
+ * and field names are field-name-safe (snake_case, no dots), so the object is
45
+ * everything up to the first dot.
46
+ */
47
+ export declare function fieldKeyObject(key: string): string;
48
+ /**
49
+ * Narrow a permission set down to just the rows whose object is in `scope`.
50
+ * Used to drive the matrix display so a package panel lists only its own
51
+ * objects (and their field overrides), never the whole environment.
52
+ */
53
+ export declare function scopePermissionSet(set: Pick<PermissionSetDraft, 'objects' | 'fields'>, scope: Iterable<string>): {
54
+ objects: Record<string, ObjectPerm>;
55
+ fields: Record<string, FieldPerm>;
56
+ };
57
+ /**
58
+ * Merge the edited in-scope slice back onto a freshly-read full `base`.
59
+ *
60
+ * Out-of-scope rows (other packages' contributions) are copied verbatim from
61
+ * `base`; in-scope rows are taken entirely from `edited` (so removing a grant
62
+ * in the package panel deletes only that package's row). Set-level identity and
63
+ * any extra keys (systemPermissions, tabPermissions, …) come from `base`, with
64
+ * name / label / isProfile taking the user's edits.
65
+ */
66
+ export declare function mergePermissionSlice(base: PermissionSetDraft, edited: PermissionSetDraft, scope: Iterable<string>): PermissionSetDraft;
@@ -0,0 +1,70 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ /**
3
+ * Object name embedded in a `${object}.${field}` field-permission key. Object
4
+ * and field names are field-name-safe (snake_case, no dots), so the object is
5
+ * everything up to the first dot.
6
+ */
7
+ export function fieldKeyObject(key) {
8
+ const dot = key.indexOf('.');
9
+ return dot === -1 ? key : key.slice(0, dot);
10
+ }
11
+ function asScopeSet(scope) {
12
+ return scope instanceof Set ? scope : new Set(scope);
13
+ }
14
+ /**
15
+ * Narrow a permission set down to just the rows whose object is in `scope`.
16
+ * Used to drive the matrix display so a package panel lists only its own
17
+ * objects (and their field overrides), never the whole environment.
18
+ */
19
+ export function scopePermissionSet(set, scope) {
20
+ const scopeSet = asScopeSet(scope);
21
+ const objects = {};
22
+ for (const [k, v] of Object.entries(set.objects ?? {})) {
23
+ if (scopeSet.has(k))
24
+ objects[k] = v;
25
+ }
26
+ const fields = {};
27
+ for (const [k, v] of Object.entries(set.fields ?? {})) {
28
+ if (scopeSet.has(fieldKeyObject(k)))
29
+ fields[k] = v;
30
+ }
31
+ return { objects, fields };
32
+ }
33
+ /**
34
+ * Merge the edited in-scope slice back onto a freshly-read full `base`.
35
+ *
36
+ * Out-of-scope rows (other packages' contributions) are copied verbatim from
37
+ * `base`; in-scope rows are taken entirely from `edited` (so removing a grant
38
+ * in the package panel deletes only that package's row). Set-level identity and
39
+ * any extra keys (systemPermissions, tabPermissions, …) come from `base`, with
40
+ * name / label / isProfile taking the user's edits.
41
+ */
42
+ export function mergePermissionSlice(base, edited, scope) {
43
+ const scopeSet = asScopeSet(scope);
44
+ const objects = {};
45
+ for (const [k, v] of Object.entries(base.objects ?? {})) {
46
+ if (!scopeSet.has(k))
47
+ objects[k] = v; // preserve other packages' rows
48
+ }
49
+ for (const [k, v] of Object.entries(edited.objects ?? {})) {
50
+ if (scopeSet.has(k))
51
+ objects[k] = v; // write this package's slice
52
+ }
53
+ const fields = {};
54
+ for (const [k, v] of Object.entries(base.fields ?? {})) {
55
+ if (!scopeSet.has(fieldKeyObject(k)))
56
+ fields[k] = v;
57
+ }
58
+ for (const [k, v] of Object.entries(edited.fields ?? {})) {
59
+ if (scopeSet.has(fieldKeyObject(k)))
60
+ fields[k] = v;
61
+ }
62
+ return {
63
+ ...base,
64
+ name: edited.name,
65
+ label: edited.label,
66
+ isProfile: edited.isProfile,
67
+ objects,
68
+ fields,
69
+ };
70
+ }
@@ -19,6 +19,7 @@ import * as React from 'react';
19
19
  import { BarChart3, Compass, Database, FileText, Folder, GripVertical, LayoutDashboard, Link as LinkIcon, Plus, Trash2, } from 'lucide-react';
20
20
  import { Badge, cn } from '@object-ui/components';
21
21
  import { appendArray, moveArray, spliceArray } from '../inspectors/_shared';
22
+ import { t, useMetadataLocale } from '../i18n';
22
23
  const DND_MIME = 'text/x-objectui-nav';
23
24
  function inferKind(it) {
24
25
  if (it.kind)
@@ -100,6 +101,7 @@ function navPath(it) {
100
101
  return typeof p === 'string' && p ? p : undefined;
101
102
  }
102
103
  export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionChange, }) {
104
+ const locale = useMetadataLocale();
103
105
  const items = React.useMemo(() => {
104
106
  const v = draft[rootKey];
105
107
  return Array.isArray(v) ? v : [];
@@ -114,15 +116,16 @@ export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionCh
114
116
  const addItem = React.useCallback(() => {
115
117
  if (!onPatch)
116
118
  return;
117
- const newItem = { label: 'New item', path: '' };
119
+ const newLabel = t('engine.appNav.newItem', locale);
120
+ const newItem = { label: newLabel, path: '' };
118
121
  const next = appendArray(items, newItem);
119
122
  setItems(next);
120
123
  onSelectionChange?.({
121
124
  kind: 'nav',
122
125
  id: `${rootKey}[${next.length - 1}]`,
123
- label: 'New item',
126
+ label: newLabel,
124
127
  });
125
- }, [onPatch, items, setItems, rootKey, onSelectionChange]);
128
+ }, [onPatch, items, setItems, rootKey, onSelectionChange, locale]);
126
129
  const removeItem = React.useCallback((index) => {
127
130
  if (!onPatch)
128
131
  return;
@@ -158,7 +161,7 @@ export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionCh
158
161
  label: navLabel(next[to] ?? {}, to),
159
162
  });
160
163
  }, [onPatch, items, setItems, rootKey, onSelectionChange]);
161
- return (_jsxs("div", { className: "rounded-md border bg-card/40", children: [_jsxs("div", { className: "flex items-center justify-between border-b px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { className: "font-mono uppercase tracking-wide text-muted-foreground", children: rootKey }), _jsxs(Badge, { variant: "outline", className: "text-[10px]", children: [items.length, " ", items.length === 1 ? 'item' : 'items'] })] }), onPatch && (_jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded border border-dashed px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted/30 hover:text-foreground", onClick: addItem, children: [_jsx(Plus, { className: "h-3 w-3" }), " Add nav item"] }))] }), _jsx("div", { className: "space-y-1.5 p-2", onDragOver: (e) => {
164
+ return (_jsxs("div", { className: "rounded-md border bg-card/40", children: [_jsxs("div", { className: "flex items-center justify-between border-b px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { className: "font-medium uppercase tracking-wide text-muted-foreground", children: t('engine.appNav.heading', locale) }), _jsxs(Badge, { variant: "outline", className: "text-[10px]", children: [items.length, " ", items.length === 1 ? t('engine.appNav.itemOne', locale) : t('engine.appNav.itemOther', locale)] })] }), onPatch && (_jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded border border-dashed px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted/30 hover:text-foreground", onClick: addItem, children: [_jsx(Plus, { className: "h-3 w-3" }), " ", t('engine.appNav.addItem', locale)] }))] }), _jsx("div", { className: "space-y-1.5 p-2", onDragOver: (e) => {
162
165
  if (!onPatch)
163
166
  return;
164
167
  if (!e.dataTransfer.types.includes(DND_MIME))
@@ -176,8 +179,8 @@ export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionCh
176
179
  moveItem(dragIndex, items.length);
177
180
  setDragIndex(null);
178
181
  }, children: items.length === 0 ? (_jsx("div", { className: "rounded border border-dashed px-3 py-4 text-center text-[11px] text-muted-foreground", children: onPatch
179
- ? 'Empty — click "Add nav item" to start'
180
- : 'No top-level nav items yet' })) : (items.map((it, i) => (_jsx(NavCardTree, { item: it, index: i, depth: 0, path: `${rootKey}[${i}]`, selectedId: selectedId, canEdit: !!onPatch, onClick: (p, lbl) => onSelectionChange?.({ kind: 'nav', id: p, label: lbl }), onRename: (lbl) => renameItem(i, lbl), onRemove: () => removeItem(i), onDragStart: () => setDragIndex(i), onDragEnd: () => setDragIndex(null), onDropBefore: () => {
182
+ ? t('engine.appNav.empty', locale)
183
+ : t('engine.appNav.emptyReadonly', locale) })) : (items.map((it, i) => (_jsx(NavCardTree, { item: it, index: i, depth: 0, path: `${rootKey}[${i}]`, selectedId: selectedId, canEdit: !!onPatch, onClick: (p, lbl) => onSelectionChange?.({ kind: 'nav', id: p, label: lbl }), onRename: (lbl) => renameItem(i, lbl), onRemove: () => removeItem(i), onDragStart: () => setDragIndex(i), onDragEnd: () => setDragIndex(null), onDropBefore: () => {
181
184
  if (dragIndex == null)
182
185
  return;
183
186
  moveItem(dragIndex, i);
@@ -192,6 +195,7 @@ function NavCardTree({ item, index, depth, path, selectedId, canEdit, onClick, o
192
195
  })] }));
193
196
  }
194
197
  function NavCard({ item, index, depth, path, isSelected, canEdit, onClick, onRename, onRemove, onDragStart, onDragEnd, onDropBefore, }) {
198
+ const locale = useMetadataLocale();
195
199
  const kind = inferKind(item);
196
200
  const Icon = kindIcon(kind);
197
201
  const tone = kindTone(kind);
@@ -256,5 +260,5 @@ function NavCard({ item, index, depth, path, isSelected, canEdit, onClick, onRen
256
260
  e.stopPropagation();
257
261
  onRemove();
258
262
  }
259
- }, className: "inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive", "aria-label": "Remove nav item", children: _jsx(Trash2, { className: "h-3 w-3" }) }))] })] }));
263
+ }, className: "inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive", "aria-label": t('engine.appNav.removeItem', locale), children: _jsx(Trash2, { className: "h-3 w-3" }) }))] })] }));
260
264
  }