mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-09-11 14:36:35 +03:00
feat: allow user to unsubscribe from a brain (#1254)
* feat: update translations * feat: add <DeleteOrUnsubscribeConfirmationModal /> * test(DeleteOrUnsubscribeConfirmationModal): update tests * feat: redirect to /brains-management on invalid brain id * refactor: move delete_brain_user to delete_brain_users * feat: add /POST '/brains/{brain_id}/subscribe' * feat: handle public brain unsubscription
This commit is contained in:
parent
3043f3acd0
commit
1643b54b7b
@ -2,12 +2,13 @@ from typing import Any, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from logger import get_logger
|
||||
from models.databases.supabase.supabase import SupabaseDB
|
||||
from models.settings import get_supabase_client, get_supabase_db
|
||||
from pydantic import BaseModel
|
||||
from supabase.client import Client
|
||||
from utils.vectors import get_unique_files_from_vector_ids
|
||||
|
||||
from models.databases.supabase.supabase import SupabaseDB
|
||||
from models.settings import get_supabase_client, get_supabase_db
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@ -75,11 +76,11 @@ class Brain(BaseModel):
|
||||
def delete_brain(self, user_id):
|
||||
results = self.supabase_db.delete_brain_user_by_id(user_id, self.id) # type: ignore
|
||||
|
||||
if len(results.data) == 0:
|
||||
if len(results) == 0:
|
||||
return {"message": "You are not the owner of this brain."}
|
||||
else:
|
||||
self.supabase_db.delete_brain_vector(self.id) # type: ignore
|
||||
self.supabase_db.delete_brain_user(self.id) # type: ignore
|
||||
self.supabase_db.delete_brain_users(self.id) # type: ignore
|
||||
self.supabase_db.delete_brain(self.id) # type: ignore
|
||||
|
||||
def create_brain_vector(self, vector_id, file_sha1):
|
||||
|
@ -23,7 +23,7 @@ class Repository(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_brain_user(self, brain_id: str):
|
||||
def delete_brain_users(self, brain_id: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
@ -127,14 +127,18 @@ class Brain(Repository):
|
||||
)
|
||||
return response.data
|
||||
|
||||
def delete_brain_user_by_id(self, user_id, brain_id):
|
||||
def delete_brain_user_by_id(
|
||||
self,
|
||||
user_id: UUID,
|
||||
brain_id: UUID,
|
||||
):
|
||||
results = (
|
||||
self.db.table("brains_users")
|
||||
.select("*")
|
||||
.match({"brain_id": brain_id, "user_id": user_id, "rights": "Owner"})
|
||||
.delete()
|
||||
.match({"brain_id": str(brain_id), "user_id": str(user_id)})
|
||||
.execute()
|
||||
)
|
||||
return results
|
||||
return results.data
|
||||
|
||||
def delete_brain_vector(self, brain_id: str):
|
||||
results = (
|
||||
@ -146,7 +150,7 @@ class Brain(Repository):
|
||||
|
||||
return results
|
||||
|
||||
def delete_brain_user(self, brain_id: str):
|
||||
def delete_brain_users(self, brain_id: str):
|
||||
results = (
|
||||
self.db.table("brains_users")
|
||||
.delete()
|
||||
|
11
backend/repository/brain/delete_brain_user.py
Normal file
11
backend/repository/brain/delete_brain_user.py
Normal file
@ -0,0 +1,11 @@
|
||||
from uuid import UUID
|
||||
|
||||
from models.settings import get_supabase_db
|
||||
|
||||
|
||||
def delete_brain_user(user_id: UUID, brain_id: UUID) -> None:
|
||||
supabase_db = get_supabase_db()
|
||||
supabase_db.delete_brain_user_by_id(
|
||||
user_id=user_id,
|
||||
brain_id=brain_id,
|
||||
)
|
@ -12,6 +12,7 @@ from repository.brain import (
|
||||
get_brain_for_user,
|
||||
update_brain_user_rights,
|
||||
)
|
||||
from repository.brain.delete_brain_user import delete_brain_user
|
||||
from repository.brain_subscription import (
|
||||
SubscriptionInvitationService,
|
||||
resend_invitation_email,
|
||||
@ -403,3 +404,37 @@ async def subscribe_to_brain_handler(
|
||||
raise HTTPException(status_code=400, detail=f"Error adding user to brain: {e}")
|
||||
|
||||
return {"message": "You have successfully subscribed to the brain"}
|
||||
|
||||
|
||||
@subscription_router.post(
|
||||
"/brains/{brain_id}/unsubscribe",
|
||||
tags=["Subscription"],
|
||||
)
|
||||
async def unsubscribe_from_brain_handler(
|
||||
brain_id: UUID, current_user: UserIdentity = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Unsubscribe from a brain
|
||||
"""
|
||||
if not current_user.email:
|
||||
raise HTTPException(status_code=400, detail="UserIdentity email is not defined")
|
||||
|
||||
brain = get_brain_by_id(brain_id)
|
||||
|
||||
if brain is None:
|
||||
raise HTTPException(status_code=404, detail="Brain not found")
|
||||
if brain.status != "public":
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You cannot subscribe to this brain without invitation",
|
||||
)
|
||||
# check if user is already subscribed to brain
|
||||
user_brain = get_brain_for_user(current_user.id, brain_id)
|
||||
if user_brain is None:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You are not subscribed to this brain",
|
||||
)
|
||||
delete_brain_user(user_id=current_user.id, brain_id=brain_id)
|
||||
|
||||
return {"message": "You have successfully unsubscribed from the brain"}
|
||||
|
@ -3,35 +3,34 @@ import { Content, List, Root } from "@radix-ui/react-tabs";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
|
||||
import { BrainTabTrigger, KnowledgeTab, PeopleTab } from "./components";
|
||||
import ConfirmationDeleteModal from "./components/Modals/ConfirmationDeleteModal";
|
||||
import { DeleteOrUnsubscribeConfirmationModal } from "./components/Modals/DeleteOrUnsubscribeConfirmationModal";
|
||||
import { SettingsTab } from "./components/SettingsTab/SettingsTab";
|
||||
import { useBrainManagementTabs } from "./hooks/useBrainManagementTabs";
|
||||
import { getBrainPermissions } from "./utils/getBrainPermissions";
|
||||
|
||||
export const BrainManagementTabs = (): JSX.Element => {
|
||||
const { t } = useTranslation(["translation", "config", "delete_brain"]);
|
||||
const { t } = useTranslation([
|
||||
"translation",
|
||||
"config",
|
||||
"delete_or_unsubscribe_from_brain",
|
||||
]);
|
||||
const {
|
||||
selectedTab,
|
||||
setSelectedTab,
|
||||
brainId,
|
||||
handleDeleteBrain,
|
||||
isDeleteModalOpen,
|
||||
setIsDeleteModalOpen,
|
||||
handleUnsubscribeOrDeleteBrain,
|
||||
isDeleteOrUnsubscribeModalOpened,
|
||||
setIsDeleteOrUnsubscribeModalOpened,
|
||||
hasEditRights,
|
||||
isOwnedByCurrentUser,
|
||||
isDeleteOrUnsubscribeRequestPending,
|
||||
} = useBrainManagementTabs();
|
||||
const { allBrains } = useBrainContext();
|
||||
|
||||
if (brainId === undefined) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const { hasEditRights, isOwnedByCurrentUser } = getBrainPermissions({
|
||||
brainId,
|
||||
userAccessibleBrains: allBrains,
|
||||
});
|
||||
|
||||
return (
|
||||
<Root
|
||||
className="flex flex-col w-full h-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl overflow-hidden bg-white dark:bg-black border border-black/10 dark:border-white/25 p-4 md:p-10"
|
||||
@ -78,19 +77,33 @@ export const BrainManagementTabs = (): JSX.Element => {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-4">
|
||||
<Button
|
||||
disabled={!isOwnedByCurrentUser}
|
||||
className="px-8 md:px-20 py-2 bg-red-500 text-white rounded-md"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
>
|
||||
{t("deleteButton", { ns: "delete_brain" })}
|
||||
</Button>
|
||||
{isOwnedByCurrentUser ? (
|
||||
<Button
|
||||
className="px-8 md:px-20 py-2 bg-red-500 text-white rounded-md"
|
||||
onClick={() => setIsDeleteOrUnsubscribeModalOpened(true)}
|
||||
>
|
||||
{t("deleteButton", { ns: "delete_or_unsubscribe_from_brain" })}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="px-8 md:px-20 py-2 bg-red-500 text-white rounded-md"
|
||||
onClick={() => setIsDeleteOrUnsubscribeModalOpened(true)}
|
||||
>
|
||||
{t("unsubscribeButton", {
|
||||
ns: "delete_or_unsubscribe_from_brain",
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConfirmationDeleteModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
setOpen={setIsDeleteModalOpen}
|
||||
onDelete={handleDeleteBrain}
|
||||
<DeleteOrUnsubscribeConfirmationModal
|
||||
isOpen={isDeleteOrUnsubscribeModalOpened}
|
||||
setOpen={setIsDeleteOrUnsubscribeModalOpened}
|
||||
onConfirm={() => void handleUnsubscribeOrDeleteBrain()}
|
||||
isOwnedByCurrentUser={isOwnedByCurrentUser}
|
||||
isDeleteOrUnsubscribeRequestPending={
|
||||
isDeleteOrUnsubscribeRequestPending
|
||||
}
|
||||
/>
|
||||
</Root>
|
||||
);
|
||||
|
@ -1,46 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { Modal } from "@/lib/components/ui/Modal";
|
||||
|
||||
type ConfirmationDeleteModalProps = {
|
||||
isOpen: boolean;
|
||||
setOpen: (isOpen: boolean) => void;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
const ConfirmationDeleteModal = ({
|
||||
isOpen,
|
||||
setOpen,
|
||||
onDelete,
|
||||
}: ConfirmationDeleteModalProps): JSX.Element => {
|
||||
const { t } = useTranslation(["delete_brain"]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
desc={t("deleteConfirmQuestion")}
|
||||
isOpen={isOpen}
|
||||
setOpen={setOpen}
|
||||
Trigger={<div />}
|
||||
CloseTrigger={
|
||||
<Button className="self-end" data-testid="return-button">
|
||||
{t("returnButton")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div className="flex justify-center mt-6">
|
||||
<Button
|
||||
data-testid="delete-brain"
|
||||
className="px-4 py-2 bg-red-500 text-white rounded-md"
|
||||
onClick={onDelete}
|
||||
>
|
||||
{t("deleteConfirmYes")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationDeleteModal;
|
@ -0,0 +1,55 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { Modal } from "@/lib/components/ui/Modal";
|
||||
|
||||
type DeleteOrUnsubscribeConfirmationModalProps = {
|
||||
isOpen: boolean;
|
||||
setOpen: (isOpen: boolean) => void;
|
||||
onConfirm: () => void;
|
||||
isOwnedByCurrentUser: boolean;
|
||||
isDeleteOrUnsubscribeRequestPending: boolean;
|
||||
};
|
||||
|
||||
export const DeleteOrUnsubscribeConfirmationModal = ({
|
||||
isOpen,
|
||||
setOpen,
|
||||
onConfirm,
|
||||
isOwnedByCurrentUser,
|
||||
isDeleteOrUnsubscribeRequestPending,
|
||||
}: DeleteOrUnsubscribeConfirmationModalProps): JSX.Element => {
|
||||
const { t } = useTranslation(["delete_or_unsubscribe_from_brain"]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
desc={
|
||||
isOwnedByCurrentUser
|
||||
? t("deleteConfirmQuestion")
|
||||
: t("unsubscribeConfirmQuestion")
|
||||
}
|
||||
isOpen={isOpen}
|
||||
setOpen={setOpen}
|
||||
Trigger={<div />}
|
||||
CloseTrigger={
|
||||
<Button className="self-end" data-testid="return-button">
|
||||
{t("returnButton")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div className="flex justify-center mt-6">
|
||||
<Button
|
||||
data-testid="delete-brain"
|
||||
className="px-4 py-2 bg-red-500 text-white rounded-md"
|
||||
onClick={onConfirm}
|
||||
isLoading={isDeleteOrUnsubscribeRequestPending}
|
||||
>
|
||||
{isOwnedByCurrentUser
|
||||
? t("deleteConfirmYes")
|
||||
: t("unsubscribeButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -1,9 +1,9 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import ConfirmationDeleteModal from "../ConfirmationDeleteModal";
|
||||
import { DeleteOrUnsubscribeConfirmationModal } from "../DeleteOrUnsubscribeConfirmationModal";
|
||||
|
||||
describe("ConfirmationDeleteModal", () => {
|
||||
describe("DeleteOrUnsubscribeConfirmationModal", () => {
|
||||
const isOpen = true;
|
||||
const setOpen = vi.fn();
|
||||
const onDelete = vi.fn();
|
||||
@ -14,10 +14,10 @@ describe("ConfirmationDeleteModal", () => {
|
||||
|
||||
it("should render delete modal", () => {
|
||||
const { getByTestId } = render(
|
||||
<ConfirmationDeleteModal
|
||||
<DeleteOrUnsubscribeConfirmationModal
|
||||
isOpen={isOpen}
|
||||
setOpen={setOpen}
|
||||
onDelete={onDelete}
|
||||
onConfirm={onDelete}
|
||||
/>
|
||||
);
|
||||
expect(getByTestId("modal-description")).toBeDefined();
|
||||
@ -27,10 +27,10 @@ describe("ConfirmationDeleteModal", () => {
|
||||
|
||||
it("should call onDelete when delete-brain is clicked", () => {
|
||||
const { getByTestId } = render(
|
||||
<ConfirmationDeleteModal
|
||||
<DeleteOrUnsubscribeConfirmationModal
|
||||
isOpen={isOpen}
|
||||
setOpen={setOpen}
|
||||
onDelete={onDelete}
|
||||
onConfirm={onDelete}
|
||||
/>
|
||||
);
|
||||
|
@ -7,7 +7,6 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getBrainDataKey } from "@/lib/api/brain/config";
|
||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||
import { usePromptApi } from "@/lib/api/prompt/usePromptApi";
|
||||
import { USER_DATA_KEY } from "@/lib/api/user/config";
|
||||
@ -19,6 +18,7 @@ import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
|
||||
import { getAccessibleModels } from "@/lib/helpers/getAccessibleModels";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
|
||||
import { useBrainFetcher } from "../../../hooks/useBrainFetcher";
|
||||
import { validateOpenAIKey } from "../utils/validateOpenAIKey";
|
||||
|
||||
type UseSettingsTabProps = {
|
||||
@ -32,7 +32,7 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
|
||||
const [isSettingAsDefault, setIsSettingAsDefault] = useState(false);
|
||||
const { publish } = useToast();
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const { setAsDefaultBrain, getBrain, updateBrain } = useBrainApi();
|
||||
const { setAsDefaultBrain, updateBrain } = useBrainApi();
|
||||
const { fetchAllBrains, fetchDefaultBrain, defaultBrainId } =
|
||||
useBrainContext();
|
||||
const { getPrompt, updatePrompt, createPrompt } = usePromptApi();
|
||||
@ -65,10 +65,8 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
|
||||
} = useForm({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const { data: brain } = useQuery({
|
||||
queryKey: [getBrainDataKey(brainId)],
|
||||
queryFn: () => getBrain(brainId),
|
||||
const { brain } = useBrainFetcher({
|
||||
brainId,
|
||||
});
|
||||
|
||||
const isDefaultBrain = defaultBrainId === brainId;
|
||||
|
@ -0,0 +1,38 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { UUID } from "crypto";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { getBrainDataKey } from "@/lib/api/brain/config";
|
||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||
|
||||
type UseBrainFetcherProps = {
|
||||
brainId?: UUID;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useBrainFetcher = ({ brainId }: UseBrainFetcherProps) => {
|
||||
const { getBrain } = useBrainApi();
|
||||
const router = useRouter();
|
||||
|
||||
const fetchBrain = async () => {
|
||||
try {
|
||||
if (brainId === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await getBrain(brainId);
|
||||
} catch (error) {
|
||||
router.push("/brains-management");
|
||||
}
|
||||
};
|
||||
|
||||
const { data: brain } = useQuery({
|
||||
queryKey: [getBrainDataKey(brainId!)],
|
||||
queryFn: fetchBrain,
|
||||
enabled: brainId !== undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
brain,
|
||||
};
|
||||
};
|
@ -1,19 +1,26 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { UUID } from "crypto";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getBrainDataKey } from "@/lib/api/brain/config";
|
||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||
import { useSubscriptionApi } from "@/lib/api/subscription/useSubscriptionApi";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
|
||||
|
||||
import { BrainManagementTab } from "../types";
|
||||
import { getBrainPermissions } from "../utils/getBrainPermissions";
|
||||
import { getTargetedTab } from "../utils/getTargetedTab";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useBrainManagementTabs = () => {
|
||||
const [selectedTab, setSelectedTab] =
|
||||
useState<BrainManagementTab>("settings");
|
||||
const { allBrains } = useBrainContext();
|
||||
const [
|
||||
isDeleteOrUnsubscribeRequestPending,
|
||||
setIsDeleteOrUnsubscribeRequestPending,
|
||||
] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const targetedTab = getTargetedTab();
|
||||
@ -21,39 +28,70 @@ export const useBrainManagementTabs = () => {
|
||||
setSelectedTab(targetedTab);
|
||||
}
|
||||
}, []);
|
||||
const { getBrain } = useBrainApi();
|
||||
|
||||
const { deleteBrain, setCurrentBrainId } = useBrainContext();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const { track } = useEventTracking();
|
||||
const { publish } = useToast();
|
||||
|
||||
const { unsubscribeFromBrain } = useSubscriptionApi();
|
||||
const { deleteBrain, setCurrentBrainId, fetchAllBrains } = useBrainContext();
|
||||
const [
|
||||
isDeleteOrUnsubscribeModalOpened,
|
||||
setIsDeleteOrUnsubscribeModalOpened,
|
||||
] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const params = useParams();
|
||||
|
||||
const { t } = useTranslation(["delete_or_unsubscribe_from_brain"]);
|
||||
const brainId = params?.brainId as UUID | undefined;
|
||||
|
||||
const { data: brain } = useQuery({
|
||||
queryKey: [getBrainDataKey(brainId!)],
|
||||
queryFn: () => getBrain(brainId!),
|
||||
enabled: brainId !== undefined,
|
||||
const { hasEditRights, isOwnedByCurrentUser } = getBrainPermissions({
|
||||
brainId,
|
||||
userAccessibleBrains: allBrains,
|
||||
});
|
||||
|
||||
const handleDeleteBrain = () => {
|
||||
const handleUnSubscription = async () => {
|
||||
if (brainId === undefined) {
|
||||
return;
|
||||
}
|
||||
void deleteBrain(brainId);
|
||||
setCurrentBrainId(null);
|
||||
router.push("/brains-management");
|
||||
setIsDeleteModalOpen(false);
|
||||
await unsubscribeFromBrain(brainId);
|
||||
|
||||
void track("UNSUBSCRIBE_FROM_BRAIN");
|
||||
publish({
|
||||
variant: "success",
|
||||
text: t("successfully_unsubscribed"),
|
||||
});
|
||||
};
|
||||
|
||||
const handleUnsubscribeOrDeleteBrain = async () => {
|
||||
if (brainId === undefined) {
|
||||
return;
|
||||
}
|
||||
setIsDeleteOrUnsubscribeRequestPending(true);
|
||||
try {
|
||||
if (!isOwnedByCurrentUser) {
|
||||
await handleUnSubscription();
|
||||
} else {
|
||||
await deleteBrain(brainId);
|
||||
}
|
||||
setCurrentBrainId(null);
|
||||
setIsDeleteOrUnsubscribeModalOpened(false);
|
||||
void fetchAllBrains();
|
||||
router.push("/brains-management");
|
||||
} catch (error) {
|
||||
console.error("Error deleting brain: ", error);
|
||||
} finally {
|
||||
setIsDeleteOrUnsubscribeRequestPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
selectedTab,
|
||||
setSelectedTab,
|
||||
brainId,
|
||||
handleDeleteBrain,
|
||||
isDeleteModalOpen,
|
||||
setIsDeleteModalOpen,
|
||||
brain,
|
||||
handleUnsubscribeOrDeleteBrain,
|
||||
isDeleteOrUnsubscribeModalOpened,
|
||||
setIsDeleteOrUnsubscribeModalOpened,
|
||||
hasEditRights,
|
||||
isOwnedByCurrentUser,
|
||||
isDeleteOrUnsubscribeRequestPending,
|
||||
};
|
||||
};
|
||||
|
@ -16,7 +16,7 @@ const BrainsManagement = (): JSX.Element => {
|
||||
return (
|
||||
<div className="flex justify-center mt-5 w-full">
|
||||
<div className="bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded relative max-w-md h-fit">
|
||||
<p>{ t("selectBrain") }</p>
|
||||
<p>{t("selectBrain")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -67,3 +67,13 @@ export const subscribeToBrain = async (
|
||||
|
||||
return subscribedToBrain;
|
||||
};
|
||||
|
||||
export const unsubscribeFromBrain = async (
|
||||
brainId: UUID,
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<{ message: string }> =>
|
||||
(
|
||||
await axiosInstance.post<{ message: string }>(
|
||||
`/brains/${brainId}/unsubscribe`
|
||||
)
|
||||
).data;
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
declineInvitation,
|
||||
getInvitation,
|
||||
subscribeToBrain,
|
||||
unsubscribeFromBrain,
|
||||
} from "./subscription";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
@ -22,5 +23,7 @@ export const useSubscriptionApi = () => {
|
||||
getInvitation(brainId, axiosInstance),
|
||||
subscribeToBrain: async (brainId: UUID) =>
|
||||
subscribeToBrain(brainId, axiosInstance),
|
||||
unsubscribeFromBrain: async (brainId: UUID) =>
|
||||
unsubscribeFromBrain(brainId, axiosInstance),
|
||||
};
|
||||
};
|
||||
|
@ -3,7 +3,7 @@
|
||||
import brain_en from "../../../public/locales/en/brain.json";
|
||||
import chat_en from "../../../public/locales/en/chat.json";
|
||||
import config_en from "../../../public/locales/en/config.json";
|
||||
import delete_brain_en from "../../../public/locales/en/deleteBrain.json";
|
||||
import delete_brain_en from "../../../public/locales/en/deleteOrUnsubscribeFromBrain.json";
|
||||
import explore_en from "../../../public/locales/en/explore.json";
|
||||
import invitation_en from "../../../public/locales/en/invitation.json";
|
||||
import login_en from "../../../public/locales/en/login.json";
|
||||
@ -17,7 +17,7 @@ import user_en from "../../../public/locales/en/user.json";
|
||||
import brain_es from "../../../public/locales/es/brain.json";
|
||||
import chat_es from "../../../public/locales/es/chat.json";
|
||||
import config_es from "../../../public/locales/es/config.json";
|
||||
import delete_brain_es from "../../../public/locales/es/deleteBrain.json";
|
||||
import delete_brain_es from "../../../public/locales/es/deleteOrUnsubscribeFromBrain.json";
|
||||
import explore_es from "../../../public/locales/es/explore.json";
|
||||
import invitation_es from "../../../public/locales/es/invitation.json";
|
||||
import login_es from "../../../public/locales/es/login.json";
|
||||
@ -31,7 +31,7 @@ import user_es from "../../../public/locales/es/user.json";
|
||||
import brain_fr from "../../../public/locales/fr/brain.json";
|
||||
import chat_fr from "../../../public/locales/fr/chat.json";
|
||||
import config_fr from "../../../public/locales/fr/config.json";
|
||||
import delete_brain_fr from "../../../public/locales/fr/deleteBrain.json";
|
||||
import delete_brain_fr from "../../../public/locales/fr/deleteOrUnsubscribeFromBrain.json";
|
||||
import explore_fr from "../../../public/locales/fr/explore.json";
|
||||
import invitation_fr from "../../../public/locales/fr/invitation.json";
|
||||
import login_fr from "../../../public/locales/fr/login.json";
|
||||
@ -45,7 +45,7 @@ import user_fr from "../../../public/locales/fr/user.json";
|
||||
import brain_ptbr from "../../../public/locales/pt-br/brain.json";
|
||||
import chat_ptbr from "../../../public/locales/pt-br/chat.json";
|
||||
import config_ptbr from "../../../public/locales/pt-br/config.json";
|
||||
import delete_brain_ptbr from "../../../public/locales/pt-br/deleteBrain.json";
|
||||
import delete_brain_ptbr from "../../../public/locales/pt-br/deleteOrUnsubscribeFromBrain.json";
|
||||
import explore_ptbr from "../../../public/locales/pt-br/explore.json";
|
||||
import invitation_ptbr from "../../../public/locales/pt-br/invitation.json";
|
||||
import login_ptbr from "../../../public/locales/pt-br/login.json";
|
||||
@ -59,7 +59,7 @@ import user_ptbr from "../../../public/locales/pt-br/user.json";
|
||||
import brain_ru from "../../../public/locales/ru/brain.json";
|
||||
import chat_ru from "../../../public/locales/ru/chat.json";
|
||||
import config_ru from "../../../public/locales/ru/config.json";
|
||||
import delete_brain_ru from "../../../public/locales/ru/deleteBrain.json";
|
||||
import delete_brain_ru from "../../../public/locales/ru/deleteOrUnsubscribeFromBrain.json";
|
||||
import explore_ru from "../../../public/locales/ru/explore.json";
|
||||
import invitation_ru from "../../../public/locales/ru/invitation.json";
|
||||
import login_ru from "../../../public/locales/ru/login.json";
|
||||
@ -73,7 +73,7 @@ import user_ru from "../../../public/locales/ru/user.json";
|
||||
import brain_zh_cn from "../../../public/locales/zh-cn/brain.json";
|
||||
import chat_zh_cn from "../../../public/locales/zh-cn/chat.json";
|
||||
import config_zh_cn from "../../../public/locales/zh-cn/config.json";
|
||||
import delete_brain_zh_cn from "../../../public/locales/zh-cn/deleteBrain.json";
|
||||
import delete_brain_zh_cn from "../../../public/locales/zh-cn/deleteOrUnsubscribeFromBrain.json";
|
||||
import explore_zh_cn from "../../../public/locales/zh-cn/explore.json";
|
||||
import invitation_zh_cn from "../../../public/locales/zh-cn/invitation.json";
|
||||
import login_zh_cn from "../../../public/locales/zh-cn/login.json";
|
||||
@ -89,7 +89,7 @@ export type Translations = {
|
||||
brain: typeof import("../../../public/locales/en/brain.json");
|
||||
chat: typeof import("../../../public/locales/en/chat.json");
|
||||
config: typeof import("../../../public/locales/en/config.json");
|
||||
delete_brain: typeof import("../../../public/locales/en/deleteBrain.json");
|
||||
delete_or_unsubscribe_from_brain: typeof import("../../../public/locales/en/deleteOrUnsubscribeFromBrain.json");
|
||||
explore: typeof import("../../../public/locales/en/explore.json");
|
||||
invitation: typeof import("../../../public/locales/en/invitation.json");
|
||||
login: typeof import("../../../public/locales/en/login.json");
|
||||
@ -125,7 +125,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
|
||||
updatePassword: updatePassword_en,
|
||||
upload: upload_en,
|
||||
user: user_en,
|
||||
delete_brain: delete_brain_en,
|
||||
delete_or_unsubscribe_from_brain: delete_brain_en,
|
||||
},
|
||||
es: {
|
||||
brain: brain_es,
|
||||
@ -140,7 +140,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
|
||||
updatePassword: updatePassword_es,
|
||||
upload: upload_es,
|
||||
user: user_es,
|
||||
delete_brain: delete_brain_es,
|
||||
delete_or_unsubscribe_from_brain: delete_brain_es,
|
||||
},
|
||||
fr: {
|
||||
brain: brain_fr,
|
||||
@ -155,7 +155,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
|
||||
updatePassword: updatePassword_fr,
|
||||
upload: upload_fr,
|
||||
user: user_fr,
|
||||
delete_brain: delete_brain_fr,
|
||||
delete_or_unsubscribe_from_brain: delete_brain_fr,
|
||||
},
|
||||
ptbr: {
|
||||
brain: brain_ptbr,
|
||||
@ -170,7 +170,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
|
||||
updatePassword: updatePassword_ptbr,
|
||||
upload: upload_ptbr,
|
||||
user: user_ptbr,
|
||||
delete_brain: delete_brain_ptbr,
|
||||
delete_or_unsubscribe_from_brain: delete_brain_ptbr,
|
||||
},
|
||||
ru: {
|
||||
brain: brain_ru,
|
||||
@ -185,7 +185,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
|
||||
updatePassword: updatePassword_ru,
|
||||
upload: upload_ru,
|
||||
user: user_ru,
|
||||
delete_brain: delete_brain_ru,
|
||||
delete_or_unsubscribe_from_brain: delete_brain_ru,
|
||||
},
|
||||
zh_cn: {
|
||||
brain: brain_zh_cn,
|
||||
@ -200,6 +200,6 @@ export const resources: Record<SupportedLanguages, Translations> = {
|
||||
updatePassword: updatePassword_zh_cn,
|
||||
upload: upload_zh_cn,
|
||||
user: user_zh_cn,
|
||||
delete_brain: delete_brain_zh_cn,
|
||||
delete_or_unsubscribe_from_brain: delete_brain_zh_cn,
|
||||
},
|
||||
} as const;
|
||||
|
@ -1,6 +1,7 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { UUID } from "crypto";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { CreateBrainInput } from "@/lib/api/brain/types";
|
||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||
@ -18,6 +19,7 @@ export const useBrainProvider = () => {
|
||||
const { createBrain, deleteBrain, getBrains, getDefaultBrain } =
|
||||
useBrainApi();
|
||||
const { getPublicPrompts } = usePromptApi();
|
||||
const { t } = useTranslation(["delete_or_unsubscribe_from_brain"]);
|
||||
|
||||
const [allBrains, setAllBrains] = useState<MinimalBrainForUser[]>([]);
|
||||
const [currentBrainId, setCurrentBrainId] = useState<null | UUID>(null);
|
||||
@ -60,7 +62,7 @@ export const useBrainProvider = () => {
|
||||
void track("DELETE_BRAIN");
|
||||
publish({
|
||||
variant: "success",
|
||||
text: "Brain deleted",
|
||||
text: t("successfully_deleted"),
|
||||
});
|
||||
},
|
||||
[deleteBrain, publish, track]
|
||||
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"deleteButton": "Delete Brain",
|
||||
"deleteConfirmQuestion": "Are you sure you want to delete this brain? This action cannot be undone.",
|
||||
"deleteConfirmYes": "Yes, delete this brain",
|
||||
"returnButton": "Return"
|
||||
}
|
11
frontend/public/locales/en/deleteOrUnsubscribeFromBrain.json
Normal file
11
frontend/public/locales/en/deleteOrUnsubscribeFromBrain.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"deleteButton": "Delete Brain",
|
||||
"deleteConfirmQuestion": "Are you sure you want to delete this brain? This action cannot be undone.",
|
||||
"deleteConfirmYes": "Yes, delete this brain",
|
||||
"returnButton": "Return",
|
||||
"unsubscribeButton": "Unsubscribe from brain",
|
||||
"unsubscribeConfirmQuestion": "Are you sure you want to unsubscribe from this brain?",
|
||||
"unsubscribeConfirmYes": "Yes, unsubscribe from this brain",
|
||||
"successfully_deleted": "Brain successfully deleted.",
|
||||
"successfully_unsubscribed": "You have successfully unsubscribed from this brain."
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"deleteButton": "Borrar el cerebro",
|
||||
"deleteConfirmQuestion": "¿Estás seguro de que quieres borrar el cerebro? No podrás recuperarlo.",
|
||||
"deleteConfirmYes": "Sí, borrar el cerebro",
|
||||
"returnButton": "Vuelta"
|
||||
}
|
11
frontend/public/locales/es/deleteOrUnsubscribeFromBrain.json
Normal file
11
frontend/public/locales/es/deleteOrUnsubscribeFromBrain.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"deleteButton": "Borrar el cerebro",
|
||||
"deleteConfirmQuestion": "¿Estás seguro de que quieres borrar el cerebro? No podrás recuperarlo.",
|
||||
"deleteConfirmYes": "Sí, borrar el cerebro",
|
||||
"returnButton": "Vuelta",
|
||||
"unsubscribeButton": "Cancelar suscripción del cerebro",
|
||||
"unsubscribeConfirmQuestion": "¿Estás seguro de que quieres darte de baja de este cerebro?",
|
||||
"unsubscribeConfirmYes": "Sí, cancelar la suscripción de este cerebro",
|
||||
"successfully_deleted": "Cerebro eliminado con éxito.",
|
||||
"successfully_unsubscribed": "Te has desinscrito con éxito de este cerebro."
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"deleteButton": "Supprimer le cerveau",
|
||||
"deleteConfirmQuestion": "Êtes-vous sûr de vouloir supprimer le cerveau? Cette action est irréversible!",
|
||||
"deleteConfirmYes": "Oui, supprimer",
|
||||
"returnButton": "Retour"
|
||||
}
|
11
frontend/public/locales/fr/deleteOrUnsubscribeFromBrain.json
Normal file
11
frontend/public/locales/fr/deleteOrUnsubscribeFromBrain.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"deleteButton": "Supprimer le cerveau",
|
||||
"deleteConfirmQuestion": "Êtes-vous sûr de vouloir supprimer le cerveau? Cette action est irréversible!",
|
||||
"deleteConfirmYes": "Oui, supprimer",
|
||||
"returnButton": "Retour",
|
||||
"unsubscribeButton": "Se désabonner du cerveau",
|
||||
"unsubscribeConfirmQuestion": "Êtes-vous sûr de vouloir vous désabonner de ce cerveau ?",
|
||||
"unsubscribeConfirmYes": "Oui, se désabonner de ce cerveau",
|
||||
"successfully_deleted": "Cerveau supprimé avec succès.",
|
||||
"successfully_unsubscribed": "Vous vous êtes désabonné avec succès de ce cerveau."
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"deleteButton": "Excluir Cérebro",
|
||||
"deleteConfirmQuestion": "Você tem certeza de que deseja excluir este cérebro? Essa ação não pode ser desfeita.",
|
||||
"deleteConfirmYes": "Sim, excluir este cérebro",
|
||||
"returnButton": "Retornar"
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"deleteButton": "Excluir Cérebro",
|
||||
"deleteConfirmQuestion": "Você tem certeza de que deseja excluir este cérebro? Essa ação não pode ser desfeita.",
|
||||
"deleteConfirmYes": "Sim, excluir este cérebro",
|
||||
"returnButton": "Retornar",
|
||||
"unsubscribeButton": "Cancelar inscrição do cérebro",
|
||||
"unsubscribeConfirmQuestion": "Você tem certeza de que deseja cancelar a inscrição deste cérebro?",
|
||||
"unsubscribeConfirmYes": "Sim, cancelar a inscrição deste cérebro",
|
||||
"successfully_deleted": "Cérebro excluído com sucesso.",
|
||||
"successfully_unsubscribed": "Você se desinscreveu com sucesso deste cérebro."
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"deleteButton": "Удалить мозг",
|
||||
"deleteConfirmQuestion": "Вы уверены, что хотите удалить этот мозг? Это действие нельзя отменить.",
|
||||
"deleteConfirmYes": "Да, удалить этот мозг",
|
||||
"returnButton": "Вернуться"
|
||||
}
|
11
frontend/public/locales/ru/deleteOrUnsubscribeFromBrain.json
Normal file
11
frontend/public/locales/ru/deleteOrUnsubscribeFromBrain.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"deleteButton": "Удалить мозг",
|
||||
"deleteConfirmQuestion": "Вы уверены, что хотите удалить этот мозг? Это действие нельзя отменить.",
|
||||
"deleteConfirmYes": "Да, удалить этот мозг",
|
||||
"returnButton": "Вернуться",
|
||||
"unsubscribeButton": "Отписаться от мозга",
|
||||
"unsubscribeConfirmQuestion": "Вы уверены, что хотите отписаться от этого мозга?",
|
||||
"unsubscribeConfirmYes": "Да, отписаться от этого сознания",
|
||||
"successfully_deleted": "Мозг успешно удален.",
|
||||
"successfully_unsubscribed": "Вы успешно отписались от этого мозга."
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"deleteButton": "删除大脑",
|
||||
"deleteConfirmQuestion": "您确定要删除这个大脑吗?此操作无法撤销!",
|
||||
"deleteConfirmYes": "是的,我要删除",
|
||||
"returnButton": "返回"
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"deleteButton": "删除大脑",
|
||||
"deleteConfirmQuestion": "您确定要删除这个大脑吗?此操作无法撤销!",
|
||||
"deleteConfirmYes": "是的,我要删除",
|
||||
"returnButton": "返回",
|
||||
"unsubscribeButton": "取消关注大脑",
|
||||
"unsubscribeConfirmQuestion": "您确定要取消订阅此大脑吗?",
|
||||
"unsubscribeConfirmYes": "是的,取消订阅此大脑",
|
||||
"successfully_deleted": "成功删除大脑。",
|
||||
"successfully_unsubscribed": "您已成功取消订阅此大脑。"
|
||||
}
|
Loading…
Reference in New Issue
Block a user