R: Improve suspesce and loading behavior

This commit is contained in:
Ivan 2025-01-29 16:17:50 +03:00
parent 283edcce86
commit 242d98abdc
19 changed files with 163 additions and 191 deletions

View File

@ -23,7 +23,7 @@ export const useResetPassword = () => {
data: IResetPasswordDTO, //
onSuccess?: () => void
) => resetMutation.mutate(data, { onSuccess }),
isPending: resetMutation.isPending,
isPending: resetMutation.isPending || validateMutation.isPending,
error: resetMutation.error,
reset: resetMutation.reset
};

View File

@ -1,4 +1,4 @@
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI';
import { ILibraryFilter } from '@/models/miscellaneous';
@ -6,7 +6,7 @@ import { useLibrary } from './useLibrary';
export function useApplyLibraryFilter(filter: ILibraryFilter) {
const { items } = useLibrary();
const { user } = useAuth();
const { user } = useAuthSuspense();
let result = items;
if (!filter.folderMode && filter.head) {
@ -28,10 +28,10 @@ export function useApplyLibraryFilter(filter: ILibraryFilter) {
result = result.filter(item => filter.isVisible === item.visible);
}
if (filter.isOwned !== undefined) {
result = result.filter(item => filter.isOwned === (item.owner === user?.id));
result = result.filter(item => filter.isOwned === (item.owner === user.id));
}
if (filter.isEditor !== undefined) {
result = result.filter(item => filter.isEditor == user?.editor.includes(item.id));
result = result.filter(item => filter.isEditor == user.editor.includes(item.id));
}
if (filter.filterUser !== undefined) {
result = result.filter(item => filter.filterUser === item.owner);

View File

@ -1,11 +1,11 @@
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { useLogout } from '@/backend/auth/useLogout';
import TextURL from '@/components/ui/TextURL';
function ExpectedAnonymous() {
const { user } = useAuth();
const { user } = useAuthSuspense();
const { logout } = useLogout();
const router = useConceptNavigation();
@ -15,7 +15,7 @@ function ExpectedAnonymous() {
return (
<div className='cc-fade-in flex flex-col items-center gap-3 py-6'>
<p className='font-semibold'>{`Вы вошли в систему как ${user?.username ?? ''}`}</p>
<p className='font-semibold'>{`Вы вошли в систему как ${user.username}`}</p>
<div className='flex gap-3'>
<TextURL text='Новая схема' href='/library/create' />
<span> | </span>

View File

@ -1,25 +0,0 @@
import InfoError, { ErrorData } from '@/components/info/InfoError';
import Loader from '@/components/ui/Loader';
interface DataLoaderProps {
isLoading?: boolean;
error?: ErrorData;
hasNoData?: boolean;
}
function DataLoader({ isLoading, hasNoData, error, children }: React.PropsWithChildren<DataLoaderProps>) {
if (isLoading) {
return <Loader />;
}
if (error) {
return <InfoError error={error} />;
}
if (hasNoData) {
return <div className='cc-fade-in w-full text-center p-1'>Данные не загружены</div>;
} else {
return <>{children}</>;
}
}
export default DataLoader;

View File

@ -5,7 +5,7 @@ import { useState } from 'react';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { IRSFormCloneDTO } from '@/backend/library/api';
import { useCloneItem } from '@/backend/library/useCloneItem';
import { VisibilityIcon } from '@/components/DomainIcons';
@ -35,7 +35,7 @@ function DlgCloneLibraryItem() {
state => state.props as DlgCloneLibraryItemProps
);
const router = useConceptNavigation();
const { user } = useAuth();
const { user } = useAuthSuspense();
const [title, setTitle] = useState(cloneTitle(base));
const [alias, setAlias] = useState(base.alias);

View File

@ -1,11 +1,12 @@
'use client';
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { Suspense, useEffect, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs';
import { IInlineSynthesisDTO } from '@/backend/rsform/api';
import { useRSForm } from '@/backend/rsform/useRSForm';
import Loader from '@/components/ui/Loader';
import Modal from '@/components/ui/Modal';
import TabLabel from '@/components/ui/TabLabel';
import { LibraryItemID } from '@/models/library';
@ -14,7 +15,7 @@ import { ConstituentaID, IRSForm } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import TabConstituents from './TabConstituents';
import TabSchema from './TabSchema';
import TabSource from './TabSource';
import TabSubstitutions from './TabSubstitutions';
export interface DlgInlineSynthesisProps {
@ -32,20 +33,20 @@ function DlgInlineSynthesis() {
const { receiver, onInlineSynthesis } = useDialogsStore(state => state.props as DlgInlineSynthesisProps);
const [activeTab, setActiveTab] = useState(TabID.SCHEMA);
const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined);
const [sourceID, setSourceID] = useState<LibraryItemID | undefined>(undefined);
const [selected, setSelected] = useState<ConstituentaID[]>([]);
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>([]);
const source = useRSForm({ itemID: donorID });
const { schema } = useRSForm({ itemID: sourceID });
const validated = !!source.schema && selected.length > 0;
const validated = selected.length > 0;
function handleSubmit() {
if (!source.schema) {
if (!sourceID || selected.length === 0) {
return;
}
onInlineSynthesis({
source: source.schema.id,
source: sourceID,
receiver: receiver.id,
items: selected,
substitutions: substitutions
@ -53,9 +54,16 @@ function DlgInlineSynthesis() {
}
useEffect(() => {
setSelected(source.schema ? source.schema.items.map(cst => cst.id) : []);
if (schema) {
setSelected(schema.items.map(cst => cst.id));
}
}, [schema, setSelected]);
function handleSetSource(schemaID: LibraryItemID) {
setSourceID(schemaID);
setSelected([]);
setSubstitutions([]);
}, [source.schema]);
}
return (
<Modal
@ -73,32 +81,44 @@ function DlgInlineSynthesis() {
>
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none', 'bg-prim-200')}>
<TabLabel label='Схема' title='Источник конституент' className='w-[8rem]' />
<TabLabel label='Содержание' title='Перечень конституент' className='w-[8rem]' />
<TabLabel label='Отождествления' title='Таблица отождествлений' className='w-[8rem]' />
<TabLabel
label='Содержание'
title={!sourceID ? 'Выберите схему' : 'Перечень конституент'}
className='w-[8rem]'
disabled={!sourceID}
/>
<TabLabel
label='Отождествления'
title={!sourceID ? 'Выберите схему' : 'Таблица отождествлений'}
className='w-[8rem]'
disabled={!sourceID}
/>
</TabList>
<TabPanel>
<TabSchema selected={donorID} setSelected={setDonorID} receiver={receiver} />
<TabSource selected={sourceID} setSelected={handleSetSource} receiver={receiver} />
</TabPanel>
<TabPanel>
<TabConstituents
schema={source.schema}
loading={source.isLoading}
selected={selected}
setSelected={setSelected}
/>
{!!sourceID ? (
<Suspense fallback={<Loader />}>
<TabConstituents itemID={sourceID} selected={selected} setSelected={setSelected} />
</Suspense>
) : null}
</TabPanel>
<TabPanel>
{!!sourceID ? (
<Suspense fallback={<Loader />}>
<TabSubstitutions
sourceID={sourceID}
receiver={receiver}
source={source.schema}
selected={selected}
loading={source.isLoading}
substitutions={substitutions}
setSubstitutions={setSubstitutions}
/>
</Suspense>
) : null}
</TabPanel>
</Tabs>
</Modal>

View File

@ -1,23 +1,21 @@
'use client';
import { ErrorData } from '@/components/info/InfoError';
import { useRSFormSuspense } from '@/backend/rsform/useRSForm';
import PickMultiConstituenta from '@/components/select/PickMultiConstituenta';
import DataLoader from '@/components/wrap/DataLoader';
import { ConstituentaID, IRSForm } from '@/models/rsform';
import { LibraryItemID } from '@/models/library';
import { ConstituentaID } from '@/models/rsform';
import { prefixes } from '@/utils/constants';
interface TabConstituentsProps {
schema?: IRSForm;
loading?: boolean;
error?: ErrorData;
itemID: LibraryItemID;
selected: ConstituentaID[];
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
}
function TabConstituents({ schema, error, loading, selected, setSelected }: TabConstituentsProps) {
function TabConstituents({ itemID, selected, setSelected }: TabConstituentsProps) {
const { schema } = useRSFormSuspense({ itemID });
return (
<DataLoader isLoading={loading} error={error} hasNoData={!schema}>
{schema ? (
<PickMultiConstituenta
schema={schema}
data={schema.items}
@ -26,8 +24,6 @@ function TabConstituents({ schema, error, loading, selected, setSelected }: TabC
selected={selected}
setSelected={setSelected}
/>
) : null}
</DataLoader>
);
}

View File

@ -7,13 +7,13 @@ import { LibraryItemID, LibraryItemType } from '@/models/library';
import { IRSForm } from '@/models/rsform';
import { sortItemsForInlineSynthesis } from '@/models/rsformAPI';
interface TabSchemaProps {
interface TabSourceProps {
selected?: LibraryItemID;
setSelected: (newValue: LibraryItemID) => void;
receiver: IRSForm;
}
function TabSchema({ selected, receiver, setSelected }: TabSchemaProps) {
function TabSource({ selected, receiver, setSelected }: TabSourceProps) {
const { items: libraryItems } = useLibrary();
const selectedInfo = libraryItems.find(item => item.id === selected);
const sortedItems = sortItemsForInlineSynthesis(receiver, libraryItems);
@ -43,4 +43,4 @@ function TabSchema({ selected, receiver, setSelected }: TabSchemaProps) {
);
}
export default TabSchema;
export default TabSource;

View File

@ -1,39 +1,26 @@
'use client';
import { ErrorData } from '@/components/info/InfoError';
import { useRSFormSuspense } from '@/backend/rsform/useRSForm';
import PickSubstitutions from '@/components/select/PickSubstitutions';
import DataLoader from '@/components/wrap/DataLoader';
import { LibraryItemID } from '@/models/library';
import { ICstSubstitute } from '@/models/oss';
import { ConstituentaID, IRSForm } from '@/models/rsform';
import { prefixes } from '@/utils/constants';
interface TabSubstitutionsProps {
receiver?: IRSForm;
source?: IRSForm;
receiver: IRSForm;
sourceID: LibraryItemID;
selected: ConstituentaID[];
loading?: boolean;
error?: ErrorData;
substitutions: ICstSubstitute[];
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
}
function TabSubstitutions({
source,
receiver,
selected,
error,
loading,
substitutions,
setSubstitutions
}: TabSubstitutionsProps) {
function TabSubstitutions({ sourceID, receiver, selected, substitutions, setSubstitutions }: TabSubstitutionsProps) {
const { schema: source } = useRSFormSuspense({ itemID: sourceID });
const schemas = [...(source ? [source] : []), ...(receiver ? [receiver] : [])];
return (
<DataLoader isLoading={loading} error={error} hasNoData={!source}>
<PickSubstitutions
substitutions={substitutions}
setSubstitutions={setSubstitutions}
@ -42,7 +29,6 @@ function TabSubstitutions({
schemas={schemas}
filter={cst => cst.id !== source?.id || selected.includes(cst.id)}
/>
</DataLoader>
);
}

View File

@ -9,9 +9,9 @@ import { useRSForm } from '@/backend/rsform/useRSForm';
import { RelocateUpIcon } from '@/components/DomainIcons';
import PickMultiConstituenta from '@/components/select/PickMultiConstituenta';
import SelectLibraryItem from '@/components/select/SelectLibraryItem';
import Loader from '@/components/ui/Loader';
import MiniButton from '@/components/ui/MiniButton';
import Modal from '@/components/ui/Modal';
import DataLoader from '@/components/wrap/DataLoader';
import { ILibraryItem, LibraryItemID } from '@/models/library';
import { HelpTopic } from '@/models/miscellaneous';
import { IOperation, IOperationSchema } from '@/models/oss';
@ -119,8 +119,8 @@ function DlgRelocateConstituents() {
onSelectValue={handleSelectDestination}
/>
</div>
<DataLoader isLoading={sourceData.isLoading} error={sourceData.error}>
{sourceData.schema ? (
{sourceData.isLoading ? <Loader /> : null}
{!sourceData.isLoading && sourceData.schema ? (
<PickMultiConstituenta
noBorder
schema={sourceData.schema}
@ -131,7 +131,6 @@ function DlgRelocateConstituents() {
setSelected={setSelected}
/>
) : null}
</DataLoader>
</div>
</Modal>
);

View File

@ -1,4 +1,4 @@
import RequireAuth from '@/components/wrap/RequireAuth';
import RequireAuth from '@/components/RequireAuth';
import FormCreateItem from './FormCreateItem';

View File

@ -5,7 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { ILibraryCreateDTO } from '@/backend/library/api';
import { useCreateItem } from '@/backend/library/useCreateItem';
import { VisibilityIcon } from '@/components/DomainIcons';
@ -29,7 +29,7 @@ import { EXTEOR_TRS_FILE } from '@/utils/constants';
function FormCreateItem() {
const router = useConceptNavigation();
const { user } = useAuth();
const { user } = useAuthSuspense();
const { createItem, isPending, error, reset } = useCreateItem();
const searchLocation = useLibrarySearchStore(state => state.location);

View File

@ -1,7 +1,7 @@
import clsx from 'clsx';
import { toast } from 'react-toastify';
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { useLibrary } from '@/backend/library/useLibrary';
import { SubfoldersIcon } from '@/components/DomainIcons';
import { IconFolderEdit, IconFolderTree } from '@/components/Icons';
@ -23,7 +23,7 @@ interface ViewSideLocationProps {
}
function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocationProps) {
const { user, isAnonymous } = useAuth();
const { user, isAnonymous } = useAuthSuspense();
const { items } = useLibrary();
const windowSize = useWindowSize();

View File

@ -6,13 +6,13 @@ import { useEffect, useState } from 'react';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { useLogin } from '@/backend/auth/useLogin';
import ExpectedAnonymous from '@/components/ExpectedAnonymous';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput';
import TextURL from '@/components/ui/TextURL';
import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous';
import useQueryStrings from '@/hooks/useQueryStrings';
import { resources } from '@/utils/constants';
@ -21,7 +21,7 @@ function LoginPage() {
const query = useQueryStrings();
const userQuery = query.get('username');
const { isAnonymous } = useAuth();
const { isAnonymous } = useAuthSuspense();
const { login, isPending, error, reset } = useLogin();
const [username, setUsername] = useState(userQuery || '');

View File

@ -3,7 +3,7 @@
import clsx from 'clsx';
import { useState } from 'react';
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import SelectLocationContext from '@/components/select/SelectLocationContext';
import SelectLocationHead from '@/components/select/SelectLocationHead';
import Label from '@/components/ui/Label';
@ -21,7 +21,7 @@ export interface DlgChangeLocationProps {
function DlgChangeLocation() {
const { initial, onChangeLocation } = useDialogsStore(state => state.props as DlgChangeLocationProps);
const { user } = useAuth();
const { user } = useAuthSuspense();
const [head, setHead] = useState<LocationHead>(initial.substring(0, 2) as LocationHead);
const [body, setBody] = useState<string>(initial.substring(3));

View File

@ -9,9 +9,9 @@ import { urls } from '@/app/urls';
import { IResetPasswordDTO } from '@/backend/auth/api';
import { useResetPassword } from '@/backend/auth/useResetPassword';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import Loader from '@/components/ui/Loader';
import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput';
import DataLoader from '@/components/wrap/DataLoader';
import useQueryStrings from '@/hooks/useQueryStrings';
function PasswordChangePage() {
@ -51,8 +51,15 @@ function PasswordChangePage() {
validateToken({ token: token ?? '' }, () => setIsTokenValid(true));
}, [token, validateToken]);
if (error) {
return <ProcessError error={error} />;
}
if (isPending || !isTokenValid) {
return <Loader />;
}
return (
<DataLoader isLoading={isPending} hasNoData={!isTokenValid}>
<form className={clsx('cc-fade-in cc-column', 'w-[24rem] mx-auto', 'px-6 mt-3')} onSubmit={handleSubmit}>
<TextInput
id='new_password'
@ -85,9 +92,7 @@ function PasswordChangePage() {
loading={isPending}
disabled={!canSubmit}
/>
{error ? <ProcessError error={error} /> : null}
</form>
</DataLoader>
);
}

View File

@ -1,15 +1,11 @@
import { useAuth } from '@/backend/auth/useAuth';
import Loader from '@/components/ui/Loader';
import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import ExpectedAnonymous from '@/components/ExpectedAnonymous';
import FormSignup from './FormSignup';
function RegisterPage() {
const { user, isLoading } = useAuth();
const { user } = useAuthSuspense();
if (isLoading) {
return <Loader />;
}
if (user) {
return <ExpectedAnonymous />;
} else {

View File

@ -1,7 +1,4 @@
import { Suspense } from 'react';
import Loader from '@/components/ui/Loader';
import RequireAuth from '@/components/wrap/RequireAuth';
import RequireAuth from '@/components/RequireAuth';
import EditorPassword from './EditorPassword';
import EditorProfile from './EditorProfile';
@ -9,7 +6,6 @@ import EditorProfile from './EditorProfile';
function UserProfilePage() {
return (
<RequireAuth>
<Suspense fallback={<Loader />}>
<div className='cc-fade-in flex flex-col py-2 mx-auto w-fit'>
<h1 className='mb-2 select-none'>Учетные данные пользователя</h1>
<div className='flex py-2'>
@ -17,7 +13,6 @@ function UserProfilePage() {
<EditorPassword />
</div>
</div>
</Suspense>
</RequireAuth>
);
}