@timeax/service-builder 0.1.8 → 0.1.9
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/dist/index.js +462 -71
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1077,6 +1077,110 @@ var DIAL_ACTION_TIMEOUT_MS = 1e4;
|
|
|
1077
1077
|
var OVERLAY_STALE_TIMEOUT_MS = 3e4;
|
|
1078
1078
|
var DRAG_MIME_TYPES = [DRAG_MIME_SERVICE, DRAG_MIME_ASSET_BUILTIN, DRAG_MIME_ASSET_TEMPLATE];
|
|
1079
1079
|
|
|
1080
|
+
// src/builder/node-context/template-utils.ts
|
|
1081
|
+
function deepClone(value) {
|
|
1082
|
+
return JSON.parse(JSON.stringify(value));
|
|
1083
|
+
}
|
|
1084
|
+
function sanitizeOption(option) {
|
|
1085
|
+
const next = deepClone(option);
|
|
1086
|
+
delete next.id;
|
|
1087
|
+
delete next.service_id;
|
|
1088
|
+
return next;
|
|
1089
|
+
}
|
|
1090
|
+
function sanitizeField(field) {
|
|
1091
|
+
const next = deepClone(field);
|
|
1092
|
+
delete next.id;
|
|
1093
|
+
delete next.bind_id;
|
|
1094
|
+
if (Array.isArray(next.options)) {
|
|
1095
|
+
next.options = next.options.map(
|
|
1096
|
+
(option) => option && typeof option === "object" ? sanitizeOption(option) : option
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
return next;
|
|
1100
|
+
}
|
|
1101
|
+
function pickCheckedProperties(source, checked) {
|
|
1102
|
+
const out = {};
|
|
1103
|
+
for (const key of Object.keys(source)) {
|
|
1104
|
+
if (checked[key] !== false) out[key] = source[key];
|
|
1105
|
+
}
|
|
1106
|
+
return out;
|
|
1107
|
+
}
|
|
1108
|
+
function detectRelationMaps(props) {
|
|
1109
|
+
const relations = {};
|
|
1110
|
+
for (const [key, value] of Object.entries(props)) {
|
|
1111
|
+
if (!/(includes|excludes)_for_/i.test(key)) continue;
|
|
1112
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
1113
|
+
const relation = {};
|
|
1114
|
+
for (const [source, targets] of Object.entries(value)) {
|
|
1115
|
+
if (!Array.isArray(targets)) continue;
|
|
1116
|
+
relation[String(source)] = targets.map(String);
|
|
1117
|
+
}
|
|
1118
|
+
relations[key] = relation;
|
|
1119
|
+
}
|
|
1120
|
+
return relations;
|
|
1121
|
+
}
|
|
1122
|
+
function discoverGroupFieldIds(rootFieldId, relations) {
|
|
1123
|
+
const discovered = /* @__PURE__ */ new Set([rootFieldId]);
|
|
1124
|
+
const queue = [rootFieldId];
|
|
1125
|
+
while (queue.length) {
|
|
1126
|
+
const current = queue.shift();
|
|
1127
|
+
for (const relation of Object.values(relations)) {
|
|
1128
|
+
const outbound = relation[current] ?? [];
|
|
1129
|
+
for (const id of outbound) {
|
|
1130
|
+
if (!discovered.has(id)) {
|
|
1131
|
+
discovered.add(id);
|
|
1132
|
+
queue.push(id);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
for (const [source, targets] of Object.entries(relation)) {
|
|
1136
|
+
if (targets.includes(current) && !discovered.has(source)) {
|
|
1137
|
+
discovered.add(source);
|
|
1138
|
+
queue.push(source);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return Array.from(discovered);
|
|
1144
|
+
}
|
|
1145
|
+
function filterRelationsForSelection(relations, selected) {
|
|
1146
|
+
const next = {};
|
|
1147
|
+
for (const [relationName, relation] of Object.entries(relations)) {
|
|
1148
|
+
const cleaned = {};
|
|
1149
|
+
for (const [source, targets] of Object.entries(relation)) {
|
|
1150
|
+
if (!selected.has(source)) continue;
|
|
1151
|
+
const keptTargets = targets.filter((target) => selected.has(target));
|
|
1152
|
+
if (keptTargets.length) cleaned[source] = Array.from(new Set(keptTargets));
|
|
1153
|
+
}
|
|
1154
|
+
next[relationName] = cleaned;
|
|
1155
|
+
}
|
|
1156
|
+
return next;
|
|
1157
|
+
}
|
|
1158
|
+
function isRecord(value) {
|
|
1159
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1160
|
+
}
|
|
1161
|
+
function isGroupTemplateDefinition(definition) {
|
|
1162
|
+
return isRecord(definition) && definition.templateType === "field" && definition.mode === "group" && Array.isArray(definition.fields) && typeof definition.rootOriginalId === "string" && isRecord(definition.relations);
|
|
1163
|
+
}
|
|
1164
|
+
function isSingleTemplateDefinition(definition) {
|
|
1165
|
+
return isRecord(definition) && definition.templateType === "field" && definition.mode === "single" && isRecord(definition.field);
|
|
1166
|
+
}
|
|
1167
|
+
function toSingleFieldDefinition(definition) {
|
|
1168
|
+
if (isSingleTemplateDefinition(definition)) return deepClone(definition.field);
|
|
1169
|
+
if (isRecord(definition)) return deepClone(definition);
|
|
1170
|
+
return {};
|
|
1171
|
+
}
|
|
1172
|
+
function withFreshOptionIds(field) {
|
|
1173
|
+
const next = deepClone(field);
|
|
1174
|
+
if (Array.isArray(next.options)) {
|
|
1175
|
+
next.options = next.options.map((option) => {
|
|
1176
|
+
const raw = option && typeof option === "object" ? { ...option } : {};
|
|
1177
|
+
raw.id = buildOptionId();
|
|
1178
|
+
return raw;
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
return next;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1080
1184
|
// src/builder/node-context/hooks.ts
|
|
1081
1185
|
function moveIndex(index, direction) {
|
|
1082
1186
|
return direction === "up" ? index - 1 : index + 1;
|
|
@@ -1259,45 +1363,15 @@ function useNodeActions(target, setTarget, setDialogTarget, setRenameOpen, setDe
|
|
|
1259
1363
|
const handleSaveAsTemplate = useCallback3(async () => {
|
|
1260
1364
|
if (!target || target.nodeKind !== "field") return;
|
|
1261
1365
|
if (!ensureTemplateWrite("save-field-template")) return;
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
type: node.raw.type,
|
|
1269
|
-
name: node.raw.name,
|
|
1270
|
-
required: node.raw.required,
|
|
1271
|
-
button: node.raw.button,
|
|
1272
|
-
description: node.raw.description,
|
|
1273
|
-
component: node.raw.component,
|
|
1274
|
-
options: cleanOptions,
|
|
1275
|
-
meta: node.raw.meta,
|
|
1276
|
-
validation: node.raw.validation
|
|
1277
|
-
};
|
|
1278
|
-
const result = await ws.createTemplate({
|
|
1279
|
-
name: `${node.raw.label} Template`,
|
|
1280
|
-
key: makeTemplateKey(node.raw.label ?? "field"),
|
|
1281
|
-
kind: String(node.raw.type ?? "text"),
|
|
1282
|
-
branchId: ws.branches.currentId ?? void 0,
|
|
1283
|
-
definition,
|
|
1284
|
-
defaults: node.raw.defaults,
|
|
1285
|
-
published: false
|
|
1286
|
-
});
|
|
1287
|
-
if (!result.ok) {
|
|
1288
|
-
const message = "error" in result ? result.error.message : "Failed to create template";
|
|
1289
|
-
throw new Error(message ?? "Failed to create template");
|
|
1290
|
-
}
|
|
1291
|
-
await ws.refresh.templates({ branchId: ws.branches.currentId ?? void 0 });
|
|
1292
|
-
} catch (error) {
|
|
1293
|
-
emitNodeError(error?.message ?? "Failed to save field as template", {
|
|
1294
|
-
action: "save-field-template",
|
|
1295
|
-
nodeId: target.nodeId,
|
|
1296
|
-
surface: target.surface
|
|
1297
|
-
});
|
|
1366
|
+
if (typeof window !== "undefined") {
|
|
1367
|
+
window.dispatchEvent(
|
|
1368
|
+
new CustomEvent("service-builder:open-template-save-dialog", {
|
|
1369
|
+
detail: { nodeId: target.nodeId, surface: target.surface }
|
|
1370
|
+
})
|
|
1371
|
+
);
|
|
1298
1372
|
}
|
|
1299
1373
|
setTarget(null);
|
|
1300
|
-
}, [
|
|
1374
|
+
}, [ensureTemplateWrite, setTarget, target]);
|
|
1301
1375
|
const isFieldOptionsShown = useMemo3(() => {
|
|
1302
1376
|
if (!target || target.nodeKind !== "field") return false;
|
|
1303
1377
|
const api = canvas.api;
|
|
@@ -1537,26 +1611,68 @@ function useDropHandling(setDropIntent, emitNodeError, focusNode2) {
|
|
|
1537
1611
|
if (source.kind === "asset_template") {
|
|
1538
1612
|
const template = (ws.templates.data ?? []).find((item) => item.id === source.asset.id);
|
|
1539
1613
|
const definition = template?.definition ?? {};
|
|
1540
|
-
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1614
|
+
if (isGroupTemplateDefinition(definition)) {
|
|
1615
|
+
const fieldIdMap = /* @__PURE__ */ new Map();
|
|
1616
|
+
const normalizedRelations = {};
|
|
1617
|
+
const nextFields = definition.fields.map((entry) => {
|
|
1618
|
+
const nextId = buildFieldId();
|
|
1619
|
+
fieldIdMap.set(entry.originalId, nextId);
|
|
1620
|
+
const rawField = withFreshOptionIds(entry.field ?? {});
|
|
1621
|
+
delete rawField.bind_id;
|
|
1622
|
+
return {
|
|
1623
|
+
...rawField,
|
|
1624
|
+
id: nextId
|
|
1625
|
+
};
|
|
1626
|
+
});
|
|
1627
|
+
for (const [relationName, relationMap] of Object.entries(definition.relations ?? {})) {
|
|
1628
|
+
const nextRelation = {};
|
|
1629
|
+
for (const [sourceId, targets] of Object.entries(relationMap ?? {})) {
|
|
1630
|
+
const mappedSource = fieldIdMap.get(sourceId);
|
|
1631
|
+
if (!mappedSource) continue;
|
|
1632
|
+
const mappedTargets = (targets ?? []).map((targetId) => fieldIdMap.get(String(targetId))).filter((value) => !!value);
|
|
1633
|
+
if (mappedTargets.length) nextRelation[mappedSource] = Array.from(new Set(mappedTargets));
|
|
1634
|
+
}
|
|
1635
|
+
normalizedRelations[relationName] = nextRelation;
|
|
1636
|
+
}
|
|
1637
|
+
const rootNewId = fieldIdMap.get(definition.rootOriginalId) ?? nextFields[0]?.id;
|
|
1638
|
+
const rootField = nextFields.find((field) => field.id === rootNewId);
|
|
1639
|
+
if (!rootField || !rootNewId) return null;
|
|
1640
|
+
canvas.api.editor.addField({
|
|
1641
|
+
...rootField,
|
|
1642
|
+
bind_id: currentTag,
|
|
1643
|
+
defaults: template?.defaults
|
|
1644
|
+
});
|
|
1645
|
+
for (const field of nextFields) {
|
|
1646
|
+
if (field.id === rootNewId) continue;
|
|
1647
|
+
canvas.api.editor.addField({
|
|
1648
|
+
...field,
|
|
1649
|
+
defaults: template?.defaults
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
const editorAny = canvas.api.editor;
|
|
1653
|
+
if (typeof editorAny.patchProps === "function") {
|
|
1654
|
+
editorAny.patchProps((props) => {
|
|
1655
|
+
for (const [relationName, relationMap] of Object.entries(normalizedRelations)) {
|
|
1656
|
+
const current = props[relationName] ?? {};
|
|
1657
|
+
const merged = { ...current };
|
|
1658
|
+
for (const [sourceId, targets] of Object.entries(relationMap)) {
|
|
1659
|
+
const prev = Array.isArray(merged[sourceId]) ? merged[sourceId] : [];
|
|
1660
|
+
merged[sourceId] = Array.from(/* @__PURE__ */ new Set([...prev, ...targets]));
|
|
1661
|
+
}
|
|
1662
|
+
props[relationName] = merged;
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
return rootNewId;
|
|
1667
|
+
}
|
|
1668
|
+
const singleDefinition = isSingleTemplateDefinition(definition) ? definition.field : toSingleFieldDefinition(definition);
|
|
1669
|
+
const nextField = withFreshOptionIds(singleDefinition);
|
|
1547
1670
|
canvas.api.editor.addField({
|
|
1671
|
+
...nextField,
|
|
1548
1672
|
id,
|
|
1549
|
-
label: String(definition.label ?? source.asset.name ?? "Template Field"),
|
|
1550
|
-
type: String(definition.type ?? source.asset.kind ?? "text"),
|
|
1551
1673
|
bind_id: currentTag,
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
button: Boolean(definition.button),
|
|
1555
|
-
description: typeof definition.description === "string" ? definition.description : void 0,
|
|
1556
|
-
component: typeof definition.component === "string" ? definition.component : void 0,
|
|
1557
|
-
options: nextOptions,
|
|
1558
|
-
validation: Array.isArray(definition.validation) ? definition.validation : void 0,
|
|
1559
|
-
meta: typeof definition.meta === "object" && definition.meta ? definition.meta : void 0,
|
|
1674
|
+
label: String(nextField.label ?? source.asset.name ?? "Template Field"),
|
|
1675
|
+
type: String(nextField.type ?? source.asset.kind ?? "text"),
|
|
1560
1676
|
defaults: template?.defaults
|
|
1561
1677
|
});
|
|
1562
1678
|
return id;
|
|
@@ -2444,6 +2560,12 @@ function NodeContextMenuProvider({ children }) {
|
|
|
2444
2560
|
const [editingNoticeId, setEditingNoticeId] = useState3(null);
|
|
2445
2561
|
const [noticeDraft, setNoticeDraft] = useState3(() => toDraft());
|
|
2446
2562
|
const [noticeSearch, setNoticeSearch] = useState3("");
|
|
2563
|
+
const [templateSaveOpen, setTemplateSaveOpen] = useState3(false);
|
|
2564
|
+
const [templateSaveFieldId, setTemplateSaveFieldId] = useState3(null);
|
|
2565
|
+
const [templateSaveMode, setTemplateSaveMode] = useState3("single");
|
|
2566
|
+
const [templateLabel, setTemplateLabel] = useState3("");
|
|
2567
|
+
const [templateGroupSelection, setTemplateGroupSelection] = useState3({});
|
|
2568
|
+
const [templateFieldChecks, setTemplateFieldChecks] = useState3({});
|
|
2447
2569
|
const menuRef = useRef2(null);
|
|
2448
2570
|
const dialRef = useRef2(null);
|
|
2449
2571
|
const closeNodeContextMenu = useCallback4(() => setTarget(null), []);
|
|
@@ -2473,6 +2595,7 @@ function NodeContextMenuProvider({ children }) {
|
|
|
2473
2595
|
[ws.actor, ws.authors?.data, ws.participants?.data, ws.permissions?.data]
|
|
2474
2596
|
);
|
|
2475
2597
|
const editDecision = getAuthorizationDecision(auth, "branch-content-edit");
|
|
2598
|
+
const templateDecision = getAuthorizationDecision(auth, "template-write");
|
|
2476
2599
|
const openNodeContextMenu = useCallback4(
|
|
2477
2600
|
(input) => {
|
|
2478
2601
|
const node = canvas.selector.getNode(input.nodeId);
|
|
@@ -2667,6 +2790,140 @@ function NodeContextMenuProvider({ children }) {
|
|
|
2667
2790
|
setDeleteOpen(false);
|
|
2668
2791
|
setDialogTarget(null);
|
|
2669
2792
|
}, [auth.canEditBranchContent, canvas.api.editor, dialogTarget, editDecision.reason, emitNodeError]);
|
|
2793
|
+
const templateSaveDraft = useMemo4(() => {
|
|
2794
|
+
if (!templateSaveFieldId) return null;
|
|
2795
|
+
try {
|
|
2796
|
+
const node = canvas.selector.getNode(templateSaveFieldId);
|
|
2797
|
+
if (!node || node.kind !== "field") return null;
|
|
2798
|
+
const props = canvas.api.builder.getProps();
|
|
2799
|
+
const fields = (props.fields ?? []).filter((field) => typeof field?.id === "string");
|
|
2800
|
+
const fieldsById = new Map(fields.map((field) => [String(field.id), field]));
|
|
2801
|
+
const root = fieldsById.get(templateSaveFieldId);
|
|
2802
|
+
if (!root) return null;
|
|
2803
|
+
const relations = detectRelationMaps(props);
|
|
2804
|
+
const discoveredIds = discoverGroupFieldIds(templateSaveFieldId, relations).filter((id) => fieldsById.has(id));
|
|
2805
|
+
const discoveredFields = discoveredIds.map((id) => ({
|
|
2806
|
+
originalId: id,
|
|
2807
|
+
field: sanitizeField(fieldsById.get(id) ?? {})
|
|
2808
|
+
}));
|
|
2809
|
+
return {
|
|
2810
|
+
rootLabel: String(node.raw?.label ?? "Field"),
|
|
2811
|
+
rootType: String(node.raw?.type ?? "text"),
|
|
2812
|
+
relations,
|
|
2813
|
+
discoveredFields
|
|
2814
|
+
};
|
|
2815
|
+
} catch {
|
|
2816
|
+
return null;
|
|
2817
|
+
}
|
|
2818
|
+
}, [canvas.api.builder, canvas.selector, templateSaveFieldId]);
|
|
2819
|
+
useEffect4(() => {
|
|
2820
|
+
const onOpen = (event) => {
|
|
2821
|
+
const payload = event.detail;
|
|
2822
|
+
const fieldId = String(payload?.nodeId ?? "");
|
|
2823
|
+
if (!fieldId) return;
|
|
2824
|
+
setTemplateSaveFieldId(fieldId);
|
|
2825
|
+
setTemplateSaveMode("single");
|
|
2826
|
+
setTemplateLabel("");
|
|
2827
|
+
setTemplateGroupSelection({});
|
|
2828
|
+
setTemplateFieldChecks({});
|
|
2829
|
+
setTemplateSaveOpen(true);
|
|
2830
|
+
};
|
|
2831
|
+
window.addEventListener("service-builder:open-template-save-dialog", onOpen);
|
|
2832
|
+
return () => window.removeEventListener("service-builder:open-template-save-dialog", onOpen);
|
|
2833
|
+
}, []);
|
|
2834
|
+
useEffect4(() => {
|
|
2835
|
+
if (!templateSaveDraft || !templateSaveFieldId) return;
|
|
2836
|
+
const initialGroupSelection = {};
|
|
2837
|
+
for (const entry of templateSaveDraft.discoveredFields) initialGroupSelection[entry.originalId] = true;
|
|
2838
|
+
const initialChecks = {};
|
|
2839
|
+
for (const entry of templateSaveDraft.discoveredFields) {
|
|
2840
|
+
const checks = {};
|
|
2841
|
+
for (const key of Object.keys(entry.field)) checks[key] = true;
|
|
2842
|
+
initialChecks[entry.originalId] = checks;
|
|
2843
|
+
}
|
|
2844
|
+
setTemplateGroupSelection((current) => Object.keys(current).length ? current : initialGroupSelection);
|
|
2845
|
+
setTemplateFieldChecks((current) => Object.keys(current).length ? current : initialChecks);
|
|
2846
|
+
setTemplateLabel((current) => current || `${templateSaveDraft.rootLabel} group`);
|
|
2847
|
+
}, [templateSaveDraft, templateSaveFieldId]);
|
|
2848
|
+
const handleTemplateSave = useCallback4(async () => {
|
|
2849
|
+
if (!templateSaveDraft || !templateSaveFieldId) return;
|
|
2850
|
+
if (!auth.canWriteTemplates) {
|
|
2851
|
+
emitNodeError(templateDecision.reason ?? "Template editing is disabled for this actor.", { action: "save-field-template" });
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
try {
|
|
2855
|
+
const rootEntry = templateSaveDraft.discoveredFields.find((entry) => entry.originalId === templateSaveFieldId);
|
|
2856
|
+
if (!rootEntry) throw new Error("Selected field no longer exists.");
|
|
2857
|
+
if (templateSaveMode === "single") {
|
|
2858
|
+
const field = pickCheckedProperties(rootEntry.field, templateFieldChecks[templateSaveFieldId] ?? {});
|
|
2859
|
+
const definition = {
|
|
2860
|
+
templateType: "field",
|
|
2861
|
+
mode: "single",
|
|
2862
|
+
version: 1,
|
|
2863
|
+
field
|
|
2864
|
+
};
|
|
2865
|
+
const result = await ws.createTemplate({
|
|
2866
|
+
name: `${templateSaveDraft.rootLabel} Template`,
|
|
2867
|
+
key: makeTemplateKey(templateSaveDraft.rootLabel ?? "field"),
|
|
2868
|
+
kind: templateSaveDraft.rootType,
|
|
2869
|
+
branchId: ws.branches.currentId ?? void 0,
|
|
2870
|
+
definition,
|
|
2871
|
+
defaults: canvas.selector.getNode(templateSaveFieldId)?.raw?.defaults,
|
|
2872
|
+
published: false
|
|
2873
|
+
});
|
|
2874
|
+
if (!result.ok) throw new Error(("error" in result ? result.error.message : "Failed to create template") ?? "Failed to create template");
|
|
2875
|
+
} else {
|
|
2876
|
+
const selected = new Set(
|
|
2877
|
+
Object.entries(templateGroupSelection).filter(([, included]) => included).map(([fieldId]) => fieldId)
|
|
2878
|
+
);
|
|
2879
|
+
if (!selected.size) throw new Error("Cannot save an empty group.");
|
|
2880
|
+
if (!selected.has(templateSaveFieldId)) selected.add(templateSaveFieldId);
|
|
2881
|
+
const filteredRelations = filterRelationsForSelection(templateSaveDraft.relations, selected);
|
|
2882
|
+
const fields = templateSaveDraft.discoveredFields.filter((entry) => selected.has(entry.originalId)).map((entry) => ({
|
|
2883
|
+
originalId: entry.originalId,
|
|
2884
|
+
field: pickCheckedProperties(entry.field, templateFieldChecks[entry.originalId] ?? {})
|
|
2885
|
+
}));
|
|
2886
|
+
const definition = {
|
|
2887
|
+
templateType: "field",
|
|
2888
|
+
mode: "group",
|
|
2889
|
+
version: 1,
|
|
2890
|
+
label: templateLabel.trim() || `${templateSaveDraft.rootLabel} group`,
|
|
2891
|
+
rootOriginalId: templateSaveFieldId,
|
|
2892
|
+
fields,
|
|
2893
|
+
relations: filteredRelations
|
|
2894
|
+
};
|
|
2895
|
+
const result = await ws.createTemplate({
|
|
2896
|
+
name: definition.label,
|
|
2897
|
+
key: makeTemplateKey(definition.label || "field-group"),
|
|
2898
|
+
kind: templateSaveDraft.rootType,
|
|
2899
|
+
branchId: ws.branches.currentId ?? void 0,
|
|
2900
|
+
definition,
|
|
2901
|
+
defaults: canvas.selector.getNode(templateSaveFieldId)?.raw?.defaults,
|
|
2902
|
+
published: false
|
|
2903
|
+
});
|
|
2904
|
+
if (!result.ok) throw new Error(("error" in result ? result.error.message : "Failed to create template") ?? "Failed to create template");
|
|
2905
|
+
}
|
|
2906
|
+
await ws.refresh.templates({ branchId: ws.branches.currentId ?? void 0 });
|
|
2907
|
+
setTemplateSaveOpen(false);
|
|
2908
|
+
} catch (error) {
|
|
2909
|
+
emitNodeError(error?.message ?? "Failed to save field as template", {
|
|
2910
|
+
action: "save-field-template",
|
|
2911
|
+
nodeId: templateSaveFieldId
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
}, [
|
|
2915
|
+
auth.canWriteTemplates,
|
|
2916
|
+
canvas.selector,
|
|
2917
|
+
emitNodeError,
|
|
2918
|
+
templateDecision.reason,
|
|
2919
|
+
templateFieldChecks,
|
|
2920
|
+
templateGroupSelection,
|
|
2921
|
+
templateLabel,
|
|
2922
|
+
templateSaveDraft,
|
|
2923
|
+
templateSaveFieldId,
|
|
2924
|
+
templateSaveMode,
|
|
2925
|
+
ws
|
|
2926
|
+
]);
|
|
2670
2927
|
const anchorStyle = useMemo4(() => {
|
|
2671
2928
|
if (!target || typeof window === "undefined") return null;
|
|
2672
2929
|
const width = 220;
|
|
@@ -2706,7 +2963,7 @@ function NodeContextMenuProvider({ children }) {
|
|
|
2706
2963
|
toggleNoticeManager
|
|
2707
2964
|
]
|
|
2708
2965
|
);
|
|
2709
|
-
const hasPortalContent = Boolean(target || dropIntent || renameOpen || deleteOpen || noticeManagerOpen);
|
|
2966
|
+
const hasPortalContent = Boolean(target || dropIntent || renameOpen || deleteOpen || noticeManagerOpen || templateSaveOpen);
|
|
2710
2967
|
return /* @__PURE__ */ jsxs6(NodeContextMenuContext.Provider, { value, children: [
|
|
2711
2968
|
children,
|
|
2712
2969
|
typeof document !== "undefined" && hasPortalContent ? createPortal2(
|
|
@@ -2817,6 +3074,101 @@ function NodeContextMenuProvider({ children }) {
|
|
|
2817
3074
|
] })
|
|
2818
3075
|
] })
|
|
2819
3076
|
}
|
|
3077
|
+
),
|
|
3078
|
+
/* @__PURE__ */ jsx9(
|
|
3079
|
+
AlertDialog,
|
|
3080
|
+
{
|
|
3081
|
+
open: templateSaveOpen,
|
|
3082
|
+
onOpenChange: (open) => {
|
|
3083
|
+
setTemplateSaveOpen(open);
|
|
3084
|
+
if (!open) setTemplateSaveFieldId(null);
|
|
3085
|
+
},
|
|
3086
|
+
children: /* @__PURE__ */ jsxs6(AlertDialogContent, { children: [
|
|
3087
|
+
/* @__PURE__ */ jsxs6(AlertDialogHeader, { children: [
|
|
3088
|
+
/* @__PURE__ */ jsx9(AlertDialogTitle, { children: "Save Field Template" }),
|
|
3089
|
+
/* @__PURE__ */ jsx9(AlertDialogDescription, { children: "Choose how to save this field and review copied properties." })
|
|
3090
|
+
] }),
|
|
3091
|
+
templateSaveDraft ? /* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
|
|
3092
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex gap-2", children: [
|
|
3093
|
+
/* @__PURE__ */ jsx9(
|
|
3094
|
+
"button",
|
|
3095
|
+
{
|
|
3096
|
+
type: "button",
|
|
3097
|
+
className: `rounded-lg border px-3 py-1 text-sm ${templateSaveMode === "single" ? "border-blue-500 bg-blue-50" : "border-slate-300"}`,
|
|
3098
|
+
onClick: () => setTemplateSaveMode("single"),
|
|
3099
|
+
children: "Single field"
|
|
3100
|
+
}
|
|
3101
|
+
),
|
|
3102
|
+
/* @__PURE__ */ jsx9(
|
|
3103
|
+
"button",
|
|
3104
|
+
{
|
|
3105
|
+
type: "button",
|
|
3106
|
+
className: `rounded-lg border px-3 py-1 text-sm ${templateSaveMode === "group" ? "border-blue-500 bg-blue-50" : "border-slate-300"}`,
|
|
3107
|
+
onClick: () => setTemplateSaveMode("group"),
|
|
3108
|
+
children: "Field group/tree"
|
|
3109
|
+
}
|
|
3110
|
+
)
|
|
3111
|
+
] }),
|
|
3112
|
+
templateSaveMode === "group" ? /* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
3113
|
+
/* @__PURE__ */ jsx9("label", { className: "block text-sm font-medium", children: "Template label" }),
|
|
3114
|
+
/* @__PURE__ */ jsx9(
|
|
3115
|
+
"input",
|
|
3116
|
+
{
|
|
3117
|
+
value: templateLabel,
|
|
3118
|
+
onChange: (event) => setTemplateLabel(event.target.value),
|
|
3119
|
+
className: "w-full rounded-md border border-slate-300 px-3 py-2 text-sm"
|
|
3120
|
+
}
|
|
3121
|
+
),
|
|
3122
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-1", children: [
|
|
3123
|
+
/* @__PURE__ */ jsx9("p", { className: "text-sm font-medium", children: "Fields in group" }),
|
|
3124
|
+
templateSaveDraft.discoveredFields.map((entry) => /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 text-sm", children: [
|
|
3125
|
+
/* @__PURE__ */ jsx9(
|
|
3126
|
+
"input",
|
|
3127
|
+
{
|
|
3128
|
+
type: "checkbox",
|
|
3129
|
+
checked: templateGroupSelection[entry.originalId] !== false,
|
|
3130
|
+
onChange: (event) => setTemplateGroupSelection((current) => ({
|
|
3131
|
+
...current,
|
|
3132
|
+
[entry.originalId]: event.target.checked
|
|
3133
|
+
}))
|
|
3134
|
+
}
|
|
3135
|
+
),
|
|
3136
|
+
/* @__PURE__ */ jsx9("span", { children: String(entry.field.label ?? entry.originalId) })
|
|
3137
|
+
] }, entry.originalId))
|
|
3138
|
+
] })
|
|
3139
|
+
] }) : null,
|
|
3140
|
+
/* @__PURE__ */ jsx9("div", { className: "max-h-72 space-y-2 overflow-auto rounded-lg border border-slate-200 p-2", children: (templateSaveMode === "single" ? templateSaveDraft.discoveredFields.filter((entry) => entry.originalId === templateSaveFieldId) : templateSaveDraft.discoveredFields.filter((entry) => templateGroupSelection[entry.originalId] !== false)).map((entry) => /* @__PURE__ */ jsxs6("details", { open: true, className: "rounded border border-slate-200 p-2", children: [
|
|
3141
|
+
/* @__PURE__ */ jsxs6("summary", { className: "cursor-pointer text-sm font-medium", children: [
|
|
3142
|
+
String(entry.field.label ?? entry.originalId),
|
|
3143
|
+
" (",
|
|
3144
|
+
entry.originalId,
|
|
3145
|
+
")"
|
|
3146
|
+
] }),
|
|
3147
|
+
/* @__PURE__ */ jsx9("div", { className: "mt-2 grid grid-cols-1 gap-1", children: Object.keys(entry.field).map((key) => /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 text-xs", children: [
|
|
3148
|
+
/* @__PURE__ */ jsx9(
|
|
3149
|
+
"input",
|
|
3150
|
+
{
|
|
3151
|
+
type: "checkbox",
|
|
3152
|
+
checked: templateFieldChecks[entry.originalId]?.[key] !== false,
|
|
3153
|
+
onChange: (event) => setTemplateFieldChecks((current) => ({
|
|
3154
|
+
...current,
|
|
3155
|
+
[entry.originalId]: {
|
|
3156
|
+
...current[entry.originalId] ?? {},
|
|
3157
|
+
[key]: event.target.checked
|
|
3158
|
+
}
|
|
3159
|
+
}))
|
|
3160
|
+
}
|
|
3161
|
+
),
|
|
3162
|
+
/* @__PURE__ */ jsx9("span", { className: "font-mono", children: key })
|
|
3163
|
+
] }, key)) })
|
|
3164
|
+
] }, entry.originalId)) }),
|
|
3165
|
+
/* @__PURE__ */ jsxs6(AlertDialogFooter, { children: [
|
|
3166
|
+
/* @__PURE__ */ jsx9(AlertDialogCancel, { type: "button", children: "Cancel" }),
|
|
3167
|
+
/* @__PURE__ */ jsx9(AlertDialogAction, { type: "button", onClick: () => void handleTemplateSave(), children: "Save template" })
|
|
3168
|
+
] })
|
|
3169
|
+
] }) : null
|
|
3170
|
+
] })
|
|
3171
|
+
}
|
|
2820
3172
|
)
|
|
2821
3173
|
] }),
|
|
2822
3174
|
document.body
|
|
@@ -7974,7 +8326,7 @@ function AssetsPanel() {
|
|
|
7974
8326
|
className: "w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-left transition hover:border-slate-300 hover:bg-slate-50 dark:border-slate-800 dark:bg-slate-950/50 dark:hover:border-slate-700",
|
|
7975
8327
|
children: [
|
|
7976
8328
|
/* @__PURE__ */ jsx30("p", { className: "text-sm font-medium text-slate-900 dark:text-slate-100", children: item.name }),
|
|
7977
|
-
/* @__PURE__ */ jsx30("p", { className: "text-xs text-slate-500 dark:text-slate-400", children: item.kind })
|
|
8329
|
+
/* @__PURE__ */ jsx30("p", { className: "text-xs text-slate-500 dark:text-slate-400", children: item?.definition?.mode === "group" ? "Group" : String(item?.definition?.type ?? item.kind ?? "template") })
|
|
7978
8330
|
]
|
|
7979
8331
|
},
|
|
7980
8332
|
item.id
|
|
@@ -12055,7 +12407,6 @@ function buildQuantityRule(valueBy, base = {}) {
|
|
|
12055
12407
|
import { jsx as jsx54, jsxs as jsxs36 } from "react/jsx-runtime";
|
|
12056
12408
|
var RESERVED_DESCRIPTOR_KEYS = /* @__PURE__ */ new Set([
|
|
12057
12409
|
"options",
|
|
12058
|
-
"label",
|
|
12059
12410
|
"required",
|
|
12060
12411
|
"value",
|
|
12061
12412
|
"error",
|
|
@@ -16194,7 +16545,7 @@ function useFallbackEditorModal() {
|
|
|
16194
16545
|
|
|
16195
16546
|
// src/workspace/bottom-panel/index.tsx
|
|
16196
16547
|
import { useCanvas as useCanvas22, useWorkspace as useWorkspace16 } from "@timeax/digital-service-engine/workspace";
|
|
16197
|
-
import { useCallback as useCallback22, useEffect as useEffect26, useMemo as useMemo41, useRef as
|
|
16548
|
+
import { useCallback as useCallback22, useEffect as useEffect26, useMemo as useMemo41, useRef as useRef13, useState as useState41 } from "react";
|
|
16198
16549
|
import { FiEye as FiEye3, FiEyeOff as FiEyeOff2, FiSearch, FiTerminal as FiTerminal2, FiX as FiX6 } from "react-icons/fi";
|
|
16199
16550
|
import { LuGripHorizontal, LuLayers3 } from "react-icons/lu";
|
|
16200
16551
|
import { MdOutlineSync } from "react-icons/md";
|
|
@@ -16649,7 +17000,7 @@ function LogCard({ row, onRemove }) {
|
|
|
16649
17000
|
|
|
16650
17001
|
// src/workspace/bottom-panel/service-picker-dialog.tsx
|
|
16651
17002
|
import { InputField as InputField20 } from "@timeax/form-palette";
|
|
16652
|
-
import { useEffect as useEffect24, useMemo as useMemo40, useState as useState39 } from "react";
|
|
17003
|
+
import { useEffect as useEffect24, useMemo as useMemo40, useRef as useRef12, useState as useState39 } from "react";
|
|
16653
17004
|
import { createPortal as createPortal5 } from "react-dom";
|
|
16654
17005
|
|
|
16655
17006
|
// src/workspace/bottom-panel/service-picker.ts
|
|
@@ -16780,6 +17131,7 @@ function ServicePickerDialog({
|
|
|
16780
17131
|
}) {
|
|
16781
17132
|
const [filters, setFilters] = useState39(() => createDefaultServicePickerFilters());
|
|
16782
17133
|
const [selectedIds, setSelectedIds] = useState39(/* @__PURE__ */ new Set());
|
|
17134
|
+
const selectAllRef = useRef12(null);
|
|
16783
17135
|
useEffect24(() => {
|
|
16784
17136
|
if (!open) return;
|
|
16785
17137
|
setFilters(createDefaultServicePickerFilters());
|
|
@@ -16809,9 +17161,20 @@ function ServicePickerDialog({
|
|
|
16809
17161
|
}),
|
|
16810
17162
|
[activeIds, filters, serviceMap, services2]
|
|
16811
17163
|
);
|
|
16812
|
-
|
|
17164
|
+
const filteredServiceIds = useMemo40(() => filteredServices.map((service) => String(service.id)), [filteredServices]);
|
|
17165
|
+
const filteredSelectedCount = useMemo40(
|
|
17166
|
+
() => filteredServiceIds.filter((id) => selectedIds.has(id)).length,
|
|
17167
|
+
[filteredServiceIds, selectedIds]
|
|
17168
|
+
);
|
|
17169
|
+
const allFilteredSelected = filteredServiceIds.length > 0 && filteredSelectedCount === filteredServiceIds.length;
|
|
17170
|
+
const partiallyFilteredSelected = filteredSelectedCount > 0 && filteredSelectedCount < filteredServiceIds.length;
|
|
16813
17171
|
const selectedCount = selectedIds.size;
|
|
16814
17172
|
const canConfirm = selectedCount > 0;
|
|
17173
|
+
useEffect24(() => {
|
|
17174
|
+
if (!selectAllRef.current) return;
|
|
17175
|
+
selectAllRef.current.indeterminate = partiallyFilteredSelected;
|
|
17176
|
+
}, [partiallyFilteredSelected]);
|
|
17177
|
+
if (!open || typeof document === "undefined") return null;
|
|
16815
17178
|
return createPortal5(
|
|
16816
17179
|
/* @__PURE__ */ jsx86("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-slate-950/45 p-4", children: /* @__PURE__ */ jsxs64(
|
|
16817
17180
|
"div",
|
|
@@ -16976,12 +17339,40 @@ function ServicePickerDialog({
|
|
|
16976
17339
|
) })
|
|
16977
17340
|
] }) }) }),
|
|
16978
17341
|
/* @__PURE__ */ jsxs64("div", { className: "flex min-h-0 flex-col overflow-hidden", children: [
|
|
16979
|
-
/* @__PURE__ */
|
|
16980
|
-
|
|
16981
|
-
|
|
16982
|
-
|
|
16983
|
-
|
|
16984
|
-
|
|
17342
|
+
/* @__PURE__ */ jsx86("div", { className: "border-b border-slate-200 px-4 py-3 dark:border-slate-800", children: /* @__PURE__ */ jsxs64("div", { className: "flex items-center justify-between gap-4", children: [
|
|
17343
|
+
/* @__PURE__ */ jsxs64("div", { className: "text-sm text-slate-500 dark:text-slate-400", children: [
|
|
17344
|
+
filteredServices.length,
|
|
17345
|
+
" service",
|
|
17346
|
+
filteredServices.length === 1 ? "" : "s",
|
|
17347
|
+
" found"
|
|
17348
|
+
] }),
|
|
17349
|
+
/* @__PURE__ */ jsxs64("label", { className: "inline-flex items-center gap-2 text-sm text-slate-600 dark:text-slate-300", children: [
|
|
17350
|
+
/* @__PURE__ */ jsx86(
|
|
17351
|
+
"input",
|
|
17352
|
+
{
|
|
17353
|
+
ref: selectAllRef,
|
|
17354
|
+
type: "checkbox",
|
|
17355
|
+
"aria-label": "Select all filtered services",
|
|
17356
|
+
"aria-checked": partiallyFilteredSelected ? "mixed" : allFilteredSelected,
|
|
17357
|
+
checked: allFilteredSelected,
|
|
17358
|
+
disabled: !filteredServiceIds.length,
|
|
17359
|
+
onChange: (event) => {
|
|
17360
|
+
const shouldSelectAll = event.target.checked;
|
|
17361
|
+
setSelectedIds((current) => {
|
|
17362
|
+
const next = new Set(current);
|
|
17363
|
+
if (shouldSelectAll) {
|
|
17364
|
+
for (const id of filteredServiceIds) next.add(id);
|
|
17365
|
+
} else {
|
|
17366
|
+
for (const id of filteredServiceIds) next.delete(id);
|
|
17367
|
+
}
|
|
17368
|
+
return next;
|
|
17369
|
+
});
|
|
17370
|
+
}
|
|
17371
|
+
}
|
|
17372
|
+
),
|
|
17373
|
+
/* @__PURE__ */ jsx86("span", { children: "Select all" })
|
|
17374
|
+
] })
|
|
17375
|
+
] }) }),
|
|
16985
17376
|
/* @__PURE__ */ jsx86(ScrollArea, { className: "h-full min-h-0 flex-1", children: /* @__PURE__ */ jsx86("div", { className: "space-y-2 p-4", children: filteredServices.length ? filteredServices.map((service) => {
|
|
16986
17377
|
const id = String(service.id);
|
|
16987
17378
|
const selected = selectedIds.has(id);
|
|
@@ -17817,11 +18208,11 @@ function BottomConsolePanel({ controller, errors, activeServices, allServices, o
|
|
|
17817
18208
|
logs: { minimized: false, closed: false },
|
|
17818
18209
|
notices: { minimized: false, closed: false }
|
|
17819
18210
|
});
|
|
17820
|
-
const lastHighlightedIdsRef =
|
|
17821
|
-
const panelContainerRef =
|
|
17822
|
-
const panelRef =
|
|
17823
|
-
const searchInputRef =
|
|
17824
|
-
const dragStartRef =
|
|
18211
|
+
const lastHighlightedIdsRef = useRef13([]);
|
|
18212
|
+
const panelContainerRef = useRef13(null);
|
|
18213
|
+
const panelRef = useRef13(null);
|
|
18214
|
+
const searchInputRef = useRef13(null);
|
|
18215
|
+
const dragStartRef = useRef13(null);
|
|
17825
18216
|
const selectedButtons = useMemo41(
|
|
17826
18217
|
() => (canvas.api.selection.selectedButtons?.() ?? []).map((value) => String(value)),
|
|
17827
18218
|
[canvas.api.selection, canvas.selectionInfo.ids, canvas.selectionInfo.optionIds]
|