feat: loading states update (#3639)

* dev: implement layout skeleton loader and helper function

* chore: implemented layout loader

* chore: settings loader added

* chore: cycle, module, view, pages, notification and projects loader added

* chore: kanban loader improvement

* chore: loader utils updated
This commit is contained in:
Anmol Singh Bhatia 2024-02-13 19:12:10 +05:30 committed by GitHub
parent 83139989c2
commit b1989bae1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 951 additions and 507 deletions

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

@ -15,7 +15,8 @@ import { IntegrationService } from "services/integrations";
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
@ -158,12 +159,7 @@ const IntegrationGuide = observer(() => {
</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

@ -14,7 +14,8 @@ import { IntegrationService } from "services/integrations";
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
@ -153,12 +154,7 @@ const IntegrationGuide = observer(() => {
</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

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

@ -12,8 +12,7 @@ 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";
@ -178,64 +177,60 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) {
return <SpreadsheetLayoutLoader />;
}
if (issueIds.length === 0) {
return (
<EmptyState
image={emptyStateImage}
title={(workspaceProjectIds ?? []).length > 0 ? currentViewDetails.title : "No project"}
description={
(workspaceProjectIds ?? []).length > 0
? currentViewDetails.description
: "To create issues or manage your work, you need to create a project or be a part of one."
}
size="sm"
primaryButton={
(workspaceProjectIds ?? []).length > 0
? currentView !== "custom-view" && currentView !== "subscribed"
? {
text: "Create new issue",
onClick: () => {
setTrackElement("All issues empty state");
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
}
: undefined
: {
text: "Start your first project",
onClick: () => {
setTrackElement("All issues empty state");
commandPaletteStore.toggleCreateProjectModal(true);
},
}
}
disabled={!isEditingAllowed}
/>
);
}
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
{!globalViewId || globalViewId !== dataViewId || loader === "init-loader" || !issueIds ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} />
{(issueIds ?? {}).length == 0 ? (
<EmptyState
image={emptyStateImage}
title={(workspaceProjectIds ?? []).length > 0 ? currentViewDetails.title : "No project"}
description={
(workspaceProjectIds ?? []).length > 0
? currentViewDetails.description
: "To create issues or manage your work, you need to create a project or be a part of one."
}
size="sm"
primaryButton={
(workspaceProjectIds ?? []).length > 0
? currentView !== "custom-view" && currentView !== "subscribed"
? {
text: "Create new issue",
onClick: () => {
setTrackElement("All issues empty state");
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
}
: undefined
: {
text: "Start your first project",
onClick: () => {
setTrackElement("All issues empty state");
commandPaletteStore.toggleCreateProjectModal(true);
},
}
}
disabled={!isEditingAllowed}
/>
) : (
<div className="relative h-full w-full overflow-auto">
<SpreadsheetView
displayProperties={issueFilters?.displayProperties ?? {}}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issueIds={issueIds}
quickActions={renderQuickActions}
handleIssues={handleIssues}
canEditProperties={canEditProperties}
viewId={globalViewId}
/>
</div>
)}
</>
)}
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} />
<div className="relative h-full w-full overflow-auto">
<SpreadsheetView
displayProperties={issueFilters?.displayProperties ?? {}}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issueIds={issueIds}
quickActions={renderQuickActions}
handleIssues={handleIssues}
canEditProperties={canEditProperties}
viewId={globalViewId}
/>
</div>
{/* peek overview */}
<IssuePeekOverview />
</div>

View File

@ -13,7 +13,7 @@ import {
} from "components/issues";
import { EIssuesStoreType } from "constants/issue";
// ui
import { Spinner } from "@plane/ui";
import { ListLayoutLoader } from "components/ui";
export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
// router
@ -36,31 +36,26 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
}
);
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return <ListLayoutLoader />;
}
if (issues?.groupedIssueIds?.length === 0) {
return (
<div className="relative h-full w-full overflow-y-auto">
<ProjectArchivedEmptyState />
</div>
);
}
if (!workspaceSlug || !projectId) return <></>;
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
<ArchivedIssueAppliedFiltersRoot />
{issues?.loader === "init-loader" ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
{!issues?.groupedIssueIds ? (
<ProjectArchivedEmptyState />
) : (
<>
<div className="relative h-full w-full overflow-auto">
<ArchivedIssueListLayout />
</div>
{/* peek overview */}
<IssuePeekOverview is_archived />
</>
)}
</>
)}
<div className="relative h-full w-full overflow-auto">
<ArchivedIssueListLayout />
</div>
<IssuePeekOverview is_archived />
</div>
);
});

View File

