@revenexx/studio-integrations 0.6.0 → 1.0.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +643 -0
  2. package/README.md +31 -0
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +12 -3
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +32 -28
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +45 -38
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +21 -15
  13. package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
  14. package/dist/runtime/components/Integrations/Layout.vue +28 -3
  15. package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +30 -16
  17. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
  18. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -8
  19. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
  20. package/dist/runtime/components/Integrations/NodeConfigField.vue +45 -42
  21. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
  22. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
  23. package/dist/runtime/components/Integrations/NodeInspector.vue +315 -221
  24. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
  25. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
  26. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +37 -10
  27. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
  28. package/dist/runtime/components/Integrations/NodePalette.vue +40 -35
  29. package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
  30. package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
  31. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  32. package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
  33. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  34. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  35. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  36. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  37. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  38. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  39. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  40. package/dist/runtime/components/Integrations/StickyNoteView.vue +82 -34
  41. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  42. package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
  43. package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
  44. package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
  45. package/dist/runtime/components/Integrations/TemplateCard.vue +18 -8
  46. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +22 -12
  47. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  48. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +41 -39
  49. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  50. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
  51. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
  52. package/dist/runtime/components/Integrations/WorkflowEditor.vue +131 -87
  53. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
  54. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
  55. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +27 -24
  56. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
  57. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
  58. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +76 -46
  59. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +55 -43
  60. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
  61. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +55 -43
  62. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
  63. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
  64. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -13
  65. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +79 -39
  66. package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
  67. package/dist/runtime/composables/integrationsApi.js +4 -2
  68. package/dist/runtime/composables/useCredentialTypes.js +4 -2
  69. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
  70. package/dist/runtime/composables/useIntegrationsCredentials.js +8 -6
  71. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  72. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  73. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  74. package/dist/runtime/composables/useIntegrationsNodes.js +14 -2
  75. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  76. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  77. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  78. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  79. package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
  80. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +3 -2
  81. package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
  82. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  83. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  84. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  85. package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
  86. package/dist/runtime/composables/useIntegrationsTriggers.d.ts +23 -3
  87. package/dist/runtime/composables/useIntegrationsTriggers.js +21 -8
  88. package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
  89. package/dist/runtime/composables/useIntegrationsWorkflows.js +30 -7
  90. package/dist/runtime/composables/useNodeConfigResolve.js +4 -2
  91. package/dist/runtime/data/integrationNodeCatalog.d.ts +63 -1
  92. package/dist/runtime/data/integrationNodeCatalog.js +22 -4
  93. package/dist/runtime/data/integrationTemplates.d.ts +9 -0
  94. package/dist/runtime/data/integrationTemplates.js +7 -1
  95. package/dist/runtime/data/nodeInputData.d.ts +58 -1
  96. package/dist/runtime/data/nodeInputData.js +30 -5
  97. package/dist/runtime/data/payloadForm.js +2 -1
  98. package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
  99. package/dist/runtime/data/triggerGlyphs.js +11 -0
  100. package/dist/runtime/data/workflowIconCatalog.js +2 -1
  101. package/dist/runtime/i18n/messages/de.json +1224 -0
  102. package/dist/runtime/i18n/messages/en.json +1224 -0
  103. package/dist/runtime/i18n/translate.d.ts +83 -0
  104. package/dist/runtime/i18n/translate.js +41 -0
  105. package/dist/runtime/pages/integrations/audit.vue +52 -34
  106. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
  107. package/dist/runtime/pages/integrations/credentials.vue +73 -52
  108. package/dist/runtime/pages/integrations/index.vue +66 -73
  109. package/dist/runtime/pages/integrations/runs.vue +33 -21
  110. package/dist/runtime/pages/integrations/schedules.vue +49 -28
  111. package/dist/runtime/pages/integrations/secrets.vue +34 -34
  112. package/dist/runtime/pages/integrations/templates/[slug].vue +89 -72
  113. package/dist/runtime/pages/integrations/templates/index.vue +35 -27
  114. package/dist/runtime/pages/integrations/workflows/[id].vue +351 -197
  115. package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
  116. package/dist/runtime/plugins/locale.d.ts +10 -0
  117. package/dist/runtime/plugins/locale.js +6 -0
  118. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  119. package/dist/runtime/plugins/realtime.client.js +4 -0
  120. package/dist/runtime/types/integrations.d.ts +25 -1
  121. package/dist/runtime/utils/cockpitSources.d.ts +22 -3
  122. package/dist/runtime/utils/credentialState.d.ts +30 -2
  123. package/dist/runtime/utils/credentialState.js +26 -11
  124. package/dist/runtime/utils/localize.d.ts +52 -5
  125. package/dist/runtime/utils/localize.js +47 -9
  126. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  127. package/dist/runtime/utils/pollBudget.js +17 -0
  128. package/dist/runtime/utils/schedule.d.ts +0 -25
  129. package/dist/runtime/utils/schedule.js +37 -25
  130. package/dist/runtime/utils/servedPaths.d.ts +22 -0
  131. package/dist/runtime/utils/servedPaths.js +3 -0
  132. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  133. package/dist/runtime/utils/statusStyle.js +9 -1
  134. package/dist/runtime/utils/validationFindings.d.ts +2 -2
  135. package/dist/runtime/utils/validationFindings.js +4 -3
  136. package/dist/types.d.mts +2 -6
  137. package/package.json +3 -2
