@revenexx/studio-integrations 1.1.1 → 1.3.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 (119) hide show
  1. package/CHANGELOG.md +638 -0
  2. package/README.md +3 -1
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
  7. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
  8. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  9. package/dist/runtime/components/Integrations/ExpressionInput.vue +45 -19
  10. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  11. package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
  12. package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
  13. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
  14. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
  15. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
  16. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +5 -1
  17. package/dist/runtime/components/Integrations/NodeConfigField.vue +88 -1
  18. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +5 -1
  19. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +7 -3
  20. package/dist/runtime/components/Integrations/NodeInspector.vue +125 -54
  21. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +7 -3
  22. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
  23. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
  24. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
  25. package/dist/runtime/components/Integrations/RunActionsMenu.vue +2 -1
  26. package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
  27. package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
  28. package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
  29. package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
  30. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
  31. package/dist/runtime/components/Integrations/RunsTable.vue +129 -18
  32. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
  33. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
  34. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
  35. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
  36. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
  37. package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
  38. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
  39. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
  40. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
  41. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
  42. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
  43. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
  44. package/dist/runtime/components/Integrations/WorkflowEditor.vue +78 -30
  45. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
  46. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
  47. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
  48. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
  49. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
  50. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
  51. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
  52. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
  53. package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
  54. package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
  55. package/dist/runtime/components/Integrations/canvasSides.js +22 -0
  56. package/dist/runtime/composables/useCredentialTypes.js +2 -6
  57. package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
  58. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
  59. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
  60. package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
  61. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  62. package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
  63. package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
  64. package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
  65. package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
  66. package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
  67. package/dist/runtime/composables/useIntegrationsTemplates.js +7 -5
  68. package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
  69. package/dist/runtime/composables/useIntegrationsWorkflows.js +10 -7
  70. package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
  71. package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
  72. package/dist/runtime/composables/useOAuthConnect.js +33 -0
  73. package/dist/runtime/data/expressionEditor.d.ts +17 -5
  74. package/dist/runtime/data/expressionEditor.js +11 -5
  75. package/dist/runtime/data/handleId.d.ts +61 -1
  76. package/dist/runtime/data/handleId.js +8 -1
  77. package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
  78. package/dist/runtime/data/integrationNodeCatalog.js +8 -0
  79. package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
  80. package/dist/runtime/data/nodeVersionMove.js +65 -0
  81. package/dist/runtime/data/payloadForm.js +3 -2
  82. package/dist/runtime/i18n/messages/de.json +79 -12
  83. package/dist/runtime/i18n/messages/en.json +79 -12
  84. package/dist/runtime/pages/integrations/audit.vue +7 -6
  85. package/dist/runtime/pages/integrations/credentials.vue +9 -21
  86. package/dist/runtime/pages/integrations/index.vue +55 -8
  87. package/dist/runtime/pages/integrations/runs.vue +116 -7
  88. package/dist/runtime/pages/integrations/secrets.vue +2 -1
  89. package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
  90. package/dist/runtime/pages/integrations/templates/index.vue +18 -15
  91. package/dist/runtime/pages/integrations/workflows/[id].vue +169 -24
  92. package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
  93. package/dist/runtime/types/integrations.d.ts +49 -1
  94. package/dist/runtime/utils/apiError.d.ts +21 -1
  95. package/dist/runtime/utils/apiError.js +1 -1
  96. package/dist/runtime/utils/buildSettle.d.ts +68 -0
  97. package/dist/runtime/utils/buildSettle.js +29 -0
  98. package/dist/runtime/utils/clipboard.d.ts +33 -0
  99. package/dist/runtime/utils/clipboard.js +11 -0
  100. package/dist/runtime/utils/deepClone.d.ts +41 -0
  101. package/dist/runtime/utils/deepClone.js +3 -0
  102. package/dist/runtime/utils/formatDuration.d.ts +44 -0
  103. package/dist/runtime/utils/formatDuration.js +13 -0
  104. package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
  105. package/dist/runtime/utils/nodeDisplayName.js +6 -0
  106. package/dist/runtime/utils/notify.d.ts +1 -1
  107. package/dist/runtime/utils/runEnvelope.d.ts +31 -0
  108. package/dist/runtime/utils/runEnvelope.js +12 -0
  109. package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
  110. package/dist/runtime/utils/runFeedQuery.js +66 -0
  111. package/dist/runtime/utils/runOutputs.d.ts +33 -0
  112. package/dist/runtime/utils/runOutputs.js +10 -0
  113. package/dist/runtime/utils/settingCondition.d.ts +48 -0
  114. package/dist/runtime/utils/settingCondition.js +84 -0
  115. package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
  116. package/dist/runtime/utils/stateNamespaceName.js +8 -0
  117. package/dist/runtime/utils/validationFindings.d.ts +26 -3
  118. package/dist/runtime/utils/validationFindings.js +18 -3
  119. package/package.json +1 -1
