forked from github/plane
fix: workspace settings pages authorization (#2915)
* fix: workspace settings pages authorization * chore: user cannot add a member with a higher role than theirs * chore: update workspace general settings auth
This commit is contained in:
parent
f7264364bd
commit
0cbb201348
@ -25,7 +25,6 @@ import {
|
|||||||
IViewIssuesFilterStore,
|
IViewIssuesFilterStore,
|
||||||
IViewIssuesStore,
|
IViewIssuesStore,
|
||||||
} from "store/issues";
|
} from "store/issues";
|
||||||
import { EUserWorkspaceRoles } from "layouts/settings-layout/workspace/sidebar";
|
|
||||||
import { TUnGroupedIssues } from "store/issues/types";
|
import { TUnGroupedIssues } from "store/issues/types";
|
||||||
|
|
||||||
interface IBaseGanttRoot {
|
interface IBaseGanttRoot {
|
||||||
@ -46,6 +45,10 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
|||||||
|
|
||||||
const { projectDetails } = useProjectDetails();
|
const { projectDetails } = useProjectDetails();
|
||||||
|
|
||||||
|
const {
|
||||||
|
user: { currentProjectRole },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const appliedDisplayFilters = issueFiltersStore.issueFilters?.displayFilters;
|
const appliedDisplayFilters = issueFiltersStore.issueFilters?.displayFilters;
|
||||||
|
|
||||||
const issuesResponse = issueStore.getIssues;
|
const issuesResponse = issueStore.getIssues;
|
||||||
@ -69,7 +72,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAllowed = (projectDetails?.member_role || 0) >= EUserWorkspaceRoles.MEMBER;
|
const isAllowed = currentProjectRole && currentProjectRole >= 15;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { Plus, X } from "lucide-react";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// ui
|
// ui
|
||||||
import { Button, CustomSelect, Input } from "@plane/ui";
|
import { Button, CustomSelect, Input } from "@plane/ui";
|
||||||
// icons
|
|
||||||
import { Plus, X } from "lucide-react";
|
|
||||||
// types
|
// types
|
||||||
import { IWorkspaceBulkInviteFormData, TUserWorkspaceRole } from "types";
|
import { IWorkspaceBulkInviteFormData, TUserWorkspaceRole } from "types";
|
||||||
// constants
|
// constants
|
||||||
@ -34,9 +36,12 @@ const defaultValues: FormValues = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
|
export const SendWorkspaceInvitationModal: React.FC<Props> = observer((props) => {
|
||||||
const { isOpen, onClose, onSubmit } = props;
|
const { isOpen, onClose, onSubmit } = props;
|
||||||
|
// mobx store
|
||||||
|
const {
|
||||||
|
user: { currentWorkspaceRole },
|
||||||
|
} = useMobxStore();
|
||||||
// form info
|
// form info
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@ -59,30 +64,6 @@ export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
|
|||||||
}, 350);
|
}, 350);
|
||||||
};
|
};
|
||||||
|
|
||||||
// const onSubmit = async (formData: FormValues) => {
|
|
||||||
// if (!workspaceSlug) return;
|
|
||||||
|
|
||||||
// return workspaceService
|
|
||||||
// .inviteWorkspace(workspaceSlug, formData, user)
|
|
||||||
|
|
||||||
// .then(async () => {
|
|
||||||
// if (onSuccess) await onSuccess();
|
|
||||||
// handleClose();
|
|
||||||
// setToastAlert({
|
|
||||||
// type: "success",
|
|
||||||
// title: "Success!",
|
|
||||||
// message: "Invitations sent successfully.",
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
// .catch((err) =>
|
|
||||||
// setToastAlert({
|
|
||||||
// type: "error",
|
|
||||||
// title: "Error!",
|
|
||||||
// message: `${err.error ?? "Something went wrong. Please try again."}`,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
||||||
const appendField = () => {
|
const appendField = () => {
|
||||||
append({ email: "", role: 15 });
|
append({ email: "", role: 15 });
|
||||||
};
|
};
|
||||||
@ -181,11 +162,14 @@ export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
|
|||||||
width="w-full"
|
width="w-full"
|
||||||
input
|
input
|
||||||
>
|
>
|
||||||
{Object.entries(ROLE).map(([key, value]) => (
|
{Object.entries(ROLE).map(([key, value]) => {
|
||||||
<CustomSelect.Option key={key} value={parseInt(key)}>
|
if (currentWorkspaceRole && currentWorkspaceRole >= parseInt(key))
|
||||||
{value}
|
return (
|
||||||
</CustomSelect.Option>
|
<CustomSelect.Option key={key} value={parseInt(key)}>
|
||||||
))}
|
{value}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</CustomSelect>
|
</CustomSelect>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -230,4 +214,4 @@ export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { useState, FC } from "react";
|
import { useState, FC } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
import { ChevronDown, Dot, XCircle } from "lucide-react";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
@ -10,12 +12,10 @@ import useToast from "hooks/use-toast";
|
|||||||
import { ConfirmWorkspaceMemberRemove } from "components/workspace";
|
import { ConfirmWorkspaceMemberRemove } from "components/workspace";
|
||||||
// ui
|
// ui
|
||||||
import { CustomSelect, Tooltip } from "@plane/ui";
|
import { CustomSelect, Tooltip } from "@plane/ui";
|
||||||
// icons
|
|
||||||
import { ChevronDown, Dot, XCircle } from "lucide-react";
|
|
||||||
// types
|
// types
|
||||||
import { TUserWorkspaceRole } from "types";
|
import { TUserWorkspaceRole } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ROLE } from "constants/workspace";
|
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
member: {
|
member: {
|
||||||
@ -33,7 +33,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
|
||||||
const { member } = props;
|
const { member } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -43,7 +43,6 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
|||||||
workspaceMember: { removeMember, updateMember, deleteWorkspaceInvitation },
|
workspaceMember: { removeMember, updateMember, deleteWorkspaceInvitation },
|
||||||
user: { currentWorkspaceMemberInfo, currentWorkspaceRole, currentUser, currentUserSettings, leaveWorkspace },
|
user: { currentWorkspaceMemberInfo, currentWorkspaceRole, currentUser, currentUserSettings, leaveWorkspace },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
const isAdmin = currentWorkspaceRole === 20;
|
|
||||||
// states
|
// states
|
||||||
const [removeMemberModal, setRemoveMemberModal] = useState(false);
|
const [removeMemberModal, setRemoveMemberModal] = useState(false);
|
||||||
// hooks
|
// hooks
|
||||||
@ -53,10 +52,7 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
|||||||
if (!workspaceSlug || !currentUserSettings) return;
|
if (!workspaceSlug || !currentUserSettings) return;
|
||||||
|
|
||||||
await leaveWorkspace(workspaceSlug.toString())
|
await leaveWorkspace(workspaceSlug.toString())
|
||||||
.then(() => {
|
.then(() => router.push("/profile"))
|
||||||
if (currentUserSettings.workspace?.invites > 0) router.push("/invitations");
|
|
||||||
else router.push("/create-workspace");
|
|
||||||
})
|
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
@ -114,6 +110,20 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
|||||||
} else await handleRemoveInvitation();
|
} else await handleRemoveInvitation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// is the member current logged in user
|
||||||
|
const isCurrentUser = member.memberId === currentWorkspaceMemberInfo?.member;
|
||||||
|
// is the current logged in user admin
|
||||||
|
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
// role change access-
|
||||||
|
// 1. user cannot change their own role
|
||||||
|
// 2. only admin or member can change role
|
||||||
|
// 3. user cannot change role of higher role
|
||||||
|
const hasRoleChangeAccess =
|
||||||
|
currentWorkspaceRole &&
|
||||||
|
!isCurrentUser &&
|
||||||
|
[EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole) &&
|
||||||
|
member.role <= currentWorkspaceRole;
|
||||||
|
|
||||||
if (!currentWorkspaceMemberInfo) return null;
|
if (!currentWorkspaceMemberInfo) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -180,12 +190,12 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
|||||||
<div className="flex item-center gap-1 px-2 py-0.5 rounded">
|
<div className="flex item-center gap-1 px-2 py-0.5 rounded">
|
||||||
<span
|
<span
|
||||||
className={`flex items-center text-xs font-medium rounded ${
|
className={`flex items-center text-xs font-medium rounded ${
|
||||||
member.memberId !== currentWorkspaceMemberInfo.member ? "" : "text-custom-sidebar-text-400"
|
hasRoleChangeAccess ? "" : "text-custom-sidebar-text-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{ROLE[member.role as keyof typeof ROLE]}
|
{ROLE[member.role as keyof typeof ROLE]}
|
||||||
</span>
|
</span>
|
||||||
{member.memberId !== currentWorkspaceMemberInfo.member && (
|
{hasRoleChangeAccess && (
|
||||||
<span className="grid place-items-center">
|
<span className="grid place-items-center">
|
||||||
<ChevronDown className="h-3 w-3" />
|
<ChevronDown className="h-3 w-3" />
|
||||||
</span>
|
</span>
|
||||||
@ -206,11 +216,7 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={!hasRoleChangeAccess}
|
||||||
member.memberId === currentWorkspaceMemberInfo.member ||
|
|
||||||
!member.status ||
|
|
||||||
Boolean(currentWorkspaceRole && currentWorkspaceRole !== 20 && currentWorkspaceRole < member.role)
|
|
||||||
}
|
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
>
|
>
|
||||||
{Object.keys(ROLE).map((key) => {
|
{Object.keys(ROLE).map((key) => {
|
||||||
@ -224,23 +230,24 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</CustomSelect>
|
</CustomSelect>
|
||||||
{isAdmin && (
|
<Tooltip
|
||||||
<Tooltip
|
tooltipContent={isCurrentUser ? "Leave workspace" : "Remove member"}
|
||||||
tooltipContent={
|
disabled={!isAdmin && !isCurrentUser}
|
||||||
member.memberId === currentWorkspaceMemberInfo.member ? "Leave workspace" : "Remove member"
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRemoveMemberModal(true)}
|
||||||
|
className={
|
||||||
|
isAdmin || isCurrentUser
|
||||||
|
? "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto"
|
||||||
|
: "opacity-0 pointer-events-none"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<button
|
<XCircle className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={2} />
|
||||||
type="button"
|
</button>
|
||||||
onClick={() => setRemoveMemberModal(true)}
|
</Tooltip>
|
||||||
className="opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto"
|
|
||||||
>
|
|
||||||
<XCircle className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={2} />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -9,18 +9,14 @@ import { WorkspaceMembersListItem } from "components/workspace";
|
|||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
|
|
||||||
export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer(({ searchQuery }) => {
|
export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer((props) => {
|
||||||
|
const { searchQuery } = props;
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
workspaceMember: {
|
workspaceMember: { workspaceMembersWithInvitations, fetchWorkspaceMemberInvitations },
|
||||||
workspaceMembers,
|
|
||||||
workspaceMembersWithInvitations,
|
|
||||||
workspaceMemberInvitations,
|
|
||||||
fetchWorkspaceMemberInvitations,
|
|
||||||
},
|
|
||||||
user: { currentWorkspaceMemberInfo },
|
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
// fetching workspace invitations
|
// fetching workspace invitations
|
||||||
useSWR(
|
useSWR(
|
||||||
@ -36,12 +32,7 @@ export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer(({ sea
|
|||||||
return `${email}${displayName}${fullName}`.includes(searchQuery.toLowerCase());
|
return `${email}${displayName}${fullName}`.includes(searchQuery.toLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (!workspaceMembersWithInvitations)
|
||||||
!workspaceMembers ||
|
|
||||||
!workspaceMemberInvitations ||
|
|
||||||
!workspaceMembersWithInvitations ||
|
|
||||||
!currentWorkspaceMemberInfo
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<Loader className="space-y-5">
|
<Loader className="space-y-5">
|
||||||
<Loader.Item height="40px" />
|
<Loader.Item height="40px" />
|
||||||
|
@ -14,12 +14,12 @@ import { DeleteWorkspaceModal } from "components/workspace";
|
|||||||
import { WorkspaceImageUploadModal } from "components/core";
|
import { WorkspaceImageUploadModal } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { Button, CustomSelect, Input, Spinner } from "@plane/ui";
|
import { Button, CustomSelect, Input, Spinner } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IWorkspace } from "types";
|
import { IWorkspace } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ORGANIZATION_SIZE } from "constants/workspace";
|
import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "constants/workspace";
|
||||||
import { trackEvent } from "helpers/event-tracker.helper";
|
|
||||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
|
||||||
|
|
||||||
const defaultValues: Partial<IWorkspace> = {
|
const defaultValues: Partial<IWorkspace> = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -40,9 +40,9 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
const {
|
const {
|
||||||
workspace: { currentWorkspace, updateWorkspace },
|
workspace: { currentWorkspace, updateWorkspace },
|
||||||
user: { currentWorkspaceRole },
|
user: { currentWorkspaceRole },
|
||||||
trackEvent: { postHogEventTracker }
|
trackEvent: { postHogEventTracker },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
const isAdmin = currentWorkspaceRole === 20;
|
|
||||||
// hooks
|
// hooks
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// form info
|
// form info
|
||||||
@ -67,28 +67,22 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
|
|
||||||
await updateWorkspace(currentWorkspace.slug, payload)
|
await updateWorkspace(currentWorkspace.slug, payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
postHogEventTracker(
|
postHogEventTracker("WORKSPACE_UPDATE", {
|
||||||
'WORKSPACE_UPDATE',
|
...res,
|
||||||
{
|
state: "SUCCESS",
|
||||||
...res,
|
});
|
||||||
state: "SUCCESS"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Workspace updated successfully",
|
message: "Workspace updated successfully",
|
||||||
});
|
});
|
||||||
}).catch((err) => {
|
})
|
||||||
postHogEventTracker(
|
.catch((err) => {
|
||||||
'WORKSPACE_UPDATE',
|
postHogEventTracker("WORKSPACE_UPDATE", {
|
||||||
{
|
state: "FAILED",
|
||||||
state: "FAILED"
|
});
|
||||||
}
|
console.error(err);
|
||||||
);
|
});
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveLogo = () => {
|
const handleRemoveLogo = () => {
|
||||||
@ -136,6 +130,8 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
if (currentWorkspace) reset({ ...currentWorkspace });
|
if (currentWorkspace) reset({ ...currentWorkspace });
|
||||||
}, [currentWorkspace, reset]);
|
}, [currentWorkspace, reset]);
|
||||||
|
|
||||||
|
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
|
||||||
if (!currentWorkspace)
|
if (!currentWorkspace)
|
||||||
return (
|
return (
|
||||||
<div className="grid place-items-center h-full w-full px-4 sm:px-0">
|
<div className="grid place-items-center h-full w-full px-4 sm:px-0">
|
||||||
@ -192,11 +188,10 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
<button type="button" onClick={handleCopyUrl} className="text-sm tracking-tight">{`${
|
<button type="button" onClick={handleCopyUrl} className="text-sm tracking-tight">{`${
|
||||||
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
|
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
|
||||||
}/${currentWorkspace.slug}`}</button>
|
}/${currentWorkspace.slug}`}</button>
|
||||||
<div className="flex item-center gap-2.5">
|
{isAdmin && (
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
|
className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
|
||||||
onClick={() => setIsImageUploadModalOpen(true)}
|
onClick={() => setIsImageUploadModalOpen(true)}
|
||||||
disabled={!isAdmin}
|
|
||||||
>
|
>
|
||||||
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
|
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
|
||||||
<>
|
<>
|
||||||
@ -207,14 +202,14 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
"Upload logo"
|
"Upload logo"
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-8 my-10">
|
<div className="flex flex-col gap-8 my-10">
|
||||||
<div className="grid grid-col grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-10 w-full">
|
<div className="grid grid-col grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-10 w-full">
|
||||||
<div className="flex flex-col gap-1 ">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm">Workspace Name</h4>
|
<h4 className="text-sm">Workspace name</h4>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="name"
|
name="name"
|
||||||
@ -243,7 +238,7 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1 ">
|
<div className="flex flex-col gap-1 ">
|
||||||
<h4 className="text-sm">Company Size</h4>
|
<h4 className="text-sm">Company size</h4>
|
||||||
<Controller
|
<Controller
|
||||||
name="organization_size"
|
name="organization_size"
|
||||||
control={control}
|
control={control}
|
||||||
@ -277,9 +272,10 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
id="url"
|
id="url"
|
||||||
name="url"
|
name="url"
|
||||||
type="url"
|
type="url"
|
||||||
value={`${typeof window !== "undefined" &&
|
value={`${
|
||||||
|
typeof window !== "undefined" &&
|
||||||
window.location.origin.replace("http://", "").replace("https://", "")
|
window.location.origin.replace("http://", "").replace("https://", "")
|
||||||
}/${currentWorkspace.slug}`}
|
}/${currentWorkspace.slug}`}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.url)}
|
hasError={Boolean(errors.url)}
|
||||||
@ -291,11 +287,13 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between py-2">
|
{isAdmin && (
|
||||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isAdmin}>
|
<div className="flex items-center justify-between py-2">
|
||||||
{isSubmitting ? "Updating..." : "Update Workspace"}
|
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||||
</Button>
|
{isSubmitting ? "Updating..." : "Update Workspace"}
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<Disclosure as="div" className="border-t border-custom-border-100">
|
<Disclosure as="div" className="border-t border-custom-border-100">
|
||||||
|
@ -4,8 +4,16 @@ import JiraLogo from "public/services/jira.svg";
|
|||||||
import CSVLogo from "public/services/csv.svg";
|
import CSVLogo from "public/services/csv.svg";
|
||||||
import ExcelLogo from "public/services/excel.svg";
|
import ExcelLogo from "public/services/excel.svg";
|
||||||
import JSONLogo from "public/services/json.svg";
|
import JSONLogo from "public/services/json.svg";
|
||||||
|
// types
|
||||||
import { TStaticViewTypes } from "types";
|
import { TStaticViewTypes } from "types";
|
||||||
|
|
||||||
|
export enum EUserWorkspaceRoles {
|
||||||
|
GUEST = 5,
|
||||||
|
VIEWER = 10,
|
||||||
|
MEMBER = 15,
|
||||||
|
ADMIN = 20,
|
||||||
|
}
|
||||||
|
|
||||||
export const ROLE = {
|
export const ROLE = {
|
||||||
5: "Guest",
|
5: "Guest",
|
||||||
10: "Viewer",
|
10: "Viewer",
|
||||||
@ -105,3 +113,50 @@ export const RESTRICTED_URLS = [
|
|||||||
"spaces",
|
"spaces",
|
||||||
"workspace-invitations",
|
"workspace-invitations",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const WORKSPACE_SETTINGS_LINKS: {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
access: EUserWorkspaceRoles;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
label: "General",
|
||||||
|
href: `/settings`,
|
||||||
|
access: EUserWorkspaceRoles.GUEST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Members",
|
||||||
|
href: `/settings/members`,
|
||||||
|
access: EUserWorkspaceRoles.GUEST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Billing and plans",
|
||||||
|
href: `/settings/billing`,
|
||||||
|
access: EUserWorkspaceRoles.ADMIN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Integrations",
|
||||||
|
href: `/settings/integrations`,
|
||||||
|
access: EUserWorkspaceRoles.ADMIN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Imports",
|
||||||
|
href: `/settings/imports`,
|
||||||
|
access: EUserWorkspaceRoles.ADMIN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Exports",
|
||||||
|
href: `/settings/exports`,
|
||||||
|
access: EUserWorkspaceRoles.MEMBER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Webhooks",
|
||||||
|
href: `/settings/webhooks`,
|
||||||
|
access: EUserWorkspaceRoles.ADMIN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "API tokens",
|
||||||
|
href: `/settings/api-tokens`,
|
||||||
|
access: EUserWorkspaceRoles.ADMIN,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@ -1,82 +1,35 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { RootStore } from "store/root";
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// constants
|
||||||
export enum EUserWorkspaceRoles {
|
import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_LINKS } from "constants/workspace";
|
||||||
GUEST = 5,
|
|
||||||
MEMBER = 15,
|
|
||||||
ADMIN = 20,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WorkspaceSettingsSidebar = () => {
|
export const WorkspaceSettingsSidebar = () => {
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
const { user: userStore }: RootStore = useMobxStore();
|
// mobx store
|
||||||
|
const {
|
||||||
|
user: { currentWorkspaceRole },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const workspaceMemberInfo = userStore.currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
|
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
|
||||||
|
|
||||||
const workspaceLinks: Array<{
|
|
||||||
label: string;
|
|
||||||
href: string;
|
|
||||||
access: EUserWorkspaceRoles;
|
|
||||||
}> = [
|
|
||||||
{
|
|
||||||
label: "General",
|
|
||||||
href: `/${workspaceSlug}/settings`,
|
|
||||||
access: EUserWorkspaceRoles.GUEST,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Members",
|
|
||||||
href: `/${workspaceSlug}/settings/members`,
|
|
||||||
access: EUserWorkspaceRoles.GUEST,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Billing and plans",
|
|
||||||
href: `/${workspaceSlug}/settings/billing`,
|
|
||||||
access: EUserWorkspaceRoles.ADMIN,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Integrations",
|
|
||||||
href: `/${workspaceSlug}/settings/integrations`,
|
|
||||||
access: EUserWorkspaceRoles.ADMIN,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Imports",
|
|
||||||
href: `/${workspaceSlug}/settings/imports`,
|
|
||||||
access: EUserWorkspaceRoles.ADMIN,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Exports",
|
|
||||||
href: `/${workspaceSlug}/settings/exports`,
|
|
||||||
access: EUserWorkspaceRoles.MEMBER,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Webhooks",
|
|
||||||
href: `/${workspaceSlug}/settings/webhooks`,
|
|
||||||
access: EUserWorkspaceRoles.ADMIN,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "API tokens",
|
|
||||||
href: `/${workspaceSlug}/settings/api-tokens`,
|
|
||||||
access: EUserWorkspaceRoles.ADMIN,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 w-80 px-5">
|
<div className="flex flex-col gap-6 w-80 px-5">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span>
|
<span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span>
|
||||||
<div className="flex flex-col gap-1 w-full">
|
<div className="flex flex-col gap-1 w-full">
|
||||||
{workspaceLinks.map(
|
{WORKSPACE_SETTINGS_LINKS.map(
|
||||||
(link) =>
|
(link) =>
|
||||||
workspaceMemberInfo >= link.access && (
|
workspaceMemberInfo >= link.access && (
|
||||||
<Link key={link.href} href={link.href}>
|
<Link key={link.href} href={`/${workspaceSlug}/${link.href}`}>
|
||||||
<a>
|
<a>
|
||||||
<div
|
<div
|
||||||
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
||||||
router.pathname.split("/")?.[3] === link.href.split("/")?.[3]
|
router.pathname.split("/")?.[3] === link.href.split("/")?.[2]
|
||||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||||
}`}
|
}`}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
@ -15,8 +18,7 @@ import { APITokenService } from "services/api_token.service";
|
|||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
// constants
|
// constants
|
||||||
import { API_TOKENS_LIST } from "constants/fetch-keys";
|
import { API_TOKENS_LIST } from "constants/fetch-keys";
|
||||||
import { observer } from "mobx-react-lite";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
|
|
||||||
const apiTokenService = new APITokenService();
|
const apiTokenService = new APITokenService();
|
||||||
|
|
||||||
@ -31,7 +33,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
|||||||
user: { currentWorkspaceRole },
|
user: { currentWorkspaceRole },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const isAdmin = currentWorkspaceRole === 20;
|
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
|
||||||
const { data: tokens } = useSWR(workspaceSlug && isAdmin ? API_TOKENS_LIST(workspaceSlug.toString()) : null, () =>
|
const { data: tokens } = useSWR(workspaceSlug && isAdmin ? API_TOKENS_LIST(workspaceSlug.toString()) : null, () =>
|
||||||
workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null
|
workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { ReactElement } from "react";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
@ -8,27 +10,44 @@ import { WorkspaceSettingHeader } from "components/headers";
|
|||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
|
// constants
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
const BillingSettingsPage: NextPageWithLayout = () => (
|
const BillingSettingsPage: NextPageWithLayout = observer(() => {
|
||||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
const {
|
||||||
<div>
|
user: { currentWorkspaceRole },
|
||||||
<div className="flex items-center py-3.5 border-b border-custom-border-100">
|
} = useMobxStore();
|
||||||
<h3 className="text-xl font-medium">Billing & Plans</h3>
|
|
||||||
|
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
|
||||||
|
if (!isAdmin)
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full flex justify-center mt-10 p-4">
|
||||||
|
<p className="text-custom-text-300 text-sm">You are not authorized to access this page.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
<div className="px-4 py-6">
|
|
||||||
|
return (
|
||||||
|
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-md mb-1 leading-6">Current plan</h4>
|
<div className="flex items-center py-3.5 border-b border-custom-border-100">
|
||||||
<p className="mb-3 text-sm text-custom-text-200">You are currently using the free plan</p>
|
<h3 className="text-xl font-medium">Billing & Plans</h3>
|
||||||
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
|
</div>
|
||||||
<Button variant="neutral-primary">View Plans</Button>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="px-4 py-6">
|
||||||
</section>
|
<div>
|
||||||
);
|
<h4 className="text-md mb-1 leading-6">Current plan</h4>
|
||||||
|
<p className="mb-3 text-sm text-custom-text-200">You are currently using the free plan</p>
|
||||||
|
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
|
||||||
|
<Button variant="neutral-primary">View Plans</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
BillingSettingsPage.getLayout = function getLayout(page: ReactElement) {
|
BillingSettingsPage.getLayout = function getLayout(page: React.ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<WorkspaceSettingHeader title="Billing & Plans Settings" />}>
|
<AppLayout header={<WorkspaceSettingHeader title="Billing & Plans Settings" />}>
|
||||||
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
|
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { ReactElement } from "react";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// layout
|
// layout
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
@ -7,17 +9,35 @@ import { WorkspaceSettingHeader } from "components/headers";
|
|||||||
import ExportGuide from "components/exporter/guide";
|
import ExportGuide from "components/exporter/guide";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
|
// constants
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
const ExportsPage: NextPageWithLayout = () => (
|
const ExportsPage: NextPageWithLayout = observer(() => {
|
||||||
<div className="pr-9 py-8 w-full overflow-y-auto">
|
const {
|
||||||
<div className="flex items-center py-3.5 border-b border-custom-border-100">
|
user: { currentWorkspaceRole },
|
||||||
<h3 className="text-xl font-medium">Exports</h3>
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const hasPageAccess =
|
||||||
|
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
|
||||||
|
|
||||||
|
if (!hasPageAccess)
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full flex justify-center mt-10 p-4">
|
||||||
|
<p className="text-custom-text-300 text-sm">You are not authorized to access this page.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pr-9 py-8 w-full overflow-y-auto">
|
||||||
|
<div className="flex items-center py-3.5 border-b border-custom-border-100">
|
||||||
|
<h3 className="text-xl font-medium">Exports</h3>
|
||||||
|
</div>
|
||||||
|
<ExportGuide />
|
||||||
</div>
|
</div>
|
||||||
<ExportGuide />
|
);
|
||||||
</div>
|
});
|
||||||
);
|
|
||||||
|
|
||||||
ExportsPage.getLayout = function getLayout(page: ReactElement) {
|
ExportsPage.getLayout = function getLayout(page: React.ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<WorkspaceSettingHeader title="Export Settings" />}>
|
<AppLayout header={<WorkspaceSettingHeader title="Export Settings" />}>
|
||||||
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
|
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { ReactElement } from "react";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// layouts
|
// layouts
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
@ -7,17 +9,34 @@ import IntegrationGuide from "components/integration/guide";
|
|||||||
import { WorkspaceSettingHeader } from "components/headers";
|
import { WorkspaceSettingHeader } from "components/headers";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
|
// constants
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
const ImportsPage: NextPageWithLayout = () => (
|
const ImportsPage: NextPageWithLayout = observer(() => {
|
||||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
const {
|
||||||
<div className="flex items-center py-3.5 border-b border-custom-border-100">
|
user: { currentWorkspaceRole },
|
||||||
<h3 className="text-xl font-medium">Imports</h3>
|
} = useMobxStore();
|
||||||
</div>
|
|
||||||
<IntegrationGuide />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
|
|
||||||
ImportsPage.getLayout = function getLayout(page: ReactElement) {
|
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
|
||||||
|
if (!isAdmin)
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full flex justify-center mt-10 p-4">
|
||||||
|
<p className="text-custom-text-300 text-sm">You are not authorized to access this page.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||||
|
<div className="flex items-center py-3.5 border-b border-custom-border-100">
|
||||||
|
<h3 className="text-xl font-medium">Imports</h3>
|
||||||
|
</div>
|
||||||
|
<IntegrationGuide />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImportsPage.getLayout = function getLayout(page: React.ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<WorkspaceSettingHeader title="Import Settings" />}>
|
<AppLayout header={<WorkspaceSettingHeader title="Import Settings" />}>
|
||||||
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
|
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
// services
|
||||||
import { IntegrationService } from "services/integrations";
|
import { IntegrationService } from "services/integrations";
|
||||||
// layouts
|
// layouts
|
||||||
@ -16,16 +19,31 @@ import { Loader } from "@plane/ui";
|
|||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { APP_INTEGRATIONS } from "constants/fetch-keys";
|
import { APP_INTEGRATIONS } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
// services
|
|
||||||
const integrationService = new IntegrationService();
|
const integrationService = new IntegrationService();
|
||||||
|
|
||||||
const WorkspaceIntegrationsPage: NextPageWithLayout = () => {
|
const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => {
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
// mobx store
|
||||||
|
const {
|
||||||
|
user: { currentWorkspaceRole },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const { data: appIntegrations } = useSWR(workspaceSlug ? APP_INTEGRATIONS : null, () =>
|
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||||
workspaceSlug ? integrationService.getAppIntegrationsList() : null
|
|
||||||
|
if (!isAdmin)
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full flex justify-center mt-10 p-4">
|
||||||
|
<p className="text-custom-text-300 text-sm">You are not authorized to access this page.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: appIntegrations } = useSWR(workspaceSlug && isAdmin ? APP_INTEGRATIONS : null, () =>
|
||||||
|
workspaceSlug && isAdmin ? integrationService.getAppIntegrationsList() : null
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -43,7 +61,7 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
WorkspaceIntegrationsPage.getLayout = function getLayout(page: ReactElement) {
|
WorkspaceIntegrationsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { useState, ReactElement } from "react";
|
import { useState, ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Search } from "lucide-react";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
@ -12,21 +14,20 @@ import { WorkspaceSettingHeader } from "components/headers";
|
|||||||
import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace";
|
import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// icons
|
|
||||||
import { Search } from "lucide-react";
|
|
||||||
// helpers
|
|
||||||
import { trackEvent } from "helpers/event-tracker.helper";
|
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
import { IWorkspaceBulkInviteFormData } from "types";
|
import { IWorkspaceBulkInviteFormData } from "types";
|
||||||
|
// constants
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
|
user: { currentWorkspaceRole },
|
||||||
workspaceMember: { inviteMembersToWorkspace },
|
workspaceMember: { inviteMembersToWorkspace },
|
||||||
trackEvent: { postHogEventTracker, setTrackElement }
|
trackEvent: { postHogEventTracker, setTrackElement },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
// states
|
// states
|
||||||
const [inviteModal, setInviteModal] = useState(false);
|
const [inviteModal, setInviteModal] = useState(false);
|
||||||
@ -57,15 +58,16 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasAddMemberPermission =
|
||||||
|
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{workspaceSlug && (
|
<SendWorkspaceInvitationModal
|
||||||
<SendWorkspaceInvitationModal
|
isOpen={inviteModal}
|
||||||
isOpen={inviteModal}
|
onClose={() => setInviteModal(false)}
|
||||||
onClose={() => setInviteModal(false)}
|
onSubmit={handleWorkspaceInvite}
|
||||||
onSubmit={handleWorkspaceInvite}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||||
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-100">
|
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-100">
|
||||||
<h4 className="text-xl font-medium">Members</h4>
|
<h4 className="text-xl font-medium">Members</h4>
|
||||||
@ -79,13 +81,18 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="primary" size="sm" onClick={() => {
|
{hasAddMemberPermission && (
|
||||||
setTrackElement("WORKSPACE_SETTINGS_MEMBERS_PAGE_HEADER");
|
<Button
|
||||||
setInviteModal(true)
|
variant="primary"
|
||||||
}
|
size="sm"
|
||||||
}>
|
onClick={() => {
|
||||||
Add Member
|
setTrackElement("WORKSPACE_SETTINGS_MEMBERS_PAGE_HEADER");
|
||||||
</Button>
|
setInviteModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add member
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<WorkspaceMembersList searchQuery={searchQuery} />
|
<WorkspaceMembersList searchQuery={searchQuery} />
|
||||||
</section>
|
</section>
|
||||||
|
@ -88,7 +88,8 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
|||||||
* computed value provides the members information including the invitations.
|
* computed value provides the members information including the invitations.
|
||||||
*/
|
*/
|
||||||
get workspaceMembersWithInvitations() {
|
get workspaceMembersWithInvitations() {
|
||||||
if (!this.workspaceMembers || !this.workspaceMemberInvitations) return null;
|
if (!this.workspaceMembers) return null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...(this.workspaceMemberInvitations?.map((item) => ({
|
...(this.workspaceMemberInvitations?.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
2
web/types/workspace.d.ts
vendored
2
web/types/workspace.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
import type { IProjectMember, IUser, IUserLite, IUserMemberLite, IWorkspaceViewProps } from "types";
|
import type { IProjectMember, IUser, IUserLite, IWorkspaceViewProps } from "types";
|
||||||
|
|
||||||
export type TUserWorkspaceRole = 5 | 10 | 15 | 20;
|
export type TUserWorkspaceRole = 5 | 10 | 15 | 20;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user