Procházet zdrojové kódy

Merge pull request #10267 from weseek/imprv/169644-assistant-remove-confirmation-modal

imprv: Assistant delete confirmation modal
Yuki Takei před 7 měsíci
rodič
revize
2cb3c2ed6b

+ 4 - 0
apps/app/public/static/locales/en_US/translation.json

@@ -675,6 +675,10 @@
       "thread_deleted_failed": "Failed to delete thread",
       "ai_assistant_set_default_success": "Default assistant set successfully",
       "ai_assistant_set_default_failed": "Failed to set default assistant"
+    },
+    "delete_modal": {
+      "title": "Delete Assistant",
+      "confirm_message": "Are you sure you want to delete this assistant?"
     }
   },
   "link_edit": {

+ 5 - 1
apps/app/public/static/locales/fr_FR/translation.json

@@ -258,7 +258,7 @@
       "title": "Créer un nouveau jeton d'accès",
       "expiredAt_desc": "Sélectionnez la date d'expiration de ce jeton d'accès.",
       "description_desc": "Fournissez une description pour vous aider à identifier ce jeton ultérieurement.",
-      "description_max_length": "Veuillez saisir jusqu'à {{length}} caractères."
+      "description_max_length": "Veuillez saisir jusqu'à {{length}} caractères.",
       "scope_desc": "Sélectionnez la portée du jeton d'accès."
     },
     "copy_to_clipboard": "Copier dans le presse-papiers"
@@ -669,6 +669,10 @@
       "thread_deleted_failed": "Échec de la suppression de la discussion",
       "ai_assistant_set_default_success": "Assistant par défaut défini avec succès",
       "ai_assistant_set_default_failed": "Échec de la définition de l'assistant par défaut"
