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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -42,7 +42,8 @@ interface FormConstituentaProps {
} }
export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, onOpenEdit }: 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 isProcessing = useMutatingRSForm();
const { updateConstituenta: cstUpdate } = useUpdateConstituenta(); const { updateConstituenta: cstUpdate } = useUpdateConstituenta();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -47,7 +47,7 @@ export const RSEditState = ({
const { user } = useAuthSuspense(); const { user } = useAuthSuspense();
const { schema } = useRSFormSuspense({ itemID: itemID, version: activeVersion }); 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 isOwned = !!user.id && user.id === schema.owner;
const isArchive = !!activeVersion; const isArchive = !!activeVersion;

View File

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

View File

@ -24,7 +24,7 @@ export function RSTabs({ activeID, activeTab }: RSTabsProps) {
const router = useConceptNavigation(); const router = useConceptNavigation();
const hideFooter = useAppLayoutStore(state => state.hideFooter); const hideFooter = useAppLayoutStore(state => state.hideFooter);
const { setIsModified } = useModificationStore(); const setIsModified = useModificationStore(state => state.setIsModified);
const { schema, selected, setSelected, deselectAll, navigateRSForm } = useRSEdit(); const { schema, selected, setSelected, deselectAll, navigateRSForm } = useRSEdit();
useLayoutEffect(() => { 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'; import { useModificationStore } from '@/stores/modification';
export function useResetModification() { export function useResetModification() {
const { setIsModified } = useModificationStore(); const setIsModified = useModificationStore(state => state.setIsModified);
const initialized = useRef(false);
if (!initialized.current) { useEffect(() => {
initialized.current = true;
setIsModified(false); 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 })
}));