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:
Aaryan Khandelwal 2023-11-28 17:05:42 +05:30 committed by GitHub
parent f7264364bd
commit 0cbb201348
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 317 additions and 240 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
}`}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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