refactor: unused components, hooks, constants (#7157)

* refactor: remove unused dashboard components and fetch keys

* refactor: remove unused hooks and wrappers

* chore: remove unused function
This commit is contained in:
Aaryan Khandelwal 2025-06-06 14:09:56 +05:30 committed by GitHub
parent 6be3f0ea73
commit 245167e8aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 1 additions and 2413 deletions

View file

@ -1,2 +0,0 @@
export * from "./widgets";
export * from "./project-empty-state";

View file

@ -1,46 +0,0 @@
"use client";
import { observer } from "mobx-react";
import Image from "next/image";
// ui
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { Button } from "@plane/ui";
// hooks
import { useCommandPalette, useEventTracker, useUserPermissions } from "@/hooks/store";
// assets
import ProjectEmptyStateImage from "@/public/empty-state/onboarding/dashboard-light.webp";
export const DashboardProjectEmptyState = observer(() => {
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
// derived values
const canCreateProject = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
return (
<div className="mx-auto flex h-full flex-col justify-center space-y-4 lg:w-3/5">
<h4 className="text-xl font-semibold">Overview of your projects, activity, and metrics</h4>
<p className="text-custom-text-300">
Welcome to Plane, we are excited to have you here. Create your first project and track your work items, and this
page will transform into a space that helps you progress. Admins will also see items which help their team
progress.
</p>
<Image src={ProjectEmptyStateImage} className="w-full" alt="Project empty state" />
{canCreateProject && (
<div className="flex justify-center">
<Button
variant="primary"
onClick={() => {
setTrackElement("Project empty state");
toggleCreateProjectModal(true);
}}
>
Build your first project
</Button>
</div>
)}
</div>
);
});

View file

@ -1,165 +0,0 @@
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { Tab } from "@headlessui/react";
import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@plane/constants";
import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types";
// hooks
import { Card } from "@plane/ui";
import {
DurationFilterDropdown,
IssuesErrorState,
TabsList,
WidgetIssuesList,
WidgetLoader,
WidgetProps,
} from "@/components/dashboard/widgets";
import { getCustomDates, getRedirectionFilters, getTabKey } from "@/helpers/dashboard.helper";
import { useDashboard } from "@/hooks/store";
// components
// helpers
// types
// constants
const WIDGET_KEY = "assigned_issues";
export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
const { dashboardId, workspaceSlug } = props;
// states
const [fetching, setFetching] = useState(false);
// store hooks
const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } =
useDashboard();
// derived values
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStats = getWidgetStats<TAssignedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStatsError = getWidgetStatsError(workspaceSlug, dashboardId, WIDGET_KEY);
const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE;
const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab);
const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? [];
const handleUpdateFilters = async (filters: Partial<TAssignedIssuesWidgetFilters>) => {
if (!widgetDetails) return;
setFetching(true);
await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, {
widgetKey: WIDGET_KEY,
filters,
});
const filterDates = getCustomDates(
filters.duration ?? selectedDurationFilter,
filters.custom_dates ?? selectedCustomDates
);
fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY,
issue_type: filters.tab ?? selectedTab,
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
expand: "issue_relation",
}).finally(() => setFetching(false));
};
useEffect(() => {
const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates);
fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY,
issue_type: selectedTab,
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
expand: "issue_relation",
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const filterParams = getRedirectionFilters(selectedTab);
const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab);
if ((!widgetDetails || !widgetStats) && !widgetStatsError) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return (
<Card>
{widgetStatsError ? (
<IssuesErrorState
isRefreshing={fetching}
onClick={() =>
handleUpdateFilters({
duration: EDurationFilters.NONE,
tab: "pending",
})
}
/>
) : (
widgetStats && (
<>
<div className="flex items-center justify-between gap-2 mb-4">
<Link
href={`/${workspaceSlug}/workspace-views/assigned/${filterParams}`}
className="text-lg font-semibold text-custom-text-300 hover:underline"
>
Assigned to you
</Link>
<DurationFilterDropdown
customDates={selectedCustomDates}
value={selectedDurationFilter}
onChange={(val, customDates) => {
if (val === "custom" && customDates) {
handleUpdateFilters({
duration: val,
custom_dates: customDates,
});
return;
}
if (val === selectedDurationFilter) return;
let newTab = selectedTab;
// switch to pending tab if target date is changed to none
if (val === "none" && selectedTab !== "completed") newTab = "pending";
// switch to upcoming tab if target date is changed to other than none
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed")
newTab = "upcoming";
handleUpdateFilters({
duration: val,
tab: newTab,
});
}}
/>
</div>
<Tab.Group
as="div"
selectedIndex={selectedTabIndex}
onChange={(i) => {
const newSelectedTab = tabsList[i];
handleUpdateFilters({ tab: newSelectedTab?.key ?? "completed" });
}}
className="h-full flex flex-col"
>
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} />
<Tab.Panels as="div" className="h-full">
{tabsList.map((tab) => {
if (tab.key !== selectedTab) return null;
return (
<Tab.Panel key={tab.key} as="div" className="h-full flex flex-col" static>
<WidgetIssuesList
tab={tab.key}
type="assigned"
workspaceSlug={workspaceSlug}
widgetStats={widgetStats}
isLoading={fetching}
/>
</Tab.Panel>
);
})}
</Tab.Panels>
</Tab.Group>
</>
)
)}
</Card>
);
});

View file

@ -1,162 +0,0 @@
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { Tab } from "@headlessui/react";
import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@plane/constants";
import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types";
// hooks
import { Card } from "@plane/ui";
import {
DurationFilterDropdown,
IssuesErrorState,
TabsList,
WidgetIssuesList,
WidgetLoader,
WidgetProps,
} from "@/components/dashboard/widgets";
import { getCustomDates, getRedirectionFilters, getTabKey } from "@/helpers/dashboard.helper";
import { useDashboard } from "@/hooks/store";
// components
// helpers
// types
// constants
const WIDGET_KEY = "created_issues";
export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
const { dashboardId, workspaceSlug } = props;
// states
const [fetching, setFetching] = useState(false);
// store hooks
const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } =
useDashboard();
// derived values
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStats = getWidgetStats<TCreatedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStatsError = getWidgetStatsError(workspaceSlug, dashboardId, WIDGET_KEY);
const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE;
const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab);
const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? [];
const handleUpdateFilters = async (filters: Partial<TCreatedIssuesWidgetFilters>) => {
if (!widgetDetails) return;
setFetching(true);
await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, {
widgetKey: WIDGET_KEY,
filters,
});
const filterDates = getCustomDates(
filters.duration ?? selectedDurationFilter,
filters.custom_dates ?? selectedCustomDates
);
fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY,
issue_type: filters.tab ?? selectedTab,
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
}).finally(() => setFetching(false));
};
useEffect(() => {
const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates);
fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY,
issue_type: selectedTab,
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const filterParams = getRedirectionFilters(selectedTab);
const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab);
if ((!widgetDetails || !widgetStats) && !widgetStatsError) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return (
<Card>
{widgetStatsError ? (
<IssuesErrorState
isRefreshing={fetching}
onClick={() =>
handleUpdateFilters({
duration: EDurationFilters.NONE,
tab: "pending",
})
}
/>
) : (
widgetStats && (
<>
<div className="flex items-center justify-between gap-2 mb-4">
<Link
href={`/${workspaceSlug}/workspace-views/created/${filterParams}`}
className="text-lg font-semibold text-custom-text-300 hover:underline"
>
Created by you
</Link>
<DurationFilterDropdown
customDates={selectedCustomDates}
value={selectedDurationFilter}
onChange={(val, customDates) => {
if (val === "custom" && customDates) {
handleUpdateFilters({
duration: val,
custom_dates: customDates,
});
return;
}
if (val === selectedDurationFilter) return;
let newTab = selectedTab;
// switch to pending tab if target date is changed to none
if (val === "none" && selectedTab !== "completed") newTab = "pending";
// switch to upcoming tab if target date is changed to other than none
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed")
newTab = "upcoming";
handleUpdateFilters({
duration: val,
tab: newTab,
});
}}
/>
</div>
<Tab.Group
as="div"
selectedIndex={selectedTabIndex}
onChange={(i) => {
const newSelectedTab = tabsList[i];
handleUpdateFilters({ tab: newSelectedTab.key ?? "completed" });
}}
className="h-full flex flex-col"
>
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} />
<Tab.Panels as="div" className="h-full">
{tabsList.map((tab) => {
if (tab.key !== selectedTab) return null;
return (
<Tab.Panel key={tab.key} as="div" className="h-full flex flex-col" static>
<WidgetIssuesList
tab={tab.key}
type="created"
workspaceSlug={workspaceSlug}
widgetStats={widgetStats}
isLoading={fetching}
/>
</Tab.Panel>
);
})}
</Tab.Panels>
</Tab.Group>
</>
)
)}
</Card>
);
});

View file

