Improve desktop layouts and loading states
This commit is contained in:
parent
205fd7252d
commit
5a877880ae
@ -930,7 +930,10 @@ export default function ProcurementPage() {
|
||||
) : (
|
||||
<section className={`app-surface rounded-2xl p-4 ${desktopWideMode ? "lg:min-h-0 lg:flex-1 lg:flex lg:flex-col lg:overflow-hidden" : ""}`}>
|
||||
{/* ── header ── */}
|
||||
<AutoRevealStickyHeader className="rounded-b-xl">
|
||||
<AutoRevealStickyHeader
|
||||
className="rounded-b-xl"
|
||||
disableOnDesktop={desktopWideMode}
|
||||
>
|
||||
<div className="mb-4 flex min-w-0 flex-nowrap items-center justify-between gap-2">
|
||||
<div className="shrink-0">
|
||||
<p className="app-text-muted text-sm font-semibold uppercase tracking-wide">Закупки</p>
|
||||
|
||||
@ -98,6 +98,7 @@ function RequestsPageContent() {
|
||||
pendingDecisionCount: h.pendingDecisionCount,
|
||||
periodFromFilter: h.periodFromFilter,
|
||||
periodToFilter: h.periodToFilter,
|
||||
refreshing: h.refreshing,
|
||||
search: h.search,
|
||||
scopeCounts: h.scopeCounts,
|
||||
statusFilter: h.statusFilter,
|
||||
@ -136,7 +137,10 @@ function RequestsPageContent() {
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<AutoRevealStickyHeader className="rounded-b-xl">
|
||||
<AutoRevealStickyHeader
|
||||
className="rounded-b-xl"
|
||||
disableOnDesktop={desktopWideMode}
|
||||
>
|
||||
<RequestListControls
|
||||
actions={requestControlsActions}
|
||||
desktopWideMode={desktopWideMode}
|
||||
|
||||
@ -7448,7 +7448,7 @@ function TasksPageContent() {
|
||||
desktopWideMode={desktopWideMode}
|
||||
onDesktopWideModeChange={changeDesktopWideMode}
|
||||
>
|
||||
<div className={`${desktopWideMode ? "w-full lg:flex lg:min-h-0 lg:flex-1 lg:flex-col" : "mx-auto w-full max-w-[1600px] xl:flex xl:h-[calc(100dvh-5.5rem)] xl:min-h-0 xl:flex-col"}`}>
|
||||
<div className={`${desktopWideMode ? "w-full lg:flex lg:min-h-0 lg:flex-1 lg:flex-col" : "mx-auto w-full max-w-[1600px]"}`}>
|
||||
<div className={`min-h-0 gap-4 ${desktopWideMode ? "lg:flex lg:flex-1" : "xl:flex xl:flex-1"}`}>
|
||||
<TaskBoardNavigator
|
||||
boards={boards}
|
||||
@ -7474,9 +7474,12 @@ function TasksPageContent() {
|
||||
/>
|
||||
|
||||
<div className={`${mobileBoardOpen ? "flex" : "hidden"} relative min-h-0 min-w-0 flex-1 flex-col gap-4 ${desktopWideMode ? "lg:flex lg:h-full" : ""}`}>
|
||||
{board ? (
|
||||
{board && (requestedBoardId <= 0 || board.id === requestedBoardId) ? (
|
||||
<>
|
||||
<AutoRevealStickyHeader className="rounded-b-2xl">
|
||||
<AutoRevealStickyHeader
|
||||
className="rounded-b-2xl"
|
||||
disableOnDesktop={desktopWideMode}
|
||||
>
|
||||
<section className="app-surface relative z-[35] shrink-0 overflow-visible rounded-2xl p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
@ -8047,12 +8050,32 @@ function TasksPageContent() {
|
||||
</TaskCardMenuPopover>
|
||||
) : null}
|
||||
</>
|
||||
) : requestedBoardId > 0 && !error ? (
|
||||
<section
|
||||
className="app-surface-muted flex h-full min-h-32 flex-1 items-center justify-center rounded-xl border border-[var(--border-subtle)]"
|
||||
aria-live="polite"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div className="max-w-xs text-center">
|
||||
<Loader2 size={28} className="mx-auto mb-3 animate-spin text-sky-500" />
|
||||
<p className="text-sm font-semibold text-[var(--foreground)]">Загрузка доски...</p>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<section className="app-surface-muted flex h-full min-h-0 flex-1 items-center justify-center rounded-xl border border-dashed border-[var(--border-subtle)]">
|
||||
<div className="max-w-xs text-center">
|
||||
<Kanban size={28} className="app-text-muted mx-auto mb-3" />
|
||||
<p className="text-sm font-semibold text-[var(--foreground)]">Выберите доску</p>
|
||||
<p className="app-text-muted mt-1 text-xs">Доска откроется справа от списка</p>
|
||||
{error && requestedBoardId > 0 ? (
|
||||
<>
|
||||
<p className="text-sm font-semibold text-[var(--danger-foreground)]">Не удалось загрузить доску</p>
|
||||
<p className="app-text-muted mt-1 text-xs">{error}</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Kanban size={28} className="app-text-muted mx-auto mb-3" />
|
||||
<p className="text-sm font-semibold text-[var(--foreground)]">Выберите доску</p>
|
||||
<p className="app-text-muted mt-1 text-xs">Доска откроется справа от списка</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
@ -11,7 +11,7 @@ import { HorizontalChipScroller } from "@/components/ui/HorizontalChipScroller";
|
||||
import { ModulePrimaryAction } from "@/components/ui/ModulePrimaryAction";
|
||||
import { displayUserName } from "@/lib/shared";
|
||||
import type { User } from "@/types/api";
|
||||
import { Filter, Maximize2, Minimize2, Plus, Search, Zap } from "lucide-react";
|
||||
import { Filter, Loader2, Maximize2, Minimize2, Plus, Search, Zap } from "lucide-react";
|
||||
|
||||
type RequestListControlsState = {
|
||||
activeFiltersCount: number;
|
||||
@ -25,6 +25,7 @@ type RequestListControlsState = {
|
||||
pendingDecisionCount: number;
|
||||
periodFromFilter: string;
|
||||
periodToFilter: string;
|
||||
refreshing: boolean;
|
||||
search: string;
|
||||
scopeCounts: RequestScopeCounts;
|
||||
statusFilter: string;
|
||||
@ -81,6 +82,13 @@ export function RequestListControls({
|
||||
<div className="mb-4 flex min-w-0 flex-nowrap items-center justify-between gap-2">
|
||||
<div className="flex shrink-0 items-center gap-3">
|
||||
<p className="app-text-muted text-sm font-semibold uppercase tracking-wide">Заявления</p>
|
||||
{state.refreshing ? (
|
||||
<Loader2
|
||||
size={14}
|
||||
className="app-text-muted animate-spin"
|
||||
aria-label="Обновление списка заявлений"
|
||||
/>
|
||||
) : null}
|
||||
{state.pendingDecisionCount > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -505,13 +505,18 @@ export default function TaskBoardNavigator({
|
||||
<>
|
||||
<aside
|
||||
ref={menusRef}
|
||||
className={`flex min-h-0 flex-col overflow-visible transition-[width] duration-200 lg:overflow-hidden ${
|
||||
className={`flex min-h-0 flex-col overflow-visible transition-[width] duration-200 ${
|
||||
wideMode ? "lg:overflow-hidden" : ""
|
||||
} ${
|
||||
compactNavigator ? "bg-transparent" : "app-surface rounded-2xl"
|
||||
} ${
|
||||
wideMode ? (compactNavigator ? "lg:w-[4.75rem]" : "lg:w-[24rem]") : ""
|
||||
} ${className}`}
|
||||
>
|
||||
<AutoRevealStickyHeader className={compactNavigator ? "" : "rounded-2xl"}>
|
||||
<AutoRevealStickyHeader
|
||||
className={compactNavigator ? "" : "rounded-2xl"}
|
||||
disableOnDesktop={wideMode}
|
||||
>
|
||||
<div
|
||||
className={`flex min-w-0 items-center gap-2 pb-0 ${
|
||||
compactNavigator ? "justify-center p-3" : "justify-between p-4"
|
||||
@ -637,7 +642,8 @@ export default function TaskBoardNavigator({
|
||||
</div>
|
||||
</CompactFilterPanel>
|
||||
|
||||
<HorizontalChipScroller className="mb-4" ariaLabel="Группы досок">
|
||||
<div className="mb-4 flex min-w-0 items-start gap-2">
|
||||
<HorizontalChipScroller className="min-w-0 flex-1" ariaLabel="Группы досок">
|
||||
{[
|
||||
{ key: "all" as const, label: "Все", stats: groupStats.all },
|
||||
{ key: "ungrouped" as const, label: "Без группы", stats: groupStats.ungrouped },
|
||||
@ -715,15 +721,6 @@ export default function TaskBoardNavigator({
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCreateGroup}
|
||||
className="app-pill inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full transition"
|
||||
title="Создать группу"
|
||||
aria-label="Создать группу досок"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
{hiddenGroups.length > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
@ -739,11 +736,23 @@ export default function TaskBoardNavigator({
|
||||
</button>
|
||||
) : null}
|
||||
</HorizontalChipScroller>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCreateGroup}
|
||||
className="app-pill inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full transition"
|
||||
title="Создать группу"
|
||||
aria-label="Создать группу досок"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</AutoRevealStickyHeader>
|
||||
|
||||
<div className={`tasks-column-scroll pb-4 lg:min-h-0 lg:flex-1 lg:overflow-y-auto lg:overscroll-y-contain ${compactNavigator ? "px-2 pt-2" : "px-4"}`}>
|
||||
<div className={`tasks-column-scroll pb-4 ${
|
||||
wideMode ? "lg:min-h-0 lg:flex-1 lg:overflow-y-auto lg:overscroll-y-contain" : ""
|
||||
} ${compactNavigator ? "px-2 pt-2" : "px-4"}`}>
|
||||
{visibleBoards.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{visibleBoards.map((board) => (
|
||||
|
||||
@ -7,12 +7,14 @@ import { useMobileNavPlacement } from "@/contexts/MobileNavPlacementContext";
|
||||
type AutoRevealStickyHeaderProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
disableOnDesktop?: boolean;
|
||||
surface?: "solid" | "transparent";
|
||||
};
|
||||
|
||||
export function AutoRevealStickyHeader({
|
||||
children,
|
||||
className = "",
|
||||
disableOnDesktop = false,
|
||||
surface = "solid",
|
||||
}: AutoRevealStickyHeaderProps) {
|
||||
const { mobileNavPlacement } = useMobileNavPlacement();
|
||||
@ -76,7 +78,11 @@ export function AutoRevealStickyHeader({
|
||||
<div
|
||||
data-auto-reveal-header="true"
|
||||
onFocusCapture={() => setVisible(true)}
|
||||
className={`sticky z-30 transition-[transform,box-shadow] duration-200 ease-out will-change-transform lg:static lg:translate-y-0 lg:bg-transparent lg:shadow-none ${
|
||||
className={`sticky z-30 transition-[transform,box-shadow] duration-200 ease-out will-change-transform ${
|
||||
disableOnDesktop
|
||||
? "lg:static lg:translate-y-0 lg:bg-transparent lg:shadow-none"
|
||||
: "lg:top-14"
|
||||
} ${
|
||||
surface === "transparent" ? "bg-transparent" : "bg-[var(--surface-primary-solid)]"
|
||||
} ${
|
||||
mobileNavPlacement === "top" ? "top-10" : "top-0"
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
Barcode,
|
||||
CalendarArrowDown,
|
||||
CalendarArrowUp,
|
||||
CalendarRange,
|
||||
Check,
|
||||
Clock3,
|
||||
LogIn,
|
||||
@ -35,8 +36,12 @@ export type SortIconType =
|
||||
| "identifier"
|
||||
| "updated"
|
||||
| "created"
|
||||
| "created-asc"
|
||||
| "created-desc"
|
||||
| "date-asc"
|
||||
| "date-desc"
|
||||
| "period-asc"
|
||||
| "period-desc"
|
||||
| "access-start-asc"
|
||||
| "access-start-desc"
|
||||
| "access-end-asc"
|
||||
@ -60,8 +65,12 @@ const SORT_ICONS: Record<SortIconType, LucideIcon> = {
|
||||
identifier: Barcode,
|
||||
updated: Clock3,
|
||||
created: PackagePlus,
|
||||
"created-asc": PackagePlus,
|
||||
"created-desc": PackagePlus,
|
||||
"date-asc": CalendarArrowUp,
|
||||
"date-desc": CalendarArrowDown,
|
||||
"period-asc": CalendarRange,
|
||||
"period-desc": CalendarRange,
|
||||
"access-start-asc": LogIn,
|
||||
"access-start-desc": LogIn,
|
||||
"access-end-asc": LogOut,
|
||||
@ -75,6 +84,10 @@ const SORT_ICONS: Record<SortIconType, LucideIcon> = {
|
||||
};
|
||||
|
||||
const SORT_DIRECTIONS: Partial<Record<SortIconType, LucideIcon>> = {
|
||||
"created-asc": ArrowUp,
|
||||
"created-desc": ArrowDown,
|
||||
"period-asc": ArrowUp,
|
||||
"period-desc": ArrowDown,
|
||||
"access-start-asc": ArrowUp,
|
||||
"access-start-desc": ArrowDown,
|
||||
"access-end-asc": ArrowUp,
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
import {
|
||||
type CSSProperties,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
@ -9,6 +11,13 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
type MouseDragState = {
|
||||
pointerId: number;
|
||||
startScrollLeft: number;
|
||||
startX: number;
|
||||
dragged: boolean;
|
||||
};
|
||||
|
||||
type HorizontalChipScrollerProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
@ -25,6 +34,9 @@ export function HorizontalChipScroller({
|
||||
fadeColor = "var(--surface-primary)",
|
||||
}: HorizontalChipScrollerProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const mouseDragRef = useRef<MouseDragState | null>(null);
|
||||
const suppressClickRef = useRef(false);
|
||||
const [isMouseDragging, setIsMouseDragging] = useState(false);
|
||||
const [scrollHints, setScrollHints] = useState({
|
||||
left: false,
|
||||
right: false,
|
||||
@ -59,6 +71,92 @@ export function HorizontalChipScroller({
|
||||
};
|
||||
}, [children, updateScrollHints]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
if (container.scrollWidth <= container.clientWidth + 1) return;
|
||||
|
||||
const rawDelta = Math.abs(event.deltaX) > Math.abs(event.deltaY)
|
||||
? event.deltaX
|
||||
: event.deltaY;
|
||||
if (rawDelta === 0) return;
|
||||
|
||||
const scale = event.deltaMode === WheelEvent.DOM_DELTA_LINE
|
||||
? 16
|
||||
: event.deltaMode === WheelEvent.DOM_DELTA_PAGE
|
||||
? container.clientWidth
|
||||
: 1;
|
||||
const delta = rawDelta * scale;
|
||||
const canScrollLeft = container.scrollLeft > 1;
|
||||
const canScrollRight =
|
||||
container.scrollLeft + container.clientWidth < container.scrollWidth - 1;
|
||||
|
||||
if ((delta < 0 && !canScrollLeft) || (delta > 0 && !canScrollRight)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
container.scrollLeft += delta;
|
||||
};
|
||||
|
||||
container.addEventListener("wheel", handleWheel, { passive: false });
|
||||
return () => container.removeEventListener("wheel", handleWheel);
|
||||
}, []);
|
||||
|
||||
const handlePointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (event.pointerType !== "mouse" || event.button !== 0) return;
|
||||
|
||||
mouseDragRef.current = {
|
||||
pointerId: event.pointerId,
|
||||
startScrollLeft: event.currentTarget.scrollLeft,
|
||||
startX: event.clientX,
|
||||
dragged: false,
|
||||
};
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
}, []);
|
||||
|
||||
const handlePointerMove = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
const drag = mouseDragRef.current;
|
||||
if (!drag || drag.pointerId !== event.pointerId) return;
|
||||
|
||||
const distance = event.clientX - drag.startX;
|
||||
if (!drag.dragged && Math.abs(distance) < 5) return;
|
||||
|
||||
if (!drag.dragged) {
|
||||
drag.dragged = true;
|
||||
suppressClickRef.current = true;
|
||||
setIsMouseDragging(true);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.currentTarget.scrollLeft = drag.startScrollLeft - distance;
|
||||
}, []);
|
||||
|
||||
const finishMouseDrag = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
const drag = mouseDragRef.current;
|
||||
if (!drag || drag.pointerId !== event.pointerId) return;
|
||||
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
mouseDragRef.current = null;
|
||||
setIsMouseDragging(false);
|
||||
|
||||
if (drag.dragged) {
|
||||
window.setTimeout(() => {
|
||||
suppressClickRef.current = false;
|
||||
}, 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClickCapture = useCallback((event: ReactMouseEvent<HTMLDivElement>) => {
|
||||
if (!suppressClickRef.current) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const fadeStyle = {
|
||||
"--chip-scroll-fade-color": fadeColor,
|
||||
} as CSSProperties;
|
||||
@ -72,8 +170,17 @@ export function HorizontalChipScroller({
|
||||
<div
|
||||
ref={containerRef}
|
||||
onScroll={updateScrollHints}
|
||||
className={`flex min-w-0 gap-2 overflow-x-auto overscroll-x-contain pb-1 touch-pan-x [scrollbar-width:none] [&>*]:shrink-0 [&::-webkit-scrollbar]:hidden ${contentClassName}`}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={finishMouseDrag}
|
||||
onPointerCancel={finishMouseDrag}
|
||||
onClickCapture={handleClickCapture}
|
||||
onDragStart={(event) => event.preventDefault()}
|
||||
className={`${
|
||||
isMouseDragging ? "cursor-grabbing select-none" : "cursor-grab"
|
||||
} flex min-w-0 gap-2 overflow-x-auto overscroll-x-contain pb-1 touch-pan-x [scrollbar-width:none] [&>*]:shrink-0 [&::-webkit-scrollbar]:hidden ${contentClassName}`}
|
||||
aria-label={ariaLabel}
|
||||
data-mouse-dragging={isMouseDragging ? "true" : "false"}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -62,11 +62,11 @@ export const requestTypeLabels: Record<string, string> = {
|
||||
};
|
||||
|
||||
export const orderingOptions = [
|
||||
{ value: "-created_at", label: "Сначала новые", icon: "date-desc" },
|
||||
{ value: "created_at", label: "Сначала старые", icon: "date-asc" },
|
||||
{ value: "-created_at", label: "Сначала новые", icon: "created-desc" },
|
||||
{ value: "created_at", label: "Сначала старые", icon: "created-asc" },
|
||||
{ value: "title", label: "По названию", icon: "alphabetical" },
|
||||
{ value: "date_from", label: "По периоду ↑", icon: "date-asc" },
|
||||
{ value: "-date_from", label: "По периоду ↓", icon: "date-desc" },
|
||||
{ value: "date_from", label: "По периоду ↑", icon: "period-asc" },
|
||||
{ value: "-date_from", label: "По периоду ↓", icon: "period-desc" },
|
||||
] as const;
|
||||
|
||||
export type RequestQuickScope = "" | "mine" | "addressed" | "decision";
|
||||
@ -190,12 +190,14 @@ export function useRequestsPage(_userId: number | null | undefined) {
|
||||
const [attachmentPreview, setAttachmentPreview] = useState<RequestAttachmentPreview | null>(null);
|
||||
const [detailsRequest, setDetailsRequest] = useState<Request | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const [actionSuccess, setActionSuccess] = useState<string | null>(null);
|
||||
const [busyKey, setBusyKey] = useState<string | null>(null);
|
||||
const [nextPage, setNextPage] = useState<number | null>(null);
|
||||
const requestsLoadedRef = useRef(false);
|
||||
|
||||
const departmentNameMap = useMemo(
|
||||
() => new Map((departments || []).map((d) => [d.id, d.name])),
|
||||
@ -238,10 +240,16 @@ export function useRequestsPage(_userId: number | null | undefined) {
|
||||
/* ── load data ── */
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const initialLoad = !requestsLoadedRef.current;
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
if (initialLoad) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
} else {
|
||||
setRefreshing(true);
|
||||
setActionError(null);
|
||||
}
|
||||
const response = await apiClient.getRequests(buildRequestParams(1));
|
||||
if (cancelled) return;
|
||||
setRequests(response.results || []);
|
||||
@ -250,10 +258,15 @@ export function useRequestsPage(_userId: number | null | undefined) {
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
console.error("Ошибка загрузки заявлений:", err);
|
||||
setError("Не удалось загрузить заявления");
|
||||
if (initialLoad) setError("Не удалось загрузить заявления");
|
||||
else setActionError("Не удалось обновить заявления");
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
if (!cancelled) {
|
||||
requestsLoadedRef.current = true;
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
@ -650,7 +663,7 @@ export function useRequestsPage(_userId: number | null | undefined) {
|
||||
/* UI */
|
||||
attachmentPreview, setAttachmentPreview,
|
||||
detailsRequest, setDetailsRequest,
|
||||
loading, loadingMore, error,
|
||||
loading, refreshing, loadingMore, error,
|
||||
actionError, actionSuccess,
|
||||
busyKey, nextPage,
|
||||
loadMoreRef,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user