+    },
+    "delete_modal": {
+      "title": "Supprimer l'assistant",
+      "confirm_message": "Êtes-vous sûr de vouloir supprimer cet assistant ?"
     }
   },
   "link_edit": {

+ 4 - 0
apps/app/public/static/locales/ja_JP/translation.json

@@ -708,6 +708,10 @@
       "thread_deleted_failed": "スレッドの削除に失敗しました",
       "ai_assistant_set_default_success": "デフォルトアシスタントを設定しました",
       "ai_assistant_set_default_failed": "デフォルトアシスタントの設定に失敗しました"
+      },
+    "delete_modal": {
+      "title": "アシスタントを削除する",
+      "confirm_message": "本当にアシスタントを削除しますか?"
     }
   },
   "link_edit": {

+ 4 - 0
apps/app/public/static/locales/ko_KR/translation.json

@@ -635,6 +635,10 @@
       "thread_deleted_failed": "스레드 삭제 실패",
       "ai_assistant_set_default_success": "기본 어시스턴트 설정 성공",
       "ai_assistant_set_default_failed": "기본 어시스턴트 설정 실패"
+    },
+    "delete_modal": {
+      "title": "어시스턴트 삭제",
+      "confirm_message": "정말로 이 어시스턴트를 삭제하시겠습니까?"
     }
   },
   "link_edit": {

+ 4 - 0
apps/app/public/static/locales/zh_CN/translation.json

@@ -666,6 +666,10 @@
       "thread_deleted_failed": "删除会话失败",
       "ai_assistant_set_default_success": "已成功设置默认助手",
       "ai_assistant_set_default_failed": "设置默认助手失败"
+    },
+    "delete_modal": {
+      "title": "删除助手",
+      "confirm_message": "确定要删除此助手吗?"
     }
   },
   "link_edit": {

+ 51 - 17
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantList.tsx

@@ -15,6 +15,8 @@ import { deleteAiAssistant, setDefaultAiAssistant } from '../../../services/ai-a
 import { useAiAssistantSidebar, useAiAssistantManagementModal } from '../../../stores/ai-assistant';
 import { getShareScopeIcon } from '../../../utils/get-share-scope-Icon';
 
+import { DeleteAiAssistantModal } from './DeleteAiAssistantModal';
+
 const logger = loggerFactory('growi:openai:client:components:AiAssistantList');
 
 /*
@@ -25,8 +27,8 @@ type AiAssistantItemProps = {
   aiAssistant: AiAssistantHasId;
   onEditClick: (aiAssistantData: AiAssistantHasId) => void;
   onItemClick: (aiAssistantData: AiAssistantHasId) => void;
+  onDeleteClick: (aiAssistant: AiAssistantHasId) => void;
   onUpdated?: () => void;
-  onDeleted?: (aiAssistantId: string) => void;
 };
 
 const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
@@ -34,8 +36,8 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
   aiAssistant,
   onEditClick,
   onItemClick,
+  onDeleteClick,
   onUpdated,
-  onDeleted,
 }) => {
 
   const { t } = useTranslation();
@@ -61,18 +63,6 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
     }
   }, [aiAssistant._id, aiAssistant.isDefault, onUpdated, t]);
 
-  const deleteAiAssistantHandler = useCallback(async() => {
-    try {
-      await deleteAiAssistant(aiAssistant._id);
-      onDeleted?.(aiAssistant._id);
-      toastSuccess(t('ai_assistant_substance.toaster.ai_assistant_deleted_success'));
-    }
-    catch (err) {
-      logger.error(err);
-      toastError(t('ai_assistant_substance.toaster.ai_assistant_deleted_failed'));
-    }
-  }, [aiAssistant._id, onDeleted, t]);
-
   const isOperable = currentUser?._id != null && getIdStringForRef(aiAssistant.owner) === currentUser._id;
   const isPublicAiAssistantOperable = currentUser?.admin
     && determineShareScope(aiAssistant.shareScope, aiAssistant.accessScope) === AiAssistantShareScope.PUBLIC_ONLY;
@@ -95,7 +85,7 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
           <p className="text-truncate m-auto">{aiAssistant.name}</p>
         </div>
 
-        <div className="grw-btn-actions opacity-0 d-flex justify-content-center ">
+        <div className="grw-btn-actions opacity-0 d-flex justify-content-center">
           {isPublicAiAssistantOperable && (
             <button
               type="button"
@@ -125,7 +115,7 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
                 className="btn btn-link text-secondary p-0"
                 onClick={(e) => {
                   e.stopPropagation();
-                  deleteAiAssistantHandler();
+                  onDeleteClick(aiAssistant);
                 }}
               >
                 <span className="material-symbols-outlined fs-5">delete</span>
@@ -160,6 +150,10 @@ export const AiAssistantList: React.FC<AiAssistantListProps> = ({
 
   const [isCollapsed, setIsCollapsed] = useState(false);
 
+  const [aiAssistantToBeDeleted, setAiAssistantToBeDeleted] = useState<AiAssistantHasId | null>(null);
+  const [isDeleteModalShown, setIsDeleteModalShown] = useState(false);
+  const [errorMessageOnDelete, setErrorMessageOnDelete] = useState('');
+
   const toggleCollapse = useCallback(() => {
     setIsCollapsed((prev) => {
       if (!prev) {
@@ -169,6 +163,38 @@ export const AiAssistantList: React.FC<AiAssistantListProps> = ({
     });
   }, [onCollapsed]);
 
+  const onClickDeleteButton = useCallback((aiAssistant: AiAssistantHasId) => {
+    setAiAssistantToBeDeleted(aiAssistant);
+    setIsDeleteModalShown(true);
+  }, []);
+
+  const onCancelDeleteAiAssistant = useCallback(() => {
+    setAiAssistantToBeDeleted(null);
+    setIsDeleteModalShown(false);
+    setErrorMessageOnDelete('');
+  }, []);
+
+  const onDeleteAiAssistantAfterOperation = useCallback((aiAssistantId: string) => {
+    onCancelDeleteAiAssistant();
+    onDeleted?.(aiAssistantId);
+  }, [onCancelDeleteAiAssistant, onDeleted]);
+
+  const onDeleteAiAssistant = useCallback(async() => {
+    if (aiAssistantToBeDeleted == null) return;
+
+    try {
+      await deleteAiAssistant(aiAssistantToBeDeleted._id);
+      onDeleteAiAssistantAfterOperation(aiAssistantToBeDeleted._id);
+      toastSuccess(t('ai_assistant_substance.toaster.ai_assistant_deleted_success'));
+    }
+    catch (err) {
+      const message = err instanceof Error ? err.message : String(err);
+      setErrorMessageOnDelete(message);
+      logger.error(err);
+      toastError(t('ai_assistant_substance.toaster.ai_assistant_deleted_failed'));
+    }
+  }, [aiAssistantToBeDeleted, onDeleteAiAssistantAfterOperation, t]);
+
   return (
     <>
       <button
@@ -196,12 +222,20 @@ export const AiAssistantList: React.FC<AiAssistantListProps> = ({
               aiAssistant={assistant}
               onEditClick={openAiAssistantManagementModal}
               onItemClick={openChat}
+              onDeleteClick={onClickDeleteButton}
               onUpdated={onUpdated}
-              onDeleted={onDeleted}
             />
           ))}
         </ul>
       </Collapse>
+
+      <DeleteAiAssistantModal
+        isShown={isDeleteModalShown}
+        aiAssistant={aiAssistantToBeDeleted}
+        errorMessage={errorMessageOnDelete}
+        onCancel={onCancelDeleteAiAssistant}
+        onConfirm={onDeleteAiAssistant}
+      />
     </>
   );
 };

+ 72 - 0
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/DeleteAiAssistantModal.tsx

@@ -0,0 +1,72 @@
+import React from 'react';
+
+import { useTranslation } from 'next-i18next';
+import {
+  Button, Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import type { AiAssistantHasId } from '../../../../interfaces/ai-assistant';
+
+export type DeleteAiAssistantModalProps = {
+  isShown: boolean;
+  aiAssistant: AiAssistantHasId | null;
+  errorMessage?: string;
+  onCancel: () => void;
+  onConfirm: () => void;
+};
+
+export const DeleteAiAssistantModal: React.FC<DeleteAiAssistantModalProps> = ({
+  isShown, aiAssistant, errorMessage, onCancel, onConfirm,
+}) => {
+  const { t } = useTranslation();
+
+  const headerContent = () => {
+    if (!isShown || aiAssistant == null) {
+      return null;
+    }
+    return (
+      <>
+        <span className="material-symbols-outlined me-1">delete_forever</span>
+        <span className="fw-bold">{t('ai_assistant_substance.delete_modal.title')}</span>
+      </>
+    );
+  };
+
+  const bodyContent = () => {
+    if (!isShown || aiAssistant == null) {
+      return null;
+    }
+    return <p className="fw-bold mb-0">{t('ai_assistant_substance.delete_modal.confirm_message')}</p>;
+  };
+
+  const footerContent = () => {
+    if (!isShown || aiAssistant == null) {
+      return null;
+    }
+    return (
+      <>
+        {errorMessage && <span className="text-danger">{errorMessage}</span>}
+        <Button color="outline-neutral-secondary" onClick={onCancel}>
+          {t('Cancel')}
+        </Button>
+        <Button color="danger" onClick={onConfirm}>
+          {t('Delete')}
+        </Button>
+      </>
+    );
+  };
+
+  return (
+    <Modal isOpen={isShown} toggle={onCancel} centered>
+      <ModalHeader tag="h5" toggle={onCancel} className="text-danger px-4">
+        {headerContent()}
+      </ModalHeader>
+      <ModalBody className="px-4">
+        {bodyContent()}
+      </ModalBody>
+      <ModalFooter className="px-4 gap-2">
+        {footerContent()}
+      </ModalFooter>
+    </Modal>
+  );
+};