Files
cosmos-explorer/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx
T
jawelton74 5bf0970b5e 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>
2026-04-28 07:48:33 -07:00

221 lines
8.2 KiB
TypeScript

import { IconButton, Text, TextField } from "@fluentui/react";
import { Areas } from "Common/Constants";
import DeleteFeedback from "Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { deleteCollection } from "Common/dataAccess/deleteCollection";
import { Collection } from "Contracts/ViewModels";
import { Keys, t } from "Localization";
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils";
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs";
import React, { FunctionComponent, useState } from "react";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "../PanelInfoErrorComponent";
const themedTextFieldStyles = {
fieldGroup: {
width: 300,
backgroundColor: "var(--colorNeutralBackground1)",
borderColor: "var(--colorNeutralStroke1)",
selectors: {
":hover": { borderColor: "var(--colorNeutralStroke1Hover)" },
},
},
field: {
color: "var(--colorNeutralForeground1)",
backgroundColor: "var(--colorNeutralBackground1)",
},
subComponentStyles: {
label: { root: { color: "var(--colorNeutralForeground1)" } },
},
};
export interface DeleteCollectionConfirmationPaneProps {
refreshDatabases: () => Promise<void>;
}
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
refreshDatabases,
}: DeleteCollectionConfirmationPaneProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
const [inputCollectionName, setInputCollectionName] = useState<string>("");
const [formError, setFormError] = useState<string>("");
const [isExecuting, setIsExecuting] = useState(false);
const shouldRecordFeedback = (): boolean =>
useDatabases.getState().isLastCollection() && !useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
const collectionName = getCollectionName().toLocaleLowerCase();
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 collection = useSelectedNode.getState().findSelectedCollection();
if (!collection || inputCollectionName !== collection.id()) {
const errorMessage = t(Keys.panes.deleteCollection.inputMismatch, {
input: inputCollectionName,
selectedId: collection.id(),
});
setFormError(errorMessage);
NotificationConsoleUtils.logConsoleError(
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`,
);
return;
}
const paneInfo = {
collectionId: collection.id(),
dataExplorerArea: Areas.ContextualPane,
paneTitle,
};
setFormError("");
setIsExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, paneInfo);
try {
await deleteCollection(collection.databaseId, collection.id());
setIsExecuting(false);
useSelectedNode.getState().setSelectedNode(collection.database);
useTabs
.getState()
.closeTabsByComparator(
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId,
);
refreshDatabases();
TelemetryProcessor.traceSuccess(Action.DeleteCollection, paneInfo, startKey);
if (shouldRecordFeedback()) {
const deleteFeedback = new DeleteFeedback(
userContext.databaseAccount?.id,
userContext.databaseAccount?.name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.apiType),
deleteCollectionFeedback,
);
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
}
closeSidePanel();
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(errorMessage);
setIsExecuting(false);
TelemetryProcessor.traceFailure(
Action.DeleteCollection,
{
...paneInfo,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey,
);
}
};
const props: RightPaneFormProps = {
formError: formError,
isExecuting,
submitButtonText: t(Keys.common.ok),
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, {
collectionName: collectionName.toLowerCase(),
});
const reasonInfo =
t(Keys.panes.deleteCollection.feedbackTitle) +
" " +
t(Keys.panes.deleteCollection.feedbackReason, { collectionName });
return (
<RightPaneForm {...props}>
{!formError && <PanelInfoErrorComponent {...errorProps} />}
<div className="panelFormWrapper">
<div className="panelMainContent">
<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>
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{confirmContainer}
</Text>
<TextField
id="confirmCollectionId"
autoFocus
value={inputCollectionName}
styles={themedTextFieldStyles}
onChange={(event, newInput?: string) => {
setInputCollectionName(newInput);
}}
ariaLabel={confirmContainer}
required
/>
</div>
{shouldRecordFeedback() && (
<div className="deleteCollectionFeedback">
<Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.deleteCollection.feedbackTitle)}
</Text>
<Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.deleteCollection.feedbackReason, { collectionName })}
</Text>
<TextField
id="deleteCollectionFeedbackInput"
styles={themedTextFieldStyles}
multiline
value={deleteCollectionFeedback}
rows={3}
onChange={(event, newInput?: string) => {
setDeleteCollectionFeedback(newInput);
}}
ariaLabel={reasonInfo}
/>
</div>
)}
</div>
</div>
</RightPaneForm>
);
};