@revenexx/studio-integrations 3.8.0 → 3.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @revenexx/studio-integrations
2
2
 
3
+ ## 3.8.2
4
+
5
+ ### Patch Changes
6
+
7
+ - b641ccf: A setting that holds a group of values, and whose node lets it take a value mapped from an earlier step, now has the same switch beside it as a number or a choice. The switch turns it into the box with the `{ }` list, and where the setting says what it wants and exactly one earlier output fits, that output is offered in a click. Send Email's Attachments can so be filled with the file a step before built, without typing the expression into JSON. A value already mapped into such a setting opens as the mapping; switching back empties it into its rows. A list of several mapped values stays JSON.
8
+
9
+ ## 3.8.1
10
+
11
+ ### Patch Changes
12
+
13
+ - f5c169c: The insert and delete buttons on a connection, and the corners a sticky note and a group frame are resized by, are now at least 24 pixels on screen at every zoom level. They used to shrink with the canvas: a workflow of eighteen steps opens at 58 %, and there they measured 14, 11.6 and 9.3 pixels. The connection's buttons now keep their size on screen, as the node's toolbar already did. The resize corners look the same as before, and the area that reacts to the pointer reaches outward past the note or frame, so a bigger target covers none of the content. The pencil and the note badge on a node card are not changed here.
14
+
3
15
  ## 3.8.0
4
16
 
5
17
  ### Minor Changes
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
3
  "configKey": "studioIntegrations",
4
- "version": "3.8.0",
4
+ "version": "3.8.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
@@ -6,6 +6,7 @@ import { ChevronsDownUp, ChevronsUpDown, Ungroup } from "lucide-vue-next";
6
6
  import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
7
7
  import { inputHandleId, outputHandleId } from "../../data/handleId";
8
8
  import { resolveDisplayName } from "../../utils/localize";
