mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-27 05:20:36 +03:00
Fix UI bugs and styles + refactor contexts
This commit is contained in:
parent
920a7baff4
commit
747ce126ae
|
@ -106,7 +106,8 @@ class RSForm(models.Model):
|
||||||
)
|
)
|
||||||
self._update_from_core()
|
self._update_from_core()
|
||||||
self.save()
|
self.save()
|
||||||
return Constituenta.objects.get(pk=result.pk)
|
result.refresh_from_db()
|
||||||
|
return result
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def move_cst(self, listCst: list['Constituenta'], target: int):
|
def move_cst(self, listCst: list['Constituenta'], target: int):
|
||||||
|
|
|
@ -200,25 +200,26 @@ class TestRSFormViewset(APITestCase):
|
||||||
def test_delete_constituenta(self):
|
def test_delete_constituenta(self):
|
||||||
schema = self.rsform_owned
|
schema = self.rsform_owned
|
||||||
data = json.dumps({'items': [{'id': 1337}]})
|
data = json.dumps({'items': [{'id': 1337}]})
|
||||||
response = self.client.post(f'/api/rsforms/{schema.id}/cst-multidelete/',
|
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-multidelete/',
|
||||||
data=data, content_type='application/json')
|
data=data, content_type='application/json')
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
x1 = Constituenta.objects.create(schema=schema, alias='X1', csttype='basic', order=1)
|
x1 = Constituenta.objects.create(schema=schema, alias='X1', csttype='basic', order=1)
|
||||||
x2 = Constituenta.objects.create(schema=schema, alias='X2', csttype='basic', order=2)
|
x2 = Constituenta.objects.create(schema=schema, alias='X2', csttype='basic', order=2)
|
||||||
data = json.dumps({'items': [{'id': x1.id}]})
|
data = json.dumps({'items': [{'id': x1.id}]})
|
||||||
response = self.client.post(f'/api/rsforms/{schema.id}/cst-multidelete/',
|
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-multidelete/',
|
||||||
data=data, content_type='application/json')
|
data=data, content_type='application/json')
|
||||||
x2.refresh_from_db()
|
x2.refresh_from_db()
|
||||||
schema.refresh_from_db()
|
schema.refresh_from_db()
|
||||||
self.assertEqual(response.status_code, 202)
|
self.assertEqual(response.status_code, 202)
|
||||||
|
self.assertEqual(len(response.data['items']), 1)
|
||||||
self.assertEqual(schema.constituents().count(), 1)
|
self.assertEqual(schema.constituents().count(), 1)
|
||||||
self.assertEqual(x2.alias, 'X2')
|
self.assertEqual(x2.alias, 'X2')
|
||||||
self.assertEqual(x2.order, 1)
|
self.assertEqual(x2.order, 1)
|
||||||
|
|
||||||
x3 = Constituenta.objects.create(schema=self.rsform_unowned, alias='X1', csttype='basic', order=1)
|
x3 = Constituenta.objects.create(schema=self.rsform_unowned, alias='X1', csttype='basic', order=1)
|
||||||
data = json.dumps({'items': [{'id': x3.id}]})
|
data = json.dumps({'items': [{'id': x3.id}]})
|
||||||
response = self.client.post(f'/api/rsforms/{schema.id}/cst-multidelete/',
|
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-multidelete/',
|
||||||
data=data, content_type='application/json')
|
data=data, content_type='application/json')
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
|
|
@ -69,14 +69,16 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
response['Location'] = constituenta.get_absolute_url()
|
response['Location'] = constituenta.get_absolute_url()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@action(detail=True, methods=['post'], url_path='cst-multidelete')
|
@action(detail=True, methods=['patch'], url_path='cst-multidelete')
|
||||||
def cst_multidelete(self, request, pk):
|
def cst_multidelete(self, request, pk):
|
||||||
''' Delete multiple constituents '''
|
''' Delete multiple constituents '''
|
||||||
schema: models.RSForm = self.get_object()
|
schema: models.RSForm = self.get_object()
|
||||||
serializer = serializers.CstListSerlializer(data=request.data, context={'schema': schema})
|
serializer = serializers.CstListSerlializer(data=request.data, context={'schema': schema})
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
schema.delete_cst(serializer.validated_data['constituents'])
|
schema.delete_cst(serializer.validated_data['constituents'])
|
||||||
return Response(status=202)
|
schema.refresh_from_db()
|
||||||
|
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
||||||
|
return Response(status=202, data=outSerializer.data)
|
||||||
|
|
||||||
@action(detail=True, methods=['patch'], url_path='cst-moveto')
|
@action(detail=True, methods=['patch'], url_path='cst-moveto')
|
||||||
def cst_moveto(self, request, pk):
|
def cst_moveto(self, request, pk):
|
||||||
|
|
|
@ -7,7 +7,7 @@ interface CardProps {
|
||||||
function Card({title, widthClass='min-w-fit', children}: CardProps) {
|
function Card({title, widthClass='min-w-fit', children}: CardProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`border shadow-md py-2 clr-card px-6 ${widthClass}`}>
|
<div className={`border shadow-md py-2 clr-card px-6 ${widthClass}`}>
|
||||||
{ title && <h1 className='mb-2 text-xl font-bold'>{title}</h1> }
|
{ title && <h1 className='mb-2 text-xl font-bold whitespace-nowrap'>{title}</h1> }
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import DataTable, { createTheme, TableProps } from 'react-data-table-component';
|
import DataTable, { createTheme, TableProps } from 'react-data-table-component';
|
||||||
import { useTheme } from '../../context/ThemeContext';
|
import { useConceptTheme } from '../../context/ThemeContext';
|
||||||
|
|
||||||
export interface SelectionInfo<T> {
|
export interface SelectionInfo<T> {
|
||||||
allSelected: boolean;
|
allSelected: boolean;
|
||||||
|
@ -38,7 +38,7 @@ createTheme('customDark', {
|
||||||
}, 'dark');
|
}, 'dark');
|
||||||
|
|
||||||
function DataTableThemed<T>({theme, ...props}: TableProps<T>) {
|
function DataTableThemed<T>({theme, ...props}: TableProps<T>) {
|
||||||
const { darkMode } = useTheme();
|
const { darkMode } = useConceptTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable<T>
|
<DataTable<T>
|
||||||
|
|
|
@ -6,35 +6,35 @@ import UserMenu from './UserMenu';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import UserTools from './UserTools';
|
import UserTools from './UserTools';
|
||||||
import Logo from './Logo';
|
import Logo from './Logo';
|
||||||
import { useState } from 'react';
|
import { useConceptTheme } from '../../context/ThemeContext';
|
||||||
|
|
||||||
function Navigation() {
|
function Navigation() {
|
||||||
const {user} = useAuth();
|
const {user} = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isActive, setActive] = useState(true);
|
const { noNavigation, toggleNoNavigation } = useConceptTheme();
|
||||||
|
|
||||||
const navigateCommon = () => navigate('/rsforms?filter=common');
|
const navigateCommon = () => navigate('/rsforms?filter=common');
|
||||||
const navigateHelp = () => navigate('/manuals');
|
const navigateHelp = () => navigate('/manuals');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className='sticky top-0 left-0 right-0 z-50'>
|
<nav className='sticky top-0 left-0 right-0 z-50'>
|
||||||
{isActive &&
|
{!noNavigation &&
|
||||||
<button
|
<button
|
||||||
title='Скрыть навигацию'
|
title='Скрыть навигацию'
|
||||||
className='absolute top-0 right-0 z-[60] w-[1.2rem] h-[4rem] border-b-2 border-l-2 clr-nav rounded-none'
|
className='absolute top-0 right-0 z-[60] w-[1.2rem] h-[4rem] border-b-2 border-l-2 clr-nav rounded-none'
|
||||||
onClick={() => setActive(!isActive)}
|
onClick={toggleNoNavigation}
|
||||||
>
|
>
|
||||||
<p>{'>'}</p><p>{'>'}</p>
|
<p>{'>'}</p><p>{'>'}</p>
|
||||||
</button>}
|
</button>}
|
||||||
{!isActive &&
|
{noNavigation &&
|
||||||
<button
|
<button
|
||||||
title='Показать навигацию'
|
title='Показать навигацию'
|
||||||
className='absolute top-0 right-0 z-[60] w-[4rem] h-[1.6rem] border-b-2 border-l-2 clr-nav rounded-none'
|
className='absolute top-0 right-0 z-[60] w-[4rem] h-[1.6rem] border-b-2 border-l-2 clr-nav rounded-none'
|
||||||
onClick={() => setActive(!isActive)}
|
onClick={toggleNoNavigation}
|
||||||
>
|
>
|
||||||
{'∨∨∨'}
|
{'∨∨∨'}
|
||||||
</button>}
|
</button>}
|
||||||
{isActive &&
|
{!noNavigation &&
|
||||||
<div className='pr-6 pl-2 py-2.5 h-[4rem] flex items-center justify-between border-b-2 clr-nav rounded-none'>
|
<div className='pr-6 pl-2 py-2.5 h-[4rem] flex items-center justify-between border-b-2 clr-nav rounded-none'>
|
||||||
<div className='flex items-start justify-start '>
|
<div className='flex items-start justify-start '>
|
||||||
<Logo title='КонцептПортал' />
|
<Logo title='КонцептПортал' />
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useTheme } from '../../context/ThemeContext';
|
import { useConceptTheme } from '../../context/ThemeContext';
|
||||||
import { DarkThemeIcon, LightThemeIcon } from '../Icons';
|
import { DarkThemeIcon, LightThemeIcon } from '../Icons';
|
||||||
import NavigationButton from './NavigationButton';
|
import NavigationButton from './NavigationButton';
|
||||||
|
|
||||||
function ThemeSwitcher() {
|
function ThemeSwitcher() {
|
||||||
const {darkMode, toggleDarkMode} = useTheme();
|
const {darkMode, toggleDarkMode} = useConceptTheme();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{darkMode && <NavigationButton icon={<LightThemeIcon />} description='Светлая тема' onClick={toggleDarkMode} />}
|
{darkMode && <NavigationButton icon={<LightThemeIcon />} description='Светлая тема' onClick={toggleDarkMode} />}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import DropdownButton from '../Common/DropdownButton';
|
import DropdownButton from '../Common/DropdownButton';
|
||||||
import { useTheme } from '../../context/ThemeContext';
|
import { useConceptTheme } from '../../context/ThemeContext';
|
||||||
import Dropdown from '../Common/Dropdown';
|
import Dropdown from '../Common/Dropdown';
|
||||||
|
|
||||||
interface UserDropdownProps {
|
interface UserDropdownProps {
|
||||||
|
@ -9,7 +9,7 @@ interface UserDropdownProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserDropdown({hideDropdown}: UserDropdownProps) {
|
function UserDropdown({hideDropdown}: UserDropdownProps) {
|
||||||
const {darkMode, toggleDarkMode} = useTheme();
|
const {darkMode, toggleDarkMode} = useConceptTheme();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {user, logout} = useAuth();
|
const {user, logout} = useAuth();
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { ToastContainer, ToastContainerProps } from 'react-toastify';
|
import { ToastContainer, ToastContainerProps } from 'react-toastify';
|
||||||
import { useTheme } from '../context/ThemeContext';
|
import { useConceptTheme } from '../context/ThemeContext';
|
||||||
|
|
||||||
function ToasterThemed({theme, ...props}: ToastContainerProps) {
|
function ToasterThemed({theme, ...props}: ToastContainerProps) {
|
||||||
const { darkMode } = useTheme();
|
const { darkMode } = useConceptTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
|
|
|
@ -1,29 +1,30 @@
|
||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useState } from 'react';
|
||||||
import { ICurrentUser, IUserSignupData } from '../utils/models';
|
import { ICurrentUser, IUserSignupData } from '../utils/models';
|
||||||
import { ErrorInfo } from '../components/BackendError';
|
import { ErrorInfo } from '../components/BackendError';
|
||||||
import useLocalStorage from '../hooks/useLocalStorage';
|
import useLocalStorage from '../hooks/useLocalStorage';
|
||||||
import { getAuth, postLogin, postLogout, postSignup } from '../utils/backendAPI';
|
import { BackendCallback, getAuth, postLogin, postLogout, postSignup } from '../utils/backendAPI';
|
||||||
|
|
||||||
|
|
||||||
interface IAuthContext {
|
interface IAuthContext {
|
||||||
user: ICurrentUser | undefined
|
user: ICurrentUser | undefined
|
||||||
login: (username: string, password: string) => Promise<void>
|
login: (username: string, password: string, callback?: BackendCallback) => Promise<void>
|
||||||
logout: (onSuccess?: () => void) => Promise<void>
|
logout: (callback?: BackendCallback) => Promise<void>
|
||||||
signup: (data: IUserSignupData) => Promise<void>
|
signup: (data: IUserSignupData, callback?: BackendCallback) => Promise<void>
|
||||||
loading: boolean
|
loading: boolean
|
||||||
error: ErrorInfo
|
error: ErrorInfo
|
||||||
setError: (error: ErrorInfo) => void
|
setError: (error: ErrorInfo) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthContext = createContext<IAuthContext>({
|
const AuthContext = createContext<IAuthContext | null>(null);
|
||||||
user: undefined,
|
export const useAuth = () => {
|
||||||
login: async () => {},
|
const context = useContext(AuthContext);
|
||||||
logout: async () => {},
|
if (!context) {
|
||||||
signup: async () => {},
|
throw new Error(
|
||||||
loading: false,
|
'useAuth has to be used within <AuthState.Provider>'
|
||||||
error: '',
|
);
|
||||||
setError: () => {}
|
}
|
||||||
});
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
interface AuthStateProps {
|
interface AuthStateProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
@ -49,44 +50,49 @@ export const AuthState = ({ children }: AuthStateProps) => {
|
||||||
}, [setUser]
|
}, [setUser]
|
||||||
);
|
);
|
||||||
|
|
||||||
async function login(uname: string, pw: string, onSuccess?: () => void) {
|
async function login(uname: string, pw: string, callback?: BackendCallback) {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
postLogin({
|
postLogin({
|
||||||
data: {username: uname, password: pw},
|
data: {username: uname, password: pw},
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setLoading,
|
setLoading: setLoading,
|
||||||
onError: error => setError(error),
|
onError: error => setError(error),
|
||||||
onSucccess: response => {
|
onSucccess:
|
||||||
loadCurrentUser();
|
async (response) => {
|
||||||
if(onSuccess) onSuccess();
|
await loadCurrentUser();
|
||||||
|
if(callback) callback(response);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout(callback?: BackendCallback) {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
postLogout({
|
postLogout({
|
||||||
showError: true,
|
showError: true,
|
||||||
onSucccess: response => {
|
onSucccess:
|
||||||
loadCurrentUser();
|
async (response) => {
|
||||||
|
await loadCurrentUser();
|
||||||
|
if (callback) callback(response);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function signup(data: IUserSignupData) {
|
async function signup(data: IUserSignupData, callback?: BackendCallback) {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
postSignup({
|
postSignup({
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setLoading,
|
setLoading: setLoading,
|
||||||
onError: error => setError(error),
|
onError: error => setError(error),
|
||||||
onSucccess: response => {
|
onSucccess:
|
||||||
loadCurrentUser();
|
async (response) => {
|
||||||
|
await loadCurrentUser();
|
||||||
|
if (callback) callback(response);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
loadCurrentUser();
|
loadCurrentUser();
|
||||||
}, [loadCurrentUser])
|
}, [loadCurrentUser])
|
||||||
|
|
||||||
|
@ -98,5 +104,3 @@ export const AuthState = ({ children }: AuthStateProps) => {
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAuth = () => useContext(AuthContext);
|
|
|
@ -6,7 +6,7 @@ import { useAuth } from './AuthContext';
|
||||||
import {
|
import {
|
||||||
BackendCallback, deleteRSForm, getTRSFile,
|
BackendCallback, deleteRSForm, getTRSFile,
|
||||||
patchConstituenta, patchMoveConstituenta, patchRSForm,
|
patchConstituenta, patchMoveConstituenta, patchRSForm,
|
||||||
postClaimRSForm, postDeleteConstituenta, postNewConstituenta
|
postClaimRSForm, patchDeleteConstituenta, postNewConstituenta
|
||||||
} from '../utils/backendAPI';
|
} from '../utils/backendAPI';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
@ -175,17 +175,17 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
const cstDelete = useCallback(
|
const cstDelete = useCallback(
|
||||||
async (data: any, callback?: BackendCallback) => {
|
async (data: any, callback?: BackendCallback) => {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
postDeleteConstituenta(schemaID, {
|
patchDeleteConstituenta(schemaID, {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: error => setError(error),
|
onError: error => setError(error),
|
||||||
onSucccess: async (response) => {
|
onSucccess: async (response) => {
|
||||||
await reload();
|
setSchema(response.data);
|
||||||
if (callback) callback(response);
|
if (callback) callback(response);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [schemaID, setError, reload]);
|
}, [schemaID, setError, setSchema]);
|
||||||
|
|
||||||
const cstMoveTo = useCallback(
|
const cstMoveTo = useCallback(
|
||||||
async (data: any, callback?: BackendCallback) => {
|
async (data: any, callback?: BackendCallback) => {
|
||||||
|
|
|
@ -1,16 +1,24 @@
|
||||||
import { createContext, useContext, useEffect } from 'react';
|
import { createContext, useContext, useEffect, useState } from 'react';
|
||||||
import useLocalStorage from '../hooks/useLocalStorage';
|
import useLocalStorage from '../hooks/useLocalStorage';
|
||||||
|
|
||||||
|
|
||||||
interface IThemeContext {
|
interface IThemeContext {
|
||||||
darkMode: boolean
|
darkMode: boolean
|
||||||
|
noNavigation: boolean
|
||||||
toggleDarkMode: () => void
|
toggleDarkMode: () => void
|
||||||
|
toggleNoNavigation: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThemeContext = createContext<IThemeContext>({
|
const ThemeContext = createContext<IThemeContext | null>(null);
|
||||||
darkMode: true,
|
export const useConceptTheme = () => {
|
||||||
toggleDarkMode: () => {}
|
const context = useContext(ThemeContext);
|
||||||
})
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useConceptTheme has to be used within <ThemeState.Provider>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
interface ThemeStateProps {
|
interface ThemeStateProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
@ -18,6 +26,7 @@ interface ThemeStateProps {
|
||||||
|
|
||||||
export const ThemeState = ({ children }: ThemeStateProps) => {
|
export const ThemeState = ({ children }: ThemeStateProps) => {
|
||||||
const [darkMode, setDarkMode] = useLocalStorage('darkMode', false);
|
const [darkMode, setDarkMode] = useLocalStorage('darkMode', false);
|
||||||
|
const [noNavigation, setNoNavigation] = useState(false);
|
||||||
|
|
||||||
const setDarkClass = (isDark: boolean) => {
|
const setDarkClass = (isDark: boolean) => {
|
||||||
const root = window.document.documentElement;
|
const root = window.document.documentElement;
|
||||||
|
@ -29,21 +38,16 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
|
||||||
root.setAttribute('data-color-scheme', !isDark ? 'light' : 'dark');
|
root.setAttribute('data-color-scheme', !isDark ? 'light' : 'dark');
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDarkMode = () => {
|
|
||||||
setDarkMode(!darkMode)
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDarkClass(darkMode)
|
setDarkClass(darkMode)
|
||||||
}, [darkMode]);
|
}, [darkMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{
|
<ThemeContext.Provider value={{
|
||||||
darkMode, toggleDarkMode
|
darkMode, toggleDarkMode: () => setDarkMode(prev => !prev),
|
||||||
|
noNavigation, toggleNoNavigation: () => setNoNavigation(prev => !prev),
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTheme = () => useContext(ThemeContext);
|
|
|
@ -9,11 +9,16 @@ interface IUsersContext {
|
||||||
getUserLabel: (userID?: number) => string
|
getUserLabel: (userID?: number) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UsersContext = createContext<IUsersContext>({
|
const UsersContext = createContext<IUsersContext | null>(null);
|
||||||
users: [],
|
export const useUsers = () => {
|
||||||
reload: async () => {},
|
const context = useContext(UsersContext);
|
||||||
getUserLabel: () => ''
|
if (!context) {
|
||||||
})
|
throw new Error(
|
||||||
|
'useUsers has to be used within <UsersState.Provider>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
interface UsersStateProps {
|
interface UsersStateProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
@ -64,5 +69,3 @@ export const UsersState = ({ children }: UsersStateProps) => {
|
||||||
</UsersContext.Provider>
|
</UsersContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUsers = () => useContext(UsersContext);
|
|
|
@ -30,8 +30,7 @@ function LoginPage() {
|
||||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
login(username, password)
|
login(username, password, () => navigate('/rsforms?filter=personal'));
|
||||||
.then(() => navigate('/rsforms?filter=personal'));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ function ConstituentEditor() {
|
||||||
'forms': activeCst?.term?.forms || [],
|
'forms': activeCst?.term?.forms || [],
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
cstUpdate(data).then(() => toast.success('Изменения сохранены'));
|
cstUpdate(data, () => toast.success('Изменения сохранены'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -74,13 +74,13 @@ function ConstituentEditor() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = {
|
const data = {
|
||||||
'items': [activeID]
|
'items': [{'id': activeID}]
|
||||||
}
|
}
|
||||||
const index = schema.items.findIndex((cst) => cst.id === activeID);
|
const index = schema.items.findIndex((cst) => cst.id === activeID);
|
||||||
if (index !== -1 && index + 1 < schema.items.length) {
|
if (index !== -1 && index + 1 < schema.items.length) {
|
||||||
setActiveID(schema.items[index + 1].id);
|
setActiveID(schema.items[index + 1].id);
|
||||||
}
|
}
|
||||||
cstDelete(data).then(() => toast.success('Конституента удалена'));
|
cstDelete(data, () => toast.success('Конституента удалена'));
|
||||||
}, [activeID, schema, setActiveID, cstDelete]);
|
}, [activeID, schema, setActiveID, cstDelete]);
|
||||||
|
|
||||||
const handleAddNew = useCallback(
|
const handleAddNew = useCallback(
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Divider from '../../components/Common/Divider';
|
||||||
import { createAliasFor, getCstTypeLabel, getCstTypePrefix, getStatusInfo, getTypeLabel } from '../../utils/staticUI';
|
import { createAliasFor, getCstTypeLabel, getCstTypePrefix, getStatusInfo, getTypeLabel } from '../../utils/staticUI';
|
||||||
import CreateCstModal from './CreateCstModal';
|
import CreateCstModal from './CreateCstModal';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { useConceptTheme } from '../../context/ThemeContext';
|
||||||
|
|
||||||
interface ConstituentsTableProps {
|
interface ConstituentsTableProps {
|
||||||
onOpenEdit: (cst: IConstituenta) => void
|
onOpenEdit: (cst: IConstituenta) => void
|
||||||
|
@ -19,6 +20,7 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
|
||||||
schema, isEditable,
|
schema, isEditable,
|
||||||
cstCreate, cstDelete, cstMoveTo
|
cstCreate, cstDelete, cstMoveTo
|
||||||
} = useRSForm();
|
} = useRSForm();
|
||||||
|
const { noNavigation } = useConceptTheme();
|
||||||
const [selected, setSelected] = useState<number[]>([]);
|
const [selected, setSelected] = useState<number[]>([]);
|
||||||
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
||||||
|
|
||||||
|
@ -71,7 +73,7 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
|
||||||
'items': selected.map(id => { return {'id': id }; }),
|
'items': selected.map(id => { return {'id': id }; }),
|
||||||
'move_to': insertIndex
|
'move_to': insertIndex
|
||||||
}
|
}
|
||||||
cstMoveTo(data).then(() => toast.info('Перемещение вверх ' + insertIndex));
|
cstMoveTo(data);
|
||||||
}, [selected, schema?.items, cstMoveTo]);
|
}, [selected, schema?.items, cstMoveTo]);
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,20 +83,24 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
|
||||||
if (!schema?.items || selected.length === 0) {
|
if (!schema?.items || selected.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let count = 0;
|
||||||
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
||||||
if (selected.indexOf(cst.id) < 0) {
|
if (selected.indexOf(cst.id) < 0) {
|
||||||
return prev;
|
return prev;
|
||||||
} else if (prev === -1) {
|
} else {
|
||||||
|
count += 1;
|
||||||
|
if (prev === -1) {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
return Math.max(prev, index);
|
return Math.max(prev, index);
|
||||||
|
}
|
||||||
}, -1);
|
}, -1);
|
||||||
const insertIndex = Math.min(schema.items.length - 1, currentIndex + 1) + 1
|
const insertIndex = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1
|
||||||
const data = {
|
const data = {
|
||||||
'items': selected.map(id => { return {'id': id }; }),
|
'items': selected.map(id => { return {'id': id }; }),
|
||||||
'move_to': insertIndex
|
'move_to': insertIndex
|
||||||
}
|
}
|
||||||
cstMoveTo(data).then(() => toast.info('Перемещение вниз ' + insertIndex));
|
cstMoveTo(data);
|
||||||
}, [selected, schema?.items, cstMoveTo]);
|
}, [selected, schema?.items, cstMoveTo]);
|
||||||
|
|
||||||
// Generate new names for all constituents
|
// Generate new names for all constituents
|
||||||
|
@ -131,7 +137,6 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
|
||||||
case 'ArrowUp': handleMoveUp(); return;
|
case 'ArrowUp': handleMoveUp(); return;
|
||||||
case 'ArrowDown': handleMoveDown(); return;
|
case 'ArrowDown': handleMoveDown(); return;
|
||||||
}
|
}
|
||||||
console.log(event);
|
|
||||||
}, [isEditable, selected, handleMoveUp, handleMoveDown]);
|
}, [isEditable, selected, handleMoveUp, handleMoveDown]);
|
||||||
|
|
||||||
const columns = useMemo(() =>
|
const columns = useMemo(() =>
|
||||||
|
@ -253,7 +258,10 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
|
||||||
onCreate={handleAddNew}
|
onCreate={handleAddNew}
|
||||||
/>
|
/>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<div className='sticky top-[4rem] z-10 flex justify-start w-full gap-1 px-2 py-1 border-y items-center h-[2.2rem] clr-app'>
|
<div
|
||||||
|
className={'flex justify-start w-full gap-1 px-2 py-1 border-y items-center h-[2.2rem] clr-app'
|
||||||
|
+ (!noNavigation ? ' sticky z-10 top-[4rem]' : ' sticky z-10 top-[0rem]')}
|
||||||
|
>
|
||||||
<div className='mr-3 whitespace-nowrap'>Выбраны <span className='ml-2'><b>{selected.length}</b> из {schema?.stats?.count_all || 0}</span></div>
|
<div className='mr-3 whitespace-nowrap'>Выбраны <span className='ml-2'><b>{selected.length}</b> из {schema?.stats?.count_all || 0}</span></div>
|
||||||
{isEditable && <div className='flex justify-start w-full gap-1'>
|
{isEditable && <div className='flex justify-start w-full gap-1'>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -43,7 +43,7 @@ function RSFormCard() {
|
||||||
'comment': comment,
|
'comment': comment,
|
||||||
'is_common': common,
|
'is_common': common,
|
||||||
};
|
};
|
||||||
update(data).then(() => toast.success('Изменения сохранены'));
|
update(data, () => toast.success('Изменения сохранены'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete =
|
const handleDelete =
|
||||||
|
|
|
@ -35,7 +35,7 @@ function RegisterPage() {
|
||||||
'first_name': firstName,
|
'first_name': firstName,
|
||||||
'last_name': lastName,
|
'last_name': lastName,
|
||||||
};
|
};
|
||||||
signup(data).then(() => setSuccess(true));
|
signup(data, () => setSuccess(true));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -158,8 +158,8 @@ export async function postNewConstituenta(schema: string, request?: IFrontReques
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postDeleteConstituenta(schema: string, request?: IFrontRequest) {
|
export async function patchDeleteConstituenta(schema: string, request?: IFrontRequest) {
|
||||||
AxiosPost({
|
AxiosPatch<IRSForm>({
|
||||||
title: `Delete Constituents for RSForm id=${schema}: ${request?.data['items'].toString()}`,
|
title: `Delete Constituents for RSForm id=${schema}: ${request?.data['items'].toString()}`,
|
||||||
endpoint: `${config.url.BASE}rsforms/${schema}/cst-multidelete/`,
|
endpoint: `${config.url.BASE}rsforms/${schema}/cst-multidelete/`,
|
||||||
request: request
|
request: request
|
||||||
|
|
|
@ -207,7 +207,7 @@ export function getCstTypePrefix(type: CstType) {
|
||||||
case CstType.CONSTANT: return 'C';
|
case CstType.CONSTANT: return 'C';
|
||||||
case CstType.STRUCTURED: return 'S';
|
case CstType.STRUCTURED: return 'S';
|
||||||
case CstType.AXIOM: return 'A';
|
case CstType.AXIOM: return 'A';
|
||||||
case CstType.TERM: return 'T';
|
case CstType.TERM: return 'D';
|
||||||
case CstType.FUNCTION: return 'F';
|
case CstType.FUNCTION: return 'F';
|
||||||
case CstType.PREDICATE: return 'P';
|
case CstType.PREDICATE: return 'P';
|
||||||
case CstType.THEOREM: return 'T';
|
case CstType.THEOREM: return 'T';
|
||||||
|
@ -259,17 +259,16 @@ export function extractGlobals(expression: string): Set<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAliasFor(type: CstType, schema: IRSForm): string {
|
export function createAliasFor(type: CstType, schema: IRSForm): string {
|
||||||
let index = 1;
|
|
||||||
let prefix = getCstTypePrefix(type);
|
let prefix = getCstTypePrefix(type);
|
||||||
let name = prefix + index;
|
if (!schema.items || schema.items.length <= 0) {
|
||||||
if (schema.items && schema.items.length > 0) {
|
return `${prefix}1`;
|
||||||
for (let i = 0; i < schema.items.length; ++i) {
|
|
||||||
if (schema.items[i].alias === name) {
|
|
||||||
++index;
|
|
||||||
name = prefix + index;
|
|
||||||
i = 0;
|
|
||||||
}
|
}
|
||||||
|
const index = schema.items.reduce((prev, cst, index) => {
|
||||||
|
if (cst.cstType !== type) {
|
||||||
|
return prev;
|
||||||
}
|
}
|
||||||
}
|
index = Number(cst.alias.slice(1 - cst.alias.length)) + 1;
|
||||||
return name;
|
return Math.max(prev, index);
|
||||||
|
}, 1);
|
||||||
|
return `${prefix}${index}`;
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user