2222 * :service-definition="serviceDefinition" />
2323 */
2424
25- import { BFormGroup , BFormInput } from " bootstrap-vue" ;
26- import { computed } from " vue" ;
25+ import { BButton , BFormGroup , BFormInput , BInputGroup , BInputGroupAppend } from " bootstrap-vue" ;
26+ import { computed , ref , watch } from " vue" ;
2727
2828import type {
2929 CredentialType ,
3030 ServiceCredentialGroupPayload ,
3131 ServiceCredentialsDefinition ,
3232 ServiceParameterDefinition ,
3333} from " @/api/userCredentials" ;
34+ import { SECRET_PLACEHOLDER } from " @/stores/userToolsServiceCredentialsStore" ;
35+
36+ type SecretField = ServiceCredentialGroupPayload [" secrets" ][number ];
3437
3538/**
3639 * Edit group structure for form data
@@ -61,6 +64,14 @@ interface Props {
6164
6265const props = defineProps <Props >();
6366
67+ /** Secrets that were initially set and represented by the placeholder. */
68+ const placeholderSecretNames = ref <Set <string >>(new Set ());
69+ /** Tracks whether a field has been interacted with to drive neutral initial state. */
70+ const touchedFields = ref <{ variables: Set <string >; secrets: Set <string > }>({
71+ variables: new Set (),
72+ secrets: new Set (),
73+ });
74+
6475/**
6576 * Computed property for group name with getter/setter
6677 * @returns {string} Current group name
@@ -186,11 +197,90 @@ function isVariableOptional(name: string, type: CredentialType): boolean {
186197 * @returns {boolean | null} Validation state - true if valid, false if invalid, null if neutral
187198 */
188199function getFieldState(value : string | null | undefined , name : string , type : CredentialType ): boolean | null {
200+ const isTouched = touchedFields .value .secrets .has (name );
201+ if (! isTouched ) {
202+ return null ;
203+ }
189204 if (! value ) {
190205 return isVariableOptional (name , type ) ? null : false ;
191206 }
192207 return true ;
193208}
209+
210+ /**
211+ * Marks a field as interacted with.
212+ * @param {string} name - Name of the field
213+ * @param {CredentialType} type - Type of credential (variable or secret)
214+ * @returns {void}
215+ */
216+ function markTouched(name : string , type : CredentialType ): void {
217+ const key = type === " secret" ? " secrets" : " variables" ;
218+ const focusedFields = touchedFields .value [key ];
219+
220+ if (focusedFields .has (name )) {
221+ return ;
222+ }
223+
224+ const updated = new Set (focusedFields );
225+ updated .add (name );
226+
227+ touchedFields .value = {
228+ ... touchedFields .value ,
229+ [key ]: updated ,
230+ };
231+ }
232+
233+ /**
234+ * Clears placeholder when user starts editing a secret.
235+ * @param {SecretField} secret - The secret field being focused
236+ * @returns {void}
237+ */
238+ function onSecretFocus(secret : SecretField ): void {
239+ if (secret .value === SECRET_PLACEHOLDER ) {
240+ secret .value = " " ;
241+ }
242+ }
243+
244+ /**
245+ * Restores placeholder if a placeholder-backed secret was left untouched.
246+ * @param {SecretField} secret - The secret field being blurred
247+ * @returns {void}
248+ */
249+ function onSecretBlur(secret : SecretField ): void {
250+ markTouched (secret .name , " secret" );
251+ if ((secret .value === null || secret .value === " " ) && placeholderSecretNames .value .has (secret .name )) {
252+ secret .value = SECRET_PLACEHOLDER ;
253+ }
254+ }
255+
256+ /**
257+ * Marks variable input as touched on blur.
258+ * @param {string} name - The variable name
259+ * @returns {void}
260+ */
261+ function onVariableBlur(name : string ): void {
262+ markTouched (name , " variable" );
263+ }
264+
265+ /**
266+ * Clears a secret input and prevents placeholder restore on blur.
267+ * @param {SecretField} secret - The secret field to clear
268+ * @returns {void}
269+ */
270+ function clearSecret(secret : SecretField ): void {
271+ markTouched (secret .name , " secret" );
272+ placeholderSecretNames .value .delete (secret .name );
273+ secret .value = " " ;
274+ }
275+
276+ watch (
277+ () => props .groupData .groupPayload .secrets ,
278+ (newSecrets ) => {
279+ const filtered = newSecrets .filter ((s ) => s .value === SECRET_PLACEHOLDER ).map ((s ) => s .name );
280+ placeholderSecretNames .value = new Set (filtered );
281+ },
282+ { immediate: true },
283+ );
194284 </script >
195285
196286<template >
@@ -226,7 +316,8 @@ function getFieldState(value: string | null | undefined, name: string, type: Cre
226316 :aria-label =" getVariableTitle(variable.name, 'variable')"
227317 :required =" !isVariableOptional(variable.name, 'variable')"
228318 :readonly =" false"
229- class =" mb-2" />
319+ class =" mb-2"
320+ @blur =" onVariableBlur(variable.name)" />
230321 </BFormGroup >
231322 </div >
232323
@@ -235,18 +326,34 @@ function getFieldState(value: string | null | undefined, name: string, type: Cre
235326 :id =" getFieldId(secret.name, 'secret')"
236327 :label =" getVariableTitle(secret.name, 'secret')"
237328 :description =" getVariableDescription(secret.name, 'secret')" >
238- <BFormInput
239- :id =" getInputId(secret.name, 'secret')"
240- v-model =" secret.value"
241- type =" password"
242- autocomplete =" off"
243- :state =" getFieldState(secret.value, secret.name, 'secret')"
244- :placeholder =" getVariableTitle(secret.name, 'secret')"
245- :title =" getVariableTitle(secret.name, 'secret')"
246- :aria-label =" getVariableTitle(secret.name, 'secret')"
247- :required =" !isVariableOptional(secret.name, 'secret')"
248- :readonly =" false"
249- class =" mb-2" />
329+ <BInputGroup class =" mb-2" >
330+ <BFormInput
331+ :id =" getInputId(secret.name, 'secret')"
332+ v-model =" secret.value"
333+ type =" password"
334+ autocomplete =" off"
335+ :state =" getFieldState(secret.value, secret.name, 'secret')"
336+ :placeholder =" getVariableTitle(secret.name, 'secret')"
337+ :title =" getVariableTitle(secret.name, 'secret')"
338+ :aria-label =" getVariableTitle(secret.name, 'secret')"
339+ :required =" !isVariableOptional(secret.name, 'secret')"
340+ :readonly =" false"
341+ @focus =" onSecretFocus(secret)"
342+ @blur =" onSecretBlur(secret)" />
343+ <BInputGroupAppend >
344+ <BButton
345+ v-b-tooltip.hover
346+ :disabled =" !secret.value"
347+ variant =" outline-primary"
348+ size =" sm"
349+ :aria-label =" `Clear ${getVariableTitle(secret.name, 'secret')}`"
350+ title =" Clear value"
351+ type =" button"
352+ @click =" clearSecret(secret)" >
353+ Clear
354+ </BButton >
355+ </BInputGroupAppend >
356+ </BInputGroup >
250357 </BFormGroup >
251358 </div >
252359 </div >
0 commit comments