9
+ import { useCanvasTarget } from "../../composables/useCanvasTarget";
9
10
  const props = defineProps({
10
11
  id: { type: String, required: true },
11
12
  data: { type: Object, required: true },
@@ -65,6 +66,8 @@ const resizing = ref(false);
65
66
  const liveW = ref(0);
66
67
  const liveH = ref(0);
67
68
  let resizeStart = { x: 0, y: 0, w: 0, h: 0 };
69
+ const RESIZE_CORNER = 20;
70
+ const resizeTarget = useCanvasTarget(RESIZE_CORNER);
68
71
  const frameW = computed(() => resizing.value ? liveW.value : props.data.rect.width);
69
72
  const frameH = computed(() => resizing.value ? liveH.value : props.data.rect.height);
70
73
  function onResizeStart(e) {
@@ -215,18 +218,26 @@ function onResizeEnd() {
215
218
  <!-- Bottom-right resize handle (editable frames only). -->
216
219
  <div
217
220
  v-if="editable"
218
- class="nodrag pointer-events-auto absolute bottom-0 right-0 flex size-5 cursor-se-resize items-end justify-end p-1"
219
- :style="{ color: color.chip }"
221
+ class="nodrag pointer-events-auto absolute cursor-se-resize"
222
+ :style="{
223
+ color: color.chip,
224
+ width: `${resizeTarget}px`,
225
+ height: `${resizeTarget}px`,
226
+ right: `${RESIZE_CORNER - resizeTarget}px`,
227
+ bottom: `${RESIZE_CORNER - resizeTarget}px`
228
+ }"
220
229
  :title="t('integration.canvas.group.action.resize')"
221
230
  @pointerdown="onResizeStart"
222
231
  @pointermove="onResizeMove"
223
232
  @pointerup="onResizeEnd"
224
233
  @pointercancel="onResizeEnd"
225
234
  >
226
- <svg viewBox="0 0 8 8" class="size-2.5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round">
227
- <path d="M7 1 1 7" />
228
- <path d="M7 4.5 4.5 7" />
229
- </svg>
235
+ <div class="flex size-5 items-end justify-end p-1">
236
+ <svg viewBox="0 0 8 8" class="size-2.5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round">
237
+ <path d="M7 1 1 7" />
238
+ <path d="M7 4.5 4.5 7" />
239
+ </svg>
240
+ </div>
230
241
  </div>
231
242
  </div>
232
243
  </div>
@@ -199,16 +199,28 @@ function acceptsSftp(field) {
199
199
  }
200
200
  const EXPR_TOKEN = "${{";
201
201
  const TYPED_KINDS = ["number", "boolean", "select", "multiselect"];
202
+ const MAPPABLE_KINDS = [...TYPED_KINDS, "object"];
202
203
  const valueIsExpression = computed(() => typeof value.value === "string" && value.value.includes(EXPR_TOKEN));
203
204
  const canToggleExpression = computed(
204
- () => (!!props.field.dynamic || TYPED_KINDS.includes(props.field.type)) && (!!props.field.expressionAllowed || valueIsExpression.value)
205
+ () => (!!props.field.dynamic || MAPPABLE_KINDS.includes(props.field.type)) && (!!props.field.expressionAllowed || valueIsExpression.value) && !holdsLiteralGroup.value
205
206
  );
207
+ const holdsLiteralGroup = computed(() => {
208
+ if (props.field.type !== "object") return false;
209
+ const v = value.value;
210
+ if (Array.isArray(v)) return v.length > 0;
211
+ return !!v && typeof v === "object" && Object.keys(v).length > 0;
212
+ });
206
213
  const exprMode = ref(false);
214
+ watch(canToggleExpression, (can) => {
215
+ if (!can) exprMode.value = false;
216
+ });
207
217
  const showExpression = computed(() => canToggleExpression.value && (exprMode.value || valueIsExpression.value));
208
218
  function toggleExpression() {
209
219
  if (showExpression.value) {
210
- if (valueIsExpression.value) write(void 0);
220
+ const cleared = valueIsExpression.value || props.field.type === "object" && typeof value.value === "string";
221
+ if (cleared) write(void 0);
211
222
  exprMode.value = false;
223
+ if (isObjectField.value && (cleared || flatCapable.value)) objectMode.value = "form";
212
224
  } else {
213
225
  exprMode.value = true;
214
226
  }
@@ -269,7 +281,7 @@ const isQueryField = computed(
269
281
  () => props.field.type === "array" && props.field.items?.type !== "object" && /(^|__)queries$/.test(props.field.key)
270
282
  );
271
283
  const isJsonField = computed(
272
- () => isObjectField.value && objectMode.value === "json" || props.field.type === "array" && props.field.items?.type !== "object" && !isQueryField.value
284
+ () => isObjectField.value && objectMode.value === "json" && !showExpression.value || props.field.type === "array" && props.field.items?.type !== "object" && !isQueryField.value
273
285
  );
274
286
  const jsonError = ref(null);
275
287
  const jsonDraft = ref("");
@@ -402,7 +414,9 @@ function setArrayItemField(index, fieldKey, v) {
402
414
  </div>
403
415
  <!-- object / plain-array fields: JSON editor (expressions stay usable
404
416
  inside string values). Wins over expressionAllowed — an object value
405
- in a text input would render as [object Object]. -->
417
+ in a text input would render as [object Object] — except for an object
418
+ switched to, or saved as, a mapped value, which is the expression box
419
+ below (PO-608). -->
406
420
  <!-- queries parameter: filter builder -->
407
421
  <!-- vpn-route — public internet, or a route of one of the tenant's VPN
408
422
  connections, of the kind the node asks for (specs/credential-network.md
@@ -437,7 +451,7 @@ function setArrayItemField(index, fieldKey, v) {
437
451
  </template>
438
452
 
439
453
  <!-- object field in key/value mode -->
440
- <div v-else-if="isObjectField && objectMode === 'form'" class="space-y-1.5">
454
+ <div v-else-if="isObjectField && objectMode === 'form' && !showExpression" class="space-y-1.5">
441
455
  <div v-for="(row, idx) in kvRows" :key="idx" class="flex items-center gap-1.5">
442
456
  <!-- A repeating row has no room for visible labels, so each control
443
457
  names itself and says which row of which field it belongs to. -->
@@ -3,6 +3,7 @@ import { computed, nextTick, ref, watch } from "vue";
3
3
  import { Position, useVueFlow } from "@vue-flow/core";
4
4
  import { NodeToolbar } from "@vue-flow/node-toolbar";
5
5
  import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
6
+ import { useCanvasTarget } from "../../composables/useCanvasTarget";
6
7
  const props = defineProps({
7
8
  id: { type: String, required: true },
8
9
  data: { type: Object, required: true },
@@ -34,6 +35,8 @@ const MIN_HEIGHT = 80;
34
35
  const width = ref(props.data.sticky.size?.width ?? DEFAULT_WIDTH);
35
36
  const height = ref(props.data.sticky.size?.height ?? DEFAULT_HEIGHT);
36
37
  const resizing = ref(false);
38
+ const RESIZE_CORNER = 16;
39
+ const resizeTarget = useCanvasTarget(RESIZE_CORNER);
37
40
  watch(() => props.data.sticky.size, (s) => {
38
41
  if (resizing.value) return;
39
42
  width.value = s?.width ?? DEFAULT_WIDTH;
@@ -191,7 +194,7 @@ function onViewMouseDown(e) {
191
194
 
192
195
  <!-- Card -->
193
196
  <div
194
- class="relative flex flex-col overflow-hidden rounded-lg border text-sm leading-snug text-stone-700 shadow-sm"
197
+ class="relative flex flex-col rounded-lg border text-sm leading-snug text-stone-700 shadow-sm"
195
198
  :class="[
196
199
  selected ? 'ring-2 ring-emerald-400/60' : '',
197
200
  editing ? '' : 'cursor-grab active:cursor-grabbing'
@@ -209,7 +212,7 @@ function onViewMouseDown(e) {
209
212
  <!-- View mode -->
210
213
  <div
211
214
  v-if="!editing"
212
- class="h-full w-full overflow-hidden whitespace-pre-wrap break-words px-3 py-2"
215
+ class="h-full w-full overflow-hidden rounded-[inherit] whitespace-pre-wrap break-words px-3 py-2"
213
216
  @mousedown="onViewMouseDown"
214
217
  >
215
218
  <!-- eslint-disable-next-line vue/no-v-html -- renderMarkdownLite escapes the note before it adds a tag -->
@@ -228,7 +231,7 @@ function onViewMouseDown(e) {
228
231
  ref="textareaEl"
229
232
  v-model="draft"
230
233
  :aria-label="t('integration.canvas.sticky.textarea.aria')"
231
- class="nodrag nowheel h-full w-full resize-none bg-transparent px-3 py-2 text-sm leading-snug text-stone-700 outline-none placeholder:text-stone-400 focus:ring-2 focus:ring-inset focus:ring-stone-500/40"
234
+ class="nodrag nowheel h-full w-full resize-none rounded-[inherit] bg-transparent px-3 py-2 text-sm leading-snug text-stone-700 outline-none placeholder:text-stone-400 focus:ring-2 focus:ring-inset focus:ring-stone-500/40"
232
235
  :placeholder="t('integration.canvas.sticky.textarea.placeholder')"
233
236
  spellcheck="false"
234
237
  @blur="commitEdit"
@@ -239,20 +242,30 @@ function onViewMouseDown(e) {
239
242
 
240
243
  <!-- Bottom-right resize handle (hand-rolled; min 150x80). Gone on a
241
244
  read-only canvas: a handle that cannot resize is the same lie as a bin
242
- that cannot delete. -->
245
+ that cannot delete. The hit area reaches past the note's corner (see
246
+ `resizeTarget`) — which is why the card no longer clips its overflow —
247
+ and the drawn corner sits in its top-left, exactly where it always sat. -->
243
248
  <div
244
249
  v-if="editable"
245
- class="nodrag absolute bottom-0 right-0 flex size-4 cursor-se-resize items-end justify-end p-0.5 text-stone-400/80"
250
+ class="nodrag absolute cursor-se-resize"
251
+ :style="{
252
+ width: `${resizeTarget}px`,
253
+ height: `${resizeTarget}px`,
254
+ right: `${RESIZE_CORNER - resizeTarget}px`,
255
+ bottom: `${RESIZE_CORNER - resizeTarget}px`
256
+ }"
246
257
  :title="t('integration.canvas.sticky.action.resize')"
247
258
  @pointerdown="onResizeStart"
248
259
  @pointermove="onResizeMove"
249
260
  @pointerup="onResizeEnd"
250
261
  @pointercancel="onResizeEnd"
251
262
  >
252
- <svg viewBox="0 0 8 8" class="size-2.5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round">
253
- <path d="M7 1 1 7" />
254
- <path d="M7 4.5 4.5 7" />
255
- </svg>
263
+ <div class="flex size-4 items-end justify-end p-0.5 text-stone-400/80">
264
+ <svg viewBox="0 0 8 8" class="size-2.5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round">
265
+ <path d="M7 1 1 7" />
266
+ <path d="M7 4.5 4.5 7" />
267
+ </svg>
268
+ </div>
256
269
  </div>
257
270
  </div>
258
271
  </template>
@@ -92,6 +92,7 @@ function onHoverLeave() {
92
92
  }, 200);
93
93
  }
94
94
  const toolsVisible = computed(() => edgeActions !== null && !props.data?.replay && (hovered.value || isSelected.value));
95
+ const unzoom = computed(() => 1 / (flow.viewport.value.zoom || 1));
95
96
  function removeEdge() {
96
97
  flow.removeEdges([props.id]);
97
98
  }
@@ -128,7 +129,7 @@ function spliceNode() {
128
129
  <div
129
130
  class="absolute pointer-events-none select-none rounded-md border px-1.5 py-0.5 text-mono shadow-sm"
130
131
  :style="{
131
- transform: `translate(-50%, -50%) translate(${path[1]}px, ${path[2] - (toolsVisible ? 16 : 0)}px)`,
132
+ transform: `translate(-50%, -50%) translate(${path[1]}px, ${path[2] - (toolsVisible ? 16 * unzoom : 0)}px)`,
132
133
  color: color.label,
133
134
  background: color.bg,
134
135
  borderColor: color.stroke + '55'
@@ -142,7 +143,7 @@ function spliceNode() {
142
143
  <EdgeLabelRenderer v-if="toolsVisible">
143
144
  <div
144
145
  class="absolute z-20 flex items-center gap-0.5 rounded-md border border-black/[0.08] bg-white p-0.5 shadow-md"
145
- :style="{ transform: `translate(-50%, -50%) translate(${path[1]}px, ${path[2] + (props.data?.label ? 14 : 0)}px)`, pointerEvents: 'all' }"
146
+ :style="{ transform: `translate(-50%, -50%) translate(${path[1]}px, ${path[2] + (props.data?.label ? 14 * unzoom : 0)}px) scale(${unzoom})`, pointerEvents: 'all' }"
146
147
  @mouseenter="onHoverEnter"
147
148
  @mouseleave="onHoverLeave"
148
149
  >
@@ -0,0 +1,15 @@
1
+ import { type ComputedRef } from 'vue';
2
+ /**
3
+ * The smallest a pointer target may be on screen — WCAG 2.2 SC 2.5.8, promised in
4
+ * `specs/accessibility.md` AC-7.
5
+ */
6
+ export declare const MIN_TARGET_PX = 24;
7
+ /**
8
+ * The canvas-pixel size a control drawn **inside** the zoom transform needs so that it
9
+ * still measures `MIN_TARGET_PX` on screen, and never less than the `drawn` size it
10
+ * has at full zoom. Zoomed out to 50 %, a target has to be 48 canvas pixels to be hit
11
+ * as 24 — which is the whole problem with a control the canvas scales (PO-388).
12
+ */
13
+ export declare function targetSize(drawn: number, zoom: number): number;
14
+ /** `targetSize` against the live viewport of the canvas this component sits on. */
15
+ export declare function useCanvasTarget(drawn: number): ComputedRef<number>;
@@ -0,0 +1,10 @@
1
+ import { computed } from "vue";
2
+ import { useVueFlow } from "@vue-flow/core";
3
+ export const MIN_TARGET_PX = 24;
4
+ export function targetSize(drawn, zoom) {
5
+ return Math.max(drawn, MIN_TARGET_PX / (zoom || 1));
6
+ }
7
+ export function useCanvasTarget(drawn) {
8
+ const flow = useVueFlow();
9
+ return computed(() => targetSize(drawn, flow.viewport.value.zoom));
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
- "version": "3.8.0",
3
+ "version": "3.8.2",
4
4
  "type": "module",
5
5
  "description": "Integration Studio (core: workflows, node editor, runs, schedules, credentials, secrets, audit) as a standalone, publishable Nuxt module.",
6
6
  "repository": {