@ -1,58 +0,0 @@
"use client";
import { useState } from "react";
import { ChevronDown } from "lucide-react";
// components
import { DURATION_FILTER_OPTIONS, EDurationFilters } from "@plane/constants";
import { CustomMenu } from "@plane/ui";
import { DateFilterModal } from "@/components/core";
// ui
// helpers
import { getDurationFilterDropdownLabel } from "@/helpers/dashboard.helper";
// constants
type Props = {
customDates?: string[];
onChange: (value: EDurationFilters, customDates?: string[]) => void;
value: EDurationFilters;
};
export const DurationFilterDropdown: React.FC<Props> = (props) => {
const { customDates, onChange, value } = props;
// states
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
return (
<>
<DateFilterModal
isOpen={isDateFilterModalOpen}
handleClose={() => setIsDateFilterModalOpen(false)}
onSelect={(val) => onChange(EDurationFilters.CUSTOM, val)}
title="Due date"
/>
<CustomMenu
className="flex-shrink-0"
customButton={
<div className="px-3 py-2 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 focus:bg-custom-background-80 text-xs font-medium whitespace-nowrap rounded-md outline-none flex items-center gap-2">
{getDurationFilterDropdownLabel(value, customDates ?? [])}
<ChevronDown className="h-3 w-3" />
</div>
}
placement="bottom-end"
closeOnSelect
>
{DURATION_FILTER_OPTIONS.map((option) => (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
if (option.key === "custom") setIsDateFilterModalOpen(true);
else onChange(option.key);
}}
>
{option.label}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</>
);
};

View file

@ -1 +0,0 @@
export * from "./duration-filter";

View file

@ -1,55 +0,0 @@
import Image from "next/image";
import { useTheme } from "next-themes";
import { TIssuesListTypes } from "@plane/types";
import CompletedIssuesDark from "@/public/empty-state/dashboard/dark/completed-issues.svg";
import OverdueIssuesDark from "@/public/empty-state/dashboard/dark/overdue-issues.svg";
import UpcomingIssuesDark from "@/public/empty-state/dashboard/dark/upcoming-issues.svg";
import CompletedIssuesLight from "@/public/empty-state/dashboard/light/completed-issues.svg";
import OverdueIssuesLight from "@/public/empty-state/dashboard/light/overdue-issues.svg";
import UpcomingIssuesLight from "@/public/empty-state/dashboard/light/upcoming-issues.svg";
export const ASSIGNED_ISSUES_EMPTY_STATES = {
pending: {
title: "Work items assigned to you that are pending\nwill show up here.",
darkImage: UpcomingIssuesDark,
lightImage: UpcomingIssuesLight,
},
upcoming: {
title: "Upcoming work items assigned to\nyou will show up here.",
darkImage: UpcomingIssuesDark,
lightImage: UpcomingIssuesLight,
},
overdue: {
title: "Work items assigned to you that are past\ntheir due date will show up here.",
darkImage: OverdueIssuesDark,
lightImage: OverdueIssuesLight,
},
completed: {
title: "Work items assigned to you that you have\nmarked Completed will show up here.",
darkImage: CompletedIssuesDark,
lightImage: CompletedIssuesLight,
},
};
type Props = {
type: TIssuesListTypes;
};
export const AssignedIssuesEmptyState: React.FC<Props> = (props) => {
const { type } = props;
// next-themes
const { resolvedTheme } = useTheme();
const typeDetails = ASSIGNED_ISSUES_EMPTY_STATES[type];
const image = resolvedTheme === "dark" ? typeDetails.darkImage : typeDetails.lightImage;
// TODO: update empty state logic to use a general component
return (
<div className="text-center space-y-6 flex flex-col items-center">
<div className="h-24 w-24">
<Image src={image} className="w-full h-full" alt="Assigned work items" />
</div>
<p className="text-sm font-medium text-custom-text-300 whitespace-pre-line">{typeDetails.title}</p>
</div>
);
};

View file

@ -1,55 +0,0 @@
import Image from "next/image";
import { useTheme } from "next-themes";
import { TIssuesListTypes } from "@plane/types";
import CompletedIssuesDark from "@/public/empty-state/dashboard/dark/completed-issues.svg";
import OverdueIssuesDark from "@/public/empty-state/dashboard/dark/overdue-issues.svg";
import UpcomingIssuesDark from "@/public/empty-state/dashboard/dark/upcoming-issues.svg";
import CompletedIssuesLight from "@/public/empty-state/dashboard/light/completed-issues.svg";
import OverdueIssuesLight from "@/public/empty-state/dashboard/light/overdue-issues.svg";
import UpcomingIssuesLight from "@/public/empty-state/dashboard/light/upcoming-issues.svg";
export const CREATED_ISSUES_EMPTY_STATES = {
pending: {
title: "Work items created by you that are pending\nwill show up here.",
darkImage: UpcomingIssuesDark,
lightImage: UpcomingIssuesLight,
},
upcoming: {
title: "Upcoming work items you created\nwill show up here.",
darkImage: UpcomingIssuesDark,
lightImage: UpcomingIssuesLight,
},
overdue: {
title: "Work items created by you that are past their\ndue date will show up here.",
darkImage: OverdueIssuesDark,
lightImage: OverdueIssuesLight,
},
completed: {
title: "Work items created by you that you have\nmarked completed will show up here.",
darkImage: CompletedIssuesDark,
lightImage: CompletedIssuesLight,
},
};
type Props = {
type: TIssuesListTypes;
};
export const CreatedIssuesEmptyState: React.FC<Props> = (props) => {
const { type } = props;
// next-themes
const { resolvedTheme } = useTheme();
const typeDetails = CREATED_ISSUES_EMPTY_STATES[type];
const image = resolvedTheme === "dark" ? typeDetails.darkImage : typeDetails.lightImage;
return (
<div className="text-center space-y-6 flex flex-col items-center">
<div className="h-24 w-24">
<Image src={image} className="w-full h-full" alt="Assigned work items" />
</div>
<p className="text-sm font-medium text-custom-text-300 whitespace-pre-line">{typeDetails.title}</p>
</div>
);
};

View file

@ -1,6 +0,0 @@
export * from "./assigned-issues";
export * from "./created-issues";
export * from "./issues-by-priority";
export * from "./issues-by-state-group";
export * from "./recent-activity";
export * from "./recent-collaborators";

View file

@ -1,25 +0,0 @@
import Image from "next/image";
import { useTheme } from "next-themes";
// assets
import DarkImage from "@/public/empty-state/dashboard/dark/issues-by-priority.svg";
import LightImage from "@/public/empty-state/dashboard/light/issues-by-priority.svg";
export const IssuesByPriorityEmptyState = () => {
// next-themes
const { resolvedTheme } = useTheme();
const image = resolvedTheme === "dark" ? DarkImage : LightImage;
return (
<div className="text-center space-y-6 flex flex-col items-center">
<div className="h-24 w-24">
<Image src={image} className="w-full h-full" alt="Work items by state group" />
</div>
<p className="text-sm font-medium text-custom-text-300">
Work items assigned to you, broken down by
<br />
priority will show up here.
</p>
</div>
);
};

View file

@ -1,25 +0,0 @@
import Image from "next/image";
import { useTheme } from "next-themes";
// assets
import DarkImage from "@/public/empty-state/dashboard/dark/issues-by-state-group.svg";
import LightImage from "@/public/empty-state/dashboard/light/issues-by-state-group.svg";
export const IssuesByStateGroupEmptyState = () => {
// next-themes
const { resolvedTheme } = useTheme();
const image = resolvedTheme === "dark" ? DarkImage : LightImage;
return (
<div className="text-center space-y-6 flex flex-col items-center">
<div className="h-24 w-24">
<Image src={image} className="w-full h-full" alt="Work items by state group" />
</div>
<p className="text-sm font-medium text-custom-text-300">
Work items assigned to you, broken down by state,
<br />
will show up here.
</p>
</div>
);
};

View file

@ -1,25 +0,0 @@
import Image from "next/image";
import { useTheme } from "next-themes";
// assets
import DarkImage from "@/public/empty-state/dashboard/dark/recent-activity.svg";
import LightImage from "@/public/empty-state/dashboard/light/recent-activity.svg";
export const RecentActivityEmptyState = () => {
// next-themes
const { resolvedTheme } = useTheme();
const image = resolvedTheme === "dark" ? DarkImage : LightImage;
return (
<div className="text-center space-y-6 flex flex-col items-center">
<div className="h-24 w-24">
<Image src={image} className="w-full h-full" alt="Work items by state group" />
</div>
<p className="text-sm font-medium text-custom-text-300">
All your work items activities across
<br />
projects will show up here.
</p>
</div>
);
};

View file

@ -1,39 +0,0 @@
import Image from "next/image";
import { useTheme } from "next-themes";
// assets
import DarkImage1 from "@/public/empty-state/dashboard/dark/recent-collaborators-1.svg";
import DarkImage2 from "@/public/empty-state/dashboard/dark/recent-collaborators-2.svg";
import DarkImage3 from "@/public/empty-state/dashboard/dark/recent-collaborators-3.svg";
import LightImage1 from "@/public/empty-state/dashboard/light/recent-collaborators-1.svg";
import LightImage2 from "@/public/empty-state/dashboard/light/recent-collaborators-2.svg";
import LightImage3 from "@/public/empty-state/dashboard/light/recent-collaborators-3.svg";
export const RecentCollaboratorsEmptyState = () => {
// next-themes
const { resolvedTheme } = useTheme();
const image1 = resolvedTheme === "dark" ? DarkImage1 : LightImage1;
const image2 = resolvedTheme === "dark" ? DarkImage2 : LightImage2;
const image3 = resolvedTheme === "dark" ? DarkImage3 : LightImage3;
return (
<div className="mt-7 mb-16 px-36 flex flex-col lg:flex-row items-center justify-between gap-x-24 gap-y-16">
<p className="text-sm font-medium text-custom-text-300 lg:w-2/5 flex-shrink-0 text-center lg:text-left">
Compare your activities with the top
<br />
seven in your project.
</p>
<div className="flex items-center justify-evenly gap-20 lg:w-3/5 flex-shrink-0">
<div className="h-24 w-24 flex-shrink-0">
<Image src={image1} className="w-full h-full" alt="Recent collaborators" />
</div>
<div className="h-24 w-24 flex-shrink-0">
<Image src={image2} className="w-full h-full" alt="Recent collaborators" />
</div>
<div className="h-24 w-24 flex-shrink-0 hidden xl:block">
<Image src={image3} className="w-full h-full" alt="Recent collaborators" />
</div>
</div>
</div>
);
};

View file

@ -1 +0,0 @@
export * from "./issues";

View file

@ -1,34 +0,0 @@
"use client";
import { AlertTriangle, RefreshCcw } from "lucide-react";
// ui
import { Button } from "@plane/ui";
type Props = {
isRefreshing: boolean;
onClick: () => void;
};
export const IssuesErrorState: React.FC<Props> = (props) => {
const { isRefreshing, onClick } = props;
return (
<div className="h-full w-full grid place-items-center">
<div className="text-center">
<div className="h-24 w-24 bg-red-500/20 rounded-full grid place-items-center mx-auto">
<AlertTriangle className="h-12 w-12 text-red-500" />
</div>
<p className="mt-7 text-custom-text-300 text-sm font-medium">There was an error in fetching widget details</p>
<Button
variant="neutral-primary"
prependIcon={<RefreshCcw className="h-3 w-3" />}
className="mt-2 mx-auto"
onClick={onClick}
loading={isRefreshing}
>
{isRefreshing ? "Retrying" : "Retry"}
</Button>
</div>
</div>
);
};

View file

@ -1,11 +0,0 @@
export * from "./dropdowns";
export * from "./empty-states";
export * from "./error-states";
export * from "./issue-panels";
export * from "./loaders";
export * from "./assigned-issues";
export * from "./created-issues";
export * from "./overview-stats";
export * from "./recent-activity";
export * from "./recent-collaborators";
export * from "./recent-projects";

View file

@ -1,3 +0,0 @@
export * from "./issue-list-item";
export * from "./issues-list";
export * from "./tabs-list";

View file

@ -1,401 +0,0 @@
"use client";
import { isToday } from "date-fns/isToday";
import { observer } from "mobx-react";
// types
import { TIssue, TWidgetIssue } from "@plane/types";
// ui
import { Avatar, AvatarGroup, ControlLink, PriorityIcon } from "@plane/ui";
// helpers
import { findTotalDaysInRange, getDate, renderFormattedDate } from "@/helpers/date-time.helper";
import { getFileURL } from "@/helpers/file.helper";
import { generateWorkItemLink } from "@/helpers/issue.helper";
// hooks
import { useIssueDetail, useMember, useProject } from "@/hooks/store";
// plane web components
import { IssueIdentifier } from "@/plane-web/components/issues";
export type IssueListItemProps = {
issueId: string;
onClick: (issue: TIssue) => void;
workspaceSlug: string;
};
export const AssignedUpcomingIssueListItem: React.FC<IssueListItemProps> = observer((props) => {
const { issueId, onClick, workspaceSlug } = props;
// store hooks
const { getProjectById } = useProject();
const {
issue: { getIssueById },
} = useIssueDetail();
// derived values
const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined;
if (!issueDetails || !issueDetails.project_id) return null;
const projectDetails = getProjectById(issueDetails.project_id);
const blockedByIssues = issueDetails.issue_relation?.filter((issue) => issue.relation_type === "blocked_by") ?? [];
const blockedByIssueProjectDetails =
blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null;
const targetDate = getDate(issueDetails.target_date);
const workItemLink = generateWorkItemLink({
workspaceSlug,
projectId: issueDetails?.project_id,
issueId: issueDetails?.id,
projectIdentifier: projectDetails?.identifier,
sequenceId: issueDetails?.sequence_id,
});
return (
<ControlLink
href={workItemLink}
onClick={() => onClick(issueDetails)}
className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80"
>
<div className="col-span-7 flex items-center gap-3">
{projectDetails && (
<IssueIdentifier
issueId={issueDetails.id}
projectId={projectDetails?.id}
textContainerClassName="text-xs text-custom-text-200 font-medium"
/>
)}
<h6 className="flex-grow truncate text-sm">{issueDetails.name}</h6>
</div>
<div className="flex justify-center col-span-1 items-center">
<PriorityIcon priority={issueDetails.priority} size={12} withContainer />
</div>
<div className="text-center text-xs col-span-2">
{targetDate ? (isToday(targetDate) ? "Today" : renderFormattedDate(targetDate)) : "-"}
</div>
<div className="flex justify-center text-xs col-span-2">
{blockedByIssues.length > 0
? blockedByIssues.length > 1
? `${blockedByIssues.length} blockers`
: blockedByIssueProjectDetails && (
<IssueIdentifier
projectIdentifier={blockedByIssueProjectDetails?.identifier}
projectId={blockedByIssueProjectDetails?.id}
issueSequenceId={blockedByIssues[0]?.sequence_id}
textContainerClassName="text-xs text-custom-text-200 font-medium"
/>
)
: "-"}
</div>
</ControlLink>
);
});
export const AssignedOverdueIssueListItem: React.FC<IssueListItemProps> = observer((props) => {
const { issueId, onClick, workspaceSlug } = props;
// store hooks
const { getProjectById } = useProject();
const {
issue: { getIssueById },
} = useIssueDetail();
// derived values
const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined;
if (!issueDetails || !issueDetails.project_id) return null;
const projectDetails = getProjectById(issueDetails.project_id);
const blockedByIssues = issueDetails.issue_relation?.filter((issue) => issue.relation_type === "blocked_by") ?? [];
const blockedByIssueProjectDetails =
blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null;
const dueBy = findTotalDaysInRange(getDate(issueDetails.target_date), new Date(), false) ?? 0;
const workItemLink = generateWorkItemLink({
workspaceSlug,
projectId: issueDetails?.project_id,
issueId: issueDetails?.id,
projectIdentifier: projectDetails?.identifier,
sequenceId: issueDetails?.sequence_id,
});
return (
<ControlLink
href={workItemLink}
onClick={() => onClick(issueDetails)}
className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80"
>
<div className="col-span-7 flex items-center gap-3">
{projectDetails && (
<IssueIdentifier
issueId={issueDetails.id}
projectId={projectDetails?.id}
textContainerClassName="text-xs text-custom-text-200 font-medium"
/>
)}
<h6 className="flex-grow truncate text-sm">{issueDetails.name}</h6>
</div>
<div className="flex justify-center col-span-1 items-center">
<PriorityIcon priority={issueDetails.priority} size={12} withContainer />
</div>
<div className="text-center text-xs col-span-2">
{dueBy} {`day${dueBy > 1 ? "s" : ""}`}
</div>
<div className="flex justify-center text-xs col-span-2">
{blockedByIssues.length > 0
? blockedByIssues.length > 1
? `${blockedByIssues.length} blockers`
: blockedByIssueProjectDetails && (
<IssueIdentifier
issueId={blockedByIssues[0]?.id}
projectId={blockedByIssueProjectDetails?.id}
textContainerClassName="text-xs text-custom-text-200 font-medium"
/>
)
: "-"}
</div>
</ControlLink>
);
});
export const AssignedCompletedIssueListItem: React.FC<IssueListItemProps> = observer((props) => {
const { issueId, onClick, workspaceSlug } = props;
// store hooks
const {
issue: { getIssueById },
} = useIssueDetail();
const { getProjectById } = useProject();
// derived values
const issueDetails = getIssueById(issueId);
if (!issueDetails || !issueDetails.project_id) return null;
const projectDetails = getProjectById(issueDetails.project_id);
const workItemLink = generateWorkItemLink({
workspaceSlug,
projectId: issueDetails?.project_id,
issueId: issueDetails?.id,
projectIdentifier: projectDetails?.identifier,
sequenceId: issueDetails?.sequence_id,
});
return (
<ControlLink
href={workItemLink}
onClick={() => onClick(issueDetails)}
className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80"
>
<div className="col-span-11 flex items-center gap-3">
{projectDetails && (
<IssueIdentifier
issueId={issueDetails.id}
projectId={projectDetails?.id}
textContainerClassName="text-xs text-custom-text-200 font-medium"
/>
)}
<h6 className="flex-grow truncate text-sm">{issueDetails.name}</h6>
</div>
<div className="flex justify-center col-span-1 items-center">
<PriorityIcon priority={issueDetails.priority} size={12} withContainer />
</div>
</ControlLink>
);
});
export const CreatedUpcomingIssueListItem: React.FC<IssueListItemProps> = observer((props) => {
const { issueId, onClick, workspaceSlug } = props;
// store hooks
const { getUserDetails } = useMember();
const {
issue: { getIssueById },
} = useIssueDetail();
const { getProjectById } = useProject();
// derived values
const issue = getIssueById(issueId);
if (!issue || !issue.project_id) return null;
const projectDetails = getProjectById(issue.project_id);
const targetDate = getDate(issue.target_date);
const workItemLink = generateWorkItemLink({
workspaceSlug,
projectId: issue?.project_id,
issueId: issue?.id,
projectIdentifier: projectDetails?.identifier,
sequenceId: issue?.sequence_id,
});
return (
<ControlLink
href={workItemLink}
onClick={() => onClick(issue)}
className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80"
>
<div className="col-span-7 flex items-center gap-3">
{projectDetails && (
<IssueIdentifier
issueId={issue.id}
projectId={projectDetails?.id}
textContainerClassName="text-xs text-custom-text-200 font-medium"
/>
)}
<h6 className="flex-grow truncate text-sm">{issue.name}</h6>
</div>
<div className="flex justify-center col-span-1 items-center">
<PriorityIcon priority={issue.priority} size={12} withContainer />
</div>
<div className="text-center text-xs col-span-2">
{targetDate ? (isToday(targetDate) ? "Today" : renderFormattedDate(targetDate)) : "-"}
</div>
<div className="flex justify-center text-xs col-span-2">
{issue.assignee_ids && issue.assignee_ids?.length > 0 ? (
<AvatarGroup>
{issue.assignee_ids?.map((assigneeId) => {
const userDetails = getUserDetails(assigneeId);
if (!userDetails) return null;
return (
<Avatar key={assigneeId} src={getFileURL(userDetails.avatar_url)} name={userDetails.display_name} />
);
})}
</AvatarGroup>
) : (
"-"
)}
</div>
</ControlLink>
);
});
export const CreatedOverdueIssueListItem: React.FC<IssueListItemProps> = observer((props) => {
const { issueId, onClick, workspaceSlug } = props;
// store hooks
const { getUserDetails } = useMember();
const {
issue: { getIssueById },
} = useIssueDetail();
const { getProjectById } = useProject();
// derived values
const issue = getIssueById(issueId);
if (!issue || !issue.project_id) return null;
const projectDetails = getProjectById(issue.project_id);
const dueBy: number = findTotalDaysInRange(getDate(issue.target_date), new Date(), false) ?? 0;
const workItemLink = generateWorkItemLink({
workspaceSlug,
projectId: issue?.project_id,
issueId: issue?.id,
projectIdentifier: projectDetails?.identifier,
sequenceId: issue?.sequence_id,
});
return (
<ControlLink
href={workItemLink}
onClick={() => onClick(issue)}
className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80"
>
<div className="col-span-7 flex items-center gap-3">
{projectDetails && (
<IssueIdentifier
issueId={issue.id}
projectId={projectDetails?.id}
textContainerClassName="text-xs text-custom-text-200 font-medium"
/>
)}
<h6 className="flex-grow truncate text-sm">{issue.name}</h6>
</div>
<div className="flex justify-center col-span-1 items-center">
<PriorityIcon priority={issue.priority} size={12} withContainer />
</div>
<div className="text-center text-xs col-span-2">
{dueBy} {`day${dueBy > 1 ? "s" : ""}`}
</div>
<div className="flex justify-center text-xs col-span-2">
{issue.assignee_ids.length > 0 ? (
<AvatarGroup>
{issue.assignee_ids?.map((assigneeId) => {
const userDetails = getUserDetails(assigneeId);
if (!userDetails) return null;
return (
<Avatar key={assigneeId} src={getFileURL(userDetails.avatar_url)} name={userDetails.display_name} />
);
})}
</AvatarGroup>
) : (
"-"
)}
</div>
</ControlLink>
);
});
export const CreatedCompletedIssueListItem: React.FC<IssueListItemProps> = observer((props) => {
const { issueId, onClick, workspaceSlug } = props;
// store hooks
const { getUserDetails } = useMember();
const {
issue: { getIssueById },
} = useIssueDetail();
const { getProjectById } = useProject();
// derived values
const issue = getIssueById(issueId);
if (!issue || !issue.project_id) return null;
const projectDetails = getProjectById(issue.project_id);
const workItemLink = generateWorkItemLink({
workspaceSlug,
projectId: issue?.project_id,
issueId: issue?.id,
projectIdentifier: projectDetails?.identifier,
sequenceId: issue?.sequence_id,
});
return (
<ControlLink
href={workItemLink}
onClick={() => onClick(issue)}
className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80"
>
<div className="col-span-9 flex items-center gap-3">
{projectDetails && (
<IssueIdentifier
issueId={issue.id}
projectId={projectDetails?.id}
textContainerClassName="text-xs text-custom-text-200 font-medium"
/>
)}
<h6 className="flex-grow truncate text-sm">{issue.name}</h6>
</div>
<div className="flex justify-center col-span-1 items-center">
<PriorityIcon priority={issue.priority} size={12} withContainer />
</div>
<div className="flex justify-center text-xs col-span-2">
{issue.assignee_ids.length > 0 ? (
<AvatarGroup>
{issue.assignee_ids?.map((assigneeId) => {
const userDetails = getUserDetails(assigneeId);
if (!userDetails) return null;
return (
<Avatar key={assigneeId} src={getFileURL(userDetails.avatar_url)} name={userDetails.display_name} />
);
})}
</AvatarGroup>
) : (
"-"
)}
</div>
</ControlLink>
);
});

View file

@ -1,134 +0,0 @@
"use client";
import Link from "next/link";
import { TAssignedIssuesWidgetResponse, TCreatedIssuesWidgetResponse, TIssue, TIssuesListTypes } from "@plane/types";
// hooks
// components
import { Loader, getButtonStyling } from "@plane/ui";
import {
AssignedCompletedIssueListItem,
AssignedIssuesEmptyState,
AssignedOverdueIssueListItem,
AssignedUpcomingIssueListItem,
CreatedCompletedIssueListItem,
CreatedIssuesEmptyState,
CreatedOverdueIssueListItem,
CreatedUpcomingIssueListItem,
IssueListItemProps,
} from "@/components/dashboard/widgets";
// ui
// helpers
import { cn } from "@/helpers/common.helper";
import { getRedirectionFilters } from "@/helpers/dashboard.helper";
// hooks
import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection";
import { usePlatformOS } from "@/hooks/use-platform-os";
export type WidgetIssuesListProps = {
isLoading: boolean;
tab: TIssuesListTypes;
type: "assigned" | "created";
widgetStats: TAssignedIssuesWidgetResponse | TCreatedIssuesWidgetResponse;
workspaceSlug: string;
};
export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
const { isLoading, tab, type, widgetStats, workspaceSlug } = props;
// hooks
const { isMobile } = usePlatformOS();
const { handleRedirection } = useIssuePeekOverviewRedirection();
// handlers
const handleIssuePeekOverview = (issue: TIssue) => handleRedirection(workspaceSlug, issue, isMobile);
const filterParams = getRedirectionFilters(tab);
const ISSUE_LIST_ITEM: {
[key: string]: {
[key in TIssuesListTypes]: React.FC<IssueListItemProps>;
};
} = {
assigned: {
pending: AssignedUpcomingIssueListItem,
upcoming: AssignedUpcomingIssueListItem,
overdue: AssignedOverdueIssueListItem,
completed: AssignedCompletedIssueListItem,
},
created: {
pending: CreatedUpcomingIssueListItem,
upcoming: CreatedUpcomingIssueListItem,
overdue: CreatedOverdueIssueListItem,
completed: CreatedCompletedIssueListItem,
},
};
const issuesList = widgetStats.issues;
return (
<>
<div className="h-full">
{isLoading ? (
<Loader className="space-y-4 mt-7">
<Loader.Item height="25px" />
<Loader.Item height="25px" />
<Loader.Item height="25px" />
<Loader.Item height="25px" />
</Loader>
) : issuesList.length > 0 ? (
<>
<div className="mt-7 border-b-[0.5px] border-custom-border-200 grid grid-cols-12 gap-1 text-xs text-custom-text-300 pb-1">
<h6
className={cn("pl-1 flex items-center gap-1 col-span-7", {
"col-span-11": type === "assigned" && tab === "completed",
"col-span-9": type === "created" && tab === "completed",
})}
>
Work items
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium rounded-xl px-2 flex items-center text-center justify-center">
{widgetStats.count}
</span>
</h6>
<h6 className="text-center col-span-1">Priority</h6>
{["upcoming", "pending"].includes(tab) && <h6 className="text-center col-span-2">Due date</h6>}
{tab === "overdue" && <h6 className="text-center col-span-2">Due by</h6>}
{type === "assigned" && tab !== "completed" && <h6 className="text-center col-span-2">Blocked by</h6>}
{type === "created" && <h6 className="text-center col-span-2">Assigned to</h6>}
</div>
<div className="pb-3 mt-2">
{issuesList.map((issue) => {
const IssueListItem = ISSUE_LIST_ITEM[type][tab];
if (!IssueListItem) return null;
return (
<IssueListItem
key={issue.id}
issueId={issue.id}
workspaceSlug={workspaceSlug}
onClick={handleIssuePeekOverview}
/>
);
})}
</div>
</>
) : (
<div className="h-full grid place-items-center my-6">
{type === "assigned" && <AssignedIssuesEmptyState type={tab} />}
{type === "created" && <CreatedIssuesEmptyState type={tab} />}
</div>
)}
</div>
{!isLoading && issuesList.length > 0 && (
<Link
href={`/${workspaceSlug}/workspace-views/${type}/${filterParams}`}
className={cn(
getButtonStyling("link-primary", "sm"),
"w-min my-3 mx-auto py-1 px-2 text-xs hover:bg-custom-primary-100/20"
)}
>
View all work items
</Link>
)}
</>
);
};

