fix: merge conflicts resolved
@@ -137,7 +137,7 @@ services:
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: no
|
||||
restart: "no"
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
|
||||
2
packages/types/src/projects.d.ts
vendored
@@ -128,7 +128,7 @@ export type TProjectIssuesSearchParams = {
|
||||
parent?: boolean;
|
||||
issue_relation?: boolean;
|
||||
cycle?: boolean;
|
||||
module?: string[];
|
||||
module?: string;
|
||||
sub_issue?: boolean;
|
||||
issue_id?: string;
|
||||
workspace_search: boolean;
|
||||
|
||||
@@ -78,7 +78,6 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !workspaceSlug || !projectId) return;
|
||||
if (issues.length <= 0) setIsSearching(true);
|
||||
|
||||
projectService
|
||||
.projectIssuesSearch(workspaceSlug as string, projectId as string, {
|
||||
@@ -88,16 +87,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
})
|
||||
.then((res) => setIssues(res))
|
||||
.finally(() => setIsSearching(false));
|
||||
}, [issues, debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, searchParams, workspaceSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchTerm("");
|
||||
setIssues([]);
|
||||
setSelectedIssues([]);
|
||||
setIsSearching(false);
|
||||
setIsSubmitting(false);
|
||||
setIsWorkspaceLevel(false);
|
||||
}, [isOpen]);
|
||||
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, searchParams, workspaceSlug]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -8,7 +8,7 @@ export const SidebarHamburgerToggle: FC = observer(() => {
|
||||
return (
|
||||
<div
|
||||
className="w-7 h-7 flex-shrink-0 rounded flex justify-center items-center bg-custom-background-80 transition-all hover:bg-custom-background-90 cursor-pointer group md:hidden"
|
||||
onClick={() => themStore.toggleSidebar()}
|
||||
onClick={() => themStore.toggleMobileSidebar()}
|
||||
>
|
||||
<Menu size={14} className="text-custom-text-200 group-hover:text-custom-text-100 transition-all" />
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,8 @@ import { ICycle, TCycleGroups } from "@plane/types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS, CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle";
|
||||
import { CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
interface IActiveCycleDetails {
|
||||
workspaceSlug: string;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useUser } from "hooks/store";
|
||||
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// constants
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export interface ICyclesBoard {
|
||||
cycleIds: string[];
|
||||
|
||||
@@ -9,7 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// constants
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export interface ICyclesList {
|
||||
cycleIds: string[];
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useCycle } from "hooks/store";
|
||||
// components
|
||||
import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles";
|
||||
// ui components
|
||||
import { Loader } from "@plane/ui";
|
||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui";
|
||||
// types
|
||||
import { TCycleLayout, TCycleView } from "@plane/types";
|
||||
|
||||
@@ -25,6 +25,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
|
||||
currentProjectDraftCycleIds,
|
||||
currentProjectUpcomingCycleIds,
|
||||
currentProjectCycleIds,
|
||||
loader,
|
||||
} = useCycle();
|
||||
|
||||
const cyclesList =
|
||||
@@ -36,55 +37,32 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
|
||||
? currentProjectUpcomingCycleIds
|
||||
: currentProjectCycleIds;
|
||||
|
||||
if (loader || !cyclesList)
|
||||
return (
|
||||
<>
|
||||
{layout === "list" && <CycleModuleListLayout />}
|
||||
{layout === "board" && <CycleModuleBoardLayout />}
|
||||
{layout === "gantt" && <GanttLayoutLoader />}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{layout === "list" && (
|
||||
<>
|
||||
{cyclesList ? (
|
||||
<CyclesList cycleIds={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
) : (
|
||||
<Loader className="space-y-4 p-8">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
<CyclesList cycleIds={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
)}
|
||||
|
||||
{layout === "board" && (
|
||||
<>
|
||||
{cyclesList ? (
|
||||
<CyclesBoard
|
||||
cycleIds={cyclesList}
|
||||
filter={filter}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
peekCycle={peekCycle}
|
||||
/>
|
||||
) : (
|
||||
<Loader className="grid grid-cols-1 gap-9 p-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
<CyclesBoard
|
||||
cycleIds={cyclesList}
|
||||
filter={filter}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
peekCycle={peekCycle}
|
||||
/>
|
||||
)}
|
||||
|
||||
{layout === "gantt" && (
|
||||
<>
|
||||
{cyclesList ? (
|
||||
<CyclesListGanttChartView cycleIds={cyclesList} workspaceSlug={workspaceSlug} />
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{layout === "gantt" && <CyclesListGanttChartView cycleIds={cyclesList} workspaceSlug={workspaceSlug} />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
// store hooks
|
||||
import { useEstimate, useProject } from "hooks/store";
|
||||
import { useEstimate, useProject, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { EmptyState } from "components/common";
|
||||
// images
|
||||
import emptyEstimate from "public/empty-state/estimate.svg";
|
||||
// types
|
||||
import { IEstimate } from "@plane/types";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// constants
|
||||
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export const EstimatesList: React.FC = observer(() => {
|
||||
// states
|
||||
@@ -25,9 +25,12 @@ export const EstimatesList: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { updateProject, currentProjectDetails } = useProject();
|
||||
const { projectEstimates, getProjectEstimateById } = useEstimate();
|
||||
const { currentUser } = useUser();
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@@ -55,6 +58,10 @@ export const EstimatesList: React.FC = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["estimate"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("project-settings", "estimates", isLightMode);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateEstimateModal
|
||||
@@ -108,19 +115,12 @@ export const EstimatesList: React.FC = observer(() => {
|
||||
))}
|
||||
</section>
|
||||
) : (
|
||||
<div className="w-full py-8">
|
||||
<div className="h-full w-full py-8">
|
||||
<EmptyState
|
||||
title="No estimates yet"
|
||||
description="Estimates help you communicate the complexity of an issue."
|
||||
image={emptyEstimate}
|
||||
primaryButton={{
|
||||
icon: <Plus className="h-4 w-4" />,
|
||||
text: "Add Estimate",
|
||||
onClick: () => {
|
||||
setEstimateFormOpen(true);
|
||||
setEstimateToUpdate(undefined);
|
||||
},
|
||||
}}
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,25 +3,28 @@ import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// services
|
||||
import { IntegrationService } from "services/integrations";
|
||||
// components
|
||||
import { Exporter, SingleExport } from "components/exporter";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { Button } from "@plane/ui";
|
||||
import { ImportExportSettingsLoader } from "components/ui";
|
||||
// icons
|
||||
import { MoveLeft, MoveRight, RefreshCw } from "lucide-react";
|
||||
// fetch-keys
|
||||
import { EXPORT_SERVICES_LIST } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { EXPORTERS_LIST } from "constants/workspace";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useUser } from "hooks/store";
|
||||
|
||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
// services
|
||||
const integrationService = new IntegrationService();
|
||||
@@ -34,6 +37,8 @@ const IntegrationGuide = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, provider } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser, currentUserLoader } = useUser();
|
||||
// custom hooks
|
||||
@@ -46,6 +51,10 @@ const IntegrationGuide = observer(() => {
|
||||
: null
|
||||
);
|
||||
|
||||
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["export"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "exports", isLightMode);
|
||||
|
||||
const handleCsvClose = () => {
|
||||
router.replace(`/${workspaceSlug?.toString()}/settings/exports`);
|
||||
};
|
||||
@@ -140,15 +149,17 @@ const IntegrationGuide = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="px-4 py-6 text-sm text-custom-text-200">No previous export available.</p>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="mt-6 grid grid-cols-1 gap-3">
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
</Loader>
|
||||
<ImportExportSettingsLoader />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -86,7 +86,7 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = observer((props)
|
||||
className="h-full"
|
||||
style={{
|
||||
width: `${itemsContainerWidth}px`,
|
||||
marginTop: `${HEADER_HEIGHT}px`,
|
||||
transform: `translateY(${HEADER_HEIGHT}px)`,
|
||||
}}
|
||||
>
|
||||
{blocks?.map((block) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
@@ -11,8 +12,10 @@ import useUserAuth from "hooks/use-user-auth";
|
||||
import { IntegrationService } from "services/integrations";
|
||||
// components
|
||||
import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { Button } from "@plane/ui";
|
||||
import { ImportExportSettingsLoader } from "components/ui";
|
||||
// icons
|
||||
import { RefreshCw } from "lucide-react";
|
||||
// types
|
||||
@@ -21,6 +24,7 @@ import { IImporterService } from "@plane/types";
|
||||
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { IMPORTERS_LIST } from "constants/workspace";
|
||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
// services
|
||||
const integrationService = new IntegrationService();
|
||||
@@ -33,6 +37,8 @@ const IntegrationGuide = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, provider } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser, currentUserLoader } = useUser();
|
||||
// custom hooks
|
||||
@@ -43,6 +49,10 @@ const IntegrationGuide = observer(() => {
|
||||
workspaceSlug ? () => integrationService.getImporterServicesList(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["import"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "imports", isLightMode);
|
||||
|
||||
const handleDeleteImport = (importService: IImporterService) => {
|
||||
setImportToDelete(importService);
|
||||
setDeleteImportModal(true);
|
||||
@@ -134,15 +144,17 @@ const IntegrationGuide = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="px-4 py-6 text-sm text-custom-text-200">No previous imports available.</p>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="mt-6 grid grid-cols-1 gap-3">
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
</Loader>
|
||||
<ImportExportSettingsLoader />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -168,7 +168,7 @@ export const SingleIntegrationCard: React.FC<Props> = observer(({ integration })
|
||||
)
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="35px" width="150px" />
|
||||
<Loader.Item height="32px" width="64px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
@@ -65,20 +66,19 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => {
|
||||
const emptyStateProps: EmptyStateProps =
|
||||
issueFilterCount > 0
|
||||
? {
|
||||
title: "No issues found matching the filters applied",
|
||||
title: EMPTY_FILTER_STATE_DETAILS["archived"].title,
|
||||
image: currentLayoutEmptyStateImagePath,
|
||||
secondaryButton: {
|
||||
text: "Clear all filters",
|
||||
text: EMPTY_FILTER_STATE_DETAILS["archived"].secondaryButton?.text,
|
||||
onClick: handleClearAllFilters,
|
||||
},
|
||||
}
|
||||
: {
|
||||
title: "No archived issues yet",
|
||||
description:
|
||||
"Archived issues help you remove issues you completed or cancelled from focus. You can set automation to auto archive issues and find them here.",
|
||||
title: EMPTY_ISSUE_STATE_DETAILS["archived"].title,
|
||||
description: EMPTY_ISSUE_STATE_DETAILS["archived"].description,
|
||||
image: EmptyStateImagePath,
|
||||
primaryButton: {
|
||||
text: "Set Automation",
|
||||
text: EMPTY_ISSUE_STATE_DETAILS["archived"].primaryButton.text,
|
||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`),
|
||||
},
|
||||
size: "sm",
|
||||
|
||||
@@ -5,28 +5,29 @@ import { PlusIcon } from "lucide-react";
|
||||
import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { EmptyState } from "components/common";
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// assets
|
||||
import emptyIssue from "public/empty-state/issue.svg";
|
||||
// types
|
||||
import { ISearchIssueResponse } from "@plane/types";
|
||||
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { useTheme } from "next-themes";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
projectId: string | undefined;
|
||||
cycleId: string | undefined;
|
||||
activeLayout: TIssueLayouts | undefined;
|
||||
};
|
||||
|
||||
export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, cycleId } = props;
|
||||
const { workspaceSlug, projectId, cycleId, activeLayout } = props;
|
||||
// states
|
||||
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { issues } = useIssues(EIssuesStoreType.CYCLE);
|
||||
const { updateIssue, fetchIssue } = useIssueDetail();
|
||||
@@ -36,6 +37,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const {
|
||||
membership: { currentProjectRole: userRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
@@ -60,6 +62,11 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"];
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("cycle-issues", activeLayout ?? "list", isLightMode);
|
||||
|
||||
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
return (
|
||||
@@ -74,27 +81,23 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<EmptyState
|
||||
title="Cycle issues will appear here"
|
||||
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
|
||||
image={emptyIssue}
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
primaryButton={{
|
||||
text: "New issue",
|
||||
text: emptyStateDetail.primaryButton.text,
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => {
|
||||
setTrackElement("Cycle issue empty state");
|
||||
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
|
||||
},
|
||||
}}
|
||||
secondaryButton={
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
|
||||
onClick={() => setCycleIssuesListModal(true)}
|
||||
disabled={!isEditingAllowed}
|
||||
>
|
||||
Add an existing issue
|
||||
</Button>
|
||||
}
|
||||
secondaryButton={{
|
||||
text: emptyStateDetail.secondaryButton.text,
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => setCycleIssuesListModal(true),
|
||||
}}
|
||||
size="sm"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
@@ -65,17 +66,16 @@ export const ProjectDraftEmptyState: React.FC = observer(() => {
|
||||
const emptyStateProps: EmptyStateProps =
|
||||
issueFilterCount > 0
|
||||
? {
|
||||
title: "No issues found matching the filters applied",
|
||||
title: EMPTY_FILTER_STATE_DETAILS["draft"].title,
|
||||
image: currentLayoutEmptyStateImagePath,
|
||||
secondaryButton: {
|
||||
text: "Clear all filters",
|
||||
text: EMPTY_FILTER_STATE_DETAILS["draft"].secondaryButton.text,
|
||||
onClick: handleClearAllFilters,
|
||||
},
|
||||
}
|
||||
: {
|
||||
title: "No draft issues yet",
|
||||
description:
|
||||
"Quickly stepping away but want to keep your place? No worries – save a draft now. Your issues will be right here waiting for you.",
|
||||
title: EMPTY_ISSUE_STATE_DETAILS["draft"].title,
|
||||
description: EMPTY_ISSUE_STATE_DETAILS["draft"].description,
|
||||
image: EmptyStateImagePath,
|
||||
size: "sm",
|
||||
disabled: !isEditingAllowed,
|
||||
|
||||
@@ -5,37 +5,38 @@ import { PlusIcon } from "lucide-react";
|
||||
import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { EmptyState } from "components/common";
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// assets
|
||||
import emptyIssue from "public/empty-state/issue.svg";
|
||||
// types
|
||||
import { ISearchIssueResponse } from "@plane/types";
|
||||
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { useTheme } from "next-themes";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
projectId: string | undefined;
|
||||
moduleId: string | undefined;
|
||||
activeLayout: TIssueLayouts | undefined;
|
||||
};
|
||||
|
||||
export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, moduleId } = props;
|
||||
const { workspaceSlug, projectId, moduleId, activeLayout } = props;
|
||||
// states
|
||||
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { issues } = useIssues(EIssuesStoreType.MODULE);
|
||||
|
||||
const {
|
||||
commandPalette: { toggleCreateIssueModal },
|
||||
} = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const {
|
||||
membership: { currentProjectRole: userRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
@@ -55,6 +56,11 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const emptyStateDetail = MODULE_EMPTY_STATE_DETAILS["no-issues"];
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("cycle-issues", activeLayout ?? "list", isLightMode);
|
||||
|
||||
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
return (
|
||||
@@ -64,32 +70,27 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
projectId={projectId}
|
||||
isOpen={moduleIssuesListModal}
|
||||
handleClose={() => setModuleIssuesListModal(false)}
|
||||
searchParams={{ module: moduleId != undefined ? [moduleId.toString()] : [] }}
|
||||
searchParams={{ module: moduleId != undefined ? moduleId.toString() : "" }}
|
||||
handleOnSubmit={handleAddIssuesToModule}
|
||||
/>
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<EmptyState
|
||||
title="Module issues will appear here"
|
||||
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
|
||||
image={emptyIssue}
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
primaryButton={{
|
||||
text: "New issue",
|
||||
text: emptyStateDetail.primaryButton.text,
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => {
|
||||
setTrackElement("Module issue empty state");
|
||||
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
|
||||
},
|
||||
}}
|
||||
secondaryButton={
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
|
||||
onClick={() => setModuleIssuesListModal(true)}
|
||||
disabled={!isEditingAllowed}
|
||||
>
|
||||
Add an existing issue
|
||||
</Button>
|
||||
}
|
||||
secondaryButton={{
|
||||
text: emptyStateDetail.secondaryButton.text,
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => setModuleIssuesListModal(true),
|
||||
}}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
@@ -67,26 +68,23 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
||||
const emptyStateProps: EmptyStateProps =
|
||||
issueFilterCount > 0
|
||||
? {
|
||||
title: "No issues found matching the filters applied",
|
||||
title: EMPTY_FILTER_STATE_DETAILS["project"].title,
|
||||
image: currentLayoutEmptyStateImagePath,
|
||||
secondaryButton: {
|
||||
text: "Clear all filters",
|
||||
text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text,
|
||||
onClick: handleClearAllFilters,
|
||||
},
|
||||
}
|
||||
: {
|
||||
title: "Create an issue and assign it to someone, even yourself",
|
||||
description:
|
||||
"Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.",
|
||||
title: EMPTY_ISSUE_STATE_DETAILS["project"].title,
|
||||
description: EMPTY_ISSUE_STATE_DETAILS["project"].description,
|
||||
image: EmptyStateImagePath,
|
||||
comicBox: {
|
||||
title: "Issues are building blocks in Plane.",
|
||||
description:
|
||||
"Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.",
|
||||
title: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.title,
|
||||
description: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.description,
|
||||
},
|
||||
primaryButton: {
|
||||
text: "Create your first issue",
|
||||
|
||||
text: EMPTY_ISSUE_STATE_DETAILS["project"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Project issue empty state");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||
|
||||
@@ -138,14 +138,14 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm transition-all hover:border-custom-border-400",
|
||||
"rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
||||
{ "hover:cursor-grab": !isDragDisabled },
|
||||
{ "border-custom-primary-100": snapshot.isDragging },
|
||||
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id }
|
||||
)}
|
||||
>
|
||||
<RenderIfVisible
|
||||
classNames="space-y-2"
|
||||
classNames="space-y-2 px-3 py-2"
|
||||
root={scrollableContainerRef}
|
||||
defaultHeight="100px"
|
||||
horizonatlOffset={50}
|
||||
|
||||
@@ -59,7 +59,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const renderExistingIssueModal = moduleId || cycleId;
|
||||
const ExistingIssuesListModalPayload = moduleId ? { module: [moduleId.toString()] } : { cycle: true };
|
||||
const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true };
|
||||
|
||||
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
@@ -67,10 +67,13 @@ export const handleDragDrop = async (
|
||||
|
||||
let updateIssue: any = {};
|
||||
|
||||
const sourceColumnId = (source?.droppableId && source?.droppableId.split("__")) || null;
|
||||
const destinationColumnId = (destination?.droppableId && destination?.droppableId.split("__")) || null;
|
||||
const sourceDroppableId = source?.droppableId;
|
||||
const destinationDroppableId = destination?.droppableId;
|
||||
|
||||
if (!sourceColumnId || !destinationColumnId) return;
|
||||
const sourceColumnId = (sourceDroppableId && sourceDroppableId.split("__")) || null;
|
||||
const destinationColumnId = (destinationDroppableId && destinationDroppableId.split("__")) || null;
|
||||
|
||||
if (!sourceColumnId || !destinationColumnId || !sourceDroppableId || !destinationDroppableId) return;
|
||||
|
||||
const sourceGroupByColumnId = sourceColumnId[0] || null;
|
||||
const destinationGroupByColumnId = destinationColumnId[0] || null;
|
||||
@@ -101,9 +104,13 @@ export const handleDragDrop = async (
|
||||
else return await store?.removeIssue(workspaceSlug, projectId, removed);
|
||||
}
|
||||
} else {
|
||||
const sourceIssues = subGroupBy
|
||||
? (issueWithIds as TSubGroupedIssues)[sourceSubGroupByColumnId][sourceGroupByColumnId]
|
||||
: (issueWithIds as TGroupedIssues)[sourceGroupByColumnId];
|
||||
//spreading the array to stop changing the original reference
|
||||
//since we are removing an id from array further down
|
||||
const sourceIssues = [
|
||||
...(subGroupBy
|
||||
? (issueWithIds as TSubGroupedIssues)[sourceSubGroupByColumnId][sourceGroupByColumnId]
|
||||
: (issueWithIds as TGroupedIssues)[sourceGroupByColumnId]),
|
||||
];
|
||||
const destinationIssues = subGroupBy
|
||||
? (issueWithIds as TSubGroupedIssues)[sourceSubGroupByColumnId][destinationGroupByColumnId]
|
||||
: (issueWithIds as TGroupedIssues)[destinationGroupByColumnId];
|
||||
@@ -119,7 +126,11 @@ export const handleDragDrop = async (
|
||||
// for both horizontal and vertical dnd
|
||||
updateIssue = {
|
||||
...updateIssue,
|
||||
...handleSortOrder(destinationIssues, destination.index, issueMap),
|
||||
...handleSortOrder(
|
||||
sourceDroppableId === destinationDroppableId ? sourceIssues : destinationIssues,
|
||||
destination.index,
|
||||
issueMap
|
||||
),
|
||||
};
|
||||
|
||||
if (subGroupBy && sourceSubGroupByColumnId && destinationSubGroupByColumnId) {
|
||||
|
||||
@@ -41,7 +41,7 @@ export const HeaderGroupByCard = observer(
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const renderExistingIssueModal = moduleId || cycleId;
|
||||
const ExistingIssuesListModalPayload = moduleId ? { module: [moduleId.toString()] } : { cycle: true };
|
||||
const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true };
|
||||
|
||||
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
@@ -12,15 +12,15 @@ import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/iss
|
||||
import { SpreadsheetView } from "components/issues/issue-layouts";
|
||||
import { AllIssueQuickActions } from "components/issues/issue-layouts/quick-action-dropdowns";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { SpreadsheetLayoutLoader } from "components/ui";
|
||||
// types
|
||||
import { TIssue, IIssueDisplayFilterOptions } from "@plane/types";
|
||||
import { EIssueActions } from "../types";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { ALL_ISSUES_EMPTY_STATE_DETAILS, EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { ALL_ISSUES_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
// router
|
||||
@@ -177,64 +177,60 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) {
|
||||
return <SpreadsheetLayoutLoader />;
|
||||
}
|
||||
|
||||
if (issueIds.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title={(workspaceProjectIds ?? []).length > 0 ? currentViewDetails.title : "No project"}
|
||||
description={
|
||||
(workspaceProjectIds ?? []).length > 0
|
||||
? currentViewDetails.description
|
||||
: "To create issues or manage your work, you need to create a project or be a part of one."
|
||||
}
|
||||
size="sm"
|
||||
primaryButton={
|
||||
(workspaceProjectIds ?? []).length > 0
|
||||
? currentView !== "custom-view" && currentView !== "subscribed"
|
||||
? {
|
||||
text: "Create new issue",
|
||||
onClick: () => {
|
||||
setTrackElement("All issues empty state");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
: {
|
||||
text: "Start your first project",
|
||||
onClick: () => {
|
||||
setTrackElement("All issues empty state");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
},
|
||||
}
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{!globalViewId || globalViewId !== dataViewId || loader === "init-loader" || !issueIds ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} />
|
||||
|
||||
{(issueIds ?? {}).length == 0 ? (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title={(workspaceProjectIds ?? []).length > 0 ? currentViewDetails.title : "No project"}
|
||||
description={
|
||||
(workspaceProjectIds ?? []).length > 0
|
||||
? currentViewDetails.description
|
||||
: "To create issues or manage your work, you need to create a project or be a part of one."
|
||||
}
|
||||
size="sm"
|
||||
primaryButton={
|
||||
(workspaceProjectIds ?? []).length > 0
|
||||
? currentView !== "custom-view" && currentView !== "subscribed"
|
||||
? {
|
||||
text: "Create new issue",
|
||||
onClick: () => {
|
||||
setTrackElement("All issues empty state");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
: {
|
||||
text: "Start your first project",
|
||||
onClick: () => {
|
||||
setTrackElement("All issues empty state");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
},
|
||||
}
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
) : (
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<SpreadsheetView
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||
issueIds={issueIds}
|
||||
quickActions={renderQuickActions}
|
||||
handleIssues={handleIssues}
|
||||
canEditProperties={canEditProperties}
|
||||
viewId={globalViewId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} />
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<SpreadsheetView
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||
issueIds={issueIds}
|
||||
quickActions={renderQuickActions}
|
||||
handleIssues={handleIssues}
|
||||
canEditProperties={canEditProperties}
|
||||
viewId={globalViewId}
|
||||
/>
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "components/issues";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { ListLayoutLoader } from "components/ui";
|
||||
|
||||
export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
||||
// router
|
||||
@@ -36,31 +36,26 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
||||
}
|
||||
);
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <ListLayoutLoader />;
|
||||
}
|
||||
|
||||
if (issues?.groupedIssueIds?.length === 0) {
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ProjectArchivedEmptyState />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<ArchivedIssueAppliedFiltersRoot />
|
||||
|
||||
{issues?.loader === "init-loader" ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!issues?.groupedIssueIds ? (
|
||||
<ProjectArchivedEmptyState />
|
||||
) : (
|
||||
<>
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<ArchivedIssueListLayout />
|
||||
</div>
|
||||
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview is_archived />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<ArchivedIssueListLayout />
|
||||
</div>
|
||||
<IssuePeekOverview is_archived />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
IssuePeekOverview,
|
||||
} from "components/issues";
|
||||
import { TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// constants
|
||||
@@ -53,6 +54,34 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase() ?? "draft";
|
||||
|
||||
if (!workspaceSlug || !projectId || !cycleId) return <></>;
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return (
|
||||
<>
|
||||
{activeLayout ? (
|
||||
<ActiveLoader layout={activeLayout} />
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (issues?.groupedIssueIds?.length === 0) {
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<CycleEmptyState
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
cycleId={cycleId.toString()}
|
||||
activeLayout={activeLayout}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
|
||||
@@ -61,39 +90,21 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
|
||||
<CycleAppliedFiltersRoot />
|
||||
|
||||
{issues?.loader === "init-loader" || !issues?.groupedIssueIds ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<CycleEmptyState
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
cycleId={cycleId.toString()}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<CycleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<CycleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CycleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<CycleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<CycleSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<CycleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<CycleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CycleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<CycleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<CycleSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/d
|
||||
import { DraftIssueListLayout } from "../list/roots/draft-issue-root";
|
||||
import { ProjectDraftEmptyState } from "../empty-states";
|
||||
import { IssuePeekOverview } from "components/issues/peek-overview";
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root";
|
||||
@@ -39,31 +40,37 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return (
|
||||
<>
|
||||
{activeLayout ? (
|
||||
<ActiveLoader layout={activeLayout} />
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (issues?.groupedIssueIds?.length === 0) {
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ProjectDraftEmptyState />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<DraftIssueAppliedFiltersRoot />
|
||||
|
||||
{issues?.loader === "init-loader" ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!issues?.groupedIssueIds ? (
|
||||
<ProjectDraftEmptyState />
|
||||
) : (
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<DraftIssueListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<DraftKanBanLayout />
|
||||
) : null}
|
||||
{/* issue peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? <DraftIssueListLayout /> : activeLayout === "kanban" ? <DraftKanBanLayout /> : null}
|
||||
{/* issue peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ModuleListLayout,
|
||||
ModuleSpreadsheetLayout,
|
||||
} from "components/issues";
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// constants
|
||||
@@ -44,46 +45,56 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
|
||||
}
|
||||
);
|
||||
|
||||
if (!workspaceSlug || !projectId || !moduleId) return <></>;
|
||||
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
if (!workspaceSlug || !projectId || !moduleId) return <></>;
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return (
|
||||
<>
|
||||
{activeLayout ? (
|
||||
<ActiveLoader layout={activeLayout} />
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (issues?.groupedIssueIds?.length === 0) {
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ModuleEmptyState
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
moduleId={moduleId.toString()}
|
||||
activeLayout={activeLayout}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<ModuleAppliedFiltersRoot />
|
||||
|
||||
{issues?.loader === "init-loader" || !issues?.groupedIssueIds ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<ModuleEmptyState
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
moduleId={moduleId.toString()}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ModuleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ModuleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ModuleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ModuleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ModuleSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ModuleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ModuleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ModuleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ModuleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ModuleSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
import { Spinner } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssues } from "hooks/store";
|
||||
// helpers
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
|
||||
@@ -41,49 +43,45 @@ export const ProjectLayoutRoot: FC = observer(() => {
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
|
||||
}
|
||||
|
||||
if (issues?.groupedIssueIds?.length === 0) {
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ProjectEmptyState />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<ProjectAppliedFiltersRoot />
|
||||
|
||||
{issues?.loader === "init-loader" || !issues?.groupedIssueIds ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ProjectEmptyState />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
|
||||
{/* mutation loader */}
|
||||
{issues?.loader === "mutation" && (
|
||||
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded">
|
||||
<Spinner className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
|
||||
{/* mutation loader */}
|
||||
{issues?.loader === "mutation" && (
|
||||
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded">
|
||||
<Spinner className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
{activeLayout === "list" ? (
|
||||
<ListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<KanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<GanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{activeLayout === "list" ? (
|
||||
<ListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<KanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<GanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ProjectViewSpreadsheetLayout,
|
||||
} from "components/issues";
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
// types
|
||||
@@ -63,40 +64,49 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
|
||||
if (!workspaceSlug || !projectId || !viewId) return <></>;
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return (
|
||||
<>
|
||||
{activeLayout ? (
|
||||
<ActiveLoader layout={activeLayout} />
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (issues?.groupedIssueIds?.length === 0) {
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ProjectViewEmptyState />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<ProjectViewAppliedFiltersRoot />
|
||||
|
||||
{issues?.loader === "init-loader" || !issues?.groupedIssueIds ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<ProjectViewEmptyState />
|
||||
) : (
|
||||
<>
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ProjectViewListLayout issueActions={issueActions} />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ProjectViewKanBanLayout issueActions={issueActions} />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ProjectViewCalendarLayout issueActions={issueActions} />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ProjectViewGanttLayout issueActions={issueActions} />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectViewSpreadsheetLayout issueActions={issueActions} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ProjectViewListLayout issueActions={issueActions} />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ProjectViewKanBanLayout issueActions={issueActions} />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ProjectViewCalendarLayout issueActions={issueActions} />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ProjectViewGanttLayout issueActions={issueActions} />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectViewSpreadsheetLayout issueActions={issueActions} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,8 +9,9 @@ import {
|
||||
DropResult,
|
||||
Droppable,
|
||||
} from "@hello-pangea/dnd";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useLabel } from "hooks/store";
|
||||
import { useLabel, useUser } from "hooks/store";
|
||||
import useDraggableInPortal from "hooks/use-draggable-portal";
|
||||
// components
|
||||
import {
|
||||
@@ -19,13 +20,13 @@ import {
|
||||
ProjectSettingLabelGroup,
|
||||
ProjectSettingLabelItem,
|
||||
} from "components/labels";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { EmptyState } from "components/common";
|
||||
// images
|
||||
import emptyLabel from "public/empty-state/label.svg";
|
||||
// types
|
||||
import { IIssueLabel } from "@plane/types";
|
||||
// constants
|
||||
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
const LABELS_ROOT = "labels.root";
|
||||
|
||||
@@ -40,7 +41,10 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel();
|
||||
// portal
|
||||
const renderDraggable = useDraggableInPortal();
|
||||
@@ -50,6 +54,10 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||
setLabelForm(true);
|
||||
};
|
||||
|
||||
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["labels"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("project-settings", "labels", isLightMode);
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
const { combine, draggableId, destination, source } = result;
|
||||
|
||||
@@ -94,7 +102,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||
Add label
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full py-8">
|
||||
<div className="h-full w-full py-8">
|
||||
{showLabelForm && (
|
||||
<div className="w-full rounded border border-custom-border-200 px-3.5 py-2 my-2">
|
||||
<CreateUpdateLabelInline
|
||||
@@ -111,15 +119,14 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||
)}
|
||||
{projectLabels ? (
|
||||
projectLabels.length === 0 && !showLabelForm ? (
|
||||
<EmptyState
|
||||
title="No labels yet"
|
||||
description="Create labels to help organize and filter issues in you project"
|
||||
image={emptyLabel}
|
||||
primaryButton={{
|
||||
text: "Add label",
|
||||
onClick: () => newLabel(),
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
projectLabelsTree && (
|
||||
<DragDropContext
|
||||
|
||||
@@ -8,9 +8,10 @@ import useLocalStorage from "hooks/use-local-storage";
|
||||
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Loader, Spinner } from "@plane/ui";
|
||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export const ModulesListView: React.FC = observer(() => {
|
||||
// router
|
||||
@@ -34,23 +35,13 @@ export const ModulesListView: React.FC = observer(() => {
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
if (loader)
|
||||
if (loader || !projectModuleIds)
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!projectModuleIds)
|
||||
return (
|
||||
<Loader className="grid grid-cols-3 gap-4 p-8">
|
||||
<Loader.Item height="176px" />
|
||||
<Loader.Item height="176px" />
|
||||
<Loader.Item height="176px" />
|
||||
<Loader.Item height="176px" />
|
||||
<Loader.Item height="176px" />
|
||||
<Loader.Item height="176px" />
|
||||
</Loader>
|
||||
<>
|
||||
{modulesView === "list" && <CycleModuleListLayout />}
|
||||
{modulesView === "grid" && <CycleModuleBoardLayout />}
|
||||
{modulesView === "gantt_chart" && <GanttLayoutLoader />}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -97,16 +88,15 @@ export const ModulesListView: React.FC = observer(() => {
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="Map your project milestones to Modules and track aggregated work easily."
|
||||
description="A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone."
|
||||
title={MODULE_EMPTY_STATE_DETAILS["modules"].title}
|
||||
description={MODULE_EMPTY_STATE_DETAILS["modules"].description}
|
||||
image={EmptyStateImagePath}
|
||||
comicBox={{
|
||||
title: "Modules help group work by hierarchy.",
|
||||
description:
|
||||
"A cart module, a chassis module, and a warehouse module are all good example of this grouping.",
|
||||
title: MODULE_EMPTY_STATE_DETAILS["modules"].comicBox.title,
|
||||
description: MODULE_EMPTY_STATE_DETAILS["modules"].comicBox.description,
|
||||
}}
|
||||
primaryButton={{
|
||||
text: "Build your first module",
|
||||
text: MODULE_EMPTY_STATE_DETAILS["modules"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Module empty state");
|
||||
commandPaletteStore.toggleCreateModuleModal(true);
|
||||
|
||||
@@ -9,7 +9,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { EmptyState } from "components/common";
|
||||
import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications";
|
||||
import { Loader, Tooltip } from "@plane/ui";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { NotificationsLoader } from "components/ui";
|
||||
// images
|
||||
import emptyNotification from "public/empty-state/notification.svg";
|
||||
// helpers
|
||||
@@ -188,13 +189,7 @@ export const NotificationPopover = observer(() => {
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-4 overflow-y-auto p-5">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
<NotificationsLoader />
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Spinner } from "@plane/ui";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker";
|
||||
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export const WorkspaceDashboardView = observer(() => {
|
||||
// theme
|
||||
@@ -78,20 +79,18 @@ export const WorkspaceDashboardView = observer(() => {
|
||||
) : (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title="Overview of your projects, activity, and metrics"
|
||||
description=" Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this
|
||||
page will transform into a space that helps you progress. Admins will also see items which help their team
|
||||
progress."
|
||||
title={WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].title}
|
||||
description={WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].description}
|
||||
primaryButton={{
|
||||
text: "Build your first project",
|
||||
text: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Dashboard empty state");
|
||||
toggleCreateProjectModal(true);
|
||||
},
|
||||
}}
|
||||
comicBox={{
|
||||
title: "Everything starts with a project in Plane",
|
||||
description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.",
|
||||
title: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.title,
|
||||
description: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.description,
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PagesListItem } from "./list-item";
|
||||
import { Loader } from "@plane/ui";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { PAGE_EMPTY_STATE_DETAILS } from "constants/page";
|
||||
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
type IPagesListView = {
|
||||
pageIds: string[];
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Loader } from "@plane/ui";
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export const RecentPagesList: FC = observer(() => {
|
||||
// theme
|
||||
@@ -63,11 +64,11 @@ export const RecentPagesList: FC = observer(() => {
|
||||
) : (
|
||||
<>
|
||||
<EmptyState
|
||||
title="Write a note, a doc, or a full knowledge base"
|
||||
description="Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely! Pages will be sorted and grouped by last updated."
|
||||
title={PAGE_EMPTY_STATE_DETAILS["Recent"].title}
|
||||
description={PAGE_EMPTY_STATE_DETAILS["Recent"].description}
|
||||
image={EmptyStateImagePath}
|
||||
primaryButton={{
|
||||
text: "Create new page",
|
||||
text: PAGE_EMPTY_STATE_DETAILS["Recent"].primaryButton.text,
|
||||
onClick: () => commandPaletteStore.toggleCreatePageModal(true),
|
||||
}}
|
||||
size="sm"
|
||||
|
||||
@@ -7,14 +7,14 @@ import { useTheme } from "next-themes";
|
||||
import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root";
|
||||
import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root";
|
||||
import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues";
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { KanbanLayoutLoader, ListLayoutLoader } from "components/ui";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// hooks
|
||||
import { useIssues, useUser } from "hooks/store";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { PROFILE_EMPTY_STATE_DETAILS } from "constants/profile";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
interface IProfileIssuesPage {
|
||||
type: "assigned" | "subscribed" | "created";
|
||||
@@ -57,38 +57,33 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
|
||||
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
if (!groupedIssueIds || loader === "init-loader")
|
||||
return <>{activeLayout === "list" ? <ListLayoutLoader /> : <KanbanLayoutLoader />}</>;
|
||||
|
||||
if (groupedIssueIds.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title={PROFILE_EMPTY_STATE_DETAILS[type].title}
|
||||
description={PROFILE_EMPTY_STATE_DETAILS[type].description}
|
||||
size="sm"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{loader === "init-loader" ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{groupedIssueIds ? (
|
||||
<>
|
||||
<ProfileIssuesAppliedFiltersRoot />
|
||||
<div className="-z-1 relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ProfileIssuesListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ProfileIssuesKanBanLayout />
|
||||
) : null}
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title={PROFILE_EMPTY_STATE_DETAILS[type].title}
|
||||
description={PROFILE_EMPTY_STATE_DETAILS[type].description}
|
||||
size="sm"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<ProfileIssuesAppliedFiltersRoot />
|
||||
<div className="-z-1 relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ProfileIssuesListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ProfileIssuesKanBanLayout />
|
||||
) : null}
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,10 +4,11 @@ import { useTheme } from "next-themes";
|
||||
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
||||
// components
|
||||
import { ProjectCard } from "components/project";
|
||||
import { Loader } from "@plane/ui";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { ProjectsLoader } from "components/ui";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export const ProjectCardList = observer(() => {
|
||||
// theme
|
||||
@@ -26,17 +27,7 @@ export const ProjectCardList = observer(() => {
|
||||
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
if (!workspaceProjectIds)
|
||||
return (
|
||||
<Loader className="grid grid-cols-3 gap-4">
|
||||
<Loader.Item height="100px" />
|
||||
<Loader.Item height="100px" />
|
||||
<Loader.Item height="100px" />
|
||||
<Loader.Item height="100px" />
|
||||
<Loader.Item height="100px" />
|
||||
<Loader.Item height="100px" />
|
||||
</Loader>
|
||||
);
|
||||
if (!workspaceProjectIds) return <ProjectsLoader />;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -59,18 +50,18 @@ export const ProjectCardList = observer(() => {
|
||||
) : (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title="Start a Project"
|
||||
description="Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal."
|
||||
title={WORKSPACE_EMPTY_STATE_DETAILS["projects"].title}
|
||||
description={WORKSPACE_EMPTY_STATE_DETAILS["projects"].description}
|
||||
primaryButton={{
|
||||
text: "Start your first project",
|
||||
text: WORKSPACE_EMPTY_STATE_DETAILS["projects"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Project empty state");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
},
|
||||
}}
|
||||
comicBox={{
|
||||
title: "Everything starts with a project in Plane",
|
||||
description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.",
|
||||
title: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.title,
|
||||
description: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.description,
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
|
||||
@@ -6,7 +6,8 @@ import { useEventTracker, useMember } from "hooks/store";
|
||||
// components
|
||||
import { ProjectMemberListItem, SendProjectInvitationModal } from "components/project";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { Button } from "@plane/ui";
|
||||
import { MembersSettingsLoader } from "components/ui";
|
||||
|
||||
export const ProjectMemberList: React.FC = observer(() => {
|
||||
// states
|
||||
@@ -56,12 +57,7 @@ export const ProjectMemberList: React.FC = observer(() => {
|
||||
</Button>
|
||||
</div>
|
||||
{!projectMemberIds ? (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
<MembersSettingsLoader />
|
||||
) : (
|
||||
<div className="divide-y divide-custom-border-100">
|
||||
{projectMemberIds.length > 0
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from "./markdown-to-component";
|
||||
export * from "./integration-and-import-export-banner";
|
||||
export * from "./range-datepicker";
|
||||
export * from "./profile-empty-state";
|
||||
export * from "./loader";
|
||||
|
||||
38
web/components/ui/loader/cycle-module-board-loader.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
export const CycleModuleBoardLayout = () => (
|
||||
<div className="h-full w-full animate-pulse">
|
||||
<div className="flex h-full w-full justify-between">
|
||||
<div className="grid h-full w-full grid-cols-1 gap-6 overflow-y-auto p-8 lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4 auto-rows-max transition-all">
|
||||
{[...Array(5)].map(() => (
|
||||
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="h-6 w-24 bg-custom-background-80 rounded" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-6 w-20 bg-custom-background-80 rounded" />
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded-full" />
|
||||
</div>
|
||||
<span className="h-1.5 bg-custom-background-80 rounded" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="h-4 w-16 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-4 w-4 bg-custom-background-80 rounded" />
|
||||
<span className="h-4 w-4 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
28
web/components/ui/loader/cycle-module-list-loader.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
export const CycleModuleListLayout = () => (
|
||||
<div className="h-full overflow-y-auto animate-pulse">
|
||||
<div className="flex h-full w-full justify-between">
|
||||
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||
{[...Array(5)].map(() => (
|
||||
<div className="flex w-full items-center justify-between gap-5 border-b border-custom-border-100 flex-col sm:flex-row px-5 py-6">
|
||||
<div className="relative flex w-full items-center gap-3 justify-between overflow-hidden">
|
||||
<div className="relative w-full flex items-center gap-3 overflow-hidden">
|
||||
<div className="flex items-center gap-4 truncate">
|
||||
<span className="h-10 w-10 bg-custom-background-80 rounded-full" />
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="h-6 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex w-full sm:w-auto relative overflow-hidden items-center gap-2.5 justify-between sm:justify-end sm:flex-shrink-0 ">
|
||||
<div className="flex-shrink-0 relative flex items-center gap-3">
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
9
web/components/ui/loader/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from "./layouts";
|
||||
export * from "./settings";
|
||||
export * from "./pages-loader";
|
||||
export * from "./notification-loader";
|
||||
export * from "./cycle-module-board-loader";
|
||||
export * from "./cycle-module-list-loader";
|
||||
export * from "./view-list-loader";
|
||||
export * from "./projects-loader";
|
||||
export * from "./utils";
|
||||
48
web/components/ui/loader/layouts/calendar-layout-loader.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { getRandomInt } from "../utils";
|
||||
|
||||
const CalendarDay = () => {
|
||||
const dataCount = getRandomInt(0, 1);
|
||||
const dataBlocks = Array.from({ length: dataCount }, (_, index) => (
|
||||
<span key={index} className="h-8 w-full bg-custom-background-80 rounded mb-2" />
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col min-h-[9rem]">
|
||||
<div className="flex items-center justify-end p-2 w-full">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2.5 p-2">{dataBlocks}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CalendarLayoutLoader = () => (
|
||||
<div className="h-full w-full overflow-y-auto bg-custom-background-100 pt-4 animate-pulse">
|
||||
<div className="mb-4 flex items-center justify-between gap-2 px-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-7 w-10 bg-custom-background-80 rounded" />
|
||||
<span className="h-7 w-32 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-7 w-12 bg-custom-background-80 rounded" />
|
||||
<span className="h-7 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="relative grid divide-x-[0.5px] divide-custom-border-200 text-sm font-medium grid-cols-5">
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<span key={index} className="h-11 w-full bg-custom-background-80" />
|
||||
))}
|
||||
</span>
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
<div className="grid h-full w-full grid-cols-1 divide-y-[0.5px] divide-custom-border-200 overflow-y-auto">
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<div key={index} className="grid divide-x-[0.5px] divide-custom-border-200 grid-cols-5">
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<CalendarDay key={index} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
50
web/components/ui/loader/layouts/gantt-layout-loader.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { getRandomLength } from "../utils";
|
||||
|
||||
export const GanttLayoutLoader = () => (
|
||||
<div className="flex flex-col h-full overflow-x-auto animate-pulse">
|
||||
<div className="min-h-10 w-full border-b border-custom-border-200 ">
|
||||
<span className="h-6 w-12 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex h-full">
|
||||
<div className="h-full w-[25.5rem] border-r border-custom-border-200">
|
||||
<div className="flex items-end h-[3.75rem] py-2 px-4 border-b border-custom-border-200">
|
||||
<div className="flex items-center pl-6 justify-between w-full">
|
||||
<span className="h-5 w-14 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 h-11 p-4 w-full">
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<div key={index} className="flex items-center gap-3 h-11 pl-6 w-full">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
<span className={`h-6 w-${getRandomLength(["32", "52", "72"])} bg-custom-background-80 rounded`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full border-r border-custom-border-200">
|
||||
<div className="flex flex-col justify-between gap-2 h-[3.75rem] py-1.5 px-4 border-b border-custom-border-200">
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3 justify-between w-full">
|
||||
{[...Array(15)].map((_, index) => (
|
||||
<span key={index} className="h-5 w-10 bg-custom-background-80 rounded" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 h-11 p-4 w-full">
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center gap-3 h-11 w-full`}
|
||||
style={{ paddingLeft: getRandomLength(["115px", "208px", "260px"]) }}
|
||||
>
|
||||
<span className={`h-6 w-40 w-${getRandomLength(["32", "52", "72"])} bg-custom-background-80 rounded`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
5
web/components/ui/loader/layouts/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./list-layout-loader";
|
||||
export * from "./kanban-layout-loader";
|
||||
export * from "./calendar-layout-loader";
|
||||
export * from "./spreadsheet-layout-loader";
|
||||
export * from "./gantt-layout-loader";
|
||||
18
web/components/ui/loader/layouts/kanban-layout-loader.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export const KanbanLayoutLoader = ({ cardsInEachColumn = [2, 3, 2, 4, 3] }: { cardsInEachColumn?: number[] }) => (
|
||||
<div className="flex gap-5 px-3.5 py-1.5 overflow-x-auto">
|
||||
{cardsInEachColumn.map((cardsInColumn, columnIndex) => (
|
||||
<div key={columnIndex} className="flex flex-col gap-3 animate-pulse">
|
||||
<div className="flex items-center justify-between h-9 w-80">
|
||||
<div className="flex item-center gap-1.5">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
<span className="h-6 w-24 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
{Array.from({ length: cardsInColumn }, (_, cardIndex) => (
|
||||
<span key={cardIndex} className="h-28 w-80 bg-custom-background-80 rounded" />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
45
web/components/ui/loader/layouts/list-layout-loader.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { getRandomInt, getRandomLength } from "../utils";
|
||||
|
||||
const ListItemRow = () => (
|
||||
<div className="flex items-center justify-between h-11 p-3 border-b border-custom-border-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="h-5 w-10 bg-custom-background-80 rounded" />
|
||||
<span className={`h-5 w-${getRandomLength(["32", "52", "72"])} bg-custom-background-80 rounded`} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<>
|
||||
{getRandomInt(1, 2) % 2 === 0 ? (
|
||||
<span key={index} className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
) : (
|
||||
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ListSection = ({ itemCount }: { itemCount: number }) => (
|
||||
<div className="flex flex-shrink-0 flex-col">
|
||||
<div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 py-1">
|
||||
<div className="flex items-center gap-2 py-1.5 w-full">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
<span className="h-6 w-24 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-full w-full">
|
||||
{[...Array(itemCount)].map((_, index) => (
|
||||
<ListItemRow key={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ListLayoutLoader = () => (
|
||||
<div className="flex flex-shrink-0 flex-col animate-pulse">
|
||||
{[6, 5, 2].map((itemCount, index) => (
|
||||
<ListSection key={index} itemCount={itemCount} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getRandomLength } from "../utils";
|
||||
|
||||
export const SpreadsheetLayoutLoader = () => (
|
||||
<div className="horizontal-scroll-enable h-full w-full animate-pulse">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="h-11 min-w-[28rem] bg-custom-background-90 border-r border-custom-border-100" />
|
||||
{[...Array(10)].map((_, index) => (
|
||||
<th
|
||||
key={index}
|
||||
className="h-11 w-full min-w-[8rem] bg-custom-background-90 border-r border-custom-border-100"
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...Array(16)].map((_, rowIndex) => (
|
||||
<tr key={rowIndex} className="border-b border-custom-border-100">
|
||||
<td className="h-11 min-w-[28rem] border-r border-custom-border-100">
|
||||
<div className="flex items-center gap-3 px-3">
|
||||
<span className="h-5 w-10 bg-custom-background-80 rounded" />
|
||||
<span className={`h-5 w-${getRandomLength(["32", "52", "72"])} bg-custom-background-80 rounded`} />
|
||||
</div>
|
||||
</td>
|
||||
{[...Array(10)].map((_, colIndex) => (
|
||||
<td key={colIndex} className="h-11 w-full min-w-[8rem] border-r border-custom-border-100">
|
||||
<div className="flex items-center justify-center gap-3 px-3">
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
16
web/components/ui/loader/notification-loader.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export const NotificationsLoader = () => (
|
||||
<div className="divide-y divide-custom-border-100 animate-pulse overflow-hidden">
|
||||
{[...Array(3)].map(() => (
|
||||
<div className="flex w-full items-center gap-4 p-3">
|
||||
<span className="min-h-12 min-w-12 bg-custom-background-80 rounded-full" />
|
||||
<div className="flex flex-col gap-2.5 w-full">
|
||||
<span className="h-5 w-36 bg-custom-background-80 rounded" />
|
||||
<div className="flex items-center justify-between gap-2 w-full">
|
||||
<span className="h-5 w-28 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
28
web/components/ui/loader/pages-loader.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
export const PagesLoader = () => (
|
||||
<div className="flex h-full flex-col space-y-5 overflow-hidden p-6">
|
||||
<div className="flex justify-between gap-4">
|
||||
<h3 className="text-2xl font-semibold text-custom-text-100">Pages</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{[...Array(5)].map(() => (
|
||||
<span className="h-8 w-20 bg-custom-background-80 rounded-full" />
|
||||
))}
|
||||
</div>
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{[...Array(5)].map(() => (
|
||||
<div className="h-12 w-full flex items-center justify-between px-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
34
web/components/ui/loader/projects-loader.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
export const ProjectsLoader = () => (
|
||||
<div className="h-full w-full overflow-y-auto p-8 animate-pulse">
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[...Array(3)].map(() => (
|
||||
<div className="flex cursor-pointer flex-col rounded border border-custom-border-200 bg-custom-background-100">
|
||||
<div className="relative min-h-[118px] w-full rounded-t border-b border-custom-border-200 ">
|
||||
<div className="absolute inset-0 z-[1] bg-gradient-to-t from-black/20 to-transparent">
|
||||
<div className="absolute bottom-4 z-10 flex h-10 w-full items-center justify-between gap-3 px-4">
|
||||
<div className="flex flex-grow items-center gap-2.5 truncate">
|
||||
<span className="min-h-9 min-w-9 bg-custom-background-80 rounded" />
|
||||
<div className="flex w-full flex-col justify-between gap-0.5 truncate">
|
||||
<span className="h-4 w-28 bg-custom-background-80 rounded" />
|
||||
<span className="h-4 w-16 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full flex-shrink-0 items-center gap-2">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-[104px] w-full flex-col justify-between rounded-b p-4">
|
||||
<span className="h-4 w-36 bg-custom-background-80 rounded" />
|
||||
<div className="item-center flex justify-between">
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
12
web/components/ui/loader/settings/activity.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { getRandomLength } from "../utils";
|
||||
|
||||
export const ActivitySettingsLoader = () => (
|
||||
<div className="flex flex-col gap-3 animate-pulse">
|
||||
{[...Array(10)].map(() => (
|
||||
<div className="relative flex items-center gap-2 h-12 border-b border-custom-border-200">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
<span className={`h-6 w-${getRandomLength(["52", "72", "96"])} bg-custom-background-80 rounded`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
19
web/components/ui/loader/settings/api-token.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
export const APITokenSettingsLoader = () => (
|
||||
<section className="w-full overflow-y-auto py-8 pr-9">
|
||||
<div className="mb-2 flex items-center justify-between border-b border-custom-border-200 py-3.5">
|
||||
<h3 className="text-xl font-medium">API tokens</h3>
|
||||
<span className="h-8 w-28 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="divide-y-[0.5px] divide-custom-border-200">
|
||||
{[...Array(2)].map(() => (
|
||||
<div className="flex flex-col gap-2 px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-5 w-28 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<span className="h-5 w-36 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
27
web/components/ui/loader/settings/email.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
export const EmailSettingsLoader = () => (
|
||||
<div className="mx-auto mt-8 h-full w-full overflow-y-auto px-6 lg:px-20 pb- animate-pulse">
|
||||
<div className="flex flex-col gap-2 pt-6 mb-2 pb-6 border-b border-custom-border-100">
|
||||
<span className="h-7 w-40 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-96 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center py-3">
|
||||
<span className="h-7 w-32 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
{[...Array(4)].map(() => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-2 py-3">
|
||||
<span className="h-6 w-28 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-96 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center py-12">
|
||||
<span className="h-8 w-32 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
18
web/components/ui/loader/settings/import-and-export.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export const ImportExportSettingsLoader = () => (
|
||||
<div className="divide-y-[0.5px] divide-custom-border-200 animate-pulse">
|
||||
{[...Array(2)].map(() => (
|
||||
<div className="flex items-center justify-between gap-2 px-4 py-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-4 w-28 bg-custom-background-80 rounded" />
|
||||
<span className="h-4 w-28 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
7
web/components/ui/loader/settings/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./activity";
|
||||
export * from "./api-token";
|
||||
export * from "./email";
|
||||
export * from "./integration";
|
||||
export * from "./members";
|
||||
export * from "./web-hook";
|
||||
export * from "./import-and-export";
|
||||
16
web/components/ui/loader/settings/integration.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export const IntegrationsSettingsLoader = () => (
|
||||
<div className="divide-y-[0.5px] divide-custom-border-100 animate-pulse">
|
||||
{[...Array(2)].map(() => (
|
||||
<div className="flex items-center justify-between gap-2 border-b border-custom-border-100 bg-custom-background-100 px-4 py-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="h-10 w-10 bg-custom-background-80 rounded-full" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
<span className="h-4 w-60 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="h-8 w-16 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
16
web/components/ui/loader/settings/members.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export const MembersSettingsLoader = () => (
|
||||
<div className="divide-y-[0.5px] divide-custom-border-100 animate-pulse">
|
||||
{[...Array(4)].map(() => (
|
||||
<div className="group flex items-center justify-between px-3 py-4">
|
||||
<div className="flex items-center gap-x-4 gap-y-2">
|
||||
<span className="h-10 w-10 bg-custom-background-80 rounded-full" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||
<span className="h-4 w-36 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="h-6 w-16 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
20
web/components/ui/loader/settings/web-hook.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
export const WebhookSettingsLoader = () => (
|
||||
<div className="h-full w-full overflow-hidden py-8 pr-9">
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
|
||||
<div className="text-xl font-medium">Webhooks</div>
|
||||
<span className="h-8 w-28 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
<div className="border-b border-custom-border-200">
|
||||
<div>
|
||||
<span className="flex items-center justify-between gap-4 px-3.5 py-[18px]">
|
||||
<span className="h-5 w-36 bg-custom-background-80 rounded" />
|
||||
<span className="h-6 w-12 bg-custom-background-80 rounded" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
35
web/components/ui/loader/utils.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
CalendarLayoutLoader,
|
||||
GanttLayoutLoader,
|
||||
KanbanLayoutLoader,
|
||||
ListLayoutLoader,
|
||||
SpreadsheetLayoutLoader,
|
||||
} from "./layouts";
|
||||
|
||||
export const getRandomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
|
||||
export const getRandomLength = (lengthArray: string[]) => {
|
||||
const randomIndex = Math.floor(Math.random() * lengthArray.length);
|
||||
return `${lengthArray[randomIndex]}`;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
layout: string;
|
||||
}
|
||||
export const ActiveLoader: React.FC<Props> = (props) => {
|
||||
const { layout } = props;
|
||||
switch (layout) {
|
||||
case "list":
|
||||
return <ListLayoutLoader />;
|
||||
case "kanban":
|
||||
return <KanbanLayoutLoader />;
|
||||
case "spreadsheet":
|
||||
return <SpreadsheetLayoutLoader />;
|
||||
case "calendar":
|
||||
return <CalendarLayoutLoader />;
|
||||
case "gantt_chart":
|
||||
return <GanttLayoutLoader />;
|
||||
default:
|
||||
return <KanbanLayoutLoader />;
|
||||
}
|
||||
};
|
||||
18
web/components/ui/loader/view-list-loader.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export const ViewListLoader = () => (
|
||||
<div className="flex h-full w-full flex-col animate-pulse">
|
||||
{[...Array(8)].map(() => (
|
||||
<div className="group border-b border-custom-border-200">
|
||||
<div className="relative flex w-full items-center justify-between rounded p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="min-h-10 min-w-10 bg-custom-background-80 rounded" />
|
||||
<span className="h-6 w-28 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-5 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -8,9 +8,11 @@ import { useApplication, useProjectView, useUser } from "hooks/store";
|
||||
import { ProjectViewListItem } from "components/views";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Input, Loader, Spinner } from "@plane/ui";
|
||||
import { Input } from "@plane/ui";
|
||||
import { ViewListLoader } from "components/ui";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { VIEW_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export const ProjectViewsList = observer(() => {
|
||||
// states
|
||||
@@ -27,22 +29,7 @@ export const ProjectViewsList = observer(() => {
|
||||
} = useUser();
|
||||
const { projectViewIds, getViewById, loader } = useProjectView();
|
||||
|
||||
if (loader)
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!projectViewIds)
|
||||
return (
|
||||
<Loader className="space-y-4 p-4">
|
||||
<Loader.Item height="72px" />
|
||||
<Loader.Item height="72px" />
|
||||
<Loader.Item height="72px" />
|
||||
<Loader.Item height="72px" />
|
||||
</Loader>
|
||||
);
|
||||
if (loader || !projectViewIds) return <ViewListLoader />;
|
||||
|
||||
const viewsList = projectViewIds.map((viewId) => getViewById(viewId));
|
||||
|
||||
@@ -77,15 +64,15 @@ export const ProjectViewsList = observer(() => {
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="Save filtered views for your project. Create as many as you need"
|
||||
description="Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best."
|
||||
title={VIEW_EMPTY_STATE_DETAILS["project-views"].title}
|
||||
description={VIEW_EMPTY_STATE_DETAILS["project-views"].description}
|
||||
image={EmptyStateImagePath}
|
||||
comicBox={{
|
||||
title: "Views work atop Issue properties.",
|
||||
description: "You can create a view from here with as many properties as filters as you see fit.",
|
||||
title: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.title,
|
||||
description: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.description,
|
||||
}}
|
||||
primaryButton={{
|
||||
text: "Create your first view",
|
||||
text: VIEW_EMPTY_STATE_DETAILS["project-views"].primaryButton.text,
|
||||
onClick: () => toggleCreateViewModal(true),
|
||||
}}
|
||||
size="lg"
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useMember } from "hooks/store";
|
||||
// components
|
||||
import { WorkspaceInvitationsListItem, WorkspaceMembersListItem } from "components/workspace";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
import { MembersSettingsLoader } from "components/ui";
|
||||
|
||||
export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer((props) => {
|
||||
const { searchQuery } = props;
|
||||
@@ -30,15 +30,7 @@ export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer((props
|
||||
workspaceSlug ? () => fetchWorkspaceMemberInvitations(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
if (!workspaceMemberIds && !workspaceMemberInvitationIds)
|
||||
return (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
);
|
||||
if (!workspaceMemberIds && !workspaceMemberInvitationIds) return <MembersSettingsLoader />;
|
||||
|
||||
// derived values
|
||||
const searchedMemberIds = getSearchedWorkspaceMemberIds(searchQuery);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useGlobalView } from "hooks/store";
|
||||
// components
|
||||
import { GlobalViewListItem } from "components/workspace";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
import { ViewListLoader } from "components/ui";
|
||||
|
||||
type Props = {
|
||||
searchQuery: string;
|
||||
@@ -25,15 +25,7 @@ export const GlobalViewsList: React.FC<Props> = observer((props) => {
|
||||
workspaceSlug ? () => fetchAllGlobalViews(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
if (!currentWorkspaceViews)
|
||||
return (
|
||||
<Loader className="space-y-4 p-4">
|
||||
<Loader.Item height="72px" />
|
||||
<Loader.Item height="72px" />
|
||||
<Loader.Item height="72px" />
|
||||
<Loader.Item height="72px" />
|
||||
</Loader>
|
||||
);
|
||||
if (!currentWorkspaceViews) return <ViewListLoader />;
|
||||
|
||||
const filteredViewsList = getSearchedViews(searchQuery);
|
||||
|
||||
|
||||
@@ -163,30 +163,6 @@ 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.",
|
||||
},
|
||||
};
|
||||
|
||||
// ee
|
||||
export const WORKSPACE_ACTIVE_CYCLE_STATE_GROUPS_DETAILS = [
|
||||
{
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
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.",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import { FC, useRef } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import {
|
||||
@@ -12,7 +12,7 @@ import { ProjectSidebarList } from "components/project";
|
||||
import { useApplication } from "hooks/store";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
|
||||
export interface IAppSidebar { }
|
||||
export interface IAppSidebar {}
|
||||
|
||||
export const AppSidebar: FC<IAppSidebar> = observer(() => {
|
||||
// store hooks
|
||||
@@ -20,35 +20,19 @@ export const AppSidebar: FC<IAppSidebar> = observer(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClickDetector(ref, () => {
|
||||
if (themStore.sidebarCollapsed === false) {
|
||||
if (themStore.mobileSidebarCollapsed === false) {
|
||||
if (window.innerWidth < 768) {
|
||||
themStore.toggleSidebar();
|
||||
themStore.toggleMobileSidebar();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth <= 768) {
|
||||
themStore.toggleSidebar(true);
|
||||
}
|
||||
if (window.innerWidth > 768) {
|
||||
themStore.toggleSidebar(false);
|
||||
}
|
||||
};
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [themStore]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300
|
||||
fixed md:relative
|
||||
${themStore.sidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
sm:${themStore.sidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
${themStore.mobileSidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
sm:${themStore.mobileSidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
md:ml-0 ${themStore.sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
||||
lg:ml-0 ${themStore.sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
||||
`}
|
||||
|
||||
@@ -16,10 +16,11 @@ import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
// type
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
import { useRouter } from "next/router";
|
||||
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
const AnalyticsPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter()
|
||||
const { analytics_tab } = router.query
|
||||
const router = useRouter();
|
||||
const { analytics_tab } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
@@ -41,18 +42,21 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
|
||||
<>
|
||||
{workspaceProjectIds && workspaceProjectIds.length > 0 ? (
|
||||
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
|
||||
<Tab.Group as={Fragment} defaultIndex={analytics_tab === 'custom' ? 1 : 0}>
|
||||
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
|
||||
<Tab.List as="div" className="flex space-x-2 border-b border-custom-border-200 px-0 md:px-5 py-0 md:py-3">
|
||||
{ANALYTICS_TABS.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`rounded-0 w-full md:w-max md:rounded-3xl border-b md:border border-custom-border-200 focus:outline-none px-0 md:px-4 py-2 text-xs hover:bg-custom-background-80 ${selected ? "border-custom-primary-100 text-custom-primary-100 md:bg-custom-background-80 md:text-custom-text-200 md:border-custom-border-200" : "border-transparent"
|
||||
`rounded-0 w-full md:w-max md:rounded-3xl border-b md:border border-custom-border-200 focus:outline-none px-0 md:px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
||||
selected
|
||||
? "border-custom-primary-100 text-custom-primary-100 md:bg-custom-background-80 md:text-custom-text-200 md:border-custom-border-200"
|
||||
: "border-transparent"
|
||||
}`
|
||||
}
|
||||
onClick={() => {
|
||||
router.query.analytics_tab = tab.key
|
||||
router.push(router)
|
||||
router.query.analytics_tab = tab.key;
|
||||
router.push(router);
|
||||
}}
|
||||
>
|
||||
{tab.title}
|
||||
@@ -72,19 +76,18 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
|
||||
) : (
|
||||
<EmptyState
|
||||
image={EmptyStateImagePath}
|
||||
title="Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster"
|
||||
description="See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time."
|
||||
title={WORKSPACE_EMPTY_STATE_DETAILS["analytics"].title}
|
||||
description={WORKSPACE_EMPTY_STATE_DETAILS["analytics"].description}
|
||||
primaryButton={{
|
||||
text: "Create Cycles and Modules first",
|
||||
text: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Analytics empty state");
|
||||
toggleCreateProjectModal(true);
|
||||
},
|
||||
}}
|
||||
comicBox={{
|
||||
title: "Analytics works best with Cycles + Modules",
|
||||
description:
|
||||
"First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.",
|
||||
title: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.title,
|
||||
description: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.description,
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
|
||||
@@ -13,13 +13,15 @@ import { CyclesHeader } from "components/headers";
|
||||
import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Spinner, Tooltip } from "@plane/ui";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui";
|
||||
// types
|
||||
import { TCycleView, TCycleLayout } from "@plane/types";
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||
const [createModal, setCreateModal] = useState(false);
|
||||
@@ -65,9 +67,11 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
if (loader)
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
<>
|
||||
{cycleLayout === "list" && <CycleModuleListLayout />}
|
||||
{cycleLayout === "board" && <CycleModuleBoardLayout />}
|
||||
{cycleLayout === "gantt" && <GanttLayoutLoader />}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -81,16 +85,15 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||
{totalCycles === 0 ? (
|
||||
<div className="h-full place-items-center">
|
||||
<EmptyState
|
||||
title="Group and timebox your work in Cycles."
|
||||
description="Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team."
|
||||
title={CYCLE_EMPTY_STATE_DETAILS["cycles"].title}
|
||||
description={CYCLE_EMPTY_STATE_DETAILS["cycles"].description}
|
||||
image={EmptyStateImagePath}
|
||||
comicBox={{
|
||||
title: "Cycles are repetitive time-boxes.",
|
||||
description:
|
||||
"A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.",
|
||||
title: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.title,
|
||||
description: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.description,
|
||||
}}
|
||||
primaryButton={{
|
||||
text: "Set your first cycle",
|
||||
text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Cycle empty state");
|
||||
setCreateModal(true);
|
||||
@@ -114,7 +117,8 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`border-b-2 p-4 text-sm font-medium outline-none ${selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
|
||||
`border-b-2 p-4 text-sm font-medium outline-none ${
|
||||
selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
|
||||
}`
|
||||
}
|
||||
>
|
||||
@@ -132,14 +136,16 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
||||
<button
|
||||
type="button"
|
||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
||||
}`}
|
||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
||||
cycleLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
||||
}`}
|
||||
onClick={() => handleCurrentLayout(layout.key as TCycleLayout)}
|
||||
>
|
||||
<layout.icon
|
||||
strokeWidth={2}
|
||||
className={`h-3.5 w-3.5 ${cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
className={`h-3.5 w-3.5 ${
|
||||
cycleLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -16,13 +16,14 @@ import { AppLayout } from "layouts/app-layout";
|
||||
import { RecentPagesList, CreateUpdatePageModal } from "components/pages";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { PagesHeader } from "components/headers";
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { PagesLoader } from "components/ui";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { PAGE_TABS_LIST } from "constants/page";
|
||||
import { useProjectPages } from "hooks/store/use-project-page";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), {
|
||||
ssr: false,
|
||||
@@ -124,12 +125,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
</Tab.List>
|
||||
);
|
||||
|
||||
if (loader || archivedPageLoader)
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
if (loader || archivedPageLoader) return <PagesLoader />;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -217,19 +213,18 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
) : (
|
||||
<EmptyState
|
||||
image={EmptyStateImagePath}
|
||||
title="Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started"
|
||||
description="Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button."
|
||||
title={PAGE_EMPTY_STATE_DETAILS["pages"].title}
|
||||
description={PAGE_EMPTY_STATE_DETAILS["pages"].description}
|
||||
primaryButton={{
|
||||
text: "Create your first page",
|
||||
text: PAGE_EMPTY_STATE_DETAILS["pages"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Pages empty state");
|
||||
toggleCreatePageModal(true);
|
||||
},
|
||||
}}
|
||||
comicBox={{
|
||||
title: "A page can be a doc or a doc of docs.",
|
||||
description:
|
||||
"We wrote Nikhil and Meera’s love story. You could write your project’s mission, goals, and eventual vision.",
|
||||
title: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.title,
|
||||
description: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.description,
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
|
||||
@@ -21,7 +21,7 @@ const EstimatesSettingsPage: NextPageWithLayout = observer(() => {
|
||||
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||
|
||||
return (
|
||||
<div className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "pointer-events-none opacity-60"}`}>
|
||||
<div className={`h-full w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "pointer-events-none opacity-60"}`}>
|
||||
<EstimatesList />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||
@@ -10,16 +13,15 @@ import { ProjectService } from "services/project";
|
||||
// components
|
||||
import { IntegrationCard } from "components/project";
|
||||
import { ProjectSettingHeader } from "components/headers";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { EmptyState } from "components/common";
|
||||
import { Loader } from "@plane/ui";
|
||||
// images
|
||||
import emptyIntegration from "public/empty-state/integration.svg";
|
||||
import { IntegrationsSettingsLoader } from "components/ui";
|
||||
// types
|
||||
import { IProject } from "@plane/types";
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// fetch-keys
|
||||
import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
|
||||
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
// services
|
||||
const integrationService = new IntegrationService();
|
||||
@@ -28,6 +30,10 @@ const projectService = new ProjectService();
|
||||
const ProjectIntegrationsPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
|
||||
const { data: projectDetails } = useSWR<IProject>(
|
||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||
@@ -39,10 +45,14 @@ const ProjectIntegrationsPage: NextPageWithLayout = () => {
|
||||
() => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null)
|
||||
);
|
||||
|
||||
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode);
|
||||
|
||||
const isAdmin = projectDetails?.member_role === 20;
|
||||
|
||||
return (
|
||||
<div className={`w-full gap-10 overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className={`h-full w-full gap-10 overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Integrations</h3>
|
||||
</div>
|
||||
@@ -54,26 +64,22 @@ const ProjectIntegrationsPage: NextPageWithLayout = () => {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full py-8">
|
||||
<div className="h-full w-full py-8">
|
||||
<EmptyState
|
||||
title="You haven't configured integrations"
|
||||
description="Configure GitHub and other integrations to sync your project issues."
|
||||
image={emptyIntegration}
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
primaryButton={{
|
||||
text: "Configure now",
|
||||
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
<IntegrationsSettingsLoader />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ProjectSettingHeader } from "components/headers";
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
const LabelsSettingsPage: NextPageWithLayout = () => (
|
||||
<div className="w-full gap-10 overflow-y-auto py-8 pr-9">
|
||||
<div className="h-full w-full gap-10 overflow-y-auto py-8 pr-9">
|
||||
<ProjectSettingsLabelList />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
import { useTheme } from "next-themes";
|
||||
// store hooks
|
||||
import { useUser } from "hooks/store";
|
||||
// layouts
|
||||
@@ -9,9 +10,11 @@ import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
// component
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { ApiTokenEmptyState, ApiTokenListItem, CreateApiTokenModal } from "components/api-token";
|
||||
import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Button, Spinner } from "@plane/ui";
|
||||
import { Button } from "@plane/ui";
|
||||
import { APITokenSettingsLoader } from "components/ui";
|
||||
// services
|
||||
import { APITokenService } from "services/api_token.service";
|
||||
// types
|
||||
@@ -19,6 +22,7 @@ import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { API_TOKENS_LIST } from "constants/fetch-keys";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
const apiTokenService = new APITokenService();
|
||||
|
||||
@@ -28,9 +32,12 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
|
||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
@@ -39,6 +46,10 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
||||
workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["api-tokens"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "api-tokens", isLightMode);
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
@@ -46,36 +57,47 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!tokens) {
|
||||
return <APITokenSettingsLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
|
||||
{tokens ? (
|
||||
<section className="w-full overflow-y-auto py-8 pr-9">
|
||||
{tokens.length > 0 ? (
|
||||
<>
|
||||
<div className="mb-2 flex items-center justify-between border-b border-custom-border-200 py-3.5">
|
||||
<h3 className="text-xl font-medium">API tokens</h3>
|
||||
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
|
||||
Add API token
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
{tokens.map((token) => (
|
||||
<ApiTokenListItem key={token.id} token={token} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mx-auto">
|
||||
<ApiTokenEmptyState onClick={() => setIsCreateTokenModalOpen(true)} />
|
||||
<section className="h-full w-full overflow-y-auto py-8 pr-9">
|
||||
{tokens.length > 0 ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between border-b border-custom-border-200 py-3.5">
|
||||
<h3 className="text-xl font-medium">API tokens</h3>
|
||||
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
|
||||
Add API token
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center p-4">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{tokens.map((token) => (
|
||||
<ApiTokenListItem key={token.id} token={token} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
|
||||
<h3 className="text-xl font-medium">API tokens</h3>
|
||||
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
|
||||
Add API token
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,8 +13,7 @@ import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
import { SingleIntegrationCard } from "components/integration";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
// ui
|
||||
import { IntegrationAndImportExportBanner } from "components/ui";
|
||||
import { Loader } from "@plane/ui";
|
||||
import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "components/ui";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// fetch-keys
|
||||
@@ -53,10 +52,7 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => {
|
||||
{appIntegrations ? (
|
||||
appIntegrations.map((integration) => <SingleIntegrationCard key={integration.id} integration={integration} />)
|
||||
) : (
|
||||
<Loader className="mt-4 space-y-2.5">
|
||||
<Loader.Item height="89px" />
|
||||
<Loader.Item height="89px" />
|
||||
</Loader>
|
||||
<IntegrationsSettingsLoader />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useUser, useWebhook, useWorkspace } from "hooks/store";
|
||||
// layouts
|
||||
@@ -9,11 +10,15 @@ import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
// components
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { WebhooksList, WebhooksEmptyState, CreateWebhookModal } from "components/web-hooks";
|
||||
import { WebhooksList, CreateWebhookModal } from "components/web-hooks";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { Button, Spinner } from "@plane/ui";
|
||||
import { Button } from "@plane/ui";
|
||||
import { WebhookSettingsLoader } from "components/ui";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||
// states
|
||||
@@ -21,9 +26,12 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// mobx store
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@@ -35,6 +43,11 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||
workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["webhooks"];
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode);
|
||||
|
||||
// clear secret key when modal is closed.
|
||||
useEffect(() => {
|
||||
if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey();
|
||||
@@ -47,12 +60,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!webhooks)
|
||||
return (
|
||||
<div className="grid h-full w-full place-items-center p-4">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
if (!webhooks) return <WebhookSettingsLoader />;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-hidden py-8 pr-9">
|
||||
@@ -76,8 +84,21 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||
<WebhooksList />
|
||||
</div>
|
||||
) : (
|
||||
<div className="mx-auto">
|
||||
<WebhooksEmptyState onClick={() => setShowCreateWebhookModal(true)} />
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
|
||||
<div className="text-xl font-medium">Webhooks</div>
|
||||
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
|
||||
Add webhook
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { RichReadOnlyEditor } from "@plane/rich-text-editor";
|
||||
// icons
|
||||
import { History, MessageSquare } from "lucide-react";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
import { ActivitySettingsLoader } from "components/ui";
|
||||
// fetch-keys
|
||||
import { USER_ACTIVITY } from "constants/fetch-keys";
|
||||
// helper
|
||||
@@ -31,7 +31,6 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
|
||||
const { currentUser } = useUser();
|
||||
|
||||
return (
|
||||
|
||||
<section className="mx-auto mt-5 md:mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5">
|
||||
<div className="flex items-center border-b border-custom-border-100 gap-4 pb-3.5">
|
||||
<SidebarHamburgerToggle />
|
||||
@@ -97,12 +96,12 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
const message =
|
||||
activityItem.verb === "created" &&
|
||||
activityItem.field !== "cycles" &&
|
||||
activityItem.field !== "modules" &&
|
||||
activityItem.field !== "attachment" &&
|
||||
activityItem.field !== "link" &&
|
||||
activityItem.field !== "estimate" &&
|
||||
!activityItem.field ? (
|
||||
activityItem.field !== "cycles" &&
|
||||
activityItem.field !== "modules" &&
|
||||
activityItem.field !== "attachment" &&
|
||||
activityItem.field !== "link" &&
|
||||
activityItem.field !== "estimate" &&
|
||||
!activityItem.field ? (
|
||||
<span>
|
||||
created <IssueLink activity={activityItem} />
|
||||
</span>
|
||||
@@ -182,15 +181,9 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
<ActivitySettingsLoader />
|
||||
)}
|
||||
</section>
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import useSWR from "swr";
|
||||
// layouts
|
||||
import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
import { EmailSettingsLoader } from "components/ui";
|
||||
// components
|
||||
import { EmailNotificationForm } from "components/profile/preferences";
|
||||
// services
|
||||
@@ -20,18 +20,8 @@ const ProfilePreferencesThemePage: NextPageWithLayout = () => {
|
||||
userService.currentUserEmailNotificationSettings()
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Loader className="space-y-4 mt-8 px-6 lg:px-20">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
if (!data || isLoading) {
|
||||
return <EmailSettingsLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
|
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 |