Merge pull request #3666 from makeplane/preview

release: v0.15.2-dev
This commit is contained in:
sriram veeraghanta 2024-02-14 19:41:55 +05:30 committed by GitHub
commit 1e27e37b51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
278 changed files with 2072 additions and 1137 deletions

View File

@ -247,12 +247,7 @@ class IssueSearchEndpoint(BaseAPIView):
if parent == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(
~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True
).exclude(
pk__in=Issue.issue_objects.filter(
parent__isnull=False
).values_list("parent_id", flat=True)
)
~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id))
if issue_relation == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(

View File

@ -137,7 +137,7 @@ services:
dockerfile: Dockerfile.dev
args:
DOCKER_BUILDKIT: 1
restart: no
restart: "no"
networks:
- dev_env
volumes:

View File

@ -1,5 +1,11 @@
import { EUserProjectRoles } from "constants/project";
import type { IUser, IUserLite, IWorkspace, IWorkspaceLite, TStateGroups } from ".";
import type {
IUser,
IUserLite,
IWorkspace,
IWorkspaceLite,
TStateGroups,
} from ".";
export interface IProject {
archive_in: number;
@ -117,7 +123,7 @@ export type TProjectIssuesSearchParams = {
parent?: boolean;
issue_relation?: boolean;
cycle?: boolean;
module?: string[];
module?: string;
sub_issue?: boolean;
issue_id?: string;
workspace_search: boolean;

View File

@ -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 (
<>

View File

@ -3,12 +3,20 @@ import { Menu } from "lucide-react";
import { useApplication } from "hooks/store";
import { observer } from "mobx-react";
export const SidebarHamburgerToggle: FC = observer(() => {
const { theme: themStore } = useApplication();
type Props = {
onClick?: () => void;
}
export const SidebarHamburgerToggle: FC<Props> = observer((props) => {
const { onClick } = props
const { theme: themeStore } = useApplication();
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={() => {
if (onClick) onClick()
else themeStore.toggleMobileSidebar()
}}
>
<Menu size={14} className="text-custom-text-200 group-hover:text-custom-text-100 transition-all" />
</div>

View File

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

View File

@ -1,6 +1,7 @@
import { FC, MouseEvent, useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { observer } from "mobx-react";
// hooks
import { useEventTracker, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
@ -26,7 +27,7 @@ export interface ICyclesBoardCard {
cycleId: string;
}
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
const { cycleId, workspaceSlug, projectId } = props;
// states
const [updateModal, setUpdateModal] = useState(false);
@ -69,8 +70,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
? cycleTotalIssues === 0
? "0 Issue"
: cycleTotalIssues === cycleDetails.completed_issues
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
: `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues`
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
: `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues`
: "0 Issue";
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
@ -295,4 +296,4 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
</Link>
</div>
);
};
});

View File

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

View File

@ -1,6 +1,7 @@
import { FC, MouseEvent, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { observer } from "mobx-react";
// hooks
import { useEventTracker, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
@ -30,7 +31,7 @@ type TCyclesListItem = {
projectId: string;
};
export const CyclesListItem: FC<TCyclesListItem> = (props) => {
export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
const { cycleId, workspaceSlug, projectId } = props;
// states
const [updateModal, setUpdateModal] = useState(false);
@ -289,4 +290,4 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
</Link>
</>
);
};
});

View File

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

View File

@ -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} />}
</>
);
});

View File

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

View File

@ -3,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>

View File

@ -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) => {

View File

@ -33,6 +33,7 @@ type Props = {
sidebarToRender: (props: any) => React.ReactNode;
title: string;
updateCurrentViewRenderPayload: (direction: "left" | "right", currentView: TGanttViews) => void;
quickAdd?: React.JSX.Element | undefined;
};
export const GanttChartMainContent: React.FC<Props> = (props) => {
@ -52,6 +53,7 @@ export const GanttChartMainContent: React.FC<Props> = (props) => {
sidebarToRender,
title,
updateCurrentViewRenderPayload,
quickAdd,
} = props;
// chart hook
const { currentView, currentViewData, updateScrollLeft } = useChart();
@ -101,6 +103,7 @@ export const GanttChartMainContent: React.FC<Props> = (props) => {
enableReorder={enableReorder}
sidebarToRender={sidebarToRender}
title={title}
quickAdd={quickAdd}
/>
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
<ActiveChartView />

View File

@ -31,6 +31,7 @@ type ChartViewRootProps = {
enableAddBlock: boolean;
bottomSpacing: boolean;
showAllBlocks: boolean;
quickAdd?: React.JSX.Element | undefined;
};
export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
@ -49,6 +50,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
enableAddBlock,
bottomSpacing,
showAllBlocks,
quickAdd,
} = props;
// states
const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
@ -200,6 +202,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
sidebarToRender={sidebarToRender}
title={title}
updateCurrentViewRenderPayload={updateCurrentViewRenderPayload}
quickAdd={quickAdd}
/>
</div>
);

View File

@ -12,6 +12,7 @@ type GanttChartRootProps = {
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blockToRender: (data: any) => React.ReactNode;
sidebarToRender: (props: any) => React.ReactNode;
quickAdd?: React.JSX.Element | undefined;
enableBlockLeftResize?: boolean;
enableBlockRightResize?: boolean;
enableBlockMove?: boolean;
@ -37,6 +38,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
enableAddBlock = false,
bottomSpacing = false,
showAllBlocks = false,
quickAdd,
} = props;
return (
@ -56,6 +58,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
enableAddBlock={enableAddBlock}
bottomSpacing={bottomSpacing}
showAllBlocks={showAllBlocks}
quickAdd={quickAdd}
/>
</ChartContextProvider>
);

View File

@ -7,42 +7,23 @@ import { useIssueDetail } from "hooks/store";
// ui
import { Loader } from "@plane/ui";
// components
import { GanttQuickAddIssueForm, IssueGanttSidebarBlock } from "components/issues";
import { IssueGanttSidebarBlock } from "components/issues";
// helpers
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
import { TIssue } from "@plane/types";
import { BLOCK_HEIGHT } from "../constants";
type Props = {
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null;
enableReorder: boolean;
enableQuickIssueCreate?: boolean;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: TIssue,
viewId?: string
) => Promise<TIssue | undefined>;
viewId?: string;
disableIssueCreation?: boolean;
showAllBlocks?: boolean;
};
export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
const {
blockUpdateHandler,
blocks,
enableReorder,
enableQuickIssueCreate,
quickAddCallback,
viewId,
disableIssueCreation,
showAllBlocks = false,
} = props;
export const IssueGanttSidebar: React.FC<Props> = observer((props: Props) => {
const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props;
const { activeBlock, dispatch } = useChart();
const { peekIssue } = useIssueDetail();
@ -187,9 +168,6 @@ export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
)}
</Droppable>
</DragDropContext>
{enableQuickIssueCreate && !disableIssueCreation && (
<GanttQuickAddIssueForm quickAddCallback={quickAddCallback} viewId={viewId} />
)}
</>
);
});

