@revenexx/studio-integrations 3.8.1 → 3.8.3
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 +13 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/NodeConfigField.vue +19 -5
- package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.vue +4 -2
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +3 -2
- package/dist/runtime/composables/useNodeConfigResolve.d.ts +12 -0
- package/dist/runtime/composables/useNodeConfigResolve.js +10 -3
- package/dist/runtime/types/integrations.d.ts +6 -1
- package/dist/runtime/utils/credentialState.d.ts +3 -0
- package/dist/runtime/utils/credentialState.js +4 -2
- package/dist/runtime/utils/credentialTest.js +5 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @revenexx/studio-integrations
|
|
2
2
|
|
|
3
|
+
## 3.8.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3d202af: A connection test's sentence reads in the language the studio is set to (PO-400) — in the live answer, on the credentials list and in the detail view, and still after a language switch made later. A credential type sends it in both languages; a sentence recorded as one plain string is shown as it was recorded.
|
|
8
|
+
- 1a04c39: A setting whose options a node loads now asks for them in the studio's interface language, so a node that can label its options in the author's language does — Odoo Search Records names its fields in German in a German studio. Changing the language loads the options again in the new one.
|
|
9
|
+
|
|
10
|
+
## 3.8.2
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 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.
|
|
15
|
+
|
|
3
16
|
## 3.8.1
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/dist/module.json
CHANGED
|
@@ -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. -->
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, watch } from "vue";
|
|
3
|
-
import { useNodeConfigResolve } from "../../composables/useNodeConfigResolve";
|
|
3
|
+
import { resolveLocale, useNodeConfigResolve } from "../../composables/useNodeConfigResolve";
|
|
4
|
+
import { useIntegrationsLocale } from "../../composables/useIntegrationsLocale";
|
|
4
5
|
const props = defineProps({
|
|
5
6
|
nodeId: { type: String, required: true },
|
|
6
7
|
slug: { type: String, required: true },
|
|
@@ -20,7 +21,8 @@ function readCache() {
|
|
|
20
21
|
}
|
|
21
22
|
const cache = useState("integrations.resolvedOutputs", () => null);
|
|
22
23
|
if (cache.value === null) cache.value = readCache();
|
|
23
|
-
const
|
|
24
|
+
const { locale } = useIntegrationsLocale();
|
|
25
|
+
const cacheKey = computed(() => `${props.slug}@${props.version}:${resolveLocale(locale.value)}:${JSON.stringify(props.config)}`);
|
|
24
26
|
function storeCache(key, outputs) {
|
|
25
27
|
const next = { ...cache.value ?? {} };
|
|
26
28
|
if (!(key in next) && Object.keys(next).length >= CACHE_MAX_ENTRIES) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Credential, CredentialNetwork } from '../types/integrations.js';
|
|
1
|
+
import type { Credential, CredentialNetwork, LocalizedString } from '../types/integrations.js';
|
|
2
2
|
import { IntegrationsApiError } from '../utils/apiError.js';
|
|
3
3
|
/**
|
|
4
4
|
* Typed, per-tenant credential instances, backed by the Integrations service
|
|
@@ -32,7 +32,8 @@ import { IntegrationsApiError } from '../utils/apiError.js';
|
|
|
32
32
|
*/
|
|
33
33
|
export interface CredentialTestResult {
|
|
34
34
|
ok: boolean;
|
|
35
|
-
|
|
35
|
+
/** The type's own words: one sentence per language, or a plain string (PO-400). */
|
|
36
|
+
message?: LocalizedString | null;
|
|
36
37
|
error?: string;
|
|
37
38
|
/**
|
|
38
39
|
* Whether the platform kept this answer as the credential's state. `false` for a
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* (IN-80).
|
|
16
16
|
* `dependsOn` omitted (outputs): no gate, and the whole `config` keys the query
|
|
17
17
|
* and is sent — so any config edit re-resolves.
|
|
18
|
+
*
|
|
19
|
+
* `locale` is the studio's interface language (`useIntegrationsLocale`). It is
|
|
20
|
+
* sent with every question, so a node can label what it answers in the editor's
|
|
21
|
+
* language, and it keys the query, so a change of language asks again rather
|
|
22
|
+
* than keeping the answer given in the old one (PO-792).
|
|
18
23
|
*/
|
|
19
24
|
import type { MaybeRefOrGetter, Ref } from 'vue';
|
|
20
25
|
import type { NodeConfigResolveError, NodeConfigResolveResult } from '../types/integrations.js';
|
|
@@ -36,6 +41,13 @@ export declare function optionalDependencies(dependsOn: string[] | undefined, si
|
|
|
36
41
|
key: string;
|
|
37
42
|
required?: boolean;
|
|
38
43
|
}> | undefined): string[];
|
|
44
|
+
/**
|
|
45
|
+
* The interface language as the question carries it: the tag the host set, so a
|
|
46
|
+
* node can tell `de-AT` from `de-DE`, unless it is longer than the 16 characters
|
|
47
|
+
* the API accepts — then its language alone, rather than a question refused with
|
|
48
|
+
* a 422 for a setting the author cannot see.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveLocale(locale: string): string;
|
|
39
51
|
export declare function useNodeConfigResolve(opts: {
|
|
40
52
|
slug: MaybeRefOrGetter<string>;
|
|
41
53
|
version: MaybeRefOrGetter<string>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { computed, onScopeDispose, ref, toValue, watch } from "vue";
|
|
2
2
|
import { isVersionExecutable } from "../data/integrationNodeCatalog.js";
|
|
3
3
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
4
|
+
import { useIntegrationsLocale } from "./useIntegrationsLocale.js";
|
|
4
5
|
import { forgetAnswer, keepAnswer, keptAnswer } from "../data/resolveAnswerStore.js";
|
|
5
6
|
import { IntegrationsApiError, errorReason } from "../utils/apiError.js";
|
|
7
|
+
import { baseLanguage } from "../utils/localize.js";
|
|
6
8
|
export function isResolvableValue(v) {
|
|
7
9
|
if (v === void 0 || v === null) return false;
|
|
8
10
|
if (typeof v === "string") {
|
|
@@ -48,10 +50,15 @@ export function optionalDependencies(dependsOn, siblings) {
|
|
|
48
50
|
return sibling !== void 0 && sibling.required !== true;
|
|
49
51
|
});
|
|
50
52
|
}
|
|
53
|
+
export function resolveLocale(locale) {
|
|
54
|
+
const tag = locale.trim();
|
|
55
|
+
return tag !== "" && tag.length <= 16 ? tag : baseLanguage(tag);
|
|
56
|
+
}
|
|
51
57
|
const DEBOUNCE_MS = 300;
|
|
52
58
|
const STALE_MS = 3e4;
|
|
53
59
|
export function useNodeConfigResolve(opts) {
|
|
54
60
|
const api = useIntegrationsApi();
|
|
61
|
+
const { locale } = useIntegrationsLocale();
|
|
55
62
|
const data = ref(null);
|
|
56
63
|
const error = ref(null);
|
|
57
64
|
const isFetching = ref(false);
|
|
@@ -74,7 +81,7 @@ export function useNodeConfigResolve(opts) {
|
|
|
74
81
|
for (const k of keyKeys) subset[k] = cfg[k];
|
|
75
82
|
const rawSearch = toValue(opts.search);
|
|
76
83
|
const search = typeof rawSearch === "string" ? rawSearch.trim().slice(0, 200) : "";
|
|
77
|
-
return JSON.stringify({ config: subset, search: search || void 0 });
|
|
84
|
+
return JSON.stringify({ config: subset, search: search || void 0, locale: resolveLocale(locale.value) });
|
|
78
85
|
});
|
|
79
86
|
const debouncedKey = ref(subsetKey.value);
|
|
80
87
|
let debounceTimer = null;
|
|
@@ -101,12 +108,12 @@ export function useNodeConfigResolve(opts) {
|
|
|
101
108
|
const slug = toValue(opts.slug);
|
|
102
109
|
const version = toValue(opts.version);
|
|
103
110
|
const target = toValue(opts.target);
|
|
104
|
-
const { config, search } = JSON.parse(key);
|
|
111
|
+
const { config, search, locale: language } = JSON.parse(key);
|
|
105
112
|
isFetching.value = true;
|
|
106
113
|
try {
|
|
107
114
|
const result = await api.post(
|
|
108
115
|
`/nodes/${encodeURIComponent(slug)}/${encodeURIComponent(version)}/config:resolve`,
|
|
109
|
-
{ target, config, ...search ? { search } : {} },
|
|
116
|
+
{ target, config, locale: language, ...search ? { search } : {} },
|
|
110
117
|
{ signal }
|
|
111
118
|
);
|
|
112
119
|
data.value = result;
|
|
@@ -561,7 +561,12 @@ export interface Credential {
|
|
|
561
561
|
/** Outcome of the last connection test (persisted by the API). Null when never tested. */
|
|
562
562
|
last_test_at?: string | null;
|
|
563
563
|
last_test_ok?: boolean | null;
|
|
564
|
-
|
|
564
|
+
/**
|
|
565
|
+
* What the last test said: one sentence per language (`{ de, en }`), or a plain
|
|
566
|
+
* string from a type that wrote one and from any test recorded before PO-400.
|
|
567
|
+
* Resolve it with `resolveLocalized`, at render, so a language switch moves it.
|
|
568
|
+
*/
|
|
569
|
+
last_test_message?: LocalizedString | null;
|
|
565
570
|
/** The VPN route this credential is reached through; `null` or absent = public internet. */
|
|
566
571
|
network?: CredentialNetwork | null;
|
|
567
572
|
created_at: string;
|
|
@@ -21,6 +21,9 @@ export interface CredentialStateInfo {
|
|
|
21
21
|
* reached something can name it — which host, which environment, which plan —
|
|
22
22
|
* and that is exactly what an operator cannot see anywhere else once a node
|
|
23
23
|
* stops asking them to configure it.
|
|
24
|
+
*
|
|
25
|
+
* Already in the studio's language: the record keeps every language the type
|
|
26
|
+
* wrote, and this picks one each time it is called (PO-400).
|
|
24
27
|
*/
|
|
25
28
|
message?: string | null;
|
|
26
29
|
/** When the underlying result was recorded (ISO). */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useIntegrationsT } from "../composables/useIntegrationsT.js";
|
|
2
|
+
import { resolveLocalized } from "./localize.js";
|
|
2
3
|
export function providerStepWord(state) {
|
|
3
4
|
if (state === "connected") return null;
|
|
4
5
|
return state === "untested" ? "connect" : "reconnect";
|
|
@@ -23,14 +24,15 @@ const TONE = {
|
|
|
23
24
|
};
|
|
24
25
|
export function credentialState(c) {
|
|
25
26
|
const t = useIntegrationsT();
|
|
27
|
+
const message = resolveLocalized(c.last_test_message) || null;
|
|
26
28
|
if (c.status === "needs_reauth") {
|
|
27
29
|
return { key: "needs_reauth", label: t(MESSAGE_KEY.needs_reauth), toneClass: TONE.needs_reauth, at: c.last_test_at };
|
|
28
30
|
}
|
|
29
31
|
if (c.last_test_ok === true) {
|
|
30
|
-
return { key: "connected", label: t(MESSAGE_KEY.connected), toneClass: TONE.connected, message
|
|
32
|
+
return { key: "connected", label: t(MESSAGE_KEY.connected), toneClass: TONE.connected, message, at: c.last_test_at };
|
|
31
33
|
}
|
|
32
34
|
if (c.last_test_ok === false) {
|
|
33
|
-
return { key: "failed", label: t(MESSAGE_KEY.failed), toneClass: TONE.failed, message
|
|
35
|
+
return { key: "failed", label: t(MESSAGE_KEY.failed), toneClass: TONE.failed, message, at: c.last_test_at };
|
|
34
36
|
}
|
|
35
37
|
return { key: "untested", label: t(MESSAGE_KEY.untested), toneClass: TONE.untested };
|
|
36
38
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import { resolveLocalized } from "./localize.js";
|
|
1
2
|
export function testOutcomeView(result, t) {
|
|
3
|
+
const message = resolveLocalized(result.message) || void 0;
|
|
2
4
|
if (result.ok) {
|
|
3
|
-
return { kind: "ok", toneClass: "bg-emerald-500/10 text-emerald-700", text:
|
|
5
|
+
return { kind: "ok", toneClass: "bg-emerald-500/10 text-emerald-700", text: message ?? t("integration.credentials.shared.test.ok") };
|
|
4
6
|
}
|
|
5
7
|
if (result.untested) {
|
|
6
|
-
const reason = result.error ??
|
|
8
|
+
const reason = result.error ?? message;
|
|
7
9
|
return {
|
|
8
10
|
kind: "untested",
|
|
9
11
|
toneClass: "bg-slate-500/10 text-slate-700",
|
|
10
12
|
text: reason ? t("integration.credentials.shared.test.notTestedBecause", { reason }) : t("integration.credentials.shared.test.notTested")
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
|
-
return { kind: "failed", toneClass: "bg-rose-500/10 text-rose-700", text: result.error ??
|
|
15
|
+
return { kind: "failed", toneClass: "bg-rose-500/10 text-rose-700", text: result.error ?? message ?? t("integration.credentials.shared.test.failed") };
|
|
14
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/studio-integrations",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.3",
|
|
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": {
|