@object-ui/app-shell 7.0.0 → 7.1.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 (73) hide show
  1. package/CHANGELOG.md +281 -0
  2. package/dist/console/AppContent.js +14 -2
  3. package/dist/console/ai/AiChatPage.js +11 -7
  4. package/dist/console/ai/LiveCanvas.d.ts +8 -2
  5. package/dist/console/ai/LiveCanvas.js +6 -4
  6. package/dist/hooks/useChatConversation.d.ts +30 -0
  7. package/dist/hooks/useChatConversation.js +63 -0
  8. package/dist/hooks/useConsoleActionRuntime.js +6 -2
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.js +5 -1
  11. package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
  12. package/dist/layout/ConsoleFloatingChatbot.js +25 -8
  13. package/dist/layout/ContextSelectors.js +59 -35
  14. package/dist/layout/agentPicker.d.ts +56 -0
  15. package/dist/layout/agentPicker.js +40 -0
  16. package/dist/preview/CommitTimeline.d.ts +15 -0
  17. package/dist/preview/CommitTimeline.js +82 -0
  18. package/dist/preview/UnpublishedAppBar.js +11 -7
  19. package/dist/preview/commitHistory.d.ts +28 -0
  20. package/dist/preview/commitHistory.js +48 -0
  21. package/dist/providers/MetadataProvider.js +9 -0
  22. package/dist/views/FlowRunner.d.ts +2 -30
  23. package/dist/views/FlowRunner.js +18 -50
  24. package/dist/views/ScreenView.d.ts +70 -0
  25. package/dist/views/ScreenView.js +73 -0
  26. package/dist/views/metadata-admin/DirectoryPage.js +2 -14
  27. package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
  28. package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
  29. package/dist/views/metadata-admin/PackagesPage.js +9 -1
  30. package/dist/views/metadata-admin/ResourceEditPage.js +47 -20
  31. package/dist/views/metadata-admin/ResourceListPage.js +8 -16
  32. package/dist/views/metadata-admin/StudioHomePage.js +6 -14
  33. package/dist/views/metadata-admin/anchors.js +20 -2
  34. package/dist/views/metadata-admin/i18n.js +88 -2
  35. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +2 -2
  36. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +122 -8
  37. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +84 -3
  38. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +67 -2
  39. package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
  40. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
  41. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
  42. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
  43. package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
  44. package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
  45. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
  46. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +97 -0
  47. package/dist/views/metadata-admin/inspectors/flow-node-config.js +46 -1
  48. package/dist/views/metadata-admin/issuePath.d.ts +22 -0
  49. package/dist/views/metadata-admin/issuePath.js +65 -0
  50. package/dist/views/metadata-admin/package-scope.d.ts +26 -0
  51. package/dist/views/metadata-admin/package-scope.js +43 -0
  52. package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
  53. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +7 -1
  54. package/dist/views/metadata-admin/previews/FlowCanvas.js +104 -16
  55. package/dist/views/metadata-admin/previews/FlowPreview.js +31 -3
  56. package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
  57. package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
  58. package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
  59. package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
  60. package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
  61. package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
  62. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
  63. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +21 -6
  64. package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
  65. package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
  66. package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
  67. package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
  68. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +11 -0
  69. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
  70. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +72 -0
  71. package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
  72. package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
  73. package/package.json +38 -38
@@ -352,6 +352,15 @@ export function registerBuiltinAnchors() {
352
352
  createDerive: [
353
353
  { from: 'label', to: 'name', transform: 'slugify', untilUserEdits: true },
354
354
  ],
355
+ // A new action defaults to `type: 'script'` (ActionType.default), which the
356
+ // spec requires to carry an executable `body` or `target` — otherwise the
357
+ // draft fails validation on save (422) and AppPlugin registers no engine
358
+ // handler (the #2169 "Mark Done" runtime miss). Seed a no-op L2 body so
359
+ // "New action -> name -> Save" round-trips; the author edits the source after.
360
+ createDefaults: {
361
+ type: 'script',
362
+ body: { language: 'js', source: 'return { success: true };' },
363
+ },
355
364
  });
356
365
  // dashboard / report — surface ones bound to a specific object.
357
366
  // Many will not have an explicit anchor; those simply don't appear.
@@ -377,11 +386,20 @@ export function registerBuiltinAnchors() {
377
386
  groupLabel: 'Reports',
378
387
  order: 81,
379
388
  }],
380
- createFields: ['label', 'name', 'objectName', 'description'],
389
+ // ADR-0021 single-form: a report is dataset-bound — `objectName` and the
390
+ // legacy `columns` array were removed from ReportSchema, so seeding them
391
+ // here produced a stub that failed server validation ("a report needs
392
+ // `dataset` + `values`"). Report-create now lights up the canvas +
393
+ // ReportDefaultInspector (see CREATE_MODE_CANVAS_TYPES), where the author
394
+ // picks the dataset/measures/dimensions directly; we just seed a sensible
395
+ // starting type.
396
+ createFields: ['label', 'name', 'description'],
381
397
  createDerive: [
382
398
  { from: 'label', to: 'name', transform: 'slugify', untilUserEdits: true },
383
399
  ],
384
- createDefaults: { columns: [] },
400
+ // Seed `drilldown:true` so the inspector toggle reflects the schema default
401
+ // (otherwise it shows OFF on a fresh report while the spec default is true).
402
+ createDefaults: { type: 'summary', drilldown: true },
385
403
  });
386
404
  // Cosmetic defaults for the `object` type list page — gives Object the
387
405
  // same look as every other metadata type while still surfacing the