@@ -20,6 +20,7 @@ import {
20
20
  } from "@revenexx/studio";
21
21
  import { toast } from "vue-sonner";
22
22
  import { resolveLocalized } from "../../utils/localize";
23
+ import { errorReason } from "../../utils/apiError";
23
24
  const props = defineProps({
24
25
  open: { type: Boolean, required: true },
25
26
  credentialType: { type: [String, Array], required: false }
@@ -102,24 +103,11 @@ async function runTest() {
102
103
  testing.value = false;
103
104
  }
104
105
  }
106
+ const { open: openAuthorisation } = useOAuthConnect(() => {
107
+ void load(true);
108
+ });
105
109
  function connect(id) {
106
- return oauthAuthorizeUrl(id).then((authorizeUrl) => {
107
- const popup = window.open(authorizeUrl, "oauth-connect", "width=600,height=720");
108
- const onMessage = (ev) => {
109
- if (ev.data?.type === "integrations-oauth-complete") {
110
- window.removeEventListener("message", onMessage);
111
- void load(true);
112
- }
113
- };
114
- window.addEventListener("message", onMessage);
115
- const poll = window.setInterval(() => {
116
- if (popup?.closed) {
117
- window.clearInterval(poll);
118
- window.removeEventListener("message", onMessage);
119
- void load(true);
120
- }
121
- }, 1e3);
122
- });
110
+ return oauthAuthorizeUrl(id).then(openAuthorisation);
123
111
  }
124
112
  async function submit() {
125
113
  if (!pickedType.value || !form.name.trim()) return;
@@ -138,7 +126,7 @@ async function submit() {
138
126
  emit("created", created.id);
139
127
  emit("update:open", false);
140
128
  } catch (e) {
141
- toast.error(e?.message ?? t("integration.credentials.createDialog.error.create"));
129
+ toast.error(errorReason(e, t("integration.credentials.createDialog.error.create")));
142
130
  } finally {
143
131
  saving.value = false;
144
132
  }
@@ -30,7 +30,7 @@ import {
30
30
  SelectValue
31
31
  } from "@revenexx/studio";
32
32
  import { toast } from "../../utils/notify";
33
- import { apiFieldErrors, refusalReason } from "../../utils/apiError";
33
+ import { apiFieldErrors, errorReason } from "../../utils/apiError";
34
34
  import { credentialState, relativeTime } from "../../utils/credentialState";
35
35
  import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
36
36
  import { resolveLocalized } from "../../utils/localize";
@@ -122,7 +122,7 @@ async function save() {
122
122
  toast.success(t("integration.credentials.toast.updated"));
123
123
  } catch (e) {
124
124
  fieldErrors.value = apiFieldErrors(e);
125
- saveError.value = refusalReason(e, t("integration.credentials.error.save"));
125
+ saveError.value = errorReason(e, t("integration.credentials.error.save"));
126
126
  toast.error(saveError.value);
127
127
  } finally {
128
128
  saving.value = false;
@@ -139,36 +139,26 @@ async function destroy() {
139
139
  emit("changed");
140
140
  emit("update:open", false);
141
141
  } catch (e) {
142
- toast.error(e?.message ?? t("integration.credentials.error.delete"));
142
+ toast.error(errorReason(e, t("integration.credentials.error.delete")));
143
143
  } finally {
144
144
  deleting.value = false;
145
145
  }
146
146
  }
147
+ const { open: openAuthorisation, cancel: cancelAuthorisation } = useOAuthConnect(async (outcome) => {
148
+ await load(true);
149
+ emit("changed");
150
+ if (outcome === "completed") toast.success(t("integration.credentials.toast.connected"));
151
+ });
152
+ watch(() => props.open, (open) => {
153
+ if (!open) cancelAuthorisation();
154
+ });
147
155
  async function connect() {
148
156
  if (!props.credential) return;
149
157
  connecting.value = true;
150
158
  try {
151
- const authorizeUrl = await oauthAuthorizeUrl(props.credential.id);
152
- const popup = window.open(authorizeUrl, "oauth-connect", "width=600,height=720");
153
- const onMessage = async (ev) => {
154
- if (ev.data?.type === "integrations-oauth-complete") {
155
- window.removeEventListener("message", onMessage);
156
- await load(true);
157
- emit("changed");
158
- toast.success(t("integration.credentials.toast.connected"));
159
- }
160
- };
161
- window.addEventListener("message", onMessage);
162
- const poll = window.setInterval(async () => {
163
- if (popup?.closed) {
164
- window.clearInterval(poll);
165
- window.removeEventListener("message", onMessage);
166
- await load(true);
167
- emit("changed");
168
- }
169
- }, 1e3);
159
+ openAuthorisation(await oauthAuthorizeUrl(props.credential.id));
170
160
  } catch (e) {
171
- toast.error(e?.message ?? t("integration.credentials.error.oauthConnect"));
161
+ toast.error(errorReason(e, t("integration.credentials.error.oauthConnect")));
172
162
  } finally {
173
163
  connecting.value = false;
174
164
  }
@@ -25,6 +25,7 @@ import {
25
25
  import { toast } from "vue-sonner";
26
26
  import { REVENEXX_SOURCE_KIND_BY_SLUG } from "../../utils/revenexxCredentialSource";
27
27
  import { cockpitSources } from "../../utils/cockpitSources";
28
+ import { errorReason } from "../../utils/apiError";
28
29
  const props = defineProps({
29
30
  type: { type: [Object, null], required: true }
30
31
  });
@@ -93,7 +94,7 @@ async function ensureLists() {
93
94
  await fetchMarkets();
94
95
  }
95
96
  } catch (e) {
96
- toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.loadResources"));
97
+ toast.error(errorReason(e, t("integration.credentials.revenexxSource.error.loadResources")));
97
98
  } finally {
98
99
  loadingList.value = false;
99
100
  }
@@ -124,7 +125,7 @@ async function applySftp() {
124
125
  });
125
126
  applied.value = `${acc.login_username}@${sftpHost}:${sftpPort}`;
126
127
  } catch (e) {
127
- toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.readSftpPassword"));
128
+ toast.error(errorReason(e, t("integration.credentials.revenexxSource.error.readSftpPassword")));
128
129
  } finally {
129
130
  applying.value = false;
130
131
  }
@@ -147,7 +148,7 @@ async function applySmtp() {
147
148
  });
