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,
|
||||
IViewIssuesStore,
|
||||
} from "store/issues";
|
||||
import { EUserWorkspaceRoles } from "layouts/settings-layout/workspace/sidebar";
|
||||
import { TUnGroupedIssues } from "store/issues/types";
|
||||
|
||||
interface IBaseGanttRoot {
|
||||
@ -46,6 +45,10 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const {
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const appliedDisplayFilters = issueFiltersStore.issueFilters?.displayFilters;
|
||||
|
||||
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 (
|
||||
<>
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Plus, X } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { Button, CustomSelect, Input } from "@plane/ui";
|
||||
// icons
|
||||
import { Plus, X } from "lucide-react";
|
||||
// types
|
||||
import { IWorkspaceBulkInviteFormData, TUserWorkspaceRole } from "types";
|
||||
// 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;
|
||||
|
||||
// mobx store
|
||||
const {
|
||||
user: { currentWorkspaceRole },
|
||||
} = useMobxStore();
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
@ -59,30 +64,6 @@ export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
|
||||
}, 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 = () => {
|
||||
append({ email: "", role: 15 });
|
||||
};
|
||||
@ -181,11 +162,14 @@ export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
|
||||
width="w-full"
|
||||
input
|
||||
>
|
||||
{Object.entries(ROLE).map(([key, value]) => (
|
||||
<CustomSelect.Option key={key} value={parseInt(key)}>
|
||||
{value}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
{Object.entries(ROLE).map(([key, value]) => {
|
||||
if (currentWorkspaceRole && currentWorkspaceRole >= parseInt(key))
|
||||
return (
|
||||
<CustomSelect.Option key={key} value={parseInt(key)}>
|
||||
{value}
|
||||
</CustomSelect.Option>
|
||||
);
|
||||
})}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
@ -230,4 +214,4 @@ export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { useState, FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { mutate } from "swr";
|
||||
import { ChevronDown, Dot, XCircle } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
@ -10,12 +12,10 @@ import useToast from "hooks/use-toast";
|
||||
import { ConfirmWorkspaceMemberRemove } from "components/workspace";
|
||||
// ui
|
||||
import { CustomSelect, Tooltip } from "@plane/ui";
|
||||
// icons
|
||||
import { ChevronDown, Dot, XCircle } from "lucide-react";
|
||||
// types
|
||||
import { TUserWorkspaceRole } from "types";
|
||||
// constants
|
||||
import { ROLE } from "constants/workspace";
|
||||
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
|
||||
|
||||
type Props = {
|
||||
member: {
|
||||
@ -33,7 +33,7 @@ type Props = {
|
||||
};
|
||||
};
|
||||
|
||||
export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
||||
export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
|
||||
const { member } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
@ -43,7 +43,6 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
||||
workspaceMember: { removeMember, updateMember, deleteWorkspaceInvitation },
|
||||
user: { currentWorkspaceMemberInfo, currentWorkspaceRole, currentUser, currentUserSettings, leaveWorkspace },
|
||||
} = useMobxStore();
|
||||
const isAdmin = currentWorkspaceRole === 20;
|
||||
// states
|
||||
const [removeMemberModal, setRemoveMemberModal] = useState(false);
|
||||
// hooks
|
||||
@ -53,10 +52,7 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
||||
if (!workspaceSlug || !currentUserSettings) return;
|
||||
|
||||
await leaveWorkspace(workspaceSlug.toString())
|
||||
.then(() => {
|
||||
if (currentUserSettings.workspace?.invites > 0) router.push("/invitations");
|
||||
else router.push("/create-workspace");
|
||||
})
|
||||
.then(() => router.push("/profile"))
|
||||
.catch((err) =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
@ -114,6 +110,20 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
||||
} 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;
|
||||
|
||||
return (
|
||||
@ -180,12 +190,12 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
||||
<div className="flex item-center gap-1 px-2 py-0.5 rounded">
|
||||
<span
|
||||
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]}
|
||||
</span>
|
||||
{member.memberId !== currentWorkspaceMemberInfo.member && (
|
||||
{hasRoleChangeAccess && (
|
||||
<span className="grid place-items-center">
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</span>
|
||||
@ -206,11 +216,7 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
||||
});
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
member.memberId === currentWorkspaceMemberInfo.member ||
|
||||
!member.status ||
|
||||
Boolean(currentWorkspaceRole && currentWorkspaceRole !== 20 && currentWorkspaceRole < member.role)
|
||||
}
|
||||
disabled={!hasRoleChangeAccess}
|
||||
placement="bottom-end"
|
||||
>
|
||||
{Object.keys(ROLE).map((key) => {
|
||||
@ -224,23 +230,24 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
|
||||
);
|
||||
})}
|
||||
</CustomSelect>
|
||||
{isAdmin && (
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
member.memberId === currentWorkspaceMemberInfo.member ? "Leave workspace" : "Remove member"
|
||||
<Tooltip
|
||||
tooltipContent={isCurrentUser ? "Leave workspace" : "Remove member"}
|
||||
disabled={!isAdmin && !isCurrentUser}
|
||||
>
|
||||
<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
|
||||
type="button"
|
||||
onClick={() => setRemoveMemberModal(true)}
|
||||
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>
|
||||
)}
|
||||
<XCircle className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={2} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -9,18 +9,14 @@ import { WorkspaceMembersListItem } from "components/workspace";
|
||||
// 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 { workspaceSlug } = router.query;
|
||||
// store
|
||||
const {
|
||||
workspaceMember: {
|
||||
workspaceMembers,
|
||||
workspaceMembersWithInvitations,
|
||||
workspaceMemberInvitations,
|
||||
fetchWorkspaceMemberInvitations,
|
||||
},
|
||||
user: { currentWorkspaceMemberInfo },
|
||||
workspaceMember: { workspaceMembersWithInvitations, fetchWorkspaceMemberInvitations },
|
||||
} = useMobxStore();
|
||||
// fetching workspace invitations
|
||||
useSWR(
|
||||
@ -36,12 +32,7 @@ export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer(({ sea
|
||||
return `${email}${displayName}${fullName}`.includes(searchQuery.toLowerCase());
|
||||
});
|
||||
|
||||
if (
|
||||
!workspaceMembers ||
|
||||
!workspaceMemberInvitations ||
|
||||
!workspaceMembersWithInvitations ||
|
||||
!currentWorkspaceMemberInfo
|
||||
)
|
||||
if (!workspaceMembersWithInvitations)
|
||||
return (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
|
@ -14,12 +14,12 @@ import { DeleteWorkspaceModal } from "components/workspace";
|
||||
import { WorkspaceImageUploadModal } from "components/core";
|
||||
// ui
|
||||
import { Button, CustomSelect, Input, Spinner } from "@plane/ui";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IWorkspace } from "types";
|
||||
// constants
|
||||
import { ORGANIZATION_SIZE } from "constants/workspace";
|
||||
import { trackEvent } from "helpers/event-tracker.helper";
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "constants/workspace";
|
||||
|
||||
const defaultValues: Partial<IWorkspace> = {
|
||||
name: "",
|
||||
@ -40,9 +40,9 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
const {
|
||||
workspace: { currentWorkspace, updateWorkspace },
|
||||
user: { currentWorkspaceRole },
|
||||
trackEvent: { postHogEventTracker }
|
||||
trackEvent: { postHogEventTracker },
|
||||
} = useMobxStore();
|
||||
const isAdmin = currentWorkspaceRole === 20;
|
||||
|
||||
// hooks
|
||||
const { setToastAlert } = useToast();
|
||||
// form info
|
||||
@ -67,28 +67,22 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
|
||||
await updateWorkspace(currentWorkspace.slug, payload)
|
||||
.then((res) => {
|
||||
postHogEventTracker(
|
||||
'WORKSPACE_UPDATE',
|
||||
{
|
||||
...res,
|
||||
state: "SUCCESS"
|
||||
}
|
||||
)
|
||||
postHogEventTracker("WORKSPACE_UPDATE", {
|
||||
...res,
|
||||
state: "SUCCESS",
|
||||
});
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Workspace updated successfully",
|
||||
});
|
||||
}).catch((err) => {
|
||||
postHogEventTracker(
|
||||
'WORKSPACE_UPDATE',
|
||||
{
|
||||
state: "FAILED"
|
||||
}
|
||||
);
|
||||
console.error(err)
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
postHogEventTracker("WORKSPACE_UPDATE", {
|
||||
state: "FAILED",
|
||||
});
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveLogo = () => {
|
||||
@ -136,6 +130,8 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
if (currentWorkspace) reset({ ...currentWorkspace });
|
||||
}, [currentWorkspace, reset]);
|
||||
|
||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
|
||||
if (!currentWorkspace)
|
||||
return (
|
||||
<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">{`${
|
||||
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
|
||||
}/${currentWorkspace.slug}`}</button>
|
||||
<div className="flex item-center gap-2.5">
|
||||
{isAdmin && (
|
||||
<button
|
||||
className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
|
||||
onClick={() => setIsImageUploadModalOpen(true)}
|
||||
disabled={!isAdmin}
|
||||
>
|
||||
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
|
||||
<>
|
||||
@ -207,14 +202,14 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
"Upload logo"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Workspace Name</h4>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Workspace name</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
@ -243,7 +238,7 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Company Size</h4>
|
||||
<h4 className="text-sm">Company size</h4>
|
||||
<Controller
|
||||
name="organization_size"
|
||||
control={control}
|
||||
@ -277,9 +272,10 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
id="url"
|
||||
name="url"
|
||||
type="url"
|
||||
value={`${typeof window !== "undefined" &&
|
||||
value={`${
|
||||
typeof window !== "undefined" &&
|
||||
window.location.origin.replace("http://", "").replace("https://", "")
|
||||
}/${currentWorkspace.slug}`}
|
||||
}/${currentWorkspace.slug}`}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.url)}
|
||||
@ -291,11 +287,13 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isAdmin}>
|
||||
{isSubmitting ? "Updating..." : "Update Workspace"}
|
||||
</Button>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Updating..." : "Update Workspace"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<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 ExcelLogo from "public/services/excel.svg";
|
||||
import JSONLogo from "public/services/json.svg";
|
||||
// types
|
||||
import { TStaticViewTypes } from "types";
|
||||
|
||||
export enum EUserWorkspaceRoles {
|
||||
GUEST = 5,
|
||||
VIEWER = 10,
|
||||
MEMBER = 15,
|
||||
ADMIN = 20,
|
||||
}
|
||||
|
||||
export const ROLE = {
|
||||
5: "Guest",
|
||||
10: "Viewer",
|
||||
@ -105,3 +113,50 @@ export const RESTRICTED_URLS = [
|
||||
"spaces",
|
||||
"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 { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import { RootStore } from "store/root";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
export enum EUserWorkspaceRoles {
|
||||
GUEST = 5,
|
||||
MEMBER = 15,
|
||||
ADMIN = 20,
|
||||
}
|
||||
// constants
|
||||
import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_LINKS } from "constants/workspace";
|
||||
|
||||
export const WorkspaceSettingsSidebar = () => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { user: userStore }: RootStore = useMobxStore();
|
||||
// mobx store
|
||||
const {
|
||||
user: { currentWorkspaceRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const workspaceMemberInfo = userStore.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,
|
||||
},
|
||||
];
|
||||
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 w-80 px-5">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
{workspaceLinks.map(
|
||||
{WORKSPACE_SETTINGS_LINKS.map(
|
||||
(link) =>
|
||||
workspaceMemberInfo >= link.access && (
|
||||
<Link key={link.href} href={link.href}>
|
||||
<Link key={link.href} href={`/${workspaceSlug}/${link.href}`}>
|
||||
<a>
|
||||
<div
|
||||
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"
|
||||
: "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 { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
@ -15,8 +18,7 @@ import { APITokenService } from "services/api_token.service";
|
||||
import { NextPageWithLayout } from "types/app";
|
||||
// constants
|
||||
import { API_TOKENS_LIST } from "constants/fetch-keys";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
const apiTokenService = new APITokenService();
|
||||
|
||||
@ -31,7 +33,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
||||
user: { currentWorkspaceRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const isAdmin = currentWorkspaceRole === 20;
|
||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
|
||||
const { data: tokens } = useSWR(workspaceSlug && isAdmin ? API_TOKENS_LIST(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
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
@ -8,27 +10,44 @@ import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import { NextPageWithLayout } from "types/app";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
const BillingSettingsPage: NextPageWithLayout = () => (
|
||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||
<div>
|
||||
<div className="flex items-center py-3.5 border-b border-custom-border-100">
|
||||
<h3 className="text-xl font-medium">Billing & Plans</h3>
|
||||
const BillingSettingsPage: NextPageWithLayout = observer(() => {
|
||||
const {
|
||||
user: { currentWorkspaceRole },
|
||||
} = useMobxStore();
|
||||
|
||||
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 className="px-4 py-6">
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||
<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 className="flex items-center py-3.5 border-b border-custom-border-100">
|
||||
<h3 className="text-xl font-medium">Billing & Plans</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
<div className="px-4 py-6">
|
||||
<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 (
|
||||
<AppLayout header={<WorkspaceSettingHeader title="Billing & Plans Settings" />}>
|
||||
<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
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
@ -7,17 +9,35 @@ import { WorkspaceSettingHeader } from "components/headers";
|
||||
import ExportGuide from "components/exporter/guide";
|
||||
// types
|
||||
import { NextPageWithLayout } from "types/app";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
const ExportsPage: NextPageWithLayout = () => (
|
||||
<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>
|
||||
const ExportsPage: NextPageWithLayout = observer(() => {
|
||||
const {
|
||||
user: { currentWorkspaceRole },
|
||||
} = 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>
|
||||
<ExportGuide />
|
||||
</div>
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
ExportsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
ExportsPage.getLayout = function getLayout(page: React.ReactElement) {
|
||||
return (
|
||||
<AppLayout header={<WorkspaceSettingHeader title="Export Settings" />}>
|
||||
<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
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
@ -7,17 +9,34 @@ import IntegrationGuide from "components/integration/guide";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
// types
|
||||
import { NextPageWithLayout } from "types/app";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
const ImportsPage: NextPageWithLayout = () => (
|
||||
<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>
|
||||
);
|
||||
const ImportsPage: NextPageWithLayout = observer(() => {
|
||||
const {
|
||||
user: { currentWorkspaceRole },
|
||||
} = useMobxStore();
|
||||
|
||||
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 (
|
||||
<AppLayout header={<WorkspaceSettingHeader title="Import Settings" />}>
|
||||
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { IntegrationService } from "services/integrations";
|
||||
// layouts
|
||||
@ -16,16 +19,31 @@ import { Loader } from "@plane/ui";
|
||||
import { NextPageWithLayout } from "types/app";
|
||||
// fetch-keys
|
||||
import { APP_INTEGRATIONS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
// services
|
||||
const integrationService = new IntegrationService();
|
||||
|
||||
const WorkspaceIntegrationsPage: NextPageWithLayout = () => {
|
||||
const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// mobx store
|
||||
const {
|
||||
user: { currentWorkspaceRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const { data: appIntegrations } = useSWR(workspaceSlug ? APP_INTEGRATIONS : null, () =>
|
||||
workspaceSlug ? integrationService.getAppIntegrationsList() : null
|
||||
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>
|
||||
);
|
||||
|
||||
const { data: appIntegrations } = useSWR(workspaceSlug && isAdmin ? APP_INTEGRATIONS : null, () =>
|
||||
workspaceSlug && isAdmin ? integrationService.getAppIntegrationsList() : null
|
||||
);
|
||||
|
||||
return (
|
||||
@ -43,7 +61,7 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = () => {
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
WorkspaceIntegrationsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { useState, ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Search } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
@ -12,21 +14,20 @@ import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { Search } from "lucide-react";
|
||||
// helpers
|
||||
import { trackEvent } from "helpers/event-tracker.helper";
|
||||
// types
|
||||
import { NextPageWithLayout } from "types/app";
|
||||
import { IWorkspaceBulkInviteFormData } from "types";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// store
|
||||
const {
|
||||
user: { currentWorkspaceRole },
|
||||
workspaceMember: { inviteMembersToWorkspace },
|
||||
trackEvent: { postHogEventTracker, setTrackElement }
|
||||
trackEvent: { postHogEventTracker, setTrackElement },
|
||||
} = useMobxStore();
|
||||
// states
|
||||
const [inviteModal, setInviteModal] = useState(false);
|
||||
@ -57,15 +58,16 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const hasAddMemberPermission =
|
||||
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
{workspaceSlug && (
|
||||
<SendWorkspaceInvitationModal
|
||||
isOpen={inviteModal}
|
||||
onClose={() => setInviteModal(false)}
|
||||
onSubmit={handleWorkspaceInvite}
|
||||
/>
|
||||
)}
|
||||
<SendWorkspaceInvitationModal
|
||||
isOpen={inviteModal}
|
||||
onClose={() => setInviteModal(false)}
|
||||
onSubmit={handleWorkspaceInvite}
|
||||
/>
|
||||
<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">
|
||||
<h4 className="text-xl font-medium">Members</h4>
|
||||
@ -79,13 +81,18 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="primary" size="sm" onClick={() => {
|
||||
setTrackElement("WORKSPACE_SETTINGS_MEMBERS_PAGE_HEADER");
|
||||
setInviteModal(true)
|
||||
}
|
||||
}>
|
||||
Add Member
|
||||
</Button>
|
||||
{hasAddMemberPermission && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setTrackElement("WORKSPACE_SETTINGS_MEMBERS_PAGE_HEADER");
|
||||
setInviteModal(true);
|
||||
}}
|
||||
>
|
||||
Add member
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<WorkspaceMembersList searchQuery={searchQuery} />
|
||||
</section>
|
||||
|
@ -88,7 +88,8 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
* computed value provides the members information including the invitations.
|
||||
*/
|
||||
get workspaceMembersWithInvitations() {
|
||||
if (!this.workspaceMembers || !this.workspaceMemberInvitations) return null;
|
||||
if (!this.workspaceMembers) return null;
|
||||
|
||||
return [
|
||||
...(this.workspaceMemberInvitations?.map((item) => ({
|
||||
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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user