diff --git a/.claude/rules/destructive-confirmation.md b/.claude/rules/destructive-confirmation.md
new file mode 100644
index 0000000000..cfaafa44d8
--- /dev/null
+++ b/.claude/rules/destructive-confirmation.md
@@ -0,0 +1,95 @@
+# Destructive Action Confirmation Rule
+
+For **irreversible destructive actions** (permanent deletion, purge, force termination, data wipe), confirmation must be collected through a **modal that requires the user to type a confirmation string** — not a `Popconfirm`, not a plain `Modal.confirm`, not a single-click dialog.
+
+## Why
+
+Popconfirm and one-click confirmation dialogs are appropriate for **reversible or low-impact** actions (inactivating, hiding, unassigning, canceling a draft). For actions the user cannot undo, a single misclick has permanent consequences. Requiring the user to type a specific string (typically the resource's name) forces a deliberate pause and prevents accidental destruction.
+
+This convention was applied project-wide in FR-2479 ("standardize confirmation UX"), which replaced the legacy `PopConfirmWithInput.tsx` with the shared `BAIConfirmModalWithInput` component.
+
+## Rules
+
+1. **Irreversible actions → `BAIConfirmModalWithInput`** (from `backend.ai-ui`). The user must type a confirmation string (usually the resource's name) before the OK button enables. Examples: permanently delete a VFolder, terminate a model service endpoint, purge a user, delete an image, delete a resource preset, remove a shell script.
+2. **Reversible / low-impact actions → `Popconfirm`** or `App.useApp().modal.confirm({ ... })`. Examples: deactivating (not deleting) a user, canceling an in-progress action, hiding an item, marking inactive.
+3. **Never use `Popconfirm` for permanent deletion**, even when the action is guarded server-side. The UX contract is about *user intent*, not backend safety.
+4. Do **not** reintroduce `PopConfirmWithInput` or any ad-hoc "modal with a text input" for destructive flows — use the shared `BAIConfirmModalWithInput`. This keeps the copy, layout, danger styling, and accessibility consistent.
+5. The confirmation string should be something the user sees on screen and can copy unambiguously (e.g., the resource's `name` or `id`). Avoid opaque tokens.
+
+## Pattern
+
+### ❌ Wrong — Popconfirm for permanent deletion
+
+```tsx
+ deleteForever(row.id)}
+>
+ } />
+
+```
+
+### ❌ Wrong — single-click `modal.confirm` for permanent deletion
+
+```tsx
+modal.confirm({
+ title: t('dialog.ask.DoYouWantToDeleteSomething', { name: row.name }),
+ okButtonProps: { danger: true },
+ onOk: () => deleteForever(row.id),
+});
+```
+
+### ✅ Correct — typed confirmation for permanent deletion
+
+```tsx
+import { BAIConfirmModalWithInput } from 'backend.ai-ui';
+
+const [deletingTarget, setDeletingTarget] = useState(null);
+
+// Trigger
+}
+ onClick={() => setDeletingTarget(row)}
+/>
+
+// Modal
+ {
+ if (deletingTarget) await deleteForever(deletingTarget.id);
+ setDeletingTarget(null);
+ }}
+ onCancel={() => setDeletingTarget(null)}
+/>
+```
+
+### ✅ Correct — `Popconfirm` for reversible actions
+
+```tsx
+ setInactive(row.id)}
+>
+ } />
+
+```
+
+## How to decide
+
+Ask: *"If the user clicks OK by accident, can they recover the state in <30 seconds without contacting support?"*
+
+- **Yes** → `Popconfirm` / `modal.confirm` is fine.
+- **No** → `BAIConfirmModalWithInput`.
+
+Soft-delete / trash-bin flows count as reversible **only if** the UI actually exposes a restore path the user can reach on their own. If restoration requires admin intervention or database access, treat it as irreversible.
+
+## Related
+
+- `BAIConfirmModalWithInput` — `packages/backend.ai-ui/src/components/BAIConfirmModalWithInput.tsx`
+- `BAIDeleteConfirmModal` — higher-level convenience wrapper for common delete flows
+- FR-2479 — the refactor that standardized this convention across the project
diff --git a/packages/backend.ai-ui/src/locale/ko.json b/packages/backend.ai-ui/src/locale/ko.json
index f69ea6b31f..31c9427e19 100644
--- a/packages/backend.ai-ui/src/locale/ko.json
+++ b/packages/backend.ai-ui/src/locale/ko.json
@@ -388,7 +388,7 @@
"modal": {
"DeleteForeverDesc": "경고: 선택한 항목은 완전히 삭제되며 복원할 수 없습니다.",
"ItemSelectedWithCount": "{{count}}개 선택됨.",
- "PleaseTypeToConfirm": "{{ confirmText }}를 입력하십시오."
+ "PleaseTypeToConfirm": "{{ confirmText }}를 입력해주세요."
},
"validation": {
"LetterNumber-_dot": "문자, 숫자, '-', '_', '.'만 입력할 수 있습니다.",
diff --git a/react/src/components/EndpointList.tsx b/react/src/components/EndpointList.tsx
index d132f5016e..4a1002d540 100644
--- a/react/src/components/EndpointList.tsx
+++ b/react/src/components/EndpointList.tsx
@@ -18,7 +18,7 @@ import {
DeleteOutlined,
SettingOutlined,
} from '@ant-design/icons';
-import { Typography, theme, App, TablePaginationConfig } from 'antd';
+import { Typography, theme, App, TablePaginationConfig, Alert } from 'antd';
import type { ColumnType } from 'antd/lib/table';
import {
filterOutEmpty,
@@ -26,10 +26,12 @@ import {
BAITable,
BAITableProps,
BAINameActionCell,
+ BAIConfirmModalWithInput,
+ BAIFlex,
} from 'backend.ai-ui';
import dayjs from 'dayjs';
import * as _ from 'lodash-es';
-import React from 'react';
+import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { graphql, useFragment } from 'react-relay';
@@ -76,10 +78,13 @@ const EndpointList: React.FC = ({
}) => {
const { t } = useTranslation();
const { token } = theme.useToken();
- const { message, modal } = App.useApp();
+ const { message } = App.useApp();
const [currentUser] = useCurrentUserInfo();
const baiClient = useSuspendedBackendaiClient();
const webuiNavigate = useWebUINavigate();
+ const [deletingEndpoint, setDeletingEndpoint] = useState(
+ null,
+ );
const endpoints = useFragment(
graphql`
@@ -151,46 +156,7 @@ const EndpointList: React.FC = ({
type: 'danger',
disabled: isEndpointInDestroyingCategory(row),
onClick: () => {
- modal.confirm({
- title: t('dialog.ask.DoYouWantToDeleteSomething', {
- name: row.name,
- }),
- content: t('dialog.warning.CannotBeUndone'),
- okText: t('button.Delete'),
- okButtonProps: {
- danger: true,
- type: 'primary',
- },
- onOk: () => {
- if (row.endpoint_id) {
- return new Promise((resolve) => {
- terminateModelServiceMutation.mutate(row.endpoint_id!, {
- onSuccess: (res) => {
- onDeleted?.(row);
- if (res.success) {
- message.success(
- t('modelService.ServiceTerminated', {
- name: row?.name,
- }),
- );
- } else {
- message.error(
- t('modelService.FailedToTerminateService'),
- );
- }
- resolve();
- },
- onError: () => {
- message.error(
- t('modelService.FailedToTerminateService'),
- );
- resolve();
- },
- });
- });
- }
- },
- });
+ setDeletingEndpoint(row);
},
},
]}
@@ -282,16 +248,63 @@ const EndpointList: React.FC = ({
]);
return (
-
+ <>
+
+
+
+
+
+ {t('dialog.TypeNameToConfirmDeletion')}
+
+ ({deletingEndpoint?.name})
+
+
+ }
+ confirmText={deletingEndpoint?.name ?? ''}
+ inputProps={{ placeholder: deletingEndpoint?.name ?? '' }}
+ okText={t('button.Delete')}
+ okButtonProps={{ loading: terminateModelServiceMutation.isPending }}
+ onOk={() => {
+ if (deletingEndpoint?.endpoint_id) {
+ terminateModelServiceMutation.mutate(deletingEndpoint.endpoint_id, {
+ onSuccess: (res) => {
+ onDeleted?.(deletingEndpoint);
+ if (res.success) {
+ message.success(
+ t('modelService.ServiceTerminated', {
+ name: deletingEndpoint?.name,
+ }),
+ );
+ } else {
+ message.error(t('modelService.FailedToTerminateService'));
+ }
+ setDeletingEndpoint(null);
+ },
+ onError: () => {
+ message.error(t('modelService.FailedToTerminateService'));
+ setDeletingEndpoint(null);
+ },
+ });
+ }
+ }}
+ onCancel={() => setDeletingEndpoint(null)}
+ />
+ >
);
};
diff --git a/react/src/components/ManageAppsModal.tsx b/react/src/components/ManageAppsModal.tsx
index 533398ffef..5613f65571 100644
--- a/react/src/components/ManageAppsModal.tsx
+++ b/react/src/components/ManageAppsModal.tsx
@@ -11,13 +11,17 @@ import {
Form,
message,
Typography,
- App,
FormInstance,
theme,
} from 'antd';
-import { BAIFlex, BAIModal, BAIModalProps } from 'backend.ai-ui';
+import {
+ BAIFlex,
+ BAIModal,
+ BAIModalProps,
+ BAIConfirmModalWithInput,
+} from 'backend.ai-ui';
import * as _ from 'lodash-es';
-import React from 'react';
+import React, { useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { graphql, useFragment, useMutation } from 'react-relay';
@@ -37,7 +41,10 @@ const ManageAppsModal: React.FC = ({
}) => {
const { t } = useTranslation();
const formRef = React.useRef(null);
- const app = App.useApp();
+ const [isReinstallConfirmOpen, setIsReinstallConfirmOpen] = useState(false);
+ const [pendingCommitRequest, setPendingCommitRequest] = useState<
+ (() => void) | null
+ >(null);
const { token } = theme.useToken();
@@ -157,21 +164,8 @@ const ManageAppsModal: React.FC = ({
});
if (image?.installed) {
- app.modal.confirm({
- title: 'Image reinstallation required',
- content: (
- <>
-
- >
- ),
- onOk: commitRequest,
- getContainer: () => document.body,
- closable: true,
- });
+ setPendingCommitRequest(() => commitRequest);
+ setIsReinstallConfirmOpen(true);
} else {
commitRequest();
}
@@ -329,6 +323,25 @@ const ManageAppsModal: React.FC = ({
+
+ }
+ confirmText={image?.name ?? image?.namespace ?? ''}
+ onOk={() => {
+ setIsReinstallConfirmOpen(false);
+ pendingCommitRequest?.();
+ setPendingCommitRequest(null);
+ }}
+ onCancel={() => {
+ setIsReinstallConfirmOpen(false);
+ setPendingCommitRequest(null);
+ }}
+ />
);
};
diff --git a/react/src/components/ManageImageResourceLimitModal.tsx b/react/src/components/ManageImageResourceLimitModal.tsx
index ec11d2aed3..442e514a3f 100644
--- a/react/src/components/ManageImageResourceLimitModal.tsx
+++ b/react/src/components/ManageImageResourceLimitModal.tsx
@@ -8,23 +8,16 @@ import {
} from '../__generated__/ManageImageResourceLimitModalMutation.graphql';
import { ManageImageResourceLimitModal_image$key } from '../__generated__/ManageImageResourceLimitModal_image.graphql';
import { compareNumberWithUnits } from '../helper';
-import {
- App,
- Form,
- type FormInstance,
- message,
- InputNumber,
- Row,
- Col,
-} from 'antd';
+import { Form, type FormInstance, message, InputNumber, Row, Col } from 'antd';
import {
useResourceSlotsDetails,
BAIModal,
BAIModalProps,
BAIDynamicUnitInputNumber,
+ BAIConfirmModalWithInput,
} from 'backend.ai-ui';
import * as _ from 'lodash-es';
-import React, { useRef, Fragment } from 'react';
+import React, { useRef, useState, Fragment } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { graphql, useFragment, useMutation } from 'react-relay';
@@ -46,8 +39,11 @@ const ManageImageResourceLimitModal: React.FC<
const { t } = useTranslation();
const formRef = useRef(null);
- const app = App.useApp();
const { mergedResourceSlots } = useResourceSlotsDetails();
+ const [isReinstallConfirmOpen, setIsReinstallConfirmOpen] = useState(false);
+ const [pendingCommitRequest, setPendingCommitRequest] = useState<
+ (() => void) | null
+ >(null);
const image = useFragment(
graphql`
@@ -129,17 +125,8 @@ const ManageImageResourceLimitModal: React.FC<
});
if (image?.installed) {
- app.modal.confirm({
- title: t('environment.ImageReinstallationRequired'),
- content: (
-
- ),
- onOk: commitRequest,
- getContainer: () => document.body,
- closable: true,
- });
+ setPendingCommitRequest(() => commitRequest);
+ setIsReinstallConfirmOpen(true);
} else {
commitRequest();
}
@@ -245,6 +232,25 @@ const ManageImageResourceLimitModal: React.FC<
)}
+
+ }
+ confirmText={image?.name ?? image?.namespace ?? ''}
+ onOk={() => {
+ setIsReinstallConfirmOpen(false);
+ pendingCommitRequest?.();
+ setPendingCommitRequest(null);
+ }}
+ onCancel={() => {
+ setIsReinstallConfirmOpen(false);
+ setPendingCommitRequest(null);
+ }}
+ />
);
};
diff --git a/react/src/components/PopConfirmWithInput.tsx b/react/src/components/PopConfirmWithInput.tsx
deleted file mode 100644
index 7f2703525f..0000000000
--- a/react/src/components/PopConfirmWithInput.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- @license
- Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
- */
-import { ExclamationCircleFilled } from '@ant-design/icons';
-import {
- Form,
- Input,
- Popconfirm,
- type PopconfirmProps,
- Typography,
-} from 'antd';
-import { BAIFlex } from 'backend.ai-ui';
-import * as _ from 'lodash-es';
-import React from 'react';
-
-const { Text } = Typography;
-
-interface Props extends PopconfirmProps {
- confirmText: string;
- content: React.ReactNode;
- title: React.ReactNode;
-}
-const PopConfirmWithInput: React.FC = ({
- confirmText,
- children,
- onConfirm,
- onCancel,
- title,
- icon,
- content,
- ...props
-}) => {
- const [form] = Form.useForm();
- const typedText = Form.useWatch('confirmText', form);
-
- return (
- {
- alert(e);
- e.preventDefault();
- e.stopPropagation();
- }}
- >
-
- {icon ?? (
-
- )}
- {title}
-
- {content}
- {/*
- {' '}
- Please type {confirmText} to confirm.
- */}
- {
- if (value === confirmText) {
- return Promise.resolve();
- }
- return Promise.reject();
- },
- },
- ]}
- >
- {
- e.preventDefault();
- e.stopPropagation();
- }}
- />
-
-
-
- }
- icon={null}
- okButtonProps={{ disabled: confirmText !== typedText, danger: true }}
- onConfirm={(e) => {
- form.resetFields();
- _.isFunction(onConfirm) && onConfirm(e);
- }}
- onCancel={(e) => {
- form.resetFields();
- _.isFunction(onCancel) && onCancel(e);
- }}
- okText="Delete"
- cancelText="No"
- {...props}
- >
- {children}
-
- );
-};
-
-export default PopConfirmWithInput;
diff --git a/react/src/components/ResourcePresetList.tsx b/react/src/components/ResourcePresetList.tsx
index f8996781a2..7668c0e52c 100644
--- a/react/src/components/ResourcePresetList.tsx
+++ b/react/src/components/ResourcePresetList.tsx
@@ -17,7 +17,15 @@ import {
SettingOutlined,
DeleteOutlined,
} from '@ant-design/icons';
-import { Tooltip, Button, App, Typography, TableColumnsType } from 'antd';
+import {
+ Tooltip,
+ Button,
+ App,
+ Typography,
+ TableColumnsType,
+ Alert,
+ theme,
+} from 'antd';
import {
filterOutEmpty,
filterOutNullAndUndefined,
@@ -27,6 +35,7 @@ import {
useUpdatableState,
BAIResourceNumberWithIcon,
BAINameActionCell,
+ BAIConfirmModalWithInput,
} from 'backend.ai-ui';
import * as _ from 'lodash-es';
import React, { Suspense, useState, useTransition } from 'react';
@@ -41,13 +50,17 @@ interface ResourcePresetListProps {}
const ResourcePresetList: React.FC = () => {
const { t } = useTranslation();
- const { modal, message } = App.useApp();
+ const { token } = theme.useToken();
+ const { message } = App.useApp();
const [isRefetchPending, startRefetchTransition] = useTransition();
const [resourcePresetsFetchKey, updateResourcePresetsFetchKey] =
useUpdatableState('initial-fetch');
const [editingResourcePreset, setEditingResourcePreset] =
useState(null);
const [isCreating, setIsCreating] = useState(false);
+ const [deletingPresetName, setDeletingPresetName] = useState(
+ null,
+ );
const baiClient = useSuspendedBackendaiClient();
const { resource_presets } = useLazyLoadQuery(
@@ -72,14 +85,15 @@ const ResourcePresetList: React.FC = () => {
},
);
- const [commitDelete] = useMutation(graphql`
- mutation ResourcePresetListDeleteMutation($name: String!) {
- delete_resource_preset(name: $name) {
- ok
- msg
+ const [commitDelete, isDeleteInFlight] =
+ useMutation(graphql`
+ mutation ResourcePresetListDeleteMutation($name: String!) {
+ delete_resource_preset(name: $name) {
+ ok
+ msg
+ }
}
- }
- `);
+ `);
const columns: TableColumnsType = filterOutEmpty([
{
@@ -107,52 +121,7 @@ const ResourcePresetList: React.FC = () => {
icon: ,
type: 'danger',
onClick: () => {
- modal.confirm({
- title: t('resourcePreset.DeleteResourcePreset'),
- content: (
- <>
- {t('resourcePreset.AboutToDeletePreset')}{' '}
- {record?.name}
- >
- ),
- onOk: () => {
- return new Promise((resolve) => {
- commitDelete({
- variables: {
- name: record?.name ?? '',
- },
- onCompleted: (res, errors) => {
- if (!res?.delete_resource_preset?.ok) {
- message.error(res?.delete_resource_preset?.msg);
- } else if (errors && errors?.length > 0) {
- const errorMsgList = _.map(
- errors,
- (err) => err?.message,
- );
- _.forEach(errorMsgList, (err) =>
- message.error(err),
- );
- } else {
- message.success(t('resourcePreset.Deleted'));
- startRefetchTransition(() => {
- updateResourcePresetsFetchKey();
- });
- }
- resolve();
- },
- onError: (error) => {
- message.error(error?.message);
- resolve();
- },
- });
- });
- },
- okText: t('button.Delete'),
- okType: 'primary',
- okButtonProps: {
- danger: true,
- },
- });
+ setDeletingPresetName(record?.name ?? null);
},
},
]}
@@ -235,6 +204,51 @@ const ResourcePresetList: React.FC = () => {
showSorterTooltip={false}
columns={columns}
/>
+
+
+
+
+ {t('dialog.TypeNameToConfirmDeletion')}
+
+ ({deletingPresetName})
+
+
+ }
+ confirmText={deletingPresetName ?? ''}
+ inputProps={{ placeholder: deletingPresetName ?? '' }}
+ okText={t('button.Delete')}
+ okButtonProps={{ loading: isDeleteInFlight }}
+ onOk={() => {
+ commitDelete({
+ variables: {
+ name: deletingPresetName ?? '',
+ },
+ onCompleted: (res, errors) => {
+ if (!res?.delete_resource_preset?.ok) {
+ message.error(res?.delete_resource_preset?.msg);
+ } else if (errors && errors?.length > 0) {
+ const errorMsgList = _.map(errors, (err) => err?.message);
+ _.forEach(errorMsgList, (err) => message.error(err));
+ } else {
+ message.success(t('resourcePreset.Deleted'));
+ startRefetchTransition(() => {
+ updateResourcePresetsFetchKey();
+ });
+ }
+ setDeletingPresetName(null);
+ },
+ onError: (error) => {
+ message.error(error?.message);
+ setDeletingPresetName(null);
+ },
+ });
+ }}
+ onCancel={() => setDeletingPresetName(null)}
+ />
= ({
...modalProps
}) => {
const { t } = useTranslation();
+ const { token } = theme.useToken();
const { logger } = useBAILogger();
- const { message, modal } = App.useApp();
+ const { message } = App.useApp();
+ const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
+ const [isResetConfirmOpen, setIsResetConfirmOpen] = useState(false);
const { getErrorMessage } = useErrorMessageResolver();
const [rcfileNames, setRcfileNames] = useState('.bashrc');
const [script, setScript] = useState('');
@@ -195,42 +210,39 @@ const ShellScriptEditModal: React.FC = ({
type="default"
danger
onClick={() => {
- modal.confirm({
- title: t('dialog.title.LetsDouble-Check'),
- content: t('dialog.ask.DoYouWantToDeleteSomething', {
- name:
- shellInfo === 'bootstrap'
- ? t('session.launcher.BootstrapScriptDetail')
- : rcfileNames,
- }),
- onOk: deleteScript,
- });
+ setIsDeleteConfirmOpen(true);
}}
>
- {
- modal.confirm({
- title: t('dialog.title.LetsDouble-Check'),
- content: t('dialog.ask.DoYouWantToResetChanges'),
- onOk: () => {
- setScript('');
- },
- });
- },
- danger: true,
- },
- ],
+ {
+ setScript('');
+ setIsResetConfirmOpen(false);
}}
+ onCancel={() => setIsResetConfirmOpen(false)}
>
- } />
-
+ {
+ setIsResetConfirmOpen(true);
+ },
+ danger: true,
+ },
+ ],
+ }}
+ >
+ } />
+
+
@@ -311,6 +323,38 @@ const ShellScriptEditModal: React.FC = ({
value={script}
/>
+
+
+
+
+ {t('dialog.TypeNameToConfirmDeletion')}
+
+ (
+
+ {shellInfo === 'bootstrap' ? t('button.Delete') : rcfileNames}
+
+ )
+
+
+ }
+ confirmText={
+ shellInfo === 'bootstrap' ? t('button.Delete') : rcfileNames
+ }
+ inputProps={{
+ placeholder:
+ shellInfo === 'bootstrap' ? t('button.Delete') : rcfileNames,
+ }}
+ okText={t('button.Delete')}
+ onOk={() => {
+ setIsDeleteConfirmOpen(false);
+ deleteScript();
+ }}
+ onCancel={() => setIsDeleteConfirmOpen(false)}
+ />
);
};
diff --git a/react/src/components/VFolderNodes.tsx b/react/src/components/VFolderNodes.tsx
index 82f6b4d6a5..34a36a6e90 100644
--- a/react/src/components/VFolderNodes.tsx
+++ b/react/src/components/VFolderNodes.tsx
@@ -582,6 +582,7 @@ const VFolderNodes: React.FC = ({
setDeletingVFolder(null);
}}
confirmText={deletingVFolder?.name ?? ''}
+ inputProps={{ placeholder: deletingVFolder?.name ?? '' }}
content={
{
const { token } = theme.useToken();
const { agents, builtInAgents, deleteAgent } = useAIAgent();
const webuiNavigate = useWebUINavigate();
- const { modal } = App.useApp();
const { styles } = useStyles();
const [isEditorOpen, setIsEditorOpen] = useState(false);
const [editingAgent, setEditingAgent] = useState();
const [deletingAgent, setDeletingAgent] = useState(null);
+ const [resettingAgent, setResettingAgent] = useState(null);
const builtInIds = new Set(builtInAgents.map((a) => a.id));
@@ -200,11 +210,7 @@ const AIAgentPage: React.FC = () => {
};
const handleReset = (agent: AIAgent) => {
- modal.confirm({
- title: t('aiAgent.ResetConfirmTitle'),
- content: t('aiAgent.ResetConfirmDescription'),
- onOk: () => deleteAgent(agent.id),
- });
+ setResettingAgent(agent);
};
return (
@@ -270,28 +276,70 @@ const AIAgentPage: React.FC = () => {
}}
/>
-
+
+
+
+ {t('dialog.TypeNameToConfirmDeletion')}
+
+ (
+
+ {deletingAgent?.meta.title}
+
+ )
+
+
+ }
+ confirmText={deletingAgent?.meta.title ?? ''}
+ inputProps={{ placeholder: deletingAgent?.meta.title ?? '' }}
+ okText={t('button.Delete')}
onOk={() => {
if (deletingAgent) {
deleteAgent(deletingAgent.id);
- setDeletingAgent(null);
}
+ setDeletingAgent(null);
}}
onCancel={() => setDeletingAgent(null)}
/>
+
+
+
+
+ {t('dialog.TypeNameToConfirmDeletion')}
+
+ (
+
+ {resettingAgent?.meta.title}
+
+ )
+
+
+ }
+ confirmText={resettingAgent?.meta.title ?? ''}
+ inputProps={{ placeholder: resettingAgent?.meta.title ?? '' }}
+ okText={t('button.Reset')}
+ onOk={() => {
+ if (resettingAgent) {
+ deleteAgent(resettingAgent.id);
+ }
+ setResettingAgent(null);
+ }}
+ onCancel={() => setResettingAgent(null)}
+ />
);
diff --git a/resources/i18n/de.json b/resources/i18n/de.json
index fad553a645..89db7dbc28 100644
--- a/resources/i18n/de.json
+++ b/resources/i18n/de.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Ein Fehler ist aufgetreten",
"PleaseTypeToConfirm": "Bitte geben Sie {{confirmText}} ein, um zu bestätigen.",
+ "TypeNameToConfirmDeletion": "Geben Sie den Namen ein, um das Löschen zu bestätigen.",
"ask": {
"DoYouWantToDelete": "Möchten Sie dies wirklich löschen?",
"DoYouWantToDeleteSomething": "Sind Sie sicher, dass Sie \"{{ Name }}\" löschen wollen?",
diff --git a/resources/i18n/el.json b/resources/i18n/el.json
index 299b5340a0..65cf2e3d10 100644
--- a/resources/i18n/el.json
+++ b/resources/i18n/el.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Προέκυψε σφάλμα",
"PleaseTypeToConfirm": "Πληκτρολογήστε {{confirmText}} για επιβεβαίωση.",
+ "TypeNameToConfirmDeletion": "Πληκτρολογήστε το όνομα για να επιβεβαιώσετε τη διαγραφή.",
"ask": {
"DoYouWantToDelete": "Είστε σίγουροι ότι θέλετε να διαγράψετε;",
"DoYouWantToDeleteSomething": "Είστε σίγουροι ότι θέλετε να διαγράψετε το \"{{ name }}\";",
diff --git a/resources/i18n/en.json b/resources/i18n/en.json
index a4f4057cd3..80265674cf 100644
--- a/resources/i18n/en.json
+++ b/resources/i18n/en.json
@@ -867,6 +867,7 @@
"dialog": {
"ErrorOccurred": "Error Occurred",
"PleaseTypeToConfirm": "Please type {{ confirmText }} to confirm.",
+ "TypeNameToConfirmDeletion": "Type the name to confirm deletion.",
"ask": {
"DoYouWantToDelete": "Are you sure you want to delete?",
"DoYouWantToDeleteSomething": "Are you sure you want to delete \"{{ name }}\"?",
diff --git a/resources/i18n/es.json b/resources/i18n/es.json
index 07ea3ff9df..27f799fa64 100644
--- a/resources/i18n/es.json
+++ b/resources/i18n/es.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Se ha producido un error",
"PleaseTypeToConfirm": "Escriba {{confirmText}} para confirmar.",
+ "TypeNameToConfirmDeletion": "Escriba el nombre para confirmar la eliminación.",
"ask": {
"DoYouWantToDelete": "¿Está seguro de que desea eliminar?",
"DoYouWantToDeleteSomething": "¿Estás seguro de que quieres borrar \"{{ name }}\"?",
diff --git a/resources/i18n/fi.json b/resources/i18n/fi.json
index 68fabefa12..6cad02176a 100644
--- a/resources/i18n/fi.json
+++ b/resources/i18n/fi.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Tapahtunut virhe",
"PleaseTypeToConfirm": "Kirjoita {{confirmText}} vahvistaaksesi.",
+ "TypeNameToConfirmDeletion": "Kirjoita nimi vahvistaaksesi poistamisen.",
"ask": {
"DoYouWantToDelete": "Haluatko varmasti poistaa tämän?",
"DoYouWantToDeleteSomething": "Oletko varma, että haluat poistaa \"{{ name }}\"?\"?",
diff --git a/resources/i18n/fr.json b/resources/i18n/fr.json
index cb06ff4b9f..f78b4df781 100644
--- a/resources/i18n/fr.json
+++ b/resources/i18n/fr.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Erreur est survenue",
"PleaseTypeToConfirm": "Veuillez taper {{confirmText}} pour confirmer.",
+ "TypeNameToConfirmDeletion": "Saisissez le nom pour confirmer la suppression.",
"ask": {
"DoYouWantToDelete": "Voulez-vous vraiment supprimer ?",
"DoYouWantToDeleteSomething": "Êtes-vous sûr de vouloir supprimer \"{{ nom }}\" ?",
diff --git a/resources/i18n/id.json b/resources/i18n/id.json
index 722e445a62..a8fe4322f2 100644
--- a/resources/i18n/id.json
+++ b/resources/i18n/id.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Terjadi Galat",
"PleaseTypeToConfirm": "Harap ketik {{confirmText}} untuk mengonfirmasi.",
+ "TypeNameToConfirmDeletion": "Ketik nama untuk mengonfirmasi penghapusan.",
"ask": {
"DoYouWantToDelete": "Apakah Anda yakin ingin menghapus?",
"DoYouWantToDeleteSomething": "Apakah Anda yakin ingin menghapus \"{{ nama }}\"?",
diff --git a/resources/i18n/it.json b/resources/i18n/it.json
index c05b35a74a..df09ed2278 100644
--- a/resources/i18n/it.json
+++ b/resources/i18n/it.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Errore",
"PleaseTypeToConfirm": "Si prega di digitare {{confirmText}} per confermare.",
+ "TypeNameToConfirmDeletion": "Digitare il nome per confermare l'eliminazione.",
"ask": {
"DoYouWantToDelete": "Sei sicuro di voler eliminare?",
"DoYouWantToDeleteSomething": "Si è sicuri di voler eliminare \"{{ name }}\"?",
diff --git a/resources/i18n/ja.json b/resources/i18n/ja.json
index 8851e63a78..0b4516f5e2 100644
--- a/resources/i18n/ja.json
+++ b/resources/i18n/ja.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "エラーが発生しました",
"PleaseTypeToConfirm": "確認するには{{confirmText}}を入力してください。",
+ "TypeNameToConfirmDeletion": "削除を確認するために名前を入力してください。",
"ask": {
"DoYouWantToDelete": "本当に削除しますか?",
"DoYouWantToDeleteSomething": "本当に\"{{ name }}\"を削除しますか?",
diff --git a/resources/i18n/ko.json b/resources/i18n/ko.json
index db6f431acd..9ab92d95bb 100644
--- a/resources/i18n/ko.json
+++ b/resources/i18n/ko.json
@@ -866,7 +866,8 @@
},
"dialog": {
"ErrorOccurred": "문제가 발생했습니다",
- "PleaseTypeToConfirm": "{{confirmText}}를 입력하십시오.",
+ "PleaseTypeToConfirm": "{{confirmText}}를 입력해주세요.",
+ "TypeNameToConfirmDeletion": "삭제 확인을 위해 이름을 입력해주세요.",
"ask": {
"DoYouWantToDelete": "정말로 삭제하시겠습니까?",
"DoYouWantToDeleteSomething": "\"{{ name }}\"을(를) 삭제하시겠습니까?",
diff --git a/resources/i18n/mn.json b/resources/i18n/mn.json
index 3976dd620a..8e81fa1c73 100644
--- a/resources/i18n/mn.json
+++ b/resources/i18n/mn.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Алдаа гарлаа",
"PleaseTypeToConfirm": "Баталгаажуулахын тулд {{confirmText}} оруулна уу.",
+ "TypeNameToConfirmDeletion": "Устгахыг баталгаажуулахын тулд нэрийг бичнэ үү.",
"ask": {
"DoYouWantToDelete": "Та үүнийг устгахдаа итгэлтэй байна уу?",
"DoYouWantToDeleteSomething": "Та \"{{ name }}\"-г устгахдаа итгэлтэй байна уу?",
diff --git a/resources/i18n/ms.json b/resources/i18n/ms.json
index 9b65ef56be..27728b2d97 100644
--- a/resources/i18n/ms.json
+++ b/resources/i18n/ms.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Ralat Berlaku",
"PleaseTypeToConfirm": "Sila taipkan {{confirmText}} untuk mengesahkan.",
+ "TypeNameToConfirmDeletion": "Taip nama untuk mengesahkan pemadaman.",
"ask": {
"DoYouWantToDelete": "Adakah anda pasti mahu memadam?",
"DoYouWantToDeleteSomething": "Adakah anda pasti mahu memadamkan \"{{ name }}\"?",
diff --git a/resources/i18n/pl.json b/resources/i18n/pl.json
index 606455810e..53e9a30aea 100644
--- a/resources/i18n/pl.json
+++ b/resources/i18n/pl.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Wystąpił błąd",
"PleaseTypeToConfirm": "Wpisz {{confirmText}}, aby potwierdzić.",
+ "TypeNameToConfirmDeletion": "Wpisz nazwę, aby potwierdzić usunięcie.",
"ask": {
"DoYouWantToDelete": "Czy na pewno chcesz to usunąć?",
"DoYouWantToDeleteSomething": "Czy na pewno chcesz usunąć \"{{ name }}\"?",
diff --git a/resources/i18n/pt-BR.json b/resources/i18n/pt-BR.json
index c9691f5138..64ad17c247 100644
--- a/resources/i18n/pt-BR.json
+++ b/resources/i18n/pt-BR.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Ocorreu um erro",
"PleaseTypeToConfirm": "Digite {{confirmText}} para confirmar.",
+ "TypeNameToConfirmDeletion": "Digite o nome para confirmar a exclusão.",
"ask": {
"DoYouWantToDelete": "Tem certeza de que deseja excluir?",
"DoYouWantToDeleteSomething": "Tem a certeza de que pretende eliminar \"{{ name }}\"?",
diff --git a/resources/i18n/pt.json b/resources/i18n/pt.json
index 25e75f7ac1..aa75aeb210 100644
--- a/resources/i18n/pt.json
+++ b/resources/i18n/pt.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Ocorreu um erro",
"PleaseTypeToConfirm": "Digite {{confirmText}} para confirmar.",
+ "TypeNameToConfirmDeletion": "Digite o nome para confirmar a exclusão.",
"ask": {
"DoYouWantToDelete": "Tem certeza de que deseja excluir?",
"DoYouWantToDeleteSomething": "Tem a certeza de que pretende eliminar \"{{ name }}\"?",
diff --git a/resources/i18n/ru.json b/resources/i18n/ru.json
index a6f5669e25..2891cafc74 100644
--- a/resources/i18n/ru.json
+++ b/resources/i18n/ru.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Возникла ошибка",
"PleaseTypeToConfirm": "Пожалуйста, введите {{confirmText}}, чтобы подтвердить.",
+ "TypeNameToConfirmDeletion": "Введите имя для подтверждения удаления.",
"ask": {
"DoYouWantToDelete": "Вы уверены, что хотите удалить?",
"DoYouWantToDeleteSomething": "Вы уверены, что хотите удалить \"{{ имя }}\"?",
diff --git a/resources/i18n/th.json b/resources/i18n/th.json
index 7c9e1e2a76..d7e1a55163 100644
--- a/resources/i18n/th.json
+++ b/resources/i18n/th.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "เกิดข้อผิดพลาด",
"PleaseTypeToConfirm": "กรุณาพิมพ์ {{confirmText}} เพื่อยืนยัน",
+ "TypeNameToConfirmDeletion": "พิมพ์ชื่อเพื่อยืนยันการลบ",
"ask": {
"DoYouWantToDelete": "คุณแน่ใจว่าต้องการลบหรือไม่?",
"DoYouWantToDeleteSomething": "คุณแน่ใจหรือไม่ที่จะลบ \"{{ name }}\"?",
diff --git a/resources/i18n/tr.json b/resources/i18n/tr.json
index 01a8bfb7b3..14861c42e3 100644
--- a/resources/i18n/tr.json
+++ b/resources/i18n/tr.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Hata oluştu",
"PleaseTypeToConfirm": "Onaylamak için lütfen {{confirmText}} yazın.",
+ "TypeNameToConfirmDeletion": "Silme işlemini onaylamak için adı yazın.",
"ask": {
"DoYouWantToDelete": "Silmek istediğinizden emin misiniz?",
"DoYouWantToDeleteSomething": "\"{{ name }}\" silmek istediğinizden emin misiniz?",
diff --git a/resources/i18n/vi.json b/resources/i18n/vi.json
index bdd464c432..b02a6365ba 100644
--- a/resources/i18n/vi.json
+++ b/resources/i18n/vi.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "Xảy ra lỗi",
"PleaseTypeToConfirm": "Vui lòng nhập {{confirmText}} để xác nhận.",
+ "TypeNameToConfirmDeletion": "Nhập tên để xác nhận xóa.",
"ask": {
"DoYouWantToDelete": "Bạn có chắc chắn muốn xóa không?",
"DoYouWantToDeleteSomething": "Bạn có chắc chắn muốn xóa \"{{ name }}\" không?",
diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json
index a6f2ca4fe8..f6d0a876f6 100644
--- a/resources/i18n/zh-CN.json
+++ b/resources/i18n/zh-CN.json
@@ -866,6 +866,7 @@
"dialog": {
"ErrorOccurred": "错误发生",
"PleaseTypeToConfirm": "请键入{{confirmText}}以确认。",
+ "TypeNameToConfirmDeletion": "请输入名称以确认删除。",
"ask": {
"DoYouWantToDelete": "您确定要删除吗?",
"DoYouWantToDeleteSomething": "您确定要删除\"{{ 名称 }}}\"吗?",
diff --git a/resources/i18n/zh-TW.json b/resources/i18n/zh-TW.json
index 8f03292816..93e740ace3 100644
--- a/resources/i18n/zh-TW.json
+++ b/resources/i18n/zh-TW.json
@@ -867,6 +867,7 @@
"dialog": {
"ErrorOccurred": "錯誤發生",
"PleaseTypeToConfirm": "請鍵入{{confirmText}}以確認。",
+ "TypeNameToConfirmDeletion": "請輸入名稱以確認刪除。",
"ask": {
"DoYouWantToDelete": "您確定要刪除嗎?",
"DoYouWantToDeleteSomething": "您确定要删除\"{{ 名称 }}}\"吗?",