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

View File

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

View File

@ -32,13 +32,13 @@ export enum RSTabsList {
function RSTabs() {
const navigate = useNavigate();
const { setActiveID, activeID, error, schema, loading, cstCreate } = useRSForm();
const [activeTab, setActiveTab] = useLocalStorage('rsform_edit_tab', RSTabsList.CARD);
const [init, setInit] = useState(false);
const [showUpload, setShowUpload] = useState(false);
const [showClone, setShowClone] = useState(false);
const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]);
const [showAST, setShowAST] = useState(false);
@ -46,60 +46,6 @@ function RSTabs() {
const [insertWhere, setInsertWhere] = useState<number | undefined>(undefined);
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(() => {
if (schema) {
const url = new URL(window.location.href);
@ -146,6 +92,61 @@ function RSTabs() {
}
}, [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 (
<div className='w-full'>
{ loading && <Loader /> }

View File

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

View File

@ -2,12 +2,12 @@ import { useLayoutEffect, useState } from 'react';
import { toast } from 'react-toastify';
import TextInput from '../../components/Common/TextInput';
import { useUserProfile } from '../../hooks/useUserProfile';
import { useUserProfile } from '../../context/UserProfileContext';
import { IUserUpdateData } from '../../utils/models';
export function UserProfile() {
const { updateUser, user} = useUserProfile();
const { updateUser, user, processing } = useUserProfile();
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
@ -23,7 +23,7 @@ export function UserProfile() {
}
}, [user]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const data: IUserUpdateData = {
username: username,
@ -32,18 +32,30 @@ export function UserProfile() {
last_name: last_name,
};
updateUser(data, () => toast.success('Изменения сохранены'));
};
}
// console.log(user)
return (
<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'>
<TextInput id='username' label="Логин:" value={username} 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); }}/>
<TextInput id='username'
label='Логин:'
value={username}
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'>
<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>

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 { Loader } from '../../components/Common/Loader';
import { useUserProfile } from '../../hooks/useUserProfile';
import { UserProfile } from './UserProfile';
import RequireAuth from '../../components/RequireAuth';
import { UserProfileState } from '../../context/UserProfileContext';
import UserTabs from './UserTabs';
function UserProfilePage() {
const { user, error, loading } = useUserProfile();
return (
<div className='w-full'>
{ loading && <Loader /> }
{ error && <BackendError error={error} />}
{ user && <UserProfile /> }
</div>
<RequireAuth>
<UserProfileState>
<UserTabs />
</UserProfileState>
</RequireAuth>
);
}