@revenexx/studio-integrations 0.5.0 → 0.6.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.
- package/CHANGELOG.md +201 -0
- package/README.md +17 -4
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +15 -14
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +91 -19
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -21
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +4 -8
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -2
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +54 -25
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +12 -12
- package/dist/runtime/components/Integrations/Layout.vue +21 -2
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +10 -11
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +2 -5
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +160 -67
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +1075 -303
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
- package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +42 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +100 -34
- package/dist/runtime/components/Integrations/QueryBuilder.vue +8 -1
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -1
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +17 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +2 -1
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +109 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +7 -10
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +21 -37
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +23 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +22 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +170 -48
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +22 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +11 -2
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +189 -46
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -20
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +7 -2
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -2
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +30 -17
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +47 -7
- package/dist/runtime/composables/useCredentialTypes.js +4 -4
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +4 -4
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- package/dist/runtime/composables/useIntegrationsNodes.js +4 -4
- package/dist/runtime/composables/useIntegrationsRuns.js +11 -6
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +10 -7
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsTemplates.js +12 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +6 -6
- package/dist/runtime/composables/useIntegrationsWorkflows.js +8 -4
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- package/dist/runtime/data/branchReorder.d.ts +37 -0
- package/dist/runtime/data/branchReorder.js +71 -0
- package/dist/runtime/data/handleId.d.ts +23 -0
- package/dist/runtime/data/handleId.js +6 -0
- package/dist/runtime/data/integrationTemplates.d.ts +39 -0
- package/dist/runtime/data/integrationTemplates.js +15 -0
- package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
- package/dist/runtime/data/nodeIcons.d.ts +2 -0
- package/dist/runtime/data/nodeIcons.js +73 -0
- package/dist/runtime/data/nodeInputData.d.ts +16 -4
- package/dist/runtime/data/nodeInputData.js +4 -0
- package/dist/runtime/data/payloadForm.d.ts +9 -2
- package/dist/runtime/data/payloadForm.js +27 -3
- package/dist/runtime/data/portTone.d.ts +17 -0
- package/dist/runtime/data/portTone.js +7 -0
- package/dist/runtime/data/webhookAuth.d.ts +18 -0
- package/dist/runtime/data/webhookAuth.js +19 -0
- package/dist/runtime/data/workflowIconCatalog.js +2 -5
- package/dist/runtime/pages/integrations/audit.vue +91 -6
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +6 -4
- package/dist/runtime/pages/integrations/credentials.vue +119 -21
- package/dist/runtime/pages/integrations/index.vue +49 -44
- package/dist/runtime/pages/integrations/runs.vue +1 -1
- package/dist/runtime/pages/integrations/schedules.vue +51 -7
- package/dist/runtime/pages/integrations/secrets.vue +29 -7
- package/dist/runtime/pages/integrations/templates/[slug].vue +52 -30
- package/dist/runtime/pages/integrations/templates/index.vue +57 -86
- package/dist/runtime/pages/integrations/workflows/[id].vue +243 -25
- package/dist/runtime/pages/integrations/workflows/index.vue +15 -2
- package/dist/runtime/types/integrations.d.ts +65 -6
- package/dist/runtime/utils/apiError.d.ts +47 -0
- package/dist/runtime/utils/apiError.js +32 -0
- package/dist/runtime/utils/cockpitSources.d.ts +153 -0
- package/dist/runtime/utils/cockpitSources.js +14 -0
- package/dist/runtime/utils/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +29 -0
- package/dist/runtime/utils/localize.js +4 -0
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -0
- package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
- package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
- package/dist/runtime/utils/schedule.d.ts +141 -0
- package/dist/runtime/utils/schedule.js +207 -0
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +53 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/package.json +8 -2
|
@@ -20,6 +20,12 @@ type __VLS_Props = {
|
|
|
20
20
|
dynamicOutputs?: Record<string, NodeManifestOutput[]>;
|
|
21
21
|
/** Workflow id — enables sampling real values from the latest run. */
|
|
22
22
|
workflowId?: number | string;
|
|
23
|
+
/**
|
|
24
|
+
* Config key → why the LAST SAVE refused it. Not live validation: the server
|
|
25
|
+
* is the only judge, so a fix shows once it is saved. Absent for a node the
|
|
26
|
+
* last save had no complaint about.
|
|
27
|
+
*/
|
|
28
|
+
refusedFields?: Record<string, string>;
|
|
23
29
|
};
|
|
24
30
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
25
31
|
close: () => any;
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Use `NodeMark` directly. This is a thin back-compat shim kept so
|
|
3
|
+
* that hosts still referencing the auto-registered `<IntegrationsNodeLogo>`
|
|
4
|
+
* (e.g. integrations/studio-ui `local-nodes.vue`) keep resolving the component
|
|
5
|
+
* after PO-194 renamed NodeLogo → NodeMark. Auto-registered components are
|
|
6
|
+
* public API surface for the hosts, so a silent delete breaks them at a
|
|
7
|
+
* different repo's build, invisible to this repo's CI.
|
|
8
|
+
*
|
|
9
|
+
* The old NodeLogo rendered "logo OR glyph, logo preferred" — exactly
|
|
10
|
+
* `NodeMark`'s `logo-only` behaviour (logo when present, glyph fallback). Drop
|
|
11
|
+
* this shim one release after the hosts have migrated to `NodeMark`.
|
|
12
|
+
*/
|
|
1
13
|
import type { NodeManifest } from '../../types/integrations.js';
|
|
2
14
|
type __VLS_Props = {
|
|
3
15
|
manifest: NodeManifest | null;
|
|
@@ -1,38 +1,10 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
|
|
3
|
-
import * as Lucide from "lucide-vue-next";
|
|
4
|
-
const props = defineProps({
|
|
2
|
+
defineProps({
|
|
5
3
|
manifest: { type: [Object, null], required: true },
|
|
6
4
|
size: { type: Number, required: false, default: 16 }
|
|
7
5
|
});
|
|
8
|
-
const failed = ref(false);
|
|
9
|
-
watch(() => props.manifest?.slug, () => {
|
|
10
|
-
failed.value = false;
|
|
11
|
-
});
|
|
12
|
-
const logoSrc = computed(() => {
|
|
13
|
-
const imgs = props.manifest?.images ?? [];
|
|
14
|
-
const logo = imgs.find((i) => i.category === "logo") ?? imgs[0];
|
|
15
|
-
return logo?.src ?? null;
|
|
16
|
-
});
|
|
17
|
-
const glyph = computed(() => {
|
|
18
|
-
const name = props.manifest?.icon;
|
|
19
|
-
if (!name) return Lucide.Box;
|
|
20
|
-
const cleaned = (name.includes(":") ? name.split(":").pop() : name).split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
21
|
-
return Lucide[cleaned] || Lucide.Box;
|
|
22
|
-
});
|
|
23
6
|
</script>
|
|
24
7
|
|
|
25
8
|
<template>
|
|
26
|
-
<
|
|
27
|
-
v-if="logoSrc && !failed"
|
|
28
|
-
:src="logoSrc"
|
|
29
|
-
:width="size"
|
|
30
|
-
:height="size"
|
|
31
|
-
class="shrink-0 rounded-[3px] object-contain"
|
|
32
|
-
:style="{ width: `${size}px`, height: `${size}px` }"
|
|
33
|
-
alt=""
|
|
34
|
-
loading="lazy"
|
|
35
|
-
@error="failed = true"
|
|
36
|
-
>
|
|
37
|
-
<component :is="glyph" v-else :size="size" />
|
|
9
|
+
<IntegrationsNodeMark :manifest="manifest" :size="size" logo-only />
|
|
38
10
|
</template>
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Use `NodeMark` directly. This is a thin back-compat shim kept so
|
|
3
|
+
* that hosts still referencing the auto-registered `<IntegrationsNodeLogo>`
|
|
4
|
+
* (e.g. integrations/studio-ui `local-nodes.vue`) keep resolving the component
|
|
5
|
+
* after PO-194 renamed NodeLogo → NodeMark. Auto-registered components are
|
|
6
|
+
* public API surface for the hosts, so a silent delete breaks them at a
|
|
7
|
+
* different repo's build, invisible to this repo's CI.
|
|
8
|
+
*
|
|
9
|
+
* The old NodeLogo rendered "logo OR glyph, logo preferred" — exactly
|
|
10
|
+
* `NodeMark`'s `logo-only` behaviour (logo when present, glyph fallback). Drop
|
|
11
|
+
* this shim one release after the hosts have migrated to `NodeMark`.
|
|
12
|
+
*/
|
|
1
13
|
import type { NodeManifest } from '../../types/integrations.js';
|
|
2
14
|
type __VLS_Props = {
|
|
3
15
|
manifest: NodeManifest | null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { NodeManifest } from '../../types/integrations.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
manifest: NodeManifest | null;
|
|
4
|
+
size?: number;
|
|
5
|
+
/** Render only the vendor logo (folder/group rows), never the glyph+badge composite. */
|
|
6
|
+
logoOnly?: boolean;
|
|
7
|
+
/** Overlay the vendor logo as a corner badge. Off in rows that already name the vendor. */
|
|
8
|
+
badge?: boolean;
|
|
9
|
+
};
|
|
10
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
11
|
+
size: number;
|
|
12
|
+
badge: boolean;
|
|
13
|
+
logoOnly: boolean;
|
|
14
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { nodeIcon } from "../../data/nodeIcons";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
manifest: { type: [Object, null], required: true },
|
|
6
|
+
size: { type: Number, required: false, default: 16 },
|
|
7
|
+
logoOnly: { type: Boolean, required: false, default: false },
|
|
8
|
+
badge: { type: Boolean, required: false, default: true }
|
|
9
|
+
});
|
|
10
|
+
const failed = ref(false);
|
|
11
|
+
const { load: reloadCatalog } = useIntegrationsNodes();
|
|
12
|
+
const refreshed = ref(false);
|
|
13
|
+
watch(() => props.manifest?.slug, () => {
|
|
14
|
+
failed.value = false;
|
|
15
|
+
refreshed.value = false;
|
|
16
|
+
});
|
|
17
|
+
function onImageError() {
|
|
18
|
+
if (!refreshed.value) {
|
|
19
|
+
refreshed.value = true;
|
|
20
|
+
void reloadCatalog(true);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
failed.value = true;
|
|
24
|
+
}
|
|
25
|
+
function onImageLoad() {
|
|
26
|
+
refreshed.value = false;
|
|
27
|
+
}
|
|
28
|
+
const logoSrc = computed(() => {
|
|
29
|
+
const imgs = props.manifest?.images ?? [];
|
|
30
|
+
const logo = imgs.find((i) => i.category === "logo") ?? imgs[0];
|
|
31
|
+
return logo?.src ?? null;
|
|
32
|
+
});
|
|
33
|
+
const hasIcon = computed(() => typeof props.manifest?.icon === "string" && props.manifest.icon.trim() !== "");
|
|
34
|
+
const glyph = computed(() => nodeIcon(props.manifest?.icon));
|
|
35
|
+
const logoOk = computed(() => !!logoSrc.value && !failed.value);
|
|
36
|
+
const logoPrimary = computed(() => logoOk.value && (props.logoOnly || !hasIcon.value));
|
|
37
|
+
const composite = computed(() => props.badge && !props.logoOnly && logoOk.value && hasIcon.value);
|
|
38
|
+
const badgeSize = computed(() => Math.max(12, Math.round(props.size * 0.72)));
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<!-- Logo as the primary mark (folder rows, or a node without its own glyph). -->
|
|
43
|
+
<img
|
|
44
|
+
v-if="logoPrimary"
|
|
45
|
+
:src="logoSrc"
|
|
46
|
+
:width="size"
|
|
47
|
+
:height="size"
|
|
48
|
+
class="shrink-0 rounded-[3px] object-contain"
|
|
49
|
+
:style="{ width: `${size}px`, height: `${size}px` }"
|
|
50
|
+
alt=""
|
|
51
|
+
loading="lazy"
|
|
52
|
+
@error="onImageError"
|
|
53
|
+
@load="onImageLoad"
|
|
54
|
+
>
|
|
55
|
+
<!-- Per-node glyph, with the vendor logo as a plated corner badge when present. -->
|
|
56
|
+
<span
|
|
57
|
+
v-else
|
|
58
|
+
class="relative inline-flex shrink-0"
|
|
59
|
+
:style="{ width: `${size}px`, height: `${size}px` }"
|
|
60
|
+
>
|
|
61
|
+
<component :is="glyph" :size="size" :stroke-width="2.25" />
|
|
62
|
+
<!-- The badge deliberately sits 3px OUTSIDE this box (integrations-ui
|
|
63
|
+
geometry): it overlaps the glyph's corner instead of shrinking the
|
|
64
|
+
glyph. Consequence for call sites — a chip hosting a mark has to leave
|
|
65
|
+
its overflow visible; an `overflow-hidden` wrapper clips the badge. -->
|
|
66
|
+
<img
|
|
67
|
+
v-if="composite"
|
|
68
|
+
:src="logoSrc"
|
|
69
|
+
:width="badgeSize"
|
|
70
|
+
:height="badgeSize"
|
|
71
|
+
class="absolute rounded-[3px] bg-white object-contain p-px ring-1 ring-black/10"
|
|
72
|
+
:style="{ width: `${badgeSize}px`, height: `${badgeSize}px`, right: '-3px', bottom: '-3px' }"
|
|
73
|
+
alt=""
|
|
74
|
+
loading="lazy"
|
|
75
|
+
@error="onImageError"
|
|
76
|
+
@load="onImageLoad"
|
|
77
|
+
>
|
|
78
|
+
</span>
|
|
79
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { NodeManifest } from '../../types/integrations.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
manifest: NodeManifest | null;
|
|
4
|
+
size?: number;
|
|
5
|
+
/** Render only the vendor logo (folder/group rows), never the glyph+badge composite. */
|
|
6
|
+
logoOnly?: boolean;
|
|
7
|
+
/** Overlay the vendor logo as a corner badge. Off in rows that already name the vendor. */
|
|
8
|
+
badge?: boolean;
|
|
9
|
+
};
|
|
10
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
11
|
+
size: number;
|
|
12
|
+
badge: boolean;
|
|
13
|
+
logoOnly: boolean;
|
|
14
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PortEntry } from '../../data/nodeInputData.js';
|
|
2
|
+
/**
|
|
3
|
+
* The ports a node hands downstream, with their declared fields and whatever
|
|
4
|
+
* sample value is known for each.
|
|
5
|
+
*
|
|
6
|
+
* Rendered from one place — the Output tab (specs/node-inspector.md AC-4). It
|
|
7
|
+
* keeps its own file for readability rather than for reuse: the schema list is
|
|
8
|
+
* a self-contained tree with its own type glyphs and sample formatting, and
|
|
9
|
+
* inlining it would bury the tab's own markup.
|
|
10
|
+
*
|
|
11
|
+
* It was briefly read from a second place, an always-visible summary strip
|
|
12
|
+
* above the form. That was AC-5, retired before it ever held — see the Gaps
|
|
13
|
+
* section of the spec. Do not go looking for the second call site.
|
|
14
|
+
*/
|
|
15
|
+
type __VLS_Props = {
|
|
16
|
+
ports: PortEntry[];
|
|
17
|
+
/** Sample preview for a port, or for one field of it; null when unknown. */
|
|
18
|
+
valueAt: (port: string, field?: string) => string | null;
|
|
19
|
+
};
|
|
20
|
+
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>;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { portDotClass } from "../../data/portTone";
|
|
3
|
+
defineProps({
|
|
4
|
+
ports: { type: Array, required: true },
|
|
5
|
+
valueAt: { type: Function, required: true }
|
|
6
|
+
});
|
|
7
|
+
const TYPE_GLYPHS = {
|
|
8
|
+
string: { char: "A", cls: "text-sky-600" },
|
|
9
|
+
number: { char: "#", cls: "text-amber-600" },
|
|
10
|
+
integer: { char: "#", cls: "text-amber-600" },
|
|
11
|
+
boolean: { char: "\u2713", cls: "text-emerald-600" },
|
|
12
|
+
object: { char: "{}", cls: "text-violet-600" },
|
|
13
|
+
array: { char: "[]", cls: "text-violet-600" }
|
|
14
|
+
};
|
|
15
|
+
function typeGlyph(dataType) {
|
|
16
|
+
return dataType ? TYPE_GLYPHS[dataType] ?? null : null;
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div>
|
|
22
|
+
<div v-for="p in ports" :key="p.name" class="mb-3 space-y-1">
|
|
23
|
+
<div class="flex items-center gap-1.5">
|
|
24
|
+
<span class="size-1.5 rounded-full" :class="portDotClass(p.kind, p.name)" />
|
|
25
|
+
<span class="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">{{ p.name }}</span>
|
|
26
|
+
</div>
|
|
27
|
+
<div v-if="p.fields.length" class="space-y-0.5">
|
|
28
|
+
<div v-for="f in p.fields" :key="f.name" class="flex min-w-0 items-baseline gap-1 font-mono text-[11px]">
|
|
29
|
+
<span v-if="typeGlyph(f.dataType)" class="shrink-0 font-semibold" :class="typeGlyph(f.dataType).cls">{{ typeGlyph(f.dataType).char }}</span>
|
|
30
|
+
<span class="shrink-0 text-slate-700">{{ f.name }}</span>
|
|
31
|
+
<span v-if="valueAt(p.name, f.name) !== null" class="min-w-0 truncate text-slate-400">: {{ valueAt(p.name, f.name) }}</span>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<p v-else class="text-[11px] leading-snug text-muted-foreground">
|
|
35
|
+
No declared fields<template v-if="valueAt(p.name) !== null">; sampled: <span class="font-mono">{{ valueAt(p.name) }}</span></template>.
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
<p v-if="!ports.length" class="text-xs leading-snug text-muted-foreground">
|
|
39
|
+
This node declares no outputs.
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PortEntry } from '../../data/nodeInputData.js';
|
|
2
|
+
/**
|
|
3
|
+
* The ports a node hands downstream, with their declared fields and whatever
|
|
4
|
+
* sample value is known for each.
|
|
5
|
+
*
|
|
6
|
+
* Rendered from one place — the Output tab (specs/node-inspector.md AC-4). It
|
|
7
|
+
* keeps its own file for readability rather than for reuse: the schema list is
|
|
8
|
+
* a self-contained tree with its own type glyphs and sample formatting, and
|
|
9
|
+
* inlining it would bury the tab's own markup.
|
|
10
|
+
*
|
|
11
|
+
* It was briefly read from a second place, an always-visible summary strip
|
|
12
|
+
* above the form. That was AC-5, retired before it ever held — see the Gaps
|
|
13
|
+
* section of the spec. Do not go looking for the second call site.
|
|
14
|
+
*/
|
|
15
|
+
type __VLS_Props = {
|
|
16
|
+
ports: PortEntry[];
|
|
17
|
+
/** Sample preview for a port, or for one field of it; null when unknown. */
|
|
18
|
+
valueAt: (port: string, field?: string) => string | null;
|
|
19
|
+
};
|
|
20
|
+
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>;
|
|
21
|
+
export default _default;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
3
|
import * as Lucide from "lucide-vue-next";
|
|
4
4
|
import { INTEGRATION_NODE_CATALOG, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
5
|
+
import { nodeIcon } from "../../data/nodeIcons";
|
|
6
|
+
import { resolveLocalized } from "../../utils/localize";
|
|
5
7
|
const { load: loadNodes } = useIntegrationsNodes();
|
|
6
8
|
onMounted(() => {
|
|
7
9
|
void loadNodes();
|
|
@@ -10,15 +12,7 @@ const emit = defineEmits(["insert"]);
|
|
|
10
12
|
const search = ref("");
|
|
11
13
|
const searchInput = ref(null);
|
|
12
14
|
defineExpose({ focusSearch: () => searchInput.value?.focus() });
|
|
13
|
-
|
|
14
|
-
if (!value) return "";
|
|
15
|
-
return typeof value === "string" ? value : value.en ?? Object.values(value)[0] ?? "";
|
|
16
|
-
}
|
|
17
|
-
function iconFor(name) {
|
|
18
|
-
if (!name) return Lucide.Box;
|
|
19
|
-
const cleaned = (name.includes(":") ? name.split(":").pop() : name).split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
20
|
-
return Lucide[cleaned] || Lucide.Box;
|
|
21
|
-
}
|
|
15
|
+
const iconFor = nodeIcon;
|
|
22
16
|
const CORE_GROUP_BY_CATEGORY = {
|
|
23
17
|
control: "Flow",
|
|
24
18
|
transform: "Data",
|
|
@@ -61,7 +55,7 @@ function hasLogo(m) {
|
|
|
61
55
|
}
|
|
62
56
|
function groupPath(m) {
|
|
63
57
|
if (m.groups?.length) {
|
|
64
|
-
const path = m.groups.map((g) =>
|
|
58
|
+
const path = m.groups.map((g) => resolveLocalized(g)).filter(Boolean);
|
|
65
59
|
if (isIntegration(m) || !path.length) return path;
|
|
66
60
|
return [CORE_LABEL_ALIAS[path[0]] ?? path[0], ...path.slice(1)];
|
|
67
61
|
}
|
|
@@ -91,6 +85,31 @@ const latestManifests = computed(() => {
|
|
|
91
85
|
}
|
|
92
86
|
return out;
|
|
93
87
|
});
|
|
88
|
+
const versionsBySlug = computed(() => {
|
|
89
|
+
const map = /* @__PURE__ */ new Map();
|
|
90
|
+
for (const m of INTEGRATION_NODE_CATALOG) {
|
|
91
|
+
const list = map.get(m.slug);
|
|
92
|
+
if (list) list.push(m);
|
|
93
|
+
else map.set(m.slug, [m]);
|
|
94
|
+
}
|
|
95
|
+
return map;
|
|
96
|
+
});
|
|
97
|
+
function versionsOf(slug) {
|
|
98
|
+
return versionsBySlug.value.get(slug) ?? [];
|
|
99
|
+
}
|
|
100
|
+
const selectedVersion = ref({});
|
|
101
|
+
function setSelectedVersion(slug, version) {
|
|
102
|
+
selectedVersion.value = { ...selectedVersion.value, [slug]: version };
|
|
103
|
+
}
|
|
104
|
+
function effVersion(m) {
|
|
105
|
+
const sel = selectedVersion.value[m.slug];
|
|
106
|
+
return sel && versionsOf(m.slug).some((v) => v.version === sel) ? sel : m.version;
|
|
107
|
+
}
|
|
108
|
+
function effManifest(m) {
|
|
109
|
+
const v = effVersion(m);
|
|
110
|
+
if (v === m.version) return m;
|
|
111
|
+
return versionsOf(m.slug).find((x) => x.version === v) ?? m;
|
|
112
|
+
}
|
|
94
113
|
const tree = computed(() => {
|
|
95
114
|
const apps = makeGroup("Integrations");
|
|
96
115
|
const core = makeGroup("Core");
|
|
@@ -166,7 +185,7 @@ const searchHits = computed(() => {
|
|
|
166
185
|
const scope = currentGroup.value ? subtreeManifests(currentGroup.value) : latestManifests.value.filter((m) => m.category !== "trigger");
|
|
167
186
|
const hits = [];
|
|
168
187
|
for (const m of scope) {
|
|
169
|
-
const hay = `${
|
|
188
|
+
const hay = `${resolveLocalized(m.name)} ${m.slug} ${resolveLocalized(m.description)}`.toLowerCase();
|
|
170
189
|
if (hay.includes(q)) hits.push({ manifest: m, path: groupPath(m) });
|
|
171
190
|
if (hits.length >= 50) break;
|
|
172
191
|
}
|
|
@@ -175,7 +194,7 @@ const searchHits = computed(() => {
|
|
|
175
194
|
const matchedTriggers = computed(() => {
|
|
176
195
|
const q = search.value.trim().toLowerCase();
|
|
177
196
|
if (!q || currentGroup.value) return [];
|
|
178
|
-
const matches = (t) =>
|
|
197
|
+
const matches = (t) => resolveLocalized(t.name).toLowerCase().includes(q) || t.type.includes(q) || resolveLocalized(t.description).toLowerCase().includes(q);
|
|
179
198
|
return TRIGGER_TEMPLATES.filter(matches);
|
|
180
199
|
});
|
|
181
200
|
const nodeDrag = useNodeDrag();
|
|
@@ -205,24 +224,31 @@ function logoSrcOf(m) {
|
|
|
205
224
|
const imgs = m.images ?? [];
|
|
206
225
|
return (imgs.find((i) => i.category === "logo") ?? imgs[0])?.src ?? null;
|
|
207
226
|
}
|
|
208
|
-
|
|
227
|
+
const paletteDragEnabled = ref(true);
|
|
228
|
+
function onVersionSelectLeave(ev) {
|
|
229
|
+
const el = ev.currentTarget;
|
|
230
|
+
if (el && el !== el.ownerDocument.activeElement) paletteDragEnabled.value = true;
|
|
231
|
+
}
|
|
232
|
+
function onManifestDragStart(leaf, ev) {
|
|
209
233
|
if (!ev.dataTransfer) return;
|
|
234
|
+
const m = effManifest(leaf);
|
|
210
235
|
ev.dataTransfer.setData("application/integration-node", JSON.stringify({ kind: "manifest", slug: m.slug, version: m.version }));
|
|
211
236
|
ev.dataTransfer.effectAllowed = "move";
|
|
212
|
-
setCustomDragImage(ev,
|
|
237
|
+
setCustomDragImage(ev, resolveLocalized(m.name), logoSrcOf(m), "#8b5cf6");
|
|
213
238
|
nodeDrag.start({ kind: "manifest", manifest: m });
|
|
214
239
|
}
|
|
215
240
|
function onTriggerDragStart(tpl, ev) {
|
|
216
241
|
if (!ev.dataTransfer) return;
|
|
217
242
|
ev.dataTransfer.setData("application/integration-node", JSON.stringify({ kind: "trigger", type: tpl.type }));
|
|
218
243
|
ev.dataTransfer.effectAllowed = "move";
|
|
219
|
-
setCustomDragImage(ev,
|
|
244
|
+
setCustomDragImage(ev, resolveLocalized(tpl.name), null, "#10b981");
|
|
220
245
|
nodeDrag.start({ kind: "trigger", tpl });
|
|
221
246
|
}
|
|
222
247
|
function onDragEnd() {
|
|
223
248
|
nodeDrag.end();
|
|
224
249
|
}
|
|
225
|
-
function insertManifest(
|
|
250
|
+
function insertManifest(leaf) {
|
|
251
|
+
const m = effManifest(leaf);
|
|
226
252
|
emit("insert", { kind: "manifest", slug: m.slug, version: m.version });
|
|
227
253
|
}
|
|
228
254
|
function insertTrigger(tpl) {
|
|
@@ -234,6 +260,9 @@ function categoryColor(label) {
|
|
|
234
260
|
const cat = NODE_CATEGORIES.find((c) => c.label === label || CORE_GROUP_BY_CATEGORY[String(c.key)] === label);
|
|
235
261
|
return cat?.color ?? "bg-slate-100 text-slate-500";
|
|
236
262
|
}
|
|
263
|
+
function nodeCategoryColor(m) {
|
|
264
|
+
return NODE_CATEGORIES.find((c) => c.key === m.category)?.color ?? "bg-slate-100 text-slate-500";
|
|
265
|
+
}
|
|
237
266
|
watch(tree, () => {
|
|
238
267
|
if (stack.value.length && !triggersLevel.value && !currentGroup.value) stack.value = [];
|
|
239
268
|
});
|
|
@@ -264,6 +293,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
264
293
|
ref="searchInput"
|
|
265
294
|
v-model="search"
|
|
266
295
|
type="text"
|
|
296
|
+
:aria-label="stack.length ? `Search in ${levelTitle}` : 'Search all nodes'"
|
|
267
297
|
:placeholder="stack.length ? `Search in ${levelTitle}\u2026` : 'Search all nodes\u2026'"
|
|
268
298
|
class="h-7 w-full rounded-md border border-black/[0.08] bg-white pl-7 pr-2 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500/40"
|
|
269
299
|
@keydown.esc="search ? search = '' : stack.length && popLevel()"
|
|
@@ -305,7 +335,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
305
335
|
<component :is="iconFor(t.icon)" :size="13" />
|
|
306
336
|
</span>
|
|
307
337
|
<div class="min-w-0">
|
|
308
|
-
<div class="truncate text-sm font-medium">{{
|
|
338
|
+
<div class="truncate text-sm font-medium">{{ resolveLocalized(t.name) }}</div>
|
|
309
339
|
<div class="text-xs leading-snug text-muted-foreground">Trigger</div>
|
|
310
340
|
</div>
|
|
311
341
|
</div>
|
|
@@ -318,18 +348,36 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
318
348
|
v-for="hit in searchHits"
|
|
319
349
|
:key="`${hit.manifest.slug}@${hit.manifest.version}`"
|
|
320
350
|
class="group flex cursor-grab items-start gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
|
|
321
|
-
draggable="
|
|
322
|
-
:title="
|
|
351
|
+
:draggable="paletteDragEnabled"
|
|
352
|
+
:title="resolveLocalized(hit.manifest.name)"
|
|
323
353
|
@dragstart="onManifestDragStart(hit.manifest, $event)"
|
|
324
354
|
@dragend="onDragEnd"
|
|
325
355
|
@click="insertManifest(hit.manifest)"
|
|
326
356
|
>
|
|
327
|
-
<span class="flex size-
|
|
328
|
-
<
|
|
357
|
+
<span class="flex size-8 shrink-0 items-center justify-center rounded-md" :class="nodeCategoryColor(hit.manifest)">
|
|
358
|
+
<IntegrationsNodeMark :manifest="hit.manifest" :size="18" :badge="false" />
|
|
329
359
|
</span>
|
|
330
360
|
<div class="min-w-0 flex-1">
|
|
331
|
-
<div class="
|
|
332
|
-
|
|
361
|
+
<div class="flex items-center gap-1.5">
|
|
362
|
+
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ resolveLocalized(hit.manifest.name) }}</span>
|
|
363
|
+
<select
|
|
364
|
+
v-if="versionsOf(hit.manifest.slug).length > 1"
|
|
365
|
+
:value="effVersion(hit.manifest)"
|
|
366
|
+
class="shrink-0 rounded border border-black/10 bg-white px-1 py-px text-xs tabular-nums text-muted-foreground"
|
|
367
|
+
:aria-label="`Version to insert for ${resolveLocalized(hit.manifest.name)}`"
|
|
368
|
+
@click.stop
|
|
369
|
+
@mousedown.stop
|
|
370
|
+
@change.stop="setSelectedVersion(hit.manifest.slug, $event.target.value)"
|
|
371
|
+
@mouseenter="paletteDragEnabled = false"
|
|
372
|
+
@mouseleave="onVersionSelectLeave"
|
|
373
|
+
@focus="paletteDragEnabled = false"
|
|
374
|
+
@blur="paletteDragEnabled = true"
|
|
375
|
+
>
|
|
376
|
+
<option v-for="v in versionsOf(hit.manifest.slug)" :key="v.version" :value="v.version">v{{ v.version }}</option>
|
|
377
|
+
</select>
|
|
378
|
+
<span v-else class="shrink-0 text-xs tabular-nums text-muted-foreground/60">v{{ hit.manifest.version }}</span>
|
|
379
|
+
</div>
|
|
380
|
+
<div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(hit.manifest.description) }}</div>
|
|
333
381
|
<div class="truncate text-xs text-muted-foreground/70">{{ hit.path.join(" \u203A ") }}</div>
|
|
334
382
|
</div>
|
|
335
383
|
</div>
|
|
@@ -342,7 +390,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
342
390
|
:key="t.id"
|
|
343
391
|
class="group flex cursor-grab items-center gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
|
|
344
392
|
draggable="true"
|
|
345
|
-
:title="
|
|
393
|
+
:title="resolveLocalized(t.name)"
|
|
346
394
|
@dragstart="onTriggerDragStart(t, $event)"
|
|
347
395
|
@dragend="onDragEnd"
|
|
348
396
|
@click="insertTrigger(t)"
|
|
@@ -351,8 +399,8 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
351
399
|
<component :is="iconFor(t.icon)" :size="13" />
|
|
352
400
|
</span>
|
|
353
401
|
<div class="min-w-0">
|
|
354
|
-
<div class="truncate text-sm font-medium">{{
|
|
355
|
-
<div class="text-xs leading-snug text-muted-foreground">{{
|
|
402
|
+
<div class="truncate text-sm font-medium">{{ resolveLocalized(t.name) }}</div>
|
|
403
|
+
<div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(t.description) }}</div>
|
|
356
404
|
</div>
|
|
357
405
|
</div>
|
|
358
406
|
</template>
|
|
@@ -400,7 +448,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
400
448
|
@click="pushPath(['__apps', label])"
|
|
401
449
|
>
|
|
402
450
|
<span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-white ring-1 ring-black/[0.06]">
|
|
403
|
-
<
|
|
451
|
+
<IntegrationsNodeMark v-if="groupLogoManifest(group)" :manifest="groupLogoManifest(group)" :size="14" logo-only />
|
|
404
452
|
<Lucide.Package v-else :size="13" class="text-violet-600" />
|
|
405
453
|
</span>
|
|
406
454
|
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ label }}</span>
|
|
@@ -423,7 +471,7 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
423
471
|
class="flex size-6 shrink-0 items-center justify-center rounded-md"
|
|
424
472
|
:class="groupLogoManifest(group) ? 'bg-white ring-1 ring-black/[0.06]' : 'bg-slate-100 text-slate-500'"
|
|
425
473
|
>
|
|
426
|
-
<
|
|
474
|
+
<IntegrationsNodeMark v-if="groupLogoManifest(group)" :manifest="groupLogoManifest(group)" :size="14" logo-only />
|
|
427
475
|
<Lucide.FolderOpen v-else :size="13" />
|
|
428
476
|
</span>
|
|
429
477
|
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ label }}</span>
|
|
@@ -435,18 +483,36 @@ const searching = computed(() => search.value.trim().length > 0);
|
|
|
435
483
|
v-for="m in currentGroup.leaves"
|
|
436
484
|
:key="`${m.slug}@${m.version}`"
|
|
437
485
|
class="group flex cursor-grab items-start gap-2 rounded-md border border-transparent px-2 py-1.5 transition hover:border-black/[0.08] hover:bg-muted/40 active:cursor-grabbing"
|
|
438
|
-
draggable="
|
|
439
|
-
:title="
|
|
486
|
+
:draggable="paletteDragEnabled"
|
|
487
|
+
:title="resolveLocalized(m.name)"
|
|
440
488
|
@dragstart="onManifestDragStart(m, $event)"
|
|
441
489
|
@dragend="onDragEnd"
|
|
442
490
|
@click="insertManifest(m)"
|
|
443
491
|
>
|
|
444
|
-
<span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="
|
|
445
|
-
<
|
|
492
|
+
<span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="nodeCategoryColor(m)">
|
|
493
|
+
<IntegrationsNodeMark :manifest="m" :size="14" :badge="false" />
|
|
446
494
|
</span>
|
|
447
495
|
<div class="min-w-0 flex-1">
|
|
448
|
-
<div class="
|
|
449
|
-
|
|
496
|
+
<div class="flex items-center gap-1.5">
|
|
497
|
+
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ resolveLocalized(m.name) }}</span>
|
|
498
|
+
<select
|
|
499
|
+
v-if="versionsOf(m.slug).length > 1"
|
|
500
|
+
:value="effVersion(m)"
|
|
501
|
+
class="shrink-0 rounded border border-black/10 bg-white px-1 py-px text-xs tabular-nums text-muted-foreground"
|
|
502
|
+
:aria-label="`Version to insert for ${resolveLocalized(m.name)}`"
|
|
503
|
+
@click.stop
|
|
504
|
+
@mousedown.stop
|
|
505
|
+
@change.stop="setSelectedVersion(m.slug, $event.target.value)"
|
|
506
|
+
@mouseenter="paletteDragEnabled = false"
|
|
507
|
+
@mouseleave="onVersionSelectLeave"
|
|
508
|
+
@focus="paletteDragEnabled = false"
|
|
509
|
+
@blur="paletteDragEnabled = true"
|
|
510
|
+
>
|
|
511
|
+
<option v-for="v in versionsOf(m.slug)" :key="v.version" :value="v.version">v{{ v.version }}</option>
|
|
512
|
+
</select>
|
|
513
|
+
<span v-else class="shrink-0 text-xs tabular-nums text-muted-foreground/60">v{{ m.version }}</span>
|
|
514
|
+
</div>
|
|
515
|
+
<div class="text-xs leading-snug text-muted-foreground">{{ resolveLocalized(m.description) }}</div>
|
|
450
516
|
</div>
|
|
451
517
|
</div>
|
|
452
518
|
</template>
|
|
@@ -173,7 +173,9 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
|
|
|
173
173
|
:model-value="row.method"
|
|
174
174
|
@update:model-value="(v) => setRow(idx, { method: String(v) })"
|
|
175
175
|
>
|
|
176
|
-
|
|
176
|
+
<!-- A repeating row has no room for visible labels, so each
|
|
177
|
+
control names itself and says which row it is. -->
|
|
178
|
+
<SelectTrigger :aria-label="`Filter ${idx + 1} \u2014 comparison`" class="h-7 w-40 shrink-0 bg-white text-sm">
|
|
177
179
|
<SelectValue />
|
|
178
180
|
</SelectTrigger>
|
|
179
181
|
<SelectContent>
|
|
@@ -184,6 +186,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
|
|
|
184
186
|
</Select>
|
|
185
187
|
<Input
|
|
186
188
|
v-if="rowMeta[idx]?.attribute"
|
|
189
|
+
:aria-label="`Filter ${idx + 1} \u2014 field`"
|
|
187
190
|
:model-value="row.attribute"
|
|
188
191
|
placeholder="field"
|
|
189
192
|
class="h-7 w-36 shrink-0 bg-white font-mono text-sm"
|
|
@@ -191,6 +194,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
|
|
|
191
194
|
/>
|
|
192
195
|
<Input
|
|
193
196
|
v-if="rowMeta[idx]?.values !== 'none'"
|
|
197
|
+
:aria-label="`Filter ${idx + 1} \u2014 value`"
|
|
194
198
|
:model-value="row.values"
|
|
195
199
|
:placeholder="rowMeta[idx]?.values === 'list' ? 'value, value, \u2026' : 'value'"
|
|
196
200
|
class="h-7 min-w-0 flex-1 bg-white text-sm"
|
|
@@ -200,6 +204,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
|
|
|
200
204
|
</template>
|
|
201
205
|
<Input
|
|
202
206
|
v-else
|
|
207
|
+
:aria-label="`Filter ${idx + 1} \u2014 raw text`"
|
|
203
208
|
:model-value="row.text"
|
|
204
209
|
class="h-7 min-w-0 flex-1 bg-white font-mono text-sm"
|
|
205
210
|
title="This entry is not in the builder vocabulary; edit it as text or in the JSON view."
|
|
@@ -207,6 +212,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
|
|
|
207
212
|
/>
|
|
208
213
|
<button
|
|
209
214
|
type="button"
|
|
215
|
+
:aria-label="`Remove filter ${idx + 1}`"
|
|
210
216
|
class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:text-rose-600"
|
|
211
217
|
@click="removeRow(idx)"
|
|
212
218
|
>
|
|
@@ -227,6 +233,7 @@ const rowMeta = computed(() => rows.value.map((r) => r.kind === "query" ? method
|
|
|
227
233
|
|
|
228
234
|
<template v-else>
|
|
229
235
|
<Textarea
|
|
236
|
+
aria-label="Filters as JSON"
|
|
230
237
|
:model-value="jsonDraft"
|
|
231
238
|
:rows="6"
|
|
232
239
|
spellcheck="false"
|
|
@@ -68,7 +68,9 @@ function confirmTerminate() {
|
|
|
68
68
|
<template>
|
|
69
69
|
<DropdownMenu>
|
|
70
70
|
<DropdownMenuTrigger as-child>
|
|
71
|
-
|
|
71
|
+
<!-- Icon-only, so it needs a name of its own — the run id is the only
|
|
72
|
+
thing that tells two rows' menus apart. -->
|
|
73
|
+
<Button variant="ghost" size="icon" class="size-7" :disabled="busy" :aria-label="`Actions for run #${run.id}`" @click.stop>
|
|
72
74
|
<MoreHorizontal :size="16" />
|
|
73
75
|
</Button>
|
|
74
76
|
</DropdownMenuTrigger>
|