mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: projects page flickering, mutation of workspace members
This commit is contained in:
parent
02f0d14dc0
commit
e4de5e62c9
@ -1,14 +1,15 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
// swr
|
||||
import { mutate } from "swr";
|
||||
import useSWR, { mutate } from "swr";
|
||||
// react hook form
|
||||
import { useForm } from "react-hook-form";
|
||||
// headless
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import projectServices from "lib/services/project.service";
|
||||
import workspaceService from "lib/services/workspace.service";
|
||||
// fetch keys
|
||||
import { PROJECTS_LIST } from "constants/fetch-keys";
|
||||
import { PROJECTS_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
import useToast from "lib/hooks/useToast";
|
||||
@ -17,7 +18,7 @@ import { Button, Input, TextArea, Select } from "ui";
|
||||
// common
|
||||
import { debounce } from "constants/common";
|
||||
// types
|
||||
import { IProject } from "types";
|
||||
import { IProject, WorkspaceMember } from "types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@ -31,6 +32,23 @@ const defaultValues: Partial<IProject> = {
|
||||
description: "",
|
||||
};
|
||||
|
||||
const IsGuestCondition: React.FC<{
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}> = ({ setIsOpen }) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(false);
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
type: "error",
|
||||
message: "You don't have permission to create project.",
|
||||
});
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
@ -40,7 +58,12 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const { activeWorkspace } = useUser();
|
||||
const { activeWorkspace, user } = useUser();
|
||||
|
||||
const { data: workspaceMembers } = useSWR<WorkspaceMember[]>(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@ -56,6 +79,8 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
setValue,
|
||||
} = useForm<IProject>({
|
||||
defaultValues,
|
||||
reValidateMode: "onChange",
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: IProject) => {
|
||||
@ -77,6 +102,15 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
handleClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.status === 403) {
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
type: "error",
|
||||
message: "You don't have permission to create project.",
|
||||
});
|
||||
handleClose();
|
||||
return;
|
||||
}
|
||||
Object.keys(err).map((key) => {
|
||||
const errorMessages = err[key];
|
||||
setError(key as keyof IProject, {
|
||||
@ -105,6 +139,15 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
}
|
||||
}, [projectName, projectIdentifier, setValue, isChangeIdentifierRequired]);
|
||||
|
||||
if (workspaceMembers) {
|
||||
const isMember = workspaceMembers.find((member) => member.member.id === user?.id);
|
||||
const isGuest = workspaceMembers.find(
|
||||
(member) => member.member.id === user?.id && member.role === 5
|
||||
);
|
||||
|
||||
if ((!isMember || isGuest) && isOpen) return <IsGuestCondition setIsOpen={setIsOpen} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={handleClose}>
|
||||
@ -203,8 +246,8 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
message: "Identifier must at least be of 1 character",
|
||||
},
|
||||
maxLength: {
|
||||
value: 9,
|
||||
message: "Identifier must at most be of 9 characters",
|
||||
value: 5,
|
||||
message: "Identifier must at most be of 5 characters",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -50,7 +50,7 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: people } = useSWR<WorkspaceMember[]>(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
|
@ -68,7 +68,7 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
||||
);
|
||||
|
||||
const { data: people } = useSWR<WorkspaceMember[]>(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
|
@ -3,10 +3,14 @@ import React, { useState } from "react";
|
||||
// next
|
||||
import Link from "next/link";
|
||||
import useSWR from "swr";
|
||||
import _ from "lodash";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// Services
|
||||
import projectService from "lib/services/project.service";
|
||||
// fetch keys
|
||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
// commons
|
||||
import { renderShortNumericDateFormat } from "constants/common";
|
||||
// icons
|
||||
import {
|
||||
CalendarDaysIcon,
|
||||
@ -17,25 +21,41 @@ import {
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { renderShortNumericDateFormat } from "constants/common";
|
||||
// types
|
||||
import type { IProject } from "types";
|
||||
type Props = {
|
||||
project: IProject;
|
||||
slug: string;
|
||||
invitationsRespond: string[];
|
||||
handleInvitation: (project_invitation: any, action: "accepted" | "withdraw") => void;
|
||||
setDeleteProject: React.Dispatch<React.SetStateAction<IProject | undefined>>;
|
||||
};
|
||||
|
||||
const ProjectMemberInvitations = ({
|
||||
const ProjectMemberInvitations: React.FC<Props> = ({
|
||||
project,
|
||||
slug,
|
||||
invitationsRespond,
|
||||
handleInvitation,
|
||||
setDeleteProject,
|
||||
}: any) => {
|
||||
}) => {
|
||||
const { user } = useUser();
|
||||
const { data: members } = useSWR("PROJECT_MEMBERS", () =>
|
||||
|
||||
const { data: members } = useSWR<any[]>(PROJECT_MEMBERS(project.id), () =>
|
||||
projectService.projectMembers(slug, project.id)
|
||||
);
|
||||
|
||||
const isMember =
|
||||
_.filter(members, (item: any) => item.member.id === (user as any).id).length === 1;
|
||||
const isMember = members?.some((item: any) => item.member.id === (user as any)?.id);
|
||||
|
||||
const [selected, setSelected] = useState<any>(false);
|
||||
|
||||
if (!members) {
|
||||
return (
|
||||
<div className="w-full h-36 flex flex-col px-4 py-3 rounded-md bg-white">
|
||||
<div className="w-full h-full bg-gray-50 animate-pulse" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
@ -2,7 +2,7 @@ export const CURRENT_USER = "CURRENT_USER";
|
||||
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
|
||||
export const USER_WORKSPACES = "USER_WORKSPACES";
|
||||
|
||||
export const WORKSPACE_MEMBERS = "WORKSPACE_MEMBERS";
|
||||
export const WORKSPACE_MEMBERS = (workspaceSlug: string) => `WORKSPACE_MEMBERS_${workspaceSlug}`;
|
||||
export const WORKSPACE_INVITATIONS = "WORKSPACE_INVITATIONS";
|
||||
export const WORKSPACE_INVITATION = "WORKSPACE_INVITATION";
|
||||
|
||||
|
@ -9,6 +9,7 @@ import authenticationService from "lib/services/authentication.service";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
import useTheme from "lib/hooks/useTheme";
|
||||
import useToast from "lib/hooks/useToast";
|
||||
// components
|
||||
import CreateProjectModal from "components/project/CreateProjectModal";
|
||||
// headless ui
|
||||
@ -119,6 +120,8 @@ const Sidebar: React.FC = () => {
|
||||
|
||||
const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
return (
|
||||
<nav className="h-full">
|
||||
<CreateProjectModal isOpen={isCreateProjectModal} setIsOpen={setCreateProjectModal} />
|
||||
@ -282,9 +285,11 @@ const Sidebar: React.FC = () => {
|
||||
last_workspace_id: workspace?.id,
|
||||
})
|
||||
.then((res) => {
|
||||
router.push("/workspace");
|
||||
const isInProject =
|
||||
router.pathname.includes("/[projectId]/");
|
||||
if (isInProject) router.push("/workspace");
|
||||
})
|
||||
.catch((err) => console.log);
|
||||
.catch((err) => console.error(err));
|
||||
}}
|
||||
className={`${
|
||||
active ? "bg-theme text-white" : "text-gray-900"
|
||||
@ -479,7 +484,13 @@ const Sidebar: React.FC = () => {
|
||||
onClick={() =>
|
||||
copyTextToClipboard(
|
||||
`https://app.plane.so/projects/${project?.id}/issues/`
|
||||
)
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
title: "Link Copied",
|
||||
message: "Link copied to clipboard",
|
||||
type: "success",
|
||||
});
|
||||
})
|
||||
}
|
||||
>
|
||||
<ClipboardDocumentIcon className="h-3 w-3" />
|
||||
|
@ -27,7 +27,7 @@ class ProjectServices extends APIService {
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -235,7 +235,7 @@ const ProjectMembers: NextPage = () => {
|
||||
)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
||||
{member.status ? (
|
||||
{member.member ? (
|
||||
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
||||
Active
|
||||
</span>
|
||||
@ -261,7 +261,7 @@ const ProjectMembers: NextPage = () => {
|
||||
className="w-full text-left py-2 pl-2"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!member.status) {
|
||||
if (!member.member) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
message: "You can't edit a pending invitation.",
|
||||
@ -282,7 +282,7 @@ const ProjectMembers: NextPage = () => {
|
||||
className="w-full text-left py-2 pl-2"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (member.status) {
|
||||
if (member.member) {
|
||||
setSelectedRemoveMember(member.id);
|
||||
} else {
|
||||
setSelectedInviteRemoveMember(member.id);
|
||||
|
@ -78,7 +78,7 @@ const ProjectSettings: NextPage = () => {
|
||||
);
|
||||
|
||||
const { data: people } = useSWR<WorkspaceMember[]>(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
|
@ -41,7 +41,7 @@ const WorkspaceInvite: NextPage = () => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: workspaceMembers, mutate: mutateMembers } = useSWR<any[]>(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
const { data: workspaceInvitations, mutate: mutateInvitations } = useSWR<any[]>(
|
||||
@ -229,7 +229,7 @@ const WorkspaceInvite: NextPage = () => {
|
||||
)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
||||
{member.status ? (
|
||||
{member.member ? (
|
||||
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
||||
Active
|
||||
</span>
|
||||
@ -277,7 +277,7 @@ const WorkspaceInvite: NextPage = () => {
|
||||
className="w-full text-left py-2 pl-2"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (member.status) {
|
||||
if (member.member) {
|
||||
setSelectedRemoveMember(member.id);
|
||||
} else {
|
||||
setSelectedInviteRemoveMember(member.id);
|
||||
|
Loading…
Reference in New Issue
Block a user