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:
parent
6be3f0ea73
commit
245167e8aa
42 changed files with 1 additions and 2413 deletions
|
|
@ -1,2 +0,0 @@
|
||||||
export * from "./widgets";
|
|
||||||
export * from "./project-empty-state";
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./duration-filter";
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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";
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./issues";
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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";
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export * from "./issue-list-item";
|
|
||||||
export * from "./issues-list";
|
|
||||||
export * from "./tabs-list";
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./loader";
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
@ -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];
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
@ -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>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./root";
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -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}`;
|
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 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 = (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 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_DETAILS = (projectId: string) => `PROJECT_DETAILS_${projectId.toUpperCase()}`;
|
||||||
|
|
||||||
export const PROJECT_MEMBERS = (projectId: string) => `PROJECT_MEMBERS_${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()}`;
|
export const PROJECT_GITHUB_REPOSITORY = (projectId: string) => `PROJECT_GITHUB_REPOSITORY_${projectId.toUpperCase()}`;
|
||||||
|
|
||||||
// cycles
|
// 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) => {
|
export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => {
|
||||||
if (!params) return `CYCLE_ISSUES_WITH_PARAMS_${cycleId.toUpperCase()}`;
|
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()}`;
|
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_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
|
// Issues
|
||||||
export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpperCase()}`;
|
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
|
// integrations
|
||||||
export const APP_INTEGRATIONS = "APP_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) =>
|
export const SLACK_CHANNEL_INFO = (workspaceSlug: string, projectId: string) =>
|
||||||
`SLACK_CHANNEL_INFO_${workspaceSlug.toString().toUpperCase()}_${projectId.toUpperCase()}`;
|
`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
|
// profile
|
||||||
export const USER_PROFILE_DATA = (workspaceSlug: string, userId: string) =>
|
export const USER_PROFILE_DATA = (workspaceSlug: string, userId: string) =>
|
||||||
`USER_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`;
|
`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}`;
|
) => `USER_WORKSPACE_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}_${params?.cursor}`;
|
||||||
export const USER_PROFILE_PROJECT_SEGREGATION = (workspaceSlug: string, userId: string) =>
|
export const USER_PROFILE_PROJECT_SEGREGATION = (workspaceSlug: string, userId: string) =>
|
||||||
`USER_PROFILE_PROJECT_SEGREGATION_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`;
|
`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
|
// api-tokens
|
||||||
export const API_TOKENS_LIST = (workspaceSlug: string) => `API_TOKENS_LIST_${workspaceSlug.toUpperCase()}`;
|
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()}`;
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
import DOMPurify from "isomorphic-dompurify";
|
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) => {
|
export const addSpaceIfCamelCase = (str: string) => {
|
||||||
if (str === undefined || str === null) return "";
|
if (str === undefined || str === null) return "";
|
||||||
|
|
@ -150,29 +144,6 @@ export const objToQueryParams = (obj: any) => {
|
||||||
return params.toString();
|
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
|
* @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
|
* @description Returns true if searchQuery is substring of text in the same order, false otherwise
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.1.3",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.1.3",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.3.0",
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.3.0",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||||
"@blueprintjs/popover2": "^1.13.3",
|
|
||||||
"@headlessui/react": "^1.7.3",
|
"@headlessui/react": "^1.7.3",
|
||||||
"@intercom/messenger-js-sdk": "^0.0.12",
|
"@intercom/messenger-js-sdk": "^0.0.12",
|
||||||
"@plane/constants": "*",
|
"@plane/constants": "*",
|
||||||
|
|
@ -65,7 +64,6 @@
|
||||||
"smooth-scroll-into-view-if-needed": "^2.0.2",
|
"smooth-scroll-into-view-if-needed": "^2.0.2",
|
||||||
"swr": "^2.1.3",
|
"swr": "^2.1.3",
|
||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
"use-debounce": "^9.0.4",
|
|
||||||
"use-font-face-observer": "^1.2.2",
|
"use-font-face-observer": "^1.2.2",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"zxcvbn": "^4.4.2"
|
"zxcvbn": "^4.4.2"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue