chore: error state for the issues widgets (#3934)

This commit is contained in:
Aaryan Khandelwal 2024-03-11 21:12:28 +05:30 committed by GitHub
parent 8c9d328c24
commit e3ac075ee2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 256 additions and 156 deletions

View File

@ -7,6 +7,7 @@ import { useDashboard } from "hooks/store";
// components // components
import { import {
DurationFilterDropdown, DurationFilterDropdown,
IssuesErrorState,
TabsList, TabsList,
WidgetIssuesList, WidgetIssuesList,
WidgetLoader, WidgetLoader,
@ -26,10 +27,12 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
// states // states
const [fetching, setFetching] = useState(false); const [fetching, setFetching] = useState(false);
// store hooks // store hooks
const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } =
useDashboard();
// derived values // derived values
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStats = getWidgetStats<TAssignedIssuesWidgetResponse>(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 selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE;
const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab);
const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? [];
@ -73,74 +76,91 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab);
if (!widgetDetails || !widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />; if ((!widgetDetails || !widgetStats) && !widgetStatsError) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return ( return (
<div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full hover:shadow-custom-shadow-4xl duration-300 flex flex-col min-h-96"> <div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full hover:shadow-custom-shadow-4xl duration-300 flex flex-col min-h-96">
<div className="flex items-center justify-between gap-2 p-6 pl-7"> {widgetStatsError ? (
<Link <IssuesErrorState
href={`/${workspaceSlug}/workspace-views/assigned/${filterParams}`} isRefreshing={fetching}
className="text-lg font-semibold text-custom-text-300 hover:underline" onClick={() =>
>
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({ handleUpdateFilters({
duration: val, duration: EDurationFilters.NONE,
tab: newTab, tab: "pending",
}); })
}} }
/> />
</div> ) : (
<Tab.Group widgetStats && (
as="div" <>
selectedIndex={selectedTabIndex} <div className="flex items-center justify-between gap-2 p-6 pl-7">
onChange={(i) => { <Link
const newSelectedTab = tabsList[i]; href={`/${workspaceSlug}/workspace-views/assigned/${filterParams}`}
handleUpdateFilters({ tab: newSelectedTab?.key ?? "completed" }); className="text-lg font-semibold text-custom-text-300 hover:underline"
}} >
className="h-full flex flex-col" Assigned to you
> </Link>
<div className="px-6"> <DurationFilterDropdown
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} /> customDates={selectedCustomDates}
</div> value={selectedDurationFilter}
<Tab.Panels as="div" className="h-full"> onChange={(val, customDates) => {
{tabsList.map((tab) => { if (val === "custom" && customDates) {
if (tab.key !== selectedTab) return null; handleUpdateFilters({
duration: val,
custom_dates: customDates,
});
return;
}
return ( if (val === selectedDurationFilter) return;
<Tab.Panel key={tab.key} as="div" className="h-full flex flex-col" static>
<WidgetIssuesList let newTab = selectedTab;
tab={tab.key} // switch to pending tab if target date is changed to none
type="assigned" if (val === "none" && selectedTab !== "completed") newTab = "pending";
workspaceSlug={workspaceSlug} // switch to upcoming tab if target date is changed to other than none
widgetStats={widgetStats} if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed")
isLoading={fetching} newTab = "upcoming";
/>
</Tab.Panel> handleUpdateFilters({
); duration: val,
})} tab: newTab,
</Tab.Panels> });
</Tab.Group> }}
/>
</div>
<Tab.Group
as="div"
selectedIndex={selectedTabIndex}
onChange={(i) => {
const newSelectedTab = tabsList[i];
handleUpdateFilters({ tab: newSelectedTab?.key ?? "completed" });
}}
className="h-full flex flex-col"
>
<div className="px-6">
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} />
</div>
<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>
</>
)
)}
</div> </div>
); );
}); });

View File

