diff --git a/client/src/components/User/Credentials/CredentialsGroupForm.vue b/client/src/components/User/Credentials/CredentialsGroupForm.vue index f208465d0a0..d7c0f285880 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 { BFormGroup, BFormInput, BInputGroup, BInputGroupAppend } from "bootstrap-vue"; +import { computed, ref, watch } from "vue"; import type { CredentialType, @@ -31,6 +31,11 @@ import type { ServiceCredentialsDefinition, ServiceParameterDefinition, } from "@/api/userCredentials"; +import { SECRET_PLACEHOLDER } from "@/stores/userToolsServiceCredentialsStore"; + +import GButton from "@/components/BaseComponents/GButton.vue"; + +type SecretField = ServiceCredentialGroupPayload["secrets"][number]; /** * Edit group structure for form data @@ -61,6 +66,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 +199,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 }, +);