@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 +12 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/GroupFrameView.vue +17 -6
- package/dist/runtime/components/Integrations/NodeConfigField.vue +19 -5
- package/dist/runtime/components/Integrations/StickyNoteView.vue +22 -9
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +3 -2
- package/dist/runtime/composables/useCanvasTarget.d.ts +15 -0
- package/dist/runtime/composables/useCanvasTarget.js +10 -0
- package/package.json +1 -1
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
|
@@ -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
|
|
219
|
-
:style="{
|
|
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
|
-
<
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
<
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
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.
|
|
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": {
|