@ -7,6 +7,7 @@ import { useDashboard } from "hooks/store";
// components // components
import { import {
DurationFilterDropdown, DurationFilterDropdown,
IssuesErrorState,
TabsList, TabsList,
WidgetIssuesList, WidgetIssuesList,
WidgetLoader, WidgetLoader,
@ -26,10 +27,12 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
// states // states
const [fetching, setFetching] = useState(false); const [fetching, setFetching] = useState(false);
// store hooks // store hooks
const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } =
useDashboard();
// derived values // derived values
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStats = getWidgetStats<TCreatedIssuesWidgetResponse>(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 selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE;
const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab);
const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? [];
@ -70,74 +73,91 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab);
if (!widgetDetails || !widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />; if ((!widgetDetails || !widgetStats) && !widgetStatsError) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return ( return (
<div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full hover:shadow-custom-shadow-4xl duration-300 flex flex-col min-h-96"> <div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full hover:shadow-custom-shadow-4xl duration-300 flex flex-col min-h-96">
<div className="flex items-center justify-between gap-2 p-6 pl-7"> {widgetStatsError ? (
<Link <IssuesErrorState
href={`/${workspaceSlug}/workspace-views/created/${filterParams}`} isRefreshing={fetching}
className="text-lg font-semibold text-custom-text-300 hover:underline" onClick={() =>
>
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({ handleUpdateFilters({
duration: val, duration: EDurationFilters.NONE,
tab: newTab, tab: "pending",
}); })
}} }
/> />
</div> ) : (
<Tab.Group widgetStats && (
as="div" <>
selectedIndex={selectedTabIndex} <div className="flex items-center justify-between gap-2 p-6 pl-7">
onChange={(i) => { <Link
const newSelectedTab = tabsList[i]; href={`/${workspaceSlug}/workspace-views/created/${filterParams}`}
handleUpdateFilters({ tab: newSelectedTab.key ?? "completed" }); className="text-lg font-semibold text-custom-text-300 hover:underline"
}} >
className="h-full flex flex-col" Created by you
> </Link>
<div className="px-6"> <DurationFilterDropdown
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} /> customDates={selectedCustomDates}
</div> value={selectedDurationFilter}
<Tab.Panels as="div" className="h-full"> onChange={(val, customDates) => {
{tabsList.map((tab) => { if (val === "custom" && customDates) {
if (tab.key !== selectedTab) return null; handleUpdateFilters({
duration: val,
custom_dates: customDates,
});
return;
}
return ( if (val === selectedDurationFilter) return;
<Tab.Panel key={tab.key} as="div" className="h-full flex flex-col" static>
<WidgetIssuesList let newTab = selectedTab;
tab={tab.key} // switch to pending tab if target date is changed to none
type="created" if (val === "none" && selectedTab !== "completed") newTab = "pending";
workspaceSlug={workspaceSlug} // switch to upcoming tab if target date is changed to other than none
widgetStats={widgetStats} if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed")
isLoading={fetching} newTab = "upcoming";
/>
</Tab.Panel> handleUpdateFilters({
); duration: val,
})} tab: newTab,
</Tab.Panels> });
</Tab.Group> }}
/>
</div>
<Tab.Group
as="div"
selectedIndex={selectedTabIndex}
onChange={(i) => {
const newSelectedTab = tabsList[i];
handleUpdateFilters({ tab: newSelectedTab.key ?? "completed" });
}}
className="h-full flex flex-col"
>
<div className="px-6">
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} />
</div>
<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>
</>
)
)}
</div> </div>
); );
}); });

View File

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

View File

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

View File

@ -1,5 +1,6 @@
export * from "./dropdowns"; export * from "./dropdowns";
export * from "./empty-states"; export * from "./empty-states";
export * from "./error-states";
export * from "./issue-panels"; export * from "./issue-panels";
export * from "./loaders"; export * from "./loaders";
export * from "./assigned-issues"; export * from "./assigned-issues";

View File

