mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-05-14 17:27:30 +01:00
Improve delete confirmation dialogs with copyable resource ID and warning (#2464)
* feat: add copyable ID to delete confirmation dialogs When deleting databases or containers, the confirmation dialog now displays the resource ID in a read-only text field with a copy button, allowing users to copy-paste the ID into the confirmation input instead of typing it manually. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fixed formatting. * revert non-en locale changes; add localization instruction Revert changes to non-English locale files — translations are managed by a separate localization process. Add a note to copilot instructions clarifying that only en/Resources.json should be modified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: capitalize 'Id' in copyable resource ID labels Changed 'id:' to 'Id:' in the copyable ID labels for delete confirmation dialogs (both database and collection). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: capitalize collection name in copyable ID label Use getCollectionName() directly (returns 'Container', 'Collection', etc.) instead of the lowercased collectionName variable for the copyable ID label. The database panel already used getDatabaseName() which returns capitalized. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add warning message to delete container confirmation dialog Added the same warning banner that exists in the delete database dialog to the delete container dialog, informing users that the action cannot be undone and will permanently delete the resource and its children. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -109,6 +109,8 @@ const title = t("splashScreen.title.default");
|
|||||||
```
|
```
|
||||||
The `ResourceKey` type (derived from `Resources.json`) ensures compile-time safety — invalid keys will cause a type error. When adding new strings, add the English entry to `Resources.json` first, then reference it with `t()`.
|
The `ResourceKey` type (derived from `Resources.json`) ensures compile-time safety — invalid keys will cause a type error. When adding new strings, add the English entry to `Resources.json` first, then reference it with `t()`.
|
||||||
|
|
||||||
|
**Important:** Only modify the English resource file (`src/Localization/en/Resources.json`). Do not modify non-English locale files (`src/Localization/<locale>/Resources.json`) — translations are managed by a separate localization process.
|
||||||
|
|
||||||
### Imports
|
### Imports
|
||||||
|
|
||||||
TypeScript `baseUrl` is set to `src/`, so imports from `src/` are written without a leading `./src/` prefix:
|
TypeScript `baseUrl` is set to `src/`, so imports from `src/` are written without a leading `./src/` prefix:
|
||||||
|
|||||||
Generated
+1
@@ -15904,6 +15904,7 @@
|
|||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
+3
@@ -112,6 +112,9 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||||||
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
expect(wrapper.exists("#copyableCollectionId")).toBe(true);
|
||||||
|
expect(wrapper.find("#copyableCollectionId").hostNodes().prop("value")).toBe(selectedCollectionId);
|
||||||
|
|
||||||
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||||
wrapper
|
wrapper
|
||||||
.find("#confirmCollectionId")
|
.find("#confirmCollectionId")
|
||||||
|
|||||||
+34
-1
@@ -1,4 +1,4 @@
|
|||||||
import { Text, TextField } from "@fluentui/react";
|
import { IconButton, Text, TextField } from "@fluentui/react";
|
||||||
import { Areas } from "Common/Constants";
|
import { Areas } from "Common/Constants";
|
||||||
import DeleteFeedback from "Common/DeleteFeedback";
|
import DeleteFeedback from "Common/DeleteFeedback";
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
@@ -17,6 +17,7 @@ import React, { FunctionComponent, useState } from "react";
|
|||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
|
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "../PanelInfoErrorComponent";
|
||||||
|
|
||||||
const themedTextFieldStyles = {
|
const themedTextFieldStyles = {
|
||||||
fieldGroup: {
|
fieldGroup: {
|
||||||
@@ -54,6 +55,10 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
|
|
||||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||||
const paneTitle = t(Keys.panes.deleteCollection.panelTitle, { collectionName });
|
const paneTitle = t(Keys.panes.deleteCollection.panelTitle, { collectionName });
|
||||||
|
const selectedCollection = useSelectedNode.getState().selectedNode
|
||||||
|
? useSelectedNode.getState().findSelectedCollection()
|
||||||
|
: undefined;
|
||||||
|
const selectedCollectionId = selectedCollection?.id() ?? "";
|
||||||
|
|
||||||
const onSubmit = async (): Promise<void> => {
|
const onSubmit = async (): Promise<void> => {
|
||||||
const collection = useSelectedNode.getState().findSelectedCollection();
|
const collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
@@ -131,6 +136,14 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
submitButtonText: t(Keys.common.ok),
|
submitButtonText: t(Keys.common.ok),
|
||||||
onSubmit,
|
onSubmit,
|
||||||
};
|
};
|
||||||
|
const errorProps: PanelInfoErrorProps = {
|
||||||
|
messageType: "warning",
|
||||||
|
showErrorDetails: false,
|
||||||
|
message: t(Keys.panes.deleteCollection.warningMessage),
|
||||||
|
};
|
||||||
|
const copyableIdLabel = t(Keys.panes.deleteCollection.copyableId, {
|
||||||
|
collectionName: getCollectionName(),
|
||||||
|
});
|
||||||
const confirmContainer = t(Keys.panes.deleteCollection.confirmPrompt, {
|
const confirmContainer = t(Keys.panes.deleteCollection.confirmPrompt, {
|
||||||
collectionName: collectionName.toLowerCase(),
|
collectionName: collectionName.toLowerCase(),
|
||||||
});
|
});
|
||||||
@@ -140,9 +153,29 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
t(Keys.panes.deleteCollection.feedbackReason, { collectionName });
|
t(Keys.panes.deleteCollection.feedbackReason, { collectionName });
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...props}>
|
<RightPaneForm {...props}>
|
||||||
|
{!formError && <PanelInfoErrorComponent {...errorProps} />}
|
||||||
<div className="panelFormWrapper">
|
<div className="panelFormWrapper">
|
||||||
<div className="panelMainContent">
|
<div className="panelMainContent">
|
||||||
<div className="confirmDeleteInput">
|
<div className="confirmDeleteInput">
|
||||||
|
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||||
|
{copyableIdLabel}
|
||||||
|
</Text>
|
||||||
|
<TextField
|
||||||
|
id="copyableCollectionId"
|
||||||
|
readOnly
|
||||||
|
value={selectedCollectionId}
|
||||||
|
styles={themedTextFieldStyles}
|
||||||
|
onRenderSuffix={() => (
|
||||||
|
<IconButton
|
||||||
|
iconProps={{ iconName: "Copy" }}
|
||||||
|
title={t(Keys.common.copy)}
|
||||||
|
ariaLabel={t(Keys.common.copy)}
|
||||||
|
onClick={() => navigator.clipboard.writeText(selectedCollectionId)}
|
||||||
|
styles={{ root: { height: "100%" } }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
ariaLabel={copyableIdLabel}
|
||||||
|
/>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||||
{confirmContainer}
|
{confirmContainer}
|
||||||
|
|||||||
+1861
-10
File diff suppressed because it is too large
Load Diff
@@ -62,13 +62,15 @@ describe("Delete Database Confirmation Pane", () => {
|
|||||||
const wrapper = mount(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
|
const wrapper = mount(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
|
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
|
||||||
|
expect(wrapper.exists("#copyableDatabaseId")).toBe(true);
|
||||||
|
expect(wrapper.find("#copyableDatabaseId").hostNodes().prop("value")).toBe(selectedDatabaseId);
|
||||||
|
|
||||||
wrapper
|
wrapper
|
||||||
.find("#confirmDatabaseId")
|
.find("#confirmDatabaseId")
|
||||||
.hostNodes()
|
.hostNodes()
|
||||||
.simulate("change", { target: { value: selectedDatabaseId } });
|
.simulate("change", { target: { value: selectedDatabaseId } });
|
||||||
expect(wrapper.exists("button")).toBe(true);
|
expect(wrapper.exists("button")).toBe(true);
|
||||||
wrapper.find("button").hostNodes().simulate("submit");
|
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
|
||||||
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
|
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Text, TextField } from "@fluentui/react";
|
import { IconButton, Text, TextField } from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import { Areas } from "Common/Constants";
|
import { Areas } from "Common/Constants";
|
||||||
import DeleteFeedback from "Common/DeleteFeedback";
|
import DeleteFeedback from "Common/DeleteFeedback";
|
||||||
@@ -150,6 +150,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
showErrorDetails: false,
|
showErrorDetails: false,
|
||||||
message: t(Keys.panes.deleteDatabase.warningMessage),
|
message: t(Keys.panes.deleteDatabase.warningMessage),
|
||||||
};
|
};
|
||||||
|
const copyableIdLabel = t(Keys.panes.deleteDatabase.copyableId, { databaseName: getDatabaseName() });
|
||||||
const confirmDatabase = t(Keys.panes.deleteDatabase.confirmPrompt, { databaseName: getDatabaseName() });
|
const confirmDatabase = t(Keys.panes.deleteDatabase.confirmPrompt, { databaseName: getDatabaseName() });
|
||||||
const reasonInfo =
|
const reasonInfo =
|
||||||
t(Keys.panes.deleteDatabase.feedbackTitle) +
|
t(Keys.panes.deleteDatabase.feedbackTitle) +
|
||||||
@@ -160,6 +161,25 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
{!formError && <PanelInfoErrorComponent {...errorProps} />}
|
{!formError && <PanelInfoErrorComponent {...errorProps} />}
|
||||||
<div className="panelMainContent">
|
<div className="panelMainContent">
|
||||||
<div className="confirmDeleteInput">
|
<div className="confirmDeleteInput">
|
||||||
|
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||||
|
{copyableIdLabel}
|
||||||
|
</Text>
|
||||||
|
<TextField
|
||||||
|
id="copyableDatabaseId"
|
||||||
|
readOnly
|
||||||
|
value={selectedDatabase?.id() ?? ""}
|
||||||
|
styles={themedTextFieldStyles}
|
||||||
|
onRenderSuffix={() => (
|
||||||
|
<IconButton
|
||||||
|
iconProps={{ iconName: "Copy" }}
|
||||||
|
title={t(Keys.common.copy)}
|
||||||
|
ariaLabel={t(Keys.common.copy)}
|
||||||
|
onClick={() => navigator.clipboard.writeText(selectedDatabase?.id() ?? "")}
|
||||||
|
styles={{ root: { height: "100%" } }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
ariaLabel={copyableIdLabel}
|
||||||
|
/>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||||
{confirmDatabase}
|
{confirmDatabase}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -420,6 +420,7 @@
|
|||||||
"deleteDatabase": {
|
"deleteDatabase": {
|
||||||
"panelTitle": "Delete {{databaseName}}",
|
"panelTitle": "Delete {{databaseName}}",
|
||||||
"warningMessage": "Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
"warningMessage": "Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||||
|
"copyableId": "{{databaseName}} Id:",
|
||||||
"confirmPrompt": "Confirm by typing the {{databaseName}} id (name)",
|
"confirmPrompt": "Confirm by typing the {{databaseName}} id (name)",
|
||||||
"inputMismatch": "Input {{databaseName}} name \"{{input}}\" does not match the selected {{databaseName}} \"{{selectedId}}\"",
|
"inputMismatch": "Input {{databaseName}} name \"{{input}}\" does not match the selected {{databaseName}} \"{{selectedId}}\"",
|
||||||
"feedbackTitle": "Help us improve Azure Cosmos DB!",
|
"feedbackTitle": "Help us improve Azure Cosmos DB!",
|
||||||
@@ -427,6 +428,8 @@
|
|||||||
},
|
},
|
||||||
"deleteCollection": {
|
"deleteCollection": {
|
||||||
"panelTitle": "Delete {{collectionName}}",
|
"panelTitle": "Delete {{collectionName}}",
|
||||||
|
"warningMessage": "Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||||
|
"copyableId": "{{collectionName}} Id:",
|
||||||
"confirmPrompt": "Confirm by typing the {{collectionName}} id",
|
"confirmPrompt": "Confirm by typing the {{collectionName}} id",
|
||||||
"inputMismatch": "Input id {{input}} does not match the selected {{selectedId}}",
|
"inputMismatch": "Input id {{input}} does not match the selected {{selectedId}}",
|
||||||
"feedbackTitle": "Help us improve Azure Cosmos DB!",
|
"feedbackTitle": "Help us improve Azure Cosmos DB!",
|
||||||
|
|||||||
Reference in New Issue
Block a user