@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 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
3
  "configKey": "studioIntegrations",
4
- "version": "3.8.1",
4
+ "version": "3.8.3",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
@@ -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. -->
@@ -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 cacheKey = computed(() => `${props.slug}@${props.version}:${JSON.stringify(props.config)}`);
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
- message?: string;
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
- last_test_message?: string | null;
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: c.last_test_message, at: c.last_test_at };
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: c.last_test_message, at: c.last_test_at };
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: result.message ?? t("integration.credentials.shared.test.ok") };
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 ?? result.message;
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 ?? result.message ?? t("integration.credentials.shared.test.failed") };
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.1",
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": {