forked from github/plane
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:
parent
5c912b8821
commit
6c6b764421
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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, {
|
||||
|
@ -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",
|
||||
})}
|
||||
>
|
||||
|
@ -187,10 +187,12 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
size="sm"
|
||||
primaryButton={
|
||||
(workspaceProjectIds ?? []).length > 0
|
||||
? 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),
|
||||
|
@ -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 />
|
||||
|
@ -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">
|
||||
|
@ -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" ? (
|
||||
@ -48,6 +59,8 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{groupedIssueIds ? (
|
||||
<>
|
||||
<ProfileIssuesAppliedFiltersRoot />
|
||||
<div className="-z-1 relative h-full w-full overflow-auto">
|
||||
@ -60,6 +73,16 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title={PROFILE_EMPTY_STATE_DETAILS[type].title}
|
||||
description={PROFILE_EMPTY_STATE_DETAILS[type].description}
|
||||
size="sm"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -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()));
|
||||
|
||||
|
@ -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.",
|
||||
},
|
||||
};
|
||||
|
@ -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 />
|
||||
@ -92,6 +106,8 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectPageIds && archivedPageIds && projectPageIds.length + archivedPageIds.length > 0 ? (
|
||||
<>
|
||||
{workspaceSlug && projectId && (
|
||||
<CreateUpdatePageModal
|
||||
@ -167,6 +183,25 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
image={EmptyStateImagePath}
|
||||
title="Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s 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 project’s context. To make short work of any doc, invoke Galileo, Plane’s 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 Meera’s love story. You could write your project’s mission, goals, and eventual vision.",
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
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 |
@ -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;
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user