@revenexx/studio-integrations 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +581 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.d.vue.ts +9 -0
  4. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue +53 -0
  5. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue.d.ts +9 -0
  6. package/dist/runtime/components/Integrations/EditorTip.vue +2 -15
  7. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
  8. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +15 -1
  9. package/dist/runtime/components/Integrations/NodeConfigField.vue +158 -49
  10. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +15 -1
  11. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +5 -1
  12. package/dist/runtime/components/Integrations/NodeInspector.vue +136 -81
  13. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +5 -1
  14. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
  15. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
  16. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
  17. package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
  18. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
  19. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
  20. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
  21. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
  22. package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
  23. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
  24. package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
  25. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
  26. package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
  27. package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
  28. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
  29. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
  30. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
  31. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
  32. package/dist/runtime/components/Integrations/WorkflowEditor.vue +200 -54
  33. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
  34. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
  35. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
  36. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
  37. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
  38. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
  39. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
  40. package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
  41. package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
  42. package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
  43. package/dist/runtime/composables/useCredentialTypes.js +3 -9
  44. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
  45. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  46. package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
  47. package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
  48. package/dist/runtime/composables/useIntegrationsTemplates.js +10 -8
  49. package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
  50. package/dist/runtime/composables/useIntegrationsWorkflows.js +11 -7
  51. package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
  52. package/dist/runtime/data/canvasShortcuts.js +68 -0
  53. package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
  54. package/dist/runtime/data/integrationNodeCatalog.js +8 -0
  55. package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
  56. package/dist/runtime/data/nodeVersionMove.js +65 -0
  57. package/dist/runtime/i18n/messages/de.json +83 -20
  58. package/dist/runtime/i18n/messages/en.json +83 -20
  59. package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
  60. package/dist/runtime/pages/integrations/workflows/[id].vue +158 -53
  61. package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
  62. package/dist/runtime/types/integrations.d.ts +44 -1
  63. package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
  64. package/dist/runtime/utils/apiEnvelope.js +20 -0
  65. package/dist/runtime/utils/buildSettle.d.ts +68 -0
  66. package/dist/runtime/utils/buildSettle.js +29 -0
  67. package/dist/runtime/utils/configDefaults.d.ts +18 -0
  68. package/dist/runtime/utils/configDefaults.js +7 -0
  69. package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
  70. package/dist/runtime/utils/errorPathAdvice.js +19 -0
  71. package/dist/runtime/utils/notify.d.ts +2 -2
  72. package/dist/runtime/utils/runEnvelope.d.ts +12 -0
  73. package/dist/runtime/utils/runEnvelope.js +2 -1
  74. package/dist/runtime/utils/selectOptions.d.ts +48 -0
  75. package/dist/runtime/utils/selectOptions.js +19 -0
  76. package/dist/runtime/utils/settingCondition.d.ts +48 -0
  77. package/dist/runtime/utils/settingCondition.js +84 -0
  78. package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
  79. package/dist/runtime/utils/shortcutLabel.js +48 -0
  80. package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
  81. package/dist/runtime/utils/stateNamespaceName.js +8 -0
  82. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { resolveLocalized } from "../utils/localize.js";
3
3
  import { useIntegrationsApi } from "./integrationsApi.js";
4
4
  import { useIntegrationsT } from "./useIntegrationsT.js";
5
5
  import { errorReason } from "../utils/apiError.js";
6
+ import { unwrapList } from "../utils/apiEnvelope.js";
6
7
  import { useLoadGate } from "./useLoadGate.js";
7
8
  const RETRY_COOLDOWN_MS = 3e4;
