chore: settings page refactoring (#2477)
* chore: implemented project layout and integrated sidebar component * chore: implemented workspace layout and integrated sidebar component
This commit is contained in:
parent
0ec0ca133a
commit
c270c8689f
30 changed files with 1708 additions and 1747 deletions
|
|
@ -1,76 +1,32 @@
|
|||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import { WorkspaceService } from "services/workspace.service";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
|
||||
import { WorkspaceSettingLayout } from "layouts/setting-layout/workspace-setting-layout";
|
||||
// component
|
||||
import { SettingsSidebar } from "components/project";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs, Button } from "@plane/ui";
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
const BillingSettings: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, () =>
|
||||
workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
link={
|
||||
<Link href={`/${workspaceSlug}`}>
|
||||
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
|
||||
<p className="truncate">{`${truncateText(activeWorkspace?.name ?? "Workspace", 32)}`}</p>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<BreadcrumbItem title="Billing & Plans Settings" unshrinkTitle />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row gap-2 h-full">
|
||||
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
|
||||
<SettingsSidebar />
|
||||
const BillingSettings: NextPage = () => (
|
||||
<WorkspaceSettingLayout header={<WorkspaceSettingHeader title="Billing & Plans Settings" />}>
|
||||
<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-200">
|
||||
<h3 className="text-xl font-medium">Billing & Plans</h3>
|
||||
</div>
|
||||
<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-200">
|
||||
<h3 className="text-xl font-medium">Billing & Plans</h3>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
);
|
||||
};
|
||||
<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>
|
||||
</WorkspaceSettingLayout>
|
||||
);
|
||||
|
||||
export default BillingSettings;
|
||||
|
|
|
|||
|
|
@ -1,65 +1,20 @@
|
|||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import { WorkspaceService } from "services/workspace.service";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
|
||||
// components
|
||||
import ExportGuide from "components/exporter/guide";
|
||||
import { SettingsSidebar } from "components/project";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "@plane/ui";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
import { WorkspaceSettingLayout } from "layouts/setting-layout/workspace-setting-layout";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
const ImportExport: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, () =>
|
||||
workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
link={
|
||||
<Link href={`/${workspaceSlug}`}>
|
||||
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
|
||||
<p className="truncate">{`${truncateText(activeWorkspace?.name ?? "Workspace", 32)}`}</p>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<BreadcrumbItem title="Export Settings" unshrinkTitle />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row gap-2 h-full">
|
||||
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
|
||||
<SettingsSidebar />
|
||||
</div>
|
||||
<div className="pr-9 py-8 w-full overflow-y-auto">
|
||||
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||
<h3 className="text-xl font-medium">Exports</h3>
|
||||
</div>
|
||||
<ExportGuide />
|
||||
</div>
|
||||
const ImportExport: NextPage = () => (
|
||||
<WorkspaceSettingLayout header={<WorkspaceSettingHeader title="Export Settings" />}>
|
||||
<div className="pr-9 py-8 w-full overflow-y-auto">
|
||||
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||
<h3 className="text-xl font-medium">Exports</h3>
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
);
|
||||
};
|
||||
<ExportGuide />
|
||||
</div>
|
||||
</WorkspaceSettingLayout>
|
||||
);
|
||||
|
||||
export default ImportExport;
|
||||
|
|
|
|||
|
|
@ -1,65 +1,20 @@
|
|||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import { WorkspaceService } from "services/workspace.service";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
|
||||
import { WorkspaceSettingLayout } from "layouts/setting-layout/workspace-setting-layout";
|
||||
// components
|
||||
import IntegrationGuide from "components/integration/guide";
|
||||
import { SettingsSidebar } from "components/project";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "@plane/ui";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
const ImportExport: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, () =>
|
||||
workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
link={
|
||||
<Link href={`/${workspaceSlug}`}>
|
||||
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
|
||||
<p className="truncate">{`${truncateText(activeWorkspace?.name ?? "Workspace", 32)}`}</p>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<BreadcrumbItem title="Import/ Export Settings" unshrinkTitle />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row gap-2 h-full">
|
||||
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
|
||||
<SettingsSidebar />
|
||||
</div>
|
||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||
<h3 className="text-xl font-medium">Imports</h3>
|
||||
</div>
|
||||
<IntegrationGuide />
|
||||
</section>
|
||||
const ImportExport: NextPage = () => (
|
||||
<WorkspaceSettingLayout header={<WorkspaceSettingHeader title="Import Settings" />}>
|
||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||
<h3 className="text-xl font-medium">Imports</h3>
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
);
|
||||
};
|
||||
<IntegrationGuide />
|
||||
</section>
|
||||
</WorkspaceSettingLayout>
|
||||
);
|
||||
|
||||
export default ImportExport;
|
||||
|
|
|
|||
|
|
@ -13,19 +13,17 @@ import { FileService } from "services/file.service";
|
|||
import useToast from "hooks/use-toast";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
|
||||
import { WorkspaceSettingLayout } from "layouts/setting-layout/workspace-setting-layout";
|
||||
// components
|
||||
import { ImageUploadModal } from "components/core";
|
||||
import { DeleteWorkspaceModal } from "components/workspace";
|
||||
import { SettingsSidebar } from "components/project";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
// ui
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { CustomSelect } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs, Button, Input, Spinner } from "@plane/ui";
|
||||
import { Button, Input, Spinner } from "@plane/ui";
|
||||
// icons
|
||||
import { ChevronDown, ChevronUp, Pencil } from "lucide-react";
|
||||
// helpers
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import type { IWorkspace } from "types";
|
||||
import type { NextPage } from "next";
|
||||
|
|
@ -68,7 +66,6 @@ const WorkspaceSettings: NextPage = () => {
|
|||
);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
|
|
@ -156,13 +153,7 @@ const WorkspaceSettings: NextPage = () => {
|
|||
const isAdmin = memberDetails?.role === 20;
|
||||
|
||||
return (
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem title={`${truncateText(activeWorkspace?.name ?? "Workspace", 32)} Settings`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<WorkspaceSettingLayout header={<WorkspaceSettingHeader title="General Settings" />}>
|
||||
<ImageUploadModal
|
||||
isOpen={isImageUploadModalOpen}
|
||||
onClose={() => setIsImageUploadModalOpen(false)}
|
||||
|
|
@ -184,190 +175,185 @@ const WorkspaceSettings: NextPage = () => {
|
|||
data={activeWorkspace ?? null}
|
||||
user={user}
|
||||
/>
|
||||
<div className="flex flex-row gap-2 h-full">
|
||||
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
|
||||
<SettingsSidebar />
|
||||
</div>
|
||||
{activeWorkspace ? (
|
||||
<div className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className="flex gap-5 items-center pb-7 border-b border-custom-border-200">
|
||||
<div className="flex flex-col gap-1">
|
||||
<button type="button" onClick={() => setIsImageUploadModalOpen(true)} disabled={!isAdmin}>
|
||||
{activeWorkspace ? (
|
||||
<div className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className="flex gap-5 items-center pb-7 border-b border-custom-border-200">
|
||||
<div className="flex flex-col gap-1">
|
||||
<button type="button" onClick={() => setIsImageUploadModalOpen(true)} disabled={!isAdmin}>
|
||||
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
|
||||
<div className="relative mx-auto flex h-14 w-14">
|
||||
<img
|
||||
src={watch("logo")!}
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
|
||||
alt="Workspace Logo"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex h-14 w-14 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
|
||||
{activeWorkspace?.name?.charAt(0) ?? "N"}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-lg font-semibold leading-6">{watch("name")}</h3>
|
||||
<span className="text-sm tracking-tight">{`${
|
||||
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
|
||||
}/${activeWorkspace.slug}`}</span>
|
||||
<div className="flex item-center gap-2.5">
|
||||
<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") !== "" ? (
|
||||
<div className="relative mx-auto flex h-14 w-14">
|
||||
<img
|
||||
src={watch("logo")!}
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
|
||||
alt="Workspace Logo"
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit logo
|
||||
</>
|
||||
) : (
|
||||
<div className="relative flex h-14 w-14 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
|
||||
{activeWorkspace?.name?.charAt(0) ?? "N"}
|
||||
</div>
|
||||
"Upload logo"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-lg font-semibold leading-6">{watch("name")}</h3>
|
||||
<span className="text-sm tracking-tight">{`${
|
||||
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
|
||||
}/${activeWorkspace.slug}`}</span>
|
||||
<div className="flex item-center gap-2.5">
|
||||
<button
|
||||
className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
|
||||
onClick={() => setIsImageUploadModalOpen(true)}
|
||||
disabled={!isAdmin}
|
||||
</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>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: "Name is required",
|
||||
maxLength: {
|
||||
value: 80,
|
||||
message: "Workspace name should not exceed 80 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Name"
|
||||
className="rounded-md font-medium w-full"
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Company Size</h4>
|
||||
<Controller
|
||||
name="organization_size"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={ORGANIZATION_SIZE.find((c) => c === value) ?? "Select organization size"}
|
||||
width="w-full"
|
||||
input
|
||||
disabled={!isAdmin}
|
||||
>
|
||||
{ORGANIZATION_SIZE?.map((item) => (
|
||||
<CustomSelect.Option key={item} value={item}>
|
||||
{item}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Workspace URL</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="url"
|
||||
render={({ field: { onChange, ref } }) => (
|
||||
<Input
|
||||
id="url"
|
||||
name="url"
|
||||
type="url"
|
||||
value={`${
|
||||
typeof window !== "undefined" &&
|
||||
window.location.origin.replace("http://", "").replace("https://", "")
|
||||
}/${activeWorkspace.slug}`}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.url)}
|
||||
className="w-full"
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<Disclosure as="div" className="border-t border-custom-border-400">
|
||||
{({ open }) => (
|
||||
<div className="w-full">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex items-center justify-between w-full py-4"
|
||||
>
|
||||
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
|
||||
<>
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit logo
|
||||
</>
|
||||
) : (
|
||||
"Upload logo"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xl tracking-tight">Delete Workspace</span>
|
||||
{/* <Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" /> */}
|
||||
{open ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||
</Disclosure.Button>
|
||||
|
||||
<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>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: "Name is required",
|
||||
maxLength: {
|
||||
value: 80,
|
||||
message: "Workspace name should not exceed 80 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Name"
|
||||
className="rounded-md font-medium w-full"
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Company Size</h4>
|
||||
<Controller
|
||||
name="organization_size"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={ORGANIZATION_SIZE.find((c) => c === value) ?? "Select organization size"}
|
||||
width="w-full"
|
||||
input
|
||||
disabled={!isAdmin}
|
||||
>
|
||||
{ORGANIZATION_SIZE?.map((item) => (
|
||||
<CustomSelect.Option key={item} value={item}>
|
||||
{item}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 ">
|
||||
<h4 className="text-sm">Workspace URL</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="url"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="url"
|
||||
name="url"
|
||||
type="url"
|
||||
value={`${
|
||||
typeof window !== "undefined" &&
|
||||
window.location.origin.replace("http://", "").replace("https://", "")
|
||||
}/${activeWorkspace.slug}`}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.url)}
|
||||
className="w-full"
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<Disclosure as="div" className="border-t border-custom-border-400">
|
||||
{({ open }) => (
|
||||
<div className="w-full">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex items-center justify-between w-full py-4"
|
||||
>
|
||||
<span className="text-xl tracking-tight">Delete Workspace</span>
|
||||
{/* <Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" /> */}
|
||||
{open ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||
</Disclosure.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="flex flex-col gap-8">
|
||||
<span className="text-sm tracking-tight">
|
||||
The danger zone of the workspace delete page is a critical area that requires careful
|
||||
consideration and attention. When deleting a workspace, all of the data and resources within
|
||||
that workspace will be permanently removed and cannot be recovered.
|
||||
</span>
|
||||
<div>
|
||||
<Button variant="danger" onClick={() => setIsOpen(true)}>
|
||||
Delete my workspace
|
||||
</Button>
|
||||
</div>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="flex flex-col gap-8">
|
||||
<span className="text-sm tracking-tight">
|
||||
The danger zone of the workspace delete page is a critical area that requires careful
|
||||
consideration and attention. When deleting a workspace, all of the data and resources within
|
||||
that workspace will be permanently removed and cannot be recovered.
|
||||
</span>
|
||||
<div>
|
||||
<Button variant="danger" onClick={() => setIsOpen(true)}>
|
||||
Delete my workspace
|
||||
</Button>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full w-full px-4 sm:px-0">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full w-full px-4 sm:px-0">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</WorkspaceSettingLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,82 +1,54 @@
|
|||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import { WorkspaceService } from "services/workspace.service";
|
||||
import { IntegrationService } from "services/integrations";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
|
||||
import { WorkspaceSettingLayout } from "layouts/setting-layout/workspace-setting-layout";
|
||||
// components
|
||||
import { SingleIntegrationCard } from "components/integration";
|
||||
import { SettingsSidebar } from "components/project";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { Loader } from "@plane/ui";
|
||||
// ui
|
||||
import { IntegrationAndImportExportBanner } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs, Loader } from "@plane/ui";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_DETAILS, APP_INTEGRATIONS } from "constants/fetch-keys";
|
||||
import { APP_INTEGRATIONS } from "constants/fetch-keys";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
const integrationService = new IntegrationService();
|
||||
|
||||
const WorkspaceIntegrations: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, () =>
|
||||
workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
const { data: appIntegrations } = useSWR(workspaceSlug ? APP_INTEGRATIONS : null, () =>
|
||||
workspaceSlug ? integrationService.getAppIntegrationsList() : null
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
link={
|
||||
<Link href={`/${workspaceSlug}`}>
|
||||
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
|
||||
<p className="truncate">{`${truncateText(activeWorkspace?.name ?? "Workspace", 32)}`}</p>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<BreadcrumbItem title="Integrations Settings" unshrinkTitle />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row gap-2 h-full">
|
||||
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
|
||||
<SettingsSidebar />
|
||||
<WorkspaceSettingLayout header={<WorkspaceSettingHeader title="Export Settings" />}>
|
||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
||||
<div>
|
||||
{appIntegrations ? (
|
||||
appIntegrations.map((integration) => (
|
||||
<SingleIntegrationCard key={integration.id} integration={integration} />
|
||||
))
|
||||
) : (
|
||||
<Loader className="space-y-2.5 mt-4">
|
||||
<Loader.Item height="89px" />
|
||||
<Loader.Item height="89px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
||||
<div>
|
||||
{appIntegrations ? (
|
||||
appIntegrations.map((integration) => (
|
||||
<SingleIntegrationCard key={integration.id} integration={integration} />
|
||||
))
|
||||
) : (
|
||||
<Loader className="space-y-2.5 mt-4">
|
||||
<Loader.Item height="89px" />
|
||||
<Loader.Item height="89px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
</section>
|
||||
</WorkspaceSettingLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,24 +12,23 @@ import useToast from "hooks/use-toast";
|
|||
import useUser from "hooks/use-user";
|
||||
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
|
||||
import { WorkspaceSettingLayout } from "layouts/setting-layout/workspace-setting-layout";
|
||||
// components
|
||||
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
|
||||
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
|
||||
import { SettingsSidebar } from "components/project";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs, Button, Loader } from "@plane/ui";
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { CustomMenu, CustomSelect } from "components/ui";
|
||||
// icons
|
||||
import { ChevronDown, X } from "lucide-react";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_DETAILS, WORKSPACE_INVITATION_WITH_EMAIL, WORKSPACE_MEMBERS_WITH_EMAIL } from "constants/fetch-keys";
|
||||
import { WORKSPACE_INVITATION_WITH_EMAIL, WORKSPACE_MEMBERS_WITH_EMAIL } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { ROLE } from "constants/workspace";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
|
@ -48,10 +47,6 @@ const MembersSettings: NextPage = () => {
|
|||
|
||||
const { isOwner } = useWorkspaceMembers(workspaceSlug?.toString(), Boolean(workspaceSlug));
|
||||
|
||||
const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug.toString()) : null, () =>
|
||||
workspaceSlug ? workspaceService.getWorkspace(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const { data: workspaceMembers, mutate: mutateMembers } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS_WITH_EMAIL(workspaceSlug.toString()) : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMembersWithEmail(workspaceSlug.toString()) : null
|
||||
|
|
@ -98,22 +93,7 @@ const MembersSettings: NextPage = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs onBack={() => router.back()}>
|
||||
<BreadcrumbItem
|
||||
link={
|
||||
<Link href={`/${workspaceSlug}`}>
|
||||
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
|
||||
<p className="truncate">{`${truncateText(activeWorkspace?.name ?? "Workspace", 32)}`}</p>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<BreadcrumbItem title="Members Settings" unshrinkTitle />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<WorkspaceSettingLayout header={<WorkspaceSettingHeader title="Members Settings" />}>
|
||||
<ConfirmWorkspaceMemberRemove
|
||||
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
|
||||
onClose={() => {
|
||||
|
|
@ -181,155 +161,150 @@ const MembersSettings: NextPage = () => {
|
|||
user={user}
|
||||
onSuccess={handleInviteModalSuccess}
|
||||
/>
|
||||
<div className="flex flex-row gap-2 h-full">
|
||||
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
|
||||
<SettingsSidebar />
|
||||
<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-200">
|
||||
<h4 className="text-xl font-medium">Members</h4>
|
||||
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
|
||||
Add Member
|
||||
</Button>
|
||||
</div>
|
||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||
<div className="flex items-center justify-between gap-4 pt-2 pb-3.5 border-b border-custom-border-200">
|
||||
<h4 className="text-xl font-medium">Members</h4>
|
||||
<Button variant="primary" onClick={() => setInviteModal(true)}>
|
||||
Add Member
|
||||
</Button>
|
||||
</div>
|
||||
{!workspaceMembers || !workspaceInvitations ? (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
) : (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{members.length > 0
|
||||
? members.map((member) => (
|
||||
<div key={member.id} className="group flex items-center justify-between px-3.5 py-[18px]">
|
||||
<div className="flex items-center gap-x-8 gap-y-2">
|
||||
{member.avatar && member.avatar !== "" ? (
|
||||
{!workspaceMembers || !workspaceInvitations ? (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
) : (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{members.length > 0
|
||||
? members.map((member) => (
|
||||
<div key={member.id} className="group flex items-center justify-between px-3.5 py-[18px]">
|
||||
<div className="flex items-center gap-x-8 gap-y-2">
|
||||
{member.avatar && member.avatar !== "" ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||
<a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
|
||||
<img
|
||||
src={member.avatar}
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
|
||||
alt={member.display_name || member.email}
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
) : member.display_name || member.email ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||
<a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
|
||||
{(member.display_name || member.email)?.charAt(0)}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
|
||||
?
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{member.member ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||
<a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
|
||||
<img
|
||||
src={member.avatar}
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
|
||||
alt={member.display_name || member.email}
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
) : member.display_name || member.email ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||
<a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
|
||||
{(member.display_name || member.email)?.charAt(0)}
|
||||
<a className="text-sm">
|
||||
<span>
|
||||
{member.first_name} {member.last_name}
|
||||
</span>
|
||||
<span className="text-custom-text-300 text-sm ml-2">({member.display_name})</span>
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
|
||||
?
|
||||
</div>
|
||||
<h4 className="text-sm cursor-default">{member.display_name || member.email}</h4>
|
||||
)}
|
||||
<div>
|
||||
{member.member ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||
<a className="text-sm">
|
||||
<span>
|
||||
{member.first_name} {member.last_name}
|
||||
</span>
|
||||
<span className="text-custom-text-300 text-sm ml-2">({member.display_name})</span>
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<h4 className="text-sm cursor-default">{member.display_name || member.email}</h4>
|
||||
)}
|
||||
{isOwner && <p className="mt-0.5 text-xs text-custom-sidebar-text-300">{member.email}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
{!member?.status && (
|
||||
<div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
|
||||
<p>Pending</p>
|
||||
</div>
|
||||
)}
|
||||
{member?.status && !member?.accountCreated && (
|
||||
<div className="mr-2 flex items-center justify-center rounded-full bg-blue-500/20 px-2 py-1 text-center text-xs text-blue-500">
|
||||
<p>Account not created</p>
|
||||
</div>
|
||||
)}
|
||||
<CustomSelect
|
||||
customButton={
|
||||
<div className="flex item-center gap-1">
|
||||
<span
|
||||
className={`flex items-center text-sm font-medium ${
|
||||
member.memberId !== user?.id ? "" : "text-custom-sidebar-text-400"
|
||||
}`}
|
||||
>
|
||||
{ROLE[member.role as keyof typeof ROLE]}
|
||||
</span>
|
||||
{member.memberId !== user?.id && <ChevronDown className="h-4 w-4" />}
|
||||
</div>
|
||||
}
|
||||
value={member.role}
|
||||
onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
mutateMembers(
|
||||
(prevData: any) =>
|
||||
prevData?.map((m: any) => (m.id === member.id ? { ...m, role: value } : m)),
|
||||
false
|
||||
);
|
||||
|
||||
workspaceService
|
||||
.updateWorkspaceMember(workspaceSlug?.toString(), member.id, {
|
||||
role: value,
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
member.memberId === currentUser?.member.id ||
|
||||
!member.status ||
|
||||
(currentUser && currentUser.role !== 20 && currentUser.role < member.role)
|
||||
}
|
||||
>
|
||||
{Object.keys(ROLE).map((key) => {
|
||||
if (currentUser && currentUser.role !== 20 && currentUser.role < parseInt(key)) return null;
|
||||
|
||||
return (
|
||||
<CustomSelect.Option key={key} value={key}>
|
||||
<>{ROLE[parseInt(key) as keyof typeof ROLE]}</>
|
||||
</CustomSelect.Option>
|
||||
);
|
||||
})}
|
||||
</CustomSelect>
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
if (member.member) {
|
||||
setSelectedRemoveMember(member.id);
|
||||
} else {
|
||||
setSelectedInviteRemoveMember(member.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<X className="h-4 w-4" />
|
||||
|
||||
<span> {user?.id === member.memberId ? "Leave" : "Remove member"}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
{isOwner && <p className="mt-0.5 text-xs text-custom-sidebar-text-300">{member.email}</p>}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
{!member?.status && (
|
||||
<div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
|
||||
<p>Pending</p>
|
||||
</div>
|
||||
)}
|
||||
{member?.status && !member?.accountCreated && (
|
||||
<div className="mr-2 flex items-center justify-center rounded-full bg-blue-500/20 px-2 py-1 text-center text-xs text-blue-500">
|
||||
<p>Account not created</p>
|
||||
</div>
|
||||
)}
|
||||
<CustomSelect
|
||||
customButton={
|
||||
<div className="flex item-center gap-1">
|
||||
<span
|
||||
className={`flex items-center text-sm font-medium ${
|
||||
member.memberId !== user?.id ? "" : "text-custom-sidebar-text-400"
|
||||
}`}
|
||||
>
|
||||
{ROLE[member.role as keyof typeof ROLE]}
|
||||
</span>
|
||||
{member.memberId !== user?.id && <ChevronDown className="h-4 w-4" />}
|
||||
</div>
|
||||
}
|
||||
value={member.role}
|
||||
onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
mutateMembers(
|
||||
(prevData: any) =>
|
||||
prevData?.map((m: any) => (m.id === member.id ? { ...m, role: value } : m)),
|
||||
false
|
||||
);
|
||||
|
||||
workspaceService
|
||||
.updateWorkspaceMember(workspaceSlug?.toString(), member.id, {
|
||||
role: value,
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
member.memberId === currentUser?.member.id ||
|
||||
!member.status ||
|
||||
(currentUser && currentUser.role !== 20 && currentUser.role < member.role)
|
||||
}
|
||||
>
|
||||
{Object.keys(ROLE).map((key) => {
|
||||
if (currentUser && currentUser.role !== 20 && currentUser.role < parseInt(key)) return null;
|
||||
|
||||
return (
|
||||
<CustomSelect.Option key={key} value={key}>
|
||||
<>{ROLE[parseInt(key) as keyof typeof ROLE]}</>
|
||||
</CustomSelect.Option>
|
||||
);
|
||||
})}
|
||||
</CustomSelect>
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
if (member.member) {
|
||||
setSelectedRemoveMember(member.id);
|
||||
} else {
|
||||
setSelectedInviteRemoveMember(member.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<X className="h-4 w-4" />
|
||||
|
||||
<span> {user?.id === member.memberId ? "Leave" : "Remove member"}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</WorkspaceSettingLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue