Shareable brain 4 (#611)

* feat(useBrainApi): add subscription creation to sdk

* feat: add share brain submit handler
This commit is contained in:
Mamadou DICKO 2023-07-12 15:45:45 +02:00 committed by GitHub
parent 783f8dea76
commit 677e6bcefe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 138 additions and 31 deletions

View File

@ -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";

View File

@ -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
);
});
}); });

View File

@ -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);
};

View File

@ -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),
}; };
}; };

View File

@ -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 => {

View File

@ -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 />

View File

@ -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);

View File

@ -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,
}; };
}; };

View File

@ -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;