@ -16,6 +16,7 @@ import {
IssuePeekOverview,
} from "components/issues";
import { TransferIssues, TransferIssuesModal } from "components/cycles";
import { ActiveLoader } from "components/ui";
// ui
import { Spinner } from "@plane/ui";
// constants
@ -53,6 +54,34 @@ export const CycleLayoutRoot: React.FC = observer(() => {
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase() ?? "draft";
if (!workspaceSlug || !projectId || !cycleId) return <></>;
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return (
<>
{activeLayout ? (
<ActiveLoader layout={activeLayout} />
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</>
);
}
if (issues?.groupedIssueIds?.length === 0) {
return (
<div className="relative h-full w-full overflow-y-auto">
<CycleEmptyState
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
cycleId={cycleId.toString()}
activeLayout={activeLayout}
/>
</div>
);
}
return (
<>
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
@ -61,40 +90,21 @@ export const CycleLayoutRoot: React.FC = observer(() => {
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
<CycleAppliedFiltersRoot />
{issues?.loader === "init-loader" || !issues?.groupedIssueIds ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
{issues?.groupedIssueIds?.length === 0 ? (
<CycleEmptyState
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
cycleId={cycleId.toString()}
activeLayout={activeLayout}
/>
) : (
<>
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
<CycleListLayout />
) : activeLayout === "kanban" ? (
<CycleKanBanLayout />
) : activeLayout === "calendar" ? (
<CycleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<CycleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<CycleSpreadsheetLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</>
)}
</>
)}
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
<CycleListLayout />
) : activeLayout === "kanban" ? (
<CycleKanBanLayout />
) : activeLayout === "calendar" ? (
<CycleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<CycleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<CycleSpreadsheetLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</div>
</>
);

View File

@ -9,6 +9,7 @@ import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/d
import { DraftIssueListLayout } from "../list/roots/draft-issue-root";
import { ProjectDraftEmptyState } from "../empty-states";
import { IssuePeekOverview } from "components/issues/peek-overview";
import { ActiveLoader } from "components/ui";
// ui
import { Spinner } from "@plane/ui";
import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root";
@ -39,31 +40,37 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
if (!workspaceSlug || !projectId) return <></>;
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return (
<>
{activeLayout ? (
<ActiveLoader layout={activeLayout} />
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</>
);
}
if (issues?.groupedIssueIds?.length === 0) {
return (
<div className="relative h-full w-full overflow-y-auto">
<ProjectDraftEmptyState />
</div>
);
}
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
<DraftIssueAppliedFiltersRoot />
{issues?.loader === "init-loader" ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
{!issues?.groupedIssueIds ? (
<ProjectDraftEmptyState />
) : (
<div className="relative h-full w-full overflow-auto">
{activeLayout === "list" ? (
<DraftIssueListLayout />
) : activeLayout === "kanban" ? (
<DraftKanBanLayout />
) : null}
{/* issue peek overview */}
<IssuePeekOverview />
</div>
)}
</>
)}
<div className="relative h-full w-full overflow-auto">
{activeLayout === "list" ? <DraftIssueListLayout /> : activeLayout === "kanban" ? <DraftKanBanLayout /> : null}
{/* issue peek overview */}
<IssuePeekOverview />
</div>
</div>
);
});

View File

@ -15,6 +15,7 @@ import {
ModuleListLayout,
ModuleSpreadsheetLayout,
} from "components/issues";
import { ActiveLoader } from "components/ui";
// ui
import { Spinner } from "@plane/ui";
// constants
@ -44,47 +45,56 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
}
);
if (!workspaceSlug || !projectId || !moduleId) return <></>;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
if (!workspaceSlug || !projectId || !moduleId) return <></>;
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return (
<>
{activeLayout ? (
<ActiveLoader layout={activeLayout} />
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</>
);
}
if (issues?.groupedIssueIds?.length === 0) {
return (
<div className="relative h-full w-full overflow-y-auto">
<ModuleEmptyState
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
moduleId={moduleId.toString()}
activeLayout={activeLayout}
/>
</div>
);
}
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
<ModuleAppliedFiltersRoot />
{issues?.loader === "init-loader" || !issues?.groupedIssueIds ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
{issues?.groupedIssueIds?.length === 0 ? (
<ModuleEmptyState
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
moduleId={moduleId.toString()}
activeLayout={activeLayout}
/>
) : (
<>
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ModuleListLayout />
) : activeLayout === "kanban" ? (
<ModuleKanBanLayout />
) : activeLayout === "calendar" ? (
<ModuleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ModuleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ModuleSpreadsheetLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</>
)}
</>
)}
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ModuleListLayout />
) : activeLayout === "kanban" ? (
<ModuleKanBanLayout />
) : activeLayout === "calendar" ? (
<ModuleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ModuleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ModuleSpreadsheetLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</div>
);
});