View file

@ -1,61 +0,0 @@
import { observer } from "mobx-react";
import { Tab } from "@headlessui/react";
// helpers
import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@plane/constants";
import { TIssuesListTypes } from "@plane/types";
import { cn } from "@/helpers/common.helper";
// types
// constants
type Props = {
durationFilter: EDurationFilters;
selectedTab: TIssuesListTypes;
};
export const TabsList: React.FC<Props> = observer((props) => {
const { durationFilter, selectedTab } = props;
const tabsList = durationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab);
return (
<Tab.List
as="div"
className="relative border-[0.5px] border-custom-border-200 rounded bg-custom-background-80 p-[1px] grid"
style={{
gridTemplateColumns: `repeat(${tabsList.length}, 1fr)`,
}}
>
<div
className={cn(
"absolute top-1/2 left-[1px] bg-custom-background-100 rounded-[3px] transition-all duration-500 ease-in-out",
{
// right shadow
"shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== tabsList.length - 1,
// left shadow
"shadow-[-2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== 0,
}
)}
style={{
height: "calc(100% - 2px)",
width: `calc(${100 / tabsList.length}% - 1px)`,
transform: `translate(${selectedTabIndex * 100}%, -50%)`,
}}
/>
{tabsList.map((tab) => (
<Tab
key={tab.key}
className={cn(
"relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500",
{
"text-custom-text-100": selectedTab === tab.key,
"hover:text-custom-text-300": selectedTab !== tab.key,
}
)}
>
<span className="scale-110">{tab.label}</span>
</Tab>
))}
</Tab.List>
);
});