@@ -181,6 +181,7 @@ const ENGINE_STRINGS_EN = {
181
181
  'engine.list.warnCount': '{count} warning(s):',
182
182
  'engine.list.allSources': 'All sources',
183
183
  'engine.list.allPackages': 'All packages',
184
+ 'engine.package.local': 'Local / Custom (this env)',
184
185
  'engine.list.packageFilter': 'Package',
185
186
  'engine.list.source.artifact': 'Artifact',
186
187
  'engine.list.source.runtime': 'Runtime',
@@ -313,6 +314,7 @@ const ENGINE_STRINGS_EN = {
313
314
  'engine.inspector.flowEdge.source': 'From',
314
315
  'engine.inspector.flowEdge.target': 'To',
315
316
  'engine.inspector.flowEdge.routing': 'Routing',
317
+ 'engine.inspector.flowEdge.branch': 'Branch',
316
318
  'engine.inspector.flowEdge.label': 'Branch label',
317
319
  'engine.inspector.flowEdge.labelHint': 'e.g. approve / reject',
318
320
  'engine.inspector.flowEdge.condition': 'Condition',
@@ -320,6 +322,18 @@ const ENGINE_STRINGS_EN = {
320
322
  'engine.inspector.flowEdge.isDefault': 'Default branch (else)',
321
323
  'engine.inspector.flowEdge.hint': 'The engine follows this connection when its branch label is selected or its condition is met. The default branch is taken when no other matches.',
322
324
  'engine.inspector.flowEdge.remove': 'Remove connection',
325
+ 'engine.inspector.flowEdge.approvalBranch': 'Approval branch',
326
+ 'engine.inspector.flowEdge.branchApprove': 'Approve',
327
+ 'engine.inspector.flowEdge.branchReject': 'Reject',
328
+ 'engine.inspector.flowEdge.branchRevise': 'Revise — send back',
329
+ 'engine.inspector.flowEdge.branchCustom': '— Custom —',
330
+ 'engine.inspector.flowEdge.connection': 'Connection',
331
+ 'engine.inspector.flowEdge.type': 'Type',
332
+ 'engine.inspector.flowEdge.typeDefault': 'Normal',
333
+ 'engine.inspector.flowEdge.typeConditional': 'Conditional',
334
+ 'engine.inspector.flowEdge.typeFault': 'Fault (error path)',
335
+ 'engine.inspector.flowEdge.typeBack': 'Back-edge (revise loop)',
336
+ 'engine.inspector.flowEdge.backHint': 'A back-edge re-enters an earlier node to close a loop (e.g. an approval revise loop). It is traversed normally at run time but excluded from cycle validation.',
323
337
  // Workflow action inspector
324
338
  'engine.inspector.workflowAction.kind': 'Action',
325
339
  'engine.inspector.workflowAction.close': 'Close action',
@@ -378,6 +392,9 @@ const ENGINE_STRINGS_EN = {
378
392
  // Report default ("home") inspector
379
393
  'engine.inspector.report.kind': 'Report',
380
394
  'engine.inspector.report.close': 'Close report',
395
+ 'engine.inspector.report.name': 'Name',
396
+ 'engine.inspector.report.nameHint': 'snake_case identifier (cannot change after create)',
397
+ 'engine.inspector.report.namePlaceholder': 'e.g. pipeline_by_stage',
381
398
  'engine.inspector.report.label': 'Label',
382
399
  'engine.inspector.report.labelPlaceholder': 'e.g. Pipeline by Stage',
383
400
  'engine.inspector.report.type': 'Report type',
@@ -393,6 +410,12 @@ const ENGINE_STRINGS_EN = {
393
410
  'engine.inspector.report.rowsEmpty': 'No dimensions yet. Add one from the dataset below.',
394
411
  'engine.inspector.report.columnsAcross': 'Columns (across dimensions)',
395
412
  'engine.inspector.report.columnsAcrossEmpty': 'No across dimensions yet — the matrix pivots rows × columns.',
413
+ 'engine.inspector.report.chart': 'Chart',
414
+ 'engine.inspector.report.chartType': 'Chart type',
415
+ 'engine.inspector.report.chartNone': 'None (table only)',
416
+ 'engine.inspector.report.chartTitle': 'Chart title',
417
+ 'engine.inspector.report.chartX': 'X-Axis (dimension)',
418
+ 'engine.inspector.report.chartY': 'Y-Axis (measure)',
396
419
  'engine.inspector.report.noSchema': 'Spec schema unavailable — basic properties only.',
397
420
  // Trailing section for fields the live server has but the bundled spec lacks.
398
421
  'engine.inspector.moreFields': 'More fields',
@@ -423,7 +446,7 @@ const ENGINE_STRINGS_EN = {
423
446
  'engine.edit.overlay': 'overlay',
424
447
  'engine.edit.readOnlyBanner': 'Viewing in read-only mode. Click {edit} to make changes.',
425
448
  'engine.edit.readOnlyTypeBanner': 'This metadata type is read-only for safety: it ships executable code or sensitive configuration and cannot be overlaid at runtime. Edit the source in your package and redeploy. To override this lock (use with caution), set {flag} to include {type}, or flip {override} in the registry.',
426
- 'engine.edit.artifactLockedBanner': 'This item is shipped by a code package and cannot be modified at runtime. You can still create your own {type} from scratch and freely edit or delete it.',
449
+ 'engine.edit.artifactLockedBanner': 'This {type} is provided by an installed package, so it is read-only at runtime. To change it, edit it in its source package and republish or create a new {type} from scratch.',
427
450
  'engine.badge.createOnly': 'create-only',
428
451
  'engine.repeater.empty': 'No items. Click + to add.',
429
452
  'engine.badge.writable': 'writable',
@@ -698,6 +721,26 @@ const ENGINE_STRINGS_EN = {
698
721
  'designer.field.relationshipName': 'Relationship name',
699
722
  'designer.field.relationshipNameHint': 'Inverse collection key on the parent',
700
723
  'designer.field.objectNamePlaceholder': 'object_name',
724
+ // Lookup "Picker config" advanced sub-panel
725
+ 'designer.field.lookup.pickerConfig': 'Picker config',
726
+ 'designer.field.lookup.displayField': 'Display field',
727
+ 'designer.field.lookup.descriptionField': 'Description field',
728
+ 'designer.field.lookup.selectField': 'Select a field…',
729
+ 'designer.field.lookup.setTargetFirst': 'Set the target object first',
730
+ 'designer.field.lookup.searchFields': 'Search fields…',
731
+ 'designer.field.lookup.selectableRecords': 'Selectable records',
732
+ 'designer.field.lookup.addFilter': 'Add filter',
733
+ 'designer.field.lookup.noFilter': 'No filter — every {ref} record is selectable.',
734
+ 'designer.field.lookup.filterN': 'Filter {n}',
735
+ 'designer.field.lookup.removeFilter': 'Remove filter',
736
+ 'designer.field.lookup.filterField': 'Field',
737
+ 'designer.field.lookup.filterOperator': 'Operator',
738
+ 'designer.field.lookup.filterValue': 'Value',
739
+ 'designer.field.lookup.dependsOn': 'Depends on (same-record fields)',
740
+ 'designer.field.lookup.addDependsOn': 'Add a field this lookup depends on…',
741
+ 'designer.field.lookup.searchHostFields': "Search this object's fields…",
742
+ 'designer.field.lookup.pageSize': 'Picker page size',
743
+ 'designer.field.lookup.allowCreate': 'Allow quick-create',
701
744
  'designer.field.formula': 'Formula (CEL)',
702
745
  'designer.field.precision': 'Precision',
703
746
  'designer.field.scale': 'Scale',
@@ -824,6 +867,7 @@ const ENGINE_STRINGS_ZH = {
824
867
  'engine.list.warnCount': '{count} 个警告:',
825
868
  'engine.list.allSources': '全部来源',
826
869
  'engine.list.allPackages': '全部软件包',
870
+ 'engine.package.local': '本地 / 自定义(本环境)',
827
871
  'engine.list.packageFilter': '软件包',
828
872
  'engine.list.source.artifact': '代码包',
829
873
  'engine.list.source.runtime': '运行时',
@@ -956,6 +1000,7 @@ const ENGINE_STRINGS_ZH = {
956
1000
  'engine.inspector.flowEdge.source': '起点',
957
1001
  'engine.inspector.flowEdge.target': '终点',
958
1002
  'engine.inspector.flowEdge.routing': '路由',
1003
+ 'engine.inspector.flowEdge.branch': '分支',
959
1004
  'engine.inspector.flowEdge.label': '分支标签',
960
1005
  'engine.inspector.flowEdge.labelHint': '例如 approve / reject',
961
1006
  'engine.inspector.flowEdge.condition': '条件',
@@ -963,6 +1008,18 @@ const ENGINE_STRINGS_ZH = {
963
1008
  'engine.inspector.flowEdge.isDefault': '默认分支(else)',
964
1009
  'engine.inspector.flowEdge.hint': '当此连线的分支标签被选中、或其条件成立时,引擎会走这条连线。其余都不匹配时走默认分支。',
965
1010
  'engine.inspector.flowEdge.remove': '删除连线',
1011
+ 'engine.inspector.flowEdge.approvalBranch': '审批分支',
1012
+ 'engine.inspector.flowEdge.branchApprove': '通过',
1013
+ 'engine.inspector.flowEdge.branchReject': '驳回',
1014
+ 'engine.inspector.flowEdge.branchRevise': '退回修改',
1015
+ 'engine.inspector.flowEdge.branchCustom': '—— 自定义 ——',
1016
+ 'engine.inspector.flowEdge.connection': '连线',
1017
+ 'engine.inspector.flowEdge.type': '类型',
1018
+ 'engine.inspector.flowEdge.typeDefault': '普通',
1019
+ 'engine.inspector.flowEdge.typeConditional': '条件',
1020
+ 'engine.inspector.flowEdge.typeFault': '故障(错误路径)',
1021
+ 'engine.inspector.flowEdge.typeBack': '回边(退回修改环)',
1022
+ 'engine.inspector.flowEdge.backHint': '回边会重新进入更早的节点以闭合一个环(例如审批的退回修改环)。它在运行时正常通行,但不参与环路校验。',
966
1023
  // Workflow action inspector
967
1024
  'engine.inspector.workflowAction.kind': '动作',
968
1025
  'engine.inspector.workflowAction.close': '关闭动作',
@@ -1020,6 +1077,9 @@ const ENGINE_STRINGS_ZH = {
1020
1077
  // Report default ("home") inspector
1021
1078
  'engine.inspector.report.kind': '报表',
1022
1079
  'engine.inspector.report.close': '关闭报表',
1080
+ 'engine.inspector.report.name': '名称',
1081
+ 'engine.inspector.report.nameHint': 'snake_case 标识符(创建后不可修改)',
1082
+ 'engine.inspector.report.namePlaceholder': '例如:pipeline_by_stage',
1023
1083
  'engine.inspector.report.label': '显示名',
1024
1084
  'engine.inspector.report.labelPlaceholder': '例如:按阶段统计的销售管道',
1025
1085
  'engine.inspector.report.type': '报表类型',
@@ -1035,6 +1095,12 @@ const ENGINE_STRINGS_ZH = {
1035
1095
  'engine.inspector.report.rowsEmpty': '还没有维度。从下方数据集中添加。',
1036
1096
  'engine.inspector.report.columnsAcross': '列(横向维度)',
1037
1097
  'engine.inspector.report.columnsAcrossEmpty': '还没有横向维度——矩阵按 行 × 列 透视。',
1098
+ 'engine.inspector.report.chart': '图表',
1099
+ 'engine.inspector.report.chartType': '图表类型',
1100
+ 'engine.inspector.report.chartNone': '无(仅表格)',
1101
+ 'engine.inspector.report.chartTitle': '图表标题',
1102
+ 'engine.inspector.report.chartX': 'X 轴(维度)',
1103
+ 'engine.inspector.report.chartY': 'Y 轴(度量)',
1038
1104
  'engine.inspector.report.noSchema': '规格 schema 不可用 —— 仅显示基础属性。',
1039
1105
  // Trailing section for fields the live server has but the bundled spec lacks.
1040
1106
  'engine.inspector.moreFields': '更多字段',
@@ -1065,7 +1131,7 @@ const ENGINE_STRINGS_ZH = {
1065
1131
  'engine.edit.overlay': '覆盖',
1066
1132
  'engine.edit.readOnlyBanner': '当前为只读模式。点击 {edit} 进行修改。',
1067
1133
  'engine.edit.readOnlyTypeBanner': '出于安全考虑,此元数据类型为只读:它包含可执行代码或敏感配置,不允许在运行时通过覆盖层修改。请编辑包内源代码后重新部署。如需临时解除限制(请谨慎使用),可将 {flag} 设置为包含 {type},或在注册表中开启 {override}。',
1068
- 'engine.edit.artifactLockedBanner': '此条目由代码包提供,无法在运行时修改。但您可以新建自己的 {type},并自由地编辑或删除。',
1134
+ 'engine.edit.artifactLockedBanner': ' {type} 来自已安装的包,因此在运行时为只读。如需修改,请在其所属包的源中编辑并重新发布——或从零新建一个 {type}。',
1069
1135
  'engine.badge.createOnly': '仅可新建',
1070
1136
  'engine.repeater.empty': '暂无条目。点击 + 添加。',
1071
1137
  'engine.badge.writable': '可写',
@@ -1340,6 +1406,26 @@ const ENGINE_STRINGS_ZH = {
1340
1406
  'designer.field.relationshipName': '关系名称',
1341
1407
  'designer.field.relationshipNameHint': '父对象上的反向集合键',
1342
1408
  'designer.field.objectNamePlaceholder': 'object_name',
1409
+ // Lookup "Picker config" advanced sub-panel
1410
+ 'designer.field.lookup.pickerConfig': '选择器配置',
1411
+ 'designer.field.lookup.displayField': '显示字段',
1412
+ 'designer.field.lookup.descriptionField': '描述字段',
1413
+ 'designer.field.lookup.selectField': '选择字段…',
1414
+ 'designer.field.lookup.setTargetFirst': '请先设置目标对象',
1415
+ 'designer.field.lookup.searchFields': '搜索字段…',
1416
+ 'designer.field.lookup.selectableRecords': '可选记录范围',
1417
+ 'designer.field.lookup.addFilter': '添加筛选条件',
1418
+ 'designer.field.lookup.noFilter': '未设筛选 — 所有 {ref} 记录均可选。',
1419
+ 'designer.field.lookup.filterN': '筛选条件 {n}',
1420
+ 'designer.field.lookup.removeFilter': '移除筛选条件',
1421
+ 'designer.field.lookup.filterField': '字段',
1422
+ 'designer.field.lookup.filterOperator': '运算符',
1423
+ 'designer.field.lookup.filterValue': '值',
1424
+ 'designer.field.lookup.dependsOn': '依赖字段(同记录字段)',
1425
+ 'designer.field.lookup.addDependsOn': '添加该查找依赖的字段…',
1426
+ 'designer.field.lookup.searchHostFields': '搜索本对象的字段…',
1427
+ 'designer.field.lookup.pageSize': '选择器分页大小',
1428
+ 'designer.field.lookup.allowCreate': '允许快速创建',
1343
1429
  'designer.field.formula': '公式 (CEL)',
1344
1430
  'designer.field.precision': '精度',
1345
1431
  'designer.field.scale': '小数位',
@@ -6,7 +6,7 @@
6
6
  * - base `object`,
7
7
  * - `include` relationships (the join allowlist — D-C),
8
8
  * - `dimensions` (name + field/`relationship.field` + type + granularity), and
9
- * - `measures` (name + aggregate + field + certified + format/currency/derived).
9
+ * - `measures` (name + aggregate + field + format/currency/derived).
10
10
  *
11
11
  * The base object, the included relationships, and every `field` are picked
12
12
  * from the live object graph (a searchable combo over {@link useDatasetFieldCatalog})
@@ -18,4 +18,4 @@
18
18
  */
19
19
  import * as React from 'react';
20
20
  import type { MetadataDefaultInspectorProps } from '../default-inspector-registry';
21
- export declare function DatasetDefaultInspector({ draft, onPatch, readOnly }: MetadataDefaultInspectorProps): React.JSX.Element;
21
+ export declare function DatasetDefaultInspector({ draft, onPatch, readOnly, name }: MetadataDefaultInspectorProps): React.JSX.Element;
@@ -8,7 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  * - base `object`,
9
9
  * - `include` relationships (the join allowlist — D-C),
10
10
  * - `dimensions` (name + field/`relationship.field` + type + granularity), and
11
- * - `measures` (name + aggregate + field + certified + format/currency/derived).
11
+ * - `measures` (name + aggregate + field + format/currency/derived).
12
12
  *
13
13
  * The base object, the included relationships, and every `field` are picked
14
14
  * from the live object graph (a searchable combo over {@link useDatasetFieldCatalog})
@@ -19,10 +19,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
19
  * `onPatch`; the DatasetPreview on the canvas re-runs live as the draft changes.
20
20
  */
21
21
  import * as React from 'react';
22
- import { ArrowRight, Plus, Trash2, X } from 'lucide-react';
23
- import { Badge, Button, Label } from '@object-ui/components';
22
+ import { AlertTriangle, ArrowRight, ChevronDown, Plus, Trash2, X } from 'lucide-react';
23
+ import { Badge, Button, FilterBuilder, Label, Popover, PopoverContent, PopoverTrigger } from '@object-ui/components';
24
24
  import { InspectorShell, InspectorTextField, InspectorSelectField, InspectorCheckboxField, appendArray, spliceArray, } from './_shared';
25
25
  import { InspectorComboField } from './InspectorComboField';
26
+ import { toFieldName } from '../previews/object-fields-io';
27
+ import { formatMeasure } from '@object-ui/core';
28
+ import { conditionToGroup, groupToCondition } from './datasetFilterCondition';
26
29
  import { useObjectOptions, useDatasetFieldCatalog, useDatasetUsage, fieldTypeToDimensionType, } from './useDatasetFields';
27
30
  // Closed to what the dataset compiler supports (no array_agg/string_agg in v1).
28
31
  const AGGREGATE_OPTIONS = [
@@ -54,6 +57,30 @@ const DERIVED_OP_OPTIONS = [
54
57
  { value: 'difference', label: 'difference (a − b)' },
55
58
  { value: 'product', label: 'product (a × b)' },
56
59
  ];
60
+ // Display-format picker options — a business user shouldn't have to know numeral
61
+ // syntax (`$0,0.00`), so the inspector offers kind + decimals + currency and
62
+ // generates the `format`/`currency` strings.
63
+ const FORMAT_KIND_OPTIONS = [
64
+ { value: 'raw', label: 'Raw number' },
65
+ { value: 'number', label: 'Number — 1,234.5' },
66
+ { value: 'currency', label: 'Currency — $1,234.50' },
67
+ { value: 'percent', label: 'Percent — 12.3%' },
68
+ ];
69
+ const DECIMALS_OPTIONS = [
70
+ { value: '0', label: '0' },
71
+ { value: '1', label: '1' },
72
+ { value: '2', label: '2' },
73
+ ];
74
+ const CURRENCY_OPTIONS = [
75
+ { value: 'USD', label: 'USD ($)' },
76
+ { value: 'EUR', label: 'EUR (€)' },
77
+ { value: 'GBP', label: 'GBP (£)' },
78
+ { value: 'CNY', label: 'CNY (¥)' },
79
+ { value: 'JPY', label: 'JPY (¥)' },
80
+ { value: 'INR', label: 'INR (₹)' },
81
+ { value: 'CAD', label: 'CAD ($)' },
82
+ { value: 'AUD', label: 'AUD ($)' },
83
+ ];
57
84
  function SectionHeader({ title, count, onAdd, addLabel }) {
58
85
  return (_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: title }), _jsx(Badge, { variant: "outline", className: "text-[10px]", children: count })] }), onAdd && (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-6 gap-1 px-1.5 text-[11px]", onClick: onAdd, children: [_jsx(Plus, { className: "h-3 w-3" }), " ", addLabel] }))] }));
59
86
  }
@@ -61,7 +88,65 @@ function SectionHeader({ title, count, onAdd, addLabel }) {
61
88
  function Advanced({ children }) {
62
89
  return (_jsxs("details", { className: "group", children: [_jsx("summary", { className: "cursor-pointer select-none list-none text-[11px] text-muted-foreground hover:text-foreground", children: _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx(ArrowRight, { className: "h-3 w-3 transition-transform group-open:rotate-90" }), "Advanced"] }) }), _jsx("div", { className: "mt-1.5 space-y-1.5 border-l pl-2.5", children: children })] }));
63
90
  }
64
- export function DatasetDefaultInspector({ draft, onPatch, readOnly }) {
91
+ /** Best-effort parse of a stored measure format into the picker's {kind, decimals}. */
92
+ function parseMeasureFormat(format, currency) {
93
+ const f = (format ?? '').trim();
94
+ const m = f.match(/\.(0+)/);
95
+ const decimals = m ? Math.min(m[1].length, 2) : 0;
96
+ if (currency || /[$£€¥₹]/.test(f))
97
+ return { kind: 'currency', decimals };
98
+ if (f.includes('%'))
99
+ return { kind: 'percent', decimals };
100
+ if (f)
101
+ return { kind: 'number', decimals };
102
+ return { kind: 'raw', decimals: 0 };
103
+ }
104
+ /** Generate {format, currency} from the picker selection. */
105
+ function buildMeasureFormat(kind, decimals, currency) {
106
+ const dp = decimals > 0 ? '.' + '0'.repeat(decimals) : '';
107
+ switch (kind) {
108
+ case 'number': return { format: `0,0${dp}`, currency: undefined };
109
+ case 'currency': return { format: `0,0${dp}`, currency: currency || 'USD' };
110
+ case 'percent': return { format: `0${dp}%`, currency: undefined };
111
+ default: return { format: undefined, currency: undefined };
112
+ }
113
+ }
114
+ /**
115
+ * Structured display-format picker for a measure. Maps {kind, decimals, currency}
116
+ * ⇄ the spec's `format`/`currency` strings and shows a live sample so a business
117
+ * user never has to hand-write a numeral pattern.
118
+ */
119
+ function MeasureFormatField({ measure, onPatch, disabled }) {
120
+ const { kind, decimals } = parseMeasureFormat(measure.format, measure.currency);
121
+ const currency = measure.currency || 'USD';
122
+ const apply = (k, d, c) => onPatch(buildMeasureFormat(k, d, c));
123
+ const sample = formatMeasure(kind === 'percent' ? 0.1234 : 1234.5, measure.format, measure.currency);
124
+ return (_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "grid grid-cols-2 gap-1.5", children: [_jsx(InspectorSelectField, { label: "Display format", value: kind, options: FORMAT_KIND_OPTIONS, onCommit: (v) => apply(v, decimals, currency), disabled: disabled }), kind !== 'raw' && (_jsx(InspectorSelectField, { label: "Decimals", value: String(decimals), options: DECIMALS_OPTIONS, onCommit: (v) => apply(kind, parseInt(v, 10) || 0, currency), disabled: disabled }))] }), kind === 'currency' && (_jsx(InspectorSelectField, { label: "Currency", value: currency, options: CURRENCY_OPTIONS, onCommit: (v) => apply(kind, decimals, v), disabled: disabled })), kind !== 'raw' && (_jsxs("p", { className: "text-[10px] text-muted-foreground", children: ["Sample: ", _jsx("span", { className: "font-mono tabular-nums", children: sample })] }))] }));
125
+ }
126
+ /** The relationship prefix of a `relationship.field` path that isn't yet in `include`, else null. */
127
+ function missingRelationship(field, include) {
128
+ if (!field || !field.includes('.'))
129
+ return null;
130
+ const rel = field.split('.')[0];
131
+ return rel && !include.includes(rel) ? rel : null;
132
+ }
133
+ /** Inline author-time warning: a `relationship.field` whose join isn't declared in `include`. */
134
+ function RelWarning({ rel, onAdd, disabled }) {
135
+ return (_jsxs("p", { className: "flex items-center gap-1 text-[10px] text-amber-600 dark:text-amber-400", children: [_jsx(AlertTriangle, { className: "h-3 w-3 shrink-0" }), _jsxs("span", { children: ["Relationship ", _jsx("code", { className: "font-mono", children: rel }), " isn't in Included relationships."] }), !disabled && onAdd && (_jsx("button", { type: "button", className: "underline hover:no-underline", onClick: onAdd, children: "Add it" }))] }));
136
+ }
137
+ /**
138
+ * Visual filter editor for a dataset/measure `FilterCondition`. Wraps the shared
139
+ * {@link FilterBuilder} (a flat AND of `field op value` rows) and converts to/from
140
+ * the spec's Mongo-style `FilterCondition`. Filters it can't faithfully edit
141
+ * (nested / `$or` / multi-op) degrade to a "edit in Source" note rather than being
142
+ * silently rewritten. See {@link conditionToGroup} / {@link groupToCondition}.
143
+ */
144
+ function DatasetFilterField({ label, help, value, onCommit, fields, disabled }) {
145
+ const { group, representable } = conditionToGroup(value);
146
+ const count = group.conditions.length;
147
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), !representable ? (_jsxs("p", { className: "rounded-md border border-dashed bg-muted/30 px-2.5 py-1.5 text-[11px] text-muted-foreground", children: ["Advanced filter (nested / OR) \u2014 edit it in the ", _jsx("span", { className: "font-medium", children: "Source" }), " tab."] })) : (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: disabled, className: "h-8 w-full justify-between text-xs font-normal", children: [_jsx("span", { className: "truncate text-left", children: count ? `${count} condition${count === 1 ? '' : 's'}` : _jsx("span", { className: "text-muted-foreground", children: "+ Add filter\u2026" }) }), _jsx(ChevronDown, { className: "h-3.5 w-3.5 opacity-60 shrink-0" })] }) }), _jsx(PopoverContent, { align: "start", className: "w-[440px] max-w-[90vw] p-3", children: fields.length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground", children: "Pick a base object to add filter conditions." })) : (_jsx(FilterBuilder, { fields: fields, value: group, onChange: (g) => onCommit(groupToCondition(g)) })) })] })), help && _jsx("p", { className: "text-[10px] text-muted-foreground", children: help })] }));
148
+ }
149
+ export function DatasetDefaultInspector({ draft, onPatch, readOnly, name }) {
65
150
  const label = typeof draft.label === 'string' ? draft.label : '';
66
151
  const description = typeof draft.description === 'string' ? draft.description : '';
67
152
  const object = typeof draft.object === 'string' ? draft.object : '';
@@ -69,33 +154,62 @@ export function DatasetDefaultInspector({ draft, onPatch, readOnly }) {
69
154
  const dimensions = Array.isArray(draft.dimensions) ? draft.dimensions : [];
70
155
  const measures = Array.isArray(draft.measures) ? draft.measures : [];
71
156
  const datasetName = typeof draft.name === 'string' ? draft.name : undefined;
157
+ // In create mode the host passes an empty `name` (the PK is assigned on first
158
+ // save). Mirror ReportDefaultInspector: expose an editable Name that auto-
159
+ // derives a snake_case slug from the label until the author edits it directly,
160
+ // so a dataset created through the canvas saves with a valid identifier instead
161
+ // of dead-ending on the empty-name identity rule.
162
+ const createMode = !name;
163
+ const nameTouched = React.useRef(false);
164
+ const nameValue = typeof draft.name === 'string' ? draft.name : '';
72
165
  const { options: objectOptions, loading: objectsLoading } = useObjectOptions();
73
166
  const { relationships, fieldOptions, loading: catalogLoading } = useDatasetFieldCatalog(object, include);
74
167
  const usage = useDatasetUsage(datasetName);
75
168
  const objectComboOptions = React.useMemo(() => objectOptions.map((o) => ({ value: o.name, label: o.label })), [objectOptions]);
76
169
  const relationshipComboOptions = React.useMemo(() => relationships.map((r) => ({ value: r.name, label: r.label, hint: r.referenceTo ? `→ ${r.referenceTo}` : undefined })), [relationships]);
77
170
  const fieldComboOptions = React.useMemo(() => fieldOptions.map((f) => ({ value: f.value, label: f.label, hint: f.type, group: f.group })), [fieldOptions]);
171
+ // Base-object fields for the filter builders (scope + measure filters operate on
172
+ // the base table; relationship-path filters are out of scope for v1).
173
+ const filterFields = React.useMemo(() => fieldOptions.filter((f) => !f.value.includes('.')).map((f) => ({ value: f.value, label: f.label, type: f.type })), [fieldOptions]);
174
+ const datasetFilter = draft.filter && typeof draft.filter === 'object' ? draft.filter : undefined;
78
175
  const baseLabel = objectComboOptions.find((o) => o.value === object)?.label ?? object;
79
176
  const patchDimension = (i, patch) => onPatch({ dimensions: dimensions.map((d, idx) => (idx === i ? { ...d, ...patch } : d)) });
80
177
  const patchMeasure = (i, patch) => onPatch({ measures: measures.map((m, idx) => (idx === i ? { ...m, ...patch } : m)) });
81
178
  // Picking a field auto-infers the dimension type from the field's framework
82
179
  // type (region:string, close_date:date, …) — the BI "pick field, type follows"
83
180
  // convention — while leaving the Type select free to override.
181
+ const leafName = (path) => (path.includes('.') ? path.split('.').pop() ?? path : path);
84
182
  const pickDimensionField = (i, v) => {
85
183
  const opt = fieldOptions.find((o) => o.value === v);
86
- patchDimension(i, opt?.type ? { field: v, type: fieldTypeToDimensionType(opt.type) } : { field: v });
184
+ const patch = opt?.type ? { field: v, type: fieldTypeToDimensionType(opt.type) } : { field: v };
185
+ if (!dimensions[i]?.name)
186
+ patch.name = leafName(v); // auto-name from field when unnamed
187
+ patchDimension(i, patch);
188
+ };
189
+ const pickMeasureField = (i, v) => {
190
+ const patch = { field: v };
191
+ if (!measures[i]?.name)
192
+ patch.name = leafName(v); // auto-name from field when unnamed
193
+ patchMeasure(i, patch);
87
194
  };
88
195
  return (_jsxs(InspectorShell, { kindLabel: "Dataset", title: String(label || draft.name || 'Dataset'), onClose: () => { }, hideClose: true, children: [datasetName && !usage.loading && (_jsx("p", { className: usage.reports + usage.dashboards > 0
89
196
  ? 'rounded-md border border-amber-500/30 bg-amber-500/5 px-2.5 py-1.5 text-[11px] text-amber-700 dark:text-amber-300'
90
197
  : 'text-[11px] text-muted-foreground', children: usage.reports + usage.dashboards > 0
91
198
  ? `Bound by ${usage.reports} report${usage.reports === 1 ? '' : 's'} · ${usage.dashboards} dashboard${usage.dashboards === 1 ? '' : 's'} — changes affect them.`
92
- : 'Not yet bound by any report or dashboard.' })), _jsx(InspectorTextField, { label: "Label", value: label, onCommit: (v) => onPatch({ label: v }), disabled: readOnly }), _jsx(InspectorTextField, { label: "Description", value: description, onCommit: (v) => onPatch({ description: v }), disabled: readOnly }), _jsx(InspectorComboField, { label: "Base object", value: object, onCommit: (v) => onPatch({ object: v }), options: objectComboOptions, loading: objectsLoading, placeholder: "Select an object\u2026", searchPlaceholder: "Search objects\u2026", disabled: readOnly, mono: true }), _jsxs("div", { className: "border-t pt-3 space-y-1.5", children: [_jsx(SectionHeader, { title: "Included relationships", count: include.length, addLabel: "Add", onAdd: readOnly ? undefined : () => onPatch({ include: appendArray(include, '') }) }), include.length === 0 ? (_jsxs("p", { className: "rounded-md border border-dashed bg-muted/30 px-3 py-2 text-center text-[11px] text-muted-foreground", children: ["No joins. Add a relationship (a lookup field on ", _jsx("code", { children: baseLabel || 'the base object' }), ") to use ", _jsx("code", { children: "relationship.field" }), " dimensions/measures."] })) : (include.map((rel, i) => (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(InspectorComboField, { value: rel, onCommit: (v) => onPatch({ include: include.map((r, idx) => (idx === i ? v : r)) }), options: relationshipComboOptions, loading: catalogLoading, placeholder: "Select a relationship\u2026", searchPlaceholder: "Search relationships\u2026", disabled: readOnly, mono: true }), !readOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 shrink-0 p-0", onClick: () => onPatch({ include: spliceArray(include, i, null) }), "aria-label": "Remove relationship", children: _jsx(X, { className: "h-3.5 w-3.5" }) }))] }, i)))), object && include.length > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-x-1 gap-y-0.5 pt-0.5 text-[10px] text-muted-foreground", children: [_jsx("span", { className: "font-mono font-medium", children: baseLabel }), include.map((rel, i) => {
199
+ : 'Not yet bound by any report or dashboard.' })), createMode && (_jsx(InspectorTextField, { label: "Name", value: nameValue, onCommit: (v) => { nameTouched.current = true; onPatch({ name: toFieldName(v) }); }, placeholder: "snake_case identifier", disabled: readOnly, mono: true })), _jsx(InspectorTextField, { label: "Label", value: label, onCommit: (v) => {
200
+ // Live-derive the snake_case name from the label until the author edits
201
+ // the Name field directly (create mode only).
202
+ const patch = { label: v };
203
+ if (createMode && !nameTouched.current)
204
+ patch.name = toFieldName(v);
205
+ onPatch(patch);
206
+ }, disabled: readOnly }), _jsx(InspectorTextField, { label: "Description", value: description, onCommit: (v) => onPatch({ description: v }), disabled: readOnly }), _jsx(InspectorComboField, { label: "Base object", value: object, onCommit: (v) => onPatch({ object: v }), options: objectComboOptions, loading: objectsLoading, placeholder: "Select an object\u2026", searchPlaceholder: "Search objects\u2026", disabled: readOnly, mono: true }), _jsxs("div", { className: "border-t pt-3 space-y-1.5", children: [_jsx(SectionHeader, { title: "Included relationships", count: include.length, addLabel: "Add", onAdd: readOnly ? undefined : () => onPatch({ include: appendArray(include, '') }) }), include.length === 0 ? (_jsxs("p", { className: "rounded-md border border-dashed bg-muted/30 px-3 py-2 text-center text-[11px] text-muted-foreground", children: ["No joins. Add a relationship (a lookup field on ", _jsx("code", { children: baseLabel || 'the base object' }), ") to use ", _jsx("code", { children: "relationship.field" }), " dimensions/measures."] })) : (include.map((rel, i) => (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(InspectorComboField, { value: rel, onCommit: (v) => onPatch({ include: include.map((r, idx) => (idx === i ? v : r)) }), options: relationshipComboOptions, loading: catalogLoading, placeholder: "Select a relationship\u2026", searchPlaceholder: "Search relationships\u2026", disabled: readOnly, mono: true }), !readOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 shrink-0 p-0", onClick: () => onPatch({ include: spliceArray(include, i, null) }), "aria-label": "Remove relationship", children: _jsx(X, { className: "h-3.5 w-3.5" }) }))] }, i)))), object && include.length > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-x-1 gap-y-0.5 pt-0.5 text-[10px] text-muted-foreground", children: [_jsx("span", { className: "font-mono font-medium", children: baseLabel }), include.map((rel, i) => {
93
207
  const r = relationships.find((x) => x.name === rel);
94
208
  return (_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx(ArrowRight, { className: "h-3 w-3 opacity-60" }), _jsxs("span", { className: "font-mono", children: [rel, r?.referenceTo ? ` (${r.referenceTo})` : ''] })] }, i));
95
- })] }))] }), _jsxs("div", { className: "border-t pt-3 space-y-2", children: [_jsx(SectionHeader, { title: "Dimensions", count: dimensions.length, addLabel: "Add dimension", onAdd: readOnly ? undefined : () => onPatch({ dimensions: appendArray(dimensions, { name: '', field: '', type: 'string' }) }) }), dimensions.map((d, i) => (_jsxs("div", { className: "rounded-md border p-2 space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-[11px] font-medium text-muted-foreground", children: ["Dimension ", i + 1] }), !readOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Remove dimension", title: "Remove dimension", className: "h-6 w-6 shrink-0 p-0 text-muted-foreground hover:text-destructive", onClick: () => onPatch({ dimensions: spliceArray(dimensions, i, null) }), children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }), _jsx(InspectorTextField, { label: "Name", value: d.name ?? '', onCommit: (v) => patchDimension(i, { name: v }), placeholder: "e.g. region", disabled: readOnly, mono: true }), _jsx(InspectorComboField, { label: "Field", value: d.field ?? '', onCommit: (v) => pickDimensionField(i, v), options: fieldComboOptions, loading: catalogLoading, placeholder: "field or relationship.field", searchPlaceholder: "Search fields\u2026", disabled: readOnly, mono: true }), _jsx(InspectorSelectField, { label: "Type", value: d.type, options: DIMENSION_TYPE_OPTIONS, onCommit: (v) => patchDimension(i, { type: v }), disabled: readOnly }), _jsxs(Advanced, { children: [_jsx(InspectorTextField, { label: "Label (optional)", value: d.label ?? '', onCommit: (v) => patchDimension(i, { label: v || undefined }), placeholder: d.name || 'Display label', disabled: readOnly }), d.type === 'date' && (_jsx(InspectorSelectField, { label: "Date bucket", value: d.dateGranularity ?? '', options: DATE_GRANULARITY_OPTIONS, onCommit: (v) => patchDimension(i, { dateGranularity: v || undefined }), disabled: readOnly }))] })] }, i)))] }), _jsxs("div", { className: "border-t pt-3 space-y-2", children: [_jsx(SectionHeader, { title: "Measures", count: measures.length, addLabel: "Add measure", onAdd: readOnly ? undefined : () => onPatch({ measures: appendArray(measures, { name: '', aggregate: 'sum', field: '', certified: false }) }) }), measures.map((m, i) => {
209
+ })] }))] }), _jsx("div", { className: "border-t pt-3", children: _jsx(DatasetFilterField, { label: "Scope filter", help: "Intrinsic scope, ANDed into every query (e.g. exclude soft-deleted records).", value: datasetFilter, onCommit: (fc) => onPatch({ filter: fc }), fields: filterFields, disabled: readOnly }) }), _jsxs("div", { className: "border-t pt-3 space-y-2", children: [_jsx(SectionHeader, { title: "Dimensions", count: dimensions.length, addLabel: "Add dimension", onAdd: readOnly ? undefined : () => onPatch({ dimensions: appendArray(dimensions, { name: '', field: '', type: 'string' }) }) }), dimensions.map((d, i) => (_jsxs("div", { className: "rounded-md border p-2 space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-[11px] font-medium text-muted-foreground", children: ["Dimension ", i + 1] }), !readOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Remove dimension", title: "Remove dimension", className: "h-6 w-6 shrink-0 p-0 text-muted-foreground hover:text-destructive", onClick: () => onPatch({ dimensions: spliceArray(dimensions, i, null) }), children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }), _jsx(InspectorTextField, { label: "Name", value: d.name ?? '', onCommit: (v) => patchDimension(i, { name: v }), placeholder: "e.g. region", disabled: readOnly, mono: true }), _jsx(InspectorComboField, { label: "Field", value: d.field ?? '', onCommit: (v) => pickDimensionField(i, v), options: fieldComboOptions, loading: catalogLoading, placeholder: "field or relationship.field", searchPlaceholder: "Search fields\u2026", disabled: readOnly, mono: true }), (() => { const rel = missingRelationship(d.field, include); return rel ? _jsx(RelWarning, { rel: rel, disabled: readOnly, onAdd: () => onPatch({ include: appendArray(include, rel) }) }) : null; })(), _jsx(InspectorSelectField, { label: "Type", value: d.type, options: DIMENSION_TYPE_OPTIONS, onCommit: (v) => patchDimension(i, { type: v }), disabled: readOnly }), _jsxs(Advanced, { children: [_jsx(InspectorTextField, { label: "Label (optional)", value: d.label ?? '', onCommit: (v) => patchDimension(i, { label: v || undefined }), placeholder: d.name || 'Display label', disabled: readOnly }), d.type === 'date' && (_jsx(InspectorSelectField, { label: "Date bucket", value: d.dateGranularity ?? '', options: DATE_GRANULARITY_OPTIONS, onCommit: (v) => patchDimension(i, { dateGranularity: v || undefined }), disabled: readOnly }))] })] }, i)))] }), _jsxs("div", { className: "border-t pt-3 space-y-2", children: [_jsx(SectionHeader, { title: "Measures", count: measures.length, addLabel: "Add measure", onAdd: readOnly ? undefined : () => onPatch({ measures: appendArray(measures, { name: '', aggregate: 'sum', field: '' }) }) }), measures.map((m, i) => {
96
210
  const otherMeasures = measures.filter((_, idx) => idx !== i).map((x) => x.name).filter((n) => !!n);
97
211
  const derived = m.derived;
98
- return (_jsxs("div", { className: "rounded-md border p-2 space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-[11px] font-medium text-muted-foreground", children: ["Measure ", i + 1] }), !readOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Remove measure", title: "Remove measure", className: "h-6 w-6 shrink-0 p-0 text-muted-foreground hover:text-destructive", onClick: () => onPatch({ measures: spliceArray(measures, i, null) }), children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }), _jsx(InspectorTextField, { label: "Name", value: m.name ?? '', onCommit: (v) => patchMeasure(i, { name: v }), placeholder: "e.g. revenue", disabled: readOnly, mono: true }), _jsx(InspectorSelectField, { label: "Aggregate", value: m.aggregate, options: AGGREGATE_OPTIONS, onCommit: (v) => patchMeasure(i, { aggregate: v }), disabled: readOnly }), _jsx(InspectorComboField, { label: "Field", value: m.field ?? '', onCommit: (v) => patchMeasure(i, { field: v }), options: fieldComboOptions, loading: catalogLoading, placeholder: "field (optional for count)", searchPlaceholder: "Search fields\u2026", disabled: readOnly, mono: true }), _jsx(InspectorCheckboxField, { label: "Certified", value: !!m.certified, onCommit: (v) => patchMeasure(i, { certified: v }), disabled: readOnly }), _jsxs(Advanced, { children: [_jsx(InspectorTextField, { label: "Label (optional)", value: m.label ?? '', onCommit: (v) => patchMeasure(i, { label: v || undefined }), placeholder: m.name || 'Display label', disabled: readOnly }), _jsxs("div", { className: "grid grid-cols-2 gap-1.5", children: [_jsx(InspectorTextField, { label: "Format", value: m.format ?? '', onCommit: (v) => patchMeasure(i, { format: v || undefined }), placeholder: "$0,0.00", disabled: readOnly, mono: true }), _jsx(InspectorTextField, { label: "Currency", value: m.currency ?? '', onCommit: (v) => patchMeasure(i, { currency: v ? v.toUpperCase().slice(0, 3) : undefined }), placeholder: "USD", disabled: readOnly, mono: true })] }), _jsx(InspectorCheckboxField, { label: "Derived \u2014 computed from other measures", value: !!derived, onCommit: (v) => patchMeasure(i, { derived: v ? { op: 'ratio', of: [] } : undefined }), disabled: readOnly }), derived && (_jsxs("div", { className: "space-y-1.5 rounded-md border border-dashed p-2", children: [_jsx(InspectorSelectField, { label: "Operation", value: derived.op, options: DERIVED_OP_OPTIONS, onCommit: (v) => patchMeasure(i, { derived: { ...derived, op: v } }), disabled: readOnly }), _jsx(Label, { className: "text-xs text-muted-foreground", children: "Operands (other measures)" }), otherMeasures.length === 0 ? (_jsx("p", { className: "text-[11px] italic text-muted-foreground", children: "Add other measures first." })) : (_jsx("div", { className: "space-y-1", children: otherMeasures.map((nm) => {
212
+ return (_jsxs("div", { className: "rounded-md border p-2 space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-[11px] font-medium text-muted-foreground", children: ["Measure ", i + 1] }), !readOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Remove measure", title: "Remove measure", className: "h-6 w-6 shrink-0 p-0 text-muted-foreground hover:text-destructive", onClick: () => onPatch({ measures: spliceArray(measures, i, null) }), children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }), _jsx(InspectorTextField, { label: "Name", value: m.name ?? '', onCommit: (v) => patchMeasure(i, { name: v }), placeholder: "e.g. revenue", disabled: readOnly, mono: true }), _jsx(InspectorSelectField, { label: "Aggregate", value: m.aggregate, options: AGGREGATE_OPTIONS, onCommit: (v) => patchMeasure(i, { aggregate: v }), disabled: readOnly }), _jsx(InspectorComboField, { label: "Field", value: m.field ?? '', onCommit: (v) => pickMeasureField(i, v), options: fieldComboOptions, loading: catalogLoading, placeholder: "field (optional for count)", searchPlaceholder: "Search fields\u2026", disabled: readOnly, mono: true }), (() => { const rel = missingRelationship(m.field, include); return rel ? _jsx(RelWarning, { rel: rel, disabled: readOnly, onAdd: () => onPatch({ include: appendArray(include, rel) }) }) : null; })(), _jsxs(Advanced, { children: [_jsx(InspectorTextField, { label: "Label (optional)", value: m.label ?? '', onCommit: (v) => patchMeasure(i, { label: v || undefined }), placeholder: m.name || 'Display label', disabled: readOnly }), _jsx(MeasureFormatField, { measure: m, onPatch: (pp) => patchMeasure(i, pp), disabled: readOnly }), _jsx(DatasetFilterField, { label: "Filter (measure-scoped)", help: "Only rows matching this filter feed this measure (e.g. won_amount = sum(amount) where stage = won).", value: m.filter, onCommit: (fc) => patchMeasure(i, { filter: fc }), fields: filterFields, disabled: readOnly }), _jsx(InspectorCheckboxField, { label: "Derived \u2014 computed from other measures", value: !!derived, onCommit: (v) => patchMeasure(i, { derived: v ? { op: 'ratio', of: [] } : undefined }), disabled: readOnly }), derived && (_jsxs("div", { className: "space-y-1.5 rounded-md border border-dashed p-2", children: [_jsx(InspectorSelectField, { label: "Operation", value: derived.op, options: DERIVED_OP_OPTIONS, onCommit: (v) => patchMeasure(i, { derived: { ...derived, op: v } }), disabled: readOnly }), _jsx(Label, { className: "text-xs text-muted-foreground", children: "Operands (other measures)" }), (() => { const need = derived.op === 'ratio' || derived.op === 'difference' ? 2 : 1; const have = Array.isArray(derived.of) ? derived.of.length : 0; return have < need ? _jsxs("p", { className: "text-[10px] text-amber-600 dark:text-amber-400", children: ["Select ", need === 2 ? 'exactly 2 measures' : 'at least 1 measure', " for ", derived.op, "."] }) : null; })(), otherMeasures.length === 0 ? (_jsx("p", { className: "text-[11px] italic text-muted-foreground", children: "Add other measures first." })) : (_jsx("div", { className: "space-y-1", children: otherMeasures.map((nm) => {
99
213
  const checked = Array.isArray(derived.of) && derived.of.includes(nm);
100
214
  return (_jsx(InspectorCheckboxField, { label: nm, value: checked, disabled: readOnly, onCommit: (v) => {
101
215
  const current = Array.isArray(derived.of) ? derived.of : [];
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { t } from '../i18n';
3
- import { InspectorShell, InspectorTextField, InspectorCheckboxField, InspectorRemoveButton, InspectorEmptyState, spliceArray, } from './_shared';
3
+ import { InspectorShell, InspectorTextField, InspectorSelectField, InspectorCheckboxField, InspectorRemoveButton, InspectorEmptyState, spliceArray, } from './_shared';
4
4
  import { Label } from '@object-ui/components';
5
5
  import { edgeKey, conditionText } from '../previews/flow-canvas-layout';
6
6
  import { validateExpressionClient } from './expression-validate';
@@ -27,13 +27,89 @@ export function FlowEdgeInspector({ selection, draft, onPatch, onClearSelection,
27
27
  if (v === undefined || v === '' || v === false)
28
28
  delete next[k];
29
29
  }
30
+ // `type` defaults to 'default' (FlowEdgeSchema) — don't persist the noise so
31
+ // a normal edge stays `{ source, target }`; only `back`/`fault`/`conditional`
32
+ // are written.
33
+ if (next.type === 'default' || next.type === '' || next.type === undefined)
34
+ delete next.type;
30
35
  onPatch({ edges: spliceArray(edges, index, next) });
31
36
  };
32
37
  const isDefault = edge.isDefault === true;
38
+ // Decision out-edges can bind EXPLICITLY to one of the source decision's
39
+ // branches (vs the implicit by-order auto-wire): picking a branch writes its
40
+ // expression / label (or marks the default) onto this edge, so routing stays
41
+ // correct even when edges are connected out of branch order.
42
+ const nodes = Array.isArray(draft.nodes)
43
+ ? (draft.nodes)
44
+ : [];
45
+ const sourceNode = nodes.find((n) => n.id === edge.source);
46
+ const branches = sourceNode?.type === 'decision' &&
47
+ Array.isArray(sourceNode.config?.conditions)
48
+ ? (sourceNode.config.conditions)
49
+ : [];
50
+ const branchExpr = (b) => (typeof b.expression === 'string' ? b.expression.trim() : '');
51
+ const branchName = (b) => (typeof b.label === 'string' ? b.label.trim() : '');
52
+ // Which branch this edge currently represents: the default edge maps to the
53
+ // `true`/empty branch; otherwise match by condition, then by label. '' = custom.
54
+ const selectedBranch = (() => {
55
+ if (!branches.length)
56
+ return '';
57
+ if (isDefault) {
58
+ const i = branches.findIndex((b) => { const e = branchExpr(b); return e === '' || e === 'true'; });
59
+ return i >= 0 ? String(i) : '';
60
+ }
61
+ const cond = conditionText(edge.condition);
62
+ let i = cond ? branches.findIndex((b) => branchExpr(b) === cond) : -1;
63
+ if (i < 0 && edge.label)
64
+ i = branches.findIndex((b) => branchName(b) === edge.label);
65
+ return i >= 0 ? String(i) : '';
66
+ })();
67
+ const applyBranch = (key) => {
68
+ if (key === '')
69
+ return; // keep current custom values
70
+ const b = branches[Number(key)];
71
+ if (!b)
72
+ return;
73
+ const expr = branchExpr(b);
74
+ const lbl = branchName(b) || undefined;
75
+ if (expr === '' || expr === 'true')
76
+ patchEdge({ isDefault: true, condition: undefined, label: lbl });
77
+ else
78
+ patchEdge({ isDefault: false, condition: expr, label: lbl });
79
+ };
80
+ // Approval out-edges (ADR-0019/0044) route by branch *label*: the engine
81
+ // resumes down the out-edge whose label matches the decision — `approve` /
82
+ // `reject`, or `revise` (ADR-0044 send-back-for-revision). Offer those as a
83
+ // picker (mirrors APPROVAL_BRANCH_LABELS in @objectstack/spec) so the author
84
+ // need not recall the exact keyword; a free-text label is still allowed.
85
+ const isApprovalSource = sourceNode?.type === 'approval';
86
+ const APPROVAL_BRANCHES = ['approve', 'reject', 'revise'];
87
+ const currentApprovalBranch = (() => {
88
+ const l = (edge.label ?? '').trim().toLowerCase();
89
+ return APPROVAL_BRANCHES.includes(l) ? l : '';
90
+ })();
91
+ const edgeType = (typeof edge.type === 'string' && edge.type) || 'default';
33
92
  return (_jsxs(InspectorShell, { kindLabel: t('engine.inspector.flowEdge.kind', locale), title: selection.label ?? `${edge.source} → ${edge.target}`, onClose: onClearSelection, closeLabel: t('engine.inspector.flowEdge.close', locale), footer: _jsx(InspectorRemoveButton, { label: t('engine.inspector.flowEdge.remove', locale), onClick: () => {
34
93
  onPatch({ edges: spliceArray(edges, index, null) });
35
94
  onClearSelection();
36
- }, disabled: readOnly }), children: [_jsx(EndpointRow, { label: t('engine.inspector.flowEdge.source', locale), value: edge.source }), _jsx(EndpointRow, { label: t('engine.inspector.flowEdge.target', locale), value: edge.target }), _jsxs("div", { className: "flex items-center gap-2 pt-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t('engine.inspector.flowEdge.routing', locale) }), _jsx("span", { className: "h-px flex-1 bg-border", "aria-hidden": true })] }), _jsx(InspectorTextField, { label: t('engine.inspector.flowEdge.label', locale), value: edge.label ?? '', onCommit: (v) => patchEdge({ label: v }), placeholder: t('engine.inspector.flowEdge.labelHint', locale), disabled: readOnly || isDefault }), _jsx(InspectorTextField, { label: t('engine.inspector.flowEdge.condition', locale), value: conditionText(edge.condition) ?? '', onCommit: (v) => patchEdge({ condition: v || undefined }), placeholder: t('engine.inspector.flowEdge.conditionHint', locale), disabled: readOnly || isDefault, mono: true }), (() => {
95
+ }, disabled: readOnly }), children: [_jsx(EndpointRow, { label: t('engine.inspector.flowEdge.source', locale), value: edge.source }), _jsx(EndpointRow, { label: t('engine.inspector.flowEdge.target', locale), value: edge.target }), _jsxs("div", { className: "flex items-center gap-2 pt-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t('engine.inspector.flowEdge.routing', locale) }), _jsx("span", { className: "h-px flex-1 bg-border", "aria-hidden": true })] }), branches.length > 0 && (_jsx(InspectorSelectField, { label: t('engine.inspector.flowEdge.branch', locale), value: selectedBranch, options: [
96
+ ...branches.map((b, i) => {
97
+ const expr = branchExpr(b);
98
+ const nm = branchName(b) || `Branch ${i + 1}`;
99
+ const suffix = expr === '' || expr === 'true' ? ' \u00b7 default' : ` \u00b7 ${expr}`;
100
+ return { value: String(i), label: `${nm}${suffix}` };
101
+ }),
102
+ { value: '', label: '\u2014 Custom \u2014' },
103
+ ], onCommit: applyBranch, disabled: readOnly })), isApprovalSource && (_jsx(InspectorSelectField, { label: t('engine.inspector.flowEdge.approvalBranch', locale), value: currentApprovalBranch, options: [
104
+ { value: 'approve', label: t('engine.inspector.flowEdge.branchApprove', locale) },
105
+ { value: 'reject', label: t('engine.inspector.flowEdge.branchReject', locale) },
106
+ { value: 'revise', label: t('engine.inspector.flowEdge.branchRevise', locale) },
107
+ { value: '', label: t('engine.inspector.flowEdge.branchCustom', locale) },
108
+ ],
109
+ // Picking a branch writes the matching label; "Custom" keeps the
110
+ // free-text label the author typed below.
111
+ onCommit: (v) => { if (v)
112
+ patchEdge({ label: v }); }, disabled: readOnly })), _jsx(InspectorTextField, { label: t('engine.inspector.flowEdge.label', locale), value: edge.label ?? '', onCommit: (v) => patchEdge({ label: v }), placeholder: t('engine.inspector.flowEdge.labelHint', locale), disabled: readOnly || isDefault }), _jsx(InspectorTextField, { label: t('engine.inspector.flowEdge.condition', locale), value: conditionText(edge.condition) ?? '', onCommit: (v) => patchEdge({ condition: v || undefined }), placeholder: t('engine.inspector.flowEdge.conditionHint', locale), disabled: readOnly || isDefault, mono: true }), (() => {
37
113
  // ADR-0032 — flag a malformed edge guard (e.g. `{record.x}` brace-in-CEL)
38
114
  // inline, with the same corrective message as build/agent validation.
39
115
  const issue = isDefault ? null : validateExpressionClient('predicate', edge.condition);
@@ -41,5 +117,10 @@ export function FlowEdgeInspector({ selection, draft, onPatch, onClearSelection,
41
117
  })(), _jsx(InspectorCheckboxField, { label: t('engine.inspector.flowEdge.isDefault', locale), value: isDefault,
42
118
  // The default ("else") branch is taken when no other guard matches, so
43
119
  // it carries neither a condition nor a branch label — clear both.
44
- onCommit: (v) => patchEdge(v ? { isDefault: true, condition: undefined, label: undefined } : { isDefault: false }), disabled: readOnly }), _jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: t('engine.inspector.flowEdge.hint', locale) })] }));
120
+ onCommit: (v) => patchEdge(v ? { isDefault: true, condition: undefined, label: undefined } : { isDefault: false }), disabled: readOnly }), _jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: t('engine.inspector.flowEdge.hint', locale) }), _jsxs("div", { className: "flex items-center gap-2 pt-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t('engine.inspector.flowEdge.connection', locale) }), _jsx("span", { className: "h-px flex-1 bg-border", "aria-hidden": true })] }), _jsx(InspectorSelectField, { label: t('engine.inspector.flowEdge.type', locale), value: edgeType, options: [
121
+ { value: 'default', label: t('engine.inspector.flowEdge.typeDefault', locale) },
122
+ { value: 'conditional', label: t('engine.inspector.flowEdge.typeConditional', locale) },
123
+ { value: 'fault', label: t('engine.inspector.flowEdge.typeFault', locale) },
124
+ { value: 'back', label: t('engine.inspector.flowEdge.typeBack', locale) },
125
+ ], onCommit: (v) => patchEdge({ type: v }), disabled: readOnly }), edge.type === 'back' && (_jsx("p", { className: "text-[11px] leading-snug text-amber-600 dark:text-amber-400", role: "note", children: t('engine.inspector.flowEdge.backHint', locale) }))] }));
45
126
  }