View File

@ -17,6 +17,8 @@ import {
import { Spinner } from "@plane/ui";
// hooks
import { useIssues } from "hooks/store";
// helpers
import { ActiveLoader } from "components/ui";
// constants
import { EIssuesStoreType } from "constants/issue";
@ -41,49 +43,45 @@ export const ProjectLayoutRoot: FC = observer(() => {
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
if (!workspaceSlug || !projectId) return <></>;
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
}
if (issues?.groupedIssueIds?.length === 0) {
return (
<div className="relative h-full w-full overflow-y-auto">
<ProjectEmptyState />
</div>
);
}
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
<ProjectAppliedFiltersRoot />
{issues?.loader === "init-loader" || !issues?.groupedIssueIds ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
{issues?.groupedIssueIds?.length === 0 ? (
<div className="relative h-full w-full overflow-y-auto">
<ProjectEmptyState />
</div>
) : (
<>
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
{/* mutation loader */}
{issues?.loader === "mutation" && (
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded">
<Spinner className="w-4 h-4" />
</div>
)}
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
{/* mutation loader */}
{issues?.loader === "mutation" && (
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded">
<Spinner className="w-4 h-4" />
</div>
)}
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectSpreadsheetLayout />
) : null}
</div>
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectSpreadsheetLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</>
)}
</>
)}
{/* peek overview */}
<IssuePeekOverview />
</div>
);
});

View File

@ -16,6 +16,7 @@ import {
ProjectViewSpreadsheetLayout,
} from "components/issues";
import { Spinner } from "@plane/ui";
import { ActiveLoader } from "components/ui";
// constants
import { EIssuesStoreType } from "constants/issue";
// types
@ -63,40 +64,49 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
if (!workspaceSlug || !projectId || !viewId) return <></>;
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return (
<>
{activeLayout ? (
<ActiveLoader layout={activeLayout} />
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
</>
);
}
if (issues?.groupedIssueIds?.length === 0) {
return (
<div className="relative h-full w-full overflow-y-auto">
<ProjectViewEmptyState />
</div>
);
}
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
<ProjectViewAppliedFiltersRoot />
{issues?.loader === "init-loader" || !issues?.groupedIssueIds ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
{issues?.groupedIssueIds?.length === 0 ? (
<ProjectViewEmptyState />
) : (
<>
<div className="relative h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ProjectViewListLayout issueActions={issueActions} />
) : activeLayout === "kanban" ? (
<ProjectViewKanBanLayout issueActions={issueActions} />
) : activeLayout === "calendar" ? (
<ProjectViewCalendarLayout issueActions={issueActions} />
) : activeLayout === "gantt_chart" ? (
<ProjectViewGanttLayout issueActions={issueActions} />
) : activeLayout === "spreadsheet" ? (
<ProjectViewSpreadsheetLayout issueActions={issueActions} />
) : null}
</div>
<div className="relative h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ProjectViewListLayout issueActions={issueActions} />
) : activeLayout === "kanban" ? (
<ProjectViewKanBanLayout issueActions={issueActions} />
) : activeLayout === "calendar" ? (
<ProjectViewCalendarLayout issueActions={issueActions} />
) : activeLayout === "gantt_chart" ? (
<ProjectViewGanttLayout issueActions={issueActions} />
) : activeLayout === "spreadsheet" ? (
<ProjectViewSpreadsheetLayout issueActions={issueActions} />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</>
)}
</>
)}
{/* peek overview */}
<IssuePeekOverview />
</div>
);
});

View File

@ -8,7 +8,7 @@ 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";
@ -35,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 (

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

@ -7,7 +7,7 @@ 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";
@ -57,38 +57,33 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
if (!groupedIssueIds || loader === "init-loader")
return <>{activeLayout === "list" ? <ListLayoutLoader /> : <KanbanLayoutLoader />}</>;
if (groupedIssueIds.length === 0) {
return (
<EmptyState
image={emptyStateImage}
title={PROFILE_EMPTY_STATE_DETAILS[type].title}
description={PROFILE_EMPTY_STATE_DETAILS[type].description}
size="sm"
disabled={!isEditingAllowed}
/>
);
}
return (
<>
{loader === "init-loader" ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
{groupedIssueIds ? (
<>
<ProfileIssuesAppliedFiltersRoot />
<div className="-z-1 relative h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ProfileIssuesListLayout />
) : activeLayout === "kanban" ? (
<ProfileIssuesKanBanLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</>
) : (
<EmptyState
image={emptyStateImage}
title={PROFILE_EMPTY_STATE_DETAILS[type].title}
description={PROFILE_EMPTY_STATE_DETAILS[type].description}
size="sm"
disabled={!isEditingAllowed}
/>
)}
</>
)}
<ProfileIssuesAppliedFiltersRoot />
<div className="-z-1 relative h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ProfileIssuesListLayout />
) : activeLayout === "kanban" ? (
<ProfileIssuesKanBanLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</>
);
});

