diff --git a/src/commands/deleteCollection/deleteCollection.ts b/src/commands/deleteCollection/deleteCollection.ts index 0445acb21..8fa4c90f4 100644 --- a/src/commands/deleteCollection/deleteCollection.ts +++ b/src/commands/deleteCollection/deleteCollection.ts @@ -45,6 +45,7 @@ export async function deleteCollection(context: IActionContext, node: Collection l10n.t('Delete "{nodeName}"?', { nodeName: node.collectionInfo.name }), message + '\n' + l10n.t('This cannot be undone.'), node.collectionInfo.name, + { fallbackWord: 'delete' }, ); if (!confirmed) { diff --git a/src/commands/deleteDatabase/deleteDatabase.ts b/src/commands/deleteDatabase/deleteDatabase.ts index 4ad5deb45..be392733a 100644 --- a/src/commands/deleteDatabase/deleteDatabase.ts +++ b/src/commands/deleteDatabase/deleteDatabase.ts @@ -44,6 +44,7 @@ export async function deleteDatabase(context: IActionContext, node: DatabaseItem '\n' + l10n.t('This cannot be undone.'), databaseId, + { fallbackWord: 'delete' }, ); if (!confirmed) { diff --git a/src/commands/index.dropIndex/dropIndex.ts b/src/commands/index.dropIndex/dropIndex.ts index 321703788..e67cadc70 100644 --- a/src/commands/index.dropIndex/dropIndex.ts +++ b/src/commands/index.dropIndex/dropIndex.ts @@ -32,7 +32,7 @@ export async function dropIndex(context: IActionContext, node: IndexItem): Promi l10n.t('Delete index "{indexName}" from collection "{collectionName}"?', { indexName, collectionName }) + '\n' + l10n.t('This cannot be undone.'), - indexName, + 'delete', ); if (!confirmed) { diff --git a/src/utils/dialogs/getConfirmation.test.ts b/src/utils/dialogs/getConfirmation.test.ts new file mode 100644 index 000000000..6fbaa743a --- /dev/null +++ b/src/utils/dialogs/getConfirmation.test.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { resolveConfirmationWord } from './getConfirmation'; + +describe('resolveConfirmationWord', () => { + describe('when no fallback is provided', () => { + it('returns the original word regardless of length', () => { + expect(resolveConfirmationWord('myDatabase')).toBe('myDatabase'); + expect(resolveConfirmationWord('a-very-long-name-that-exceeds-the-default-limit')).toBe( + 'a-very-long-name-that-exceeds-the-default-limit', + ); + }); + + it('returns the original word even when it contains non-alpha characters', () => { + expect(resolveConfirmationWord('my-collection_2024')).toBe('my-collection_2024'); + }); + }); + + describe('when a fallback is provided', () => { + it('returns the original word when it is short and alphabetic only', () => { + expect(resolveConfirmationWord('myDatabase', { fallbackWord: 'delete' })).toBe('myDatabase'); + expect(resolveConfirmationWord('delete', { fallbackWord: 'remove' })).toBe('delete'); + }); + + it('returns the fallback when the word exceeds the default maxLength of 16', () => { + const longWord = 'averylongdatabasename'; // 21 chars, all alpha + expect(resolveConfirmationWord(longWord, { fallbackWord: 'delete' })).toBe('delete'); + }); + + it('returns the word when it is exactly at the default maxLength limit', () => { + const exactWord = 'abcdefghijklmnop'; // 16 chars + expect(resolveConfirmationWord(exactWord, { fallbackWord: 'delete' })).toBe(exactWord); + }); + + it('returns the fallback when the word contains non-alphabetic characters', () => { + expect(resolveConfirmationWord('my-collection', { fallbackWord: 'delete' })).toBe('delete'); + expect(resolveConfirmationWord('db_2024', { fallbackWord: 'delete' })).toBe('delete'); + expect(resolveConfirmationWord('name with spaces', { fallbackWord: 'delete' })).toBe('delete'); + expect(resolveConfirmationWord('507f1f77bcf86cd799439011', { fallbackWord: 'delete' })).toBe('delete'); + }); + + it('respects a custom maxLength', () => { + const word = 'abcde'; // 5 chars, all alpha + expect(resolveConfirmationWord(word, { fallbackWord: 'delete', maxLength: 4 })).toBe('delete'); + expect(resolveConfirmationWord(word, { fallbackWord: 'delete', maxLength: 5 })).toBe(word); + expect(resolveConfirmationWord(word, { fallbackWord: 'delete', maxLength: 10 })).toBe(word); + }); + }); +}); diff --git a/src/utils/dialogs/getConfirmation.ts b/src/utils/dialogs/getConfirmation.ts index 9d5e357ec..10d9b277b 100644 --- a/src/utils/dialogs/getConfirmation.ts +++ b/src/utils/dialogs/getConfirmation.ts @@ -14,25 +14,56 @@ enum ConfirmationStyle { buttonConfirmation = 'buttonConfirmation', } +export interface WordConfirmationOptions { + /** + * Fallback word to use when the challenge word is too complex to type comfortably. + * Applied when the word exceeds maxLength or contains characters outside [a-zA-Z]. + */ + fallbackWord?: string; + + /** Maximum allowed length before the fallback is used. Defaults to 16. */ + maxLength?: number; +} + +export function resolveConfirmationWord(word: string, options?: WordConfirmationOptions): string { + const fallback = options?.fallbackWord; + + if (!fallback) { + return word; + } + + const limit = options?.maxLength ?? 16; + + if (word.length > limit || !/^[a-zA-Z]+$/.test(word)) { + return fallback; + } + + return word; +} + /** * Prompts the user for a confirmation based on the configured confirmation style. * * @param title - The title of the confirmation dialog. * @param message - The message to display in the confirmation dialog. This message will be suffixed with instructions for a specific prompt. - * @param expectedConfirmationWord - The word that the user must type to confirm the action when the confirmation style is set to 'Word Confirmation'. + * @param expectedConfirmationWord - The default word used for word confirmation. It may be replaced by fallbackWord when the configured fallback rules apply. + * @param options - Optional settings for word confirmations. Only takes effect when the + * confirmation style is set to 'Word Confirmation'. When fallbackWord is provided, + * it is used if the expected word exceeds maxLength or contains characters outside [a-zA-Z]. * @returns A promise that resolves to a boolean indicating whether the user confirmed the action. */ export async function getConfirmationAsInSettings( title: string, message: string, expectedConfirmationWord: string, + options?: WordConfirmationOptions, ): Promise { const deleteConfirmation: ConfirmationStyle = vscode.workspace .getConfiguration() .get(ext.settingsKeys.confirmationStyle, ConfirmationStyle.wordConfirmation); if (deleteConfirmation === ConfirmationStyle.wordConfirmation) { - return await getConfirmationWithWordQuestion(title, message, expectedConfirmationWord); + return await getConfirmationWithWordQuestion(title, message, expectedConfirmationWord, options); } else if (deleteConfirmation === ConfirmationStyle.challengeConfirmation) { return await getConfirmationWithNumberQuiz(title, message); } @@ -40,18 +71,31 @@ export async function getConfirmationAsInSettings( return await getConfirmationWithClick(title, message); } +/** + * Prompts the user to type a confirmation word. + * + * @param title - The title of the confirmation dialog. + * @param message - The message to display in the confirmation dialog. This message will be suffixed with word confirmation instructions. + * @param expectedConfirmationWord - The default word used for word confirmation. It may be replaced by fallbackWord when the configured fallback rules apply. + * @param options - Optional settings for word confirmations. When fallbackWord is provided, + * it is used if the expected word exceeds maxLength or contains characters outside [a-zA-Z]. + * @returns A promise that resolves to a boolean indicating whether the user entered the confirmation word. + */ export async function getConfirmationWithWordQuestion( title: string, message: string, expectedConfirmationWord: string, + options?: WordConfirmationOptions, ): Promise { + const effectiveWord = resolveConfirmationWord(expectedConfirmationWord, options); + const result = await vscode.window.showInputBox({ title: title, prompt: message + '\n\n' + l10n.t('Please enter the word "{expectedConfirmationWord}" to confirm the operation.', { - expectedConfirmationWord, + expectedConfirmationWord: effectiveWord, }) + '\n\n' + l10n.t('Note: This confirmation type can be configured in the extension settings.'), @@ -61,14 +105,14 @@ export async function getConfirmationWithWordQuestion( if ( val && 0 === - val.localeCompare(expectedConfirmationWord, undefined, { + val.localeCompare(effectiveWord, undefined, { sensitivity: 'accent', }) ) { return undefined; } return l10n.t('Please enter the word "{expectedConfirmationWord}" to confirm the operation.', { - expectedConfirmationWord, + expectedConfirmationWord: effectiveWord, }); }, }); @@ -79,7 +123,7 @@ export async function getConfirmationWithWordQuestion( return ( 0 === - result.localeCompare(expectedConfirmationWord, undefined, { + result.localeCompare(effectiveWord, undefined, { sensitivity: 'accent', }) );