View File

@ -9,16 +9,17 @@ type Props = {
enableReorder: boolean;
sidebarToRender: (props: any) => React.ReactNode;
title: string;
quickAdd?: React.JSX.Element | undefined;
};
export const GanttChartSidebar: React.FC<Props> = (props) => {
const { blocks, blockUpdateHandler, enableReorder, sidebarToRender, title } = props;
const { blocks, blockUpdateHandler, enableReorder, sidebarToRender, title, quickAdd } = props;
return (
<div
// DO NOT REMOVE THE ID
id="gantt-sidebar"
className="sticky top-0 left-0 z-10 min-h-full h-max flex-shrink-0 border-r-[0.5px] border-custom-border-200 bg-custom-background-100"
className="sticky left-0 z-10 min-h-full h-max flex-shrink-0 border-r-[0.5px] border-custom-border-200 bg-custom-background-100"
style={{
width: `${SIDEBAR_WIDTH}px`,
}}
@ -33,9 +34,10 @@ export const GanttChartSidebar: React.FC<Props> = (props) => {
<h6>Duration</h6>
</div>
<div className="min-h-full h-max bg-custom-background-100">
<div className="min-h-full h-max bg-custom-background-100 overflow-x-hidden overflow-y-auto">
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
</div>
{quickAdd ? quickAdd : null}
</div>
);
};

View File

@ -4,6 +4,7 @@ import Image from "next/image";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks
import { useUser } from "hooks/store";
import useUserAuth from "hooks/use-user-auth";
@ -11,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>

View File

@ -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>

View File

@ -13,7 +13,6 @@ import { TIssueOperations } from "./issue-detail";
import { FileService } from "services/file.service";
import { useMention, useWorkspace } from "hooks/store";
import { observer } from "mobx-react";
import { isNil } from "lodash";
export interface IssueDescriptionFormValues {
name: string;
@ -79,13 +78,13 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = observer((props) => {
}, [issue.id]); // TODO: verify the exhaustive-deps warning
useEffect(() => {
if (issue.description_html) {
if (["", undefined, null].includes(localIssueDescription.description_html)) {
setLocalIssueDescription((state) => {
if (!isNil(state.description_html)) return state;
return { id: issue.id, description_html: issue.description_html };
if (!["", undefined, null].includes(state.description_html)) return state;
return { id: issue.id, description_html: issue.description_html || "<p></p>" };
});
}
}, [issue.description_html]);
}, [issue.description_html, localIssueDescription.description_html, issue.id]);
const handleDescriptionFormSubmit = useCallback(
async (formData: Partial<TIssue>) => {
@ -177,7 +176,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = observer((props) => {
</div>
<span>{errors.name ? errors.name.message : null}</span>
<div className="relative">
{issue.description_html ? (
{localIssueDescription.description_html ? (
<Controller
name="description_html"
control={control}

View File

@ -6,7 +6,7 @@ import { IssueParentSiblings } from "./siblings";
// ui
import { CustomMenu } from "@plane/ui";
// hooks
import { useIssueDetail, useIssues, useProject, useProjectState } from "hooks/store";
import { useIssues, useProject, useProjectState } from "hooks/store";
// types
import { TIssueOperations } from "../root";
import { TIssue } from "@plane/types";
@ -23,7 +23,6 @@ export const IssueParentDetail: FC<TIssueParentDetail> = (props) => {
const { workspaceSlug, projectId, issueId, issue, issueOperations } = props;
// hooks
const { issueMap } = useIssues();
const { peekIssue } = useIssueDetail();
const { getProjectById } = useProject();
const { getProjectStates } = useProjectState();
@ -39,7 +38,7 @@ export const IssueParentDetail: FC<TIssueParentDetail> = (props) => {
return (
<>
<div className="mb-5 flex w-min items-center gap-3 whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-80 px-2.5 py-1 text-xs">
<Link href={`/${peekIssue?.workspaceSlug}/projects/${parentIssue?.project_id}/issues/${parentIssue.id}`}>
<Link href={`/${workspaceSlug}/projects/${parentIssue?.project_id}/issues/${parentIssue.id}`}>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2.5">
<span className="block h-2 w-2 rounded-full" style={{ backgroundColor: stateColor }} />

View File

@ -15,7 +15,7 @@ import {
CalendarCheck2,
} from "lucide-react";
// hooks
import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "hooks/store";
import { useEstimate, useIssueDetail, useProject, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import {
@ -56,11 +56,9 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, issueOperations, is_archived, is_editable } = props;
// router
const router = useRouter();
const { inboxIssueId } = router.query;
// store hooks
const { getProjectById } = useProject();
const { currentUser } = useUser();
const { projectStates } = useProjectState();
const { areEstimatesEnabledForCurrentProject } = useEstimate();
const { setToastAlert } = useToast();
const {
@ -91,8 +89,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
maxDate?.setDate(maxDate.getDate());
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
return (
<>
{workspaceSlug && projectId && issue && (
@ -108,22 +104,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
)}
<div className="flex h-full w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
<div className="flex items-center justify-between px-5 pb-3">
<div className="flex items-center gap-x-2">
{currentIssueState ? (
<StateGroupIcon
className="h-4 w-4"
stateGroup={currentIssueState.group}
color={currentIssueState.color}
/>
) : inboxIssueId ? (
<StateGroupIcon className="h-4 w-4" stateGroup="backlog" color="#ff7700" />
) : null}
<h4 className="text-lg font-medium text-custom-text-300">
{projectDetails?.identifier}-{issue?.sequence_id}
</h4>
</div>
<div className="flex items-center justify-end px-5 pb-3">
<div className="flex flex-wrap items-center gap-2">
{currentUser && !is_archived && (
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
@ -187,8 +168,9 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"}
className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm justify-between ${issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"
}`}
buttonClassName={`text-sm justify-between ${
issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"
}`}
hideIcon={issue.assignee_ids?.length === 0}
dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
@ -232,8 +214,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
// TODO: add this logic
// showPlaceholderIcon
// TODO: add this logic
// showPlaceholderIcon
/>
</div>
@ -258,8 +240,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
// TODO: add this logic
// showPlaceholderIcon
// TODO: add this logic
// showPlaceholderIcon
/>
</div>

View File

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

View File

@ -1,32 +1,46 @@
import { useState } from "react";
import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react";
import { useTheme } from "next-themes";
// hooks
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";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// 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, EMPTY_FILTER_STATE_DETAILS } from "constants/empty-state";
type Props = {
workspaceSlug: string | undefined;
projectId: string | undefined;
cycleId: string | undefined;
activeLayout: TIssueLayouts | undefined;
handleClearAllFilters: () => void;
isEmptyFilters?: boolean;
};
interface EmptyStateProps {
title: string;
image: string;
description?: string;
comicBox?: { title: string; description: string };
primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
size?: "lg" | "sm" | undefined;
disabled?: boolean | undefined;
}
export const CycleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, cycleId } = props;
const { workspaceSlug, projectId, cycleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props;
// states
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { issues } = useIssues(EIssuesStoreType.CYCLE);
const { updateIssue, fetchIssue } = useIssueDetail();
@ -36,6 +50,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
const { setTrackElement } = useEventTracker();
const {
membership: { currentProjectRole: userRole },
currentUser,
} = useUser();
const { setToastAlert } = useToast();
@ -60,8 +75,44 @@ 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 currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
const emptyStateImage = getEmptyStateImagePath("cycle-issues", activeLayout ?? "list", isLightMode);
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
const emptyStateProps: EmptyStateProps = isEmptyFilters
? {
title: EMPTY_FILTER_STATE_DETAILS["project"].title,
image: currentLayoutEmptyStateImagePath,
secondaryButton: {
text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text,
onClick: handleClearAllFilters,
},
}
: {
title: emptyStateDetail.title,
description: emptyStateDetail.description,
image: emptyStateImage,
primaryButton: {
text: emptyStateDetail.primaryButton.text,
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("Cycle issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
},
},
secondaryButton: {
text: emptyStateDetail.secondaryButton.text,
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => setCycleIssuesListModal(true),
},
size: "sm",
disabled: !isEditingAllowed,
};
return (
<>
<ExistingIssuesListModal
@ -73,30 +124,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
handleOnSubmit={handleAddIssuesToCycle}
/>
<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}
primaryButton={{
text: "New issue",
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>
}
disabled={!isEditingAllowed}
/>
<EmptyState {...emptyStateProps} />
</div>
</>
);

View File

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

View File

@ -1,41 +1,55 @@
import { useState } from "react";
import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react";
import { useTheme } from "next-themes";
// hooks
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";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// types
import { ISearchIssueResponse } from "@plane/types";
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
// constants
import { EUserProjectRoles } from "constants/project";
import { EIssuesStoreType } from "constants/issue";
import { EMPTY_FILTER_STATE_DETAILS, MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state";
type Props = {
workspaceSlug: string | undefined;
projectId: string | undefined;
moduleId: string | undefined;
activeLayout: TIssueLayouts | undefined;
handleClearAllFilters: () => void;
isEmptyFilters?: boolean;
};
interface EmptyStateProps {
title: string;
image: string;
description?: string;
comicBox?: { title: string; description: string };
primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
size?: "lg" | "sm" | undefined;
disabled?: boolean | undefined;
}
export const ModuleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, moduleId } = props;
const { workspaceSlug, projectId, moduleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = 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,8 +69,43 @@ 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 currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
const emptyStateImage = getEmptyStateImagePath("module-issues", activeLayout ?? "list", isLightMode);
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
const emptyStateProps: EmptyStateProps = isEmptyFilters
? {
title: EMPTY_FILTER_STATE_DETAILS["project"].title,
image: currentLayoutEmptyStateImagePath,
secondaryButton: {
text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text,
onClick: handleClearAllFilters,
},
}
: {
title: emptyStateDetail.title,
description: emptyStateDetail.description,
image: emptyStateImage,
primaryButton: {
text: emptyStateDetail.primaryButton.text,
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("Module issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
},
},
secondaryButton: {
text: emptyStateDetail.secondaryButton.text,
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => setModuleIssuesListModal(true),
},
disabled: !isEditingAllowed,
};
return (
<>
<ExistingIssuesListModal
@ -64,34 +113,11 @@ 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}
primaryButton={{
text: "New issue",
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>
}
disabled={!isEditingAllowed}
/>
<EmptyState {...emptyStateProps} />
</div>
</>
);

View File

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

View File

@ -36,22 +36,34 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
if (!workspaceSlug || !projectId || !cycleId) return;
if (!value) {
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, {
[key]: null,
});
updateFilters(
workspaceSlug,
projectId,
EIssueFilterType.FILTERS,
{
[key]: null,
},
cycleId
);
return;
}
let newValues = issueFilters?.filters?.[key] ?? [];
newValues = newValues.filter((val) => val !== value);
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, {
[key]: newValues,
});
updateFilters(
workspaceSlug,
projectId,
EIssueFilterType.FILTERS,
{
[key]: newValues,
},
cycleId
);
};
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId) return;
if (!workspaceSlug || !projectId || !cycleId) return;
const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null;

View File

@ -33,24 +33,36 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
});
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
if (!workspaceSlug || !projectId) return;
if (!workspaceSlug || !projectId || !moduleId) return;
if (!value) {
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, {
[key]: null,
});
updateFilters(
workspaceSlug,
projectId,
EIssueFilterType.FILTERS,
{
[key]: null,
},
moduleId
);
return;
}
let newValues = issueFilters?.filters?.[key] ?? [];
newValues = newValues.filter((val) => val !== value);
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, {
[key]: newValues,
});
updateFilters(
workspaceSlug,
projectId,
EIssueFilterType.FILTERS,
{
[key]: newValues,
},
moduleId
);
};
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId) return;
if (!workspaceSlug || !projectId || !moduleId) return;
const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null;

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
// hooks
import { useIssues, useUser } from "hooks/store";
// components
import { IssueGanttBlock } from "components/issues";
import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues";
import {
GanttChartRoot,
IBlockUpdateData,
@ -70,21 +70,17 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null}
blockUpdateHandler={updateIssueBlockStructure}
blockToRender={(data: TIssue) => <IssueGanttBlock issueId={data.id} />}
sidebarToRender={(props) => (
<IssueGanttSidebar
{...props}
quickAddCallback={issueStore.quickAddIssue}
viewId={viewId}
enableQuickIssueCreate
disableIssueCreation={!enableIssueCreation || !isAllowed}
showAllBlocks
/>
)}
sidebarToRender={(props) => <IssueGanttSidebar {...props} showAllBlocks />}
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
enableAddBlock={isAllowed}
quickAdd={
enableIssueCreation && isAllowed ? (
<GanttQuickAddIssueForm quickAddCallback={issueStore.quickAddIssue} viewId={viewId} />
) : undefined
}
showAllBlocks
/>
</div>

View File

@ -159,7 +159,7 @@ export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observe
) : (
<button
type="button"
className="sticky bottom-0 z-[1] flex w-full cursor-pointer items-center gap-2 p-3 py-3 text-custom-primary-100 bg-custom-background-100"
className="sticky bottom-0 z-[1] flex w-full cursor-pointer items-center gap-2 p-3 py-3 text-custom-primary-100 bg-custom-background-100 border-custom-border-200 border-t-[1px]"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />

View File

@ -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}

View File

@ -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;

View File

@ -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) {

View File

@ -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;

View File

@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from "react";
import React, { Fragment, useCallback, useMemo } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
@ -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,66 +177,62 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) {
return <SpreadsheetLayoutLoader />;
}
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",
<div className="relative h-full w-full overflow-auto">
<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.toggleCreateProjectModal(true);
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
}
}
disabled={!isEditingAllowed}
: undefined
: {
text: "Start your first project",
onClick: () => {
setTrackElement("All issues empty state");
commandPaletteStore.toggleCreateProjectModal(true);
},
}
}
disabled={!isEditingAllowed}
/>
) : (
<Fragment>
<SpreadsheetView
displayProperties={issueFilters?.displayProperties ?? {}}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issueIds={issueIds}
quickActions={renderQuickActions}
handleIssues={handleIssues}
canEditProperties={canEditProperties}
viewId={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 />
{/* peek overview */}
<IssuePeekOverview />
</Fragment>
)}
</div>
</div>
);
});

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { Fragment } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
@ -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,30 +36,26 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
}
);
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return <ListLayoutLoader />;
}
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 />
{issues?.groupedIssueIds?.length === 0 ? (
<div className="relative h-full w-full overflow-y-auto">
<ProjectArchivedEmptyState />
</div>
) : (
<>
{!issues?.groupedIssueIds ? (
<ProjectArchivedEmptyState />
) : (
<>
<div className="relative h-full w-full overflow-auto">
<ArchivedIssueListLayout />
</div>
{/* peek overview */}
<IssuePeekOverview is_archived />
</>
)}
</>
<Fragment>
<div className="relative h-full w-full overflow-auto">
<ArchivedIssueListLayout />
</div>
<IssuePeekOverview is_archived />
</Fragment>
)}
</div>
);

View File

@ -1,7 +1,8 @@
import React, { useState } from "react";
import React, { Fragment, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import size from "lodash/size";
// hooks
import { useCycle, useIssues } from "hooks/store";
// components
@ -16,10 +17,11 @@ import {
IssuePeekOverview,
} from "components/issues";
import { TransferIssues, TransferIssuesModal } from "components/cycles";
// ui
import { Spinner } from "@plane/ui";
import { ActiveLoader } from "components/ui";
// constants
import { EIssuesStoreType } from "constants/issue";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
// types
import { IIssueFilterOptions } from "@plane/types";
export const CycleLayoutRoot: React.FC = observer(() => {
const router = useRouter();
@ -52,47 +54,73 @@ export const CycleLayoutRoot: React.FC = observer(() => {
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase() ?? "draft";
const userFilters = issuesFilter?.issueFilters?.filters;
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId || !cycleId) return;
const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null;
});
issuesFilter.updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{
...newFilters,
},
cycleId.toString()
);
};
if (!workspaceSlug || !projectId || !cycleId) return <></>;
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
}
return (
<>
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
<div className="relative flex h-full w-full flex-col overflow-hidden">
{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 />
{issues?.groupedIssueIds?.length === 0 ? (
<div className="relative h-full w-full overflow-y-auto">
<CycleEmptyState
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
cycleId={cycleId.toString()}
activeLayout={activeLayout}
handleClearAllFilters={handleClearAllFilters}
isEmptyFilters={issueFilterCount > 0}
/>
</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 />
</>
)}
</>
<Fragment>
<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 />
</Fragment>
)}
</div>
</>

View File

@ -9,8 +9,8 @@ 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";
// constants
import { EIssuesStoreType } from "constants/issue";
@ -39,30 +39,29 @@ 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} />}</>;
}
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 />
{issues?.groupedIssueIds?.length === 0 ? (
<div className="relative h-full w-full overflow-y-auto">
<ProjectDraftEmptyState />
</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>
);

View File

@ -1,7 +1,8 @@
import React from "react";
import React, { Fragment } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import size from "lodash/size";
// mobx store
import { useIssues } from "hooks/store";
// components
@ -15,10 +16,11 @@ import {
ModuleListLayout,
ModuleSpreadsheetLayout,
} from "components/issues";
// ui
import { Spinner } from "@plane/ui";
import { ActiveLoader } from "components/ui";
// constants
import { EIssuesStoreType } from "constants/issue";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
// types
import { IIssueFilterOptions } from "@plane/types";
export const ModuleLayoutRoot: React.FC = observer(() => {
// router
@ -44,45 +46,72 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
}
);
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
const userFilters = issuesFilter?.issueFilters?.filters;
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId || !moduleId) return;
const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null;
});
issuesFilter.updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{
...newFilters,
},
moduleId.toString()
);
};
if (!workspaceSlug || !projectId || !moduleId) return <></>;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
}
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 />
{issues?.groupedIssueIds?.length === 0 ? (
<div className="relative h-full w-full overflow-y-auto">
<ModuleEmptyState
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
moduleId={moduleId.toString()}
activeLayout={activeLayout}
handleClearAllFilters={handleClearAllFilters}
isEmptyFilters={issueFilterCount > 0}
/>
</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 />
</>
)}
</>
<Fragment>
<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 />
</Fragment>
)}
</div>
);

View File

@ -1,4 +1,4 @@
import { FC } from "react";
import { FC, Fragment } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
@ -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,48 +43,42 @@ 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} />}</>;
}
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 ? (
<ProjectEmptyState />
) : (
<>
{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>
)}
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectSpreadsheetLayout />
) : null}
<Fragment>
<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>
{/* peek overview */}
<IssuePeekOverview />
</>
)}
</>
{/* peek overview */}
<IssuePeekOverview />
</Fragment>
)}
</div>
);

View File

@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import React, { Fragment, useMemo } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
@ -15,7 +15,7 @@ import {
ProjectViewListLayout,
ProjectViewSpreadsheetLayout,
} from "components/issues";
import { Spinner } from "@plane/ui";
import { ActiveLoader } from "components/ui";
// constants
import { EIssuesStoreType } from "constants/issue";
// types
@ -63,39 +63,38 @@ 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} />}</>;
}
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 />
{issues?.groupedIssueIds?.length === 0 ? (
<div className="relative h-full w-full overflow-y-auto">
<ProjectViewEmptyState />
</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>
<Fragment>
<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 />
</Fragment>
)}
</div>
);

View File

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

View File

@ -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);

View File

@ -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>

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
@ -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";
@ -36,10 +36,14 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
currentUser,
} = useUser();
const {
issues: { loader, groupedIssueIds, fetchIssues },
issues: { loader, groupedIssueIds, fetchIssues, setViewId },
issuesFilter: { issueFilters, fetchFilters },
} = useIssues(EIssuesStoreType.PROFILE);
useEffect(() => {
setViewId(type);
}, [type]);
useSWR(
workspaceSlug && userId ? `CURRENT_WORKSPACE_PROFILE_ISSUES_${workspaceSlug}_${userId}_${type}` : null,
async () => {
@ -57,38 +61,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 />
</>
);
});

View File

@ -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 products roadmap, a marketing campaign, or launching a new car.",
title: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.title,
description: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.description,
}}
size="lg"
disabled={!isEditingAllowed}

View File

@ -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

View File

@ -18,7 +18,7 @@ import {
MoreHorizontal,
} from "lucide-react";
// hooks
import { useApplication,useEventTracker, useProject } from "hooks/store";
import { useApplication, useEventTracker, useProject } from "hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import useToast from "hooks/use-toast";
// helpers
@ -131,7 +131,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
const handleProjectClick = () => {
if (window.innerWidth < 768) {
themeStore.toggleSidebar();
themeStore.toggleMobileSidebar();
}
};
@ -147,9 +147,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
{({ open }) => (
<>
<div
className={`group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-10 hover:bg-custom-sidebar-background-80 ${
snapshot?.isDragging ? "opacity-60" : ""
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
className={`group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-10 hover:bg-custom-sidebar-background-80 ${snapshot?.isDragging ? "opacity-60" : ""
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
>
{provided && (
<Tooltip
@ -158,11 +157,9 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
>
<button
type="button"
className={`absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400 ${
isCollapsed ? "" : "group-hover:!flex"
} ${project.sort_order === null ? "cursor-not-allowed opacity-60" : ""} ${
isMenuActive ? "!flex" : ""
}`}
className={`absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400 ${isCollapsed ? "" : "group-hover:!flex"
} ${project.sort_order === null ? "cursor-not-allowed opacity-60" : ""} ${isMenuActive ? "!flex" : ""
}`}
{...provided?.dragHandleProps}
>
<MoreVertical className="h-3.5" />
@ -173,14 +170,12 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
<Tooltip tooltipContent={`${project.name}`} position="right" className="ml-2" disabled={!isCollapsed}>
<Disclosure.Button
as="div"
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${
isCollapsed ? "justify-center" : `justify-between`
}`}
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${isCollapsed ? "justify-center" : `justify-between`
}`}
>
<div
className={`flex w-full flex-grow items-center gap-x-2 truncate ${
isCollapsed ? "justify-center" : ""
}`}
className={`flex w-full flex-grow items-center gap-x-2 truncate ${isCollapsed ? "justify-center" : ""
}`}
>
{project.emoji ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
@ -200,9 +195,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
</div>
{!isCollapsed && (
<ChevronDown
className={`hidden h-4 w-4 flex-shrink-0 ${open ? "rotate-180" : ""} ${
isMenuActive ? "!block" : ""
} mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`}
className={`hidden h-4 w-4 flex-shrink-0 ${open ? "rotate-180" : ""} ${isMenuActive ? "!block" : ""
} mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`}
/>
)}
</Disclosure.Button>
@ -326,11 +320,10 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
disabled={!isCollapsed}
>
<div
className={`group flex items-center gap-2.5 rounded-md px-2 py-1.5 text-xs font-medium outline-none ${
router.asPath.includes(item.href)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${isCollapsed ? "justify-center" : ""}`}
className={`group flex items-center gap-2.5 rounded-md px-2 py-1.5 text-xs font-medium outline-none ${router.asPath.includes(item.href)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${isCollapsed ? "justify-center" : ""}`}
>
<item.Icon className="h-4 w-4 stroke-[1.5]" />
{!isCollapsed && item.name}

View File

@ -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";

View 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>
);

View 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>
);

View 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";

View 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>
);

View 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>
);

View 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";

View 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>
);

View 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>
);

View File

@ -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>
);

View 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>
);

View 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>
);

View 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>
);

View 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>
);

View 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>
);

View 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>
);

View 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>
);

View 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";

View 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>
);

View 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>
);

View 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>
);

View 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 />;
}
};

View 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>
);

View File

@ -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 everyones views and choose whichever suits their needs best."
title={VIEW_EMPTY_STATE_DETAILS["project-views"].title}
description={VIEW_EMPTY_STATE_DETAILS["project-views"].description}
image={EmptyStateImagePath}
comicBox={{
title: "Views work atop Issue properties.",
description: "You can create a view from here with as many properties as filters as you see fit.",
title: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.title,
description: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.description,
}}
primaryButton={{
text: "Create your first view",
text: VIEW_EMPTY_STATE_DETAILS["project-views"].primaryButton.text,
onClick: () => toggleCreateViewModal(true),
}}
size="lg"

View File

@ -10,6 +10,7 @@ import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-reac
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
// assets
import packageJson from "package.json";
import useSize from "hooks/use-window-size";
const helpOptions = [
{
@ -42,9 +43,11 @@ export interface WorkspaceHelpSectionProps {
export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(() => {
// store hooks
const {
theme: { sidebarCollapsed, toggleSidebar },
theme: { sidebarCollapsed, toggleSidebar, toggleMobileSidebar },
commandPalette: { toggleShortcutModal },
} = useApplication();
const [windowWidth] = useSize();
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
// refs
@ -57,9 +60,8 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
return (
<>
<div
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
isCollapsed ? "flex-col" : ""
}`}
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${isCollapsed ? "flex-col" : ""
}`}
>
{!isCollapsed && (
<div className="w-1/2 cursor-default rounded-md bg-green-500/10 px-2.5 py-1.5 text-center text-sm font-medium text-green-500 outline-none">
@ -70,9 +72,8 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
<Tooltip tooltipContent="Shortcuts">
<button
type="button"
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isCollapsed ? "w-full" : ""
}`}
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${isCollapsed ? "w-full" : ""
}`}
onClick={() => toggleShortcutModal(true)}
>
<Zap className="h-3.5 w-3.5" />
@ -81,9 +82,8 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
<Tooltip tooltipContent="Help">
<button
type="button"
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isCollapsed ? "w-full" : ""
}`}
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${isCollapsed ? "w-full" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
>
<HelpCircle className="h-3.5 w-3.5" />
@ -93,7 +93,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
<button
type="button"
className="grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:hidden"
onClick={() => toggleSidebar()}
onClick={() => windowWidth <= 768 ? toggleMobileSidebar() : toggleSidebar()}
>
<MoveLeft className="h-3.5 w-3.5" />
</button>
@ -101,10 +101,9 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
<Tooltip tooltipContent={`${isCollapsed ? "Expand" : "Hide"}`}>
<button
type="button"
className={`hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${
isCollapsed ? "w-full" : ""
}`}
onClick={() => toggleSidebar()}
className={`hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${isCollapsed ? "w-full" : ""
}`}
onClick={() => windowWidth <= 768 ? toggleMobileSidebar() : toggleSidebar()}
>
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isCollapsed ? "rotate-180" : ""}`} />
</button>
@ -122,9 +121,8 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
leaveTo="transform opacity-0 scale-95"
>
<div
className={`absolute bottom-2 min-w-[10rem] ${
isCollapsed ? "left-full" : "-left-[75px]"
} divide-y divide-custom-border-200 whitespace-nowrap rounded bg-custom-background-100 p-1 shadow-custom-shadow-xs`}
className={`absolute bottom-2 min-w-[10rem] ${isCollapsed ? "left-full" : "-left-[75px]"
} divide-y divide-custom-border-200 whitespace-nowrap rounded bg-custom-background-100 p-1 shadow-custom-shadow-xs`}
ref={helpOptionsRef}
>
<div className="space-y-1 pb-2">

View File

@ -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);

View File

@ -54,7 +54,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
const { workspaceSlug } = router.query;
// store hooks
const {
theme: { sidebarCollapsed, toggleSidebar },
theme: { sidebarCollapsed, toggleMobileSidebar },
} = useApplication();
const { setTrackElement } = useEventTracker();
const { currentUser, updateCurrentUser, isUserInstanceAdmin, signOut } = useUser();
@ -98,7 +98,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
};
const handleItemClick = () => {
if (window.innerWidth < 768) {
toggleSidebar();
toggleMobileSidebar();
}
};
const workspacesList = Object.values(workspaces ?? {});
@ -110,15 +110,13 @@ export const WorkspaceSidebarDropdown = observer(() => {
<>
<Menu.Button className="group/menu-button h-full w-full truncate rounded-md text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:outline-none">
<div
className={`flex items-center gap-x-2 truncate rounded p-1 ${
sidebarCollapsed ? "justify-center" : "justify-between"
}`}
className={`flex items-center gap-x-2 truncate rounded p-1 ${sidebarCollapsed ? "justify-center" : "justify-between"
}`}
>
<div className="flex items-center gap-2 truncate">
<div
className={`relative grid h-6 w-6 flex-shrink-0 place-items-center uppercase ${
!activeWorkspace?.logo && "rounded bg-custom-primary-500 text-white"
}`}
className={`relative grid h-6 w-6 flex-shrink-0 place-items-center uppercase ${!activeWorkspace?.logo && "rounded bg-custom-primary-500 text-white"
}`}
>
{activeWorkspace?.logo && activeWorkspace.logo !== "" ? (
<img
@ -138,9 +136,8 @@ export const WorkspaceSidebarDropdown = observer(() => {
</div>
{!sidebarCollapsed && (
<ChevronDown
className={`mx-1 hidden h-4 w-4 flex-shrink-0 group-hover/menu-button:block ${
open ? "rotate-180" : ""
} text-custom-sidebar-text-400 duration-300`}
className={`mx-1 hidden h-4 w-4 flex-shrink-0 group-hover/menu-button:block ${open ? "rotate-180" : ""
} text-custom-sidebar-text-400 duration-300`}
/>
)}
</div>
@ -179,9 +176,8 @@ export const WorkspaceSidebarDropdown = observer(() => {
>
<div className="flex items-center justify-start gap-2.5 truncate">
<span
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${
!workspace?.logo && "rounded bg-custom-primary-500 text-white"
}`}
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${!workspace?.logo && "rounded bg-custom-primary-500 text-white"
}`}
>
{workspace?.logo && workspace.logo !== "" ? (
<img
@ -194,9 +190,8 @@ export const WorkspaceSidebarDropdown = observer(() => {
)}
</span>
<h5
className={`truncate text-sm font-medium ${
workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
}`}
className={`truncate text-sm font-medium ${workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
}`}
>
{workspace.name}
</h5>

View File

@ -31,7 +31,7 @@ export const WorkspaceSidebarMenu = observer(() => {
const handleLinkClick = (itemKey: string) => {
if (window.innerWidth < 768) {
themeStore.toggleSidebar();
themeStore.toggleMobileSidebar();
}
captureEvent(SIDEBAR_CLICKED, {
destination: itemKey,
@ -52,11 +52,10 @@ export const WorkspaceSidebarMenu = observer(() => {
disabled={!themeStore?.sidebarCollapsed}
>
<div
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
link.highlight(router.asPath, `/${workspaceSlug}`)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${themeStore?.sidebarCollapsed ? "justify-center" : ""}`}
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${link.highlight(router.asPath, `/${workspaceSlug}`)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${themeStore?.sidebarCollapsed ? "justify-center" : ""}`}
>
{
<link.Icon

View File

@ -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);

View File

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

View File

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

View File

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

View File

@ -64,21 +64,3 @@ export const PROFILE_ADMINS_TAB = [
selected: "/[workspaceSlug]/profile/[userId]/subscribed",
},
];
export const PROFILE_EMPTY_STATE_DETAILS = {
assigned: {
key: "assigned",
title: "No issues are assigned to you",
description: "Issues assigned to you can be tracked from here.",
},
subscribed: {
key: "created",
title: "No issues yet",
description: "All issues created by you come here, track them here directly.",
},
created: {
key: "subscribed",
title: "No issues yet",
description: "Subscribe to issues you are interested in, track all of them here.",
},
};

View File

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

View File

@ -0,0 +1,19 @@
import { useEffect, useState } from "react";
const useSize = () => {
const [windowSize, setWindowSize] = useState([window.innerWidth, window.innerHeight]);
useEffect(() => {
const windowSizeHandler = () => {
setWindowSize([window.innerWidth, window.innerHeight]);
};
window.addEventListener("resize", windowSizeHandler);
return () => {
window.removeEventListener("resize", windowSizeHandler);
};
}, []);
return windowSize;
};
export default useSize;

View File

@ -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]"}
`}

View File

@ -7,6 +7,7 @@ import { CustomMenu } from "@plane/ui";
import { ChevronDown } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useApplication } from "hooks/store";
interface IProfilePreferenceSettingsLayout {
children: ReactNode;
@ -16,6 +17,7 @@ interface IProfilePreferenceSettingsLayout {
export const ProfilePreferenceSettingsLayout: FC<IProfilePreferenceSettingsLayout> = (props) => {
const { children, header } = props;
const router = useRouter();
const { theme: themeStore } = useApplication();
const showMenuItem = () => {
const item = router.asPath.split('/');
@ -42,7 +44,7 @@ export const ProfilePreferenceSettingsLayout: FC<IProfilePreferenceSettingsLayou
return (
<ProfileSettingsLayout header={
<div className="md:hidden flex flex-shrink-0 gap-4 items-center justify-start border-b border-custom-border-200 p-4">
<SidebarHamburgerToggle />
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm"

View File

@ -40,7 +40,7 @@ export const ProfileLayoutSidebar = observer(() => {
const { setToastAlert } = useToast();
// store hooks
const {
theme: { sidebarCollapsed, toggleSidebar },
theme: { sidebarCollapsed, toggleSidebar, toggleMobileSidebar },
} = useApplication();
const { currentUser, currentUserSettings, signOut } = useUser();
const { workspaces } = useWorkspace();
@ -78,7 +78,7 @@ export const ProfileLayoutSidebar = observer(() => {
const handleItemClick = () => {
if (window.innerWidth < 768) {
toggleSidebar();
toggleMobileSidebar();
}
};
@ -111,7 +111,7 @@ export const ProfileLayoutSidebar = observer(() => {
`}
>
<div ref={ref} className="flex h-full w-full flex-col gap-y-4">
<Link href={`/${redirectWorkspaceSlug}`}>
<Link href={`/${redirectWorkspaceSlug}`} onClick={handleItemClick}>
<div
className={`flex flex-shrink-0 items-center gap-2 truncate px-4 pt-4 ${sidebarCollapsed ? "justify-center" : ""
}`}

View File

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

View File

@ -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>

View File

@ -9,19 +9,21 @@ import { useTheme } from "next-themes";
import { useApplication, useEventTracker, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
import useUserAuth from "hooks/use-user-auth";
import useSize from "hooks/use-window-size";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { RecentPagesList, CreateUpdatePageModal } from "components/pages";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
import { PagesHeader } from "components/headers";
import { Spinner } from "@plane/ui";
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,
@ -66,6 +68,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
useProjectPages();
// hooks
const {} = useUserAuth({ user: currentUser, isLoading: currentUserLoader });
const [windowWidth] = useSize();
// local storage
const { storedValue: pageTab, setValue: setPageTab } = useLocalStorage("pageTab", "Recent");
// fetching pages from API
@ -103,7 +106,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const mobileTabList = (
const MobileTabList = () => (
<Tab.List as="div" className="flex items-center justify-between border-b border-custom-border-200 px-3 pt-3 mb-4">
<div className="flex flex-wrap items-center gap-4">
{PAGE_TABS_LIST.map((tab) => (
@ -122,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 (
<>
@ -166,25 +164,29 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
}
}}
>
<div className="md:hidden">{mobileTabList}</div>
<Tab.List as="div" className="mb-6 items-center justify-between hidden md:flex">
<div className="flex flex-wrap items-center gap-4">
{PAGE_TABS_LIST.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`rounded-full border px-5 py-1.5 text-sm outline-none ${
selected
? "border-custom-primary bg-custom-primary text-white"
: "border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90"
}`
}
>
{tab.title}
</Tab>
))}
</div>
</Tab.List>
{windowWidth < 768 ? (
<MobileTabList />
) : (
<Tab.List as="div" className="mb-6 items-center justify-between hidden md:flex">
<div className="flex flex-wrap items-center gap-4">
{PAGE_TABS_LIST.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`rounded-full border px-5 py-1.5 text-sm outline-none ${
selected
? "border-custom-primary bg-custom-primary text-white"
: "border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90"
}`
}
>
{tab.title}
</Tab>
))}
</div>
</Tab.List>
)}
<Tab.Panels as={Fragment}>
<Tab.Panel as="div" className="h-full space-y-5 overflow-y-auto">
<RecentPagesList />
@ -211,19 +213,18 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
) : (
<EmptyState
image={EmptyStateImagePath}
title="Write a note, a doc, or a full knowledge base. Get Galileo, Planes AI assistant, to help you get started"
description="Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your projects context. To make short work of any doc, invoke Galileo, Planes AI, with a shortcut or the click of a button."
title={PAGE_EMPTY_STATE_DETAILS["pages"].title}
description={PAGE_EMPTY_STATE_DETAILS["pages"].description}
primaryButton={{
text: "Create your first page",
text: PAGE_EMPTY_STATE_DETAILS["pages"].primaryButton.text,
onClick: () => {
setTrackElement("Pages empty state");
toggleCreatePageModal(true);
},
}}
comicBox={{
title: "A page can be a doc or a doc of docs.",
description:
"We wrote Nikhil and Meeras love story. You could write your projects mission, goals, and eventual vision.",
title: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.title,
description: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.description,
}}
size="lg"
disabled={!isEditingAllowed}

View File

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

View File

@ -1,6 +1,9 @@
import { ReactElement } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { useTheme } from "next-themes";
// hooks
import { useUser } from "hooks/store";
// layouts
import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout";
@ -10,16 +13,15 @@ import { ProjectService } from "services/project";
// components
import { IntegrationCard } from "components/project";
import { ProjectSettingHeader } from "components/headers";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { EmptyState } from "components/common";
import { Loader } from "@plane/ui";
// images
import emptyIntegration from "public/empty-state/integration.svg";
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>
);

View File

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

View File

@ -2,6 +2,7 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { useTheme } from "next-themes";
// store hooks
import { useUser } from "hooks/store";
// layouts
@ -9,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>
</>
);
});

View File

@ -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>

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