fix: update loading state handling in CalendarPage and DutyList components
- Set isLoading to false in CalendarPage to prevent unnecessary loading indicators. - Removed the loading spinner from CalendarHeader and adjusted rendering logic in DutyList to show a placeholder when data is not yet loaded for the month. - Enhanced tests in DutyList to verify behavior when data is empty and when data is loading, ensuring accurate user feedback during data fetching.
This commit is contained in:
@@ -145,7 +145,7 @@ export function CalendarPage({ isAllowed, initDataRaw }: CalendarPageProps) {
|
|||||||
>
|
>
|
||||||
<CalendarHeader
|
<CalendarHeader
|
||||||
month={currentMonth}
|
month={currentMonth}
|
||||||
isLoading={loading}
|
isLoading={false}
|
||||||
disabled={navDisabled}
|
disabled={navDisabled}
|
||||||
onGoToToday={handleGoToToday}
|
onGoToToday={handleGoToToday}
|
||||||
onRefresh={retry}
|
onRefresh={retry}
|
||||||
|
|||||||
@@ -30,13 +30,6 @@ export interface CalendarHeaderProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderSpinner = () => (
|
|
||||||
<span
|
|
||||||
className="block size-4 shrink-0 rounded-full border-2 border-transparent border-t-accent motion-reduce:animate-none animate-spin"
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
function isCurrentMonth(month: Date): boolean {
|
function isCurrentMonth(month: Date): boolean {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return (
|
return (
|
||||||
@@ -82,16 +75,6 @@ export function CalendarHeader({
|
|||||||
aria-atomic="true"
|
aria-atomic="true"
|
||||||
>
|
>
|
||||||
{monthName(monthIndex)} {year}
|
{monthName(monthIndex)} {year}
|
||||||
{isLoading && (
|
|
||||||
<span
|
|
||||||
role="status"
|
|
||||||
aria-live="polite"
|
|
||||||
aria-label={t("loading")}
|
|
||||||
className="flex items-center"
|
|
||||||
>
|
|
||||||
<HeaderSpinner />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</h1>
|
</h1>
|
||||||
{showToday && (
|
{showToday && (
|
||||||
<button
|
<button
|
||||||
@@ -116,10 +99,7 @@ export function CalendarHeader({
|
|||||||
disabled={disabled || isLoading}
|
disabled={disabled || isLoading}
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
>
|
>
|
||||||
<RefreshCwIcon
|
<RefreshCwIcon className="size-5" aria-hidden />
|
||||||
className={cn("size-5", isLoading && "motion-reduce:animate-none animate-spin")}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -35,17 +35,27 @@ describe("DutyList", () => {
|
|||||||
useAppStore.getState().setCurrentMonth(new Date(2025, 1, 1)); // Feb 2025
|
useAppStore.getState().setCurrentMonth(new Date(2025, 1, 1)); // Feb 2025
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders no duties message when duties empty", () => {
|
it("renders no duties message when duties empty and data loaded for month", () => {
|
||||||
useAppStore.getState().setDuties([]);
|
useAppStore.getState().setDuties([]);
|
||||||
|
useAppStore.getState().batchUpdate({ dataForMonthKey: "2025-02" });
|
||||||
render(<DutyList />);
|
render(<DutyList />);
|
||||||
expect(screen.getByText(/No duties this month/i)).toBeInTheDocument();
|
expect(screen.getByText(/No duties this month/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders quiet placeholder when data not yet loaded for month", () => {
|
||||||
|
useAppStore.getState().setDuties([]);
|
||||||
|
useAppStore.getState().batchUpdate({ dataForMonthKey: null });
|
||||||
|
render(<DutyList />);
|
||||||
|
expect(screen.queryByText(/No duties this month/i)).not.toBeInTheDocument();
|
||||||
|
expect(document.querySelector('[aria-busy="true"]')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders duty with full_name and time range", () => {
|
it("renders duty with full_name and time range", () => {
|
||||||
useAppStore.getState().setDuties([
|
useAppStore.getState().setDuties([
|
||||||
duty("Иванов", "2025-02-25T09:00:00Z", "2025-02-25T18:00:00Z"),
|
duty("Иванов", "2025-02-25T09:00:00Z", "2025-02-25T18:00:00Z"),
|
||||||
]);
|
]);
|
||||||
useAppStore.getState().setCurrentMonth(new Date(2025, 1, 1));
|
useAppStore.getState().setCurrentMonth(new Date(2025, 1, 1));
|
||||||
|
useAppStore.getState().batchUpdate({ dataForMonthKey: "2025-02" });
|
||||||
render(<DutyList />);
|
render(<DutyList />);
|
||||||
expect(screen.getByText("Иванов")).toBeInTheDocument();
|
expect(screen.getByText("Иванов")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -58,6 +68,7 @@ describe("DutyList", () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
useAppStore.getState().setCurrentMonth(new Date(2025, 2, 1));
|
useAppStore.getState().setCurrentMonth(new Date(2025, 2, 1));
|
||||||
|
useAppStore.getState().batchUpdate({ dataForMonthKey: "2025-03" });
|
||||||
render(<DutyList />);
|
render(<DutyList />);
|
||||||
expect(screen.getAllByText("Alice").length).toBeGreaterThanOrEqual(1);
|
expect(screen.getAllByText("Alice").length).toBeGreaterThanOrEqual(1);
|
||||||
expect(document.querySelector('a[href^="tel:"]')).toBeInTheDocument();
|
expect(document.querySelector('a[href^="tel:"]')).toBeInTheDocument();
|
||||||
|
|||||||
@@ -64,9 +64,15 @@ export interface DutyListProps {
|
|||||||
export function DutyList({ scrollMarginTop = 268, className }: DutyListProps) {
|
export function DutyList({ scrollMarginTop = 268, className }: DutyListProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const listRef = useRef<HTMLDivElement>(null);
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
const { currentMonth, duties, loading } = useAppStore(
|
const { currentMonth, duties, dataForMonthKey } = useAppStore(
|
||||||
useShallow((s) => ({ currentMonth: s.currentMonth, duties: s.duties, loading: s.loading }))
|
useShallow((s) => ({
|
||||||
|
currentMonth: s.currentMonth,
|
||||||
|
duties: s.duties,
|
||||||
|
dataForMonthKey: s.dataForMonthKey,
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
const monthKey = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, "0")}`;
|
||||||
|
const hasDataForMonth = dataForMonthKey === monthKey;
|
||||||
|
|
||||||
const { filtered, dates, dutiesByDateKey } = useMemo(() => {
|
const { filtered, dates, dutiesByDateKey } = useMemo(() => {
|
||||||
const filteredList = duties.filter((d) => d.event_type === "duty");
|
const filteredList = duties.filter((d) => d.event_type === "duty");
|
||||||
@@ -105,7 +111,6 @@ export function DutyList({ scrollMarginTop = 268, className }: DutyListProps) {
|
|||||||
const id = setInterval(() => setNow(new Date()), 60_000);
|
const id = setInterval(() => setNow(new Date()), 60_000);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, []);
|
}, []);
|
||||||
const monthKey = `${currentMonth.getFullYear()}-${currentMonth.getMonth()}`;
|
|
||||||
const scrolledForMonthRef = useRef<string | null>(null);
|
const scrolledForMonthRef = useRef<string | null>(null);
|
||||||
const prevScrollMarginTopRef = useRef<number>(scrollMarginTop);
|
const prevScrollMarginTopRef = useRef<number>(scrollMarginTop);
|
||||||
const prevMonthKeyRef = useRef<string>(monthKey);
|
const prevMonthKeyRef = useRef<string>(monthKey);
|
||||||
@@ -139,17 +144,25 @@ export function DutyList({ scrollMarginTop = 268, className }: DutyListProps) {
|
|||||||
});
|
});
|
||||||
}, [filtered, dates.length, scrollMarginTop, monthKey]);
|
}, [filtered, dates.length, scrollMarginTop, monthKey]);
|
||||||
|
|
||||||
|
if (!hasDataForMonth) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("min-h-[120px]", className)}
|
||||||
|
role="status"
|
||||||
|
aria-busy="true"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-label={t("loading")}
|
||||||
|
>
|
||||||
|
<span className="sr-only">{t("loading")}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
if (filtered.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col gap-1", className)}>
|
<div className={cn("flex flex-col gap-1", className)}>
|
||||||
{loading ? (
|
<p className="text-sm text-muted m-0">{t("duty.none_this_month")}</p>
|
||||||
<p className="text-sm text-muted m-0">{t("loading")}</p>
|
<p className="text-xs text-muted m-0">{t("duty.none_this_month_hint")}</p>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p className="text-sm text-muted m-0">{t("duty.none_this_month")}</p>
|
|
||||||
<p className="text-xs text-muted m-0">{t("duty.none_this_month_hint")}</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user