View file

@ -1,24 +0,0 @@
"use client";
// ui
import { Loader } from "@plane/ui";
export const AssignedIssuesWidgetLoader = () => (
<Loader className="bg-custom-background-100 p-6 rounded-xl">
<div className="flex items-center justify-between gap-2">
<Loader.Item height="17px" width="35%" />
<Loader.Item height="17px" width="10%" />
</div>
<div className="mt-6 space-y-7">
<Loader.Item height="29px" />
<Loader.Item height="17px" width="10%" />
</div>
<div className="mt-11 space-y-10">
<Loader.Item height="11px" width="35%" />
<Loader.Item height="11px" width="45%" />
<Loader.Item height="11px" width="55%" />
<Loader.Item height="11px" width="40%" />
<Loader.Item height="11px" width="60%" />
</div>
</Loader>
);

View file

@ -1 +0,0 @@
export * from "./loader";

View file

@ -1,17 +0,0 @@
"use client";
// ui
import { Loader } from "@plane/ui";
export const IssuesByPriorityWidgetLoader = () => (
<Loader className="bg-custom-background-100 rounded-xl p-6">
<Loader.Item height="17px" width="35%" />
<div className="flex items-center gap-1 h-full">
<Loader.Item height="119px" width="14%" />
<Loader.Item height="119px" width="26%" />
<Loader.Item height="119px" width="36%" />
<Loader.Item height="119px" width="18%" />
<Loader.Item height="119px" width="6%" />
</div>
</Loader>
);

View file

