Refactor UserProfileContext

This commit is contained in:
IRBorisov 2023-07-31 23:47:18 +03:00
parent f03e61e3c1
commit ae8b4afa88
10 changed files with 244 additions and 177 deletions

View File

@ -7,7 +7,8 @@ interface RequireAuthProps {
} }
function RequireAuth({ children }: RequireAuthProps) { function RequireAuth({ children }: RequireAuthProps) {
const { user } = useAuth() const { user } = useAuth();
return ( return (
<> <>
{user && children} {user && children}

View File

@ -0,0 +1,77 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { ErrorInfo } from '../components/BackendError';
import { DataCallback, getProfile, patchProfile } from '../utils/backendAPI';
import { IUserProfile, IUserUpdateData } from '../utils/models';
interface IUserProfileContextContext {
user: IUserProfile | undefined
loading: boolean
processing: boolean
error: ErrorInfo
setError: (error: ErrorInfo) => void
updateUser: (data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => void
}
const ProfileContext = createContext<IUserProfileContextContext | null>(null);
export const useUserProfile = () => {
const context = useContext(ProfileContext);
if (!context) {
throw new Error(
'useUserProfile has to be used within <UserProfileState.Provider>'
);
}
return context;
}
interface UserProfileStateProps {
children: React.ReactNode
}
export const UserProfileState = ({ children }: UserProfileStateProps) => {
const [user, setUser] = useState<IUserProfile | undefined>(undefined);
const [loading, setLoading] = useState(false);
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined);
const reload = useCallback(
() => {
setError(undefined);
setUser(undefined);
getProfile({
showError: true,
setLoading: setLoading,
onError: error => { setError(error); },
onSuccess: newData => { setUser(newData); }
});
}, [setUser]
);
const updateUser = useCallback(
(data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => {
setError(undefined);
patchProfile({
data: data,
showError: true,
setLoading: setProcessing,
onError: error => { setError(error); },
onSuccess: newData => {
setUser(newData);
if (callback) callback(newData);
}
});
}, [setUser]
);
useEffect(() => {
reload();
}, [reload]);
return (
<ProfileContext.Provider
value={{user, updateUser, error, loading, setError, processing}}
>
{children}
</ProfileContext.Provider>
);
};

View File

@ -1,45 +0,0 @@
import { useCallback, useEffect, useState } from 'react'
import { type ErrorInfo } from '../components/BackendError'
import { DataCallback, getProfile, patchProfile } from '../utils/backendAPI'
import { type IUserProfile,IUserUpdateData } from '../utils/models'
export function useUserProfile() {
const [user, setUser] = useState<IUserProfile | undefined>(undefined);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined);
const reload = useCallback(
() => {
setError(undefined);
setUser(undefined);
getProfile({
showError: true,
setLoading: setLoading,
onError: error => { setError(error); },
onSuccess: newData => { setUser(newData); }
});
}, [setUser]
)
const updateUser = useCallback(
(data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => {
setError(undefined);
patchProfile({
data: data,
showError: true,
setLoading: setLoading,
onError: error => { setError(error); },
onSuccess: newData => {
setUser(newData);
if (callback) callback(newData);
}
});
}, [setUser]
);
useEffect(() => {
reload();
}, [reload])
return { user, updateUser, error, loading };
}

View File

@ -15,6 +15,7 @@ import { IRSFormCreateData, IRSFormMeta } from '../utils/models';
function CreateRSFormPage() { function CreateRSFormPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { createSchema, error, setError, loading } = useNewRSForm()
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [alias, setAlias] = useState(''); const [alias, setAlias] = useState('');
@ -22,25 +23,24 @@ function CreateRSFormPage() {
const [common, setCommon] = useState(false); const [common, setCommon] = useState(false);
const [file, setFile] = useState<File | undefined>() const [file, setFile] = useState<File | undefined>()
const handleFile = (event: React.ChangeEvent<HTMLInputElement>) => { useEffect(() => {
setError(undefined);
}, [title, alias, setError]);
function handleFile(event: React.ChangeEvent<HTMLInputElement>) {
if (event.target.files && event.target.files.length > 0) { if (event.target.files && event.target.files.length > 0) {
setFile(event.target.files[0]); setFile(event.target.files[0]);
} else { } else {
setFile(undefined) setFile(undefined);
} }
} }
const onSuccess = (newSchema: IRSFormMeta) => { function onSuccess(newSchema: IRSFormMeta) {
toast.success('Схема успешно создана'); toast.success('Схема успешно создана');
navigate(`/rsforms/${newSchema.id}`); navigate(`/rsforms/${newSchema.id}`);
} }
const { createSchema, error, setError, loading } = useNewRSForm()
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
useEffect(() => {
setError(undefined)
}, [title, alias, setError]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
if (loading) { if (loading) {
return; return;
@ -54,43 +54,49 @@ function CreateRSFormPage() {
fileName: file?.name fileName: file?.name
}; };
createSchema(data, onSuccess); createSchema(data, onSuccess);
}; }
return ( return (
<RequireAuth> <RequireAuth>
<Form title='Создание концептуальной схемы' onSubmit={handleSubmit} widthClass='max-w-lg mt-4'> <Form title='Создание концептуальной схемы'
<TextInput id='title' label='Полное название' type='text' onSubmit={handleSubmit}
required={!file} widthClass='max-w-lg mt-4'
placeholder={file && 'Загрузить из файла'} >
value={title} <TextInput id='title' label='Полное название' type='text'
onChange={event => { setTitle(event.target.value); }} required={!file}
/> placeholder={file && 'Загрузить из файла'}
<TextInput id='alias' label='Сокращение' type='text' value={title}
required={!file} onChange={event => setTitle(event.target.value)}
value={alias} />
placeholder={file && 'Загрузить из файла'} <TextInput id='alias' label='Сокращение' type='text'
widthClass='max-w-sm' required={!file}
onChange={event => { setAlias(event.target.value); }} value={alias}
/> placeholder={file && 'Загрузить из файла'}
<TextArea id='comment' label='Комментарий' widthClass='max-w-sm'
value={comment} onChange={event => setAlias(event.target.value)}
placeholder={file && 'Загрузить из файла'} />
onChange={event => { setComment(event.target.value); }} <TextArea id='comment' label='Комментарий'
/> value={comment}
<Checkbox id='common' label='Общедоступная схема' placeholder={file && 'Загрузить из файла'}
value={common} onChange={event => setComment(event.target.value)}
onChange={event => { setCommon(event.target.checked); }} />
/> <Checkbox id='common' label='Общедоступная схема'
<FileInput id='trs' label='Загрузить *.trs' value={common}
acceptType='.trs' onChange={event => setCommon(event.target.checked)}
onChange={handleFile} />
/> <FileInput id='trs' label='Загрузить *.trs'
acceptType='.trs'
onChange={handleFile}
/>
<div className='flex items-center justify-center py-2 mt-4'> <div className='flex items-center justify-center py-2 mt-4'>
<SubmitButton text='Создать схему' loading={loading} /> <SubmitButton
</div> text='Создать схему'
{ error && <BackendError error={error} />} loading={loading}
</Form> />
</div>
{ error && <BackendError error={error} />}
</Form>
</RequireAuth> </RequireAuth>
); );
} }

View File

@ -11,12 +11,12 @@ import { useAuth } from '../context/AuthContext';
import { IUserLoginData } from '../utils/models'; import { IUserLoginData } from '../utils/models';
function LoginPage() { function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const { user, login, loading, error, setError } = useAuth()
const navigate = useNavigate(); const navigate = useNavigate();
const search = useLocation().search; const search = useLocation().search;
const { user, login, loading, error, setError } = useAuth();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
useEffect(() => { useEffect(() => {
const name = new URLSearchParams(search).get('username'); const name = new URLSearchParams(search).get('username');
@ -28,7 +28,7 @@ function LoginPage() {
setError(undefined); setError(undefined);
}, [username, password, setError]); }, [username, password, setError]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();
if (!loading) { if (!loading) {
const data: IUserLoginData = { const data: IUserLoginData = {
@ -37,7 +37,7 @@ function LoginPage() {
}; };
login(data, () => { navigate('/library?filter=personal'); }); login(data, () => { navigate('/library?filter=personal'); });
} }
}; }
return ( return (
<div className='w-full py-2'> { user <div className='w-full py-2'> { user

View File

@ -32,13 +32,13 @@ export enum RSTabsList {
function RSTabs() { function RSTabs() {
const navigate = useNavigate(); const navigate = useNavigate();
const { setActiveID, activeID, error, schema, loading, cstCreate } = useRSForm(); const { setActiveID, activeID, error, schema, loading, cstCreate } = useRSForm();
const [activeTab, setActiveTab] = useLocalStorage('rsform_edit_tab', RSTabsList.CARD); const [activeTab, setActiveTab] = useLocalStorage('rsform_edit_tab', RSTabsList.CARD);
const [init, setInit] = useState(false); const [init, setInit] = useState(false);
const [showUpload, setShowUpload] = useState(false); const [showUpload, setShowUpload] = useState(false);
const [showClone, setShowClone] = useState(false); const [showClone, setShowClone] = useState(false);
const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]); const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]);
const [showAST, setShowAST] = useState(false); const [showAST, setShowAST] = useState(false);
@ -46,60 +46,6 @@ function RSTabs() {
const [insertWhere, setInsertWhere] = useState<number | undefined>(undefined); const [insertWhere, setInsertWhere] = useState<number | undefined>(undefined);
const [showCreateCst, setShowCreateCst] = useState(false); const [showCreateCst, setShowCreateCst] = useState(false);
const handleAddNew = useCallback(
(type: CstType, selectedCst?: number) => {
if (!schema?.items) {
return;
}
const data: ICstCreateData = {
cst_type: type,
alias: createAliasFor(type, schema),
insert_after: selectedCst ?? insertWhere ?? null
}
cstCreate(data, newCst => {
toast.success(`Конституента добавлена: ${newCst.alias}`);
navigate(`/rsforms/${schema.id}?tab=${activeTab}&active=${newCst.id}`);
if (activeTab === RSTabsList.CST_EDIT || activeTab == RSTabsList.CST_LIST) {
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: "end",
inline: "nearest"
});
}
}, timeout_updateUI);
}
});
}, [schema, cstCreate, insertWhere, navigate, activeTab]);
const onShowCreateCst = useCallback(
(selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => {
if (skipDialog && type) {
handleAddNew(type, selectedID);
} else {
setDefaultType(type);
setInsertWhere(selectedID);
setShowCreateCst(true);
}
}, [handleAddNew]);
const onShowAST = useCallback(
(ast: SyntaxTree) => {
setSyntaxTree(ast);
setShowAST(true);
}, []);
const onEditCst = (cst: IConstituenta) => {
setActiveID(cst.id);
setActiveTab(RSTabsList.CST_EDIT)
};
const onSelectTab = (index: number) => {
setActiveTab(index);
};
useLayoutEffect(() => { useLayoutEffect(() => {
if (schema) { if (schema) {
const url = new URL(window.location.href); const url = new URL(window.location.href);
@ -146,6 +92,61 @@ function RSTabs() {
} }
}, [activeTab, activeID, init]); }, [activeTab, activeID, init]);
function onSelectTab(index: number) {
setActiveTab(index);
}
const handleAddNew = useCallback(
(type: CstType, selectedCst?: number) => {
if (!schema?.items) {
return;
}
const data: ICstCreateData = {
cst_type: type,
alias: createAliasFor(type, schema),
insert_after: selectedCst ?? insertWhere ?? null
}
cstCreate(data, newCst => {
toast.success(`Конституента добавлена: ${newCst.alias}`);
navigate(`/rsforms/${schema.id}?tab=${activeTab}&active=${newCst.id}`);
if (activeTab === RSTabsList.CST_EDIT || activeTab == RSTabsList.CST_LIST) {
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: "end",
inline: "nearest"
});
}
}, timeout_updateUI);
}
});
}, [schema, cstCreate, insertWhere, navigate, activeTab]);
const onShowCreateCst = useCallback(
(selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => {
if (skipDialog && type) {
handleAddNew(type, selectedID);
} else {
setDefaultType(type);
setInsertWhere(selectedID);
setShowCreateCst(true);
}
}, [handleAddNew]);
const onShowAST = useCallback(
(ast: SyntaxTree) => {
setSyntaxTree(ast);
setShowAST(true);
}, []);
const onEditCst = useCallback(
(cst: IConstituenta) => {
setActiveID(cst.id);
setActiveTab(RSTabsList.CST_EDIT)
}, [setActiveID, setActiveTab]);
return ( return (
<div className='w-full'> <div className='w-full'>
{ loading && <Loader /> } { loading && <Loader /> }

View File

@ -12,6 +12,8 @@ import { type IUserSignupData } from '../utils/models';
function RegisterPage() { function RegisterPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { user, signup, loading, error, setError } = useAuth();
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
@ -19,13 +21,11 @@ function RegisterPage() {
const [firstName, setFirstName] = useState(''); const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState(''); const [lastName, setLastName] = useState('');
const { user, signup, loading, error, setError } = useAuth()
useEffect(() => { useEffect(() => {
setError(undefined); setError(undefined);
}, [username, email, password, password2, setError]); }, [username, email, password, password2, setError]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();
if (!loading) { if (!loading) {
const data: IUserSignupData = { const data: IUserSignupData = {
@ -41,7 +41,7 @@ function RegisterPage() {
toast.success(`Пользователь успешно создан: ${createdUser.username}`); toast.success(`Пользователь успешно создан: ${createdUser.username}`);
}); });
} }
}; }
return ( return (
<div className='w-full py-2'> <div className='w-full py-2'>

View File

@ -2,12 +2,12 @@ import { useLayoutEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import TextInput from '../../components/Common/TextInput'; import TextInput from '../../components/Common/TextInput';
import { useUserProfile } from '../../hooks/useUserProfile'; import { useUserProfile } from '../../context/UserProfileContext';
import { IUserUpdateData } from '../../utils/models'; import { IUserUpdateData } from '../../utils/models';
export function UserProfile() { export function UserProfile() {
const { updateUser, user} = useUserProfile(); const { updateUser, user, processing } = useUserProfile();
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
@ -23,7 +23,7 @@ export function UserProfile() {
} }
}, [user]); }, [user]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();
const data: IUserUpdateData = { const data: IUserUpdateData = {
username: username, username: username,
@ -32,18 +32,30 @@ export function UserProfile() {
last_name: last_name, last_name: last_name,
}; };
updateUser(data, () => toast.success('Изменения сохранены')); updateUser(data, () => toast.success('Изменения сохранены'));
}; }
// console.log(user)
return ( return (
<form onSubmit={handleSubmit} className='flex-grow max-w-xl px-4 py-2 border min-w-fit'> <form onSubmit={handleSubmit} className='flex-grow max-w-xl px-4 py-2 border min-w-fit'>
<div className='flex flex-col items-center justify-center px-2 py-2 border'> <div className='flex flex-col items-center justify-center px-2 py-2 border'>
<TextInput id='username' label="Логин:" value={username} onChange={event => { setUsername(event.target.value); }}/> <TextInput id='username'
<TextInput id='first_name' label="Имя:" value={first_name} onChange={event => { setFirstName(event.target.value); }}/> label='Логин:'
<TextInput id='last_name' label="Фамилия:" value={last_name} onChange={event => { setLastName(event.target.value); }}/> value={username}
<TextInput id='email' label="Электронная почта:" value={email} onChange={event => { setEmail(event.target.value); }}/> onChange={event => setUsername(event.target.value)}
/>
<TextInput id='first_name'
label="Имя:"
value={first_name}
onChange={event => setFirstName(event.target.value)}
/>
<TextInput id='last_name' label="Фамилия:" value={last_name} onChange={event => setLastName(event.target.value)}/>
<TextInput id='email' label="Электронная почта:" value={email} onChange={event => setEmail(event.target.value)}/>
<div className='flex items-center justify-between my-4'> <div className='flex items-center justify-between my-4'>
<button className='px-2 py-1 bg-green-500 border' type='submit'>Сохранить</button> <button
type='submit'
className='px-2 py-1 bg-green-500 border'
disabled={processing}>
<span>Сохранить</span>
</button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,18 @@
import BackendError from '../../components/BackendError';
import { Loader } from '../../components/Common/Loader';
import { useUserProfile } from '../../context/UserProfileContext';
import { UserProfile } from './UserProfile';
function UserTabs() {
const { user, error, loading } = useUserProfile();
return (
<div className='w-full'>
{ loading && <Loader /> }
{ error && <BackendError error={error} />}
{ user && <UserProfile /> }
</div>
);
}
export default UserTabs;

View File

@ -1,17 +1,14 @@
import BackendError from '../../components/BackendError'; import RequireAuth from '../../components/RequireAuth';
import { Loader } from '../../components/Common/Loader'; import { UserProfileState } from '../../context/UserProfileContext';
import { useUserProfile } from '../../hooks/useUserProfile'; import UserTabs from './UserTabs';
import { UserProfile } from './UserProfile';
function UserProfilePage() { function UserProfilePage() {
const { user, error, loading } = useUserProfile();
return ( return (
<div className='w-full'> <RequireAuth>
{ loading && <Loader /> } <UserProfileState>
{ error && <BackendError error={error} />} <UserTabs />
{ user && <UserProfile /> } </UserProfileState>
</div> </RequireAuth>
); );
} }