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
@ -34,7 +34,8 @@ import { ICycle, TCycleGroups } from "@plane/types";
|
|||||||
// constants
|
// constants
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
|
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 {
|
interface IActiveCycleDetails {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
@ -7,7 +7,7 @@ import { useUser } from "hooks/store";
|
|||||||
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
|
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
|
||||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||||
// constants
|
// constants
|
||||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle";
|
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
export interface ICyclesBoard {
|
export interface ICyclesBoard {
|
||||||
cycleIds: string[];
|
cycleIds: string[];
|
||||||
|
@ -9,7 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
|||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle";
|
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
export interface ICyclesList {
|
export interface ICyclesList {
|
||||||
cycleIds: string[];
|
cycleIds: string[];
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Plus } from "lucide-react";
|
import { useTheme } from "next-themes";
|
||||||
// store hooks
|
// store hooks
|
||||||
import { useEstimate, useProject } from "hooks/store";
|
import { useEstimate, useProject, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates";
|
import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates";
|
||||||
|
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Loader } from "@plane/ui";
|
import { Button, Loader } from "@plane/ui";
|
||||||
import { EmptyState } from "components/common";
|
|
||||||
// images
|
|
||||||
import emptyEstimate from "public/empty-state/estimate.svg";
|
|
||||||
// types
|
// types
|
||||||
import { IEstimate } from "@plane/types";
|
import { IEstimate } from "@plane/types";
|
||||||
// helpers
|
// helpers
|
||||||
import { orderArrayBy } from "helpers/array.helper";
|
import { orderArrayBy } from "helpers/array.helper";
|
||||||
|
// constants
|
||||||
|
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
export const EstimatesList: React.FC = observer(() => {
|
export const EstimatesList: React.FC = observer(() => {
|
||||||
// states
|
// states
|
||||||
@ -25,9 +25,12 @@ export const EstimatesList: React.FC = observer(() => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
// theme
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { updateProject, currentProjectDetails } = useProject();
|
const { updateProject, currentProjectDetails } = useProject();
|
||||||
const { projectEstimates, getProjectEstimateById } = useEstimate();
|
const { projectEstimates, getProjectEstimateById } = useEstimate();
|
||||||
|
const { currentUser } = useUser();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateUpdateEstimateModal
|
<CreateUpdateEstimateModal
|
||||||
@ -108,19 +115,12 @@ export const EstimatesList: React.FC = observer(() => {
|
|||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full py-8">
|
<div className="h-full w-full py-8">
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="No estimates yet"
|
title={emptyStateDetail.title}
|
||||||
description="Estimates help you communicate the complexity of an issue."
|
description={emptyStateDetail.description}
|
||||||
image={emptyEstimate}
|
image={emptyStateImage}
|
||||||
primaryButton={{
|
size="lg"
|
||||||
icon: <Plus className="h-4 w-4" />,
|
|
||||||
text: "Add Estimate",
|
|
||||||
onClick: () => {
|
|
||||||
setEstimateFormOpen(true);
|
|
||||||
setEstimateToUpdate(undefined);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -3,15 +3,17 @@ import { useState } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useUser } from "hooks/store";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
// services
|
// services
|
||||||
import { IntegrationService } from "services/integrations";
|
import { IntegrationService } from "services/integrations";
|
||||||
// components
|
// components
|
||||||
import { Exporter, SingleExport } from "components/exporter";
|
import { Exporter, SingleExport } from "components/exporter";
|
||||||
|
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Loader } from "@plane/ui";
|
import { Button, Loader } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -20,8 +22,8 @@ import { MoveLeft, MoveRight, RefreshCw } from "lucide-react";
|
|||||||
import { EXPORT_SERVICES_LIST } from "constants/fetch-keys";
|
import { EXPORT_SERVICES_LIST } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { EXPORTERS_LIST } from "constants/workspace";
|
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
|
// services
|
||||||
const integrationService = new IntegrationService();
|
const integrationService = new IntegrationService();
|
||||||
@ -34,6 +36,8 @@ const IntegrationGuide = observer(() => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, provider } = router.query;
|
const { workspaceSlug, provider } = router.query;
|
||||||
|
// theme
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { currentUser, currentUserLoader } = useUser();
|
const { currentUser, currentUserLoader } = useUser();
|
||||||
// custom hooks
|
// custom hooks
|
||||||
@ -46,6 +50,10 @@ const IntegrationGuide = observer(() => {
|
|||||||
: null
|
: 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 = () => {
|
const handleCsvClose = () => {
|
||||||
router.replace(`/${workspaceSlug?.toString()}/settings/exports`);
|
router.replace(`/${workspaceSlug?.toString()}/settings/exports`);
|
||||||
};
|
};
|
||||||
@ -140,7 +148,14 @@ const IntegrationGuide = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<Loader className="mt-6 grid grid-cols-1 gap-3">
|
||||||
|
@ -4,6 +4,7 @@ import Image from "next/image";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useUser } from "hooks/store";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
@ -11,6 +12,7 @@ import useUserAuth from "hooks/use-user-auth";
|
|||||||
import { IntegrationService } from "services/integrations";
|
import { IntegrationService } from "services/integrations";
|
||||||
// components
|
// components
|
||||||
import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration";
|
import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration";
|
||||||
|
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Loader } from "@plane/ui";
|
import { Button, Loader } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -21,6 +23,7 @@ import { IImporterService } from "@plane/types";
|
|||||||
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
|
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { IMPORTERS_LIST } from "constants/workspace";
|
import { IMPORTERS_LIST } from "constants/workspace";
|
||||||
|
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
const integrationService = new IntegrationService();
|
const integrationService = new IntegrationService();
|
||||||
@ -33,6 +36,8 @@ const IntegrationGuide = observer(() => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, provider } = router.query;
|
const { workspaceSlug, provider } = router.query;
|
||||||
|
// theme
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { currentUser, currentUserLoader } = useUser();
|
const { currentUser, currentUserLoader } = useUser();
|
||||||
// custom hooks
|
// custom hooks
|
||||||
@ -43,6 +48,10 @@ const IntegrationGuide = observer(() => {
|
|||||||
workspaceSlug ? () => integrationService.getImporterServicesList(workspaceSlug as string) : null
|
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) => {
|
const handleDeleteImport = (importService: IImporterService) => {
|
||||||
setImportToDelete(importService);
|
setImportToDelete(importService);
|
||||||
setDeleteImportModal(true);
|
setDeleteImportModal(true);
|
||||||
@ -134,7 +143,14 @@ const IntegrationGuide = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<Loader className="mt-6 grid grid-cols-1 gap-3">
|
||||||
|
@ -9,6 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
|||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||||
|
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions } from "@plane/types";
|
import { IIssueFilterOptions } from "@plane/types";
|
||||||
|
|
||||||
@ -65,20 +66,19 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => {
|
|||||||
const emptyStateProps: EmptyStateProps =
|
const emptyStateProps: EmptyStateProps =
|
||||||
issueFilterCount > 0
|
issueFilterCount > 0
|
||||||
? {
|
? {
|
||||||
title: "No issues found matching the filters applied",
|
title: EMPTY_FILTER_STATE_DETAILS["archived"].title,
|
||||||
image: currentLayoutEmptyStateImagePath,
|
image: currentLayoutEmptyStateImagePath,
|
||||||
secondaryButton: {
|
secondaryButton: {
|
||||||
text: "Clear all filters",
|
text: EMPTY_FILTER_STATE_DETAILS["archived"].secondaryButton?.text,
|
||||||
onClick: handleClearAllFilters,
|
onClick: handleClearAllFilters,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
title: "No archived issues yet",
|
title: EMPTY_ISSUE_STATE_DETAILS["archived"].title,
|
||||||
description:
|
description: EMPTY_ISSUE_STATE_DETAILS["archived"].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.",
|
|
||||||
image: EmptyStateImagePath,
|
image: EmptyStateImagePath,
|
||||||
primaryButton: {
|
primaryButton: {
|
||||||
text: "Set Automation",
|
text: EMPTY_ISSUE_STATE_DETAILS["archived"].primaryButton.text,
|
||||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`),
|
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`),
|
||||||
},
|
},
|
||||||
size: "sm",
|
size: "sm",
|
||||||
|
@ -5,28 +5,29 @@ import { PlusIcon } from "lucide-react";
|
|||||||
import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store";
|
import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { EmptyState } from "components/common";
|
|
||||||
import { ExistingIssuesListModal } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
// ui
|
|
||||||
import { Button } from "@plane/ui";
|
|
||||||
// assets
|
|
||||||
import emptyIssue from "public/empty-state/issue.svg";
|
|
||||||
// types
|
// types
|
||||||
import { ISearchIssueResponse } from "@plane/types";
|
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
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 = {
|
type Props = {
|
||||||
workspaceSlug: string | undefined;
|
workspaceSlug: string | undefined;
|
||||||
projectId: string | undefined;
|
projectId: string | undefined;
|
||||||
cycleId: string | undefined;
|
cycleId: string | undefined;
|
||||||
|
activeLayout: TIssueLayouts | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, cycleId } = props;
|
const { workspaceSlug, projectId, cycleId, activeLayout } = props;
|
||||||
// states
|
// states
|
||||||
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||||
|
// theme
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { issues } = useIssues(EIssuesStoreType.CYCLE);
|
const { issues } = useIssues(EIssuesStoreType.CYCLE);
|
||||||
const { updateIssue, fetchIssue } = useIssueDetail();
|
const { updateIssue, fetchIssue } = useIssueDetail();
|
||||||
@ -36,6 +37,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
|||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole: userRole },
|
membership: { currentProjectRole: userRole },
|
||||||
|
currentUser,
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
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;
|
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -74,27 +81,23 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
<div className="grid h-full w-full place-items-center">
|
<div className="grid h-full w-full place-items-center">
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="Cycle issues will appear here"
|
title={emptyStateDetail.title}
|
||||||
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."
|
description={emptyStateDetail.description}
|
||||||
image={emptyIssue}
|
image={emptyStateImage}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "New issue",
|
text: emptyStateDetail.primaryButton.text,
|
||||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTrackElement("Cycle issue empty state");
|
setTrackElement("Cycle issue empty state");
|
||||||
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
|
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
secondaryButton={
|
secondaryButton={{
|
||||||
<Button
|
text: emptyStateDetail.secondaryButton.text,
|
||||||
variant="neutral-primary"
|
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||||
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
|
onClick: () => setCycleIssuesListModal(true),
|
||||||
onClick={() => setCycleIssuesListModal(true)}
|
}}
|
||||||
disabled={!isEditingAllowed}
|
size="sm"
|
||||||
>
|
|
||||||
Add an existing issue
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
disabled={!isEditingAllowed}
|
disabled={!isEditingAllowed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,6 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
|||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||||
|
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions } from "@plane/types";
|
import { IIssueFilterOptions } from "@plane/types";
|
||||||
|
|
||||||
@ -65,17 +66,16 @@ export const ProjectDraftEmptyState: React.FC = observer(() => {
|
|||||||
const emptyStateProps: EmptyStateProps =
|
const emptyStateProps: EmptyStateProps =
|
||||||
issueFilterCount > 0
|
issueFilterCount > 0
|
||||||
? {
|
? {
|
||||||
title: "No issues found matching the filters applied",
|
title: EMPTY_FILTER_STATE_DETAILS["draft"].title,
|
||||||
image: currentLayoutEmptyStateImagePath,
|
image: currentLayoutEmptyStateImagePath,
|
||||||
secondaryButton: {
|
secondaryButton: {
|
||||||
text: "Clear all filters",
|
text: EMPTY_FILTER_STATE_DETAILS["draft"].secondaryButton.text,
|
||||||
onClick: handleClearAllFilters,
|
onClick: handleClearAllFilters,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
title: "No draft issues yet",
|
title: EMPTY_ISSUE_STATE_DETAILS["draft"].title,
|
||||||
description:
|
description: EMPTY_ISSUE_STATE_DETAILS["draft"].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.",
|
|
||||||
image: EmptyStateImagePath,
|
image: EmptyStateImagePath,
|
||||||
size: "sm",
|
size: "sm",
|
||||||
disabled: !isEditingAllowed,
|
disabled: !isEditingAllowed,
|
||||||
|
@ -5,37 +5,38 @@ import { PlusIcon } from "lucide-react";
|
|||||||
import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store";
|
import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { EmptyState } from "components/common";
|
|
||||||
import { ExistingIssuesListModal } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
// ui
|
|
||||||
import { Button } from "@plane/ui";
|
|
||||||
// assets
|
|
||||||
import emptyIssue from "public/empty-state/issue.svg";
|
|
||||||
// types
|
// types
|
||||||
import { ISearchIssueResponse } from "@plane/types";
|
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
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 = {
|
type Props = {
|
||||||
workspaceSlug: string | undefined;
|
workspaceSlug: string | undefined;
|
||||||
projectId: string | undefined;
|
projectId: string | undefined;
|
||||||
moduleId: string | undefined;
|
moduleId: string | undefined;
|
||||||
|
activeLayout: TIssueLayouts | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, moduleId } = props;
|
const { workspaceSlug, projectId, moduleId, activeLayout } = props;
|
||||||
// states
|
// states
|
||||||
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
|
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
|
||||||
|
// theme
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { issues } = useIssues(EIssuesStoreType.MODULE);
|
const { issues } = useIssues(EIssuesStoreType.MODULE);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
commandPalette: { toggleCreateIssueModal },
|
commandPalette: { toggleCreateIssueModal },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole: userRole },
|
membership: { currentProjectRole: userRole },
|
||||||
|
currentUser,
|
||||||
} = useUser();
|
} = useUser();
|
||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
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;
|
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -69,27 +75,22 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
<div className="grid h-full w-full place-items-center">
|
<div className="grid h-full w-full place-items-center">
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="Module issues will appear here"
|
title={emptyStateDetail.title}
|
||||||
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."
|
description={emptyStateDetail.description}
|
||||||
image={emptyIssue}
|
image={emptyStateImage}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "New issue",
|
text: emptyStateDetail.primaryButton.text,
|
||||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTrackElement("Module issue empty state");
|
setTrackElement("Module issue empty state");
|
||||||
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
|
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
secondaryButton={
|
secondaryButton={{
|
||||||
<Button
|
text: emptyStateDetail.secondaryButton.text,
|
||||||
variant="neutral-primary"
|
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||||
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
|
onClick: () => setModuleIssuesListModal(true),
|
||||||
onClick={() => setModuleIssuesListModal(true)}
|
}}
|
||||||
disabled={!isEditingAllowed}
|
|
||||||
>
|
|
||||||
Add an existing issue
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
disabled={!isEditingAllowed}
|
disabled={!isEditingAllowed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,6 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
|||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||||
|
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions } from "@plane/types";
|
import { IIssueFilterOptions } from "@plane/types";
|
||||||
|
|
||||||
@ -67,26 +68,23 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
|||||||
const emptyStateProps: EmptyStateProps =
|
const emptyStateProps: EmptyStateProps =
|
||||||
issueFilterCount > 0
|
issueFilterCount > 0
|
||||||
? {
|
? {
|
||||||
title: "No issues found matching the filters applied",
|
title: EMPTY_FILTER_STATE_DETAILS["project"].title,
|
||||||
image: currentLayoutEmptyStateImagePath,
|
image: currentLayoutEmptyStateImagePath,
|
||||||
secondaryButton: {
|
secondaryButton: {
|
||||||
text: "Clear all filters",
|
text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text,
|
||||||
onClick: handleClearAllFilters,
|
onClick: handleClearAllFilters,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
title: "Create an issue and assign it to someone, even yourself",
|
title: EMPTY_ISSUE_STATE_DETAILS["project"].title,
|
||||||
description:
|
description: EMPTY_ISSUE_STATE_DETAILS["project"].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.",
|
|
||||||
image: EmptyStateImagePath,
|
image: EmptyStateImagePath,
|
||||||
comicBox: {
|
comicBox: {
|
||||||
title: "Issues are building blocks in Plane.",
|
title: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.title,
|
||||||
description:
|
description: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.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: {
|
primaryButton: {
|
||||||
text: "Create your first issue",
|
text: EMPTY_ISSUE_STATE_DETAILS["project"].primaryButton.text,
|
||||||
|
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTrackElement("Project issue empty state");
|
setTrackElement("Project issue empty state");
|
||||||
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||||
|
@ -20,7 +20,8 @@ import { EIssueActions } from "../types";
|
|||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
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(() => {
|
export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||||
// router
|
// router
|
||||||
|
@ -72,6 +72,7 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
|||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
cycleId={cycleId.toString()}
|
cycleId={cycleId.toString()}
|
||||||
|
activeLayout={activeLayout}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -62,6 +62,7 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
|
|||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
moduleId={moduleId.toString()}
|
moduleId={moduleId.toString()}
|
||||||
|
activeLayout={activeLayout}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -9,8 +9,9 @@ import {
|
|||||||
DropResult,
|
DropResult,
|
||||||
Droppable,
|
Droppable,
|
||||||
} from "@hello-pangea/dnd";
|
} from "@hello-pangea/dnd";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
// hooks
|
// hooks
|
||||||
import { useLabel } from "hooks/store";
|
import { useLabel, useUser } from "hooks/store";
|
||||||
import useDraggableInPortal from "hooks/use-draggable-portal";
|
import useDraggableInPortal from "hooks/use-draggable-portal";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
@ -19,13 +20,13 @@ import {
|
|||||||
ProjectSettingLabelGroup,
|
ProjectSettingLabelGroup,
|
||||||
ProjectSettingLabelItem,
|
ProjectSettingLabelItem,
|
||||||
} from "components/labels";
|
} from "components/labels";
|
||||||
|
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Loader } from "@plane/ui";
|
import { Button, Loader } from "@plane/ui";
|
||||||
import { EmptyState } from "components/common";
|
|
||||||
// images
|
|
||||||
import emptyLabel from "public/empty-state/label.svg";
|
|
||||||
// types
|
// types
|
||||||
import { IIssueLabel } from "@plane/types";
|
import { IIssueLabel } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
const LABELS_ROOT = "labels.root";
|
const LABELS_ROOT = "labels.root";
|
||||||
|
|
||||||
@ -40,7 +41,10 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
// theme
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
// store hooks
|
// store hooks
|
||||||
|
const { currentUser } = useUser();
|
||||||
const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel();
|
const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel();
|
||||||
// portal
|
// portal
|
||||||
const renderDraggable = useDraggableInPortal();
|
const renderDraggable = useDraggableInPortal();
|
||||||
@ -50,6 +54,10 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
|||||||
setLabelForm(true);
|
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 onDragEnd = (result: DropResult) => {
|
||||||
const { combine, draggableId, destination, source } = result;
|
const { combine, draggableId, destination, source } = result;
|
||||||
|
|
||||||
@ -94,7 +102,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
|||||||
Add label
|
Add label
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full py-8">
|
<div className="h-full w-full py-8">
|
||||||
{showLabelForm && (
|
{showLabelForm && (
|
||||||
<div className="w-full rounded border border-custom-border-200 px-3.5 py-2 my-2">
|
<div className="w-full rounded border border-custom-border-200 px-3.5 py-2 my-2">
|
||||||
<CreateUpdateLabelInline
|
<CreateUpdateLabelInline
|
||||||
@ -111,15 +119,14 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
|||||||
)}
|
)}
|
||||||
{projectLabels ? (
|
{projectLabels ? (
|
||||||
projectLabels.length === 0 && !showLabelForm ? (
|
projectLabels.length === 0 && !showLabelForm ? (
|
||||||
<EmptyState
|
<div className="flex items-center justify-center h-full w-full">
|
||||||
title="No labels yet"
|
<EmptyState
|
||||||
description="Create labels to help organize and filter issues in you project"
|
title={emptyStateDetail.title}
|
||||||
image={emptyLabel}
|
description={emptyStateDetail.description}
|
||||||
primaryButton={{
|
image={emptyStateImage}
|
||||||
text: "Add label",
|
size="lg"
|
||||||
onClick: () => newLabel(),
|
/>
|
||||||
}}
|
</div>
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
projectLabelsTree && (
|
projectLabelsTree && (
|
||||||
<DragDropContext
|
<DragDropContext
|
||||||
|
@ -11,6 +11,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
|||||||
import { Loader, Spinner } from "@plane/ui";
|
import { Loader, Spinner } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
export const ModulesListView: React.FC = observer(() => {
|
export const ModulesListView: React.FC = observer(() => {
|
||||||
// router
|
// router
|
||||||
@ -97,16 +98,15 @@ export const ModulesListView: React.FC = observer(() => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="Map your project milestones to Modules and track aggregated work easily."
|
title={MODULE_EMPTY_STATE_DETAILS["modules"].title}
|
||||||
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."
|
description={MODULE_EMPTY_STATE_DETAILS["modules"].description}
|
||||||
image={EmptyStateImagePath}
|
image={EmptyStateImagePath}
|
||||||
comicBox={{
|
comicBox={{
|
||||||
title: "Modules help group work by hierarchy.",
|
title: MODULE_EMPTY_STATE_DETAILS["modules"].comicBox.title,
|
||||||
description:
|
description: MODULE_EMPTY_STATE_DETAILS["modules"].comicBox.description,
|
||||||
"A cart module, a chassis module, and a warehouse module are all good example of this grouping.",
|
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "Build your first module",
|
text: MODULE_EMPTY_STATE_DETAILS["modules"].primaryButton.text,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTrackElement("Module empty state");
|
setTrackElement("Module empty state");
|
||||||
commandPaletteStore.toggleCreateModuleModal(true);
|
commandPaletteStore.toggleCreateModuleModal(true);
|
||||||
|
@ -14,6 +14,7 @@ import { Spinner } from "@plane/ui";
|
|||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker";
|
import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker";
|
||||||
|
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
export const WorkspaceDashboardView = observer(() => {
|
export const WorkspaceDashboardView = observer(() => {
|
||||||
// theme
|
// theme
|
||||||
@ -78,20 +79,18 @@ export const WorkspaceDashboardView = observer(() => {
|
|||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
image={emptyStateImage}
|
image={emptyStateImage}
|
||||||
title="Overview of your projects, activity, and metrics"
|
title={WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].title}
|
||||||
description=" Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this
|
description={WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].description}
|
||||||
page will transform into a space that helps you progress. Admins will also see items which help their team
|
|
||||||
progress."
|
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "Build your first project",
|
text: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].primaryButton.text,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTrackElement("Dashboard empty state");
|
setTrackElement("Dashboard empty state");
|
||||||
toggleCreateProjectModal(true);
|
toggleCreateProjectModal(true);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
comicBox={{
|
comicBox={{
|
||||||
title: "Everything starts with a project in Plane",
|
title: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.title,
|
||||||
description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.",
|
description: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.description,
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={!isEditingAllowed}
|
disabled={!isEditingAllowed}
|
||||||
|
@ -11,7 +11,7 @@ import { PagesListItem } from "./list-item";
|
|||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { PAGE_EMPTY_STATE_DETAILS } from "constants/page";
|
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
type IPagesListView = {
|
type IPagesListView = {
|
||||||
pageIds: string[];
|
pageIds: string[];
|
||||||
|
@ -13,6 +13,7 @@ import { Loader } from "@plane/ui";
|
|||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
export const RecentPagesList: FC = observer(() => {
|
export const RecentPagesList: FC = observer(() => {
|
||||||
// theme
|
// theme
|
||||||
@ -63,11 +64,11 @@ export const RecentPagesList: FC = observer(() => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="Write a note, a doc, or a full knowledge base"
|
title={PAGE_EMPTY_STATE_DETAILS["Recent"].title}
|
||||||
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."
|
description={PAGE_EMPTY_STATE_DETAILS["Recent"].description}
|
||||||
image={EmptyStateImagePath}
|
image={EmptyStateImagePath}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "Create new page",
|
text: PAGE_EMPTY_STATE_DETAILS["Recent"].primaryButton.text,
|
||||||
onClick: () => commandPaletteStore.toggleCreatePageModal(true),
|
onClick: () => commandPaletteStore.toggleCreatePageModal(true),
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -13,8 +13,8 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
|||||||
import { useIssues, useUser } from "hooks/store";
|
import { useIssues, useUser } from "hooks/store";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
import { PROFILE_EMPTY_STATE_DETAILS } from "constants/profile";
|
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
interface IProfileIssuesPage {
|
interface IProfileIssuesPage {
|
||||||
type: "assigned" | "subscribed" | "created";
|
type: "assigned" | "subscribed" | "created";
|
||||||
|
@ -8,6 +8,7 @@ import { Loader } from "@plane/ui";
|
|||||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
export const ProjectCardList = observer(() => {
|
export const ProjectCardList = observer(() => {
|
||||||
// theme
|
// theme
|
||||||
@ -59,18 +60,18 @@ export const ProjectCardList = observer(() => {
|
|||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
image={emptyStateImage}
|
image={emptyStateImage}
|
||||||
title="Start a Project"
|
title={WORKSPACE_EMPTY_STATE_DETAILS["projects"].title}
|
||||||
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."
|
description={WORKSPACE_EMPTY_STATE_DETAILS["projects"].description}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "Start your first project",
|
text: WORKSPACE_EMPTY_STATE_DETAILS["projects"].primaryButton.text,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTrackElement("Project empty state");
|
setTrackElement("Project empty state");
|
||||||
commandPaletteStore.toggleCreateProjectModal(true);
|
commandPaletteStore.toggleCreateProjectModal(true);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
comicBox={{
|
comicBox={{
|
||||||
title: "Everything starts with a project in Plane",
|
title: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.title,
|
||||||
description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.",
|
description: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.description,
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={!isEditingAllowed}
|
disabled={!isEditingAllowed}
|
||||||
|
@ -11,6 +11,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
|||||||
import { Input, Loader, Spinner } from "@plane/ui";
|
import { Input, Loader, Spinner } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
import { VIEW_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
export const ProjectViewsList = observer(() => {
|
export const ProjectViewsList = observer(() => {
|
||||||
// states
|
// states
|
||||||
@ -77,15 +78,15 @@ export const ProjectViewsList = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="Save filtered views for your project. Create as many as you need"
|
title={VIEW_EMPTY_STATE_DETAILS["project-views"].title}
|
||||||
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 everyone’s views and choose whichever suits their needs best."
|
description={VIEW_EMPTY_STATE_DETAILS["project-views"].description}
|
||||||
image={EmptyStateImagePath}
|
image={EmptyStateImagePath}
|
||||||
comicBox={{
|
comicBox={{
|
||||||
title: "Views work atop Issue properties.",
|
title: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.title,
|
||||||
description: "You can create a view from here with as many properties as filters as you see fit.",
|
description: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.description,
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "Create your first view",
|
text: VIEW_EMPTY_STATE_DETAILS["project-views"].primaryButton.text,
|
||||||
onClick: () => toggleCreateViewModal(true),
|
onClick: () => toggleCreateViewModal(true),
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
|
@ -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.",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
366
web/constants/empty-state.ts
Normal 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 product’s 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 product’s 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 everyone’s 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, 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",
|
||||||
|
},
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -52,38 +52,3 @@ export const PAGE_ACCESS_SPECIFIERS: { key: number; label: string; icon: any }[]
|
|||||||
icon: Lock,
|
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",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
@ -64,21 +64,3 @@ export const PROFILE_ADMINS_TAB = [
|
|||||||
selected: "/[workspaceSlug]/profile/[userId]/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.",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
@ -190,31 +190,3 @@ export const WORKSPACE_SETTINGS_LINKS: {
|
|||||||
Icon: SettingIcon,
|
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.",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
@ -16,10 +16,11 @@ import { EUserWorkspaceRoles } from "constants/workspace";
|
|||||||
// type
|
// type
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
const AnalyticsPage: NextPageWithLayout = observer(() => {
|
const AnalyticsPage: NextPageWithLayout = observer(() => {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const { analytics_tab } = router.query
|
const { analytics_tab } = router.query;
|
||||||
// theme
|
// theme
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
// store hooks
|
// store hooks
|
||||||
@ -41,18 +42,21 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
|
|||||||
<>
|
<>
|
||||||
{workspaceProjectIds && workspaceProjectIds.length > 0 ? (
|
{workspaceProjectIds && workspaceProjectIds.length > 0 ? (
|
||||||
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
|
<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">
|
<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) => (
|
{ANALYTICS_TABS.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
className={({ selected }) =>
|
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={() => {
|
onClick={() => {
|
||||||
router.query.analytics_tab = tab.key
|
router.query.analytics_tab = tab.key;
|
||||||
router.push(router)
|
router.push(router);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tab.title}
|
{tab.title}
|
||||||
@ -72,19 +76,18 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
|
|||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
image={EmptyStateImagePath}
|
image={EmptyStateImagePath}
|
||||||
title="Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster"
|
title={WORKSPACE_EMPTY_STATE_DETAILS["analytics"].title}
|
||||||
description="See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time."
|
description={WORKSPACE_EMPTY_STATE_DETAILS["analytics"].description}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "Create Cycles and Modules first",
|
text: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].primaryButton.text,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTrackElement("Analytics empty state");
|
setTrackElement("Analytics empty state");
|
||||||
toggleCreateProjectModal(true);
|
toggleCreateProjectModal(true);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
comicBox={{
|
comicBox={{
|
||||||
title: "Analytics works best with Cycles + Modules",
|
title: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.title,
|
||||||
description:
|
description: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.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.",
|
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={!isEditingAllowed}
|
disabled={!isEditingAllowed}
|
||||||
|
@ -20,6 +20,7 @@ import { NextPageWithLayout } from "lib/types";
|
|||||||
// constants
|
// constants
|
||||||
import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle";
|
import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle";
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||||
const [createModal, setCreateModal] = useState(false);
|
const [createModal, setCreateModal] = useState(false);
|
||||||
@ -81,16 +82,15 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
{totalCycles === 0 ? (
|
{totalCycles === 0 ? (
|
||||||
<div className="h-full place-items-center">
|
<div className="h-full place-items-center">
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="Group and timebox your work in Cycles."
|
title={CYCLE_EMPTY_STATE_DETAILS["cycles"].title}
|
||||||
description="Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team."
|
description={CYCLE_EMPTY_STATE_DETAILS["cycles"].description}
|
||||||
image={EmptyStateImagePath}
|
image={EmptyStateImagePath}
|
||||||
comicBox={{
|
comicBox={{
|
||||||
title: "Cycles are repetitive time-boxes.",
|
title: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.title,
|
||||||
description:
|
description: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.description,
|
||||||
"A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.",
|
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "Set your first cycle",
|
text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTrackElement("Cycle empty state");
|
setTrackElement("Cycle empty state");
|
||||||
setCreateModal(true);
|
setCreateModal(true);
|
||||||
@ -114,7 +114,8 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
<Tab
|
<Tab
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
className={({ selected }) =>
|
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,14 +133,16 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
|
||||||
>
|
>
|
||||||
<layout.icon
|
<layout.icon
|
||||||
strokeWidth={2}
|
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>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -23,6 +23,7 @@ import { NextPageWithLayout } from "lib/types";
|
|||||||
import { PAGE_TABS_LIST } from "constants/page";
|
import { PAGE_TABS_LIST } from "constants/page";
|
||||||
import { useProjectPages } from "hooks/store/use-project-page";
|
import { useProjectPages } from "hooks/store/use-project-page";
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
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), {
|
const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@ -217,19 +218,18 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
|||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
image={EmptyStateImagePath}
|
image={EmptyStateImagePath}
|
||||||
title="Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started"
|
title={PAGE_EMPTY_STATE_DETAILS["pages"].title}
|
||||||
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."
|
description={PAGE_EMPTY_STATE_DETAILS["pages"].description}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "Create your first page",
|
text: PAGE_EMPTY_STATE_DETAILS["pages"].primaryButton.text,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTrackElement("Pages empty state");
|
setTrackElement("Pages empty state");
|
||||||
toggleCreatePageModal(true);
|
toggleCreatePageModal(true);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
comicBox={{
|
comicBox={{
|
||||||
title: "A page can be a doc or a doc of docs.",
|
title: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.title,
|
||||||
description:
|
description: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.description,
|
||||||
"We wrote Nikhil and Meera’s love story. You could write your project’s mission, goals, and eventual vision.",
|
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={!isEditingAllowed}
|
disabled={!isEditingAllowed}
|
||||||
|
@ -21,7 +21,7 @@ const EstimatesSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||||
|
|
||||||
return (
|
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 />
|
<EstimatesList />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
// hooks
|
||||||
|
import { useUser } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { ProjectSettingLayout } from "layouts/settings-layout";
|
import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||||
@ -10,16 +13,15 @@ import { ProjectService } from "services/project";
|
|||||||
// components
|
// components
|
||||||
import { IntegrationCard } from "components/project";
|
import { IntegrationCard } from "components/project";
|
||||||
import { ProjectSettingHeader } from "components/headers";
|
import { ProjectSettingHeader } from "components/headers";
|
||||||
|
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState } from "components/common";
|
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// images
|
|
||||||
import emptyIntegration from "public/empty-state/integration.svg";
|
|
||||||
// types
|
// types
|
||||||
import { IProject } from "@plane/types";
|
import { IProject } from "@plane/types";
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
|
import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
|
||||||
|
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
const integrationService = new IntegrationService();
|
const integrationService = new IntegrationService();
|
||||||
@ -28,6 +30,10 @@ const projectService = new ProjectService();
|
|||||||
const ProjectIntegrationsPage: NextPageWithLayout = () => {
|
const ProjectIntegrationsPage: NextPageWithLayout = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
// theme
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
// store hooks
|
||||||
|
const { currentUser } = useUser();
|
||||||
|
|
||||||
const { data: projectDetails } = useSWR<IProject>(
|
const { data: projectDetails } = useSWR<IProject>(
|
||||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
@ -39,10 +45,14 @@ const ProjectIntegrationsPage: NextPageWithLayout = () => {
|
|||||||
() => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null)
|
() => (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;
|
const isAdmin = projectDetails?.member_role === 20;
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||||
<h3 className="text-xl font-medium">Integrations</h3>
|
<h3 className="text-xl font-medium">Integrations</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -54,15 +64,16 @@ const ProjectIntegrationsPage: NextPageWithLayout = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full py-8">
|
<div className="h-full w-full py-8">
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="You haven't configured integrations"
|
title={emptyStateDetail.title}
|
||||||
description="Configure GitHub and other integrations to sync your project issues."
|
description={emptyStateDetail.description}
|
||||||
image={emptyIntegration}
|
image={emptyStateImage}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "Configure now",
|
text: "Configure now",
|
||||||
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
|
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
|
||||||
}}
|
}}
|
||||||
|
size="lg"
|
||||||
disabled={!isAdmin}
|
disabled={!isAdmin}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,7 @@ import { ProjectSettingHeader } from "components/headers";
|
|||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
|
||||||
const LabelsSettingsPage: NextPageWithLayout = () => (
|
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 />
|
<ProjectSettingsLabelList />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
// store hooks
|
// store hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useUser } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
@ -9,7 +10,8 @@ import { AppLayout } from "layouts/app-layout";
|
|||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
// component
|
// component
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
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
|
// ui
|
||||||
import { Button, Spinner } from "@plane/ui";
|
import { Button, Spinner } from "@plane/ui";
|
||||||
// services
|
// services
|
||||||
@ -19,6 +21,7 @@ import { NextPageWithLayout } from "lib/types";
|
|||||||
// constants
|
// constants
|
||||||
import { API_TOKENS_LIST } from "constants/fetch-keys";
|
import { API_TOKENS_LIST } from "constants/fetch-keys";
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
const apiTokenService = new APITokenService();
|
const apiTokenService = new APITokenService();
|
||||||
|
|
||||||
@ -28,9 +31,12 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
// theme
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
|
currentUser,
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
|
||||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||||
@ -39,6 +45,10 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
|||||||
workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null
|
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)
|
if (!isAdmin)
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
<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)} />
|
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
|
||||||
{tokens ? (
|
{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 ? (
|
{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>
|
<h3 className="text-xl font-medium">API tokens</h3>
|
||||||
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
|
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
|
||||||
Add API token
|
Add API token
|
||||||
@ -66,8 +76,21 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="mx-auto">
|
<div className="flex h-full w-full flex-col">
|
||||||
<ApiTokenEmptyState onClick={() => setIsCreateTokenModalOpen(true)} />
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser, useWebhook, useWorkspace } from "hooks/store";
|
import { useUser, useWebhook, useWorkspace } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
@ -9,11 +10,14 @@ import { AppLayout } from "layouts/app-layout";
|
|||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
// components
|
// components
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
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
|
// ui
|
||||||
import { Button, Spinner } from "@plane/ui";
|
import { Button, Spinner } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// constants
|
||||||
|
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||||
|
|
||||||
const WebhooksListPage: NextPageWithLayout = observer(() => {
|
const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||||
// states
|
// states
|
||||||
@ -21,9 +25,12 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
// theme
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
// mobx store
|
// mobx store
|
||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
|
currentUser,
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook();
|
const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
@ -35,6 +42,11 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
|||||||
workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null
|
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.
|
// clear secret key when modal is closed.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey();
|
if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey();
|
||||||
@ -76,8 +88,21 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
|||||||
<WebhooksList />
|
<WebhooksList />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mx-auto">
|
<div className="flex h-full w-full flex-col">
|
||||||
<WebhooksEmptyState onClick={() => setShowCreateWebhookModal(true)} />
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 79 KiB |
BIN
web/public/empty-state/cycle-issues/calendar-dark-resp.webp
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
web/public/empty-state/cycle-issues/calendar-dark.webp
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
web/public/empty-state/cycle-issues/calendar-light-resp.webp
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
web/public/empty-state/cycle-issues/calendar-light.webp
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
web/public/empty-state/cycle-issues/gantt_chart-dark-resp.webp
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
web/public/empty-state/cycle-issues/gantt_chart-dark.webp
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
web/public/empty-state/cycle-issues/gantt_chart-light-resp.webp
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
web/public/empty-state/cycle-issues/gantt_chart-light.webp
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
web/public/empty-state/cycle-issues/kanban-dark-resp.webp
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
web/public/empty-state/cycle-issues/kanban-dark.webp
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
web/public/empty-state/cycle-issues/kanban-light-resp.webp
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
web/public/empty-state/cycle-issues/kanban-light.webp
Normal file
After Width: | Height: | Size: 98 KiB |
BIN
web/public/empty-state/cycle-issues/list-dark-resp.webp
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
web/public/empty-state/cycle-issues/list-dark.webp
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
web/public/empty-state/cycle-issues/list-light-resp.webp
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
web/public/empty-state/cycle-issues/list-light.webp
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
web/public/empty-state/cycle-issues/spreadsheet-dark-resp.webp
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
web/public/empty-state/cycle-issues/spreadsheet-dark.webp
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
web/public/empty-state/cycle-issues/spreadsheet-light-resp.webp
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
web/public/empty-state/cycle-issues/spreadsheet-light.webp
Normal file
After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 82 KiB |
BIN
web/public/empty-state/draft/draft-issues-empty-dark.webp
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
web/public/empty-state/draft/draft-issues-empty-light.webp
Normal file
After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 98 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 86 KiB |
BIN
web/public/empty-state/inbox/inbox-dark-resp.webp
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
web/public/empty-state/inbox/inbox-dark.webp
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
web/public/empty-state/inbox/inbox-light-resp.webp
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
web/public/empty-state/inbox/inbox-light.webp
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
web/public/empty-state/module-issues/calendar-dark-resp.webp
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
web/public/empty-state/module-issues/calendar-dark.webp
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
web/public/empty-state/module-issues/calendar-light-resp.webp
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
web/public/empty-state/module-issues/calendar-light.webp
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
web/public/empty-state/module-issues/gantt-dark-resp.webp
Normal file
After Width: | Height: | Size: 43 KiB |