@ -15,6 +15,8 @@ import {
} from "@plane/types"; } from "@plane/types";
export interface IDashboardStore { export interface IDashboardStore {
// error states
widgetStatsError: { [workspaceSlug: string]: Record<string, Record<TWidgetKeys, any | null>> };
// observables // observables
homeDashboardId: string | null; homeDashboardId: string | null;
widgetDetails: { [workspaceSlug: string]: Record<string, TWidget[]> }; widgetDetails: { [workspaceSlug: string]: Record<string, TWidget[]> };
@ -36,6 +38,7 @@ export interface IDashboardStore {
// computed actions // computed actions
getWidgetDetails: (workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => TWidget | undefined; getWidgetDetails: (workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => TWidget | undefined;
getWidgetStats: <T>(workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => T | undefined; getWidgetStats: <T>(workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => T | undefined;
getWidgetStatsError: (workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => any | null;
// actions // actions
fetchHomeDashboardWidgets: (workspaceSlug: string) => Promise<THomeDashboardResponse>; fetchHomeDashboardWidgets: (workspaceSlug: string) => Promise<THomeDashboardResponse>;
fetchWidgetStats: ( fetchWidgetStats: (
@ -58,6 +61,8 @@ export interface IDashboardStore {
} }
export class DashboardStore implements IDashboardStore { export class DashboardStore implements IDashboardStore {
// error states
widgetStatsError: { [workspaceSlug: string]: Record<string, Record<TWidgetKeys, any>> } = {};
// observables // observables
homeDashboardId: string | null = null; homeDashboardId: string | null = null;
widgetDetails: { [workspaceSlug: string]: Record<string, TWidget[]> } = {}; widgetDetails: { [workspaceSlug: string]: Record<string, TWidget[]> } = {};
@ -70,6 +75,8 @@ export class DashboardStore implements IDashboardStore {
constructor(_rootStore: RootStore) { constructor(_rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
// error states
widgetStatsError: observable,
// observables // observables
homeDashboardId: observable.ref, homeDashboardId: observable.ref,
widgetDetails: observable, widgetDetails: observable,
@ -93,7 +100,7 @@ export class DashboardStore implements IDashboardStore {
/** /**
* @description get home dashboard widgets * @description get home dashboard widgets
* @returns home dashboard widgets * @returns {TWidget[] | undefined}
*/ */
get homeDashboardWidgets() { get homeDashboardWidgets() {
const workspaceSlug = this.routerStore.workspaceSlug; const workspaceSlug = this.routerStore.workspaceSlug;
@ -104,10 +111,10 @@ export class DashboardStore implements IDashboardStore {
/** /**
* @description get widget details * @description get widget details
* @param workspaceSlug * @param {string} workspaceSlug
* @param dashboardId * @param {string} dashboardId
* @param widgetId * @param {TWidgetKeys} widgetKey
* @returns widget details * @returns {TWidget | undefined}
*/ */
getWidgetDetails = computedFn((workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => { getWidgetDetails = computedFn((workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => {
const widgets = this.widgetDetails?.[workspaceSlug]?.[dashboardId]; const widgets = this.widgetDetails?.[workspaceSlug]?.[dashboardId];
@ -117,20 +124,30 @@ export class DashboardStore implements IDashboardStore {
/** /**
* @description get widget stats * @description get widget stats
* @param workspaceSlug * @param {string} workspaceSlug
* @param dashboardId * @param {string} dashboardId
* @param widgetKey * @param {TWidgetKeys} widgetKey
* @returns widget stats * @returns {T | undefined}
*/ */
getWidgetStats = <T>(workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys): T | undefined => getWidgetStats = <T>(workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys): T | undefined =>
(this.widgetStats?.[workspaceSlug]?.[dashboardId]?.[widgetKey] as unknown as T) ?? undefined; (this.widgetStats?.[workspaceSlug]?.[dashboardId]?.[widgetKey] as unknown as T) ?? undefined;
/** /**
* @description fetch home dashboard details and widgets * @description get widget stats error
* @param workspaceSlug * @param {string} workspaceSlug
* @returns home dashboard response * @param {string} dashboardId
* @param {TWidgetKeys} widgetKey
* @returns {any | null}
*/ */
fetchHomeDashboardWidgets = async (workspaceSlug: string) => { getWidgetStatsError = (workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) =>
this.widgetStatsError?.[workspaceSlug]?.[dashboardId]?.[widgetKey] ?? null;
/**
* @description fetch home dashboard details and widgets
* @param {string} workspaceSlug
* @returns {Promise<THomeDashboardResponse>}
*/
fetchHomeDashboardWidgets = async (workspaceSlug: string): Promise<THomeDashboardResponse> => {
try { try {
const response = await this.dashboardService.getHomeDashboardWidgets(workspaceSlug); const response = await this.dashboardService.getHomeDashboardWidgets(workspaceSlug);
@ -151,27 +168,36 @@ export class DashboardStore implements IDashboardStore {
/** /**
* @description fetch widget stats * @description fetch widget stats
* @param workspaceSlug * @param {string} workspaceSlug
* @param dashboardId * @param {string} dashboardId
* @param widgetKey * @param {TWidgetStatsRequestParams} widgetKey
* @returns widget stats * @returns widget stats
*/ */
fetchWidgetStats = async (workspaceSlug: string, dashboardId: string, params: TWidgetStatsRequestParams) => fetchWidgetStats = async (workspaceSlug: string, dashboardId: string, params: TWidgetStatsRequestParams) =>
this.dashboardService.getWidgetStats(workspaceSlug, dashboardId, params).then((res) => { this.dashboardService
runInAction(() => { .getWidgetStats(workspaceSlug, dashboardId, params)
// @ts-ignore .then((res) => {
if (res.issues) this.issueStore.addIssue(res.issues); runInAction(() => {
set(this.widgetStats, [workspaceSlug, dashboardId, params.widget_key], res); // @ts-ignore
}); if (res.issues) this.issueStore.addIssue(res.issues);
set(this.widgetStats, [workspaceSlug, dashboardId, params.widget_key], res);
set(this.widgetStatsError, [workspaceSlug, dashboardId, params.widget_key], null);
});
return res;
})
.catch((error) => {
runInAction(() => {
set(this.widgetStatsError, [workspaceSlug, dashboardId, params.widget_key], error);
});
return res; throw error;
}); });
/** /**
* @description update dashboard widget * @description update dashboard widget
* @param dashboardId * @param {string} dashboardId
* @param widgetId * @param {string} widgetId
* @param data * @param {Partial<TWidget>} data
* @returns updated widget * @returns updated widget
*/ */
updateDashboardWidget = async ( updateDashboardWidget = async (
@ -209,9 +235,9 @@ export class DashboardStore implements IDashboardStore {
/** /**
* @description update dashboard widget filters * @description update dashboard widget filters
* @param dashboardId * @param {string} dashboardId
* @param widgetId * @param {string} widgetId
* @param data * @param {TWidgetFiltersFormData} data
* @returns updated widget * @returns updated widget
*/ */
updateDashboardWidgetFilters = async ( updateDashboardWidgetFilters = async (