package/README.md CHANGED
@@ -30,6 +30,33 @@ supply a token/tenant through the platform provider (override
30
30
  `usePlatformAuth` / `usePlatformTenant`, or the standalone defaults read
31
31
  `NUXT_PUBLIC_DEV_TOKEN` / `NUXT_PUBLIC_DEV_TENANT`).
32
32
 
33
+ ### Language
34
+
35
+ The studio speaks English and German. That covers both halves of a screen: the
36
+ text node packages publish (names, descriptions, settings) **and** the studio's
37
+ own chrome — headings, buttons, tabs, empty states, toasts. Both fall back to
38
+ English. The studio has no switcher of its own, so hand the choice over one of
39
+ two ways:
40
+
41
+ ```ts
42
+ // nuxt.config.ts — a language that does not change
43
+ export default defineNuxtConfig({
44
+ studioIntegrations: { locale: 'de' }, // or NUXT_PUBLIC_STUDIO_INTEGRATIONS_LOCALE
45
+ })
46
+ ```
47
+
48
+ ```ts
49
+ // a host plugin — a language the user switches while the app is running
50
+ const { setLocale } = useIntegrationsLocale()
51
+ watch(hostLocale, l => setLocale(l), { immediate: true })
52
+ ```
53
+
54
+ A switch reaches surfaces that are already open; nothing is refetched. With
55
+ neither wired, the studio is English. The module carries its own message
56
+ catalogue (`runtime/i18n/messages/*.json`) — it never reads the host's, and needs
57
+ no i18n library installed in the host. See
58
+ [`docs/interface-language.md`](docs/interface-language.md).
59
+
33
60
  ## ⚠️ Tailwind: you MUST scan this package
34
61
 
35
62
  The module ships its runtime as source-ish `.vue`; Tailwind generates the
@@ -63,6 +90,10 @@ pnpm build # dist/
63
90
  pnpm changeset # record what your change does to the version
