mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-15 17:43:03 +03:00
Shareable brain 4 (#611)
* feat(useBrainApi): add subscription creation to sdk * feat: add share brain submit handler
This commit is contained in:
parent
783f8dea76
commit
677e6bcefe
@ -11,7 +11,7 @@ import {
|
|||||||
import Button from "@/lib/components/ui/Button";
|
import Button from "@/lib/components/ui/Button";
|
||||||
import { AnimatedCard } from "@/lib/components/ui/Card";
|
import { AnimatedCard } from "@/lib/components/ui/Card";
|
||||||
import Ellipsis from "@/lib/components/ui/Ellipsis";
|
import Ellipsis from "@/lib/components/ui/Ellipsis";
|
||||||
import Modal from "@/lib/components/ui/Modal";
|
import { Modal } from "@/lib/components/ui/Modal";
|
||||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||||
import { useAxios, useToast } from "@/lib/hooks";
|
import { useAxios, useToast } from "@/lib/hooks";
|
||||||
import { Document } from "@/lib/types/Document";
|
import { Document } from "@/lib/types/Document";
|
||||||
|
@ -109,4 +109,26 @@ describe("useBrainApi", () => {
|
|||||||
expect(axiosGetMock).toHaveBeenCalledTimes(1);
|
expect(axiosGetMock).toHaveBeenCalledTimes(1);
|
||||||
expect(axiosGetMock).toHaveBeenCalledWith(`/brains/${id}/`);
|
expect(axiosGetMock).toHaveBeenCalledWith(`/brains/${id}/`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should call addBrainSubscription with the correct parameters", async () => {
|
||||||
|
const {
|
||||||
|
result: {
|
||||||
|
current: { addBrainSubscriptions },
|
||||||
|
},
|
||||||
|
} = renderHook(() => useBrainApi());
|
||||||
|
const id = "123";
|
||||||
|
const subscriptions = [
|
||||||
|
{
|
||||||
|
email: "user@quivr.app",
|
||||||
|
rights: "viewer",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await addBrainSubscriptions(id, subscriptions);
|
||||||
|
|
||||||
|
expect(axiosPostMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(axiosPostMock).toHaveBeenCalledWith(
|
||||||
|
`/brain/${id}/subscription`,
|
||||||
|
subscriptions
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { AxiosInstance } from "axios";
|
import { AxiosInstance } from "axios";
|
||||||
|
|
||||||
|
import { BrainRoleType } from "@/lib/components/NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
|
||||||
import { Brain } from "@/lib/context/BrainProvider/types";
|
import { Brain } from "@/lib/context/BrainProvider/types";
|
||||||
import { Document } from "@/lib/types/Document";
|
import { Document } from "@/lib/types/Document";
|
||||||
|
|
||||||
@ -59,3 +60,13 @@ export const getBrains = async (
|
|||||||
|
|
||||||
return brains.brains;
|
return brains.brains;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Subscription = { email: string; rights: BrainRoleType }[];
|
||||||
|
|
||||||
|
export const addBrainSubscriptions = async (
|
||||||
|
brainId: string,
|
||||||
|
subscriptions: Subscription,
|
||||||
|
axiosInstance: AxiosInstance
|
||||||
|
): Promise<void> => {
|
||||||
|
await axiosInstance.post(`/brain/${brainId}/subscription`, subscriptions);
|
||||||
|
};
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { useAxios } from "@/lib/hooks";
|
import { useAxios } from "@/lib/hooks";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
addBrainSubscriptions,
|
||||||
createBrain,
|
createBrain,
|
||||||
deleteBrain,
|
deleteBrain,
|
||||||
getBrain,
|
getBrain,
|
||||||
getBrainDocuments,
|
getBrainDocuments,
|
||||||
getBrains,
|
getBrains,
|
||||||
getDefaultBrain,
|
getDefaultBrain,
|
||||||
|
Subscription,
|
||||||
} from "./brain";
|
} from "./brain";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
@ -21,5 +23,9 @@ export const useBrainApi = () => {
|
|||||||
getDefaultBrain: async () => getDefaultBrain(axiosInstance),
|
getDefaultBrain: async () => getDefaultBrain(axiosInstance),
|
||||||
getBrains: async () => getBrains(axiosInstance),
|
getBrains: async () => getBrains(axiosInstance),
|
||||||
getBrain: async (id: string) => getBrain(id, axiosInstance),
|
getBrain: async (id: string) => getBrain(id, axiosInstance),
|
||||||
|
addBrainSubscriptions: async (
|
||||||
|
brainId: string,
|
||||||
|
subscriptions: Subscription
|
||||||
|
) => addBrainSubscriptions(brainId, subscriptions, axiosInstance),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { MdAdd } from "react-icons/md";
|
|||||||
|
|
||||||
import Button from "@/lib/components/ui/Button";
|
import Button from "@/lib/components/ui/Button";
|
||||||
import Field from "@/lib/components/ui/Field";
|
import Field from "@/lib/components/ui/Field";
|
||||||
import Modal from "@/lib/components/ui/Modal";
|
import { Modal } from "@/lib/components/ui/Modal";
|
||||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
|
|
||||||
export const AddBrainModal = (): JSX.Element => {
|
export const AddBrainModal = (): JSX.Element => {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { UUID } from "crypto";
|
import { UUID } from "crypto";
|
||||||
@ -5,7 +6,7 @@ import { ImUserPlus } from "react-icons/im";
|
|||||||
import { MdContentPaste, MdShare } from "react-icons/md";
|
import { MdContentPaste, MdShare } from "react-icons/md";
|
||||||
|
|
||||||
import Button from "@/lib/components/ui/Button";
|
import Button from "@/lib/components/ui/Button";
|
||||||
import Modal from "@/lib/components/ui/Modal";
|
import { Modal } from "@/lib/components/ui/Modal";
|
||||||
|
|
||||||
import { InvitedUserRow } from "./components/InvitedUserRow";
|
import { InvitedUserRow } from "./components/InvitedUserRow";
|
||||||
import { useShareBrain } from "./hooks/useShareBrain";
|
import { useShareBrain } from "./hooks/useShareBrain";
|
||||||
@ -23,6 +24,9 @@ export const ShareBrain = ({ brainId }: ShareBrainModalProps): JSX.Element => {
|
|||||||
removeRoleAssignation,
|
removeRoleAssignation,
|
||||||
inviteUsers,
|
inviteUsers,
|
||||||
addNewRoleAssignationRole,
|
addNewRoleAssignationRole,
|
||||||
|
sendingInvitation,
|
||||||
|
setIsShareModalOpen,
|
||||||
|
isShareModalOpen,
|
||||||
} = useShareBrain(brainId);
|
} = useShareBrain(brainId);
|
||||||
|
|
||||||
const canAddNewRow =
|
const canAddNewRow =
|
||||||
@ -44,6 +48,8 @@ export const ShareBrain = ({ brainId }: ShareBrainModalProps): JSX.Element => {
|
|||||||
}
|
}
|
||||||
CloseTrigger={<div />}
|
CloseTrigger={<div />}
|
||||||
title="Share brain"
|
title="Share brain"
|
||||||
|
isOpen={isShareModalOpen}
|
||||||
|
setOpen={setIsShareModalOpen}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
@ -79,7 +85,8 @@ export const ShareBrain = ({ brainId }: ShareBrainModalProps): JSX.Element => {
|
|||||||
<Button
|
<Button
|
||||||
className="my-5"
|
className="my-5"
|
||||||
onClick={addNewRoleAssignationRole}
|
onClick={addNewRoleAssignationRole}
|
||||||
disabled={!canAddNewRow}
|
disabled={sendingInvitation || !canAddNewRow}
|
||||||
|
isLoading={sendingInvitation}
|
||||||
data-testid="add-new-row-role-button"
|
data-testid="add-new-row-role-button"
|
||||||
>
|
>
|
||||||
<ImUserPlus />
|
<ImUserPlus />
|
||||||
|
@ -1,12 +1,37 @@
|
|||||||
import { fireEvent, render } from "@testing-library/react";
|
import { fireEvent, render } from "@testing-library/react";
|
||||||
import { describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BrainConfigContextMock,
|
||||||
|
BrainConfigProviderMock,
|
||||||
|
} from "@/lib/context/BrainConfigProvider/mocks/BrainConfigProviderMock";
|
||||||
|
import {
|
||||||
|
SupabaseContextMock,
|
||||||
|
SupabaseProviderMock,
|
||||||
|
} from "@/lib/context/SupabaseProvider/mocks/SupabaseProviderMock";
|
||||||
|
|
||||||
import { ShareBrain } from "../ShareBrain";
|
import { ShareBrain } from "../ShareBrain";
|
||||||
|
|
||||||
|
vi.mock("@/lib/context/SupabaseProvider/supabase-provider", () => ({
|
||||||
|
SupabaseContext: SupabaseContextMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/context/BrainConfigProvider/brain-config-provider", () => ({
|
||||||
|
BrainConfigContext: BrainConfigContextMock,
|
||||||
|
}));
|
||||||
|
|
||||||
describe("ShareBrain", () => {
|
describe("ShareBrain", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it("should render ShareBrain component properly", () => {
|
it("should render ShareBrain component properly", () => {
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<ShareBrain brainId="cf9bb422-b1b6-4fd7-abc1-01bd395d2318" />
|
<SupabaseProviderMock>
|
||||||
|
<BrainConfigProviderMock>
|
||||||
|
<ShareBrain brainId="cf9bb422-b1b6-4fd7-abc1-01bd395d2318" />
|
||||||
|
</BrainConfigProviderMock>
|
||||||
|
</SupabaseProviderMock>
|
||||||
);
|
);
|
||||||
const shareButton = getByTestId("share-brain-button");
|
const shareButton = getByTestId("share-brain-button");
|
||||||
expect(shareButton).toBeDefined();
|
expect(shareButton).toBeDefined();
|
||||||
@ -14,7 +39,12 @@ describe("ShareBrain", () => {
|
|||||||
|
|
||||||
it("should render open share modal when share button is clicked", () => {
|
it("should render open share modal when share button is clicked", () => {
|
||||||
const { getByText, getByTestId } = render(
|
const { getByText, getByTestId } = render(
|
||||||
<ShareBrain brainId="cf9bb422-b1b6-4fd7-abc1-01bd395d2318" />
|
// Todo: add a custom render function that wraps the component with the providers
|
||||||
|
<SupabaseProviderMock>
|
||||||
|
<BrainConfigProviderMock>
|
||||||
|
<ShareBrain brainId="cf9bb422-b1b6-4fd7-abc1-01bd395d2318" />
|
||||||
|
</BrainConfigProviderMock>
|
||||||
|
</SupabaseProviderMock>
|
||||||
);
|
);
|
||||||
const shareButton = getByTestId("share-brain-button");
|
const shareButton = getByTestId("share-brain-button");
|
||||||
fireEvent.click(shareButton);
|
fireEvent.click(shareButton);
|
||||||
@ -23,7 +53,11 @@ describe("ShareBrain", () => {
|
|||||||
|
|
||||||
it('shoud add new user row when "Add new user" button is clicked and only where there is no empty field', async () => {
|
it('shoud add new user row when "Add new user" button is clicked and only where there is no empty field', async () => {
|
||||||
const { getByTestId, findAllByTestId } = render(
|
const { getByTestId, findAllByTestId } = render(
|
||||||
<ShareBrain brainId="cf9bb422-b1b6-4fd7-abc1-01bd395d2318" />
|
<SupabaseProviderMock>
|
||||||
|
<BrainConfigProviderMock>
|
||||||
|
<ShareBrain brainId="cf9bb422-b1b6-4fd7-abc1-01bd395d2318" />
|
||||||
|
</BrainConfigProviderMock>
|
||||||
|
</SupabaseProviderMock>
|
||||||
);
|
);
|
||||||
const shareButton = getByTestId("share-brain-button");
|
const shareButton = getByTestId("share-brain-button");
|
||||||
fireEvent.click(shareButton);
|
fireEvent.click(shareButton);
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||||
import { useToast } from "@/lib/hooks";
|
import { useToast } from "@/lib/hooks";
|
||||||
|
|
||||||
import { BrainRoleAssignation } from "../../../types";
|
import { BrainRoleAssignation } from "../../../types";
|
||||||
@ -9,9 +11,11 @@ import { generateBrainAssignation } from "../utils/generateBrainAssignation";
|
|||||||
export const useShareBrain = (brainId: string) => {
|
export const useShareBrain = (brainId: string) => {
|
||||||
const baseUrl = window.location.origin;
|
const baseUrl = window.location.origin;
|
||||||
const { publish } = useToast();
|
const { publish } = useToast();
|
||||||
|
const { addBrainSubscriptions } = useBrainApi();
|
||||||
|
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
|
||||||
|
|
||||||
const brainShareLink = `${baseUrl}/brain_subscription_invitation=${brainId}`;
|
const brainShareLink = `${baseUrl}/brain_subscription_invitation=${brainId}`;
|
||||||
|
const [sendingInvitation, setSendingInvitation] = useState(false);
|
||||||
const [roleAssignations, setRoleAssignation] = useState<
|
const [roleAssignations, setRoleAssignation] = useState<
|
||||||
BrainRoleAssignation[]
|
BrainRoleAssignation[]
|
||||||
>([generateBrainAssignation()]);
|
>([generateBrainAssignation()]);
|
||||||
@ -52,19 +56,32 @@ export const useShareBrain = (brainId: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const inviteUsers = (): void => {
|
const inviteUsers = async (): Promise<void> => {
|
||||||
const inviteUsersPayload = roleAssignations
|
setSendingInvitation(true);
|
||||||
.filter(({ email }) => email !== "")
|
try {
|
||||||
.map((assignation) => ({
|
const inviteUsersPayload = roleAssignations
|
||||||
email: assignation.email,
|
.filter(({ email }) => email !== "")
|
||||||
role: assignation.role,
|
.map((assignation) => ({
|
||||||
}));
|
email: assignation.email,
|
||||||
|
rights: assignation.role,
|
||||||
|
}));
|
||||||
|
|
||||||
alert(
|
await addBrainSubscriptions(brainId, inviteUsersPayload);
|
||||||
`You will soon be able to invite ${JSON.stringify(
|
|
||||||
inviteUsersPayload
|
publish({
|
||||||
)}. Wait a bit`
|
variant: "success",
|
||||||
);
|
text: "Users successfully invited",
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsShareModalOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
publish({
|
||||||
|
variant: "danger",
|
||||||
|
text: "An error occurred while sending invitations",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSendingInvitation(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addNewRoleAssignationRole = () => {
|
const addNewRoleAssignationRole = () => {
|
||||||
@ -79,5 +96,8 @@ export const useShareBrain = (brainId: string) => {
|
|||||||
removeRoleAssignation,
|
removeRoleAssignation,
|
||||||
inviteUsers,
|
inviteUsers,
|
||||||
addNewRoleAssignationRole,
|
addNewRoleAssignationRole,
|
||||||
|
sendingInvitation,
|
||||||
|
setIsShareModalOpen,
|
||||||
|
isShareModalOpen,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -6,27 +6,36 @@ import { MdClose } from "react-icons/md";
|
|||||||
|
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
|
|
||||||
interface ModalProps {
|
type CommonModalProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
desc?: string;
|
desc?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
Trigger: ReactNode;
|
Trigger: ReactNode;
|
||||||
CloseTrigger?: ReactNode;
|
CloseTrigger?: ReactNode;
|
||||||
opened?: boolean;
|
isOpen?: undefined;
|
||||||
}
|
setOpen?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const Modal = ({
|
type ModalProps =
|
||||||
|
| CommonModalProps
|
||||||
|
| (Omit<CommonModalProps, "isOpen" | "setOpen"> & {
|
||||||
|
isOpen: boolean;
|
||||||
|
setOpen: (isOpen: boolean) => void;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Modal = ({
|
||||||
title,
|
title,
|
||||||
desc,
|
desc,
|
||||||
children,
|
children,
|
||||||
Trigger,
|
Trigger,
|
||||||
CloseTrigger,
|
CloseTrigger,
|
||||||
opened = false,
|
isOpen: customIsOpen,
|
||||||
|
setOpen: customSetOpen,
|
||||||
}: ModalProps): JSX.Element => {
|
}: ModalProps): JSX.Element => {
|
||||||
const [open, setOpen] = useState(opened);
|
const [isOpen, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root onOpenChange={setOpen}>
|
<Dialog.Root onOpenChange={customSetOpen ?? setOpen}>
|
||||||
<Dialog.Trigger asChild>
|
<Dialog.Trigger asChild>
|
||||||
{Trigger}
|
{Trigger}
|
||||||
{/* <button className="text-violet11 shadow-blackA7 hover:bg-mauve3 inline-flex h-[35px] items-center justify-center rounded-[4px] bg-white px-[15px] font-medium leading-none shadow-[0_2px_10px] focus:shadow-[0_0_0_2px] focus:shadow-black focus:outline-none">
|
{/* <button className="text-violet11 shadow-blackA7 hover:bg-mauve3 inline-flex h-[35px] items-center justify-center rounded-[4px] bg-white px-[15px] font-medium leading-none shadow-[0_2px_10px] focus:shadow-[0_0_0_2px] focus:shadow-black focus:outline-none">
|
||||||
@ -34,7 +43,7 @@ const Modal = ({
|
|||||||
</button> */}
|
</button> */}
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{open ? (
|
{customIsOpen ?? isOpen ? (
|
||||||
<Dialog.Portal forceMount>
|
<Dialog.Portal forceMount>
|
||||||
<Dialog.Overlay asChild forceMount>
|
<Dialog.Overlay asChild forceMount>
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -89,5 +98,3 @@ const Modal = ({
|
|||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Modal;
|
|
||||||
|
Loading…
Reference in New Issue
Block a user