8
9
  export function useIntegrationsNodes() {
@@ -18,8 +19,7 @@ export function useIntegrationsNodes() {
18
19
  if (!await gate.claim(force)) return;
19
20
  error.value = null;
20
21
  try {
21
- const res = await api.get("/nodes");
22
- const list = Array.isArray(res) ? res : res?.data ?? [];
22
+ const list = unwrapList(await api.get("/nodes"));
23
23
  const manifests = list.filter((n) => n.manifest).map((n) => ({
24
24
  ...n.manifest,
25
25
  slug: n.manifest.slug ?? n.slug,
@@ -2,6 +2,8 @@ import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { errorReason } from "../utils/apiError.js";
5
+ import { unwrapRecord } from "../utils/apiEnvelope.js";
6
+ import { RUN_OWN_FIELDS } from "../utils/runEnvelope.js";
5
7
  import { useLoadGate } from "./useLoadGate.js";
6
8
  import {
7
9
  DEFAULT_RUN_DIRECTION,
@@ -93,9 +95,7 @@ export function useIntegrationsRuns() {
93
95
  const active = runs.value.filter((r) => r.status === "running" || r.status === "queued").sort((a, b) => Number(b.id) - Number(a.id)).slice(0, SYNC_LIMIT);
94
96
  await Promise.allSettled(active.map(async (r) => {
95
97
  try {
96
- const raw = await api.get(`/workflows/${r.workflowId}/runs/${r.id}`);
97
- const apiRun = raw?.data ?? raw;
98
- const fresh = mapRun(apiRun, r.workflowName);
98
+ const fresh = await fetchRun(r.workflowId, r.id, r.workflowName);
99
99
  runs.value = runs.value.map((x) => x.id === fresh.id ? fresh : x);
100
100
  } catch {
101
101
  }
@@ -124,9 +124,7 @@ export function useIntegrationsRuns() {
124
124
  const active = list2.filter((r) => r.status === "running" || r.status === "queued").sort((a, b) => Number(b.id) - Number(a.id)).slice(0, SYNC_LIMIT);
125
125
  await Promise.allSettled(active.map(async (r) => {
126
126
  try {
127
- const raw = await api.get(`/workflows/${workflowId}/runs/${r.id}`);
128
- const apiRun = raw?.data ?? raw;
129
- const fresh = mapRun(apiRun, workflowName);
127
+ const fresh = await fetchRun(workflowId, r.id, workflowName);
130
128
  list2 = list2.map((x) => x.id === fresh.id ? fresh : x);
131
129
  } catch {
132
130
  }
@@ -185,13 +183,11 @@ export function useIntegrationsRuns() {
185
183
  async function trigger(workflowId, payload = {}, mode) {
186
184
  const body = { trigger_payload: payload };
187
185
  if (mode) body.mode = mode;
188
- const res = await api.post(`/workflows/${workflowId}/runs`, body);
189
- return res?.data ?? res;
186
+ return unwrapRecord(await api.post(`/workflows/${workflowId}/runs`, body));
190
187
  }
191
188
  async function runAction(workflowId, runId, action, reason) {
192
189
  const body = reason ? { reason } : {};
193
- const res = await api.post(`/workflows/${workflowId}/runs/${runId}/${action}`, body);
194
- return res?.data ?? res;
190
+ return unwrapRecord(await api.post(`/workflows/${workflowId}/runs/${runId}/${action}`, body));
195
191
  }
196
192
  const cancelRun = (workflowId, runId, reason) => runAction(workflowId, runId, "cancel", reason);
197
193
  const terminateRun = (workflowId, runId, reason) => runAction(workflowId, runId, "terminate", reason);
@@ -199,13 +195,12 @@ export function useIntegrationsRuns() {
199
195
  const resumeRun = (workflowId, runId, reason) => runAction(workflowId, runId, "resume", reason);
200
196
  const getRun = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}`);
201
197
  async function fetchRun(workflowId, runId, workflowName) {
202
- const raw = await getRun(workflowId, runId);
203
- const apiRun = raw?.data ?? raw;
198
+ const apiRun = unwrapRecord(await getRun(workflowId, runId), RUN_OWN_FIELDS);
204
199
  return mapRun(apiRun, workflowName);
205
200
  }
206
201
  const getDetails = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}/details`);
207
202
  const getSteps = (workflowId, runId, includePayloads = false) => api.get(
208
- `/workflows/${workflowId}/runs/${runId}/steps${includePayloads ? "?include_payloads=1" : ""}`
203
+ `/workflows/${workflowId}/runs/${runId}/steps?include_payloads=${includePayloads ? "1" : "0"}`
209
204
  );
210
205
  const getHistory = (workflowId, runId, params = {}) => {
211
206
  const qs = new URLSearchParams();
@@ -2,6 +2,7 @@ import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { errorReason } from "../utils/apiError.js";
5
+ import { unwrapList } from "../utils/apiEnvelope.js";
5
6
  import { useLoadGate } from "./useLoadGate.js";
6
7
  const KEY_PATTERN = /^[A-Z0-9_]+$/;
7
8
  export function useIntegrationsSecrets() {
@@ -17,11 +18,10 @@ export function useIntegrationsSecrets() {
17
18
  }
18
19
  function normalize(res) {
19
20
  const obj = res;
20
- if (Array.isArray(obj?.data)) return obj.data.map(toRecord);
21
- if (Array.isArray(obj?.secrets)) return obj.secrets.map(toRecord);
22
- if (Array.isArray(obj?.keys)) return obj.keys.map((key) => ({ key, created_at: "", updated_at: "" }));
23
- if (Array.isArray(res)) return res.map(toRecord);
24
- return [];
21
+ if (obj?.data === void 0 && Array.isArray(obj?.keys)) {
22
+ return obj.keys.map((key) => ({ key, created_at: "", updated_at: "" }));
23
+ }
24
+ return unwrapList(res).map(toRecord);
25
25
  }
26
26
  function toRecord(r) {
27
27
  return { key: r.key ?? "", created_at: r.created_at ?? "", updated_at: r.updated_at ?? "" };
@@ -1,6 +1,7 @@
1
1
  import { computed } from "vue";
2
2
  import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { errorReason } from "../utils/apiError.js";
4
+ import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
4
5
  import { resolveLocalized } from "../utils/localize.js";
5
6
  import { useLoadGate } from "./useLoadGate.js";
6
7
  const BLOB_VERSION = "v0-draft";
@@ -70,6 +71,11 @@ function applyDetail(base, api) {
70
71
  edges: def.edges ?? [],
71
72
  triggerPositions: def.triggerPositions ?? {},
72
73
  variables: def.variables,
74
+ // Enumerated key by key like every block above it, so one missing here is
75
+ // one lost in this direction. Harmless today — a template becomes a workflow
76
+ // server-side, so this blob only feeds the preview — but the preview is
77
+ // where an author would look to see what a template remembers.
78
+ state: def.state,
73
79
  triggers
74
80
  };
75
81
  const md = resolveLocalized(api.description ?? "") || base.descriptionMd;
@@ -97,14 +103,11 @@ export function useIntegrationsTemplates() {
97
103
  const error = useState("integrations.templates.error", () => null);
98
104
  const gate = useLoadGate("integrations.templates", { loading, loaded });
99
105
  const api = useIntegrationsApi();
100
- function unwrap(res) {
101
- return res?.data ?? res;
102
- }
103
106
  async function load(force = false) {
104
107
  if (!await gate.claim(force)) return templates.value;
105
108
  error.value = null;
106
109
  try {
107
- const list2 = unwrap(await api.get("/templates")) ?? [];
110
+ const list2 = unwrapList(await api.get("/templates"));
108
111
  rawList.value = list2;
109
112
  rawDetails.value = {};
110
113
  loaded.value = true;
@@ -128,7 +131,7 @@ export function useIntegrationsTemplates() {
128
131
  const base = get(slug);
129
132
  if (!base?.fromApi) return base;
130
133
  try {
131
- const detail = unwrap(await api.get(`/templates/${slug}`));
134
+ const detail = unwrapRecord(await api.get(`/templates/${slug}`));
132
135
  rawDetails.value = { ...rawDetails.value, [slug]: detail };
133
136
  return get(slug);
134
137
  } catch {
@@ -147,8 +150,7 @@ export function useIntegrationsTemplates() {
147
150
  }
148
151
  async function requirements(slug) {
149
152
  try {
150
- const raw = unwrap(await api.get(`/templates/${slug}/requirements`));
151
- if (!raw) return null;
153
+ const raw = unwrapRecord(await api.get(`/templates/${slug}/requirements`));
152
154
  return {
153
155
  ...raw,
154
156
  credentialTypes: (raw.credentialTypes ?? []).map((c) => ({
@@ -167,7 +169,7 @@ export function useIntegrationsTemplates() {
167
169
  Object.entries(opts.credentialBindings ?? {}).filter(([, v]) => v)
168
170
  );
169
171
  if (Object.keys(bindings).length) body.credential_bindings = bindings;
170
- const created = unwrap(await api.post(`/templates/${slug}/instantiate`, body));
172
+ const created = unwrapRecord(await api.post(`/templates/${slug}/instantiate`, body));
171
173
  return Number(created.id);
172
174
  }
173
175
  async function install(slug) {
@@ -1,6 +1,7 @@
1
1
  import { useIntegrationsApi } from "./integrationsApi.js";
2
2
  import { useIntegrationsT } from "./useIntegrationsT.js";
3
3
  import { errorReason } from "../utils/apiError.js";
4
+ import { unwrapList } from "../utils/apiEnvelope.js";
4
5
  import { useLoadGate } from "./useLoadGate.js";
5
6
  export function tenantTriggers(workflows, byWorkflow) {
6
7
  return workflows.flatMap((wf) => (byWorkflow[wf.id] ?? []).flatMap((t) => {
@@ -24,10 +25,6 @@ export function useIntegrationsTriggers() {
24
25
  const t = useIntegrationsT();
25
26
  const { workflows, apiTriggers, load: loadWorkflows, loaded: workflowsLoaded, error: workflowsError } = useIntegrationsWorkflows();
26
27
  const gate = useLoadGate("integrations.triggers", { loading, loaded });
27
- function unwrapList(res) {
28
- if (Array.isArray(res)) return res;
29
- return res?.data ?? [];
30
- }
31
28
  async function list(workflowId) {
32
29
  return unwrapList(await api.get(`/workflows/${workflowId}/triggers`));
33
30
  }
@@ -3,6 +3,7 @@ import { useIntegrationsApi } from "./integrationsApi.js";
3
3
  import { useIntegrationsT } from "./useIntegrationsT.js";
4
4
  import { deepClone } from "../utils/deepClone.js";
5
5
  import { errorReason } from "../utils/apiError.js";
6
+ import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
6
7
  import { useLoadGate } from "./useLoadGate.js";
7
8
  const BLOB_VERSION = "v0-draft";
8
9
  function toCockpit(api) {
@@ -20,6 +21,7 @@ function toCockpit(api) {
20
21
  nodes: apiBlob.nodes ?? [],
21
22
  edges: apiBlob.edges ?? [],
22
23
  variables: apiBlob.variables ?? void 0,
24
+ state: apiBlob.state ?? void 0,
23
25
  triggerPositions: apiBlob.triggerPositions ?? {},
24
26
  stickies: apiBlob.stickies ?? [],
25
27
  groups: apiBlob.groups ?? [],
@@ -64,6 +66,11 @@ function toPayload(meta) {
64
66
  ...b.groups?.length ? { groups: b.groups } : {},
65
67
  // variables were silently dropped before — the schema always allowed them.
66
68
  ...b.variables && Object.keys(b.variables).length ? { variables: b.variables } : {},
69
+ // Same shape, and the same trap: both mappers enumerate blob keys, so a
70
+ // block missing from either one is lost in that direction. `state` is the
71
+ // declaration that makes the run grant reachable at all (PO-374) — losing
72
+ // it on save would take the workflow's memory away without a message.
73
+ ...b.state?.length ? { state: b.state } : {},
67
74
  ...b.tags?.length ? { tags: b.tags } : {},
68
75
  ...b.icon ? { icon: b.icon } : {}
69
76
  },
@@ -87,9 +94,6 @@ export function useIntegrationsWorkflows() {
87
94
  const gate = useLoadGate("integrations.workflows", { loading, loaded });
88
95
  const api = useIntegrationsApi();
89
96
  const t = useIntegrationsT();
90
- function unwrap(res) {
91
- return res?.data ?? res;
92
- }
93
97
  function remember(api2, into) {
94
98
  if (api2.triggers !== void 0) into[Number(api2.id)] = api2.triggers;
95
99
  }
@@ -116,7 +120,7 @@ export function useIntegrationsWorkflows() {
116
120
  if (!await gate.claim(force)) return workflows.value;
117
121
  error.value = null;
118
122
  try {
119
- const list2 = unwrap(await api.get("/workflows")) ?? [];
123
+ const list2 = unwrapList(await api.get("/workflows"));
120
124
  workflows.value = ingestList(list2);
121
125
  loaded.value = true;
122
126
  } catch (e) {
@@ -133,7 +137,7 @@ export function useIntegrationsWorkflows() {
133
137
  }
134
138
  async function get(id) {
135
139
  try {
136
- const wf = ingest(unwrap(await api.get(`/workflows/${id}`)));
140
+ const wf = ingest(unwrapRecord(await api.get(`/workflows/${id}`)));
137
141
  upsertCache(wf);
138
142
  return wf;
139
143
  } catch {
@@ -141,7 +145,7 @@ export function useIntegrationsWorkflows() {
141
145
  }
142
146
  }
143
147
  async function create(input) {
144
- const wf = ingest(unwrap(await api.post("/workflows", toPayload({
148
+ const wf = ingest(unwrapRecord(await api.post("/workflows", toPayload({
145
149
  name: input.name,
146
150
  description: input.description,
147
151
  blob: input.blob,
@@ -192,7 +196,7 @@ export function useIntegrationsWorkflows() {
192
196
  throw lastError instanceof Error ? lastError : new Error(t("integration.workflows.error.duplicateFailed"));
193
197
  }
194
198
  async function update(id, patch) {
195
- const wf = ingest(unwrap(await api.put(`/workflows/${id}`, toPayload({
199
+ const wf = ingest(unwrapRecord(await api.put(`/workflows/${id}`, toPayload({
196
200
  name: patch.name,
197
201
  description: patch.description,
198
202
  blob: patch.blob,
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Every key the workflow canvas answers, in one list.
3
+ *
4
+ * WHY THIS FILE EXISTS. The bindings used to live only in the editor's keymap,
5
+ * which is `key → function` and carries no words at all — so each surface that
6
+ * shows a key wrote the token out again by hand: fifteen in the canvas context
7
+ * menu, twelve more in the control pane's tooltips, for twenty-two bindings.
8
+ * They had already drifted. The menu offered renaming as `F2` while the keymap
9
+ * answered `F2` *and* `Space`, and it showed `⌫` for a key the keymap does not
10
+ * hold at all — deleting is the canvas library's own, configured somewhere else
11
+ * entirely. Two of the twenty-two were shown nowhere.
12
+ *
13
+ * So the tokens live here, keyed by an id, and the three surfaces are readers:
14
+ * the keymap binds handlers by id, the menu and the tooltips ask for one id's
15
+ * keys, and the overview shows the list. Adding a key is one entry, and it
16
+ * reaches all three.
17
+ *
18
+ * WHAT IS NOT HERE. The handlers, because they belong to the surface that owns
19
+ * the canvas and close over its selection. And the menu's own labels: an entry
20
+ * there says "Copy {n} nodes" against this list's "Copy", because a menu speaks
21
+ * about what is under the pointer while an overview speaks in general. Only the
22
+ * TOKENS are shared, which is what actually drifted.
23
+ */
24
+ /** The heading a key is listed under. Ordered as the overview shows them. */
25
+ export declare const SHORTCUT_GROUPS: readonly ["nodes", "selection", "canvas", "view", "workflow"];
26
+ export type ShortcutGroup = typeof SHORTCUT_GROUPS[number];
27
+ export interface CanvasShortcut {
28
+ /** Stable id: what a handler, a menu item and a tooltip refer to. */
29
+ id: string;
30
+ /**
31
+ * The binding in the matcher's token form; `|` separates two gestures worth
32
+ * knowing about — renaming answers `F2` and `Space`, and neither can be
33
+ * guessed from the other.
34
+ *
35
+ * Everything bound is listed, and nothing is bound that is not: a key that
36
+ * answers without appearing here would be exactly the secret this file exists
37
+ * to abolish. Zooming in was briefly `+` with `=` bound quietly beside it, for
38
+ * the US keyboards that reach the same character that way — and zooming out has
39
+ * no such twin, so there was no symmetry to keep and one key each is the answer.
40
+ *
41
+ * Punctuation is written as the character, never as `shift+<key>`: a layout
42
+ * that needs Shift to type it says so in the event and the matcher drops that
43
+ * Shift, so `shift+/` would bind a US keyboard only.
44
+ */
45
+ keys: string;
46
+ /** The catalogue key for the words the overview shows. */
47
+ labelKey: string;
48
+ group: ShortcutGroup;
49
+ /**
50
+ * Answered by the canvas library rather than by the keymap, so there is no
51
+ * handler to bind — it is listed because an overview that omits the
52
+ * most-used key of the surface is worse than none.
53
+ */
54
+ library?: true;
55
+ }
56
+ export declare const CANVAS_SHORTCUTS: readonly [{
57
+ readonly id: "open";
58
+ readonly keys: "enter";
59
+ readonly labelKey: "integration.canvas.contextMenu.item.open";
60
+ readonly group: "nodes";
61
+ }, {
62
+ readonly id: "rename";
63
+ readonly keys: "f2|space";
64
+ readonly labelKey: "integration.canvas.contextMenu.item.rename";
65
+ readonly group: "nodes";
66
+ }, {
67
+ readonly id: "addNode";
68
+ readonly keys: "tab";
69
+ readonly labelKey: "integration.canvas.contextMenu.item.addNode";
70
+ readonly group: "nodes";
71
+ }, {
72
+ readonly id: "copy";
73
+ readonly keys: "mod+c";
74
+ readonly labelKey: "integration.canvas.contextMenu.item.copy";
75
+ readonly group: "nodes";
76
+ }, {
77
+ readonly id: "paste";
78
+ readonly keys: "mod+v";
79
+ readonly labelKey: "integration.canvas.contextMenu.item.paste";
80
+ readonly group: "nodes";
81
+ }, {
82
+ readonly id: "duplicate";
83
+ readonly keys: "mod+d";
84
+ readonly labelKey: "integration.canvas.contextMenu.item.duplicate";
85
+ readonly group: "nodes";
86
+ }, {
87
+ readonly id: "delete";
88
+ readonly keys: "backspace|delete";
89
+ readonly labelKey: "integration.canvas.shortcuts.item.delete";
90
+ readonly group: "nodes";
91
+ readonly library: true;
92
+ }, {
93
+ readonly id: "selectAll";
94
+ readonly keys: "mod+a";
95
+ readonly labelKey: "integration.canvas.contextMenu.item.selectAll";
96
+ readonly group: "selection";
97
+ }, {
98
+ readonly id: "clearSelection";
99
+ readonly keys: "escape";
100
+ readonly labelKey: "integration.canvas.contextMenu.item.clearSelection";
101
+ readonly group: "selection";
102
+ }, {
103
+ readonly id: "group";
104
+ readonly keys: "mod+g";
105
+ readonly labelKey: "integration.canvas.shortcuts.item.group";
106
+ readonly group: "selection";
107
+ }, {
108
+ readonly id: "pointerSelect";
109
+ readonly keys: "v";
110
+ readonly labelKey: "integration.editor.tool.select";
111
+ readonly group: "canvas";
112
+ }, {
113
+ readonly id: "pointerHand";
114
+ readonly keys: "h";
115
+ readonly labelKey: "integration.editor.tool.hand";
116
+ readonly group: "canvas";
117
+ }, {
118
+ readonly id: "addSticky";
119
+ readonly keys: "shift+s";
120
+ readonly labelKey: "integration.editor.tool.addSticky.tooltip";
121
+ readonly group: "canvas";
122
+ }, {
123
+ readonly id: "tidyUp";
124
+ readonly keys: "shift+alt+t";
125
+ readonly labelKey: "integration.editor.tool.tidyUp";
126
+ readonly group: "canvas";
127
+ }, {
128
+ readonly id: "overview";
129
+ readonly keys: "?";
130
+ readonly labelKey: "integration.canvas.shortcuts.item.overview";
131
+ readonly group: "canvas";
132
+ }, {
133
+ readonly id: "zoomIn";
134
+ readonly keys: "+";
135
+ readonly labelKey: "integration.editor.tool.zoomIn";
136
+ readonly group: "view";
137
+ }, {
138
+ readonly id: "zoomOut";
139
+ readonly keys: "-";
140
+ readonly labelKey: "integration.editor.tool.zoomOut";
141
+ readonly group: "view";
142
+ }, {
143
+ readonly id: "zoomReset";
144
+ readonly keys: "0";
145
+ readonly labelKey: "integration.canvas.shortcuts.item.zoomReset";
146
+ readonly group: "view";
147
+ }, {
148
+ readonly id: "fitView";
149
+ readonly keys: "1";
150
+ readonly labelKey: "integration.canvas.contextMenu.item.fitView";
151
+ readonly group: "view";
152
+ }, {
153
+ readonly id: "run";
154
+ readonly keys: "mod+enter";
155
+ readonly labelKey: "integration.canvas.contextMenu.item.runWorkflow";
156
+ readonly group: "workflow";
157
+ }, {
158
+ readonly id: "toggleActive";
159
+ readonly keys: "shift+a";
160
+ readonly labelKey: "integration.canvas.shortcuts.item.toggleActive";
161
+ readonly group: "workflow";
162
+ }, {
163
+ readonly id: "variables";
164
+ readonly keys: "shift+v";
165
+ readonly labelKey: "integration.editor.tool.variables.tooltip";
166
+ readonly group: "workflow";
167
+ }, {
168
+ readonly id: "state";
169
+ readonly keys: "shift+t";
170
+ readonly labelKey: "integration.canvas.shortcuts.item.state";
171
+ readonly group: "workflow";
172
+ }, {
173
+ readonly id: "settings";
174
+ readonly keys: "shift+alt+s";
175
+ readonly labelKey: "integration.editor.tool.settings.tooltip";
176
+ readonly group: "workflow";
177
+ }];
178
+ /**
179
+ * The ids, as a type.
180
+ *
181
+ * `as const` above is what makes this a union of the literal ids rather than
182
+ * `string` — so a mistyped id in a template (`primaryShortcut('zoomln')`) fails
183
+ * the type check instead of throwing while the editor renders, which is where
184
+ * the lookup below would otherwise report it.
185
+ */
186
+ export type ShortcutId = typeof CANVAS_SHORTCUTS[number]['id'];
187
+ /**
188
+ * The catalogue key per group heading, written out rather than composed.
189
+ *
190
+ * A key built from the group name (`…group.${g}`) would work and would be
191
+ * invisible to every search for it — including the catalogue's own record of
192
+ * where a string is used, which is maintained by hand.
193
+ */
194
+ export declare const SHORTCUT_GROUP_LABELS: Record<ShortcutGroup, string>;
195
+ /** One id's binding, every way of pressing it included. */
196
+ export declare function shortcutKeys(id: ShortcutId): string;
197
+ /** Its gestures one by one, which is a cap each in the overview. */
198
+ export declare function shortcutCaps(id: ShortcutId): string[];
199
+ /**
200
+ * The one way of pressing it that a hint shows.
201
+ *
202
+ * A menu row and a tooltip have room for one gesture, and showing `F2|Space`
203
+ * there would read as a key nobody has. The overview is where both appear —
204
+ * which is also how `Space` stopped being a key the product answers and names
205
+ * nowhere.
206
+ */
207
+ export declare function primaryShortcut(id: ShortcutId): string;
208
+ export declare function libraryKeyCodes(id: ShortcutId): string[];
@@ -0,0 +1,68 @@
1
+ export const SHORTCUT_GROUPS = ["nodes", "selection", "canvas", "view", "workflow"];
2
+ export const CANVAS_SHORTCUTS = [
3
+ // ── A node, and what is done to it ────────────────────────────────────────
4
+ { id: "open", keys: "enter", labelKey: "integration.canvas.contextMenu.item.open", group: "nodes" },
5
+ { id: "rename", keys: "f2|space", labelKey: "integration.canvas.contextMenu.item.rename", group: "nodes" },
6
+ { id: "addNode", keys: "tab", labelKey: "integration.canvas.contextMenu.item.addNode", group: "nodes" },
7
+ { id: "copy", keys: "mod+c", labelKey: "integration.canvas.contextMenu.item.copy", group: "nodes" },
8
+ { id: "paste", keys: "mod+v", labelKey: "integration.canvas.contextMenu.item.paste", group: "nodes" },
9
+ { id: "duplicate", keys: "mod+d", labelKey: "integration.canvas.contextMenu.item.duplicate", group: "nodes" },
10
+ { id: "delete", keys: "backspace|delete", labelKey: "integration.canvas.shortcuts.item.delete", group: "nodes", library: true },
11
+ // ── The selection ─────────────────────────────────────────────────────────
12
+ { id: "selectAll", keys: "mod+a", labelKey: "integration.canvas.contextMenu.item.selectAll", group: "selection" },
13
+ { id: "clearSelection", keys: "escape", labelKey: "integration.canvas.contextMenu.item.clearSelection", group: "selection" },
14
+ { id: "group", keys: "mod+g", labelKey: "integration.canvas.shortcuts.item.group", group: "selection" },
15
+ // ── Working on the canvas itself ──────────────────────────────────────────
16
+ { id: "pointerSelect", keys: "v", labelKey: "integration.editor.tool.select", group: "canvas" },
17
+ { id: "pointerHand", keys: "h", labelKey: "integration.editor.tool.hand", group: "canvas" },
18
+ { id: "addSticky", keys: "shift+s", labelKey: "integration.editor.tool.addSticky.tooltip", group: "canvas" },
19
+ { id: "tidyUp", keys: "shift+alt+t", labelKey: "integration.editor.tool.tidyUp", group: "canvas" },
20
+ // Last of the canvas's own keys, and not one of the workflow's: the overview is
21
+ // about the surface an operator is standing on, not about the thing on it. It
22
+ // lists itself so the key is learnt for next time, the way it is the only key a
23
+ // reader of this list has already found.
24
+ { id: "overview", keys: "?", labelKey: "integration.canvas.shortcuts.item.overview", group: "canvas" },
25
+ // ── What is on screen ─────────────────────────────────────────────────────
26
+ { id: "zoomIn", keys: "+", labelKey: "integration.editor.tool.zoomIn", group: "view" },
27
+ { id: "zoomOut", keys: "-", labelKey: "integration.editor.tool.zoomOut", group: "view" },
28
+ { id: "zoomReset", keys: "0", labelKey: "integration.canvas.shortcuts.item.zoomReset", group: "view" },
29
+ { id: "fitView", keys: "1", labelKey: "integration.canvas.contextMenu.item.fitView", group: "view" },
30
+ // ── The workflow the canvas is showing ────────────────────────────────────
31
+ { id: "run", keys: "mod+enter", labelKey: "integration.canvas.contextMenu.item.runWorkflow", group: "workflow" },
32
+ { id: "toggleActive", keys: "shift+a", labelKey: "integration.canvas.shortcuts.item.toggleActive", group: "workflow" },
33
+ { id: "variables", keys: "shift+v", labelKey: "integration.editor.tool.variables.tooltip", group: "workflow" },
34
+ { id: "state", keys: "shift+t", labelKey: "integration.canvas.shortcuts.item.state", group: "workflow" },
35
+ { id: "settings", keys: "shift+alt+s", labelKey: "integration.editor.tool.settings.tooltip", group: "workflow" }
36
+ ];
37
+ export const SHORTCUT_GROUP_LABELS = {
38
+ nodes: "integration.canvas.shortcuts.group.nodes",
39
+ selection: "integration.canvas.shortcuts.group.selection",
40
+ canvas: "integration.canvas.shortcuts.group.canvas",
41
+ view: "integration.canvas.shortcuts.group.view",
42
+ workflow: "integration.canvas.shortcuts.group.workflow"
43
+ };
44
+ const BY_ID = new Map(CANVAS_SHORTCUTS.map((s) => [s.id, s]));
45
+ export function shortcutKeys(id) {
46
+ const found = BY_ID.get(id);
47
+ if (!found) throw new Error(`Unknown canvas shortcut: ${id}`);
48
+ return found.keys;
49
+ }
50
+ export function shortcutCaps(id) {
51
+ return shortcutKeys(id).split("|");
52
+ }
53
+ export function primaryShortcut(id) {
54
+ return shortcutCaps(id)[0];
55
+ }
56
+ const LIBRARY_KEY_CODES = {
57
+ backspace: "Backspace",
58
+ delete: "Delete"
59
+ };
60
+ export function libraryKeyCodes(id) {
61
+ const entry = BY_ID.get(id);
62
+ if (!entry?.library) throw new Error(`Canvas shortcut ${id} is not the canvas library's own key`);
63
+ return shortcutKeys(id).split("|").map((token) => {
64
+ const code = LIBRARY_KEY_CODES[token];
65
+ if (!code) throw new Error(`No canvas-library key code known for ${token}`);
66
+ return code;
67
+ });
68
+ }
@@ -81,6 +81,31 @@ export declare function isVersionExecutable(slug: string, version: string): bool
81
81
  * only what is wrong.
82
82
  */
83
83
  export declare function newestExecutableVersion(slug: string): string | null;
84
+ /**
85
+ * Every version of this node the platform can execute, newest first — the
86
+ * choice offered when a step is moved onto another version (PO-339).
87
+ *
88
+ * Order is the catalogue's own: the registry returns each slug's versions
89
+ * semver-descending, which is the assumption `newestExecutableVersion` above and
90
+ * the palette's own list already rest on. So "newer than the pinned one" is
91
+ * "earlier in this array", and nothing here has to parse a semver.
92
+ *
93
+ * A version whose code was never built is left out, for the reason the palette
94
+ * leaves it out: it is not a choice, it is a dead end with a version number
95
+ * (PO-338). The version a step is pinned to is therefore not guaranteed to be in
96
+ * here — which is exactly the state the move exists to get out of.
97
+ */
98
+ export declare function executableVersionsOf(slug: string): NodeManifest[];
99
+ /**
100
+ * The versions of this node that are newer than the one pinned, and runnable.
101
+ *
102
+ * Empty when the step is already on the newest — which is the common case, and
103
+ * the reason the inspector can stay silent rather than telling most authors that
104
+ * their node is fine. A pinned version the catalogue does not carry counts as
105
+ * "nothing newer": there is no position to be newer *than*, and inventing one
106
+ * would put an upgrade notice on a node whose real problem is different.
107
+ */
108
+ export declare function newerExecutableVersions(slug: string, version: string): NodeManifest[];
84
109
  export declare function findManifest(slug: string, version?: string): NodeManifest | null;
85
110
  export declare const TRIGGER_OUTPUT_PORT = "default";
86
111
  export interface TriggerTemplate {
@@ -34,6 +34,14 @@ export function newestExecutableVersion(slug) {
34
34
  const hit = INTEGRATION_NODE_CATALOG.find((m) => m.slug === slug && m.__hasBundle !== false);
35
35
  return hit?.version ?? null;
36
36
  }
37
+ export function executableVersionsOf(slug) {
38
+ return INTEGRATION_NODE_CATALOG.filter((m) => m.slug === slug && m.__hasBundle !== false);
39
+ }
40
+ export function newerExecutableVersions(slug, version) {
41
+ const versions = executableVersionsOf(slug);
42
+ const at = versions.findIndex((m) => m.version === version);
43
+ return at < 0 ? [] : versions.slice(0, at);
44
+ }
37
45
  export function findManifest(slug, version) {
38
46
  const matches = INTEGRATION_NODE_CATALOG.filter((m) => m.slug === slug);
39
47
  if (!matches.length) return null;
@@ -0,0 +1,72 @@
1
+ import type { ConfigField, NodeManifest } from '../types/integrations.js';
2
+ /**
3
+ * What moving a step onto another version of its node costs (PO-339).
4
+ *
5
+ * A version is chosen when a node is inserted and, until this, never again — so
6
+ * the only route onto another one was to delete the step and build it back,
7
+ * losing its settings, its name, its note and its edges. Moving instead is
8
+ * cheap in the blob (one field changes) and expensive in trust: the two
9
+ * versions may not declare the same settings, and an author who is not told
10
+ * beforehand finds out by a value having quietly gone.
11
+ *
12
+ * This module answers "what would the move cost" and nothing else. It builds no
13
+ * words: labels are localised strings on the manifest, so the caller resolves
14
+ * them and this stays testable without an i18n context.
15
+ *
16
+ * **A value that is not there is not a loss.** Only settings the node actually
17
+ * holds are weighed. A field the author never filled has nothing to carry, drop
18
+ * or check, and listing it would bury the two or three that matter.
19
+ *
20
+ * **The comparison is over the config's own top-level keys**, which is what
21
+ * `nextConfig` can act on: the blob stores settings as one flat object, and
22
+ * removing `attachments` is removing one key from it. A version that changed
23
+ * something *inside* an object or an array element is caught as far as it can
24
+ * be — such a field lands in `review` rather than being silently carried — but
25
+ * the stale value inside it is not pruned. `specs/node-configuration.md`
26
+ * records that limit rather than this pretending to close it.
27
+ */
28
+ /** A setting the move has something to say about, with the field it was read from. */
29
+ export interface MoveEntry {
30
+ key: string;
31
+ /**
32
+ * The declaration to name it by. For a carried or reviewed setting that is the
33
+ * TARGET's field, because that is what the author will be looking at after the
34
+ * move; for a dropped one it is the SOURCE's, which is the only version that
35
+ * still has a word for it. `null` where neither declares it — a key left behind
36
+ * by an earlier move, which has no label anywhere and shows as its own name.
37
+ */
38
+ field: ConfigField | null;
39
+ /** Why it needs looking at. Only set on `review`. */
40
+ reason?: 'changed' | 'nowRequired';
41
+ }
42
+ export interface VersionMovePlan {
43
+ /** Settings that travel untouched: the target declares the same key, the same way. */
44
+ carried: MoveEntry[];
45
+ /** Settings the target does not declare. Their values are gone from `nextConfig`. */
46
+ dropped: MoveEntry[];
47
+ /** Settings that arrive but need the author's eye — see `reason`. Values are kept. */
48
+ review: MoveEntry[];
49
+ /** The config to save with the new version: the current one minus what was dropped. */
50
+ nextConfig: Record<string, unknown>;
51
+ /** Nothing is dropped and nothing needs checking — the move is a version change and no more. */
52
+ lossless: boolean;
53
+ /**
54
+ * Whether the version being left is still in the catalogue, so the two
55
+ * declarations could actually be held against each other. `false` for a
56
+ * withdrawn version: the move still works and nothing here is wrong, but
57
+ * `carried` then means "kept" rather than "checked and unchanged", and the
58
+ * dialog owes the author that sentence.
59
+ */
60
+ comparable: boolean;
61
+ }
62
+ /**
63
+ * What it would cost to move a step from `from` to `to`, given the settings it
64
+ * holds today.
65
+ *
66
+ * Either manifest may be `null` — a version the catalogue does not carry, which
67
+ * happens to a workflow pinned to something later withdrawn. A missing SOURCE
68
+ * only costs labels; a missing TARGET would drop every setting, so the caller
69
+ * must not offer a move onto a version it cannot find, and `planVersionMove`
70
+ * does not defend against being asked anyway.
71
+ */
72
+ export declare function planVersionMove(from: NodeManifest | null, to: NodeManifest | null, config: Record<string, unknown> | null | undefined): VersionMovePlan;