Skip to content

Commit a65caa1

Browse files
committed
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.
1 parent 4fdb8de commit a65caa1

File tree

1 file changed

+122
-15
lines changed

1 file changed

+122
-15
lines changed

client/src/components/User/Credentials/CredentialsGroupForm.vue

Lines changed: 122 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@
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
2828
import 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
6265
const 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
*/
188199
function 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

Comments
 (0)