148
149
  applied.value = `${cred.smtp_username}@${mailSmtpHost}:${mailSmtpPort}`;
149
150
  } catch (e) {
150
- toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.readMailCredential"));
151
+ toast.error(errorReason(e, t("integration.credentials.revenexxSource.error.readMailCredential")));
151
152
  } finally {
152
153
  applying.value = false;
153
154
  }
@@ -202,7 +203,7 @@ async function createSftp() {
202
203
  createOpen.value = false;
203
204
  toast.success(t("integration.credentials.revenexxSource.toast.sftpCreated"));
204
205
  } catch (e) {
205
- toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createSftp"));
206
+ toast.error(errorReason(e, t("integration.credentials.revenexxSource.error.createSftp")));
206
207
  } finally {
207
208
  creating.value = false;
208
209
  provisioningNote.value = "";
@@ -230,7 +231,7 @@ async function createSmtp() {
230
231
  createOpen.value = false;
231
232
  toast.success(t("integration.credentials.revenexxSource.toast.smtpCreated"));
232
233
  } catch (e) {
233
- toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createSmtp"));
234
+ toast.error(errorReason(e, t("integration.credentials.revenexxSource.error.createSmtp")));
234
235
  } finally {
235
236
  creating.value = false;
236
237
  }
@@ -246,7 +247,7 @@ async function createApiKey() {
246
247
  createOpen.value = false;
247
248
  toast.success(t("integration.credentials.revenexxSource.toast.apiKeyCreated"));
248
249
  } catch (e) {
249
- toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createApiKey"));
250
+ toast.error(errorReason(e, t("integration.credentials.revenexxSource.error.createApiKey")));
250
251
  } finally {
251
252
  creating.value = false;
252
253
  }
@@ -3,7 +3,9 @@ import { onMounted, ref, watch } from "vue";
3
3
  import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as TrashBin } from "#solar-icons";
4
4
  import { Badge, Button } from "@revenexx/studio";
5
5
  import { toast } from "vue-sonner";
6
+ import LoadingAnnouncement from "./LoadingAnnouncement.vue";
6
7
  import { deadLetterStatusClass, deadLetterStatusLabel } from "../../utils/statusStyle";
