fix: layout height and overflow (#1004)

* fix: kanban height issue

* dev: Layout fixes

* dev: layout changes

* fix: layout overflow settings and fixed header

* style: filters padding fixed

* fix: hide filters if none are applied

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
This commit is contained in:
Aaryan Khandelwal 2023-05-05 17:07:29 +05:30 committed by GitHub
parent 443878994a
commit a1de3f581f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1080 additions and 1035 deletions

View File

@ -44,7 +44,7 @@ export const AllBoards: React.FC<Props> = ({
return (
<>
{groupedByIssues ? (
<div className="horizontal-scroll-enable flex h-[calc(100vh-140px)] gap-x-4">
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
{Object.keys(groupedByIssues).map((singleGroup, index) => {
const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;

View File

@ -392,7 +392,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<LinkIcon className="h-3.5 w-3.5 text-brand-secondary" />
<LinkIcon className="h-3.5 w-3.5" />
{issue.link_count}
</div>
</Tooltip>
@ -402,7 +402,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" />
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
{issue.attachment_count}
</div>
</Tooltip>

View File

@ -229,7 +229,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
return calendarIssues ? (
<DragDropContext onDragEnd={onDragEnd}>
<div className="-m-2 h-full overflow-y-auto rounded-lg text-brand-secondary">
<div className="-m-2 h-full overflow-y-auto rounded-lg p-8 text-brand-secondary">
<div className="mb-4 flex items-center justify-between">
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
<Popover className="flex h-full items-center justify-start rounded-lg">

View File

@ -353,7 +353,7 @@ export const IssuesView: React.FC<Props> = ({
console.log(e);
});
},
[workspaceSlug, projectId, cycleId, params]
[workspaceSlug, projectId, cycleId, params, selectedGroup, setToastAlert]
);
const removeIssueFromModule = useCallback(
@ -396,7 +396,7 @@ export const IssuesView: React.FC<Props> = ({
console.log(e);
});
},
[workspaceSlug, projectId, moduleId, params]
[workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert]
);
const handleTrashBox = useCallback(
@ -442,39 +442,35 @@ export const IssuesView: React.FC<Props> = ({
handleClose={() => setTransferIssuesModal(false)}
isOpen={transferIssuesModal}
/>
<>
<div
className={`flex items-center justify-between gap-2 ${
issueView === "list" ? (areFiltersApplied ? "mt-6 px-8" : "") : "-mt-2"
}`}
>
<FilterList filters={filters} setFilters={setFilters} />
{areFiltersApplied && (
<PrimaryButton
onClick={() => {
if (viewId) {
setFilters({}, true);
setToastAlert({
title: "View updated",
message: "Your view has been updated",
type: "success",
});
} else
setCreateViewModal({
query: filters,
});
}}
className="flex items-center gap-2 text-sm"
>
{!viewId && <PlusIcon className="h-4 w-4" />}
{viewId ? "Update" : "Save"} view
</PrimaryButton>
)}
</div>
{areFiltersApplied && (
<div className={`${issueView === "list" ? "mt-4" : "my-4"} border-t border-brand-base`} />
)}
</>
{areFiltersApplied && (
<>
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
<FilterList filters={filters} setFilters={setFilters} />
{areFiltersApplied && (
<PrimaryButton
onClick={() => {
if (viewId) {
setFilters({}, true);
setToastAlert({
title: "View updated",
message: "Your view has been updated",
type: "success",
});
} else
setCreateViewModal({
query: filters,
});
}}
className="flex items-center gap-2 text-sm"
>
{!viewId && <PlusIcon className="h-4 w-4" />}
{viewId ? "Update" : "Save"} view
</PrimaryButton>
)}
</div>
{<div className="mt-3 border-t border-brand-base" />}
</>
)}
<DragDropContext onDragEnd={handleOnDragEnd}>
<StrictModeDroppable droppableId="trashBox">

View File

@ -314,7 +314,7 @@ export const SingleListIssue: React.FC<Props> = ({
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<LinkIcon className="h-3.5 w-3.5 text-brand-secondary" />
<LinkIcon className="h-3.5 w-3.5" />
{issue.link_count}
</div>
</Tooltip>
@ -324,7 +324,7 @@ export const SingleListIssue: React.FC<Props> = ({
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" />
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
{issue.attachment_count}
</div>
</Tooltip>

View File

@ -81,7 +81,7 @@ export const SelectRepository: React.FC<Props> = ({
{userRepositories && options.length < totalCount && (
<button
type="button"
className="w-full p-1 text-center text-[0.6rem] text-gray-500 hover:bg-hover-gray"
className="w-full p-1 text-center text-[0.6rem] text-brand-secondary hover:bg-brand-surface-2"
onClick={() => setSize(size + 1)}
disabled={isValidating}
>

View File

@ -82,8 +82,8 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
const isNotAllowed = false;
return (
<div className="mx-6 border-b border-brand-base last:border-b-0">
<div key={issue.id} className="flex items-center justify-between gap-2 py-3">
<div className="border-b border-brand-base bg-brand-base px-4 py-2.5 last:border-b-0">
<div key={issue.id} className="flex items-center justify-between gap-2">
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2">
{properties?.key && (
@ -171,7 +171,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
</Tooltip>
)}
{properties.link && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<LinkIcon className="h-3.5 w-3.5 text-brand-secondary" />
@ -181,7 +181,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
</div>
)}
{properties.attachment_count && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" />

View File

@ -1,6 +1,7 @@
export * from "./create-project-modal";
export * from "./delete-project-modal";
export * from "./sidebar-list";
export * from "./settings-header"
export * from "./single-integration-card";
export * from "./single-project-card";
export * from "./single-sidebar-project";

View File

@ -0,0 +1,13 @@
import SettingsNavbar from "layouts/settings-navbar";
export const SettingsHeader = () => (
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">Project Settings</h3>
<p className="mt-1 text-brand-secondary">
This information will be displayed to every member of the project.
</p>
</div>
<SettingsNavbar />
</div>
);

View File

@ -6,5 +6,6 @@ export * from "./help-section";
export * from "./issues-list";
export * from "./issues-pie-chart";
export * from "./issues-stats";
export * from "./settings-header";
export * from "./sidebar-dropdown";
export * from "./sidebar-menu";

View File

@ -0,0 +1,13 @@
import SettingsNavbar from "layouts/settings-navbar";
export const SettingsHeader = () => (
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">Workspace Settings</h3>
<p className="mt-1 text-brand-secondary">
This information will be displayed to every member of the workspace.
</p>
</div>
<SettingsNavbar />
</div>
);

View File

@ -9,8 +9,8 @@ type Props = {
};
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar }) => (
<div className="flex w-full flex-row flex-wrap items-center justify-between gap-y-4 border-b border-brand-base bg-brand-sidebar px-5 py-4">
<div className="flex flex-wrap items-center gap-2">
<div className="relative flex w-full flex-shrink-0 flex-row items-center justify-between gap-y-4 border border-b border-brand-base bg-brand-sidebar px-5 py-4">
<div className="flex items-center gap-2">
<div className="block md:hidden">
<button
type="button"

View File

@ -18,22 +18,20 @@ const Sidebar: React.FC<SidebarProps> = ({ toggleSidebar, setToggleSidebar }) =>
const { collapsed: sidebarCollapse } = useTheme();
return (
<nav className="relative z-20 h-screen">
<div
className={`${sidebarCollapse ? "" : "w-auto md:w-[17rem]"} fixed inset-y-0 top-0 ${
toggleSidebar ? "left-0" : "-left-full md:left-0"
} flex h-full flex-col bg-brand-sidebar duration-300 md:relative`}
>
<div className="flex h-full flex-1 flex-col border-r border-brand-base">
<div className="flex h-full flex-1 flex-col">
<WorkspaceSidebarDropdown />
<WorkspaceSidebarMenu />
<ProjectSidebarList />
<WorkspaceHelpSection setSidebarActive={setToggleSidebar} />
</div>
</div>
<div
className={`z-20 h-full flex-shrink-0 border-r border-brand-base ${
sidebarCollapse ? "" : "w-auto md:w-[17rem]"
} fixed inset-y-0 top-0 ${
toggleSidebar ? "left-0" : "-left-full md:left-0"
} flex h-full flex-col bg-brand-sidebar duration-300 md:relative`}
>
<div className="flex h-full flex-1 flex-col">
<WorkspaceSidebarDropdown />
<WorkspaceSidebarMenu />
<ProjectSidebarList />
<WorkspaceHelpSection setSidebarActive={setToggleSidebar} />
</div>
</nav>
</div>
);
};

View File

@ -11,7 +11,6 @@ import useIssuesView from "hooks/use-issues-view";
import Container from "layouts/container";
import AppHeader from "layouts/app-layout/app-header";
import AppSidebar from "layouts/app-layout/app-sidebar";
import SettingsNavbar from "layouts/settings-navbar";
// components
import { NotAuthorizedView, JoinProject } from "components/auth-screens";
import { CommandPalette } from "components/command-palette";
@ -30,7 +29,6 @@ type Meta = {
type Props = {
meta?: Meta;
children: React.ReactNode;
noPadding?: boolean;
noHeader?: boolean;
bg?: "primary" | "secondary";
breadcrumbs?: JSX.Element;
@ -47,7 +45,6 @@ export const ProjectAuthorizationWrapper: React.FC<Props> = (props) => (
const ProjectAuthorizationWrapped: React.FC<Props> = ({
meta,
children,
noPadding = false,
noHeader = false,
bg = "primary",
breadcrumbs,
@ -68,8 +65,9 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
return (
<Container meta={meta}>
<CommandPalette />
<div className="flex h-screen w-full overflow-x-hidden">
<div className="relative flex h-screen w-full overflow-hidden">
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
{loading ? (
<div className="grid h-full w-full place-items-center p-4">
<div className="flex flex-col items-center gap-3 text-center">
@ -107,7 +105,15 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
type="project"
/>
) : (
<main className="flex h-screen w-full min-w-0 flex-col overflow-y-auto">
<main
className={`relative flex h-full w-full flex-col overflow-hidden ${
bg === "primary"
? "bg-brand-surface-1"
: bg === "secondary"
? "bg-brand-sidebar"
: "bg-brand-base"
}`}
>
{!noHeader && (
<AppHeader
breadcrumbs={breadcrumbs}
@ -116,29 +122,10 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
setToggleSidebar={setToggleSidebar}
/>
)}
<div
className={`flex w-full flex-grow flex-col ${
noPadding || issueView === "list" ? "" : settingsLayout ? "p-8 lg:px-28" : "p-8"
} ${
bg === "primary"
? "bg-brand-surface-1"
: bg === "secondary"
? "bg-brand-sidebar"
: "bg-brand-base"
}`}
>
{settingsLayout && (
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">Project Settings</h3>
<p className="mt-1 text-brand-secondary">
This information will be displayed to every member of the project.
</p>
</div>
<SettingsNavbar />
</div>
)}
{children}
<div className="h-full w-full overflow-hidden">
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
{children}
</div>
</div>
</main>
)}

View File

@ -32,25 +32,21 @@ type Meta = {
type Props = {
meta?: Meta;
children: React.ReactNode;
noPadding?: boolean;
noHeader?: boolean;
bg?: "primary" | "secondary";
breadcrumbs?: JSX.Element;
left?: JSX.Element;
right?: JSX.Element;
profilePage?: boolean;
};
export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
meta,
children,
noPadding = false,
noHeader = false,
bg = "primary",
breadcrumbs,
left,
right,
profilePage = false,
}) => {
const [toggleSidebar, setToggleSidebar] = useState(false);
@ -101,7 +97,7 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
<UserAuthorizationLayout>
<Container meta={meta}>
<CommandPalette />
<div className="flex h-screen w-full overflow-x-hidden">
<div className="relative flex h-screen w-full overflow-hidden">
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
{settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
<NotAuthorizedView
@ -117,7 +113,15 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
type="workspace"
/>
) : (
<main className="flex h-screen w-full min-w-0 flex-col overflow-y-auto">
<main
className={`relative flex h-full w-full flex-col overflow-hidden ${
bg === "primary"
? "bg-brand-surface-1"
: bg === "secondary"
? "bg-brand-sidebar"
: "bg-brand-base"
}`}
>
{!noHeader && (
<AppHeader
breadcrumbs={breadcrumbs}
@ -126,33 +130,10 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
setToggleSidebar={setToggleSidebar}
/>
)}
<div
className={`flex w-full flex-grow flex-col ${
noPadding ? "" : settingsLayout || profilePage ? "p-8 lg:px-28" : "p-8"
} ${
bg === "primary"
? "bg-brand-surface-1"
: bg === "secondary"
? "bg-brand-surface-1"
: "bg-brand-base"
}`}
>
{(settingsLayout || profilePage) && (
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">
{profilePage ? "Profile" : "Workspace"} Settings
</h3>
<p className="mt-1 text-brand-secondary">
{profilePage
? "This information will be visible to only you."
: "This information will be displayed to every member of the workspace."}
</p>
</div>
<SettingsNavbar profilePage={profilePage} />
</div>
)}
{children}
<div className="h-full w-full overflow-hidden">
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
{children}
</div>
</div>
</main>
)}

View File

@ -45,7 +45,7 @@ const WorkspacePage: NextPage = () => {
isOpen={isProductUpdatesModalOpen}
setIsOpen={setIsProductUpdatesModalOpen}
/>
<div className="h-full w-full">
<div className="p-8">
<div className="flex flex-col gap-8">
<div
className="text-brand-muted-1 flex flex-col justify-between gap-x-2 gap-y-6 rounded-lg bg-brand-base px-8 py-6 md:flex-row md:items-center md:py-3"

View File

@ -43,7 +43,6 @@ const MyIssuesPage: NextPage = () => {
<BreadcrumbItem title="My Issues" />
</Breadcrumbs>
}
noPadding
right={
<div className="flex items-center gap-2">
{myIssues && myIssues.length > 0 && (
@ -115,55 +114,42 @@ const MyIssuesPage: NextPage = () => {
{myIssues ? (
<>
{myIssues.length > 0 ? (
<div className="flex flex-col space-y-5">
<Disclosure as="div" defaultOpen>
{({ open }) => (
<div className="rounded-[10px] border border-brand-base bg-brand-base">
<div
className={`flex items-center justify-start bg-brand-surface-1 px-4 py-2.5 ${
open ? "rounded-t-[10px]" : "rounded-[10px]"
}`}
>
<Disclosure.Button>
<div className="flex items-center gap-x-2">
<span>
<ChevronDownIcon
className={`h-4 w-4 text-gray-500 ${
!open ? "-rotate-90 transform" : ""
}`}
/>
</span>
<h2 className="font-medium leading-5">My Issues</h2>
<span className="rounded-full bg-brand-surface-2 py-0.5 px-3 text-sm text-brand-secondary">
{myIssues.length}
</span>
</div>
</Disclosure.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>
{myIssues.map((issue: IIssue) => (
<MyIssuesListItem
key={issue.id}
issue={issue}
properties={properties}
projectId={issue.project}
/>
))}
</Disclosure.Panel>
</Transition>
<Disclosure as="div" defaultOpen>
{({ open }) => (
<div>
<div className="flex items-center px-4 py-2.5">
<Disclosure.Button>
<div className="flex items-center gap-x-2">
<h2 className="font-medium leading-5">My Issues</h2>
<span className="rounded-full bg-brand-surface-2 py-0.5 px-3 text-sm text-brand-secondary">
{myIssues.length}
</span>
</div>
</Disclosure.Button>
</div>
)}
</Disclosure>
</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>
{myIssues.map((issue: IIssue) => (
<MyIssuesListItem
key={issue.id}
issue={issue}
properties={properties}
projectId={issue.project}
/>
))}
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
) : (
<div className="flex h-full w-full flex-col items-center justify-center px-4">
<EmptySpace

View File

@ -11,6 +11,7 @@ import { Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// fetch-keys
import { USER_ACTIVITY } from "constants/fetch-keys";
import SettingsNavbar from "layouts/settings-navbar";
const ProfileActivity = () => {
const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity());
@ -25,20 +26,30 @@ const ProfileActivity = () => {
<BreadcrumbItem title="My Profile Activity" />
</Breadcrumbs>
}
profilePage
>
{userActivity ? (
userActivity.results.length > 0 ? (
<Feeds activities={userActivity.results} />
) : null
) : (
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
<div className="px-24 py-8">
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">Profile Settings</h3>
<p className="mt-1 text-brand-secondary">
This information will be visible to only you.
</p>
</div>
<SettingsNavbar profilePage />
</div>
{userActivity ? (
userActivity.results.length > 0 ? (
<Feeds activities={userActivity.results} />
) : null
) : (
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
</div>
</WorkspaceAuthorizationLayout>
);
};

View File

@ -24,6 +24,7 @@ import type { NextPage } from "next";
import type { IUser } from "types";
// constants
import { USER_ROLES } from "constants/workspace";
import SettingsNavbar from "layouts/settings-navbar";
const defaultValues: Partial<IUser> = {
avatar: "",
@ -130,7 +131,6 @@ const Profile: NextPage = () => {
<BreadcrumbItem title="My Profile" />
</Breadcrumbs>
}
profilePage
>
<ImageUploadModal
isOpen={isImageUploadModalOpen}
@ -144,145 +144,158 @@ const Profile: NextPage = () => {
userImage
/>
{myProfile ? (
<div className="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-brand-base">Profile Picture</h4>
<p className="text-sm text-brand-secondary">
Max file size is 5MB. Supported file types are .jpg and .png.
<div className="px-24 py-8">
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">Profile Settings</h3>
<p className="mt-1 text-brand-secondary">
This information will be visible to only you.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<div className="flex items-center gap-4">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!watch("avatar") || watch("avatar") === "" ? (
<div className="h-12 w-12 rounded-md bg-brand-surface-2 p-2">
<UserIcon className="h-full w-full text-brand-secondary" />
</div>
) : (
<div className="relative h-12 w-12 overflow-hidden">
<Image
src={watch("avatar")}
alt={myProfile.first_name}
layout="fill"
objectFit="cover"
className="rounded-md"
onClick={() => setIsImageUploadModalOpen(true)}
priority
/>
</div>
)}
</button>
<div className="flex items-center gap-2">
<SecondaryButton
onClick={() => {
setIsImageUploadModalOpen(true);
}}
>
Upload
</SecondaryButton>
{myProfile.avatar && myProfile.avatar !== "" && (
<DangerButton
onClick={() => handleDelete(myProfile.avatar, true)}
loading={isRemoving}
<SettingsNavbar profilePage />
</div>
<div className="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-brand-base">Profile Picture</h4>
<p className="text-sm text-brand-secondary">
Max file size is 5MB. Supported file types are .jpg and .png.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<div className="flex items-center gap-4">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!watch("avatar") || watch("avatar") === "" ? (
<div className="h-12 w-12 rounded-md bg-brand-surface-2 p-2">
<UserIcon className="h-full w-full text-brand-secondary" />
</div>
) : (
<div className="relative h-12 w-12 overflow-hidden">
<Image
src={watch("avatar")}
alt={myProfile.first_name}
layout="fill"
objectFit="cover"
className="rounded-md"
onClick={() => setIsImageUploadModalOpen(true)}
priority
/>
</div>
)}
</button>
<div className="flex items-center gap-2">
<SecondaryButton
onClick={() => {
setIsImageUploadModalOpen(true);
}}
>
{isRemoving ? "Removing..." : "Remove"}
</DangerButton>
)}
Upload
</SecondaryButton>
{myProfile.avatar && myProfile.avatar !== "" && (
<DangerButton
onClick={() => handleDelete(myProfile.avatar, true)}
loading={isRemoving}
>
{isRemoving ? "Removing..." : "Remove"}
</DangerButton>
)}
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-brand-base">Full Name</h4>
<p className="text-sm text-brand-secondary">
This name will be reflected on all the projects you are working on.
</p>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-brand-base">Full Name</h4>
<p className="text-sm text-brand-secondary">
This name will be reflected on all the projects you are working on.
</p>
</div>
<div className="col-span-12 flex items-center gap-2 sm:col-span-6">
<Input
name="first_name"
id="first_name"
register={register}
error={errors.first_name}
placeholder="Enter your first name"
autoComplete="off"
validations={{
required: "This field is required.",
}}
/>
<Input
name="last_name"
register={register}
error={errors.last_name}
id="last_name"
placeholder="Enter your last name"
autoComplete="off"
/>
</div>
</div>
<div className="col-span-12 flex items-center gap-2 sm:col-span-6">
<Input
name="first_name"
id="first_name"
register={register}
error={errors.first_name}
placeholder="Enter your first name"
autoComplete="off"
validations={{
required: "This field is required.",
}}
/>
<Input
name="last_name"
register={register}
error={errors.last_name}
id="last_name"
placeholder="Enter your last name"
autoComplete="off"
/>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-brand-base">Email</h4>
<p className="text-sm text-brand-secondary">
The email address that you are using.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Input
id="email"
name="email"
autoComplete="off"
register={register}
error={errors.name}
className="w-full"
disabled
/>
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-brand-base">Email</h4>
<p className="text-sm text-brand-secondary">The email address that you are using.</p>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-brand-base">Role</h4>
<p className="text-sm text-brand-secondary">Add your role.</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Controller
name="role"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={value ? value.toString() : "Select your role"}
width="w-full"
input
position="right"
>
{USER_ROLES.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
</div>
<div className="col-span-12 sm:col-span-6">
<Input
id="email"
name="email"
autoComplete="off"
register={register}
error={errors.name}
className="w-full"
disabled
/>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-brand-base">Theme</h4>
<p className="text-sm text-brand-secondary">
Select or customize your interface color scheme.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<ThemeSwitch />
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-brand-base">Role</h4>
<p className="text-sm text-brand-secondary">Add your role.</p>
<div className="sm:text-right">
<SecondaryButton onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Update profile"}
</SecondaryButton>
</div>
<div className="col-span-12 sm:col-span-6">
<Controller
name="role"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={value ? value.toString() : "Select your role"}
width="w-full"
input
position="right"
>
{USER_ROLES.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-brand-base">Theme</h4>
<p className="text-sm text-brand-secondary">
Select or customize your interface color scheme.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<ThemeSwitch />
</div>
</div>
<div className="sm:text-right">
<SecondaryButton onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Update profile"}
</SecondaryButton>
</div>
</div>
) : (

View File

@ -124,7 +124,7 @@ const ProjectCycles: NextPage = () => {
handleClose={() => setCreateUpdateCycleModal(false)}
data={selectedCycle}
/>
<div className="space-y-8">
<div className="space-y-8 p-8">
<div className="flex flex-col gap-5">
{currentAndUpcomingCycles && currentAndUpcomingCycles.current_cycle.length > 0 && (
<h3 className="text-3xl font-semibold text-brand-base">Current Cycle</h3>

View File

@ -122,7 +122,6 @@ const IssueDetailsPage: NextPage = () => {
return (
<ProjectAuthorizationWrapper
noPadding
breadcrumbs={
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem

View File

@ -77,7 +77,7 @@ const ProjectModules: NextPage = () => {
document.dispatchEvent(e);
}}
>
<PlusIcon className="w-4 h-4" />
<PlusIcon className="h-4 w-4" />
Add Module
</PrimaryButton>
}
@ -89,7 +89,7 @@ const ProjectModules: NextPage = () => {
/>
{modules ? (
modules.length > 0 ? (
<div className="space-y-5">
<div className="space-y-5 p-8">
<div className="flex flex-col gap-5">
<h3 className="text-3xl font-semibold text-brand-base">Modules</h3>

View File

@ -312,7 +312,7 @@ const SinglePage: NextPage = () => {
}
>
{pageDetails ? (
<div className="h-full w-full space-y-4 rounded-md border border-brand-base bg-brand-base p-4">
<div className="space-y-4 p-4">
<div className="flex items-center justify-between gap-2 px-3">
<button
type="button"

View File

@ -25,7 +25,7 @@ import { RecentPagesList, CreateUpdatePageModal, TPagesListProps } from "compone
import { Input, PrimaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import {ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import { ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
// types
import { IPage, TPageViewProps } from "types";
import type { NextPage } from "next";
@ -195,7 +195,7 @@ const ProjectPages: NextPage = () => {
</PrimaryButton>
}
>
<div className="space-y-4">
<div className="space-y-4 p-8">
<form
onSubmit={handleSubmit(createPage)}
className="relative mb-12 flex items-center justify-between gap-2 rounded-[6px] border border-brand-base p-2 shadow"

View File

@ -20,6 +20,7 @@ import { IProject, IWorkspace } from "types";
import type { NextPage } from "next";
// fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS, PROJECT_MEMBERS } from "constants/fetch-keys";
import { SettingsHeader } from "components/project";
const defaultValues: Partial<IProject> = {
project_lead: null,
@ -103,7 +104,8 @@ const ControlSettings: NextPage = () => {
</Breadcrumbs>
}
>
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit(onSubmit)} className="px-24 py-8">
<SettingsHeader />
<div className="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 items-start gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">

View File

@ -27,6 +27,7 @@ import { IEstimate, IProject } from "types";
import type { NextPage } from "next";
// fetch-keys
import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
import { SettingsHeader } from "components/project";
const EstimatesSettings: NextPage = () => {
const [estimateFormOpen, setEstimateFormOpen] = useState(false);
@ -98,6 +99,14 @@ const EstimatesSettings: NextPage = () => {
return (
<>
<CreateUpdateEstimateModal
isOpen={estimateFormOpen}
data={estimateToUpdate}
handleClose={() => {
setEstimateFormOpen(false);
setEstimateToUpdate(undefined);
}}
/>
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
@ -109,68 +118,63 @@ const EstimatesSettings: NextPage = () => {
</Breadcrumbs>
}
>
<CreateUpdateEstimateModal
isOpen={estimateFormOpen}
data={estimateToUpdate}
handleClose={() => {
setEstimateFormOpen(false);
setEstimateToUpdate(undefined);
}}
/>
<section className="flex items-center justify-between">
<h3 className="text-2xl font-semibold">Estimates</h3>
<div className="col-span-12 space-y-5 sm:col-span-7">
<div className="flex items-center gap-2">
<span
className="flex cursor-pointer items-center gap-2 text-theme"
onClick={() => {
setEstimateToUpdate(undefined);
setEstimateFormOpen(true);
}}
>
<PlusIcon className="h-4 w-4" />
Create New Estimate
</span>
{projectDetails?.estimate && (
<SecondaryButton onClick={disableEstimates}>Disable Estimates</SecondaryButton>
)}
<div className="px-24 py-8">
<SettingsHeader />
<section className="flex items-center justify-between">
<h3 className="text-2xl font-semibold">Estimates</h3>
<div className="col-span-12 space-y-5 sm:col-span-7">
<div className="flex items-center gap-2">
<span
className="flex cursor-pointer items-center gap-2 text-theme"
onClick={() => {
setEstimateToUpdate(undefined);
setEstimateFormOpen(true);
}}
>
<PlusIcon className="h-4 w-4" />
Create New Estimate
</span>
{projectDetails?.estimate && (
<SecondaryButton onClick={disableEstimates}>Disable Estimates</SecondaryButton>
)}
</div>
</div>
</div>
</section>
{estimatesList ? (
estimatesList.length > 0 ? (
<section className="mt-4 mb-8 divide-y divide-brand-base rounded-xl border border-brand-base bg-brand-base px-6">
{estimatesList.map((estimate) => (
<SingleEstimate
key={estimate.id}
estimate={estimate}
editEstimate={(estimate) => editEstimate(estimate)}
handleEstimateDelete={(estimateId) => removeEstimate(estimateId)}
</section>
{estimatesList ? (
estimatesList.length > 0 ? (
<section className="mt-4 divide-y divide-brand-base rounded-xl border border-brand-base bg-brand-base px-6">
{estimatesList.map((estimate) => (
<SingleEstimate
key={estimate.id}
estimate={estimate}
editEstimate={(estimate) => editEstimate(estimate)}
handleEstimateDelete={(estimateId) => removeEstimate(estimateId)}
/>
))}
</section>
) : (
<div className="mt-5">
<EmptyState
type="estimate"
title="Create New Estimate"
description="Estimates help you communicate the complexity of an issue. You can create your own estimate and communicate with your team."
imgURL={emptyEstimate}
action={() => {
setEstimateToUpdate(undefined);
setEstimateFormOpen(true);
}}
/>
))}
</section>
</div>
)
) : (
<div className="mt-5">
<EmptyState
type="estimate"
title="Create New Estimate"
description="Estimates help you communicate the complexity of an issue. You can create your own estimate and communicate with your team."
imgURL={emptyEstimate}
action={() => {
setEstimateToUpdate(undefined);
setEstimateFormOpen(true);
}}
/>
</div>
)
) : (
<Loader className="mt-5 space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
<Loader className="mt-5 space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
</div>
</ProjectAuthorizationWrapper>
</>
);

View File

@ -22,6 +22,7 @@ import { IProject } from "types";
import type { NextPage } from "next";
// fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
import { SettingsHeader } from "components/project";
const featuresList = [
{
@ -134,54 +135,57 @@ const FeaturesSettings: NextPage = () => {
</Breadcrumbs>
}
>
<section className="space-y-8">
<h3 className="text-2xl font-semibold">Features</h3>
<div className="space-y-5">
{featuresList.map((feature) => (
<div
key={feature.property}
className="flex items-center justify-between gap-x-8 gap-y-2 rounded-[10px] border border-brand-base bg-brand-base p-5"
>
<div className="flex items-start gap-3">
{feature.icon}
<div>
<h4 className="text-lg font-semibold">{feature.title}</h4>
<p className="text-sm text-brand-secondary">{feature.description}</p>
<div className="px-24 py-8">
<SettingsHeader />
<section className="space-y-8">
<h3 className="text-2xl font-semibold">Features</h3>
<div className="space-y-5">
{featuresList.map((feature) => (
<div
key={feature.property}
className="flex items-center justify-between gap-x-8 gap-y-2 rounded-[10px] border border-brand-base bg-brand-base p-5"
>
<div className="flex items-start gap-3">
{feature.icon}
<div>
<h4 className="text-lg font-semibold">{feature.title}</h4>
<p className="text-sm text-brand-secondary">{feature.description}</p>
</div>
</div>
<ToggleSwitch
value={projectDetails?.[feature.property as keyof IProject]}
onChange={() => {
trackEventServices.trackMiscellaneousEvent(
{
workspaceId: (projectDetails?.workspace as any)?.id,
workspaceSlug,
projectId,
projectIdentifier: projectDetails?.identifier,
projectName: projectDetails?.name,
},
!projectDetails?.[feature.property as keyof IProject]
? getEventType(feature.title, true)
: getEventType(feature.title, false)
);
handleSubmit({
[feature.property]: !projectDetails?.[feature.property as keyof IProject],
});
}}
size="lg"
/>
</div>
<ToggleSwitch
value={projectDetails?.[feature.property as keyof IProject]}
onChange={() => {
trackEventServices.trackMiscellaneousEvent(
{
workspaceId: (projectDetails?.workspace as any)?.id,
workspaceSlug,
projectId,
projectIdentifier: projectDetails?.identifier,
projectName: projectDetails?.name,
},
!projectDetails?.[feature.property as keyof IProject]
? getEventType(feature.title, true)
: getEventType(feature.title, false)
);
handleSubmit({
[feature.property]: !projectDetails?.[feature.property as keyof IProject],
});
}}
size="lg"
/>
</div>
))}
</div>
<div className="flex items-center gap-2">
<a href="https://plane.so/" target="_blank" rel="noreferrer">
<SecondaryButton outline>Plane is open-source, view Roadmap</SecondaryButton>
</a>
<a href="https://github.com/makeplane/plane" target="_blank" rel="noreferrer">
<SecondaryButton outline>Star us on GitHub</SecondaryButton>
</a>
</div>
</section>
))}
</div>
<div className="flex items-center gap-2">
<a href="https://plane.so/" target="_blank" rel="noreferrer">
<SecondaryButton outline>Plane is open-source, view Roadmap</SecondaryButton>
</a>
<a href="https://github.com/makeplane/plane" target="_blank" rel="noreferrer">
<SecondaryButton outline>Star us on GitHub</SecondaryButton>
</a>
</div>
</section>
</div>
</ProjectAuthorizationWrapper>
);
};

View File

@ -12,7 +12,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// services
import projectService from "services/project.service";
// components
import { DeleteProjectModal } from "components/project";
import { DeleteProjectModal, SettingsHeader } from "components/project";
import { ImagePickerPopover } from "components/core";
import EmojiIconPicker from "components/emoji-icon-picker";
// hooks
@ -151,7 +151,8 @@ const GeneralSettings: NextPage = () => {
router.push(`/${workspaceSlug}/projects`);
}}
/>
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit(onSubmit)} className="py-8 px-24">
<SettingsHeader />
<div className="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 items-start gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">

View File

@ -10,7 +10,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import IntegrationService from "services/integration";
import projectService from "services/project.service";
// components
import { SingleIntegration } from "components/project";
import { SettingsHeader, SingleIntegration } from "components/project";
// ui
import { EmptySpace, EmptySpaceItem, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -54,54 +54,61 @@ const ProjectIntegrations: NextPage = () => {
</Breadcrumbs>
}
>
{workspaceIntegrations ? (
workspaceIntegrations.length > 0 ? (
<section className="space-y-8">
<div className="flex flex-col items-start gap-3">
<h3 className="text-2xl font-semibold">Integrations</h3>
<div className="flex items-center gap-3 rounded-[10px] border border-brand-accent/75 bg-brand-accent/5 p-4 text-sm text-brand-base">
<ExclamationIcon height={24} width={24} className="fill-current text-brand-base" />
<p className="leading-5">
Integrations and importers are only available on the cloud version. We plan to
open-source our SDKs in the near future so that the community can request or
contribute integrations as needed.
</p>
<div className="px-24 py-8">
<SettingsHeader />
{workspaceIntegrations ? (
workspaceIntegrations.length > 0 ? (
<section className="space-y-8">
<div className="flex flex-col items-start gap-3">
<h3 className="text-2xl font-semibold">Integrations</h3>
<div className="flex items-center gap-3 rounded-[10px] border border-brand-accent/75 bg-brand-accent/5 p-4 text-sm text-brand-base">
<ExclamationIcon
height={24}
width={24}
className="fill-current text-brand-base"
/>
<p className="leading-5">
Integrations and importers are only available on the cloud version. We plan to
open-source our SDKs in the near future so that the community can request or
contribute integrations as needed.
</p>
</div>
</div>
</div>
<div className="space-y-5">
{workspaceIntegrations.map((integration) => (
<SingleIntegration
key={integration.integration_detail.id}
integration={integration}
<div className="space-y-5">
{workspaceIntegrations.map((integration) => (
<SingleIntegration
key={integration.integration_detail.id}
integration={integration}
/>
))}
</div>
</section>
) : (
<div className="grid h-full w-full place-items-center">
<EmptySpace
title="You haven't added any integration yet."
description="Add GitHub and other integrations to sync your project issues."
Icon={PuzzlePieceIcon}
>
<EmptySpaceItem
title="Add new integration"
Icon={PlusIcon}
action={() => {
router.push(`/${workspaceSlug}/settings/integrations`);
}}
/>
))}
</EmptySpace>
</div>
</section>
)
) : (
<div className="grid h-full w-full place-items-center">
<EmptySpace
title="You haven't added any integration yet."
description="Add GitHub and other integrations to sync your project issues."
Icon={PuzzlePieceIcon}
>
<EmptySpaceItem
title="Add new integration"
Icon={PlusIcon}
action={() => {
router.push(`/${workspaceSlug}/settings/integrations`);
}}
/>
</EmptySpace>
</div>
)
) : (
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
</div>
</ProjectAuthorizationWrapper>
);
};

View File

@ -7,8 +7,6 @@ import useSWR from "swr";
// services
import projectService from "services/project.service";
import issuesService from "services/issues.service";
// lib
import { requiredAdmin } from "lib/auth";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// components
@ -24,10 +22,11 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import { IIssueLabels, UserAuth } from "types";
import type { GetServerSidePropsContext, NextPage } from "next";
import { IIssueLabels } from "types";
import type { NextPage } from "next";
// fetch-keys
import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import { SettingsHeader } from "components/project";
const LabelsSettings: NextPage = () => {
// create/edit label form
@ -103,39 +102,57 @@ const LabelsSettings: NextPage = () => {
</Breadcrumbs>
}
>
<section className="grid grid-cols-12 gap-10">
<div className="col-span-12 sm:col-span-5">
<h3 className="text-2xl font-semibold">Labels</h3>
<p className="text-brand-secondary">Manage the labels of this project.</p>
<PrimaryButton onClick={newLabel} size="sm" className="mt-4">
<span className="flex items-center gap-2">
<PlusIcon className="h-4 w-4" />
New label
</span>
</PrimaryButton>
</div>
<div className="col-span-12 space-y-5 sm:col-span-7">
{labelForm && (
<CreateUpdateLabelInline
labelForm={labelForm}
setLabelForm={setLabelForm}
isUpdating={isUpdating}
labelToUpdate={labelToUpdate}
ref={scrollToRef}
/>
)}
<>
{issueLabels ? (
issueLabels.map((label) => {
const children = issueLabels?.filter((l) => l.parent === label.id);
<div className="px-24 py-8">
<SettingsHeader />
<section className="grid grid-cols-12 gap-10">
<div className="col-span-12 sm:col-span-5">
<h3 className="text-2xl font-semibold">Labels</h3>
<p className="text-brand-secondary">Manage the labels of this project.</p>
<PrimaryButton onClick={newLabel} size="sm" className="mt-4">
<span className="flex items-center gap-2">
<PlusIcon className="h-4 w-4" />
New label
</span>
</PrimaryButton>
</div>
<div className="col-span-12 space-y-5 sm:col-span-7">
{labelForm && (
<CreateUpdateLabelInline
labelForm={labelForm}
setLabelForm={setLabelForm}
isUpdating={isUpdating}
labelToUpdate={labelToUpdate}
ref={scrollToRef}
/>
)}
<>
{issueLabels ? (
issueLabels.map((label) => {
const children = issueLabels?.filter((l) => l.parent === label.id);
if (children && children.length === 0) {
if (!label.parent)
if (children && children.length === 0) {
if (!label.parent)
return (
<SingleLabel
key={label.id}
label={label}
addLabelToGroup={() => addLabelToGroup(label)}
editLabel={(label) => {
editLabel(label);
scrollToRef.current?.scrollIntoView({
behavior: "smooth",
});
}}
handleLabelDelete={handleLabelDelete}
/>
);
} else
return (
<SingleLabel
<SingleLabelGroup
key={label.id}
label={label}
addLabelToGroup={() => addLabelToGroup(label)}
labelChildren={children}
addLabelToGroup={addLabelToGroup}
editLabel={(label) => {
editLabel(label);
scrollToRef.current?.scrollIntoView({
@ -145,34 +162,19 @@ const LabelsSettings: NextPage = () => {
handleLabelDelete={handleLabelDelete}
/>
);
} else
return (
<SingleLabelGroup
key={label.id}
label={label}
labelChildren={children}
addLabelToGroup={addLabelToGroup}
editLabel={(label) => {
editLabel(label);
scrollToRef.current?.scrollIntoView({
behavior: "smooth",
});
}}
handleLabelDelete={handleLabelDelete}
/>
);
})
) : (
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
</>
</div>
</section>
})
) : (
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
</>
</div>
</section>
</div>
</ProjectAuthorizationWrapper>
</>
);

View File

@ -27,6 +27,7 @@ import type { NextPage } from "next";
import { PROJECT_INVITATIONS, PROJECT_MEMBERS, WORKSPACE_DETAILS } from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
import { SettingsHeader } from "components/project";
const MembersSettings: NextPage = () => {
const [inviteModal, setInviteModal] = useState(false);
@ -141,120 +142,123 @@ const MembersSettings: NextPage = () => {
</Breadcrumbs>
}
>
<section className="space-y-8">
<div className="flex items-end justify-between gap-4">
<h3 className="text-2xl font-semibold">Members</h3>
<button
type="button"
className="flex items-center gap-2 text-brand-accent outline-none"
onClick={() => setInviteModal(true)}
>
<PlusIcon className="h-4 w-4" />
Add Member
</button>
</div>
{!projectMembers || !projectInvitations ? (
<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-brand-base rounded-[10px] border border-brand-base bg-brand-base px-6">
{members.length > 0
? members.map((member) => (
<div key={member.id} className="flex items-center justify-between py-6">
<div className="flex items-center gap-x-6 gap-y-2">
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg bg-gray-700 p-4 capitalize text-white">
{member.avatar && member.avatar !== "" ? (
<Image
src={member.avatar}
alt={member.first_name}
layout="fill"
objectFit="cover"
className="rounded-lg"
/>
) : member.first_name !== "" ? (
member.first_name.charAt(0)
) : (
member.email.charAt(0)
)}
</div>
<div>
<h4 className="text-sm">
{member.first_name} {member.last_name}
</h4>
<p className="mt-0.5 text-xs text-brand-secondary">{member.email}</p>
</div>
</div>
<div className="flex items-center gap-2 text-xs">
{!member.member && (
<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">
Pending
</div>
)}
<CustomSelect
label={ROLE[member.role as keyof typeof ROLE]}
value={member.role}
onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
if (!activeWorkspace || !projectDetails) return;
projectService
.updateProjectMember(
activeWorkspace.slug,
projectDetails.id,
member.id,
{
role: value,
}
)
.then((res) => {
setToastAlert({
type: "success",
message: "Member role updated successfully.",
title: "Success",
});
mutateMembers(
(prevData: any) =>
prevData.map((m: any) =>
m.id === member.id ? { ...m, ...res, role: value } : m
),
false
);
})
.catch((err) => {
console.log(err);
});
}}
position="right"
>
{Object.keys(ROLE).map((key) => (
<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">
<XMarkIcon className="h-4 w-4" />
<span>Remove member</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
))
: null}
<div className="px-24 py-8">
<SettingsHeader />
<section className="space-y-8">
<div className="flex items-end justify-between gap-4">
<h3 className="text-2xl font-semibold">Members</h3>
<button
type="button"
className="flex items-center gap-2 text-brand-accent outline-none"
onClick={() => setInviteModal(true)}
>
<PlusIcon className="h-4 w-4" />
Add Member
</button>
</div>
)}
</section>
{!projectMembers || !projectInvitations ? (
<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-brand-base rounded-[10px] border border-brand-base bg-brand-base px-6">
{members.length > 0
? members.map((member) => (
<div key={member.id} className="flex items-center justify-between py-6">
<div className="flex items-center gap-x-6 gap-y-2">
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg bg-gray-700 p-4 capitalize text-white">
{member.avatar && member.avatar !== "" ? (
<Image
src={member.avatar}
alt={member.first_name}
layout="fill"
objectFit="cover"
className="rounded-lg"
/>
) : member.first_name !== "" ? (
member.first_name.charAt(0)
) : (
member.email.charAt(0)
)}
</div>
<div>
<h4 className="text-sm">
{member.first_name} {member.last_name}
</h4>
<p className="mt-0.5 text-xs text-brand-secondary">{member.email}</p>
</div>
</div>
<div className="flex items-center gap-2 text-xs">
{!member.member && (
<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">
Pending
</div>
)}
<CustomSelect
label={ROLE[member.role as keyof typeof ROLE]}
value={member.role}
onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
if (!activeWorkspace || !projectDetails) return;
projectService
.updateProjectMember(
activeWorkspace.slug,
projectDetails.id,
member.id,
{
role: value,
}
)
.then((res) => {
setToastAlert({
type: "success",
message: "Member role updated successfully.",
title: "Success",
});
mutateMembers(
(prevData: any) =>
prevData.map((m: any) =>
m.id === member.id ? { ...m, ...res, role: value } : m
),
false
);
})
.catch((err) => {
console.log(err);
});
}}
position="right"
>
{Object.keys(ROLE).map((key) => (
<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">
<XMarkIcon className="h-4 w-4" />
<span>Remove member</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
))
: null}
</div>
)}
</section>
</div>
</ProjectAuthorizationWrapper>
</>
);

View File

@ -28,6 +28,7 @@ import { getStatesList, orderStateGroups } from "helpers/state.helper";
import type { NextPage } from "next";
// fetch-keys
import { STATES_LIST } from "constants/fetch-keys";
import { SettingsHeader } from "components/project";
const StatesSettings: NextPage = () => {
const [activeGroup, setActiveGroup] = useState<StateGroup>(null);
@ -66,79 +67,82 @@ const StatesSettings: NextPage = () => {
</Breadcrumbs>
}
>
<div className="grid grid-cols-12 gap-10">
<div className="col-span-12 sm:col-span-5">
<h3 className="text-2xl font-semibold text-brand-base">States</h3>
<p className="text-brand-secondary">Manage the states of this project.</p>
</div>
<div className="col-span-12 space-y-8 sm:col-span-7">
{states && projectDetails ? (
Object.keys(orderedStateGroups).map((key) => {
if (orderedStateGroups[key].length !== 0)
return (
<div key={key}>
<div className="mb-2 flex w-full justify-between">
<h4 className="font-medium capitalize">{key}</h4>
<button
type="button"
className="flex items-center gap-2 text-brand-accent outline-none"
onClick={() => setActiveGroup(key as keyof StateGroup)}
>
<PlusIcon className="h-4 w-4" />
Add
</button>
</div>
<div className="divide-y divide-brand-base rounded-[10px] border border-brand-base">
{key === activeGroup && (
<CreateUpdateStateInline
onClose={() => {
setActiveGroup(null);
setSelectedState(null);
}}
data={null}
selectedGroup={key as keyof StateGroup}
/>
)}
{orderedStateGroups[key].map((state, index) =>
state.id !== selectedState ? (
<SingleState
key={state.id}
index={index}
state={state}
statesList={statesList}
handleEditState={() => setSelectedState(state.id)}
handleDeleteState={() => setSelectDeleteState(state.id)}
<div className="px-24 py-8">
<SettingsHeader />
<div className="grid grid-cols-12 gap-10">
<div className="col-span-12 sm:col-span-5">
<h3 className="text-2xl font-semibold text-brand-base">States</h3>
<p className="text-brand-secondary">Manage the states of this project.</p>
</div>
<div className="col-span-12 space-y-8 sm:col-span-7">
{states && projectDetails ? (
Object.keys(orderedStateGroups).map((key) => {
if (orderedStateGroups[key].length !== 0)
return (
<div key={key}>
<div className="mb-2 flex w-full justify-between">
<h4 className="font-medium capitalize">{key}</h4>
<button
type="button"
className="flex items-center gap-2 text-brand-accent outline-none"
onClick={() => setActiveGroup(key as keyof StateGroup)}
>
<PlusIcon className="h-4 w-4" />
Add
</button>
</div>
<div className="divide-y divide-brand-base rounded-[10px] border border-brand-base">
{key === activeGroup && (
<CreateUpdateStateInline
onClose={() => {
setActiveGroup(null);
setSelectedState(null);
}}
data={null}
selectedGroup={key as keyof StateGroup}
/>
) : (
<div
className="border-b border-brand-base last:border-b-0"
key={state.id}
>
<CreateUpdateStateInline
onClose={() => {
setActiveGroup(null);
setSelectedState(null);
}}
data={
statesList?.find((state) => state.id === selectedState) ?? null
}
selectedGroup={key as keyof StateGroup}
)}
{orderedStateGroups[key].map((state, index) =>
state.id !== selectedState ? (
<SingleState
key={state.id}
index={index}
state={state}
statesList={statesList}
handleEditState={() => setSelectedState(state.id)}
handleDeleteState={() => setSelectDeleteState(state.id)}
/>
</div>
)
)}
) : (
<div
className="border-b border-brand-base last:border-b-0"
key={state.id}
>
<CreateUpdateStateInline
onClose={() => {
setActiveGroup(null);
setSelectedState(null);
}}
data={
statesList?.find((state) => state.id === selectedState) ?? null
}
selectedGroup={key as keyof StateGroup}
/>
</div>
)
)}
</div>
</div>
</div>
);
})
) : (
<Loader className="space-y-5 md:w-2/3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
);
})
) : (
<Loader className="space-y-5 md:w-2/3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
</div>
</div>
</div>
</ProjectAuthorizationWrapper>

View File

@ -97,7 +97,7 @@ const ProjectViews: NextPage = () => {
/>
{views ? (
views.length > 0 ? (
<div className="space-y-5">
<div className="space-y-5 p-8">
<h3 className="text-3xl font-semibold text-brand-base">Views</h3>
<div className="divide-y divide-brand-base rounded-[10px] border border-brand-base">
{views.map((view) => (

View File

@ -83,7 +83,7 @@ const ProjectsPage: NextPage = () => {
data={projects?.find((item) => item.id === deleteProject) ?? null}
/>
{projects ? (
<>
<div className="p-8">
{projects.length === 0 ? (
<EmptyState
type="project"
@ -103,7 +103,7 @@ const ProjectsPage: NextPage = () => {
))}
</div>
)}
</>
</div>
) : (
<Loader className="grid grid-cols-3 gap-4">
<Loader.Item height="100px" />

View File

@ -8,6 +8,7 @@ import useSWR from "swr";
import workspaceService from "services/workspace.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// ui
import { SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -38,33 +39,36 @@ const BillingSettings: NextPage = () => {
</Breadcrumbs>
}
>
<section className="space-y-8">
<div>
<h3 className="text-3xl font-bold leading-6">Billing & Plans</h3>
<p className="mt-4 text-sm text-brand-secondary">[Free launch preview] plan Pro</p>
</div>
<div className="space-y-8 md:w-2/3">
<div className="px-24 py-8">
<SettingsHeader />
<section className="space-y-8">
<div>
<div className="w-80 rounded-md border border-brand-base bg-brand-base p-4 text-center">
<h4 className="text-md mb-1 leading-6">Payment due</h4>
<h2 className="text-3xl font-extrabold">--</h2>
<h3 className="text-3xl font-bold leading-6">Billing & Plans</h3>
<p className="mt-4 text-sm text-brand-secondary">[Free launch preview] plan Pro</p>
</div>
<div className="space-y-8 md:w-2/3">
<div>
<div className="w-80 rounded-md border border-brand-base bg-brand-base p-4 text-center">
<h4 className="text-md mb-1 leading-6">Payment due</h4>
<h2 className="text-3xl font-extrabold">--</h2>
</div>
</div>
<div>
<h4 className="text-md mb-1 leading-6">Current plan</h4>
<p className="mb-3 text-sm text-brand-secondary">
You are currently using the free plan
</p>
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
<SecondaryButton outline>View Plans and Upgrade</SecondaryButton>
</a>
</div>
<div>
<h4 className="text-md mb-1 leading-6">Billing history</h4>
<p className="mb-3 text-sm text-brand-secondary">There are no invoices to display</p>
</div>
</div>
<div>
<h4 className="text-md mb-1 leading-6">Current plan</h4>
<p className="mb-3 text-sm text-brand-secondary">
You are currently using the free plan
</p>
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
<SecondaryButton outline>View Plans and Upgrade</SecondaryButton>
</a>
</div>
<div>
<h4 className="text-md mb-1 leading-6">Billing history</h4>
<p className="mb-3 text-sm text-brand-secondary">There are no invoices to display</p>
</div>
</div>
</section>
</section>
</div>
</WorkspaceAuthorizationLayout>
);
};

View File

@ -2,6 +2,7 @@ import { useRouter } from "next/router";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components
import IntegrationGuide from "components/integration/guide";
// ui
@ -22,7 +23,10 @@ const ImportExport: NextPage = () => {
</Breadcrumbs>
}
>
<IntegrationGuide />
<div className="px-24 py-8">
<SettingsHeader />
<IntegrationGuide />
</div>
</WorkspaceAuthorizationLayout>
);
};

View File

@ -14,9 +14,10 @@ import fileService from "services/file.service";
import useToast from "hooks/use-toast";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import SettingsNavbar from "layouts/settings-navbar";
// components
import { ImageUploadModal } from "components/core";
import { DeleteWorkspaceModal } from "components/workspace";
import { DeleteWorkspaceModal, SettingsHeader } from "components/workspace";
// ui
import { Spinner, Input, CustomSelect, SecondaryButton, DangerButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -172,163 +173,167 @@ const WorkspaceSettings: NextPage = () => {
}}
data={activeWorkspace ?? null}
/>
{activeWorkspace ? (
<div className="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Logo</h4>
<p className="text-sm text-brand-secondary">
Max file size is 5MB. Supported file types are .jpg and .png.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<div className="flex items-center gap-4">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
<div className="relative mx-auto flex h-12 w-12">
<Image
src={watch("logo")!}
alt="Workspace Logo"
objectFit="cover"
layout="fill"
className="rounded-md"
priority
/>
</div>
) : (
<div className="relative flex h-12 w-12 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
{activeWorkspace?.name?.charAt(0) ?? "N"}
</div>
)}
</button>
<div className="flex gap-4">
<SecondaryButton
onClick={() => {
setIsImageUploadModalOpen(true);
}}
>
{isImageUploading ? "Uploading..." : "Upload"}
</SecondaryButton>
{activeWorkspace.logo && activeWorkspace.logo !== "" && (
<DangerButton onClick={() => handleDelete(activeWorkspace.logo)}>
{isImageRemoving ? "Removing..." : "Remove"}
</DangerButton>
)}
<div className="px-24 py-8">
<SettingsHeader />
{activeWorkspace ? (
<div className="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Logo</h4>
<p className="text-sm text-brand-secondary">
Max file size is 5MB. Supported file types are .jpg and .png.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<div className="flex items-center gap-4">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
<div className="relative mx-auto flex h-12 w-12">
<Image
src={watch("logo")!}
alt="Workspace Logo"
objectFit="cover"
layout="fill"
className="rounded-md"
priority
/>
</div>
) : (
<div className="relative flex h-12 w-12 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
{activeWorkspace?.name?.charAt(0) ?? "N"}
</div>
)}
</button>
<div className="flex gap-4">
<SecondaryButton
onClick={() => {
setIsImageUploadModalOpen(true);
}}
>
{isImageUploading ? "Uploading..." : "Upload"}
</SecondaryButton>
{activeWorkspace.logo && activeWorkspace.logo !== "" && (
<DangerButton onClick={() => handleDelete(activeWorkspace.logo)}>
{isImageRemoving ? "Removing..." : "Remove"}
</DangerButton>
)}
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">URL</h4>
<p className="text-sm text-brand-secondary">Your workspace URL.</p>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">URL</h4>
<p className="text-sm text-brand-secondary">Your workspace URL.</p>
</div>
<div className="col-span-12 flex items-center gap-2 sm:col-span-6">
<Input
id="url"
name="url"
autoComplete="off"
register={register}
error={errors.name}
className="w-full"
value={`${
typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "")
}/${activeWorkspace.slug}`}
disabled
/>
<SecondaryButton
className="h-min"
onClick={() =>
copyTextToClipboard(
`${typeof window !== "undefined" && window.location.origin}/${
activeWorkspace.slug
}`
).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Workspace link copied to clipboard.",
});
})
}
outline
>
<LinkIcon className="h-[18px] w-[18px]" />
</SecondaryButton>
</div>
</div>
<div className="col-span-12 flex items-center gap-2 sm:col-span-6">
<Input
id="url"
name="url"
autoComplete="off"
register={register}
error={errors.name}
className="w-full"
value={`${
typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "")
}/${activeWorkspace.slug}`}
disabled
/>
<SecondaryButton
className="h-min"
onClick={() =>
copyTextToClipboard(
`${typeof window !== "undefined" && window.location.origin}/${
activeWorkspace.slug
}`
).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Workspace link copied to clipboard.",
});
})
}
outline
>
<LinkIcon className="h-[18px] w-[18px]" />
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Name</h4>
<p className="text-sm text-brand-secondary">Give a name to your workspace.</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Input
id="name"
name="name"
placeholder="Name"
autoComplete="off"
register={register}
error={errors.name}
validations={{
required: "Name is required",
}}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Company Size</h4>
<p className="text-sm text-brand-secondary">How big is your company?</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Controller
name="company_size"
control={control}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={value ? value.toString() : "Select company size"}
input
>
{COMPANY_SIZE?.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
</div>
<div className="sm:text-right">
<SecondaryButton onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Update Workspace"}
</SecondaryButton>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Name</h4>
<p className="text-sm text-brand-secondary">Give a name to your workspace.</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Input
id="name"
name="name"
placeholder="Name"
autoComplete="off"
register={register}
error={errors.name}
validations={{
required: "Name is required",
}}
/>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Danger Zone</h4>
<p className="text-sm text-brand-secondary">
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.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<DangerButton onClick={() => setIsOpen(true)} outline>
Delete the workspace
</DangerButton>
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Company Size</h4>
<p className="text-sm text-brand-secondary">How big is your company?</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Controller
name="company_size"
control={control}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={value ? value.toString() : "Select company size"}
input
>
{COMPANY_SIZE?.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
<Spinner />
</div>
<div className="sm:text-right">
<SecondaryButton onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Update Workspace"}
</SecondaryButton>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Danger Zone</h4>
<p className="text-sm text-brand-secondary">
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.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<DangerButton onClick={() => setIsOpen(true)} outline>
Delete the workspace
</DangerButton>
</div>
</div>
</div>
) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
<Spinner />
</div>
)}
)}
</div>
</WorkspaceAuthorizationLayout>
);
};

View File

@ -9,6 +9,7 @@ import workspaceService from "services/workspace.service";
import IntegrationService from "services/integration";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components
import { SingleIntegrationCard } from "components/integration";
// ui
@ -46,31 +47,34 @@ const WorkspaceIntegrations: NextPage = () => {
</Breadcrumbs>
}
>
<section className="space-y-8">
<div className="flex flex-col items-start gap-3">
<h3 className="text-2xl font-semibold">Integrations</h3>
<div className="flex items-center gap-3 rounded-[10px] border border-brand-accent/75 bg-brand-accent/5 p-4 text-sm text-brand-base">
<ExclamationIcon height={24} width={24} className="fill-current text-brand-base" />
<p className="leading-5">
Integrations and importers are only available on the cloud version. We plan to
open-source our SDKs in the near future so that the community can request or
contribute integrations as needed.
</p>
<div className="px-24 py-8">
<SettingsHeader />
<section className="space-y-8">
<div className="flex flex-col items-start gap-3">
<h3 className="text-2xl font-semibold">Integrations</h3>
<div className="flex items-center gap-3 rounded-[10px] border border-brand-accent/75 bg-brand-accent/5 p-4 text-sm text-brand-base">
<ExclamationIcon height={24} width={24} className="fill-current text-brand-base" />
<p className="leading-5">
Integrations and importers are only available on the cloud version. We plan to
open-source our SDKs in the near future so that the community can request or
contribute integrations as needed.
</p>
</div>
</div>
</div>
<div className="space-y-5">
{appIntegrations ? (
appIntegrations.map((integration) => (
<SingleIntegrationCard key={integration.id} integration={integration} />
))
) : (
<Loader className="space-y-5">
<Loader.Item height="60px" />
<Loader.Item height="60px" />
</Loader>
)}
</div>
</section>
<div className="space-y-5">
{appIntegrations ? (
appIntegrations.map((integration) => (
<SingleIntegrationCard key={integration.id} integration={integration} />
))
) : (
<Loader className="space-y-5">
<Loader.Item height="60px" />
<Loader.Item height="60px" />
</Loader>
)}
</div>
</section>
</div>
</WorkspaceAuthorizationLayout>
);
};

View File

@ -11,6 +11,7 @@ import useToast from "hooks/use-toast";
import workspaceService from "services/workspace.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
@ -137,117 +138,120 @@ const MembersSettings: NextPage = () => {
</Breadcrumbs>
}
>
<section className="space-y-8">
<div className="flex items-end justify-between gap-4">
<h3 className="text-2xl font-semibold">Members</h3>
<button
type="button"
className="flex items-center gap-2 text-brand-accent outline-none"
onClick={() => setInviteModal(true)}
>
<PlusIcon className="h-4 w-4" />
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-brand-base rounded-[10px] border border-brand-base bg-brand-base px-6">
{members.length > 0
? members.map((member) => (
<div key={member.id} className="flex items-center justify-between py-6">
<div className="flex items-center gap-x-8 gap-y-2">
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg bg-gray-700 p-4 capitalize text-white">
{member.avatar && member.avatar !== "" ? (
<Image
src={member.avatar}
alt={member.first_name}
layout="fill"
objectFit="cover"
className="rounded-lg"
/>
) : member.first_name !== "" ? (
member.first_name.charAt(0)
) : (
member.email.charAt(0)
)}
</div>
<div>
<h4 className="text-sm">
{member.first_name} {member.last_name}
</h4>
<p className="text-xs text-brand-secondary">{member.email}</p>
</div>
</div>
<div className="flex items-center gap-2 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>
)}
<CustomSelect
label={ROLE[member.role as keyof typeof ROLE]}
value={member.role}
onChange={(value: any) => {
workspaceService
.updateWorkspaceMember(activeWorkspace?.slug as string, member.id, {
role: value,
})
.then(() => {
mutateMembers(
(prevData) =>
prevData?.map((m) =>
m.id === member.id ? { ...m, role: value } : m
),
false
);
setToastAlert({
title: "Success",
type: "success",
message: "Member role updated successfully.",
});
})
.catch(() => {
setToastAlert({
title: "Error",
type: "error",
message: "An error occurred while updating member role.",
});
});
}}
position="right"
>
{Object.keys(ROLE).map((key) => (
<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);
}
}}
>
Remove member
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
))
: null}
<div className="px-24 py-8">
<SettingsHeader />
<section className="space-y-8">
<div className="flex items-end justify-between gap-4">
<h3 className="text-2xl font-semibold">Members</h3>
<button
type="button"
className="flex items-center gap-2 text-brand-accent outline-none"
onClick={() => setInviteModal(true)}
>
<PlusIcon className="h-4 w-4" />
Add Member
</button>
</div>
)}
</section>
{!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-brand-base rounded-[10px] border border-brand-base bg-brand-base px-6">
{members.length > 0
? members.map((member) => (
<div key={member.id} className="flex items-center justify-between py-6">
<div className="flex items-center gap-x-8 gap-y-2">
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg bg-gray-700 p-4 capitalize text-white">
{member.avatar && member.avatar !== "" ? (
<Image
src={member.avatar}
alt={member.first_name}
layout="fill"
objectFit="cover"
className="rounded-lg"
/>
) : member.first_name !== "" ? (
member.first_name.charAt(0)
) : (
member.email.charAt(0)
)}
</div>
<div>
<h4 className="text-sm">
{member.first_name} {member.last_name}
</h4>
<p className="text-xs text-brand-secondary">{member.email}</p>
</div>
</div>
<div className="flex items-center gap-2 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>
)}
<CustomSelect
label={ROLE[member.role as keyof typeof ROLE]}
value={member.role}
onChange={(value: any) => {
workspaceService
.updateWorkspaceMember(activeWorkspace?.slug as string, member.id, {
role: value,
})
.then(() => {
mutateMembers(
(prevData) =>
prevData?.map((m) =>
m.id === member.id ? { ...m, role: value } : m
),
false
);
setToastAlert({
title: "Success",
type: "success",
message: "Member role updated successfully.",
});
})
.catch(() => {
setToastAlert({
title: "Error",
type: "error",
message: "An error occurred while updating member role.",
});
});
}}
position="right"
>
{Object.keys(ROLE).map((key) => (
<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);
}
}}
>
Remove member
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
))
: null}
</div>
)}
</section>
</div>
</WorkspaceAuthorizationLayout>
</>
);

View File

@ -112,6 +112,7 @@ body {
.horizontal-scroll-enable::-webkit-scrollbar {
display: block;
height: 7px;
width: 0;
}
.horizontal-scroll-enable::-webkit-scrollbar-track {

View File

@ -12,10 +12,6 @@ module.exports = {
theme: {
extend: {
colors: {
theme: "#3f76ff",
"hover-gray": "#f5f5f5",
primary: "#f9fafb", // gray-50
secondary: "white",
brand: {
accent: withOpacity("--color-accent"),
base: withOpacity("--color-bg-base"),