View File

@ -4,8 +4,8 @@ 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";
@ -27,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 (
<>

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

@ -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,7 +8,8 @@ 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";
@ -28,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));

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

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

@ -13,7 +13,8 @@ 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";
@ -66,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 (

View File

@ -16,7 +16,7 @@ import { AppLayout } from "layouts/app-layout";
import { RecentPagesList, CreateUpdatePageModal } from "components/pages";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
import { PagesHeader } from "components/headers";
import { Spinner } from "@plane/ui";
import { PagesLoader } from "components/ui";
// types
import { NextPageWithLayout } from "lib/types";
// constants
@ -125,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 (
<>

View File

@ -15,7 +15,7 @@ import { IntegrationCard } from "components/project";
import { ProjectSettingHeader } from "components/headers";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { Loader } from "@plane/ui";
import { IntegrationsSettingsLoader } from "components/ui";
// types
import { IProject } from "@plane/types";
import { NextPageWithLayout } from "lib/types";
@ -79,12 +79,7 @@ const ProjectIntegrationsPage: NextPageWithLayout = () => {
</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

@ -13,7 +13,8 @@ import { WorkspaceSettingHeader } from "components/headers";
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
@ -56,49 +57,47 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
</div>
);
if (!tokens) {
return <APITokenSettingsLoader />;
}
return (
<>
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
{tokens ? (
<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>
<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>
<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>

View File

@ -13,7 +13,8 @@ import { WorkspaceSettingHeader } from "components/headers";
import { WebhooksList, CreateWebhookModal } from "components/web-hooks";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { Button, Spinner } from "@plane/ui";
import { Button } from "@plane/ui";
import { WebhookSettingsLoader } from "components/ui";
// types
import { NextPageWithLayout } from "lib/types";
// constants
@ -59,12 +60,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
</div>
);
if (!webhooks)
return (
<div className="grid h-full w-full place-items-center p-4">
<Spinner />
</div>
);
if (!webhooks) return <WebhookSettingsLoader />;
return (
<div className="h-full w-full overflow-hidden py-8 pr-9">

View File

@ -14,7 +14,7 @@ import { RichReadOnlyEditor } from "@plane/rich-text-editor";
// icons
import { History, MessageSquare } from "lucide-react";
// ui
import { Loader } from "@plane/ui";
import { ActivitySettingsLoader } from "components/ui";
// fetch-keys
import { USER_ACTIVITY } from "constants/fetch-keys";
// helper
@ -31,7 +31,6 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
const { currentUser } = useUser();
return (
<section className="mx-auto mt-5 md:mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5">
<div className="flex items-center border-b border-custom-border-100 gap-4 pb-3.5">
<SidebarHamburgerToggle />
@ -97,12 +96,12 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
const message =
activityItem.verb === "created" &&
activityItem.field !== "cycles" &&
activityItem.field !== "modules" &&
activityItem.field !== "attachment" &&
activityItem.field !== "link" &&
activityItem.field !== "estimate" &&
!activityItem.field ? (
activityItem.field !== "cycles" &&
activityItem.field !== "modules" &&
activityItem.field !== "attachment" &&
activityItem.field !== "link" &&
activityItem.field !== "estimate" &&
!activityItem.field ? (
<span>
created <IssueLink activity={activityItem} />
</span>
@ -182,15 +181,9 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
</ul>
</div>
) : (
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
<ActivitySettingsLoader />
)}
</section>
);
});

View File

@ -3,7 +3,7 @@ import useSWR from "swr";
// layouts
import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences";
// ui
import { Loader } from "@plane/ui";
import { EmailSettingsLoader } from "components/ui";
// components
import { EmailNotificationForm } from "components/profile/preferences";
// services
@ -20,18 +20,8 @@ const ProfilePreferencesThemePage: NextPageWithLayout = () => {
userService.currentUserEmailNotificationSettings()
);
if (isLoading) {
return (
<Loader className="space-y-4 mt-8 px-6 lg:px-20">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
);
}
if (!data) {
return null;
if (!data || isLoading) {
return <EmailSettingsLoader />;
}
return (