8
+ import { errorReason } from "../../utils/apiError";
7
9
  const props = defineProps({
8
10
  workflowId: { type: Number, required: true }
9
11
  });
@@ -27,7 +29,7 @@ async function load(p = page.value) {
27
29
  lastPage.value = res.lastPage;
28
30
  page.value = res.page;
29
31
  } catch (e) {
30
- error.value = e?.message ?? t("integration.deadLetters.error.load");
32
+ error.value = errorReason(e, t("integration.deadLetters.error.load"));
31
33
  } finally {
32
34
  loading.value = false;
33
35
  }
@@ -46,7 +48,7 @@ async function onReplay(dl) {
46
48
  toast.success(newId ? t("integration.deadLetters.toast.replay.started", { id: newId }) : t("integration.deadLetters.toast.replay.done"));
47
49
  await load();
48
50
  } catch (e) {
49
- toast.error(e?.message ?? t("integration.deadLetters.toast.replay.error"));
51
+ toast.error(errorReason(e, t("integration.deadLetters.toast.replay.error")));
50
52
  } finally {
51
53
  busyId.value = null;
52
54
  }
@@ -58,7 +60,7 @@ async function onDiscard(dl) {
58
60
  toast.success(t("integration.deadLetters.toast.discard.done"));
59
61
  await load();
60
62
  } catch (e) {
61
- toast.error(e?.message ?? t("integration.deadLetters.toast.discard.error"));
63
+ toast.error(errorReason(e, t("integration.deadLetters.toast.discard.error")));
62
64
  } finally {
63
65
  busyId.value = null;
64
66
  }
@@ -75,6 +77,15 @@ function fmt(iso) {
75
77
 
76
78
  <template>
77
79
  <div class="space-y-3">
80
+ <!-- Outside the branch below, so the words are on the page before the answer is
81
+ (PO-251). This panel is the workflow editor's own copy of the missed-runs
82
+ list; the region is named after the panel rather than the tab. -->
83
+ <LoadingAnnouncement
84
+ :label="t('integration.deadLetters.panel.title')"
85
+ :loading="loading && !items.length"
86
+ :text="t('integration.deadLetters.state.loading')"
87
+ />
88
+
78
89
  <div class="flex items-center justify-between">
79
90
  <div>
80
91
  <h3 class="text-sm font-semibold">{{ t("integration.deadLetters.panel.title") }}</h3>
@@ -92,7 +103,7 @@ function fmt(iso) {
92
103
  {{ error }}
93
104
  </div>
94
105
 
95
- <div v-if="loading && !items.length">
106
+ <div v-if="loading && !items.length" aria-hidden="true">
96
107
  <div class="rounded-lg border border-black/[0.06] bg-white divide-y divide-black/[0.04]">
97
108
  <div v-for="n in 4" :key="`sk-${n}`" class="flex items-start gap-3 px-3 py-3">
98
109
  <div class="h-5 w-16 shrink-0 rounded bg-black/[0.07] animate-pulse" />
@@ -14,6 +14,10 @@ type __VLS_Props = {
14
14
  invalid?: boolean;
15
15
  /** The setting's label, so the editable region has an accessible name. */
16
16
  label?: string;
17
+ /** The setting takes several lines: Enter opens one instead of being
18
+ * swallowed, and the box opens at four rows and grows to ten before it
19
+ * scrolls. */
20
+ multiline?: boolean;
17
21
  };
18
22
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
23
  "update:modelValue": (value: string) => any;
@@ -14,7 +14,8 @@ const props = defineProps({
14
14
  variables: { type: Object, required: false },
15
15
  upstreamOutputs: { type: Array, required: false },
16
16
  invalid: { type: Boolean, required: false },
17
- label: { type: String, required: false }
17
+ label: { type: String, required: false },
18
+ multiline: { type: Boolean, required: false }
18
19
  });
19
20
  const emit = defineEmits(["update:modelValue"]);
20
21
  const t = useIntegrationsT();
@@ -76,23 +77,40 @@ const ExprToken = Node.create({
76
77
  };
77
78
  }
78
79
  });