@ -1,24 +0,0 @@
"use client";
import range from "lodash/range";
// ui
import { Loader } from "@plane/ui";
export const IssuesByStateGroupWidgetLoader = () => (
<Loader className="bg-custom-background-100 rounded-xl p-6">
<Loader.Item height="17px" width="35%" />
<div className="flex items-center justify-between gap-32 mt-12 pl-6">
<div className="w-1/2 grid place-items-center">
<div className="rounded-full overflow-hidden relative flex-shrink-0 h-[184px] w-[184px]">
<Loader.Item height="184px" width="184px" />
<div className="absolute h-[100px] w-[100px] top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-custom-background-100 rounded-full" />
</div>
</div>
<div className="w-1/2 space-y-7 flex-shrink-0">
{range(5).map((index) => (
<Loader.Item key={index} height="11px" width="100%" />
))}
</div>
</div>
</Loader>
);

View file

@ -1,31 +0,0 @@
// components
import { TWidgetKeys } from "@plane/types";
import { AssignedIssuesWidgetLoader } from "./assigned-issues";
import { IssuesByPriorityWidgetLoader } from "./issues-by-priority";
import { IssuesByStateGroupWidgetLoader } from "./issues-by-state-group";
import { OverviewStatsWidgetLoader } from "./overview-stats";
import { RecentActivityWidgetLoader } from "./recent-activity";
import { RecentCollaboratorsWidgetLoader } from "./recent-collaborators";
import { RecentProjectsWidgetLoader } from "./recent-projects";
// types
type Props = {
widgetKey: TWidgetKeys;
};
export const WidgetLoader: React.FC<Props> = (props) => {
const { widgetKey } = props;
const loaders = {
overview_stats: <OverviewStatsWidgetLoader />,
assigned_issues: <AssignedIssuesWidgetLoader />,
created_issues: <AssignedIssuesWidgetLoader />,
issues_by_state_groups: <IssuesByStateGroupWidgetLoader />,
issues_by_priority: <IssuesByPriorityWidgetLoader />,
recent_activity: <RecentActivityWidgetLoader />,
recent_projects: <RecentProjectsWidgetLoader />,
recent_collaborators: <RecentCollaboratorsWidgetLoader />,
};
return loaders[widgetKey];
};

View file

@ -1,16 +0,0 @@
"use client";
import range from "lodash/range";
// ui
import { Loader } from "@plane/ui";
export const OverviewStatsWidgetLoader = () => (
<Loader className="bg-custom-background-100 rounded-xl py-6 grid grid-cols-4 gap-36 px-12">
{range(4).map((index) => (
<div key={index} className="space-y-3">
<Loader.Item height="11px" width="50%" />
<Loader.Item height="15px" />
</div>
))}
</Loader>
);

View file

@ -1,22 +0,0 @@
"use client";
import range from "lodash/range";
// ui
import { Loader } from "@plane/ui";
export const RecentActivityWidgetLoader = () => (
<Loader className="bg-custom-background-100 rounded-xl p-6 space-y-6">
<Loader.Item height="17px" width="35%" />
{range(7).map((index) => (
<div key={index} className="flex items-start gap-3.5">
<div className="flex-shrink-0">
<Loader.Item height="16px" width="16px" />
</div>
<div className="space-y-3 flex-shrink-0 w-full">
<Loader.Item height="15px" width="70%" />
<Loader.Item height="11px" width="10%" />
</div>
</div>
))}
</Loader>
);

View file

@ -1,20 +0,0 @@
"use client";
import range from "lodash/range";
// ui
import { Loader } from "@plane/ui";
export const RecentCollaboratorsWidgetLoader = () => (
<>
{range(8).map((index) => (
<Loader key={index} className="bg-custom-background-100 rounded-xl px-6 pb-12">
<div className="space-y-11 flex flex-col items-center">
<div className="rounded-full overflow-hidden h-[69px] w-[69px]">
<Loader.Item height="69px" width="69px" />
</div>
<Loader.Item height="11px" width="70%" />
</div>
</Loader>
))}
</>
);

View file

@ -1,22 +0,0 @@
"use client";
import range from "lodash/range";
// ui
import { Loader } from "@plane/ui";
export const RecentProjectsWidgetLoader = () => (
<Loader className="bg-custom-background-100 rounded-xl p-6 space-y-6">
<Loader.Item height="17px" width="35%" />
{range(5).map((index) => (
<div key={index} className="flex items-center gap-6">
<div className="flex-shrink-0">
<Loader.Item height="60px" width="60px" />
</div>
<div className="space-y-3 flex-shrink-0 w-full">
<Loader.Item height="17px" width="42%" />
<Loader.Item height="23px" width="10%" />
</div>
</div>
))}
</Loader>
);

View file

