chore: new empty state (#3640)

* chore: empty state asset updated

* chore: empty state config updated

* chore: cycle and module issues layout empty state added

* chore: workspace and project settings empty state added
This commit is contained in:
Anmol Singh Bhatia 2024-02-13 16:35:20 +05:30 committed by GitHub
parent f64284f6a0
commit eea3b4fa54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
202 changed files with 658 additions and 284 deletions

View File

@ -34,7 +34,8 @@ import { ICycle, TCycleGroups } from "@plane/types";
// constants
import { EIssuesStoreType } from "constants/issue";
import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
import { CYCLE_EMPTY_STATE_DETAILS, CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle";
import { CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle";
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
interface IActiveCycleDetails {
workspaceSlug: string;

View File

@ -7,7 +7,7 @@ import { useUser } from "hooks/store";
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// constants
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle";
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
export interface ICyclesBoard {
cycleIds: string[];

View File

@ -9,7 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { Loader } from "@plane/ui";
// constants
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle";
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
export interface ICyclesList {
cycleIds: string[];

View File

@ -1,21 +1,21 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
import { useTheme } from "next-themes";
// store hooks
import { useEstimate, useProject } from "hooks/store";
import { useEstimate, useProject, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { Button, Loader } from "@plane/ui";
import { EmptyState } from "components/common";
// images
import emptyEstimate from "public/empty-state/estimate.svg";
// types
import { IEstimate } from "@plane/types";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// constants
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
export const EstimatesList: React.FC = observer(() => {
// states
@ -25,9 +25,12 @@ export const EstimatesList: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { updateProject, currentProjectDetails } = useProject();
const { projectEstimates, getProjectEstimateById } = useEstimate();
const { currentUser } = useUser();
// toast alert
const { setToastAlert } = useToast();
@ -55,6 +58,10 @@ export const EstimatesList: React.FC = observer(() => {
});
};
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["estimate"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("project-settings", "estimates", isLightMode);
return (
<>
<CreateUpdateEstimateModal
@ -108,19 +115,12 @@ export const EstimatesList: React.FC = observer(() => {
))}
</section>
) : (
<div className="w-full py-8">
<div className="h-full w-full py-8">
<EmptyState
title="No estimates yet"
description="Estimates help you communicate the complexity of an issue."
image={emptyEstimate}
primaryButton={{
icon: <Plus className="h-4 w-4" />,
text: "Add Estimate",
onClick: () => {
setEstimateFormOpen(true);
setEstimateToUpdate(undefined);
},
}}
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="lg"
/>
</div>
)

View File

@ -3,15 +3,17 @@ import { useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import useSWR, { mutate } from "swr";
import { observer } from "mobx-react-lite";
// hooks
import { useUser } from "hooks/store";
import useUserAuth from "hooks/use-user-auth";
// services
import { IntegrationService } from "services/integrations";
// components
import { Exporter, SingleExport } from "components/exporter";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { Button, Loader } from "@plane/ui";
// icons
@ -20,8 +22,8 @@ import { MoveLeft, MoveRight, RefreshCw } from "lucide-react";
import { EXPORT_SERVICES_LIST } from "constants/fetch-keys";
// constants
import { EXPORTERS_LIST } from "constants/workspace";
import { observer } from "mobx-react-lite";
import { useUser } from "hooks/store";
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
// services
const integrationService = new IntegrationService();
@ -34,6 +36,8 @@ const IntegrationGuide = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, provider } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { currentUser, currentUserLoader } = useUser();
// custom hooks
@ -46,6 +50,10 @@ const IntegrationGuide = observer(() => {
: null
);
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["export"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "exports", isLightMode);
const handleCsvClose = () => {
router.replace(`/${workspaceSlug?.toString()}/settings/exports`);
};
@ -140,7 +148,14 @@ const IntegrationGuide = observer(() => {
</div>
</div>
) : (
<p className="px-4 py-6 text-sm text-custom-text-200">No previous export available.</p>
<div className="h-full w-full flex items-center justify-center">
<EmptyState
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="sm"
/>
</div>
)
) : (
<Loader className="mt-6 grid grid-cols-1 gap-3">

View File

@ -4,6 +4,7 @@ import Image from "next/image";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks
import { useUser } from "hooks/store";
import useUserAuth from "hooks/use-user-auth";
@ -11,6 +12,7 @@ import useUserAuth from "hooks/use-user-auth";
import { IntegrationService } from "services/integrations";
// components
import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { Button, Loader } from "@plane/ui";
// icons
@ -21,6 +23,7 @@ import { IImporterService } from "@plane/types";
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
// constants
import { IMPORTERS_LIST } from "constants/workspace";
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
// services
const integrationService = new IntegrationService();
@ -33,6 +36,8 @@ const IntegrationGuide = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, provider } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { currentUser, currentUserLoader } = useUser();
// custom hooks
@ -43,6 +48,10 @@ const IntegrationGuide = observer(() => {
workspaceSlug ? () => integrationService.getImporterServicesList(workspaceSlug as string) : null
);
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["import"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "imports", isLightMode);
const handleDeleteImport = (importService: IImporterService) => {
setImportToDelete(importService);
setDeleteImportModal(true);
@ -134,7 +143,14 @@ const IntegrationGuide = observer(() => {
</div>
</div>
) : (
<p className="px-4 py-6 text-sm text-custom-text-200">No previous imports available.</p>
<div className="h-full w-full flex items-center justify-center">
<EmptyState
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="sm"
/>
</div>
)
) : (
<Loader className="mt-6 grid grid-cols-1 gap-3">

View File

@ -9,6 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// constants
import { EUserProjectRoles } from "constants/project";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
// types
import { IIssueFilterOptions } from "@plane/types";
@ -65,20 +66,19 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => {
const emptyStateProps: EmptyStateProps =
issueFilterCount > 0
? {
title: "No issues found matching the filters applied",
title: EMPTY_FILTER_STATE_DETAILS["archived"].title,
image: currentLayoutEmptyStateImagePath,
secondaryButton: {
text: "Clear all filters",
text: EMPTY_FILTER_STATE_DETAILS["archived"].secondaryButton?.text,
onClick: handleClearAllFilters,
},
}
: {
title: "No archived issues yet",
description:
"Archived issues help you remove issues you completed or cancelled from focus. You can set automation to auto archive issues and find them here.",
title: EMPTY_ISSUE_STATE_DETAILS["archived"].title,
description: EMPTY_ISSUE_STATE_DETAILS["archived"].description,
image: EmptyStateImagePath,
primaryButton: {
text: "Set Automation",
text: EMPTY_ISSUE_STATE_DETAILS["archived"].primaryButton.text,
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`),
},
size: "sm",

View File

@ -5,28 +5,29 @@ import { PlusIcon } from "lucide-react";
import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { EmptyState } from "components/common";
import { ExistingIssuesListModal } from "components/core";
// ui
import { Button } from "@plane/ui";
// assets
import emptyIssue from "public/empty-state/issue.svg";
// types
import { ISearchIssueResponse } from "@plane/types";
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
// constants
import { EUserProjectRoles } from "constants/project";
import { EIssuesStoreType } from "constants/issue";
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
import { useTheme } from "next-themes";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
type Props = {
workspaceSlug: string | undefined;
projectId: string | undefined;
cycleId: string | undefined;
activeLayout: TIssueLayouts | undefined;
};
export const CycleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, cycleId } = props;
const { workspaceSlug, projectId, cycleId, activeLayout } = props;
// states
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { issues } = useIssues(EIssuesStoreType.CYCLE);
const { updateIssue, fetchIssue } = useIssueDetail();
@ -36,6 +37,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
const { setTrackElement } = useEventTracker();
const {
membership: { currentProjectRole: userRole },
currentUser,
} = useUser();
const { setToastAlert } = useToast();
@ -60,6 +62,11 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
});
};
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("cycle-issues", activeLayout ?? "list", isLightMode);
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
return (
@ -74,27 +81,23 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
/>
<div className="grid h-full w-full place-items-center">
<EmptyState
title="Cycle issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
primaryButton={{
text: "New issue",
text: emptyStateDetail.primaryButton.text,
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("Cycle issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
},
}}
secondaryButton={
<Button
variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
onClick={() => setCycleIssuesListModal(true)}
disabled={!isEditingAllowed}
>
Add an existing issue
</Button>
}
secondaryButton={{
text: emptyStateDetail.secondaryButton.text,
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => setCycleIssuesListModal(true),
}}
size="sm"
disabled={!isEditingAllowed}
/>
</div>

View File

@ -9,6 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// constants
import { EUserProjectRoles } from "constants/project";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
// types
import { IIssueFilterOptions } from "@plane/types";
@ -65,17 +66,16 @@ export const ProjectDraftEmptyState: React.FC = observer(() => {
const emptyStateProps: EmptyStateProps =
issueFilterCount > 0
? {
title: "No issues found matching the filters applied",
title: EMPTY_FILTER_STATE_DETAILS["draft"].title,
image: currentLayoutEmptyStateImagePath,
secondaryButton: {
text: "Clear all filters",
text: EMPTY_FILTER_STATE_DETAILS["draft"].secondaryButton.text,
onClick: handleClearAllFilters,
},
}
: {
title: "No draft issues yet",
description:
"Quickly stepping away but want to keep your place? No worries save a draft now. Your issues will be right here waiting for you.",
title: EMPTY_ISSUE_STATE_DETAILS["draft"].title,
description: EMPTY_ISSUE_STATE_DETAILS["draft"].description,
image: EmptyStateImagePath,
size: "sm",
disabled: !isEditingAllowed,

View File

@ -5,37 +5,38 @@ import { PlusIcon } from "lucide-react";
import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { EmptyState } from "components/common";
import { ExistingIssuesListModal } from "components/core";
// ui
import { Button } from "@plane/ui";
// assets
import emptyIssue from "public/empty-state/issue.svg";
// types
import { ISearchIssueResponse } from "@plane/types";
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
// constants
import { EUserProjectRoles } from "constants/project";
import { EIssuesStoreType } from "constants/issue";
import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state";
import { useTheme } from "next-themes";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
type Props = {
workspaceSlug: string | undefined;
projectId: string | undefined;
moduleId: string | undefined;
activeLayout: TIssueLayouts | undefined;
};
export const ModuleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, moduleId } = props;
const { workspaceSlug, projectId, moduleId, activeLayout } = props;
// states
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { issues } = useIssues(EIssuesStoreType.MODULE);
const {
commandPalette: { toggleCreateIssueModal },
} = useApplication();
const { setTrackElement } = useEventTracker();
const {
membership: { currentProjectRole: userRole },
currentUser,
} = useUser();
// toast alert
const { setToastAlert } = useToast();
@ -55,6 +56,11 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
);
};
const emptyStateDetail = MODULE_EMPTY_STATE_DETAILS["no-issues"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("cycle-issues", activeLayout ?? "list", isLightMode);
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
return (
@ -69,27 +75,22 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
/>
<div className="grid h-full w-full place-items-center">
<EmptyState
title="Module issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
primaryButton={{
text: "New issue",
text: emptyStateDetail.primaryButton.text,
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("Module issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
},
}}
secondaryButton={
<Button
variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
onClick={() => setModuleIssuesListModal(true)}
disabled={!isEditingAllowed}
>
Add an existing issue
</Button>
}
secondaryButton={{
text: emptyStateDetail.secondaryButton.text,
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => setModuleIssuesListModal(true),
}}
disabled={!isEditingAllowed}
/>
</div>

View File

@ -9,6 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// constants
import { EUserProjectRoles } from "constants/project";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
// types
import { IIssueFilterOptions } from "@plane/types";
@ -67,26 +68,23 @@ export const ProjectEmptyState: React.FC = observer(() => {
const emptyStateProps: EmptyStateProps =
issueFilterCount > 0
? {
title: "No issues found matching the filters applied",
title: EMPTY_FILTER_STATE_DETAILS["project"].title,
image: currentLayoutEmptyStateImagePath,
secondaryButton: {
text: "Clear all filters",
text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text,
onClick: handleClearAllFilters,
},
}
: {
title: "Create an issue and assign it to someone, even yourself",
description:
"Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.",
title: EMPTY_ISSUE_STATE_DETAILS["project"].title,
description: EMPTY_ISSUE_STATE_DETAILS["project"].description,
image: EmptyStateImagePath,
comicBox: {
title: "Issues are building blocks in Plane.",
description:
"Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.",
title: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.title,
description: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.description,
},
primaryButton: {
text: "Create your first issue",
text: EMPTY_ISSUE_STATE_DETAILS["project"].primaryButton.text,
onClick: () => {
setTrackElement("Project issue empty state");
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);

View File

@ -20,7 +20,8 @@ import { EIssueActions } from "../types";
// constants
import { EUserProjectRoles } from "constants/project";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { ALL_ISSUES_EMPTY_STATE_DETAILS, EUserWorkspaceRoles } from "constants/workspace";
import { EUserWorkspaceRoles } from "constants/workspace";
import { ALL_ISSUES_EMPTY_STATE_DETAILS } from "constants/empty-state";
export const AllIssueLayoutRoot: React.FC = observer(() => {
// router

View File

@ -72,6 +72,7 @@ export const CycleLayoutRoot: React.FC = observer(() => {
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
cycleId={cycleId.toString()}
activeLayout={activeLayout}
/>
) : (
<>

View File

@ -62,6 +62,7 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
moduleId={moduleId.toString()}
activeLayout={activeLayout}
/>
) : (
<>

View File

@ -9,8 +9,9 @@ import {
DropResult,
Droppable,
} from "@hello-pangea/dnd";
import { useTheme } from "next-themes";
// hooks
import { useLabel } from "hooks/store";
import { useLabel, useUser } from "hooks/store";
import useDraggableInPortal from "hooks/use-draggable-portal";
// components
import {
@ -19,13 +20,13 @@ import {
ProjectSettingLabelGroup,
ProjectSettingLabelItem,
} from "components/labels";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { Button, Loader } from "@plane/ui";
import { EmptyState } from "components/common";
// images
import emptyLabel from "public/empty-state/label.svg";
// types
import { IIssueLabel } from "@plane/types";
// constants
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
const LABELS_ROOT = "labels.root";
@ -40,7 +41,10 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { currentUser } = useUser();
const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel();
// portal
const renderDraggable = useDraggableInPortal();
@ -50,6 +54,10 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
setLabelForm(true);
};
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["labels"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("project-settings", "labels", isLightMode);
const onDragEnd = (result: DropResult) => {
const { combine, draggableId, destination, source } = result;
@ -94,7 +102,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
Add label
</Button>
</div>
<div className="w-full py-8">
<div className="h-full w-full py-8">
{showLabelForm && (
<div className="w-full rounded border border-custom-border-200 px-3.5 py-2 my-2">
<CreateUpdateLabelInline
@ -111,15 +119,14 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
)}
{projectLabels ? (
projectLabels.length === 0 && !showLabelForm ? (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
title="No labels yet"
description="Create labels to help organize and filter issues in you project"
image={emptyLabel}
primaryButton={{
text: "Add label",
onClick: () => newLabel(),
}}
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="lg"
/>
</div>
) : (
projectLabelsTree && (
<DragDropContext

View File

@ -11,6 +11,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
import { Loader, Spinner } from "@plane/ui";
// constants
import { EUserProjectRoles } from "constants/project";
import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state";
export const ModulesListView: React.FC = observer(() => {
// router
@ -97,16 +98,15 @@ export const ModulesListView: React.FC = observer(() => {
</>
) : (
<EmptyState
title="Map your project milestones to Modules and track aggregated work easily."
description="A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone."
title={MODULE_EMPTY_STATE_DETAILS["modules"].title}
description={MODULE_EMPTY_STATE_DETAILS["modules"].description}
image={EmptyStateImagePath}
comicBox={{
title: "Modules help group work by hierarchy.",
description:
"A cart module, a chassis module, and a warehouse module are all good example of this grouping.",
title: MODULE_EMPTY_STATE_DETAILS["modules"].comicBox.title,
description: MODULE_EMPTY_STATE_DETAILS["modules"].comicBox.description,
}}
primaryButton={{
text: "Build your first module",
text: MODULE_EMPTY_STATE_DETAILS["modules"].primaryButton.text,
onClick: () => {
setTrackElement("Module empty state");
commandPaletteStore.toggleCreateModuleModal(true);

View File

@ -14,6 +14,7 @@ import { Spinner } from "@plane/ui";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker";
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
export const WorkspaceDashboardView = observer(() => {
// theme
@ -78,20 +79,18 @@ export const WorkspaceDashboardView = observer(() => {
) : (
<EmptyState
image={emptyStateImage}
title="Overview of your projects, activity, and metrics"
description=" Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this
page will transform into a space that helps you progress. Admins will also see items which help their team
progress."
title={WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].title}
description={WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].description}
primaryButton={{
text: "Build your first project",
text: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].primaryButton.text,
onClick: () => {
setTrackElement("Dashboard empty state");
toggleCreateProjectModal(true);
},
}}
comicBox={{
title: "Everything starts with a project in Plane",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
title: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.title,
description: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.description,
}}
size="lg"
disabled={!isEditingAllowed}

View File

@ -11,7 +11,7 @@ import { PagesListItem } from "./list-item";
import { Loader } from "@plane/ui";
// constants
import { EUserProjectRoles } from "constants/project";
import { PAGE_EMPTY_STATE_DETAILS } from "constants/page";
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
type IPagesListView = {
pageIds: string[];

View File

@ -13,6 +13,7 @@ import { Loader } from "@plane/ui";
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// constants
import { EUserProjectRoles } from "constants/project";
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
export const RecentPagesList: FC = observer(() => {
// theme
@ -63,11 +64,11 @@ export const RecentPagesList: FC = observer(() => {
) : (
<>
<EmptyState
title="Write a note, a doc, or a full knowledge base"
description="Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely! Pages will be sorted and grouped by last updated."
title={PAGE_EMPTY_STATE_DETAILS["Recent"].title}
description={PAGE_EMPTY_STATE_DETAILS["Recent"].description}
image={EmptyStateImagePath}
primaryButton={{
text: "Create new page",
text: PAGE_EMPTY_STATE_DETAILS["Recent"].primaryButton.text,
onClick: () => commandPaletteStore.toggleCreatePageModal(true),
}}
size="sm"

View File

@ -13,8 +13,8 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
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";
import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state";
interface IProfileIssuesPage {
type: "assigned" | "subscribed" | "created";

View File

@ -8,6 +8,7 @@ import { Loader } from "@plane/ui";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
export const ProjectCardList = observer(() => {
// theme
@ -59,18 +60,18 @@ export const ProjectCardList = observer(() => {
) : (
<EmptyState
image={emptyStateImage}
title="Start a Project"
description="Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal."
title={WORKSPACE_EMPTY_STATE_DETAILS["projects"].title}
description={WORKSPACE_EMPTY_STATE_DETAILS["projects"].description}
primaryButton={{
text: "Start your first project",
text: WORKSPACE_EMPTY_STATE_DETAILS["projects"].primaryButton.text,
onClick: () => {
setTrackElement("Project empty state");
commandPaletteStore.toggleCreateProjectModal(true);
},
}}
comicBox={{
title: "Everything starts with a project in Plane",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
title: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.title,
description: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.description,
}}
size="lg"
disabled={!isEditingAllowed}

View File

@ -11,6 +11,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
import { Input, Loader, Spinner } from "@plane/ui";
// constants
import { EUserProjectRoles } from "constants/project";
import { VIEW_EMPTY_STATE_DETAILS } from "constants/empty-state";
export const ProjectViewsList = observer(() => {
// states
@ -77,15 +78,15 @@ export const ProjectViewsList = observer(() => {
</div>
) : (
<EmptyState
title="Save filtered views for your project. Create as many as you need"
description="Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyones views and choose whichever suits their needs best."
title={VIEW_EMPTY_STATE_DETAILS["project-views"].title}
description={VIEW_EMPTY_STATE_DETAILS["project-views"].description}
image={EmptyStateImagePath}
comicBox={{
title: "Views work atop Issue properties.",
description: "You can create a view from here with as many properties as filters as you see fit.",
title: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.title,
description: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.description,
}}
primaryButton={{
text: "Create your first view",
text: VIEW_EMPTY_STATE_DETAILS["project-views"].primaryButton.text,
onClick: () => toggleCreateViewModal(true),
}}
size="lg"

View File

@ -163,26 +163,4 @@ export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [
},
];
export const CYCLE_EMPTY_STATE_DETAILS = {
active: {
key: "active",
title: "No active cycles",
description:
"An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.",
},
upcoming: {
key: "upcoming",
title: "No upcoming cycles",
description: "Upcoming cycles on deck! Just add dates to cycles in draft, and they'll show up right here.",
},
completed: {
key: "completed",
title: "No completed cycles",
description: "Any cycle with a past due date is considered completed. Explore all completed cycles here.",
},
draft: {
key: "draft",
title: "No draft cycles",
description: "No dates added in cycles? Find them here as drafts.",
},
};

View File

@ -0,0 +1,366 @@
// workspace empty state
export const WORKSPACE_EMPTY_STATE_DETAILS = {
dashboard: {
title: "Overview of your projects, activity, and metrics",
description:
" Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this page will transform into a space that helps you progress. Admins will also see items which help their team progress.",
primaryButton: {
text: "Build your first project",
},
comicBox: {
title: "Everything starts with a project in Plane",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
},
},
analytics: {
title: "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster",
description:
"See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.",
primaryButton: {
text: "Create Cycles and Modules first",
},
comicBox: {
title: "Analytics works best with Cycles + Modules",
description:
"First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.",
},
},
projects: {
title: "Start a Project",
description:
"Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal.",
primaryButton: {
text: "Start your first project",
},
comicBox: {
title: "Everything starts with a project in Plane",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
},
},
"assigned-notification": {
key: "assigned-notification",
title: "No issues assigned",
description: "Updates for issues assigned to you can be seen here",
},
"created-notification": {
key: "created-notification",
title: "No updates to issues",
description: "Updates to issues created by you can be seen here",
},
"subscribed-notification": {
key: "subscribed-notification",
title: "No updates to issues",
description: "Updates to any issue you are subscribed to can be seen here",
},
};
export const ALL_ISSUES_EMPTY_STATE_DETAILS = {
"all-issues": {
key: "all-issues",
title: "No issues in the project",
description: "First project done! Now, slice your work into trackable pieces with issues. Let's go!",
},
assigned: {
key: "assigned",
title: "No issues yet",
description: "Issues assigned to you can be tracked from here.",
},
created: {
key: "created",
title: "No issues yet",
description: "All issues created by you come here, track them here directly.",
},
subscribed: {
key: "subscribed",
title: "No issues yet",
description: "Subscribe to issues you are interested in, track all of them here.",
},
"custom-view": {
key: "custom-view",
title: "No issues yet",
description: "Issues that applies to the filters, track all of them here.",
},
};
export const SEARCH_EMPTY_STATE_DETAILS = {
views: {
key: "views",
title: "No matching views",
description: "No views match the search criteria. Create a new view instead.",
},
projects: {
key: "projects",
title: "No matching projects",
description: "No projects detected with the matching criteria. Create a new project instead.",
},
commandK: {
key: "commandK",
title: "No results found. ",
},
members: {
key: "members",
title: "No matching members",
description: "Add them to the project if they are already a part of the workspace",
},
};
export const WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS = {
"api-tokens": {
key: "api-tokens",
title: "No API tokens created",
description:
"Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started.",
},
webhooks: {
key: "webhooks",
title: "No webhooks added",
description: "Create webhooks to receive real-time updates and automate actions.",
},
export: {
key: "export",
title: "No previous exports yet",
description: "Anytime you export, you will also have a copy here for reference.",
},
import: {
key: "export",
title: "No previous imports yet",
description: "Find all your previous imports here and download them.",
},
};
// profile empty state
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.",
},
};
// project empty state
export const PROJECT_SETTINGS_EMPTY_STATE_DETAILS = {
labels: {
key: "labels",
title: "No labels yet",
description: "Create labels to help organize and filter issues in you project.",
},
integrations: {
key: "integrations",
title: "No integrations configured",
description: "Configure GitHub and other integrations to sync your project issues.",
},
estimate: {
key: "estimate",
title: "No estimates added",
description: "Create a set of estimates to communicate the amount of work per issue.",
},
};
export const CYCLE_EMPTY_STATE_DETAILS = {
cycles: {
title: "Group and timebox your work in Cycles.",
description:
"Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.",
comicBox: {
title: "Cycles are repetitive time-boxes.",
description:
"A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.",
},
primaryButton: {
text: "Set your first cycle",
},
},
"no-issues": {
key: "no-issues",
title: "No issues added to the cycle",
description: "Add or create issues you wish to timebox and deliver within this cycle",
primaryButton: {
text: "Create new issue ",
},
secondaryButton: {
text: "Add an existing issue",
},
},
active: {
key: "active",
title: "No active cycles",
description:
"An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.",
},
upcoming: {
key: "upcoming",
title: "No upcoming cycles",
description: "Upcoming cycles on deck! Just add dates to cycles in draft, and they'll show up right here.",
},
completed: {
key: "completed",
title: "No completed cycles",
description: "Any cycle with a past due date is considered completed. Explore all completed cycles here.",
},
draft: {
key: "draft",
title: "No draft cycles",
description: "No dates added in cycles? Find them here as drafts.",
},
};
export const EMPTY_FILTER_STATE_DETAILS = {
archived: {
key: "archived",
title: "No issues found matching the filters applied",
secondaryButton: {
text: "Clear all filters",
},
},
draft: {
key: "draft",
title: "No issues found matching the filters applied",
secondaryButton: {
text: "Clear all filters",
},
},
project: {
key: "project",
title: "No issues found matching the filters applied",
secondaryButton: {
text: "Clear all filters",
},
},
};
export const EMPTY_ISSUE_STATE_DETAILS = {
archived: {
key: "archived",
title: "No archived issues yet",
description:
"Archived issues help you remove issues you completed or cancelled from focus. You can set automation to auto archive issues and find them here.",
primaryButton: {
text: "Set Automation",
},
},
draft: {
key: "draft",
title: "No draft issues yet",
description:
"Quickly stepping away but want to keep your place? No worries save a draft now. Your issues will be right here waiting for you.",
},
project: {
key: "project",
title: "Create an issue and assign it to someone, even yourself",
description:
"Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.",
comicBox: {
title: "Issues are building blocks in Plane.",
description:
"Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.",
},
primaryButton: {
text: "Create your first issue",
},
},
};
export const MODULE_EMPTY_STATE_DETAILS = {
"no-issues": {
key: "no-issues",
title: "No issues in the module",
description: "Create or add issues which you want to accomplish as part of this module",
primaryButton: {
text: "Create new issue ",
},
secondaryButton: {
text: "Add an existing issue",
},
},
modules: {
title: "Map your project milestones to Modules and track aggregated work easily.",
description:
"A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone.",
comicBox: {
title: "Modules help group work by hierarchy.",
description: "A cart module, a chassis module, and a warehouse module are all good example of this grouping.",
},
primaryButton: {
text: "Build your first module",
},
},
};
export const VIEW_EMPTY_STATE_DETAILS = {
"project-views": {
title: "Save filtered views for your project. Create as many as you need",
description:
"Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyones views and choose whichever suits their needs best.",
comicBox: {
title: "Views work atop Issue properties.",
description: "You can create a view from here with as many properties as filters as you see fit.",
},
primaryButton: {
text: "Create your first view",
},
},
};
export const PAGE_EMPTY_STATE_DETAILS = {
pages: {
key: "pages",
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",
},
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.",
},
},
All: {
key: "all",
title: "Write a note, a doc, or a full knowledge base",
description:
"Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely!",
},
Favorites: {
key: "favorites",
title: "No favorite pages yet",
description: "Favorites for quick access? mark them and find them right here.",
},
Private: {
key: "private",
title: "No private pages yet",
description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.",
},
Shared: {
key: "shared",
title: "No shared pages yet",
description: "See pages shared with everyone in your project right here.",
},
Archived: {
key: "archived",
title: "No archived pages yet",
description: "Archive pages not on your radar. Access them here when needed.",
},
Recent: {
key: "recent",
title: "Write a note, a doc, or a full knowledge base",
description:
"Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely! Pages will be sorted and grouped by last updated",
primaryButton: {
text: "Create new page",
},
},
};

View File

@ -52,38 +52,3 @@ export const PAGE_ACCESS_SPECIFIERS: { key: number; label: string; icon: any }[]
icon: Lock,
},
];
export const PAGE_EMPTY_STATE_DETAILS = {
All: {
key: "all",
title: "Write a note, a doc, or a full knowledge base",
description:
"Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely!",
},
Favorites: {
key: "favorites",
title: "No favorite pages yet",
description: "Favorites for quick access? mark them and find them right here.",
},
Private: {
key: "private",
title: "No private pages yet",
description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.",
},
Shared: {
key: "shared",
title: "No shared pages yet",
description: "See pages shared with everyone in your project right here.",
},
Archived: {
key: "archived",
title: "No archived pages yet",
description: "Archive pages not on your radar. Access them here when needed.",
},
Recent: {
key: "recent",
title: "Write a note, a doc, or a full knowledge base",
description:
"Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely! Pages will be sorted and grouped by last updated",
},
};

View File

@ -64,21 +64,3 @@ export const PROFILE_ADMINS_TAB = [
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

@ -190,31 +190,3 @@ export const WORKSPACE_SETTINGS_LINKS: {
Icon: SettingIcon,
},
];
export const ALL_ISSUES_EMPTY_STATE_DETAILS = {
"all-issues": {
key: "all-issues",
title: "No issues in the project",
description: "First project done! Now, slice your work into trackable pieces with issues. Let's go!",
},
assigned: {
key: "assigned",
title: "No issues yet",
description: "Issues assigned to you can be tracked from here.",
},
created: {
key: "created",
title: "No issues yet",
description: "All issues created by you come here, track them here directly.",
},
subscribed: {
key: "subscribed",
title: "No issues yet",
description: "Subscribe to issues you are interested in, track all of them here.",
},
"custom-view": {
key: "custom-view",
title: "No issues yet",
description: "Issues that applies to the filters, track all of them here.",
},
};

View File

@ -16,10 +16,11 @@ import { EUserWorkspaceRoles } from "constants/workspace";
// type
import { NextPageWithLayout } from "lib/types";
import { useRouter } from "next/router";
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
const AnalyticsPage: NextPageWithLayout = observer(() => {
const router = useRouter()
const { analytics_tab } = router.query
const router = useRouter();
const { analytics_tab } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
@ -41,18 +42,21 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
<>
{workspaceProjectIds && workspaceProjectIds.length > 0 ? (
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
<Tab.Group as={Fragment} defaultIndex={analytics_tab === 'custom' ? 1 : 0}>
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
<Tab.List as="div" className="flex space-x-2 border-b border-custom-border-200 px-0 md:px-5 py-0 md:py-3">
{ANALYTICS_TABS.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`rounded-0 w-full md:w-max md:rounded-3xl border-b md:border border-custom-border-200 focus:outline-none px-0 md:px-4 py-2 text-xs hover:bg-custom-background-80 ${selected ? "border-custom-primary-100 text-custom-primary-100 md:bg-custom-background-80 md:text-custom-text-200 md:border-custom-border-200" : "border-transparent"
`rounded-0 w-full md:w-max md:rounded-3xl border-b md:border border-custom-border-200 focus:outline-none px-0 md:px-4 py-2 text-xs hover:bg-custom-background-80 ${
selected
? "border-custom-primary-100 text-custom-primary-100 md:bg-custom-background-80 md:text-custom-text-200 md:border-custom-border-200"
: "border-transparent"
}`
}
onClick={() => {
router.query.analytics_tab = tab.key
router.push(router)
router.query.analytics_tab = tab.key;
router.push(router);
}}
>
{tab.title}
@ -72,19 +76,18 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
) : (
<EmptyState
image={EmptyStateImagePath}
title="Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster"
description="See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time."
title={WORKSPACE_EMPTY_STATE_DETAILS["analytics"].title}
description={WORKSPACE_EMPTY_STATE_DETAILS["analytics"].description}
primaryButton={{
text: "Create Cycles and Modules first",
text: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].primaryButton.text,
onClick: () => {
setTrackElement("Analytics empty state");
toggleCreateProjectModal(true);
},
}}
comicBox={{
title: "Analytics works best with Cycles + Modules",
description:
"First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.",
title: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.title,
description: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.description,
}}
size="lg"
disabled={!isEditingAllowed}

View File

@ -20,6 +20,7 @@ import { NextPageWithLayout } from "lib/types";
// constants
import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace";
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const [createModal, setCreateModal] = useState(false);
@ -81,16 +82,15 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
{totalCycles === 0 ? (
<div className="h-full place-items-center">
<EmptyState
title="Group and timebox your work in Cycles."
description="Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team."
title={CYCLE_EMPTY_STATE_DETAILS["cycles"].title}
description={CYCLE_EMPTY_STATE_DETAILS["cycles"].description}
image={EmptyStateImagePath}
comicBox={{
title: "Cycles are repetitive time-boxes.",
description:
"A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.",
title: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.title,
description: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.description,
}}
primaryButton={{
text: "Set your first cycle",
text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text,
onClick: () => {
setTrackElement("Cycle empty state");
setCreateModal(true);
@ -114,7 +114,8 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
<Tab
key={tab.key}
className={({ selected }) =>
`border-b-2 p-4 text-sm font-medium outline-none ${selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
`border-b-2 p-4 text-sm font-medium outline-none ${
selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
}`
}
>
@ -132,13 +133,15 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
<Tooltip key={layout.key} tooltipContent={layout.title}>
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
}`}
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
>
<layout.icon
strokeWidth={2}
className={`h-3.5 w-3.5 ${cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
className={`h-3.5 w-3.5 ${
cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
}`}
/>
</button>

View File

@ -23,6 +23,7 @@ import { NextPageWithLayout } from "lib/types";
import { PAGE_TABS_LIST } from "constants/page";
import { useProjectPages } from "hooks/store/use-project-page";
import { EUserWorkspaceRoles } from "constants/workspace";
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), {
ssr: false,
@ -217,19 +218,18 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
) : (
<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."
title={PAGE_EMPTY_STATE_DETAILS["pages"].title}
description={PAGE_EMPTY_STATE_DETAILS["pages"].description}
primaryButton={{
text: "Create your first page",
text: PAGE_EMPTY_STATE_DETAILS["pages"].primaryButton.text,
onClick: () => {
setTrackElement("Pages empty state");
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.",
title: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.title,
description: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.description,
}}
size="lg"
disabled={!isEditingAllowed}

View File

@ -21,7 +21,7 @@ const EstimatesSettingsPage: NextPageWithLayout = observer(() => {
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
return (
<div className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "pointer-events-none opacity-60"}`}>
<div className={`h-full w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "pointer-events-none opacity-60"}`}>
<EstimatesList />
</div>
);

View File

@ -1,6 +1,9 @@
import { ReactElement } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { useTheme } from "next-themes";
// hooks
import { useUser } from "hooks/store";
// layouts
import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout";
@ -10,16 +13,15 @@ import { ProjectService } from "services/project";
// components
import { IntegrationCard } from "components/project";
import { ProjectSettingHeader } from "components/headers";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { EmptyState } from "components/common";
import { Loader } from "@plane/ui";
// images
import emptyIntegration from "public/empty-state/integration.svg";
// types
import { IProject } from "@plane/types";
import { NextPageWithLayout } from "lib/types";
// fetch-keys
import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
// services
const integrationService = new IntegrationService();
@ -28,6 +30,10 @@ const projectService = new ProjectService();
const ProjectIntegrationsPage: NextPageWithLayout = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { currentUser } = useUser();
const { data: projectDetails } = useSWR<IProject>(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
@ -39,10 +45,14 @@ const ProjectIntegrationsPage: NextPageWithLayout = () => {
() => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null)
);
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode);
const isAdmin = projectDetails?.member_role === 20;
return (
<div className={`w-full gap-10 overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
<div className={`h-full w-full gap-10 overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex items-center border-b border-custom-border-100 py-3.5">
<h3 className="text-xl font-medium">Integrations</h3>
</div>
@ -54,15 +64,16 @@ const ProjectIntegrationsPage: NextPageWithLayout = () => {
))}
</div>
) : (
<div className="w-full py-8">
<div className="h-full w-full py-8">
<EmptyState
title="You haven't configured integrations"
description="Configure GitHub and other integrations to sync your project issues."
image={emptyIntegration}
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
primaryButton={{
text: "Configure now",
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
}}
size="lg"
disabled={!isAdmin}
/>
</div>

View File

@ -9,7 +9,7 @@ import { ProjectSettingHeader } from "components/headers";
import { NextPageWithLayout } from "lib/types";
const LabelsSettingsPage: NextPageWithLayout = () => (
<div className="w-full gap-10 overflow-y-auto py-8 pr-9">
<div className="h-full w-full gap-10 overflow-y-auto py-8 pr-9">
<ProjectSettingsLabelList />
</div>
);

View File

@ -2,6 +2,7 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { useTheme } from "next-themes";
// store hooks
import { useUser } from "hooks/store";
// layouts
@ -9,7 +10,8 @@ import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingLayout } from "layouts/settings-layout";
// component
import { WorkspaceSettingHeader } from "components/headers";
import { ApiTokenEmptyState, ApiTokenListItem, CreateApiTokenModal } from "components/api-token";
import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { Button, Spinner } from "@plane/ui";
// services
@ -19,6 +21,7 @@ import { NextPageWithLayout } from "lib/types";
// constants
import { API_TOKENS_LIST } from "constants/fetch-keys";
import { EUserWorkspaceRoles } from "constants/workspace";
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
const apiTokenService = new APITokenService();
@ -28,9 +31,12 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const {
membership: { currentWorkspaceRole },
currentUser,
} = useUser();
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
@ -39,6 +45,10 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null
);
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["api-tokens"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "api-tokens", isLightMode);
if (!isAdmin)
return (
<div className="mt-10 flex h-full w-full justify-center p-4">
@ -50,10 +60,10 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
<>
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
{tokens ? (
<section className="w-full overflow-y-auto py-8 pr-9">
<section className="h-full w-full overflow-y-auto py-8 pr-9">
{tokens.length > 0 ? (
<>
<div className="mb-2 flex items-center justify-between border-b border-custom-border-200 py-3.5">
<div className="flex items-center justify-between border-b border-custom-border-200 py-3.5">
<h3 className="text-xl font-medium">API tokens</h3>
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
Add API token
@ -66,8 +76,21 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
</div>
</>
) : (
<div className="mx-auto">
<ApiTokenEmptyState onClick={() => setIsCreateTokenModalOpen(true)} />
<div className="flex h-full w-full flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
<h3 className="text-xl font-medium">API tokens</h3>
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
Add API token
</Button>
</div>
<div className="h-full w-full flex items-center justify-center">
<EmptyState
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="lg"
/>
</div>
</div>
)}
</section>

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks
import { useUser, useWebhook, useWorkspace } from "hooks/store";
// layouts
@ -9,11 +10,14 @@ import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingLayout } from "layouts/settings-layout";
// components
import { WorkspaceSettingHeader } from "components/headers";
import { WebhooksList, WebhooksEmptyState, CreateWebhookModal } from "components/web-hooks";
import { WebhooksList, CreateWebhookModal } from "components/web-hooks";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { Button, Spinner } from "@plane/ui";
// types
import { NextPageWithLayout } from "lib/types";
// constants
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
const WebhooksListPage: NextPageWithLayout = observer(() => {
// states
@ -21,9 +25,12 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// theme
const { resolvedTheme } = useTheme();
// mobx store
const {
membership: { currentWorkspaceRole },
currentUser,
} = useUser();
const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook();
const { currentWorkspace } = useWorkspace();
@ -35,6 +42,11 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null
);
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["webhooks"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode);
// clear secret key when modal is closed.
useEffect(() => {
if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey();
@ -76,8 +88,21 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
<WebhooksList />
</div>
) : (
<div className="mx-auto">
<WebhooksEmptyState onClick={() => setShowCreateWebhookModal(true)} />
<div className="flex h-full w-full flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
<div className="text-xl font-medium">Webhooks</div>
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
Add webhook
</Button>
</div>
<div className="h-full w-full flex items-center justify-center">
<EmptyState
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="lg"
/>
</div>
</div>
)}
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Some files were not shown because too many files have changed in this diff Show More