mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-13 20:30:36 +03:00
F: Improve navigation tracking and loaders. Fix store utilization
This commit is contained in:
parent
5d8cfe0b21
commit
e00b8da6ee
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -26,7 +26,7 @@ export function PromptTemplatesPage() {
|
|||
active: query.get('active')
|
||||
});
|
||||
|
||||
const { isModified } = useModificationStore();
|
||||
const isModified = useModificationStore(state => state.isModified);
|
||||
useBlockNavigation(isModified);
|
||||
|
||||
return (
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
24
rsconcept/frontend/src/hooks/use-browser-navigation.ts
Normal file
24
rsconcept/frontend/src/hooks/use-browser-navigation.ts
Normal 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]);
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
|
|
37
rsconcept/frontend/src/hooks/use-transition-delay.ts
Normal file
37
rsconcept/frontend/src/hooks/use-transition-delay.ts
Normal 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;
|
||||
}
|
13
rsconcept/frontend/src/stores/app-transition.ts
Normal file
13
rsconcept/frontend/src/stores/app-transition.ts
Normal 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 })
|
||||
}));
|
Loading…
Reference in New Issue
Block a user