From a65caa10fed488904e93581bb8665c08e419967b Mon Sep 17 00:00:00 2001 From: Alireza Heidari Date: Fri, 23 Jan 2026 11:58:05 +0100 Subject: [PATCH 1/2] feat(credentials): improve secret placeholder handling and validation Enhance the credentials group form UX by tracking touched fields to provide a neutral initial validation state. Add focus/blur handling for secret inputs to properly clear and restore placeholder values, support explicit clearing of secrets, and ensure validation only triggers after user interaction. This prevents accidental overwrites of existing secrets while giving clearer feedback during editing. --- .../User/Credentials/CredentialsGroupForm.vue | 137 ++++++++++++++++-- 1 file changed, 122 insertions(+), 15 deletions(-) diff --git a/client/src/components/User/Credentials/CredentialsGroupForm.vue b/client/src/components/User/Credentials/CredentialsGroupForm.vue index f208465d0a0..40a25f99d68 100644 --- a/client/src/components/User/Credentials/CredentialsGroupForm.vue +++ b/client/src/components/User/Credentials/CredentialsGroupForm.vue @@ -22,8 +22,8 @@ * :service-definition="serviceDefinition" /> */ -import { BFormGroup, BFormInput } from "bootstrap-vue"; -import { computed } from "vue"; +import { BButton, BFormGroup, BFormInput, BInputGroup, BInputGroupAppend } from "bootstrap-vue"; +import { computed, ref, watch } from "vue"; import type { CredentialType, @@ -31,6 +31,9 @@ import type { ServiceCredentialsDefinition, ServiceParameterDefinition, } from "@/api/userCredentials"; +import { SECRET_PLACEHOLDER } from "@/stores/userToolsServiceCredentialsStore"; + +type SecretField = ServiceCredentialGroupPayload["secrets"][number]; /** * Edit group structure for form data @@ -61,6 +64,14 @@ interface Props { const props = defineProps(); +/** Secrets that were initially set and represented by the placeholder. */ +const placeholderSecretNames = ref>(new Set()); +/** Tracks whether a field has been interacted with to drive neutral initial state. */ +const touchedFields = ref<{ variables: Set; secrets: Set }>({ + variables: new Set(), + secrets: new Set(), +}); + /** * Computed property for group name with getter/setter * @returns {string} Current group name @@ -186,11 +197,90 @@ function isVariableOptional(name: string, type: CredentialType): boolean { * @returns {boolean | null} Validation state - true if valid, false if invalid, null if neutral */ function getFieldState(value: string | null | undefined, name: string, type: CredentialType): boolean | null { + const isTouched = touchedFields.value.secrets.has(name); + if (!isTouched) { + return null; + } if (!value) { return isVariableOptional(name, type) ? null : false; } return true; } + +/** + * Marks a field as interacted with. + * @param {string} name - Name of the field + * @param {CredentialType} type - Type of credential (variable or secret) + * @returns {void} + */ +function markTouched(name: string, type: CredentialType): void { + const key = type === "secret" ? "secrets" : "variables"; + const focusedFields = touchedFields.value[key]; + + if (focusedFields.has(name)) { + return; + } + + const updated = new Set(focusedFields); + updated.add(name); + + touchedFields.value = { + ...touchedFields.value, + [key]: updated, + }; +} + +/** + * Clears placeholder when user starts editing a secret. + * @param {SecretField} secret - The secret field being focused + * @returns {void} + */ +function onSecretFocus(secret: SecretField): void { + if (secret.value === SECRET_PLACEHOLDER) { + secret.value = ""; + } +} + +/** + * Restores placeholder if a placeholder-backed secret was left untouched. + * @param {SecretField} secret - The secret field being blurred + * @returns {void} + */ +function onSecretBlur(secret: SecretField): void { + markTouched(secret.name, "secret"); + if ((secret.value === null || secret.value === "") && placeholderSecretNames.value.has(secret.name)) { + secret.value = SECRET_PLACEHOLDER; + } +} + +/** + * Marks variable input as touched on blur. + * @param {string} name - The variable name + * @returns {void} + */ +function onVariableBlur(name: string): void { + markTouched(name, "variable"); +} + +/** + * Clears a secret input and prevents placeholder restore on blur. + * @param {SecretField} secret - The secret field to clear + * @returns {void} + */ +function clearSecret(secret: SecretField): void { + markTouched(secret.name, "secret"); + placeholderSecretNames.value.delete(secret.name); + secret.value = ""; +} + +watch( + () => props.groupData.groupPayload.secrets, + (newSecrets) => { + const filtered = newSecrets.filter((s) => s.value === SECRET_PLACEHOLDER).map((s) => s.name); + placeholderSecretNames.value = new Set(filtered); + }, + { immediate: true }, +);