64
91
  ```
65
92
 
93
+ Branch off `main` as `feature/<desc>`, `fix/<desc>` or `chore/<desc>` — GitHub refuses
94
+ the first push of any other name, and `main` itself takes a reviewed pull request with
95
+ CI green: [`docs/branch-protection.md`](docs/branch-protection.md).
96
+
66
97
  Hosts to develop against:
67
98
 
68
99
  * **Playground** (`playground/`, in this repo): thin standalone Nuxt host that
package/dist/module.d.mts CHANGED
@@ -1,6 +1,22 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
  export * from '../dist/runtime/types/integrations.js';
3
3
 
4
- declare const _default: _nuxt_schema.NuxtModule<_nuxt_schema.ModuleOptions, _nuxt_schema.ModuleOptions, false>;
4
+ interface ModuleOptions {
5
+ /**
6
+ * The language the studio renders in, as a BCP-47 tag (`en`, `de`, `de-CH`).
7
+ *
8
+ * This is the *initial* language and the whole of the contract for a host
9
+ * whose language never changes. A host with a switcher leaves it alone and
10
+ * calls `useIntegrationsLocale().setLocale()` instead — its plugins run after
11
+ * this module's, so the two do not fight over the value.
12
+ *
13
+ * It reaches the runtime through `runtimeConfig.public.studioIntegrations`,
14
+ * so a deployment can override it with
15
+ * `NUXT_PUBLIC_STUDIO_INTEGRATIONS_LOCALE` without a rebuild.
16
+ */
17
+ locale?: string;
18
+ }
19
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
5
20
 
6
21
  export { _default as default };
22
+ export type { ModuleOptions };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
3
  "configKey": "studioIntegrations",
4
- "version": "0.6.0",
4
+ "version": "1.0.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { existsSync, readdirSync, statSync } from 'node:fs';
1
+ import { readdirSync, statSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { defineNuxtModule, createResolver, hasNuxtModule, installModule, addComponentsDir, addImportsDir, extendPages } from '@nuxt/kit';
3
+ import { defineNuxtModule, createResolver, addPlugin, hasNuxtModule, installModule, addComponentsDir, addImportsDir, extendPages } from '@nuxt/kit';
4
4
 
5
5
  function toSegment(name) {
6
6
  if (name.startsWith("[...")) return `:${name.slice(4, -1)}(.*)*`;
@@ -22,14 +22,22 @@ function scanRoutes(dir, baseUrl, baseName) {
22
22
  }
23
23
  return routes;
24
24
  }
25
+
25
26
  const module$1 = defineNuxtModule({
26
27
  meta: {
27
28
  name: "@revenexx/studio-integrations",
28
29
  configKey: "studioIntegrations"
29
30
  },
30
- async setup(_options, nuxt) {
31
+ defaults: {
32
+ locale: "en"
33
+ },
34
+ async setup(options, nuxt) {
31
35
  const { resolve } = createResolver(import.meta.url);
32
36
  const runtime = resolve("./runtime");
37
+ const publicConfig = nuxt.options.runtimeConfig.public;
38
+ const configured = publicConfig.studioIntegrations ?? {};
39
+ publicConfig.studioIntegrations = { ...configured, locale: configured.locale ?? options.locale };
40
+ addPlugin(resolve("./runtime/plugins/locale"));
33
41
  if (!hasNuxtModule("@solar-icons/nuxt", nuxt)) {
34
42
  await installModule("@solar-icons/nuxt", {
35
43
  namePrefix: "Solar",
@@ -52,6 +60,7 @@ const module$1 = defineNuxtModule({
52
60
  nuxt.options.build.transpile.push(runtime);
53
61
  addComponentsDir({ path: join(runtime, "components"), pathPrefix: true });
54
62
  addImportsDir(join(runtime, "composables"));
63
+ addPlugin(join(runtime, "plugins", "realtime.client"));
55
64
  extendPages((pages) => {
56
65
  const pagesDir = join(runtime, "pages", "integrations");
57
66
  if (!existsSync(pagesDir)) return;
@@ -25,29 +25,30 @@ const props = defineProps({
25
25
  credentialType: { type: [String, Array], required: false }
26
26
  });
27
27
  const emit = defineEmits(["update:open", "created"]);
28
+ const t = useIntegrationsT();
28
29
  const { create, testInline, load, oauthAuthorizeUrl } = useIntegrationsCredentials();
29
30
  const { load: loadTypes, bySlug } = useCredentialTypes();
30
31
  function authKindLabel(k) {
31
32
  switch (k) {
32
33
  case "api-key":
33
- return "API Key";
34
+ return t("integration.credentials.shared.authKind.apiKey");
34
35
  case "oauth2-authcode":
35
36
  case "oauth2-client-credentials":
36
- return "OAuth2";
37
+ return t("integration.credentials.shared.authKind.oauth2");
37
38
  case "basic-auth":
38
- return "Basic Auth";
39
+ return t("integration.credentials.shared.authKind.basicAuth");
39
40
  case "static":
40
- return "Static";
41
+ return t("integration.credentials.shared.authKind.static");
41
42
  case "secret":
42
- return "Secret";
43
+ return t("integration.credentials.shared.authKind.secret");
43
44
  default:
44
45
  return k ?? "";
45
46
  }
46
47
  }
47
48
  const allowedSlugs = computed(() => {
48
- const t = props.credentialType;
49
- if (!t) return [];
50
- return (Array.isArray(t) ? t : [t]).map((s) => s.trim()).filter(Boolean);
49
+ const t2 = props.credentialType;
50
+ if (!t2) return [];
51
+ return (Array.isArray(t2) ? t2 : [t2]).map((s) => s.trim()).filter(Boolean);
51
52
  });
52
53
  const allowedTypes = computed(() => allowedSlugs.value.map((s) => bySlug(s)).filter((x) => !!x));
53
54
  const pickedType = ref(null);
@@ -56,11 +57,11 @@ const sourceActive = ref(false);
56
57
  const saving = ref(false);
57
58
  const testing = ref(false);
58
59
  const testResult = ref(null);
59
- function fieldsOf(t) {
60
- return t?.fields ?? [];
60
+ function fieldsOf(t2) {
61
+ return t2?.fields ?? [];
61
62
  }
62
- function isOAuthAuthcode(t) {
63
- return t?.auth_kind === "oauth2-authcode";
63
+ function isOAuthAuthcode(t2) {
64
+ return t2?.auth_kind === "oauth2-authcode";
64
65
  }
65
66
  watch(() => props.open, async (o) => {
66
67
  if (!o) return;
@@ -76,12 +77,12 @@ watch(() => props.open, async (o) => {
76
77
  form.name = "";
77
78
  }
78
79
  }, { immediate: true });
79
- function pickType(t) {
80
- pickedType.value = t;
80
+ function pickType(t2) {
81
+ pickedType.value = t2;
81
82
  form.config = {};
82
83
  testResult.value = null;
83
84
  sourceActive.value = false;
84
- if (!form.name) form.name = resolveLocalized(t.name);
85
+ if (!form.name) form.name = resolveLocalized(t2.name);
85
86
  }
86
87
  function buildConfigPayload() {
87
88
  const out = {};
@@ -130,14 +131,14 @@ async function submit() {
130
131
  config: buildConfigPayload()
131
132
  });
132
133
  await load(true);
133
- toast.success("Credential created");
134
+ toast.success(t("integration.credentials.toast.created"));
134
135
  if (isOAuthAuthcode(pickedType.value)) {
135
136
  void connect(created.id);
136
137
  }
137
138
  emit("created", created.id);
138
139
  emit("update:open", false);
139
140
  } catch (e) {
140
- toast.error(e?.message ?? "Could not create credential.");
141
+ toast.error(e?.message ?? t("integration.credentials.createDialog.error.create"));
141
142
  } finally {
142
143
  saving.value = false;
143
144
  }
@@ -148,16 +149,16 @@ async function submit() {
148
149
  <Dialog :open="open" @update:open="(v) => emit('update:open', v)">
149
150
  <DialogContent class="sm:max-w-lg">
150
151
  <DialogHeader>
151
- <DialogTitle>New credential</DialogTitle>
152
+ <DialogTitle>{{ t("integration.credentials.shared.dialog.title.create") }}</DialogTitle>
152
153
  <DialogDescription class="text-sm">
153
- Create a credential for this node without leaving the editor.
154
+ {{ t("integration.credentials.createDialog.description") }}
154
155
  </DialogDescription>
155
156
  </DialogHeader>
156
157
 
157
158
  <!-- Choose the type when the field accepts more than one -->
158
159
  <div v-if="!pickedType" class="space-y-2 py-1">
159
160
  <p v-if="!allowedTypes.length" class="text-sm text-muted-foreground">
160
- No credential type available for this field.
161
+ {{ t("integration.credentials.createDialog.noTypeAvailable") }}
161
162
  </p>
162
163
  <button
163
164
  v-for="t in allowedTypes"
@@ -185,13 +186,13 @@ async function submit() {
185
186
  class="ml-auto text-sm text-muted-foreground hover:text-foreground"
186
187
  @click="pickedType = null"
187
188
  >
188
- Change type
189
+ {{ t("integration.credentials.createDialog.action.changeType") }}
189
190
  </button>
190
191
  </div>
191
192
 
192
193
  <div class="space-y-1.5">
193
- <Label for="cred-name">Name</Label>
194
- <Input id="cred-name" v-model="form.name" class="bg-white" placeholder="e.g. Production key" />
194
+ <Label for="cred-name">{{ t("integration.credentials.shared.field.name.label") }}</Label>
195
+ <Input id="cred-name" v-model="form.name" class="bg-white" :placeholder="t('integration.credentials.createDialog.field.name.placeholder')" />
195
196
  </div>
196
197
 
197
198
  <!-- "Use revenexx …" source picker (SFTP / Mail / API key) -->
@@ -232,7 +233,7 @@ async function submit() {
232
233
  @update:model-value="(v) => form.config[f.key] = v"
233
234
  >
234
235
  <SelectTrigger :id="`create-field-${f.key}`" class="bg-white">
235
- <SelectValue placeholder="Pick one" />
236
+ <SelectValue :placeholder="t('integration.credentials.shared.field.select.placeholder')" />
236
237
  </SelectTrigger>
237
238
  <SelectContent>
238
239
  <SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
@@ -250,7 +251,7 @@ async function submit() {
250
251
  </div>
251
252
 
252
253
  <p v-if="isOAuthAuthcode(pickedType)" class="text-sm text-muted-foreground">
253
- After saving, a window opens to connect with the provider.
254
+ {{ t("integration.credentials.shared.oauthHint") }}
254
255
  </p>
255
256
 
256
257
  <div
@@ -258,7 +259,10 @@ async function submit() {
258
259
  class="rounded-md px-3 py-2 text-sm"
259
260
  :class="testResult.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
260
261
  >
261
- {{ testResult.ok ? "Connection ok." : testResult.error ?? "Connection failed." }}
262
+ <!-- The type's own sentence, either way. This is the surface where it is
263
+ worth the most: nothing is stored yet, so the operator is deciding
264
+ whether these access data reach what they meant (specs/credential-test.md). -->
265
+ {{ testResult.ok ? testResult.message ?? t("integration.credentials.shared.test.ok") : testResult.error ?? testResult.message ?? t("integration.credentials.shared.test.failed") }}
262
266
  </div>
263
267
  </div>
264
268
 
@@ -267,11 +271,11 @@ async function submit() {
267
271
  <Button variant="outline" :disabled="saving || testing" @click="runTest">
268
272
  <Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
269
273
  <TestIcon v-else :size="14" class="mr-1" />
270
- Connect
274
+ {{ t("integration.credentials.shared.action.testConnection") }}
271
275
  </Button>
272
276
  <Button :disabled="saving || !form.name.trim()" @click="submit">
273
277
  <Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
274
- Save
278
+ {{ t("integration.common.action.save") }}
275
279
  </Button>
276
280
  </div>
277
281
  </DialogFooter>
@@ -5,7 +5,7 @@ import {
5
5
  CloseCircle as XCircle,
6
6
  RefreshCircle as Loader2,
7
7
  TestTube as TestIcon,
8
- TrashBinTrash as Trash2
8
+ TrashBinMinimalistic as TrashBin
9
9
  } from "#solar-icons";
10
10
  import {
11
11
  AlertDialog,
@@ -40,12 +40,13 @@ const props = defineProps({
40
40
  type: { type: [Object, null], required: true }
41
41
  });
42
42
  const emit = defineEmits(["update:open", "changed"]);
43
+ const t = useIntegrationsT();
43
44
  const { update, remove, test, oauthAuthorizeUrl, load } = useIntegrationsCredentials();
44
45
  const tab = ref("credentials");
45
- const tabs = [
46
- { key: "credentials", label: "Credentials" },
47
- { key: "details", label: "Details" }
48
- ];
46
+ const tabs = computed(() => [
47
+ { key: "credentials", label: t("integration.credentials.detail.tab.credentials") },
48
+ { key: "details", label: t("integration.credentials.detail.tab.details") }
49
+ ]);
49
50
  const form = reactive({ name: "", config: {} });
50
51
  const sourceActive = ref(false);
51
52
  const saving = ref(false);
@@ -65,8 +66,8 @@ const connecting = ref(false);
65
66
  const typeName = computed(() => resolveLocalized(props.type?.name) || props.credential?.credential_type_slug || "");
66
67
  const isOAuthAuthcode = computed(() => props.type?.auth_kind === "oauth2-authcode");
67
68
  const state = computed(() => props.credential ? credentialState(props.credential) : null);
68
- function fieldsOf(t) {
69
- return t?.fields ?? [];
69
+ function fieldsOf(t2) {
70
+ return t2?.fields ?? [];
70
71
  }
71
72
  function formatDateTime(iso) {
72
73
  if (!iso) return "\u2014";
@@ -118,10 +119,10 @@ async function save() {
118
119
  await update(props.credential.id, { name: form.name.trim(), config: buildConfigPayload() });
119
120
  emit("changed");
120
121
  emit("update:open", false);
121
- toast.success("Credential updated");
122
+ toast.success(t("integration.credentials.toast.updated"));
122
123
  } catch (e) {
123
124
  fieldErrors.value = apiFieldErrors(e);
124
- saveError.value = refusalReason(e, "Could not save credential.");
125
+ saveError.value = refusalReason(e, t("integration.credentials.error.save"));
125
126
  toast.error(saveError.value);
126
127
  } finally {
127
128
  saving.value = false;
@@ -134,11 +135,11 @@ async function destroy() {
134
135
  deleting.value = true;
135
136
  try {
136
137
  await remove(props.credential.id);
137
- toast.success("Credential deleted");
138
+ toast.success(t("integration.credentials.toast.deleted"));
138
139
  emit("changed");
139
140
  emit("update:open", false);
140
141
  } catch (e) {
141
- toast.error(e?.message ?? "Could not delete credential.");
142
+ toast.error(e?.message ?? t("integration.credentials.error.delete"));
142
143
  } finally {
143
144
  deleting.value = false;
144
145
  }
@@ -154,7 +155,7 @@ async function connect() {
154
155
  window.removeEventListener("message", onMessage);
155
156
  await load(true);
156
157
  emit("changed");
157
- toast.success("Connected");
158
+ toast.success(t("integration.credentials.toast.connected"));
158
159
  }
159
160
  };
160
161
  window.addEventListener("message", onMessage);
@@ -167,7 +168,7 @@ async function connect() {
167
168
  }
168
169
  }, 1e3);
169
170
  } catch (e) {
170
- toast.error(e?.message ?? "Could not start OAuth connect.");
171
+ toast.error(e?.message ?? t("integration.credentials.error.oauthConnect"));
171
172
  } finally {
172
173
  connecting.value = false;
173
174
  }
@@ -210,21 +211,27 @@ async function connect() {
210
211
  @click="connect"
211
212
  >
212
213
  <Loader2 v-if="connecting" :size="13" class="mr-1 animate-spin" />
213
- Reconnect
214
+ {{ t("integration.credentials.detail.action.reconnect") }}
214
215
  </Button>
215
216
  </div>
216
- <p v-if="state.key === 'failed' && state.message" class="mt-1 text-sm break-words opacity-90">
217
+ <!-- The stored sentence, and only while nothing fresher is on screen. The
218
+ live box below shows the result of a test just run, and the reload that
219
+ follows it puts the very same sentence here — so both would print it
220
+ twice, and past the length the platform truncates at, twice differently.
221
+ The freshly returned one wins, being whole (specs/credential-test.md
222
+ AC-1). -->
223
+ <p v-if="state.message && !liveTest" class="mt-1 text-sm break-words opacity-90">
217
224
  {{ state.message }}
218
225
  </p>
219
226
  <p v-else-if="state.key === 'untested'" class="mt-1 text-sm opacity-80">
220
- Run a test to verify these credentials actually connect.
227
+ {{ t("integration.credentials.detail.state.untestedHint") }}
221
228
  </p>
222
229
  </div>
223
230
 
224
231
  <!-- Editable fields -->
225
232
  <div class="space-y-3">
226
233
  <div class="space-y-1.5">
227
- <Label for="detail-cred-name">Name</Label>
234
+ <Label for="detail-cred-name">{{ t("integration.credentials.shared.field.name.label") }}</Label>
228
235
  <Input
229
236
  id="detail-cred-name"
230
237
  v-model="form.name"
@@ -262,7 +269,7 @@ async function connect() {
262
269
  :model-value="form.config[f.key] ?? ''"
263
270
  type="password"
264
271
  autocomplete="new-password"
265
- placeholder="Leave blank to keep current"
272
+ :placeholder="t('integration.credentials.shared.field.secret.placeholder')"
266
273
  class="bg-white"
267
274
  v-bind="configFieldAria(f.key)"
268
275
  @update:model-value="(v) => form.config[f.key] = v"
@@ -285,7 +292,7 @@ async function connect() {
285
292
  @update:model-value="(v) => form.config[f.key] = v"
286
293
  >
287
294
  <SelectTrigger :id="`detail-field-${f.key}`" class="bg-white" v-bind="configFieldAria(f.key)">
288
- <SelectValue placeholder="Pick one" />
295
+ <SelectValue :placeholder="t('integration.credentials.shared.field.select.placeholder')" />
289
296
  </SelectTrigger>
290
297
  <SelectContent>
291
298
  <SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
@@ -313,7 +320,7 @@ async function connect() {
313
320
  class="rounded-md px-3 py-2 text-sm"
314
321
  :class="liveTest.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
315
322
  >
316
- {{ liveTest.ok ? "Connection ok." : liveTest.error ?? "Connection failed." }}
323
+ {{ liveTest.ok ? liveTest.message ?? t("integration.credentials.shared.test.ok") : liveTest.error ?? liveTest.message ?? t("integration.credentials.shared.test.failed") }}
317
324
  </div>
318
325
 
319
326
  <!-- The save was refused. Same rule as the create dialog: stated here, and at
@@ -327,11 +334,11 @@ async function connect() {
327
334
  <Button variant="outline" :disabled="testing || saving" @click="runTest">
328
335
  <Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
329
336
  <TestIcon v-else :size="14" class="mr-1" />
330
- Connect
337
+ {{ t("integration.credentials.shared.action.testConnection") }}
331
338
  </Button>
332
339
  <Button :disabled="saving || !form.name.trim()" @click="save">
333
340
  <Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
334
- Save
341
+ {{ t("integration.common.action.save") }}
335
342
  </Button>
336
343
  </div>
337
344
  </div>
@@ -341,37 +348,37 @@ async function connect() {
341
348
  <!-- Tab 2: Details -->
342
349
  <div v-else class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
343
350
  <div class="grid grid-cols-[9rem_1fr] gap-x-4 gap-y-2 text-sm">
344
- <span class="text-muted-foreground">Type</span>
351
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.type") }}</span>
345
352
  <span>{{ typeName }}</span>
346
353
 
347
- <span class="text-muted-foreground">Type slug</span>
354
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.typeSlug") }}</span>
348
355
  <span class="font-mono text-xs">{{ credential.credential_type_slug }}</span>
349
356
 
350
- <span class="text-muted-foreground">Version</span>
357
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.version") }}</span>
351
358
  <span class="font-mono text-xs">{{ type?.version ?? "\u2014" }}</span>
352
359
 
353
- <span class="text-muted-foreground">Auth kind</span>
360
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.authKind") }}</span>
354
361
  <span class="font-mono text-xs">{{ type?.auth_kind ?? "\u2014" }}</span>
355
362
 
356
- <span class="text-muted-foreground">Last tested</span>
363
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.lastTested") }}</span>
357
364
  <span class="tabular-nums">{{ formatDateTime(credential.last_test_at) }}</span>
358
365
 
359
- <span class="text-muted-foreground">Credential ID</span>
366
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.id") }}</span>
360
367
  <span class="font-mono text-xs break-all">{{ credential.id }}</span>
361
368
 
362
- <span class="text-muted-foreground">Created</span>
369
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.created") }}</span>
363
370
  <span class="tabular-nums">{{ formatDateTime(credential.created_at) }}</span>
364
371
 
365
- <span class="text-muted-foreground">Updated</span>
372
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.updated") }}</span>
366
373
  <span class="tabular-nums">{{ formatDateTime(credential.updated_at) }}</span>
367
374
  </div>
368
375
 
369
376
  <!-- Danger zone -->
370
377
  <div class="rounded-lg border border-rose-500/30 bg-rose-500/[0.03] p-3">
371
- <div class="text-sm font-semibold text-rose-700">Danger zone</div>
378
+ <div class="text-sm font-semibold text-rose-700">{{ t("integration.credentials.detail.dangerZone.title") }}</div>
372
379
  <div class="mt-1.5 flex items-center justify-between gap-3">
373
380
  <p class="text-sm text-muted-foreground">
374
- Deleting this credential breaks any workflow that references it until it's re-bound.
381
+ {{ t("integration.credentials.detail.dangerZone.description") }}
375
382
  </p>
376
383
  <Button
377
384
  variant="outline"
@@ -381,8 +388,8 @@ async function connect() {
381
388
  @click="pendingDelete = true"
382
389
  >
383
390
  <Loader2 v-if="deleting" :size="14" class="mr-1 animate-spin" />
384
- <Trash2 v-else :size="14" class="mr-1" />
385
- Delete credential
391
+ <TrashBin v-else :size="14" class="mr-1" />
392
+ {{ t("integration.credentials.detail.action.deleteCredential") }}
386
393
  </Button>
387
394
  </div>
388
395
  </div>
@@ -392,19 +399,19 @@ async function connect() {
392
399
  <AlertDialog :open="pendingDelete" @update:open="(v) => pendingDelete = v">
393
400
  <AlertDialogContent>
394
401
  <AlertDialogHeader>
395
- <AlertDialogTitle>Delete credential?</AlertDialogTitle>
402
+ <AlertDialogTitle>{{ t("integration.credentials.shared.confirm.delete.title") }}</AlertDialogTitle>
396
403
  <AlertDialogDescription>
397
404
  <span class="font-medium text-foreground">{{ credential?.name }}</span>
398
- will be removed. Workflows that reference it will fail until they are re-bound to another credential.
405
+ {{ t("integration.credentials.shared.confirm.delete.body") }}
399
406
  </AlertDialogDescription>
400
407
  </AlertDialogHeader>
401
408
  <AlertDialogFooter>
402
409
  <Button variant="outline" :disabled="deleting" @click="pendingDelete = false">
403
- Cancel
410
+ {{ t("integration.common.action.cancel") }}
404
411
  </Button>
405
412
  <Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="deleting" @click="destroy">
406
413
  <Loader2 v-if="deleting" :size="14" class="animate-spin" />
407
- {{ deleting ? "Deleting\u2026" : "Delete" }}
414
+ {{ deleting ? t("integration.common.state.deleting") : t("integration.common.action.delete") }}
408
415
  </Button>
409
416
  </AlertDialogFooter>
410
417
  </AlertDialogContent>