chore: empty state and project active cycle improvement (#3472)

* chore: pages empty state improvement

* chore: workspace all issues empty state improvement

* chore: profile issue empty state improvement

* chore: empty state sm size updated

* chore: project view empty state image updated

* chore: dashboard widgets permission uodated

* chore: draft issues and project issue empty state image

* chore: active cycle label updated
This commit is contained in:
Anmol Singh Bhatia 2024-01-25 18:00:45 +05:30 committed by GitHub
parent 5c912b8821
commit 6c6b764421
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 215 additions and 131 deletions

View File

@ -138,6 +138,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
});
};
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date ?? new Date());
return (
<div>
<CycleCreateUpdateModal
@ -177,7 +179,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
}}
>
{currentCycle.value === "current"
? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
? `${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`
: `${currentCycle.label}`}
</span>
)}

View File

@ -141,6 +141,8 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date ?? new Date());
return (
<>
<CycleCreateUpdateModal
@ -202,7 +204,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
}}
>
{currentCycle.value === "current"
? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
? `${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`
: `${currentCycle.label}`}
</span>
)}

View File

@ -78,7 +78,7 @@ export const RecentProjectsWidget: React.FC<WidgetProps> = observer((props) => {
const { fetchWidgetStats, getWidgetStats } = useDashboard();
// derived values
const widgetStats = getWidgetStats<TRecentProjectsWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
const canCreateProject = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
const canCreateProject = currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
useEffect(() => {
fetchWidgetStats(workspaceSlug, dashboardId, {

View File

@ -68,7 +68,7 @@ export const EmptyState: React.FC<Props> = ({
<div className="flex items-center justify-center min-h-full min-w-full overflow-y-auto py-10 px-20">
<div
className={cn("flex flex-col gap-5", {
"min-w-[24rem] max-w-[38rem]": size === "sm",
"min-w-[24rem] max-w-[45rem]": size === "sm",
"min-w-[30rem] max-w-[60rem]": size === "lg",
})}
>

View File

@ -187,10 +187,12 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
size="sm"
primaryButton={
(workspaceProjectIds ?? []).length > 0
? {
text: "Create new issue",
onClick: () => commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT),
}
? currentView !== "custom-view" && currentView !== "subscribed"
? {
text: "Create new issue",
onClick: () => commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT),
}
: undefined
: {
text: "Start your first project",
onClick: () => commandPaletteStore.toggleCreateProjectModal(true),

View File

@ -9,9 +9,9 @@ import { useProjectPages } from "hooks/store/use-project-specific-pages";
export const ArchivedPagesList: FC = observer(() => {
const projectPageStore = useProjectPages();
const { archivedPageIds, archivedProjectLoader } = projectPageStore;
const { archivedPageIds, archivedPageLoader } = projectPageStore;
if (archivedProjectLoader) {
if (archivedPageLoader) {
return (
<div className="flex items-center justify-center h-full w-full">
<Spinner />

View File

@ -5,45 +5,21 @@ import Link from "next/link";
// components
import { ProfileIssuesFilter } from "components/profile";
// constants
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "constants/profile";
type Props = {
isAuthorized: boolean;
showProfileIssuesFilter?: boolean;
};
const viewerTabs = [
{
route: "",
label: "Summary",
selected: "/[workspaceSlug]/profile/[userId]",
},
];
const adminTabs = [
{
route: "assigned",
label: "Assigned",
selected: "/[workspaceSlug]/profile/[userId]/assigned",
},
{
route: "created",
label: "Created",
selected: "/[workspaceSlug]/profile/[userId]/created",
},
{
route: "subscribed",
label: "Subscribed",
selected: "/[workspaceSlug]/profile/[userId]/subscribed",
},
];
export const ProfileNavbar: React.FC<Props> = (props) => {
const { isAuthorized, showProfileIssuesFilter } = props;
const router = useRouter();
const { workspaceSlug, userId } = router.query;
const tabsList = isAuthorized ? [...viewerTabs, ...adminTabs] : viewerTabs;
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
return (
<div className="sticky -top-0.5 z-10 flex items-center justify-between gap-4 border-b border-custom-border-300 bg-custom-background-100 px-4 sm:px-5 md:static">

View File

@ -7,8 +7,12 @@ import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/ro
import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root";
import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues";
import { Spinner } from "@plane/ui";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// hooks
import { useIssues } from "hooks/store";
import { useIssues, useUser } from "hooks/store";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
import { PROFILE_EMPTY_STATE_DETAILS } from "constants/profile";
import { EIssuesStoreType } from "constants/issue";
interface IProfileIssuesPage {
@ -23,7 +27,10 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
workspaceSlug: string;
userId: string;
};
const {
membership: { currentWorkspaceRole },
currentUser,
} = useUser();
const {
issues: { loader, groupedIssueIds, fetchIssues },
issuesFilter: { issueFilters, fetchFilters },
@ -39,8 +46,12 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
}
);
const emptyStateImage = getEmptyStateImagePath("profile", type, currentUser?.theme.theme === "light");
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
return (
<>
{loader === "init-loader" ? (
@ -49,16 +60,28 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
</div>
) : (
<>
<ProfileIssuesAppliedFiltersRoot />
<div className="-z-1 relative h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ProfileIssuesListLayout />
) : activeLayout === "kanban" ? (
<ProfileIssuesKanBanLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
{groupedIssueIds ? (
<>
<ProfileIssuesAppliedFiltersRoot />
<div className="-z-1 relative h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ProfileIssuesListLayout />
) : activeLayout === "kanban" ? (
<ProfileIssuesKanBanLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</>
) : (
<EmptyState
image={emptyStateImage}
title={PROFILE_EMPTY_STATE_DETAILS[type].title}
description={PROFILE_EMPTY_STATE_DETAILS[type].description}
size="sm"
disabled={!isEditingAllowed}
/>
)}
</>
)}
</>

View File

@ -43,7 +43,7 @@ export const ProjectViewsList = observer(() => {
const viewsList = projectViewIds.map((viewId) => getViewById(viewId));
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", currentUser?.theme.theme === "light");
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", currentUser?.theme.theme === "light");
const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(query.toLowerCase()));

View File

@ -38,3 +38,47 @@ export const PROFILE_ACTION_LINKS: {
Icon: Settings2,
},
];
export const PROFILE_VIEWER_TAB = [
{
route: "",
label: "Summary",
selected: "/[workspaceSlug]/profile/[userId]",
},
];
export const PROFILE_ADMINS_TAB = [
{
route: "assigned",
label: "Assigned",
selected: "/[workspaceSlug]/profile/[userId]/assigned",
},
{
route: "created",
label: "Created",
selected: "/[workspaceSlug]/profile/[userId]/created",
},
{
route: "subscribed",
label: "Subscribed",
selected: "/[workspaceSlug]/profile/[userId]/subscribed",
},
];
export const PROFILE_EMPTY_STATE_DETAILS = {
assigned: {
key: "assigned",
title: "No issues are assigned to you",
description: "Issues assigned to you can be tracked from here.",
},
subscribed: {
key: "created",
title: "No issues yet",
description: "All issues created by you come here, track them here directly.",
},
created: {
key: "subscribed",
title: "No issues yet",
description: "Subscribe to issues you are interested in, track all of them here.",
},
};

View File

@ -5,13 +5,14 @@ import { Tab } from "@headlessui/react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// hooks
import { useUser } from "hooks/store";
import { useApplication, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
import useUserAuth from "hooks/use-user-auth";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { RecentPagesList, CreateUpdatePageModal } from "components/pages";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
import { PagesHeader } from "components/headers";
import { Spinner } from "@plane/ui";
// types
@ -19,6 +20,7 @@ import { NextPageWithLayout } from "lib/types";
// constants
import { PAGE_TABS_LIST } from "constants/page";
import { useProjectPages } from "hooks/store/use-project-page";
import { EUserWorkspaceRoles } from "constants/workspace";
const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), {
ssr: false,
@ -47,9 +49,17 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
// states
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
// store
const { currentUser, currentUserLoader } = useUser();
const {
currentUser,
currentUserLoader,
membership: { currentProjectRole },
} = useUser();
const {
commandPalette: { toggleCreatePageModal },
} = useApplication();
const { fetchProjectPages, fetchArchivedProjectPages, loader } = useProjectPages();
const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } =
useProjectPages();
// hooks
const {} = useUserAuth({ user: currentUser, isLoading: currentUserLoader });
// local storage
@ -84,7 +94,11 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
}
};
if (loader)
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", currentUser?.theme.theme === "light");
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
if (loader || archivedPageLoader)
return (
<div className="flex items-center justify-center h-full w-full">
<Spinner />
@ -93,79 +107,100 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
return (
<>
{workspaceSlug && projectId && (
<CreateUpdatePageModal
isOpen={createUpdatePageModal}
handleClose={() => setCreateUpdatePageModal(false)}
projectId={projectId.toString()}
{projectPageIds && archivedPageIds && projectPageIds.length + archivedPageIds.length > 0 ? (
<>
{workspaceSlug && projectId && (
<CreateUpdatePageModal
isOpen={createUpdatePageModal}
handleClose={() => setCreateUpdatePageModal(false)}
projectId={projectId.toString()}
/>
)}
<div className="flex h-full flex-col space-y-5 overflow-hidden p-6">
<div className="flex justify-between gap-4">
<h3 className="text-2xl font-semibold text-custom-text-100">Pages</h3>
</div>
<Tab.Group
as={Fragment}
defaultIndex={currentTabValue(pageTab)}
onChange={(i) => {
switch (i) {
case 0:
return setPageTab("Recent");
case 1:
return setPageTab("All");
case 2:
return setPageTab("Favorites");
case 3:
return setPageTab("Private");
case 4:
return setPageTab("Shared");
case 5:
return setPageTab("Archived");
default:
return setPageTab("All");
}
}}
>
<Tab.List as="div" className="mb-6 flex items-center justify-between">
<div className="flex flex-wrap items-center gap-4">
{PAGE_TABS_LIST.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`rounded-full border px-5 py-1.5 text-sm outline-none ${
selected
? "border-custom-primary bg-custom-primary text-white"
: "border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90"
}`
}
>
{tab.title}
</Tab>
))}
</div>
</Tab.List>
<Tab.Panels as={Fragment}>
<Tab.Panel as="div" className="h-full space-y-5 overflow-y-auto">
<RecentPagesList />
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-hidden">
<AllPagesList />
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-hidden">
<FavoritePagesList />
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-hidden">
<PrivatePagesList />
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-hidden">
<SharedPagesList />
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-hidden">
<ArchivedPagesList />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</>
) : (
<EmptyState
image={EmptyStateImagePath}
title="Write a note, a doc, or a full knowledge base. Get Galileo, Planes AI assistant, to help you get started"
description="Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your projects context. To make short work of any doc, invoke Galileo, Planes AI, with a shortcut or the click of a button."
primaryButton={{
text: "Create your first page",
onClick: () => toggleCreatePageModal(true),
}}
comicBox={{
title: "A page can be a doc or a doc of docs.",
description:
"We wrote Nikhil and Meeras love story. You could write your projects mission, goals, and eventual vision.",
}}
size="lg"
disabled={!isEditingAllowed}
/>
)}
<div className="flex h-full flex-col space-y-5 overflow-hidden p-6">
<div className="flex justify-between gap-4">
<h3 className="text-2xl font-semibold text-custom-text-100">Pages</h3>
</div>
<Tab.Group
as={Fragment}
defaultIndex={currentTabValue(pageTab)}
onChange={(i) => {
switch (i) {
case 0:
return setPageTab("Recent");
case 1:
return setPageTab("All");
case 2:
return setPageTab("Favorites");
case 3:
return setPageTab("Private");
case 4:
return setPageTab("Shared");
case 5:
return setPageTab("Archived");
default:
return setPageTab("All");
}
}}
>
<Tab.List as="div" className="mb-6 flex items-center justify-between">
<div className="flex flex-wrap items-center gap-4">
{PAGE_TABS_LIST.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`rounded-full border px-5 py-1.5 text-sm outline-none ${
selected
? "border-custom-primary bg-custom-primary text-white"
: "border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90"
}`
}
>
{tab.title}
</Tab>
))}
</div>
</Tab.List>
<Tab.Panels as={Fragment}>
<Tab.Panel as="div" className="h-full space-y-5 overflow-y-auto">
<RecentPagesList />
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-hidden">
<AllPagesList />
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-hidden">
<FavoritePagesList />
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-hidden">
<PrivatePagesList />
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-hidden">
<SharedPagesList />
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-hidden">
<ArchivedPagesList />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</>
);
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 231 KiB

View File

@ -11,7 +11,7 @@ import { isThisWeek, isToday, isYesterday } from "date-fns";
export interface IProjectPageStore {
loader: boolean;
archivedProjectLoader: boolean;
archivedPageLoader: boolean;
projectPageMap: Record<string, Record<string, IPageStore>>;
projectArchivedPageMap: Record<string, Record<string, IPageStore>>;
@ -33,7 +33,7 @@ export interface IProjectPageStore {
export class ProjectPageStore implements IProjectPageStore {
loader: boolean = false;
archivedProjectLoader: boolean = false;
archivedPageLoader: boolean = false;
projectPageMap: Record<string, Record<string, IPageStore>> = {}; // { projectId: [page1, page2] }
projectArchivedPageMap: Record<string, Record<string, IPageStore>> = {}; // { projectId: [page1, page2] }
@ -44,7 +44,7 @@ export class ProjectPageStore implements IProjectPageStore {
constructor(_rootStore: RootStore) {
makeObservable(this, {
loader: observable.ref,
archivedProjectLoader: observable.ref,
archivedPageLoader: observable.ref,
projectPageMap: observable,
projectArchivedPageMap: observable,
@ -183,18 +183,18 @@ export class ProjectPageStore implements IProjectPageStore {
*/
fetchArchivedProjectPages = async (workspaceSlug: string, projectId: string) => {
try {
this.archivedProjectLoader = true;
this.archivedPageLoader = true;
await this.pageService.getArchivedPages(workspaceSlug, projectId).then((response) => {
runInAction(() => {
for (const page of response) {
set(this.projectArchivedPageMap, [projectId, page.id], new PageStore(page, this.rootStore));
}
this.archivedProjectLoader = false;
this.archivedPageLoader = false;
});
return response;
});
} catch (e) {
this.archivedProjectLoader = false;
this.archivedPageLoader = false;
throw e;
}
};