80
+ const lineMode = () => ({ multiline: props.multiline });
81
+ function editableAttributes() {
82
+ return {
83
+ "role": "textbox",
84
+ ...props.multiline ? { "aria-multiline": "true" } : {},
85
+ ...props.label ? { "aria-label": props.label } : {}
86
+ };
87
+ }
88
+ function endOfLine(line) {
89
+ const doc = editor.state.doc;
90
+ const index = Math.min(Math.max(line, 0), doc.childCount - 1);
91
+ let pos = 0;
92
+ doc.forEach((node, offset, i) => {
93
+ if (i === index) pos = offset + node.nodeSize - 1;
94
+ });
95
+ return pos;
96
+ }
79
97
  const editor = new Editor({
80
98
  extensions: [Document, Paragraph, Text, History, ExprToken],
81
- content: textToDoc(text.value),
99
+ content: textToDoc(text.value, lineMode()),
82
100
  editorProps: {
83
- // Single line: swallow Enter (paragraph splits would break the field).
84
- handleKeyDown: (_view, event) => event.key === "Enter" && !event.isComposing,
85
- // A contenteditable region is not a text box to assistive technology
86
- // unless it says so, and its label sits in a sibling element — so it would
87
- // otherwise be an anonymous blob to a screen reader.
88
- attributes: { role: "textbox", ...props.label ? { "aria-label": props.label } : {} }
101
+ // Single line: swallow Enter (paragraph splits would break the field). A
102
+ // multiline field wants exactly that split, so it lets the key through.
103
+ handleKeyDown: (_view, event) => !props.multiline && event.key === "Enter" && !event.isComposing,
104
+ attributes: editableAttributes()
89
105
  },
90
106
  onUpdate({ editor: ed }) {
91
107
  const json = ed.getJSON();
92
- let raw = docToText(json);
108
+ let raw = docToText(json, lineMode());
93
109
  if (hasRawTokenText(json)) {
94
- ed.commands.setContent(textToDoc(raw));
95
- raw = docToText(ed.getJSON());
110
+ const line = ed.state.selection.$from.index(0);
111
+ ed.commands.setContent(textToDoc(raw, lineMode()));
112
+ ed.commands.setTextSelection(endOfLine(line));
113
+ raw = docToText(ed.getJSON(), lineMode());
96
114
  }
97
115
  if (raw !== text.value) emit("update:modelValue", raw);
98
116
  },
@@ -106,10 +124,14 @@ onBeforeUnmount(() => {
106
124
  editor.destroy();
107
125
  });
108
126
  watch(text, (t2) => {
109
- if (docToText(editor.getJSON()) !== t2) {
110
- editor.commands.setContent(textToDoc(t2));
127
+ if (docToText(editor.getJSON(), lineMode()) !== t2) {
128
+ editor.commands.setContent(textToDoc(t2, lineMode()));
111
129
  }
112
130
  });
131
+ watch(() => props.multiline, () => {
132
+ editor.setOptions({ editorProps: { ...editor.options.editorProps, attributes: editableAttributes() } });
133
+ editor.commands.setContent(textToDoc(text.value, lineMode()));
134
+ });
113
135
  function insertToken(expr) {
114
136
  if (editor.isDestroyed) return;
115
137
  editor.chain().focus().insertContent({ type: EXPR_TOKEN_NODE, attrs: { token: expr } }).run();
@@ -208,14 +230,18 @@ function insert(expr) {
208
230
  <div ref="wrapRef" class="relative" @focusout="onFocusOut" @keydown="onKeydown">
209
231
  <div class="flex items-center gap-1">
210
232
  <div
211
- class="expr-editor relative h-8 min-w-0 flex-1 overflow-hidden rounded-md border bg-white px-2 text-sm transition focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-ring"
212
- :class="invalid ? 'border-rose-400' : 'border-black/[0.08]'"
233
+ class="expr-editor relative min-w-0 flex-1 rounded-md border bg-white px-2 text-sm transition focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-ring"
234
+ :class="[
235
+ multiline ? 'expr-multiline' : 'h-8 overflow-hidden',
236
+ invalid ? 'border-rose-400' : 'border-black/[0.08]'
237
+ ]"
213
238
  >
214
239
  <span
215
240
  v-if="!text"
216
- class="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 truncate text-muted-foreground/60"
241
+ class="pointer-events-none absolute left-2 truncate text-muted-foreground/60"
242
+ :class="multiline ? 'top-1' : 'top-1/2 -translate-y-1/2'"
217
243
  >{{ placeholder }}</span>
218
- <EditorContent :editor="editor" class="h-full" @keydown="onKeydown" />
244
+ <EditorContent :editor="editor" :class="multiline ? '' : 'h-full'" @keydown="onKeydown" />
219
245
  </div>
220
246
  <!-- The name says what the button does AND what it opens, because the glyph
221
247
  says neither; `aria-expanded` says whether the list is there, and
@@ -230,7 +256,7 @@ function insert(expr) {
230
256
  :aria-expanded="listOpen"
231
257
  :aria-controls="listOpen ? pickerId : void 0"
232
258
  class="flex h-8 shrink-0 items-center rounded-md border border-black/[0.08] bg-white px-1.5 font-mono text-sm text-muted-foreground transition hover:text-foreground hover:bg-muted/30"
233
- :class="listOpen ? 'ring-2 ring-emerald-500/40' : ''"
259
+ :class="[listOpen ? 'ring-2 ring-emerald-500/40' : '', multiline ? 'self-start' : '']"
234
260
  @mousedown.prevent="toggle"
235
261
  @click="onTriggerClick"
236
262
  >
@@ -296,5 +322,5 @@ function insert(expr) {
296
322
  </template>
297
323
 
298
324
  <style>
299
- .expr-editor .ProseMirror{align-items:center;display:flex;height:100%;outline:none;overflow-x:auto;scrollbar-width:none}.expr-editor .ProseMirror p{line-height:30px;margin:0;white-space:pre}.expr-pill{align-items:center;background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.25);border-radius:6px;color:#047857;display:inline-flex;font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;height:20px;line-height:1;margin:0 2px;padding:0 7px;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;white-space:nowrap}.expr-pill.ProseMirror-selectednode{outline:2px solid rgba(16,185,129,.5)}
325
+ .expr-editor .ProseMirror{align-items:center;display:flex;height:100%;outline:none;overflow-x:auto;scrollbar-width:none}.expr-editor .ProseMirror p{line-height:30px;margin:0;white-space:pre}.expr-editor.expr-multiline{--expr-line:1.375rem;max-height:calc(var(--expr-line)*10 + .5rem);min-height:calc(var(--expr-line)*4 + .5rem);overflow-x:hidden;overflow-y:auto;padding-bottom:.25rem;padding-top:.25rem}.expr-editor.expr-multiline .ProseMirror{display:block;height:auto;overflow-x:visible}.expr-editor.expr-multiline .ProseMirror p{line-height:var(--expr-line);white-space:pre-wrap}.expr-pill{align-items:center;background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.25);border-radius:6px;color:#047857;display:inline-flex;font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;height:20px;line-height:1;margin:0 2px;padding:0 7px;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;white-space:nowrap}.expr-pill.ProseMirror-selectednode{outline:2px solid rgba(16,185,129,.5)}
300
326
  </style>
@@ -14,6 +14,10 @@ type __VLS_Props = {
14
14
  invalid?: boolean;
15
15
  /** The setting's label, so the editable region has an accessible name. */
16
16
  label?: string;
17
+ /** The setting takes several lines: Enter opens one instead of being
18
+ * swallowed, and the box opens at four rows and grows to ten before it
19
+ * scrolls. */
20
+ multiline?: boolean;
17
21
  };
18
22
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
23
  "update:modelValue": (value: string) => any;
@@ -4,6 +4,7 @@ import { Handle, Position, useVueFlow } from "@vue-flow/core";
4
4
  import { NodeToolbar } from "@vue-flow/node-toolbar";
5
5
  import { ChevronsDownUp, ChevronsUpDown, Ungroup } from "lucide-vue-next";
6
6
  import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
7
+ import { inputHandleId, outputHandleId } from "../../data/handleId";
7
8
  import { resolveDisplayName } from "../../utils/localize";
8
9
  const props = defineProps({
9
10
  id: { type: String, required: true },
@@ -100,9 +101,9 @@ function onResizeEnd() {
100
101
  v-for="(c, i) in GROUP_COLORS"
101
102
  :key="i"
102
103
  type="button"
103
- class="size-4.5 rounded-full border transition hover:scale-110"
104
+ class="size-6 rounded-full border transition hover:scale-110"
104
105
  :class="(data.group.color ?? 4) === i ? 'ring-2 ring-black/25 ring-offset-1' : ''"
105
- :style="{ backgroundColor: c.chip, borderColor: c.border, width: '16px', height: '16px' }"
106
+ :style="{ backgroundColor: c.chip, borderColor: c.border }"
106
107
  :title="t(c.nameKey)"
107
108
  :aria-label="t(c.nameKey)"
108
109
  @click="data.actions?.setColor(i)"
@@ -138,7 +139,7 @@ function onResizeEnd() {
138
139
  <!-- Collapsed: compact stand-in card with generic edge handles. -->
139
140
  <template v-if="collapsed">
140
141
  <Handle
141
- id="__in__top"
142
+ :id="inputHandleId('top')"
142
143
  type="target"
143
144
  :position="Position.Top"
144
145
  class="!size-3 !rounded-full !border-2 !border-white !bg-slate-500 !shadow-sm"
@@ -165,7 +166,7 @@ function onResizeEnd() {
165
166
  </div>
166
167
  </div>
167
168
  <Handle
168
- id="default__bottom"
169
+ :id="outputHandleId('default', 'bottom')"
169
170
  type="source"
170
171
  :position="Position.Bottom"
171
172
  class="!size-3 !rounded-full !border-2 !border-white !bg-slate-500 !shadow-sm"
@@ -0,0 +1,38 @@
1
+ /**
2
+ * What a surface is doing, in words, for somebody who cannot see the shimmer.
3
+ *
4
+ * A studio surface that is waiting draws placeholder blocks and — before PO-251 —
5
+ * said nothing, which is exactly what it says when there is genuinely nothing to
6
+ * show. Those two states ask for opposite reactions: wait, or go and do something
7
+ * about it. The shimmer separates them for a sighted reader and is not information a
8
+ * screen reader can reach.
9
+ *
10
+ * Three things this component exists to keep from drifting, all of them learned on
11
+ * the template gallery (PO-246), which was the first surface to get this right:
12
+ *
13
+ * - **It is always in the DOM**, and only its text changes. A live region that
14
+ * appears together with its content is a region assistive technology may never
15
+ * have been watching: what gets announced is a change *inside* an existing region.
16
+ * So mount this outside the `v-if` chain it describes, never inside the branch
17
+ * that draws the placeholders.
18
+ * - **It is named**, because a page can hold more than one. The template gallery
19
+ * draws one per card for its flow miniature; the dashboard has four panels that
20
+ * each load on their own. Two unnamed status regions on one page are two things
21
+ * neither a reader nor a test can tell apart.
22
+ * - **`aria-busy` is not what this is.** The attribute only asks assistive
23
+ * technology to defer exposing changes to an element, and on a role-less `div`
24
+ * that is not a live region NVDA and JAWS effectively ignore it — it expresses the
25
+ * state without keeping anything from being walked. The placeholders themselves
26
+ * get `aria-hidden` at the call site instead: a placeholder carries no
27
+ * information, so there is nothing to lose by removing it from the tree.
28
+ */
29
+ type __VLS_Props = {
30
+ /** Names the region, so several on one page stay distinguishable. */
31
+ label: string;
32
+ /** Whether the surface this speaks for is waiting. */
33
+ loading: boolean;
34
+ /** What to say while it is — "Loading the runs…", in the surface's own words. */
35
+ text: string;
36
+ };
37
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
+ export default _default;
@@ -0,0 +1,16 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ label: { type: String, required: true },
4
+ loading: { type: Boolean, required: true },
5
+ text: { type: String, required: true }
6
+ });
7
+ </script>
8
+
9
+ <template>
10
+ <p
11
+ role="status"
12
+ aria-live="polite"
13
+ :aria-label="props.label"
14
+ class="sr-only"
15
+ >{{ props.loading ? props.text : "" }}</p>
16
+ </template>
@@ -0,0 +1,38 @@
1
+ /**
2
+ * What a surface is doing, in words, for somebody who cannot see the shimmer.
3
+ *
4
+ * A studio surface that is waiting draws placeholder blocks and — before PO-251 —
5
+ * said nothing, which is exactly what it says when there is genuinely nothing to
6
+ * show. Those two states ask for opposite reactions: wait, or go and do something
7
+ * about it. The shimmer separates them for a sighted reader and is not information a
8
+ * screen reader can reach.
9
+ *
10
+ * Three things this component exists to keep from drifting, all of them learned on
11
+ * the template gallery (PO-246), which was the first surface to get this right:
12
+ *
13
+ * - **It is always in the DOM**, and only its text changes. A live region that
14
+ * appears together with its content is a region assistive technology may never
15
+ * have been watching: what gets announced is a change *inside* an existing region.
16
+ * So mount this outside the `v-if` chain it describes, never inside the branch
17
+ * that draws the placeholders.
18
+ * - **It is named**, because a page can hold more than one. The template gallery
19
+ * draws one per card for its flow miniature; the dashboard has four panels that
20
+ * each load on their own. Two unnamed status regions on one page are two things
21
+ * neither a reader nor a test can tell apart.
22
+ * - **`aria-busy` is not what this is.** The attribute only asks assistive
23
+ * technology to defer exposing changes to an element, and on a role-less `div`
24
+ * that is not a live region NVDA and JAWS effectively ignore it — it expresses the
25
+ * state without keeping anything from being walked. The placeholders themselves
26
+ * get `aria-hidden` at the call site instead: a placeholder carries no
27
+ * information, so there is nothing to lose by removing it from the tree.
28
+ */
29
+ type __VLS_Props = {
30
+ /** Names the region, so several on one page stay distinguishable. */
31
+ label: string;
32
+ /** Whether the surface this speaks for is waiting. */
33
+ loading: boolean;
34
+ /** What to say while it is — "Loading the runs…", in the surface's own words. */
35
+ text: string;
36
+ };
37
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
+ export default _default;
@@ -3,6 +3,7 @@ import { computed } from "vue";
3
3
  import { Button } from "@revenexx/studio";
4
4
  import { IntegrationsApiError } from "../../utils/apiError";
5
5
  import { resolveLocalized } from "../../utils/localize";
6
+ import { applyingFields, staleKeys } from "../../utils/settingCondition";
6
7
  const props = defineProps({
7
8
  slug: { type: String, required: true },
8
9
  version: { type: String, required: true },
@@ -24,6 +25,13 @@ const q = useNodeConfigResolve({
24
25
  dependsOn: () => props.field.dependsOn
25
26
  });
26
27
  const resolvedFields = computed(() => q.data.value?.fields ?? []);
28
+ const applyingResolvedFields = computed(() => applyingFields(resolvedFields.value, props.config));
29
+ function writeResolved(key, value) {
30
+ emit("set", key, value);
31
+ const after = { ...props.config, [key]: value };
32
+ for (const stale of staleKeys(resolvedFields.value, key, after))
33
+ emit("set", stale, void 0);
34
+ }
27
35
  </script>
28
36
 
29
37
  <template>
@@ -67,7 +75,7 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
67
75
  <legend class="px-1 text-mono text-muted-foreground">
68
76
  {{ resolveLocalized(field.label) }}
69
77
  </legend>
70
- <template v-for="f in resolvedFields" :key="f.key">
78
+ <template v-for="f in applyingResolvedFields" :key="f.key">
71
79
  <!-- collision guard: a resolved key that shadows a static config field is skipped -->
72
80
  <p v-if="staticKeys.includes(f.key)" class="text-sm text-amber-600">
73
81
  {{ t("integration.nodeConfig.dynamicSchema.fieldCollision", { key: f.key }) }}
@@ -89,7 +97,7 @@ const resolvedFields = computed(() => q.data.value?.fields ?? []);
89
97
  :static-keys="staticKeys"
90
98
  :sibling-fields="resolvedFields"
91
99
  :depth="(depth ?? 0) + 1"
92
- @set="(k, v) => emit('set', k, v)"
100
+ @set="writeResolved"
93
101
  />
94
102
  </template>
95
103
 
@@ -1,4 +1,4 @@
1
- import type { ConfigField, WorkflowVariable } from '../../types/integrations.js';
1
+ import type { ConfigField, WorkflowStateNamespace, WorkflowStateRole, WorkflowStateVisibility, WorkflowVariable } from '../../types/integrations.js';
2
2
  type __VLS_Props = {
3
3
  field: ConfigField;
4
4
  config: Record<string, unknown>;
@@ -11,6 +11,8 @@ type __VLS_Props = {
11
11
  siblingFields?: ConfigField[];
12
12
  depth?: number;
13
13
  variables?: Record<string, WorkflowVariable>;
14
+ /** The workflow's declared state namespaces — the options a `state-ref` offers. */
15
+ stateNamespaces?: WorkflowStateNamespace[];
14
16
  upstreamOutputs?: Array<{
15
17
  nodeId: string;
16
18
  nodeName?: string;
@@ -22,7 +24,9 @@ type __VLS_Props = {
22
24
  };
23
25
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
24
26
  set: (key: string, value: unknown) => any;
27
+ "declare-state": (namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any;
25
28
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
26
29
  onSet?: ((key: string, value: unknown) => any) | undefined;
30
+ "onDeclare-state"?: ((namespace: string, role: WorkflowStateRole, visibility: WorkflowStateVisibility) => any) | undefined;
27
31
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
32
  export default _default;