F: Improve navigation tracking and loaders. Fix store utilization
Some checks are pending
Frontend CI / build (22.x) (push) Waiting to run
Frontend CI / notify-failure (push) Blocked by required conditions

This commit is contained in:
Ivan 2025-07-25 10:54:50 +03:00
parent 5d8cfe0b21
commit e00b8da6ee
22 changed files with 109 additions and 24 deletions

View File

@ -3,6 +3,7 @@ import { Outlet } from 'react-router';
import clsx from 'clsx';
import { ModalLoader } from '@/components/modal';
import { useBrowserNavigation } from '@/hooks/use-browser-navigation';
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/app-layout';
import { useDialogsStore } from '@/stores/dialogs';
@ -16,6 +17,8 @@ import { MutationErrors } from './mutation-errors';
import { Navigation } from './navigation';
export function ApplicationLayout() {
useBrowserNavigation();
const mainHeight = useMainHeight();
const viewportHeight = useViewportHeight();
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);

View File

@ -3,13 +3,21 @@ import { useDebounce } from 'use-debounce';
import { Loader } from '@/components/loader';
import { ModalBackdrop } from '@/components/modal/modal-backdrop';
import { useTransitionTracker } from '@/hooks/use-transition-delay';
import { useAppTransitionStore } from '@/stores/app-transition';
import { PARAMETER } from '@/utils/constants';
export function GlobalLoader() {
const navigation = useNavigation();
const isLoading = navigation.state === 'loading';
const [loadingDebounced] = useDebounce(isLoading, PARAMETER.navigationPopupDelay);
const isTransitioning = useTransitionTracker();
const isManualNav = useAppTransitionStore(state => state.isNavigating);
const isRouterLoading = navigation.state === 'loading';
const [loadingDebounced] = useDebounce(
isRouterLoading || isTransitioning || isManualNav,
PARAMETER.navigationPopupDelay
);
if (!loadingDebounced) {
return null;

View File

@ -26,7 +26,7 @@ export function PromptTemplatesPage() {
active: query.get('active')
});
const { isModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
useBlockNavigation(isModified);
return (

View File

@ -43,7 +43,7 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
const setGlobalLocation = useLibrarySearchStore(state => state.setLocation);
const isProcessing = useMutatingLibrary();
const { isModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
const { setOwner } = useSetOwner();
const { setLocation } = useSetLocation();

View File

@ -39,7 +39,7 @@ export function ToolbarItemCard({
}: ToolbarItemCardProps) {
const role = useRoleStore(state => state.role);
const router = useConceptNavigation();
const { isModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
const isProcessing = useMutatingLibrary();
const canSave = isModified && !isProcessing;

View File

@ -19,7 +19,7 @@ const SIDELIST_LAYOUT_THRESHOLD = 768; // px
export function EditorOssCard() {
const { schema, isMutable, deleteSchema } = useOssEdit();
const { isModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
const showOSSStats = usePreferencesStore(state => state.showOSSStats);
const windowSize = useWindowSize();
const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD;

View File

@ -20,7 +20,8 @@ import { useOssEdit } from '../oss-edit-context';
export function FormOSS() {
const { updateItem: updateOss } = useUpdateItem();
const { isModified, setIsModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
const setIsModified = useModificationStore(state => state.setIsModified);
const isProcessing = useMutatingOss();
const { schema, isMutable } = useOssEdit();

View File

@ -37,7 +37,7 @@ export function OssPage() {
tab: query.get('tab')
});
const { isModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
useBlockNavigation(isModified);
if (!urlData.id) {

View File

@ -42,7 +42,8 @@ interface FormConstituentaProps {
}
export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, onOpenEdit }: FormConstituentaProps) {
const { isModified, setIsModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
const setIsModified = useModificationStore(state => state.setIsModified);
const isProcessing = useMutatingRSForm();
const { updateConstituenta: cstUpdate } = useUpdateConstituenta();

View File

@ -64,7 +64,7 @@ export function ToolbarConstituenta({
const showList = usePreferencesStore(state => state.showCstSideList);
const toggleList = usePreferencesStore(state => state.toggleShowCstSideList);
const { isModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
const isProcessing = useMutatingRSForm();
function viewPredecessor(target: number) {

View File

@ -19,7 +19,7 @@ const SIDELIST_LAYOUT_THRESHOLD = 768; // px
export function EditorRSFormCard() {
const { schema, isMutable, deleteSchema, isAttachedToOSS } = useRSEdit();
const { isModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
const showRSFormStats = usePreferencesStore(state => state.showRSFormStats);
const windowSize = useWindowSize();
const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD;

View File

@ -30,7 +30,7 @@ import { ToolbarVersioning } from './toolbar-versioning';
export function FormRSForm() {
const router = useConceptNavigation();
const { updateItem: updateSchema } = useUpdateItem();
const { setIsModified } = useModificationStore();
const setIsModified = useModificationStore(state => state.setIsModified);
const isProcessing = useMutatingRSForm();
const { schema, isAttachedToOSS, isContentEditable } = useRSEdit();

View File

@ -20,7 +20,7 @@ interface ToolbarVersioningProps {
}
export function ToolbarVersioning({ blockReload, className }: ToolbarVersioningProps) {
const { isModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
const { restoreVersion: versionRestore } = useRestoreVersion();
const { schema, isMutable, isContentEditable, navigateVersion, activeVersion, selected } = useRSEdit();

View File

@ -29,7 +29,7 @@ import { useRSEdit } from './rsedit-context';
export function MenuEditSchema() {
const { isAnonymous } = useAuthSuspense();
const { isModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
const router = useConceptNavigation();
const menu = useDropdown();
const { schema, activeCst, setSelected, isArchive, isContentEditable, promptTemplate, deselectAll } = useRSEdit();

View File

@ -40,7 +40,7 @@ export function MenuMain() {
const { user, isAnonymous } = useAuthSuspense();
const role = useRoleStore(state => state.role);
const { isModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
const { download } = useDownloadRSForm();

View File

@ -47,7 +47,7 @@ export const RSEditState = ({
const { user } = useAuthSuspense();
const { schema } = useRSFormSuspense({ itemID: itemID, version: activeVersion });
const { isModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
const isOwned = !!user.id && user.id === schema.owner;
const isArchive = !!activeVersion;

View File

@ -47,7 +47,7 @@ export function RSFormPage() {
activeID: query.get('active')
});
const { isModified } = useModificationStore();
const isModified = useModificationStore(state => state.isModified);
useBlockNavigation(isModified);
if (!urlData.id) {

View File

@ -24,7 +24,7 @@ export function RSTabs({ activeID, activeTab }: RSTabsProps) {
const router = useConceptNavigation();
const hideFooter = useAppLayoutStore(state => state.hideFooter);
const { setIsModified } = useModificationStore();
const setIsModified = useModificationStore(state => state.setIsModified);
const { schema, selected, setSelected, deselectAll, navigateRSForm } = useRSEdit();
useLayoutEffect(() => {

View File

@ -0,0 +1,24 @@
import { useEffect } from 'react';
import { useAppTransitionStore } from '@/stores/app-transition';
const DELAY_CACHE_CHECK = 100; // ms
export function useBrowserNavigation() {
const start = useAppTransitionStore(state => state.startNavigation);
const end = useAppTransitionStore(state => state.endNavigation);
useEffect(() => {
const onPopState = () => {
start();
// Fallback to end the navigation in case route completes with cache
setTimeout(() => {
end();
}, DELAY_CACHE_CHECK); // or cancel after Suspense/loader finishes
};
window.addEventListener('popstate', onPopState);
return () => window.removeEventListener('popstate', onPopState);
}, [start, end]);
}

View File

@ -1,13 +1,11 @@
import { useRef } from 'react';
import { useEffect } from 'react';
import { useModificationStore } from '@/stores/modification';
export function useResetModification() {
const { setIsModified } = useModificationStore();
const initialized = useRef(false);
const setIsModified = useModificationStore(state => state.setIsModified);
if (!initialized.current) {
initialized.current = true;
useEffect(() => {
setIsModified(false);
}
}, [setIsModified]);
}

View File

@ -0,0 +1,37 @@
import { useEffect, useState } from 'react';
import { useNavigation } from 'react-router';
const DEFAULT_DEBOUNCE_DELAY = 100; // ms
/**
* Tracks whether a route transition is in progress, even if data is cached or rendering takes time.
* Adds an optional debounce to avoid flashing the loader.
*
* @param delay Optional debounce delay in milliseconds before showing the loader.
* @returns `true` if a transition is in progress (after debounce), `false` otherwise.
*/
export function useTransitionTracker(delay: number = DEFAULT_DEBOUNCE_DELAY): boolean {
const navigation = useNavigation();
const [showPending, setShowPending] = useState<boolean>(false);
useEffect(() => {
let timeout: ReturnType<typeof setTimeout> | null = null;
if (navigation.location) {
timeout = setTimeout(() => setShowPending(true), delay);
} else {
setShowPending(false);
if (timeout) {
clearTimeout(timeout);
}
}
return () => {
if (timeout) {
clearTimeout(timeout);
}
};
}, [navigation.location, delay]);
return showPending;
}

View File

@ -0,0 +1,13 @@
import { create } from 'zustand';
interface TransitionState {
isNavigating: boolean;
startNavigation: () => void;
endNavigation: () => void;
}
export const useAppTransitionStore = create<TransitionState>(set => ({
isNavigating: false,
startNavigation: () => set({ isNavigating: true }),
endNavigation: () => set({ isNavigating: false })
}));