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 ( return (
<> <>
{groupedByIssues ? ( {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) => { {Object.keys(groupedByIssues).map((singleGroup, index) => {
const currentState = const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; 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"> <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}`}> <Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <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} {issue.link_count}
</div> </div>
</Tooltip> </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"> <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}`}> <Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <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} {issue.attachment_count}
</div> </div>
</Tooltip> </Tooltip>

View File

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

View File

@ -81,7 +81,7 @@ export const SelectRepository: React.FC<Props> = ({
{userRepositories && options.length < totalCount && ( {userRepositories && options.length < totalCount && (
<button <button
type="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)} onClick={() => setSize(size + 1)}
disabled={isValidating} disabled={isValidating}
> >

View File

@ -82,8 +82,8 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
const isNotAllowed = false; const isNotAllowed = false;
return ( return (
<div className="mx-6 border-b border-brand-base last:border-b-0"> <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 py-3"> <div key={issue.id} className="flex items-center justify-between gap-2">
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}> <Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2"> <a className="group relative flex items-center gap-2">
{properties?.key && ( {properties?.key && (
@ -171,7 +171,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
</Tooltip> </Tooltip>
)} )}
{properties.link && ( {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}`}> <Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <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 text-brand-secondary" />
@ -181,7 +181,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
</div> </div>
)} )}
{properties.attachment_count && ( {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}`}> <Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <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 text-brand-secondary" />

View File

@ -1,6 +1,7 @@
export * from "./create-project-modal"; export * from "./create-project-modal";
export * from "./delete-project-modal"; export * from "./delete-project-modal";
export * from "./sidebar-list"; export * from "./sidebar-list";
export * from "./settings-header"
export * from "./single-integration-card"; export * from "./single-integration-card";
export * from "./single-project-card"; export * from "./single-project-card";
export * from "./single-sidebar-project"; 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-list";
export * from "./issues-pie-chart"; export * from "./issues-pie-chart";
export * from "./issues-stats"; export * from "./issues-stats";
export * from "./settings-header";
export * from "./sidebar-dropdown"; export * from "./sidebar-dropdown";
export * from "./sidebar-menu"; 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 }) => ( 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="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 flex-wrap items-center gap-2"> <div className="flex items-center gap-2">
<div className="block md:hidden"> <div className="block md:hidden">
<button <button
type="button" type="button"

View File

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

View File

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

View File

@ -32,25 +32,21 @@ type Meta = {
type Props = { type Props = {
meta?: Meta; meta?: Meta;
children: React.ReactNode; children: React.ReactNode;
noPadding?: boolean;
noHeader?: boolean; noHeader?: boolean;
bg?: "primary" | "secondary"; bg?: "primary" | "secondary";
breadcrumbs?: JSX.Element; breadcrumbs?: JSX.Element;
left?: JSX.Element; left?: JSX.Element;
right?: JSX.Element; right?: JSX.Element;
profilePage?: boolean;
}; };
export const WorkspaceAuthorizationLayout: React.FC<Props> = ({ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
meta, meta,
children, children,
noPadding = false,
noHeader = false, noHeader = false,
bg = "primary", bg = "primary",
breadcrumbs, breadcrumbs,
left, left,
right, right,
profilePage = false,
}) => { }) => {
const [toggleSidebar, setToggleSidebar] = useState(false); const [toggleSidebar, setToggleSidebar] = useState(false);
@ -101,7 +97,7 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
<UserAuthorizationLayout> <UserAuthorizationLayout>
<Container meta={meta}> <Container meta={meta}>
<CommandPalette /> <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} /> <AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
{settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? ( {settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
<NotAuthorizedView <NotAuthorizedView
@ -117,7 +113,15 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
type="workspace" 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 && ( {!noHeader && (
<AppHeader <AppHeader
breadcrumbs={breadcrumbs} breadcrumbs={breadcrumbs}
@ -126,33 +130,10 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
setToggleSidebar={setToggleSidebar} setToggleSidebar={setToggleSidebar}
/> />
)} )}
<div <div className="h-full w-full overflow-hidden">
className={`flex w-full flex-grow flex-col ${ <div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
noPadding ? "" : settingsLayout || profilePage ? "p-8 lg:px-28" : "p-8" {children}
} ${ </div>
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> </div>
</main> </main>
)} )}

View File

@ -45,7 +45,7 @@ const WorkspacePage: NextPage = () => {
isOpen={isProductUpdatesModalOpen} isOpen={isProductUpdatesModalOpen}
setIsOpen={setIsProductUpdatesModalOpen} setIsOpen={setIsProductUpdatesModalOpen}
/> />
<div className="h-full w-full"> <div className="p-8">
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<div <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" 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" /> <BreadcrumbItem title="My Issues" />
</Breadcrumbs> </Breadcrumbs>
} }
noPadding
right={ right={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{myIssues && myIssues.length > 0 && ( {myIssues && myIssues.length > 0 && (
@ -115,55 +114,42 @@ const MyIssuesPage: NextPage = () => {
{myIssues ? ( {myIssues ? (
<> <>
{myIssues.length > 0 ? ( {myIssues.length > 0 ? (
<div className="flex flex-col space-y-5"> <Disclosure as="div" defaultOpen>
<Disclosure as="div" defaultOpen> {({ open }) => (
{({ open }) => ( <div>
<div className="rounded-[10px] border border-brand-base bg-brand-base"> <div className="flex items-center px-4 py-2.5">
<div <Disclosure.Button>
className={`flex items-center justify-start bg-brand-surface-1 px-4 py-2.5 ${ <div className="flex items-center gap-x-2">
open ? "rounded-t-[10px]" : "rounded-[10px]" <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}
<Disclosure.Button> </span>
<div className="flex items-center gap-x-2"> </div>
<span> </Disclosure.Button>
<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>
</div> </div>
)} <Transition
</Disclosure> show={open}
</div> 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"> <div className="flex h-full w-full flex-col items-center justify-center px-4">
<EmptySpace <EmptySpace

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -312,7 +312,7 @@ const SinglePage: NextPage = () => {
} }
> >
{pageDetails ? ( {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"> <div className="flex items-center justify-between gap-2 px-3">
<button <button
type="button" type="button"

View File

@ -25,7 +25,7 @@ import { RecentPagesList, CreateUpdatePageModal, TPagesListProps } from "compone
import { Input, PrimaryButton } from "components/ui"; import { Input, PrimaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import {ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; import { ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
// types // types
import { IPage, TPageViewProps } from "types"; import { IPage, TPageViewProps } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
@ -195,7 +195,7 @@ const ProjectPages: NextPage = () => {
</PrimaryButton> </PrimaryButton>
} }
> >
<div className="space-y-4"> <div className="space-y-4 p-8">
<form <form
onSubmit={handleSubmit(createPage)} onSubmit={handleSubmit(createPage)}
className="relative mb-12 flex items-center justify-between gap-2 rounded-[6px] border border-brand-base p-2 shadow" 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"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS, PROJECT_MEMBERS } from "constants/fetch-keys"; import { PROJECTS_LIST, PROJECT_DETAILS, PROJECT_MEMBERS } from "constants/fetch-keys";
import { SettingsHeader } from "components/project";
const defaultValues: Partial<IProject> = { const defaultValues: Partial<IProject> = {
project_lead: null, project_lead: null,
@ -103,7 +104,8 @@ const ControlSettings: NextPage = () => {
</Breadcrumbs> </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="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 items-start gap-4 sm:gap-16"> <div className="grid grid-cols-12 items-start gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6"> <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"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
import { SettingsHeader } from "components/project";
const EstimatesSettings: NextPage = () => { const EstimatesSettings: NextPage = () => {
const [estimateFormOpen, setEstimateFormOpen] = useState(false); const [estimateFormOpen, setEstimateFormOpen] = useState(false);
@ -98,6 +99,14 @@ const EstimatesSettings: NextPage = () => {
return ( return (
<> <>
<CreateUpdateEstimateModal
isOpen={estimateFormOpen}
data={estimateToUpdate}
handleClose={() => {
setEstimateFormOpen(false);
setEstimateToUpdate(undefined);
}}
/>
<ProjectAuthorizationWrapper <ProjectAuthorizationWrapper
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
@ -109,68 +118,63 @@ const EstimatesSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<CreateUpdateEstimateModal <div className="px-24 py-8">
isOpen={estimateFormOpen} <SettingsHeader />
data={estimateToUpdate} <section className="flex items-center justify-between">
handleClose={() => { <h3 className="text-2xl font-semibold">Estimates</h3>
setEstimateFormOpen(false); <div className="col-span-12 space-y-5 sm:col-span-7">
setEstimateToUpdate(undefined); <div className="flex items-center gap-2">
}} <span
/> className="flex cursor-pointer items-center gap-2 text-theme"
<section className="flex items-center justify-between"> onClick={() => {
<h3 className="text-2xl font-semibold">Estimates</h3> setEstimateToUpdate(undefined);
<div className="col-span-12 space-y-5 sm:col-span-7"> setEstimateFormOpen(true);
<div className="flex items-center gap-2"> }}
<span >
className="flex cursor-pointer items-center gap-2 text-theme" <PlusIcon className="h-4 w-4" />
onClick={() => { Create New Estimate
setEstimateToUpdate(undefined); </span>
setEstimateFormOpen(true); {projectDetails?.estimate && (
}} <SecondaryButton onClick={disableEstimates}>Disable Estimates</SecondaryButton>
> )}
<PlusIcon className="h-4 w-4" /> </div>
Create New Estimate
</span>
{projectDetails?.estimate && (
<SecondaryButton onClick={disableEstimates}>Disable Estimates</SecondaryButton>
)}
</div> </div>
</div> </section>
</section> {estimatesList ? (
{estimatesList ? ( estimatesList.length > 0 ? (
estimatesList.length > 0 ? ( <section className="mt-4 divide-y divide-brand-base rounded-xl border border-brand-base bg-brand-base px-6">
<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) => (
{estimatesList.map((estimate) => ( <SingleEstimate
<SingleEstimate key={estimate.id}
key={estimate.id} estimate={estimate}
estimate={estimate} editEstimate={(estimate) => editEstimate(estimate)}
editEstimate={(estimate) => editEstimate(estimate)} handleEstimateDelete={(estimateId) => removeEstimate(estimateId)}
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);
}}
/> />
))} </div>
</section> )
) : ( ) : (
<div className="mt-5"> <Loader className="mt-5 space-y-5">
<EmptyState <Loader.Item height="40px" />
type="estimate" <Loader.Item height="40px" />
title="Create New Estimate" <Loader.Item height="40px" />
description="Estimates help you communicate the complexity of an issue. You can create your own estimate and communicate with your team." <Loader.Item height="40px" />
imgURL={emptyEstimate} </Loader>
action={() => { )}
setEstimateToUpdate(undefined); </div>
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>
)}
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>
</> </>
); );

View File

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

View File

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

View File

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

View File

@ -27,6 +27,7 @@ import type { NextPage } from "next";
import { PROJECT_INVITATIONS, PROJECT_MEMBERS, WORKSPACE_DETAILS } from "constants/fetch-keys"; import { PROJECT_INVITATIONS, PROJECT_MEMBERS, WORKSPACE_DETAILS } from "constants/fetch-keys";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
import { SettingsHeader } from "components/project";
const MembersSettings: NextPage = () => { const MembersSettings: NextPage = () => {
const [inviteModal, setInviteModal] = useState(false); const [inviteModal, setInviteModal] = useState(false);
@ -141,120 +142,123 @@ const MembersSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<section className="space-y-8"> <div className="px-24 py-8">
<div className="flex items-end justify-between gap-4"> <SettingsHeader />
<h3 className="text-2xl font-semibold">Members</h3> <section className="space-y-8">
<button <div className="flex items-end justify-between gap-4">
type="button" <h3 className="text-2xl font-semibold">Members</h3>
className="flex items-center gap-2 text-brand-accent outline-none" <button
onClick={() => setInviteModal(true)} type="button"
> className="flex items-center gap-2 text-brand-accent outline-none"
<PlusIcon className="h-4 w-4" /> onClick={() => setInviteModal(true)}
Add Member >
</button> <PlusIcon className="h-4 w-4" />
</div> Add Member
{!projectMembers || !projectInvitations ? ( </button>
<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> </div>
)} {!projectMembers || !projectInvitations ? (
</section> <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> </ProjectAuthorizationWrapper>
</> </>
); );

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import useSWR from "swr";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// ui // ui
import { SecondaryButton } from "components/ui"; import { SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -38,33 +39,36 @@ const BillingSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<section className="space-y-8"> <div className="px-24 py-8">
<div> <SettingsHeader />
<h3 className="text-3xl font-bold leading-6">Billing & Plans</h3> <section className="space-y-8">
<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>
<div className="w-80 rounded-md border border-brand-base bg-brand-base p-4 text-center"> <h3 className="text-3xl font-bold leading-6">Billing & Plans</h3>
<h4 className="text-md mb-1 leading-6">Payment due</h4> <p className="mt-4 text-sm text-brand-secondary">[Free launch preview] plan Pro</p>
<h2 className="text-3xl font-extrabold">--</h2> </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> </div>
<div> </section>
<h4 className="text-md mb-1 leading-6">Current plan</h4> </div>
<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>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
); );
}; };

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import useToast from "hooks/use-toast";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components // components
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove"; import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal"; import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
@ -137,117 +138,120 @@ const MembersSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<section className="space-y-8"> <div className="px-24 py-8">
<div className="flex items-end justify-between gap-4"> <SettingsHeader />
<h3 className="text-2xl font-semibold">Members</h3> <section className="space-y-8">
<button <div className="flex items-end justify-between gap-4">
type="button" <h3 className="text-2xl font-semibold">Members</h3>
className="flex items-center gap-2 text-brand-accent outline-none" <button
onClick={() => setInviteModal(true)} type="button"
> className="flex items-center gap-2 text-brand-accent outline-none"
<PlusIcon className="h-4 w-4" /> onClick={() => setInviteModal(true)}
Add Member >
</button> <PlusIcon className="h-4 w-4" />
</div> Add Member
{!workspaceMembers || !workspaceInvitations ? ( </button>
<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> </div>
)} {!workspaceMembers || !workspaceInvitations ? (
</section> <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> </WorkspaceAuthorizationLayout>
</> </>
); );

View File

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

View File

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