@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.
- package/CHANGELOG.md +281 -0
- package/dist/console/AppContent.js +14 -2
- package/dist/console/ai/AiChatPage.js +11 -7
- package/dist/console/ai/LiveCanvas.d.ts +8 -2
- package/dist/console/ai/LiveCanvas.js +6 -4
- package/dist/hooks/useChatConversation.d.ts +30 -0
- package/dist/hooks/useChatConversation.js +63 -0
- package/dist/hooks/useConsoleActionRuntime.js +6 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
- package/dist/layout/ConsoleFloatingChatbot.js +25 -8
- package/dist/layout/ContextSelectors.js +59 -35
- package/dist/layout/agentPicker.d.ts +56 -0
- package/dist/layout/agentPicker.js +40 -0
- package/dist/preview/CommitTimeline.d.ts +15 -0
- package/dist/preview/CommitTimeline.js +82 -0
- package/dist/preview/UnpublishedAppBar.js +11 -7
- package/dist/preview/commitHistory.d.ts +28 -0
- package/dist/preview/commitHistory.js +48 -0
- package/dist/providers/MetadataProvider.js +9 -0
- package/dist/views/FlowRunner.d.ts +2 -30
- package/dist/views/FlowRunner.js +18 -50
- package/dist/views/ScreenView.d.ts +70 -0
- package/dist/views/ScreenView.js +73 -0
- package/dist/views/metadata-admin/DirectoryPage.js +2 -14
- package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
- package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
- package/dist/views/metadata-admin/PackagesPage.js +9 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +47 -20
- package/dist/views/metadata-admin/ResourceListPage.js +8 -16
- package/dist/views/metadata-admin/StudioHomePage.js +6 -14
- package/dist/views/metadata-admin/anchors.js +20 -2
- package/dist/views/metadata-admin/i18n.js +88 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +2 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +122 -8
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +84 -3
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +67 -2
- package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
- package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
- package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +97 -0
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +46 -1
- package/dist/views/metadata-admin/issuePath.d.ts +22 -0
- package/dist/views/metadata-admin/issuePath.js +65 -0
- package/dist/views/metadata-admin/package-scope.d.ts +26 -0
- package/dist/views/metadata-admin/package-scope.js +43 -0
- package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +7 -1
- package/dist/views/metadata-admin/previews/FlowCanvas.js +104 -16
- package/dist/views/metadata-admin/previews/FlowPreview.js +31 -3
- package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
- package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
- package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
- package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +21 -6
- package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
- package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
- package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
- package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +11 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +72 -0
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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': '
|
|
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 +
|
|
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 +
|
|
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
|
-
|
|
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
|
-
|
|
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.' })),
|
|
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: ''
|
|
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) =>
|
|
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 })] }),
|
|
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
|
}
|