@ -1,100 +0,0 @@
import { useEffect } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { TOverviewStatsWidgetResponse } from "@plane/types";
// hooks
import { Card, ECardSpacing } from "@plane/ui";
import { WidgetLoader } from "@/components/dashboard/widgets";
import { cn } from "@/helpers/common.helper";
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { useDashboard } from "@/hooks/store";
// components
// helpers
// types
export type WidgetProps = {
dashboardId: string;
workspaceSlug: string;
};
const WIDGET_KEY = "overview_stats";
export const OverviewStatsWidget: React.FC<WidgetProps> = observer((props) => {
const { dashboardId, workspaceSlug } = props;
// store hooks
const { fetchWidgetStats, getWidgetStats } = useDashboard();
// derived values
const widgetStats = getWidgetStats<TOverviewStatsWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
const today = renderFormattedPayloadDate(new Date());
const STATS_LIST = [
{
key: "assigned",
title: "Work items assigned",
count: widgetStats?.assigned_issues_count,
link: `/${workspaceSlug}/workspace-views/assigned`,
},
{
key: "overdue",
title: "Work items overdue",
count: widgetStats?.pending_issues_count,
link: `/${workspaceSlug}/workspace-views/assigned/?state_group=backlog,unstarted,started&target_date=${today};before`,
},
{
key: "created",
title: "Work items created",
count: widgetStats?.created_issues_count,
link: `/${workspaceSlug}/workspace-views/created`,
},
{
key: "completed",
title: "Work items completed",
count: widgetStats?.completed_issues_count,
link: `/${workspaceSlug}/workspace-views/assigned?state_group=completed`,
},
];
useEffect(() => {
fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return (
<Card
spacing={ECardSpacing.SM}
className="flex-row grid lg:grid-cols-4 md:grid-cols-2 sm:grid-cols-2 grid-cols-2 space-y-0 p-0.5
[&>div>a>div]:border-r
[&>div:last-child>a>div]:border-0
[&>div>a>div]:border-custom-border-200
[&>div:nth-child(2)>a>div]:border-0
[&>div:nth-child(2)>a>div]:lg:border-r
"
>
{STATS_LIST.map((stat, index) => (
<div
key={stat.key}
className={cn(
`w-full flex flex-col gap-2 hover:bg-custom-background-80`,
index === 0 ? "rounded-l-md" : "",
index === STATS_LIST.length - 1 ? "rounded-r-md" : "",
index === 1 ? "rounded-tr-xl lg:rounded-[0px]" : "",
index == 2 ? "rounded-bl-xl lg:rounded-[0px]" : ""
)}
>
<Link href={stat.link} className="py-4 duration-300 rounded-[10px] w-full ">
<div className={`relative flex pl-10 sm:pl-20 md:pl-20 lg:pl-20 items-center`}>
<div>
<h5 className="font-semibold text-xl">{stat.count}</h5>
<p className="text-custom-text-300 text-sm xl:text-base">{stat.title}</p>
</div>
</div>
</Link>
</div>
))}
</Card>
);
});

View file

@ -1,109 +0,0 @@
"use client";
import { useEffect } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { History } from "lucide-react";
// types
import { TRecentActivityWidgetResponse } from "@plane/types";
// components
import { Card, Avatar, getButtonStyling } from "@plane/ui";
import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core";
import { RecentActivityEmptyState, WidgetLoader, WidgetProps } from "@/components/dashboard/widgets";
// helpers
import { cn } from "@/helpers/common.helper";
import { calculateTimeAgo } from "@/helpers/date-time.helper";
import { getFileURL } from "@/helpers/file.helper";
// hooks
import { useDashboard, useUser } from "@/hooks/store";
const WIDGET_KEY = "recent_activity";
export const RecentActivityWidget: React.FC<WidgetProps> = observer((props) => {
const { dashboardId, workspaceSlug } = props;
// store hooks
const { data: currentUser } = useUser();
// derived values
const { fetchWidgetStats, getWidgetStats } = useDashboard();
const widgetStats = getWidgetStats<TRecentActivityWidgetResponse[]>(workspaceSlug, dashboardId, WIDGET_KEY);
const redirectionLink = `/${workspaceSlug}/profile/${currentUser?.id}/activity`;
useEffect(() => {
fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return (
<Card>
<Link href={redirectionLink} className="text-lg font-semibold text-custom-text-300 hover:underline mb-4">
Your work item activities
</Link>
{widgetStats.length > 0 ? (
<div className="mt-4 space-y-6">
{widgetStats.map((activity) => (
<div key={activity.id} className="flex gap-5">
<div className="flex-shrink-0">
{activity.field ? (
activity.new_value === "restore" ? (
<History className="h-3.5 w-3.5 text-custom-text-200" />
) : (
<div className="flex h-6 w-6 justify-center">
<ActivityIcon activity={activity} />
</div>
)
) : activity.actor_detail.avatar_url && activity.actor_detail.avatar_url !== "" ? (
<Avatar
src={getFileURL(activity.actor_detail.avatar_url)}
name={activity.actor_detail.display_name}
size={24}
className="h-full w-full rounded-full object-cover"
/>
) : (
<div className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white">
{activity.actor_detail.is_bot
? activity.actor_detail.first_name.charAt(0)
: activity.actor_detail.display_name.charAt(0)}
</div>
)}
</div>
<div className="-mt-2 break-words">
<p className="inline text-sm text-custom-text-200">
<span className="font-medium text-custom-text-100">
{currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail?.display_name}{" "}
</span>
{activity.field ? (
<ActivityMessage activity={activity} showIssue />
) : (
<span>
created <IssueLink activity={activity} />
</span>
)}
</p>
<p className="text-xs text-custom-text-200 whitespace-nowrap">
{calculateTimeAgo(activity.created_at)}
</p>
</div>
</div>
))}
<Link
href={redirectionLink}
className={cn(
getButtonStyling("link-primary", "sm"),
"mx-auto w-min px-2 py-1 text-xs hover:bg-custom-primary-100/20"
)}
>
View all
</Link>
</div>
) : (
<div className="grid h-full place-items-center">
<RecentActivityEmptyState />
</div>
)}
</Card>
);
});

View file

@ -1,154 +0,0 @@
"use client";
import { useState } from "react";
import sortBy from "lodash/sortBy";
import { observer } from "mobx-react";
import Link from "next/link";
import useSWR from "swr";
// types
import { TRecentCollaboratorsWidgetResponse } from "@plane/types";
// ui
import { Avatar } from "@plane/ui";
// helpers
import { getFileURL } from "@/helpers/file.helper";
// hooks
import { useDashboard, useMember, useUser } from "@/hooks/store";
// components
import { WidgetLoader } from "../loaders";
type CollaboratorListItemProps = {
issueCount: number;
userId: string;
workspaceSlug: string;
};
const CollaboratorListItem: React.FC<CollaboratorListItemProps> = observer((props) => {
const { issueCount, userId, workspaceSlug } = props;
// store hooks
const { data: currentUser } = useUser();
const { getUserDetails } = useMember();
// derived values
const userDetails = getUserDetails(userId);
const isCurrentUser = userId === currentUser?.id;
if (!userDetails || userDetails.is_bot) return null;
return (
<Link href={`/${workspaceSlug}/profile/${userId}`} className="group text-center">
<div className="flex justify-center">
<Avatar
src={getFileURL(userDetails.avatar_url)}
name={userDetails.display_name}
size={69}
className="!text-3xl !font-medium"
showTooltip={false}
/>
</div>
<h6 className="mt-6 truncate text-xs font-semibold group-hover:underline">
{isCurrentUser ? "You" : userDetails?.display_name}
</h6>
<p className="mt-2 text-sm">
{issueCount} active work items{issueCount > 1 ? "s" : ""}
</p>
</Link>
);
});
type CollaboratorsListProps = {
dashboardId: string;
searchQuery?: string;
workspaceSlug: string;
};
const WIDGET_KEY = "recent_collaborators";
export const CollaboratorsList: React.FC<CollaboratorsListProps> = (props) => {
const { dashboardId, searchQuery = "", workspaceSlug } = props;
// state
const [visibleItems, setVisibleItems] = useState(16);
const [isExpanded, setIsExpanded] = useState(false);
// store hooks
const { fetchWidgetStats } = useDashboard();
const { getUserDetails } = useMember();
const { data: currentUser } = useUser();
const { data: widgetStats } = useSWR(
workspaceSlug && dashboardId ? `WIDGET_STATS_${workspaceSlug}_${dashboardId}` : null,
workspaceSlug && dashboardId
? () =>
fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY,
})
: null
) as {
data: TRecentCollaboratorsWidgetResponse[] | undefined;
};
if (!widgetStats)
return (
<div className="mt-7 mb-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 gap-2 gap-y-8">
<WidgetLoader widgetKey={WIDGET_KEY} />
</div>
);
const sortedStats = sortBy(widgetStats, [(user) => user?.user_id !== currentUser?.id]);
const filteredStats = sortedStats.filter((user) => {
if (!user) return false;
const userDetails = getUserDetails(user?.user_id);
if (!userDetails || userDetails.is_bot) return false;
const { display_name, first_name, last_name } = userDetails;
const searchLower = searchQuery.toLowerCase();
return (
display_name?.toLowerCase().includes(searchLower) ||
first_name?.toLowerCase().includes(searchLower) ||
last_name?.toLowerCase().includes(searchLower)
);
});
// Update the displayedStats to always use the visibleItems limit
const handleLoadMore = () => {
setVisibleItems((prev) => {
const newValue = prev + 16;
if (newValue >= filteredStats.length) {
setIsExpanded(true);
return filteredStats.length;
}
return newValue;
});
};
const handleHide = () => {
setVisibleItems(16);
setIsExpanded(false);
};
const displayedStats = filteredStats.slice(0, visibleItems);
return (
<>
<div className="mt-7 mb-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 gap-2 gap-y-8">
{displayedStats?.map((user) => (
<CollaboratorListItem
key={user?.user_id}
issueCount={user?.active_issue_count}
userId={user?.user_id}
workspaceSlug={workspaceSlug}
/>
))}
</div>
{filteredStats.length > visibleItems && !isExpanded && (
<div className="py-4 flex justify-center items-center text-sm font-medium" onClick={handleLoadMore}>
<div className="text-custom-primary-90 hover:text-custom-primary-100 transition-all cursor-pointer">
Load more
</div>
</div>
)}
{isExpanded && (
<div className="py-4 flex justify-center items-center text-sm font-medium" onClick={handleHide}>
<div className="text-custom-primary-90 hover:text-custom-primary-100 transition-all cursor-pointer">Hide</div>
</div>
)}
</>
);
};

View file

@ -1 +0,0 @@
export * from "./root";

View file

@ -1,36 +0,0 @@
import { useState } from "react";
import { Search } from "lucide-react";
// types
import { Card } from "@plane/ui";
import { WidgetProps } from "@/components/dashboard/widgets";
// components
import { CollaboratorsList } from "./collaborators-list";
export const RecentCollaboratorsWidget: React.FC<WidgetProps> = (props) => {
const { dashboardId, workspaceSlug } = props;
// states
const [searchQuery, setSearchQuery] = useState("");
return (
<Card>
<div className="flex flex-col sm:flex-row items-start justify-between mb-6">
<div>
<h4 className="text-lg font-semibold text-custom-text-300">Collaborators</h4>
<p className="mt-2 text-xs font-medium text-custom-text-300">
View and find all members you collaborate with across projects
</p>
</div>
<div className="mt-5 sm:mt-0 flex min-w-full md:min-w-72 items-center justify-start gap-2 rounded-md border border-custom-border-200 px-2.5 py-1.5 placeholder:text-custom-text-400">
<Search className="h-3.5 w-3.5 text-custom-text-400" />
<input
className="w-full border-none bg-transparent text-sm focus:outline-none"
placeholder="Search for collaborators"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
<CollaboratorsList dashboardId={dashboardId} searchQuery={searchQuery} workspaceSlug={workspaceSlug} />
</Card>
);
};

View file

@ -1,134 +0,0 @@
"use client";
import { useEffect } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { Plus } from "lucide-react";
// plane types
import { PROJECT_BACKGROUND_COLORS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { TRecentProjectsWidgetResponse } from "@plane/types";
// plane ui
import { Avatar, AvatarGroup, Card } from "@plane/ui";
// components
import { Logo } from "@/components/common";
import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets";
// constants
// helpers
import { getFileURL } from "@/helpers/file.helper";
// hooks
import {
useEventTracker,
useDashboard,
useProject,
useCommandPalette,
useUserPermissions,
useMember,
} from "@/hooks/store";
// plane web constants
const WIDGET_KEY = "recent_projects";
type ProjectListItemProps = {
projectId: string;
workspaceSlug: string;
};
const ProjectListItem: React.FC<ProjectListItemProps> = observer((props) => {
const { projectId, workspaceSlug } = props;
// store hooks
const { getProjectById } = useProject();
const { getUserDetails } = useMember();
// derived values
const projectDetails = getProjectById(projectId);
const randomBgColor = PROJECT_BACKGROUND_COLORS[Math.floor(Math.random() * PROJECT_BACKGROUND_COLORS.length)];
if (!projectDetails) return null;
return (
<Link href={`/${workspaceSlug}/projects/${projectId}/issues`} className="group flex items-center gap-8">
<div
className={`grid h-[3.375rem] w-[3.375rem] flex-shrink-0 place-items-center rounded border border-transparent ${randomBgColor}`}
>
<div className="grid h-7 w-7 place-items-center">
<Logo logo={projectDetails.logo_props} size={20} />
</div>
</div>
<div className="flex-grow truncate">
<h6 className="truncate text-sm font-medium text-custom-text-300 group-hover:text-custom-text-100 group-hover:underline">
{projectDetails.name}
</h6>
<div className="mt-2">
<AvatarGroup>
{projectDetails.members?.map((memberId) => {
const userDetails = getUserDetails(memberId);
if (!userDetails) return null;
return (
<Avatar key={userDetails.id} src={getFileURL(userDetails.avatar_url)} name={userDetails.display_name} />
);
})}
</AvatarGroup>
</div>
</div>
</Link>
);
});
export const RecentProjectsWidget: React.FC<WidgetProps> = observer((props) => {
const { dashboardId, workspaceSlug } = props;
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { fetchWidgetStats, getWidgetStats } = useDashboard();
// derived values
const widgetStats = getWidgetStats<TRecentProjectsWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
const canCreateProject = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
useEffect(() => {
fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return (
<Card>
<Link
href={`/${workspaceSlug}/projects`}
className="text-lg font-semibold text-custom-text-300 hover:underline mb-4"
>
Recent projects
</Link>
<div className="mt-4 space-y-8">
{canCreateProject && (
<button
type="button"
className="group flex items-center gap-8"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setTrackElement("Sidebar");
toggleCreateProjectModal(true);
}}
>
<div className="grid h-[3.375rem] w-[3.375rem] flex-shrink-0 place-items-center rounded border border-dashed border-custom-primary-60 bg-custom-primary-100/20 text-custom-primary-100">
<Plus className="h-6 w-6" />
</div>
<p className="text-sm font-medium text-custom-text-300 group-hover:text-custom-text-100 group-hover:underline">
Create new project
</p>
</button>
)}
{widgetStats.map((projectId) => (
<ProjectListItem key={projectId} projectId={projectId} workspaceSlug={workspaceSlug} />
))}
</div>
</Card>
);
});

View file

@ -47,105 +47,19 @@ const paramsToKey = (params: any) => {
return `${layoutKey}_${projectKey}_${stateGroupKey}_${stateKey}_${priorityKey}_${assigneesKey}_${mentionsKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${subscriberKey}`;
};
const myIssuesParamsToKey = (params: any) => {
const { assignees, created_by, labels, priority, state_group, subscriber, start_date, target_date } = params;
let assigneesKey = assignees ? assignees.split(",") : [];
let createdByKey = created_by ? created_by.split(",") : [];
let stateGroupKey = state_group ? state_group.split(",") : [];
let subscriberKey = subscriber ? subscriber.split(",") : [];
let priorityKey = priority ? priority.split(",") : [];
let labelsKey = labels ? labels.split(",") : [];
const startDateKey = start_date ?? "";
const targetDateKey = target_date ?? "";
const type = params?.type ? params.type.toUpperCase() : "NULL";
const groupBy = params?.group_by ? params.group_by.toUpperCase() : "NULL";
const orderBy = params?.order_by ? params.order_by.toUpperCase() : "NULL";
// sorting each keys in ascending order
assigneesKey = assigneesKey.sort().join("_");
createdByKey = createdByKey.sort().join("_");
stateGroupKey = stateGroupKey.sort().join("_");
subscriberKey = subscriberKey.sort().join("_");
priorityKey = priorityKey.sort().join("_");
labelsKey = labelsKey.sort().join("_");
return `${assigneesKey}_${createdByKey}_${stateGroupKey}_${subscriberKey}_${priorityKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}`;
};
export const CURRENT_USER = "CURRENT_USER";
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
export const USER_WORKSPACES_LIST = "USER_WORKSPACES_LIST";
export const WORKSPACE_DETAILS = (workspaceSlug: string) => `WORKSPACE_DETAILS_${workspaceSlug.toUpperCase()}`;
export const WORKSPACE_MEMBERS = (workspaceSlug: string) => `WORKSPACE_MEMBERS_${workspaceSlug.toUpperCase()}`;
export const WORKSPACE_MEMBERS_ME = (workspaceSlug: string) => `WORKSPACE_MEMBERS_ME${workspaceSlug.toUpperCase()}`;
export const WORKSPACE_INVITATIONS = (workspaceSlug: string) => `WORKSPACE_INVITATIONS_${workspaceSlug.toString()}`;
export const WORKSPACE_INVITATION = (invitationId: string) => `WORKSPACE_INVITATION_${invitationId}`;
export const LAST_ACTIVE_WORKSPACE_AND_PROJECTS = "LAST_ACTIVE_WORKSPACE_AND_PROJECTS";
export const PROJECTS_LIST = (
workspaceSlug: string,
params: {
is_favorite: "all" | boolean;
}
) => {
if (!params) return `PROJECTS_LIST_${workspaceSlug.toUpperCase()}`;
return `PROJECTS_LIST_${workspaceSlug.toUpperCase()}_${params.is_favorite.toString().toUpperCase()}`;
};
export const PROJECT_DETAILS = (projectId: string) => `PROJECT_DETAILS_${projectId.toUpperCase()}`;
export const PROJECT_MEMBERS = (projectId: string) => `PROJECT_MEMBERS_${projectId.toUpperCase()}`;
export const PROJECT_INVITATIONS = (projectId: string) => `PROJECT_INVITATIONS_${projectId.toString()}`;
export const PROJECT_ISSUES_LIST = (workspaceSlug: string, projectId: string) =>
`PROJECT_ISSUES_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}`;
export const PROJECT_ISSUES_LIST_WITH_PARAMS = (projectId: string, params?: any) => {
if (!params) return `PROJECT_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}`;
const paramsKey = paramsToKey(params);
return `PROJECT_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}_${paramsKey}`;
};
export const PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS = (projectId: string, params?: any) => {
if (!params) return `PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}`;
const paramsKey = paramsToKey(params);
return `PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}_${paramsKey}`;
};
export const PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS = (projectId: string, params?: any) => {
if (!params) return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}`;
const paramsKey = paramsToKey(params);
return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}_${paramsKey}`;
};
export const GLOBAL_VIEWS_LIST = (workspaceSlug: string) => `GLOBAL_VIEWS_LIST_${workspaceSlug.toUpperCase()}`;
export const GLOBAL_VIEW_DETAILS = (globalViewId: string) => `GLOBAL_VIEW_DETAILS_${globalViewId.toUpperCase()}`;
export const GLOBAL_VIEW_ISSUES = (globalViewId: string) => `GLOBAL_VIEW_ISSUES_${globalViewId.toUpperCase()}`;
export const PROJECT_ISSUES_DETAILS = (issueId: string) => `PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`;
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) => `PROJECT_ISSUES_PROPERTIES_${projectId.toUpperCase()}`;
export const PROJECT_ISSUES_COMMENTS = (issueId: string) => `PROJECT_ISSUES_COMMENTS_${issueId.toUpperCase()}`;
export const PROJECT_ISSUES_ACTIVITY = (issueId: string) => `PROJECT_ISSUES_ACTIVITY_${issueId.toUpperCase()}`;
export const PROJECT_ISSUE_BY_STATE = (projectId: string) => `PROJECT_ISSUE_BY_STATE_${projectId.toUpperCase()}`;
export const PROJECT_ISSUE_LABELS = (projectId: string) => `PROJECT_ISSUE_LABELS_${projectId.toUpperCase()}`;
export const WORKSPACE_LABELS = (workspaceSlug: string) => `WORKSPACE_LABELS_${workspaceSlug.toUpperCase()}`;
export const PROJECT_GITHUB_REPOSITORY = (projectId: string) => `PROJECT_GITHUB_REPOSITORY_${projectId.toUpperCase()}`;
// cycles
export const CYCLES_LIST = (projectId: string) => `CYCLE_LIST_${projectId.toUpperCase()}`;
export const INCOMPLETE_CYCLES_LIST = (projectId: string) => `INCOMPLETE_CYCLES_LIST_${projectId.toUpperCase()}`;
export const CURRENT_CYCLE_LIST = (projectId: string) => `CURRENT_CYCLE_LIST_${projectId.toUpperCase()}`;
export const UPCOMING_CYCLES_LIST = (projectId: string) => `UPCOMING_CYCLES_LIST_${projectId.toUpperCase()}`;
export const DRAFT_CYCLES_LIST = (projectId: string) => `DRAFT_CYCLES_LIST_${projectId.toUpperCase()}`;
export const COMPLETED_CYCLES_LIST = (projectId: string) => `COMPLETED_CYCLES_LIST_${projectId.toUpperCase()}`;
export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId.toUpperCase()}`;
export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => {
if (!params) return `CYCLE_ISSUES_WITH_PARAMS_${cycleId.toUpperCase()}`;
@ -153,47 +67,11 @@ export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => {
return `CYCLE_ISSUES_WITH_PARAMS_${cycleId.toUpperCase()}_${paramsKey.toUpperCase()}`;
};
export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId.toUpperCase()}`;
export const STATES_LIST = (projectId: string) => `STATES_LIST_${projectId.toUpperCase()}`;
export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug.toUpperCase()}`;
export const USER_ISSUES = (workspaceSlug: string, params: any) => {
const paramsKey = myIssuesParamsToKey(params);
return `USER_ISSUES_${workspaceSlug.toUpperCase()}_${paramsKey}`;
};
export const USER_ACTIVITY = (params: { cursor?: string }) => `USER_ACTIVITY_${params?.cursor}`;
export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) =>
`USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`;
export const USER_PROJECT_VIEW = (projectId: string) => `USER_PROJECT_VIEW_${projectId.toUpperCase()}`;
export const MODULE_LIST = (projectId: string) => `MODULE_LIST_${projectId.toUpperCase()}`;
export const MODULE_ISSUES = (moduleId: string) => `MODULE_ISSUES_${moduleId.toUpperCase()}`;
export const MODULE_ISSUES_WITH_PARAMS = (moduleId: string, params?: any) => {
if (!params) return `MODULE_ISSUES_WITH_PARAMS_${moduleId.toUpperCase()}`;
const paramsKey = paramsToKey(params);
return `MODULE_ISSUES_WITH_PARAMS_${moduleId.toUpperCase()}_${paramsKey.toUpperCase()}`;
};
export const MODULE_DETAILS = (moduleId: string) => `MODULE_DETAILS_${moduleId.toUpperCase()}`;
export const VIEWS_LIST = (projectId: string) => `VIEWS_LIST_${projectId.toUpperCase()}`;
export const VIEW_DETAILS = (viewId: string) => `VIEW_DETAILS_${viewId.toUpperCase()}`;
export const VIEW_ISSUES = (viewId: string, params: any) => {
if (!params) return `VIEW_ISSUES_${viewId.toUpperCase()}`;
const paramsKey = paramsToKey(params);
return `VIEW_ISSUES_${viewId.toUpperCase()}_${paramsKey.toUpperCase()}`;
};
// Issues
export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpperCase()}`;
export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId.toUpperCase()}`;
export const ISSUE_ATTACHMENTS = (issueId: string) => `ISSUE_ATTACHMENTS_${issueId.toUpperCase()}`;
export const ARCHIVED_ISSUE_DETAILS = (issueId: string) => `ARCHIVED_ISSUE_DETAILS_${issueId.toUpperCase()}`;
// integrations
export const APP_INTEGRATIONS = "APP_INTEGRATIONS";
@ -222,21 +100,6 @@ export const GITHUB_REPOSITORY_INFO = (workspaceSlug: string, repoName: string)
export const SLACK_CHANNEL_INFO = (workspaceSlug: string, projectId: string) =>
`SLACK_CHANNEL_INFO_${workspaceSlug.toString().toUpperCase()}_${projectId.toUpperCase()}`;
// Pages
export const RECENT_PAGES_LIST = (projectId: string) => `RECENT_PAGES_LIST_${projectId.toUpperCase()}`;
export const ALL_PAGES_LIST = (projectId: string) => `ALL_PAGES_LIST_${projectId.toUpperCase()}`;
export const ARCHIVED_PAGES_LIST = (projectId: string) => `ARCHIVED_PAGES_LIST_${projectId.toUpperCase}`;
export const FAVORITE_PAGES_LIST = (projectId: string) => `FAVORITE_PAGES_LIST_${projectId.toUpperCase()}`;
export const PRIVATE_PAGES_LIST = (projectId: string) => `PRIVATE_PAGES_LIST_${projectId.toUpperCase()}`;
export const SHARED_PAGES_LIST = (projectId: string) => `SHARED_PAGES_LIST_${projectId.toUpperCase()}`;
export const PAGE_DETAILS = (pageId: string) => `PAGE_DETAILS_${pageId.toUpperCase()}`;
export const PAGE_BLOCKS_LIST = (pageId: string) => `PAGE_BLOCK_LIST_${pageId.toUpperCase()}`;
export const PAGE_BLOCK_DETAILS = (pageId: string) => `PAGE_BLOCK_DETAILS_${pageId.toUpperCase()}`;
export const MY_PAGES_LIST = (pageId: string) => `MY_PAGE_LIST_${pageId}`;
// estimates
export const ESTIMATES_LIST = (projectId: string) => `ESTIMATES_LIST_${projectId.toUpperCase()}`;
export const ESTIMATE_DETAILS = (estimateId: string) => `ESTIMATE_DETAILS_${estimateId.toUpperCase()}`;
// profile
export const USER_PROFILE_DATA = (workspaceSlug: string, userId: string) =>
`USER_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`;
@ -249,19 +112,6 @@ export const USER_PROFILE_ACTIVITY = (
) => `USER_WORKSPACE_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}_${params?.cursor}`;
export const USER_PROFILE_PROJECT_SEGREGATION = (workspaceSlug: string, userId: string) =>
`USER_PROFILE_PROJECT_SEGREGATION_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`;
export const USER_PROFILE_ISSUES = (workspaceSlug: string, userId: string, params: any) => {
const paramsKey = myIssuesParamsToKey(params);
return `USER_PROFILE_ISSUES_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}_${paramsKey}`;
};
// reactions
export const ISSUE_REACTION_LIST = (workspaceSlug: string, projectId: string, issueId: string) =>
`ISSUE_REACTION_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}_${issueId.toUpperCase()}`;
export const COMMENT_REACTION_LIST = (workspaceSlug: string, projectId: string, commendId: string) =>
`COMMENT_REACTION_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}_${commendId.toUpperCase()}`;
// api-tokens
export const API_TOKENS_LIST = (workspaceSlug: string) => `API_TOKENS_LIST_${workspaceSlug.toUpperCase()}`;
export const API_TOKEN_DETAILS = (workspaceSlug: string, tokenId: string) =>
`API_TOKEN_DETAILS_${workspaceSlug.toUpperCase()}_${tokenId.toUpperCase()}`;

View file

@ -1,93 +0,0 @@
import useSWR from "swr";
// fetch keys
import { COMMENT_REACTION_LIST } from "@/constants/fetch-keys";
// services
import { groupReactions } from "@/helpers/emoji.helper";
import { useUser } from "@/hooks/store";
import { IssueReactionService } from "@/services/issue";
// helpers
// hooks
// services
const issueReactionService = new IssueReactionService();
const useCommentReaction: any = (
workspaceSlug?: string | string[] | null,
projectId?: string | string[] | null,
commendId?: string | string[] | null
) => {
const {
data: commentReactions,
mutate: mutateCommentReactions,
error,
} = useSWR(
workspaceSlug && projectId && commendId
? COMMENT_REACTION_LIST(workspaceSlug.toString(), projectId.toString(), commendId.toString())
: null,
workspaceSlug && projectId && commendId
? () =>
issueReactionService.listIssueCommentReactions(
workspaceSlug.toString(),
projectId.toString(),
commendId.toString()
)
: null
);
const { data: currentUser } = useUser();
const groupedReactions = groupReactions(commentReactions || [], "reaction");
/**
* @description Use this function to create user's reaction to an issue. This function will mutate the reactions state.
* @param {string} reaction
* @example handleReactionDelete("123") // 123 -> is emoji hexa-code
*/
const handleReactionCreate = async (reaction: string) => {
if (!workspaceSlug || !projectId || !commendId) return;
const data = await issueReactionService.createIssueCommentReaction(
workspaceSlug.toString(),
projectId.toString(),
commendId.toString(),
{ reaction }
);
mutateCommentReactions((prev: any) => [...(prev || []), data]);
};
/**
* @description Use this function to delete user's reaction from an issue. This function will mutate the reactions state.
* @param {string} reaction
* @example handleReactionDelete("123") // 123 -> is emoji hexa-code
*/
const handleReactionDelete = async (reaction: string) => {
if (!workspaceSlug || !projectId || !commendId) return;
mutateCommentReactions(
(prevData: any) => prevData?.filter((r: any) => r.actor !== currentUser?.id || r.reaction !== reaction) || [],
false
);
await issueReactionService.deleteIssueCommentReaction(
workspaceSlug.toString(),
projectId.toString(),
commendId.toString(),
reaction
);
mutateCommentReactions();
};
return {
isLoading: !commentReactions && !error,
commentReactions,
groupedReactions,
handleReactionCreate,
handleReactionDelete,
mutateCommentReactions,
} as const;
};
export default useCommentReaction;

View file

@ -1,63 +0,0 @@
import React, { useCallback, useEffect } from "react";
// plane helpers
import { useOutsideClickDetector } from "@plane/hooks";
/**
* Custom hook for dynamic dropdown position calculation.
* @param isOpen - Indicates whether the dropdown is open.
* @param handleClose - Callback to handle closing the dropdown.
* @param buttonRef - Ref object for the button triggering the dropdown.
* @param dropdownRef - Ref object for the dropdown element.
*/
const useDynamicDropdownPosition = (
isOpen: boolean,
handleClose: () => void,
buttonRef: React.RefObject<any>,
dropdownRef: React.RefObject<any>
) => {
const handlePosition = useCallback(() => {
const button = buttonRef.current;
const dropdown = dropdownRef.current;
if (!dropdown || !button) return;
const buttonRect = button.getBoundingClientRect();
const dropdownRect = dropdown.getBoundingClientRect();
const { innerHeight, innerWidth, scrollX, scrollY } = window;
let top: number = buttonRect.bottom + scrollY;
if (top + dropdownRect.height > innerHeight) top = innerHeight - dropdownRect.height;
let left: number = buttonRect.left + scrollX + (buttonRect.width - dropdownRect.width) / 2;
if (left + dropdownRect.width > innerWidth) left = innerWidth - dropdownRect.width;
dropdown.style.top = `${Math.max(top, 5)}px`;
dropdown.style.left = `${Math.max(left, 5)}px`;
}, [buttonRef, dropdownRef]);
useEffect(() => {
if (isOpen) handlePosition();
}, [handlePosition, isOpen]);
useOutsideClickDetector(dropdownRef, () => {
if (isOpen) handleClose();
});
const handleResize = useCallback(() => {
if (isOpen) {
handlePosition();
}
}, [handlePosition, isOpen]);
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [isOpen, handleResize]);
};
export default useDynamicDropdownPosition;

View file

@ -1,14 +0,0 @@
import { useEffect, useState } from "react";
const useURLHash = () => {
const [hashValue, setHashValue] = useState<string>();
useEffect(() => {
const hash = window.location.hash?.split("#")[1];
setHashValue(hash);
}, []);
return hashValue;
};
export default useURLHash;

View file

@ -1,41 +0,0 @@
import { useEffect, ReactNode, FC } from "react";
import { observer } from "mobx-react";
// hooks
import { useUser } from "@/hooks/store";
declare global {
interface Window {
$crisp: unknown[];
CRISP_WEBSITE_ID: unknown;
}
}
export interface ICrispWrapper {
children: ReactNode;
}
const CrispWrapper: FC<ICrispWrapper> = observer((props) => {
const { children } = props;
const { data: user } = useUser();
useEffect(() => {
if (typeof window && user?.email && process.env.NEXT_PUBLIC_CRISP_ID) {
window.$crisp = [];
window.CRISP_WEBSITE_ID = process.env.NEXT_PUBLIC_CRISP_ID;
(function () {
const d = document;
const s = d.createElement("script");
s.src = "https://client.crisp.chat/l.js";
s.async = true;
d.getElementsByTagName("head")[0].appendChild(s);
window.$crisp.push(["set", "user:email", [user.email]]);
window.$crisp.push(["do", "chat:hide"]);
window.$crisp.push(["do", "chat:close"]);
})();
}
}, [user?.email]);
return <>{children}</>;
});
export default CrispWrapper;

View file

@ -1,10 +1,4 @@
import DOMPurify from "isomorphic-dompurify";
import {
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
VIEW_ISSUES,
} from "@/constants/fetch-keys";
export const addSpaceIfCamelCase = (str: string) => {
if (str === undefined || str === null) return "";
@ -150,29 +144,6 @@ export const objToQueryParams = (obj: any) => {
return params.toString();
};
export const getFetchKeysForIssueMutation = (options: {
cycleId?: string | string[];
moduleId?: string | string[];
viewId?: string | string[];
projectId: string;
viewGanttParams: any;
ganttParams: any;
}) => {
const { cycleId, moduleId, viewId, projectId, viewGanttParams, ganttParams } = options;
const ganttFetchKey = cycleId
? { ganttFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), ganttParams) }
: moduleId
? { ganttFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), ganttParams) }
: viewId
? { ganttFetchKey: VIEW_ISSUES(viewId.toString(), viewGanttParams) }
: { ganttFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", ganttParams) };
return {
...ganttFetchKey,
};
};
/**
* @returns {boolean} true if searchQuery is substring of text in the same order, false otherwise
* @description Returns true if searchQuery is substring of text in the same order, false otherwise

View file

@ -19,7 +19,6 @@
"@atlaskit/pragmatic-drag-and-drop": "^1.1.3",
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.3.0",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@blueprintjs/popover2": "^1.13.3",
"@headlessui/react": "^1.7.3",
"@intercom/messenger-js-sdk": "^0.0.12",
"@plane/constants": "*",
@ -65,7 +64,6 @@
"smooth-scroll-into-view-if-needed": "^2.0.2",
"swr": "^2.1.3",
"tailwind-merge": "^2.0.0",
"use-debounce": "^9.0.4",
"use-font-face-observer": "^1.2.2",
"uuid": "^9.0.0",
"zxcvbn": "^4.4.2"