diff --git a/web/components/dashboard/widgets/assigned-issues.tsx b/web/components/dashboard/widgets/assigned-issues.tsx index 1e031cacd..cd4115ac7 100644 --- a/web/components/dashboard/widgets/assigned-issues.tsx +++ b/web/components/dashboard/widgets/assigned-issues.tsx @@ -7,6 +7,7 @@ import { useDashboard } from "hooks/store"; // components import { DurationFilterDropdown, + IssuesErrorState, TabsList, WidgetIssuesList, WidgetLoader, @@ -26,10 +27,12 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { // states const [fetching, setFetching] = useState(false); // store hooks - const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); + const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } = + useDashboard(); // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(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 ?? []; @@ -73,74 +76,91 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); - if (!widgetDetails || !widgetStats) return ; + if ((!widgetDetails || !widgetStats) && !widgetStatsError) return ; return (
-
- - Assigned to you - - { - 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"; - + {widgetStatsError ? ( + handleUpdateFilters({ - duration: val, - tab: newTab, - }); - }} + duration: EDurationFilters.NONE, + tab: "pending", + }) + } /> -
- { - const newSelectedTab = tabsList[i]; - handleUpdateFilters({ tab: newSelectedTab?.key ?? "completed" }); - }} - className="h-full flex flex-col" - > -
- -
- - {tabsList.map((tab) => { - if (tab.key !== selectedTab) return null; + ) : ( + widgetStats && ( + <> +
+ + Assigned to you + + { + if (val === "custom" && customDates) { + handleUpdateFilters({ + duration: val, + custom_dates: customDates, + }); + return; + } - 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, + }); + }} + /> +
+ { + const newSelectedTab = tabsList[i]; + handleUpdateFilters({ tab: newSelectedTab?.key ?? "completed" }); + }} + className="h-full flex flex-col" + > +
+ +
+ + {tabsList.map((tab) => { + if (tab.key !== selectedTab) return null; + + return ( + + + + ); + })} + +
+ + ) + )}
); }); diff --git a/web/components/dashboard/widgets/created-issues.tsx b/web/components/dashboard/widgets/created-issues.tsx index d36260f21..2b2954018 100644 --- a/web/components/dashboard/widgets/created-issues.tsx +++ b/web/components/dashboard/widgets/created-issues.tsx @@ -7,6 +7,7 @@ import { useDashboard } from "hooks/store"; // components import { DurationFilterDropdown, + IssuesErrorState, TabsList, WidgetIssuesList, WidgetLoader, @@ -26,10 +27,12 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { // states const [fetching, setFetching] = useState(false); // store hooks - const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); + const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } = + useDashboard(); // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(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 ?? []; @@ -70,74 +73,91 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); - if (!widgetDetails || !widgetStats) return ; + if ((!widgetDetails || !widgetStats) && !widgetStatsError) return ; return (
-
- - Created by you - - { - 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"; - + {widgetStatsError ? ( + handleUpdateFilters({ - duration: val, - tab: newTab, - }); - }} + duration: EDurationFilters.NONE, + tab: "pending", + }) + } /> -
- { - const newSelectedTab = tabsList[i]; - handleUpdateFilters({ tab: newSelectedTab.key ?? "completed" }); - }} - className="h-full flex flex-col" - > -
- -
- - {tabsList.map((tab) => { - if (tab.key !== selectedTab) return null; + ) : ( + widgetStats && ( + <> +
+ + Created by you + + { + if (val === "custom" && customDates) { + handleUpdateFilters({ + duration: val, + custom_dates: customDates, + }); + return; + } - 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, + }); + }} + /> +
+ { + const newSelectedTab = tabsList[i]; + handleUpdateFilters({ tab: newSelectedTab.key ?? "completed" }); + }} + className="h-full flex flex-col" + > +
+ +
+ + {tabsList.map((tab) => { + if (tab.key !== selectedTab) return null; + + return ( + + + + ); + })} + +
+ + ) + )}
); }); diff --git a/web/components/dashboard/widgets/error-states/index.ts b/web/components/dashboard/widgets/error-states/index.ts new file mode 100644 index 000000000..bd8854f37 --- /dev/null +++ b/web/components/dashboard/widgets/error-states/index.ts @@ -0,0 +1 @@ +export * from "./issues"; diff --git a/web/components/dashboard/widgets/error-states/issues.tsx b/web/components/dashboard/widgets/error-states/issues.tsx new file mode 100644 index 000000000..6cfce13b4 --- /dev/null +++ b/web/components/dashboard/widgets/error-states/issues.tsx @@ -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) => { + const { isRefreshing, onClick } = props; + + return ( +
+
+
+ +
+

There was an error in fetching widget details

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