diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index 4dffd7b2f..d2279aeb4 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -138,6 +138,8 @@ export const CyclesBoardCard: FC = (props) => { }); }; + const daysLeft = findHowManyDaysLeft(cycleDetails.end_date ?? new Date()); + return (
= (props) => { }} > {currentCycle.value === "current" - ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}` + ? `${daysLeft} ${daysLeft > 1 ? "days" : "day"} left` : `${currentCycle.label}`} )} diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index a4ce3941b..e34f4b30b 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -141,6 +141,8 @@ export const CyclesListItem: FC = (props) => { const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); + const daysLeft = findHowManyDaysLeft(cycleDetails.end_date ?? new Date()); + return ( <> = (props) => { }} > {currentCycle.value === "current" - ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}` + ? `${daysLeft} ${daysLeft > 1 ? "days" : "day"} left` : `${currentCycle.label}`} )} diff --git a/web/components/dashboard/widgets/recent-projects.tsx b/web/components/dashboard/widgets/recent-projects.tsx index cfa4ae6a1..c4318d889 100644 --- a/web/components/dashboard/widgets/recent-projects.tsx +++ b/web/components/dashboard/widgets/recent-projects.tsx @@ -78,7 +78,7 @@ export const RecentProjectsWidget: React.FC = observer((props) => { const { fetchWidgetStats, getWidgetStats } = useDashboard(); // derived values const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const canCreateProject = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + const canCreateProject = currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; useEffect(() => { fetchWidgetStats(workspaceSlug, dashboardId, { diff --git a/web/components/empty-state/empty-state.tsx b/web/components/empty-state/empty-state.tsx index a74502f4d..adc11b546 100644 --- a/web/components/empty-state/empty-state.tsx +++ b/web/components/empty-state/empty-state.tsx @@ -68,7 +68,7 @@ export const EmptyState: React.FC = ({
diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index b8a2b571b..d808bded2 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -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), diff --git a/web/components/pages/pages-list/archived-pages-list.tsx b/web/components/pages/pages-list/archived-pages-list.tsx index a14034daf..eb57d7558 100644 --- a/web/components/pages/pages-list/archived-pages-list.tsx +++ b/web/components/pages/pages-list/archived-pages-list.tsx @@ -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 (
diff --git a/web/components/profile/navbar.tsx b/web/components/profile/navbar.tsx index c4a3ecf46..44dfe57d1 100644 --- a/web/components/profile/navbar.tsx +++ b/web/components/profile/navbar.tsx @@ -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) => { 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 (
diff --git a/web/components/profile/profile-issues.tsx b/web/components/profile/profile-issues.tsx index 5d7575835..4b3721103 100644 --- a/web/components/profile/profile-issues.tsx +++ b/web/components/profile/profile-issues.tsx @@ -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) => {
) : ( <> - -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : null} -
- {/* peek overview */} - + {groupedIssueIds ? ( + <> + +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : null} +
+ {/* peek overview */} + + + ) : ( + + )} )} diff --git a/web/components/views/views-list.tsx b/web/components/views/views-list.tsx index c944c8e71..6b3b3e45a 100644 --- a/web/components/views/views-list.tsx +++ b/web/components/views/views-list.tsx @@ -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())); diff --git a/web/constants/profile.ts b/web/constants/profile.ts index 063bb7e44..0fffdbc9b 100644 --- a/web/constants/profile.ts +++ b/web/constants/profile.ts @@ -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.", + }, +}; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index 2aaefc517..32299747f 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -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(() => 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 (
@@ -93,79 +107,100 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { return ( <> - {workspaceSlug && projectId && ( - setCreateUpdatePageModal(false)} - projectId={projectId.toString()} + {projectPageIds && archivedPageIds && projectPageIds.length + archivedPageIds.length > 0 ? ( + <> + {workspaceSlug && projectId && ( + setCreateUpdatePageModal(false)} + projectId={projectId.toString()} + /> + )} +
+
+

Pages

+
+ { + 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"); + } + }} + > + +
+ {PAGE_TABS_LIST.map((tab) => ( + + `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} + + ))} +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + ) : ( + 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} /> )} -
-
-

Pages

-
- { - 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"); - } - }} - > - -
- {PAGE_TABS_LIST.map((tab) => ( - - `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} - - ))} -
-
- - - - - - - - - - - - - - - - - - - - -
-
); }); diff --git a/web/public/empty-state/draft/empty-issues-dark.webp b/web/public/empty-state/draft/empty-issues-dark.webp index 6f3f7b54e..0973e5290 100644 Binary files a/web/public/empty-state/draft/empty-issues-dark.webp and b/web/public/empty-state/draft/empty-issues-dark.webp differ diff --git a/web/public/empty-state/draft/empty-issues-light.webp b/web/public/empty-state/draft/empty-issues-light.webp index 427c5cdb1..0ce0bb9f3 100644 Binary files a/web/public/empty-state/draft/empty-issues-light.webp and b/web/public/empty-state/draft/empty-issues-light.webp differ diff --git a/web/public/empty-state/onboarding/issues-dark.webp b/web/public/empty-state/onboarding/issues-dark.webp index 5444ac383..d1b1338a1 100644 Binary files a/web/public/empty-state/onboarding/issues-dark.webp and b/web/public/empty-state/onboarding/issues-dark.webp differ diff --git a/web/store/project-page.store.ts b/web/store/project-page.store.ts index dabb42ca5..072605bc3 100644 --- a/web/store/project-page.store.ts +++ b/web/store/project-page.store.ts @@ -11,7 +11,7 @@ import { isThisWeek, isToday, isYesterday } from "date-fns"; export interface IProjectPageStore { loader: boolean; - archivedProjectLoader: boolean; + archivedPageLoader: boolean; projectPageMap: Record>; projectArchivedPageMap: Record>; @@ -33,7 +33,7 @@ export interface IProjectPageStore { export class ProjectPageStore implements IProjectPageStore { loader: boolean = false; - archivedProjectLoader: boolean = false; + archivedPageLoader: boolean = false; projectPageMap: Record> = {}; // { projectId: [page1, page2] } projectArchivedPageMap: Record> = {}; // { 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; } };