This commit is contained in:
Ulle9 2023-08-27 19:47:26 +03:00
commit 029298a9d3
61 changed files with 848 additions and 415 deletions

View File

@ -29,5 +29,13 @@
"name": "django", "name": "django",
"depth": 5 "depth": 5
} }
],
"colorize.include": [".tsx", ".jsx", ".ts", ".js"],
"colorize.languages": [
"typescript",
"javascript",
"css",
"typescriptreact",
"javascriptreact"
] ]
} }

View File

@ -53,7 +53,7 @@ This readme file is used mostly to document project dependencies
<summary>VS Code plugins</summary> <summary>VS Code plugins</summary>
<pre> <pre>
- ESLint - ESLint
- - Colorize
</pre> </pre>
</details> </details>

View File

@ -4,8 +4,6 @@ For more specific TODOs see comments in code
[Functionality] [Functionality]
- home page - home page
- manuals
- текстовый модуль для разрешения отсылок
- блок нотификаций пользователей - блок нотификаций пользователей
- блок синтеза - блок синтеза
- блок организации библиотеки моделей - блок организации библиотеки моделей
@ -13,9 +11,6 @@ For more specific TODOs see comments in code
- обратная связь - система баг репортов - обратная связь - система баг репортов
[Tech] [Tech]
- Use migtation/fixtures to provide initial data for testing
- USe migtation/fixtures to load example common data
- create custom Select component - create custom Select component
- reload react-data-table-component - reload react-data-table-component

View File

@ -58,7 +58,7 @@ RUN pip install --no-cache /wheels/* && \
COPY apps/ ./apps COPY apps/ ./apps
COPY cctext/ ./cctext COPY cctext/ ./cctext
COPY project/ ./project COPY project/ ./project
COPY data/ ./data COPY fixtures/ ./fixtures
COPY manage.py entrypoint.sh ./ COPY manage.py entrypoint.sh ./
RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.sh && \ RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.sh && \
chmod +x $APP_HOME/entrypoint.sh && \ chmod +x $APP_HOME/entrypoint.sh && \

View File

@ -24,7 +24,7 @@ class LibraryActiveView(generics.ListAPIView):
def get_queryset(self): def get_queryset(self):
user = self.request.user user = self.request.user
if not user.is_anonymous: if not user.is_anonymous:
# pyling: disable=unsupported-binary-operation # pylint: disable=unsupported-binary-operation
return m.LibraryItem.objects.filter(Q(is_common=True) | Q(owner=user) | Q(subscription__user=user)) return m.LibraryItem.objects.filter(Q(is_common=True) | Q(owner=user) | Q(subscription__user=user))
else: else:
return m.LibraryItem.objects.filter(is_common=True) return m.LibraryItem.objects.filter(is_common=True)

View File

@ -16,7 +16,7 @@ import RSFormPage from './pages/RSFormPage';
import UserProfilePage from './pages/UserProfilePage'; import UserProfilePage from './pages/UserProfilePage';
function App () { function App () {
const { noNavigation, viewportHeight, mainHeight } = useConceptTheme(); const { noNavigation, noFooter, viewportHeight, mainHeight } = useConceptTheme();
return ( return (
<div className='antialiased clr-app'> <div className='antialiased clr-app'>
<Navigation /> <Navigation />
@ -45,7 +45,7 @@ function App () {
<Route path='*' element={ <NotFoundPage/>} /> <Route path='*' element={ <NotFoundPage/>} />
</Routes> </Routes>
</main> </main>
{!noNavigation && <Footer />} {!noNavigation && !noFooter && <Footer />}
</div> </div>
</div> </div>
); );

View File

@ -1,6 +1,7 @@
import DataTable, { createTheme, type TableProps } from 'react-data-table-component'; import DataTable, { createTheme, type TableProps } from 'react-data-table-component';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import { dataTableDarkT, dataTableLightT } from '../../utils/color';
export interface SelectionInfo<T> { export interface SelectionInfo<T> {
allSelected: boolean allSelected: boolean
@ -8,47 +9,21 @@ export interface SelectionInfo<T> {
selectedRows: T[] selectedRows: T[]
} }
createTheme('customDark', { createTheme('customDark', dataTableDarkT, 'dark');
text: { createTheme('customLight', dataTableLightT, 'light');
primary: 'rgba(228, 228, 231, 1)',
secondary: 'rgba(228, 228, 231, 0.87)',
disabled: 'rgba(228, 228, 231, 0.54)'
},
background: {
default: '#111827'
},
highlightOnHover: {
default: '#4d6080',
text: 'rgba(228, 228, 231, 1)'
},
divider: {
default: '#6b6b6b'
},
striped: {
default: '#374151',
text: 'rgba(228, 228, 231, 1)'
},
selected: {
default: '#4d6080',
text: 'rgba(228, 228, 231, 1)'
}
}, 'dark');
createTheme('customLight', { interface ConceptDataTableProps<T>
divider: { extends Omit<TableProps<T>, 'paginationComponentOptions'> {}
default: '#d1d5db'
},
striped: {
default: '#f0f2f7'
},
}, 'light');
function ConceptDataTable<T>({ theme, ...props }: TableProps<T>) { function ConceptDataTable<T>({ theme, ...props }: ConceptDataTableProps<T>) {
const { darkMode } = useConceptTheme(); const { darkMode } = useConceptTheme();
return ( return (
<DataTable<T> <DataTable<T>
theme={ theme ?? (darkMode ? 'customDark' : 'customLight')} theme={ theme ?? (darkMode ? 'customDark' : 'customLight')}
paginationComponentOptions={{
rowsPerPageText: 'строк на страницу'
}}
{...props} {...props}
/> />
); );

View File

@ -1,11 +1,15 @@
import type { TabProps } from 'react-tabs'; import type { TabProps } from 'react-tabs';
import { Tab } from 'react-tabs'; import { Tab } from 'react-tabs';
function ConceptTab({ children, className, ...otherProps }: TabProps) { interface ConceptTabProps
extends Omit<TabProps, 'className'> {
className?: string
}
function ConceptTab({ children, className, ...otherProps }: ConceptTabProps) {
return ( return (
<Tab <Tab
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions className={`px-2 py-1 text-sm hover:cursor-pointer clr-tab whitespace-nowrap ${className}`}
className={`px-2 py-1 border-r-2 text-sm hover:cursor-pointer clr-tab whitespace-nowrap ${className}`}
{...otherProps} {...otherProps}
> >
{children} {children}

View File

@ -6,8 +6,8 @@ interface DropdownProps {
function Dropdown({ children, widthClass = 'w-fit', stretchLeft }: DropdownProps) { function Dropdown({ children, widthClass = 'w-fit', stretchLeft }: DropdownProps) {
return ( return (
<div className='relative'> <div className='relative text-sm'>
<div className={`absolute ${stretchLeft ? 'right-0' : 'left-0'} py-2 z-40 flex flex-col items-stretch justify-start px-2 mt-2 text-sm origin-top-right bg-white border border-gray-100 divide-y rounded-md shadow-lg dark:border-gray-500 dark:bg-gray-900 ${widthClass}`}> <div className={`absolute ${stretchLeft ? 'right-0' : 'left-0'} mt-2 z-40 flex flex-col items-stretch justify-start origin-top-right border divide-y rounded-md shadow-lg clr-input clr-border ${widthClass}`}>
{children} {children}
</div> </div>
</div> </div>

View File

@ -46,7 +46,7 @@ function Modal({
<div className='max-h-[calc(95vh-15rem)]'> <div className='max-h-[calc(95vh-15rem)]'>
{children} {children}
</div> </div>
<div className='flex justify-center w-full gap-4 pt-4 mt-2 border-t-4'> <div className='flex justify-center w-full gap-4 pt-4 mt-2 border-t-4 clr-border'>
{!readonly && {!readonly &&
<Button <Button
text={submitText} text={submitText}

View File

@ -27,7 +27,7 @@ function TextInput({
/> />
<input id={id} <input id={id}
title={tooltip} title={tooltip}
className={`px-3 py-2 mt-2 leading-tight border shadow truncate hover:text-clip ${colorClass} ${singleRow ? '' : widthClass}`} className={`px-3 py-2 leading-tight border shadow truncate hover:text-clip ${colorClass} ${singleRow ? '' : 'mt-2 ' + widthClass}`}
required={required} required={required}
{...props} {...props}
/> />

View File

@ -11,7 +11,7 @@ function Footer() {
<Link to='/manuals' tabIndex={-1}>Справка</Link> <br/> <Link to='/manuals' tabIndex={-1}>Справка</Link> <br/>
</div> </div>
<div className=''> <div className=''>
<a href={urls.concept} tabIndex={-1} className='underline'>Центр Концепт</a> <p className='w-full text-center underline'><a href={urls.concept} tabIndex={-1} >Центр Концепт</a></p>
<p className='mt-0.5 text-center'>© 2023 ЦИВТ КОНЦЕПТ</p> <p className='mt-0.5 text-center'>© 2023 ЦИВТ КОНЦЕПТ</p>
</div> </div>
<div className='flex flex-col underline'> <div className='flex flex-col underline'>

View File

@ -1,4 +1,5 @@
import { urls } from '../../utils/constants'; import { urls } from '../../utils/constants';
import { LibraryFilterStrategy } from '../../utils/models';
import TextURL from '../Common/TextURL'; import TextURL from '../Common/TextURL';
function HelpMain() { function HelpMain() {
@ -9,8 +10,9 @@ function HelpMain() {
<p>Навигация по порталу осуществляется верхнюю панель или ссылки в "подвале" страницы. Их можно скрыть с помощью кнопки в правом верхнем углу</p> <p>Навигация по порталу осуществляется верхнюю панель или ссылки в "подвале" страницы. Их можно скрыть с помощью кнопки в правом верхнем углу</p>
<p>В меню пользователя (правый верхний угол) редактирование данных пользователя и изменение цветовой темы.</p> <p>В меню пользователя (правый верхний угол) редактирование данных пользователя и изменение цветовой темы.</p>
<p className='mt-2'><b>Основные разделы Портала</b></p> <p className='mt-2'><b>Основные разделы Портала</b></p>
<li><TextURL text='Библиотека' href='/library?filter=common' /> - общедоступные схемы и инструменты поиска и навигации по ним</li> <li><TextURL text='Библиотека' href='/library' /> - все схемы доступные пользователю</li>
<li><TextURL text='Мои схемы' href='/library?filter=personal' /> - отслеживаемые и редактируемые схемы. Основной рабочий раздел</li> <li><TextURL text='Общие схемы' href={`/library?filter=${LibraryFilterStrategy.COMMON}`} /> - общедоступные схемы и инструменты поиска и навигации по ним</li>
<li><TextURL text='Мои схемы' href={`/library?filter=${LibraryFilterStrategy.PERSONAL}`} /> - отслеживаемые и редактируемые схемы. Основной рабочий раздел</li>
<li><TextURL text='Профиль' href='/profile' /> - данные пользователя и смена пароля</li> <li><TextURL text='Профиль' href='/profile' /> - данные пользователя и смена пароля</li>
<p className='mt-2'><b>Поддержка Портала</b></p> <p className='mt-2'><b>Поддержка Портала</b></p>
<p>Портал разрабатывается <TextURL text='Центром Концепт' href={urls.concept}/> и является проектом с открытым исходным кодом, доступным на <TextURL text='Github' href={urls.gitrepo}/>.</p> <p>Портал разрабатывается <TextURL text='Центром Концепт' href={urls.concept}/> и является проектом с открытым исходным кодом, доступным на <TextURL text='Github' href={urls.gitrepo}/>.</p>

View File

@ -6,6 +6,7 @@ function HelpRSFormMeta() {
<p>Для <b>общедоступных</b> схем владельцем может стать любой пользователь</p> <p>Для <b>общедоступных</b> схем владельцем может стать любой пользователь</p>
<p>Для <b>библиотечных</b> схем правом редактирования обладают только администраторы</p> <p>Для <b>библиотечных</b> схем правом редактирования обладают только администраторы</p>
<p><b>Клонировать</b> - создать копию схемы для дальнейшего редактирования</p> <p><b>Клонировать</b> - создать копию схемы для дальнейшего редактирования</p>
<p><b>Отслеживание</b> - возможность видеть схему в Библиотеке и использовать фильтры</p>
<p><b>Загрузить/Выгрузить схему</b> - взаимодействие с Экстеор через файлы формата TRS</p> <p><b>Загрузить/Выгрузить схему</b> - взаимодействие с Экстеор через файлы формата TRS</p>
</div> </div>
); );

View File

@ -1,19 +1,26 @@
import { useConceptTheme } from '../../context/ThemeContext';
import { prefixes } from '../../utils/constants'; import { prefixes } from '../../utils/constants';
import { mapCstClassInfo } from '../../utils/staticUI'; import { getCstClassColor, mapCstClassInfo } from '../../utils/staticUI';
interface InfoCstClassProps { interface InfoCstClassProps {
title?: string title?: string
} }
function InfoCstClass({ title }: InfoCstClassProps) { function InfoCstClass({ title }: InfoCstClassProps) {
const { colors } = useConceptTheme();
return ( return (
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
{ title && <h1>{title}</h1>} { title && <h1>{title}</h1>}
{ [... mapCstClassInfo.values()].map( { [... mapCstClassInfo.entries()].map(
(info, index) => { ([cstClass, info], index) => {
return ( return (
<p key={`${prefixes.cst_status_list}${index}`}> <p key={`${prefixes.cst_status_list}${index}`}>
<span className={`px-1 inline-block font-semibold min-w-[6.5rem] text-center border ${info.color}`}> <span
className='px-1 inline-block font-semibold min-w-[6.5rem] text-center borde'
style={{backgroundColor: getCstClassColor(cstClass, colors)}}
>
{info.text} {info.text}
</span> </span>
<span> - </span> <span> - </span>

View File

@ -1,19 +1,25 @@
import { useConceptTheme } from '../../context/ThemeContext';
import { prefixes } from '../../utils/constants'; import { prefixes } from '../../utils/constants';
import { mapStatusInfo } from '../../utils/staticUI'; import { getCstStatusColor, mapStatusInfo } from '../../utils/staticUI';
interface InfoCstStatusProps { interface InfoCstStatusProps {
title?: string title?: string
} }
function InfoCstStatus({ title }: InfoCstStatusProps) { function InfoCstStatus({ title }: InfoCstStatusProps) {
const { colors } = useConceptTheme();
return ( return (
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
{ title && <h1>{title}</h1>} { title && <h1>{title}</h1>}
{ [... mapStatusInfo.values()].map( { [... mapStatusInfo.entries()].map(
(info, index) => { ([status, info], index) => {
return ( return (
<p key={`${prefixes.cst_status_list}${index}`}> <p key={`${prefixes.cst_status_list}${index}`}>
<span className={`px-1 inline-block font-semibold min-w-[4rem] text-center border ${info.color}`}> <span
className='px-1 inline-block font-semibold min-w-[4rem] text-center border'
style={{backgroundColor: getCstStatusColor(status, colors)}}
>
{info.text} {info.text}
</span> </span>
<span> - </span> <span> - </span>

View File

@ -8,7 +8,7 @@ function Logo({ title }: LogoProps) {
return ( return (
<Link to='/' className='flex items-center mr-4' tabIndex={-1}> <Link to='/' className='flex items-center mr-4' tabIndex={-1}>
<img src='/favicon.svg' className='min-h-[2rem] mr-2 min-w-[2rem]' alt=''/> <img src='/favicon.svg' className='min-h-[2rem] mr-2 min-w-[2rem]' alt=''/>
<span className='self-center hidden text-xl font-semibold lg:block whitespace-nowrap dark:text-white'>{title}</span> <span className='self-center hidden text-xl font-semibold md:block whitespace-nowrap dark:text-white'>{title}</span>
</Link> </Link>
); );
} }

View File

@ -10,8 +10,8 @@ function Navigation () {
const navigate = useNavigate(); const navigate = useNavigate();
const { noNavigation, toggleNoNavigation } = useConceptTheme(); const { noNavigation, toggleNoNavigation } = useConceptTheme();
const navigateCommon = () => { navigate('/library?filter=common') }; const navigateLibrary = () => navigate('/library');
const navigateHelp = () => { navigate('/manuals') }; const navigateHelp = () => navigate('/manuals');
return ( return (
<nav className='sticky top-0 left-0 right-0 z-50 select-none h-fit'> <nav className='sticky top-0 left-0 right-0 z-50 select-none h-fit'>
@ -42,7 +42,7 @@ function Navigation () {
text='Библиотека' text='Библиотека'
description='Библиотека концептуальных схем' description='Библиотека концептуальных схем'
icon={<LibraryIcon />} icon={<LibraryIcon />}
onClick={navigateCommon} onClick={navigateLibrary}
/> />
<NavigationButton <NavigationButton
text='Справка' text='Справка'

View File

@ -2,6 +2,7 @@ import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import { LibraryFilterStrategy } from '../../utils/models';
import Dropdown from '../Common/Dropdown'; import Dropdown from '../Common/Dropdown';
import DropdownButton from '../Common/DropdownButton'; import DropdownButton from '../Common/DropdownButton';
@ -15,27 +16,33 @@ function UserDropdown({ hideDropdown }: UserDropdownProps) {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const navigateProfile = () => { const navigateProfile = () => {
hideDropdown() hideDropdown();
navigate('/profile'); navigate('/profile');
}; };
const logoutAndRedirect = const logoutAndRedirect =
() => { () => {
hideDropdown(); hideDropdown();
logout(() => { navigate('/login/'); }) logout(() => navigate('/login/'));
}; };
const navigateMyWork = () => { const navigateMyWork = () => {
hideDropdown(); hideDropdown();
navigate('/library?filter=personal'); navigate(`/library?filter=${LibraryFilterStrategy.PERSONAL}`);
}; };
return ( return (
<Dropdown widthClass='w-36' stretchLeft> <Dropdown widthClass='w-36' stretchLeft>
<DropdownButton description='Профиль пользователя' onClick={navigateProfile}> <DropdownButton
description='Профиль пользователя'
onClick={navigateProfile}
>
{user?.username} {user?.username}
</DropdownButton> </DropdownButton>
<DropdownButton description='Переключение темы оформления' onClick={toggleDarkMode}> <DropdownButton
description='Переключение темы оформления'
onClick={toggleDarkMode}
>
{darkMode ? 'Светлая тема' : 'Темная тема'} {darkMode ? 'Светлая тема' : 'Темная тема'}
</DropdownButton> </DropdownButton>
<DropdownButton onClick={navigateMyWork}> <DropdownButton onClick={navigateMyWork}>

View File

@ -30,7 +30,7 @@ function UserMenu() {
</div> </div>
{ user && menu.isActive && { user && menu.isActive &&
<UserDropdown <UserDropdown
hideDropdown={() => { menu.hide(); }} hideDropdown={() => menu.hide()}
/>} />}
</div> </div>
); );

View File

@ -1,6 +1,8 @@
import { bracketMatching, MatchResult } from '@codemirror/language'; import { bracketMatching, MatchResult } from '@codemirror/language';
import { Decoration, EditorView } from '@codemirror/view'; import { Decoration, EditorView } from '@codemirror/view';
import { bracketsDarkT, bracketsLightT } from '../../utils/color';
const matchingMark = Decoration.mark({class: "cc-matchingBracket"}); const matchingMark = Decoration.mark({class: "cc-matchingBracket"});
const nonmatchingMark = Decoration.mark({class: "cc-nonmatchingBracket"}); const nonmatchingMark = Decoration.mark({class: "cc-nonmatchingBracket"});
@ -14,31 +16,9 @@ function bracketRender(match: MatchResult) {
return decorations; return decorations;
} }
const darkTheme = EditorView.baseTheme({ const darkTheme = EditorView.baseTheme(bracketsDarkT);
'.cc-matchingBracket': {
fontWeight: 600,
},
'.cc-nonmatchingBracket': {
color: '#ef4444',
fontWeight: 700,
},
'&.cm-focused .cc-matchingBracket': {
backgroundColor: '#734f00',
},
});
const lightTheme = EditorView.baseTheme({ const lightTheme = EditorView.baseTheme(bracketsLightT);
'.cc-matchingBracket': {
fontWeight: 600,
},
'.cc-nonmatchingBracket': {
color: '#ef4444',
fontWeight: 700,
},
'&.cm-focused .cc-matchingBracket': {
backgroundColor: '#dae6f2',
},
});
export function ccBracketMatching(darkMode: boolean) { export function ccBracketMatching(darkMode: boolean) {
return [bracketMatching({ renderMatch: bracketRender }), darkMode ? darkTheme : lightTheme]; return [bracketMatching({ renderMatch: bracketRender }), darkMode ? darkTheme : lightTheme];

View File

@ -54,7 +54,7 @@ function RSInput({
id, label, innerref, onChange, editable, id, label, innerref, onChange, editable,
...props ...props
}: RSInputProps) { }: RSInputProps) {
const { darkMode } = useConceptTheme(); const { darkMode, colors } = useConceptTheme();
const { schema } = useRSForm(); const { schema } = useRSForm();
const internalRef = useRef<ReactCodeMirrorRef>(null); const internalRef = useRef<ReactCodeMirrorRef>(null);
@ -69,9 +69,9 @@ function RSInput({
theme: 'light', theme: 'light',
settings: { settings: {
fontFamily: 'inherit', fontFamily: 'inherit',
background: editable ? '#ffffff' : '#f0f2f7', background: editable ? colors.input : colors.inputDisabled,
foreground: '#000000', foreground: colors.text,
selection: '#aacef2', selection: colors.selection,
caret: '#5d00ff', caret: '#5d00ff',
}, },
styles: [ styles: [
@ -83,16 +83,16 @@ function RSInput({
{ tag: t.controlKeyword, class: 'font-semibold'}, // R | I | D { tag: t.controlKeyword, class: 'font-semibold'}, // R | I | D
{ tag: t.unit, class: 'text-[0.75rem]' }, // indicies { tag: t.unit, class: 'text-[0.75rem]' }, // indicies
] ]
}), [editable]); }), [editable, colors]);
const darkTheme: Extension = useMemo( const darkTheme: Extension = useMemo(
() => createTheme({ () => createTheme({
theme: 'dark', theme: 'dark',
settings: { settings: {
fontFamily: 'inherit', fontFamily: 'inherit',
background: editable ? '#070b12' : '#374151', background: editable ? colors.input : colors.inputDisabled,
foreground: '#e4e4e7', foreground: colors.text,
selection: '#8c6000', selection: colors.selection,
caret: '#ffaa00' caret: '#ffaa00'
}, },
styles: [ styles: [
@ -104,7 +104,7 @@ function RSInput({
{ tag: t.controlKeyword, class: 'font-semibold'}, // R | I | D { tag: t.controlKeyword, class: 'font-semibold'}, // R | I | D
{ tag: t.unit, class: 'text-[0.75rem]' }, // indicies { tag: t.unit, class: 'text-[0.75rem]' }, // indicies
] ]
}), [editable]); }), [editable, colors]);
const editorExtensions = useMemo( const editorExtensions = useMemo(
() => [ () => [

View File

@ -3,7 +3,8 @@ import { createContext, useCallback, useContext, useLayoutEffect, useState } fro
import { type ErrorInfo } from '../components/BackendError'; import { type ErrorInfo } from '../components/BackendError';
import useLocalStorage from '../hooks/useLocalStorage'; import useLocalStorage from '../hooks/useLocalStorage';
import { type DataCallback, getAuth, patchPassword,postLogin, postLogout, postSignup } from '../utils/backendAPI'; import { type DataCallback, getAuth, patchPassword,postLogin, postLogout, postSignup } from '../utils/backendAPI';
import { ICurrentUser, IUserLoginData, IUserProfile, IUserSignupData, IUserUpdatePassword } from '../utils/models'; import { ICurrentUser, IUserInfo, IUserLoginData, IUserProfile, IUserSignupData, IUserUpdatePassword } from '../utils/models';
import { useUsers } from './UsersContext';
interface IAuthContext { interface IAuthContext {
user: ICurrentUser | undefined user: ICurrentUser | undefined
@ -32,6 +33,7 @@ interface AuthStateProps {
} }
export const AuthState = ({ children }: AuthStateProps) => { export const AuthState = ({ children }: AuthStateProps) => {
const { users } = useUsers();
const [user, setUser] = useLocalStorage<ICurrentUser | undefined>('user', undefined); const [user, setUser] = useLocalStorage<ICurrentUser | undefined>('user', undefined);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
@ -82,6 +84,7 @@ export const AuthState = ({ children }: AuthStateProps) => {
setLoading: setLoading, setLoading: setLoading,
onError: error => setError(error), onError: error => setError(error),
onSuccess: newData => reload(() => { onSuccess: newData => reload(() => {
users.push(newData as IUserInfo);
if (callback) callback(newData); if (callback) callback(newData);
}) })
}); });

View File

@ -43,16 +43,23 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
const filter = useCallback( const filter = useCallback(
(params: ILibraryFilter) => { (params: ILibraryFilter) => {
let result = items; let result = items;
if (params.ownedBy) { if (params.is_owned) {
result = result.filter(item => result = result.filter(item => item.owner === user?.id);
item.owner === params.ownedBy
|| user?.subscriptions.includes(item.id));
} }
if (params.is_common !== undefined) { if (params.is_common !== undefined) {
result = result.filter(item => item.is_common === params.is_common); result = result.filter(item => item.is_common === params.is_common);
} }
if (params.queryMeta) { if (params.is_canonical !== undefined) {
result = result.filter(item => matchLibraryItem(params.queryMeta!, item)); result = result.filter(item => item.is_canonical === params.is_canonical);
}
if (params.is_subscribed !== undefined) {
result = result.filter(item => user?.subscriptions.includes(item.id));
}
if (params.is_personal !== undefined) {
result = result.filter(item => user?.subscriptions.includes(item.id) || item.owner === user?.id);
}
if (params.query) {
result = result.filter(item => matchLibraryItem(params.query!, item));
} }
return result; return result;
}, [items, user]); }, [items, user]);
@ -84,12 +91,14 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => setError(error),
onSuccess: newSchema => { onSuccess: newSchema => reload(() => {
reload(); if (user && !user.subscriptions.includes(newSchema.id)) {
user.subscriptions.push(newSchema.id);
}
if (callback) callback(newSchema); if (callback) callback(newSchema);
} })
}); });
}, [reload]); }, [reload, user]);
const destroySchema = useCallback( const destroySchema = useCallback(
(target: number, callback?: () => void) => { (target: number, callback?: () => void) => {
@ -99,10 +108,13 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => setError(error),
onSuccess: () => reload(() => { onSuccess: () => reload(() => {
if (user && user.subscriptions.includes(target)) {
user.subscriptions.splice(user.subscriptions.findIndex(item => item === target), 1);
}
if (callback) callback(); if (callback) callback();
}) })
}); });
}, [setError, reload]); }, [setError, reload, user]);
const cloneSchema = useCallback( const cloneSchema = useCallback(
(target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => { (target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => {
@ -116,6 +128,9 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => setError(error),
onSuccess: newSchema => reload(() => { onSuccess: newSchema => reload(() => {
if (user && !user.subscriptions.includes(newSchema.id)) {
user.subscriptions.push(newSchema.id);
}
if (callback) callback(newSchema); if (callback) callback(newSchema);
}) })
}); });

View File

@ -15,6 +15,7 @@ import {
ILibraryUpdateData, IRSForm, IRSFormUploadData ILibraryUpdateData, IRSForm, IRSFormUploadData
} from '../utils/models' } from '../utils/models'
import { useAuth } from './AuthContext' import { useAuth } from './AuthContext'
import { useLibrary } from './LibraryContext'
interface IRSFormContext { interface IRSFormContext {
schema?: IRSForm schema?: IRSForm
@ -66,6 +67,7 @@ interface RSFormStateProps {
} }
export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
const library = useLibrary();
const { user } = useAuth(); const { user } = useAuth();
const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID }); const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID });
const [ processing, setProcessing ] = useState(false); const [ processing, setProcessing ] = useState(false);
@ -114,10 +116,14 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
onError: error => setError(error), onError: error => setError(error),
onSuccess: newData => { onSuccess: newData => {
setSchema(Object.assign(schema, newData)); setSchema(Object.assign(schema, newData));
const libraryItem = library.items.find(item => item.id === newData.id);
if (libraryItem) {
Object.assign(libraryItem, newData);
}
if (callback) callback(newData); if (callback) callback(newData);
} }
}); });
}, [schemaID, setError, setSchema, schema]); }, [schemaID, setError, setSchema, schema, library]);
const upload = useCallback( const upload = useCallback(
(data: IRSFormUploadData, callback?: () => void) => { (data: IRSFormUploadData, callback?: () => void) => {
@ -132,10 +138,14 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
onError: error => setError(error), onError: error => setError(error),
onSuccess: newData => { onSuccess: newData => {
setSchema(newData); setSchema(newData);
const libraryItem = library.items.find(item => item.id === newData.id);
if (libraryItem) {
Object.assign(libraryItem, newData);
}
if (callback) callback(); if (callback) callback();
} }
}); });
}, [schemaID, setError, setSchema, schema]); }, [schemaID, setError, setSchema, schema, library]);
const claim = useCallback( const claim = useCallback(
(callback?: DataCallback<ILibraryItem>) => { (callback?: DataCallback<ILibraryItem>) => {
@ -149,10 +159,17 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
onError: error => setError(error), onError: error => setError(error),
onSuccess: newData => { onSuccess: newData => {
setSchema(Object.assign(schema, newData)); setSchema(Object.assign(schema, newData));
const libraryItem = library.items.find(item => item.id === newData.id);
if (libraryItem) {
libraryItem.owner = user.id
}
if (!user.subscriptions.includes(schema.id)) {
user.subscriptions.push(schema.id);
}
if (callback) callback(newData); if (callback) callback(newData);
} }
}); });
}, [schemaID, setError, schema, user, setSchema]); }, [schemaID, setError, schema, user, setSchema, library]);
const subscribe = useCallback( const subscribe = useCallback(
(callback?: () => void) => { (callback?: () => void) => {
@ -266,9 +283,9 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => setError(error),
onSuccess: newData => { onSuccess: newData => reload(setProcessing, () => {
reload(setProcessing, () => { if (callback) callback(newData); }) if (callback) callback(newData);
} })
}); });
}, [setError, reload]); }, [setError, reload]);

View File

@ -1,13 +1,20 @@
import { createContext, useContext, useLayoutEffect, useMemo, useState } from 'react'; import { createContext, useContext, useLayoutEffect, useMemo, useState } from 'react';
import useLocalStorage from '../hooks/useLocalStorage'; import useLocalStorage from '../hooks/useLocalStorage';
import { darkT, IColorTheme, lightT } from '../utils/color';
interface IThemeContext { interface IThemeContext {
darkMode: boolean
noNavigation: boolean
viewportHeight: string viewportHeight: string
mainHeight: string mainHeight: string
colors: IColorTheme
darkMode: boolean
toggleDarkMode: () => void toggleDarkMode: () => void
noNavigation: boolean
noFooter: boolean
setNoFooter: (value: boolean) => void
toggleNoNavigation: () => void toggleNoNavigation: () => void
} }
@ -15,9 +22,7 @@ const ThemeContext = createContext<IThemeContext | null>(null);
export const useConceptTheme = () => { export const useConceptTheme = () => {
const context = useContext(ThemeContext); const context = useContext(ThemeContext);
if (!context) { if (!context) {
throw new Error( throw new Error('useConceptTheme has to be used within <ThemeState.Provider>');
'useConceptTheme has to be used within <ThemeState.Provider>'
);
} }
return context; return context;
} }
@ -28,9 +33,11 @@ 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 [colors, setColors] = useState<IColorTheme>(lightT);
const [noNavigation, setNoNavigation] = useState(false); const [noNavigation, setNoNavigation] = useState(false);
const [noFooter, setNoFooter] = useState(false);
const setDarkClass = (isDark: boolean) => { function setDarkClass(isDark: boolean) {
const root = window.document.documentElement; const root = window.document.documentElement;
if (isDark) { if (isDark) {
root.classList.add('dark'); root.classList.add('dark');
@ -38,12 +45,16 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
root.classList.remove('dark'); root.classList.remove('dark');
} }
root.setAttribute('data-color-scheme', !isDark ? 'light' : 'dark'); root.setAttribute('data-color-scheme', !isDark ? 'light' : 'dark');
}; }
useLayoutEffect(() => { useLayoutEffect(() => {
setDarkClass(darkMode) setDarkClass(darkMode)
}, [darkMode]); }, [darkMode]);
useLayoutEffect(() => {
setColors(darkMode ? darkT : lightT)
}, [darkMode, setColors]);
const mainHeight = useMemo( const mainHeight = useMemo(
() => { () => {
return !noNavigation ? return !noNavigation ?
@ -60,10 +71,11 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
return ( return (
<ThemeContext.Provider value={{ <ThemeContext.Provider value={{
darkMode, darkMode, colors,
noNavigation, noNavigation, noFooter,
toggleDarkMode: () => setDarkMode(prev => !prev), toggleDarkMode: () => setDarkMode(prev => !prev),
toggleNoNavigation: () => setNoNavigation(prev => !prev), toggleNoNavigation: () => setNoNavigation(prev => !prev),
setNoFooter,
viewportHeight, mainHeight viewportHeight, mainHeight
}}> }}>
{children} {children}

View File

@ -3,6 +3,7 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea
import { ErrorInfo } from '../components/BackendError'; import { ErrorInfo } from '../components/BackendError';
import { DataCallback, getProfile, patchProfile } from '../utils/backendAPI'; import { DataCallback, getProfile, patchProfile } from '../utils/backendAPI';
import { IUserProfile, IUserUpdateData } from '../utils/models'; import { IUserProfile, IUserUpdateData } from '../utils/models';
import { useUsers } from './UsersContext';
interface IUserProfileContext { interface IUserProfileContext {
user: IUserProfile | undefined user: IUserProfile | undefined
@ -30,39 +31,43 @@ interface UserProfileStateProps {
} }
export const UserProfileState = ({ children }: UserProfileStateProps) => { export const UserProfileState = ({ children }: UserProfileStateProps) => {
const { users } = useUsers();
const [user, setUser] = useState<IUserProfile | undefined>(undefined); const [user, setUser] = useState<IUserProfile | undefined>(undefined);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
const reload = useCallback( const reload = useCallback(
() => { () => {
setError(undefined); setError(undefined);
setUser(undefined); setUser(undefined);
getProfile({ getProfile({
showError: true, showError: true,
setLoading: setLoading, setLoading: setLoading,
onError: error => { setError(error); }, onError: error => setError(error),
onSuccess: newData => { setUser(newData); } onSuccess: newData => setUser(newData)
}); });
}, [setUser] }, [setUser]);
);
const updateUser = useCallback( const updateUser = useCallback(
(data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => { (data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => {
setError(undefined); setError(undefined);
patchProfile({ patchProfile({
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => { setError(error); }, onError: error => setError(error),
onSuccess: newData => { onSuccess: newData => {
setUser(newData); setUser(newData);
if (callback) callback(newData); const libraryUser = users.find(item => item.id === user?.id);
if (libraryUser) {
libraryUser.first_name = newData.first_name;
libraryUser.last_name = newData.last_name;
} }
}); if (callback) callback(newData);
}, [setUser] }
); });
}, [setUser, users]);
useEffect(() => { useEffect(() => {
reload(); reload();

View File

@ -13,9 +13,7 @@ const UsersContext = createContext<IUsersContext | null>(null)
export const useUsers = (): IUsersContext => { export const useUsers = (): IUsersContext => {
const context = useContext(UsersContext); const context = useContext(UsersContext);
if (context === null) { if (context === null) {
throw new Error( throw new Error('useUsers has to be used within <UsersState.Provider>');
'useUsers has to be used within <UsersState.Provider>'
);
} }
return context; return context;
} }

View File

@ -6,23 +6,52 @@ import { RSErrorType } from '../utils/enums';
import { CstType, IConstituenta, IExpressionParse, IFunctionArg, type IRSForm } from '../utils/models'; import { CstType, IConstituenta, IExpressionParse, IFunctionArg, type IRSForm } from '../utils/models';
import { getCstExpressionPrefix } from '../utils/staticUI'; import { getCstExpressionPrefix } from '../utils/staticUI';
const LOGIC_TYPIIFCATION = 'LOGIC';
function checkTypeConsistency(type: CstType, typification: string, args: IFunctionArg[]): boolean { function checkTypeConsistency(type: CstType, typification: string, args: IFunctionArg[]): boolean {
console.log(typification)
switch (type) { switch (type) {
case CstType.BASE: case CstType.BASE:
case CstType.CONSTANT: case CstType.CONSTANT:
case CstType.STRUCTURED: case CstType.STRUCTURED:
case CstType.TERM: case CstType.TERM:
return typification !== '' && args.length === 0; return typification !== LOGIC_TYPIIFCATION && args.length === 0;
case CstType.AXIOM: case CstType.AXIOM:
case CstType.THEOREM: case CstType.THEOREM:
return typification === '' && args.length === 0; return typification === LOGIC_TYPIIFCATION && args.length === 0;
case CstType.FUNCTION: case CstType.FUNCTION:
return typification !== '' && args.length !== 0; return typification !== LOGIC_TYPIIFCATION && args.length !== 0;
case CstType.PREDICATE: case CstType.PREDICATE:
return typification === '' && args.length !== 0; return typification === LOGIC_TYPIIFCATION && args.length !== 0;
}
}
function adjustResults(parse: IExpressionParse, emptyExpression: boolean, cstType: CstType) {
if (!parse.parseResult && parse.errors.length > 0 && parse.errors[0].errorType !== RSErrorType.syntax) {
return;
}
if (cstType === CstType.BASE || cstType === CstType.CONSTANT) {
if (!emptyExpression) {
parse.parseResult = false;
parse.errors.push({
errorType: RSErrorType.globalNonemptyBase,
isCritical: true,
params: [],
position: 0
});
}
}
if (!checkTypeConsistency(cstType, parse.typification, parse.args)) {
parse.parseResult = false;
parse.errors.push({
errorType: RSErrorType.globalUnexpectedType,
isCritical: true,
params: [],
position: 0
});
} }
} }
@ -31,7 +60,7 @@ function useCheckExpression({ schema }: { schema?: IRSForm }) {
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
const [parseData, setParseData] = useState<IExpressionParse | undefined>(undefined); const [parseData, setParseData] = useState<IExpressionParse | undefined>(undefined);
const resetParse = useCallback(() => { setParseData(undefined); }, []); const resetParse = useCallback(() => setParseData(undefined), []);
function checkExpression(expression: string, activeCst?: IConstituenta, onSuccess?: DataCallback<IExpressionParse>) { function checkExpression(expression: string, activeCst?: IConstituenta, onSuccess?: DataCallback<IExpressionParse>) {
setError(undefined); setError(undefined);
@ -39,29 +68,10 @@ function useCheckExpression({ schema }: { schema?: IRSForm }) {
data: { expression: expression }, data: { expression: expression },
showError: true, showError: true,
setLoading, setLoading,
onError: error => { setError(error); }, onError: error => setError(error),
onSuccess: parse => { onSuccess: parse => {
if (activeCst && parse.parseResult) { if (activeCst) {
if (activeCst.cstType == CstType.BASE || activeCst.cstType == CstType.CONSTANT) { adjustResults(parse, expression === getCstExpressionPrefix(activeCst), activeCst.cstType);
if (expression !== getCstExpressionPrefix(activeCst)) {
parse.parseResult = false;
parse.errors.push({
errorType: RSErrorType.globalNonemptyBase,
isCritical: true,
params: [],
position: 0
});
}
}
if (!checkTypeConsistency(activeCst.cstType, parse.typification, parse.args)) {
parse.parseResult = false;
parse.errors.push({
errorType: RSErrorType.globalUnexpectedType,
isCritical: true,
params: [],
position: 0
});
}
} }
setParseData(parse); setParseData(parse);
if (onSuccess) onSuccess(parse); if (onSuccess) onSuccess(parse);

View File

@ -6,14 +6,14 @@ function useDropdown() {
const [isActive, setIsActive] = useState(false); const [isActive, setIsActive] = useState(false);
const ref = useRef(null); const ref = useRef(null);
useClickedOutside({ ref, callback: () => { setIsActive(false); } }) useClickedOutside({ ref, callback: () => setIsActive(false) })
return { return {
ref, ref,
isActive, isActive,
setIsActive, setIsActive,
toggle: () => { setIsActive(!isActive); }, toggle: () => setIsActive(!isActive),
hide: () => { setIsActive(false); } hide: () => setIsActive(false)
}; };
} }

View File

@ -27,7 +27,10 @@ export function useRSFormDetails({ target }: { target?: string }) {
getRSFormDetails(target, { getRSFormDetails(target, {
showError: true, showError: true,
setLoading: setCustomLoading ?? setLoading, setLoading: setCustomLoading ?? setLoading,
onError: error => { setInnerSchema(undefined); setError(error); }, onError: error => {
setInnerSchema(undefined);
setError(error);
},
onSuccess: schema => { onSuccess: schema => {
setSchema(schema); setSchema(schema);
if (callback) callback(); if (callback) callback();

View File

@ -9,7 +9,7 @@ function useResolveText({ schema }: { schema?: IRSForm }) {
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
const [refsData, setRefsData] = useState<IReferenceData | undefined>(undefined); const [refsData, setRefsData] = useState<IReferenceData | undefined>(undefined);
const resetData = useCallback(() => { setRefsData(undefined); }, []); const resetData = useCallback(() => setRefsData(undefined), []);
function resolveText(text: string, onSuccess?: DataCallback<IReferenceData>) { function resolveText(text: string, onSuccess?: DataCallback<IReferenceData>) {
setError(undefined); setError(undefined);
@ -17,7 +17,7 @@ function useResolveText({ schema }: { schema?: IRSForm }) {
data: { text: text }, data: { text: text },
showError: true, showError: true,
setLoading, setLoading,
onError: error => { setError(error); }, onError: error => setError(error),
onSuccess: data => { onSuccess: data => {
setRefsData(data); setRefsData(data);
if (onSuccess) onSuccess(data); if (onSuccess) onSuccess(data);

View File

@ -42,6 +42,10 @@
@apply clr-border rounded @apply clr-border rounded
} }
.text-btn {
@apply text-gray-600 dark:text-zinc-200 dark:disabled:text-zinc-400 disabled:text-gray-400
}
.clr-border { .clr-border {
@apply border-gray-300 dark:border-[#434343] @apply border-gray-300 dark:border-[#434343]
} }
@ -102,7 +106,7 @@
@apply text-white bg-blue-400 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-400 dark:text-black disabled:bg-gray-400 dark:disabled:bg-gray-600 @apply text-white bg-blue-400 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-400 dark:text-black disabled:bg-gray-400 dark:disabled:bg-gray-600
} }
.clr-btn-default { .clr-btn-default {
@apply text-gray-600 dark:text-zinc-200 dark:disabled:text-zinc-400 disabled:text-gray-400 bg-[#f0f2f7] hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-400 @apply bg-[#f0f2f7] hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-400 text-btn
} }
/* Transparent button */ /* Transparent button */

View File

@ -35,15 +35,15 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
> >
<IntlProvider locale='ru' defaultLocale='ru'> <IntlProvider locale='ru' defaultLocale='ru'>
<ThemeState> <ThemeState>
<AuthState>
<UsersState> <UsersState>
<AuthState>
<LibraryState> <LibraryState>
<App /> <App />
</LibraryState> </LibraryState>
</UsersState>
</AuthState> </AuthState>
</UsersState>
</ThemeState> </ThemeState>
</IntlProvider> </IntlProvider>
</ErrorBoundary> </ErrorBoundary>

View File

@ -15,7 +15,7 @@ function HomePage() {
}, TIMEOUT_UI_REFRESH); }, TIMEOUT_UI_REFRESH);
} else if(!user.is_staff) { } else if(!user.is_staff) {
setTimeout(() => { setTimeout(() => {
navigate('/library?filter=personal'); navigate('/library');
}, TIMEOUT_UI_REFRESH); }, TIMEOUT_UI_REFRESH);
} }
}, [navigate, user]) }, [navigate, user])

View File

@ -0,0 +1,89 @@
import { useCallback } from 'react';
import Button from '../../components/Common/Button';
import Checkbox from '../../components/Common/Checkbox';
import Dropdown from '../../components/Common/Dropdown';
import DropdownButton from '../../components/Common/DropdownButton';
import { FilterCogIcon } from '../../components/Icons';
import useDropdown from '../../hooks/useDropdown';
import { LibraryFilterStrategy } from '../../utils/models';
interface PickerStrategyProps {
value: LibraryFilterStrategy
onChange: (value: LibraryFilterStrategy) => void
}
function PickerStrategy({ value, onChange }: PickerStrategyProps) {
const pickerMenu = useDropdown();
const handleChange = useCallback(
(newValue: LibraryFilterStrategy) => {
pickerMenu.hide();
onChange(newValue);
}, [pickerMenu, onChange]);
return (
<div ref={pickerMenu.ref} className='h-full text-right'>
<Button
icon={<FilterCogIcon size={6} />}
dense
tooltip='Фильтры'
colorClass='clr-input clr-hover text-btn'
widthClass='h-full py-1 px-2 border-none'
onClick={pickerMenu.toggle}
/>
{ pickerMenu.isActive &&
<Dropdown>
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.MANUAL)}>
<Checkbox
value={value === LibraryFilterStrategy.MANUAL}
label='Отображать все'
widthClass='w-fit px-2'
/>
</DropdownButton>
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.COMMON)}>
<Checkbox
value={value === LibraryFilterStrategy.COMMON}
label='Общедоступные'
widthClass='w-fit px-2'
tooltip='Отображать только общедоступные схемы'
/>
</DropdownButton>
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.CANONICAL)}>
<Checkbox
value={value === LibraryFilterStrategy.CANONICAL}
label='Библиотечные'
widthClass='w-fit px-2'
tooltip='Отображать только библиотечные схемы'
/>
</DropdownButton>
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.PERSONAL)}>
<Checkbox
value={value === LibraryFilterStrategy.PERSONAL}
label='Личные'
widthClass='w-fit px-2'
tooltip='Отображать только подписки и владеемые схемы'
/>
</DropdownButton>
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.SUBSCRIBE)}>
<Checkbox
value={value === LibraryFilterStrategy.SUBSCRIBE}
label='Подписки'
widthClass='w-fit px-2'
tooltip='Отображать только подписки'
/>
</DropdownButton>
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.OWNED)}>
<Checkbox
value={value === LibraryFilterStrategy.OWNED}
label='Я - Владелец!'
widthClass='w-fit px-2'
tooltip='Отображать только владеемые схемы'
/>
</DropdownButton>
</Dropdown>}
</div>
);
}
export default PickerStrategy;

View File

@ -1,70 +1,96 @@
import { useLayoutEffect, useState } from 'react'; import { useCallback, useLayoutEffect, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { MagnifyingGlassIcon } from '../../components/Icons'; import { MagnifyingGlassIcon } from '../../components/Icons';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { ILibraryFilter } from '../../utils/models'; import { ILibraryFilter, LibraryFilterStrategy } from '../../utils/models';
import PickerStrategy from './PickerStrategy';
function ApplyStrategy(strategy: LibraryFilterStrategy): ILibraryFilter {
switch (strategy) {
case LibraryFilterStrategy.MANUAL: return {};
case LibraryFilterStrategy.COMMON: return { is_common: true };
case LibraryFilterStrategy.CANONICAL: return { is_canonical: true };
case LibraryFilterStrategy.PERSONAL: return { is_personal: true };
case LibraryFilterStrategy.SUBSCRIBE: return { is_subscribed: true };
case LibraryFilterStrategy.OWNED: return { is_owned: true };
}
}
interface SearchPanelProps { interface SearchPanelProps {
filter: ILibraryFilter total: number
filtered: number
setFilter: React.Dispatch<React.SetStateAction<ILibraryFilter>> setFilter: React.Dispatch<React.SetStateAction<ILibraryFilter>>
} }
function SearchPanel({ filter, setFilter }: SearchPanelProps) { function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) {
const navigate = useNavigate();
const search = useLocation().search; const search = useLocation().search;
const { user } = useAuth(); const { user } = useAuth();
const [query, setQuery] = useState('') const [query, setQuery] = useState('');
const [strategy, setStrategy] = useState(LibraryFilterStrategy.MANUAL);
function handleChangeQuery(event: React.ChangeEvent<HTMLInputElement>) {
const newQuery = event.target.value;
setQuery(newQuery);
setFilter(prev => {
return {
query: newQuery,
is_owned: prev.is_owned,
is_common: prev.is_common,
is_canonical: prev.is_canonical,
is_subscribed: prev.is_subscribed,
is_personal: prev.is_personal
};
});
}
useLayoutEffect(() => { useLayoutEffect(() => {
const filterType = new URLSearchParams(search).get('filter'); const searchFilter = new URLSearchParams(search).get('filter') as LibraryFilterStrategy | null;
if (filterType === 'common') { const inputStrategy = searchFilter && Object.values(LibraryFilterStrategy).includes(searchFilter) ? searchFilter : LibraryFilterStrategy.MANUAL;
setQuery(''); setQuery('')
setFilter({ setStrategy(inputStrategy)
is_common: true setFilter(ApplyStrategy(inputStrategy));
});
} else if (filterType === 'personal' && user) {
setQuery('');
setFilter({
ownedBy: user.id!
});
}
}, [user, search, setQuery, setFilter]); }, [user, search, setQuery, setFilter]);
const handleChangeStrategy = useCallback(
(value: LibraryFilterStrategy) => {
if (value === strategy) {
return;
}
navigate(`/library?filter=${value}`)
}, [strategy, navigate]);
return ( return (
<div className='sticky top-0 left-0 right-0 z-10 flex justify-center w-full border-b clr-bg-pop'> <div className='sticky top-0 left-0 right-0 z-10 flex items-center justify-start w-full border-b clr-input'>
<div className='relative w-96'> <div className='px-2 py-1 select-none whitespace-nowrap min-w-[10rem]'>
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'> Фильтр
<MagnifyingGlassIcon /> <span className='ml-2'>
</div> <b>{filtered}</b> из {total}
<input </span>
type='text' </div>
value={query} <div className='flex items-center justify-center w-full pr-[10rem]'>
className='w-full p-2 pl-10 text-sm outline-none clr-bg-pop border-x clr-border' <PickerStrategy
placeholder='Поиск схемы...' value={strategy}
onChange={data => setQuery(data.target.value)} onChange={handleChangeStrategy}
/> />
<div className='relative w-96 min-w-[10rem]'>
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
<MagnifyingGlassIcon />
</div>
<input
type='text'
value={query}
className='w-full p-2 pl-10 text-sm outline-none clr-input'
placeholder='Поиск схемы...'
onChange={handleChangeQuery}
/>
</div>
</div> </div>
</div> </div>
); );
} }
export default SearchPanel; export default SearchPanel;
{/* <div className='sticky top-0 left-0 right-0 z-10 flex items-start justify-between w-full gap-1 px-2 py-1 bg-white border-b rounded clr-bg-pop clr-border'>
<MatchModePicker
value={filterMatch}
onChange={setFilterMatch}
/>
<input type='text'
className='w-full px-2 bg-white outline-none select-none hover:text-clip clr-bg-pop'
placeholder='наберите текст фильтра'
value={filterText}
onChange={event => setFilterText(event.target.value)}
/>
<DependencyModePicker
value={filterSource}
onChange={setFilterSource}
/>
</div> */}

View File

@ -124,7 +124,7 @@ function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {
<p> <p>
<TextURL text='Создать схему' href='/rsform-create'/> <TextURL text='Создать схему' href='/rsform-create'/>
<span> | </span> <span> | </span>
<TextURL text='Все схемы' href='/library?filter=common'/> <TextURL text='Все схемы' href='/library'/>
<span> | </span> <span> | </span>
<span className='cursor-pointer hover:underline text-url' onClick={cleanQuery}> <span className='cursor-pointer hover:underline text-url' onClick={cleanQuery}>
<b>Очистить фильтр</b> <b>Очистить фильтр</b>

View File

@ -10,13 +10,13 @@ import ViewLibrary from './ViewLibrary';
function LibraryPage() { function LibraryPage() {
const library = useLibrary(); const library = useLibrary();
const [ filterParams, setFilterParams ] = useState<ILibraryFilter>({}); const [ filter, setFilter ] = useState<ILibraryFilter>({});
const [ items, setItems ] = useState<ILibraryItem[]>([]); const [ items, setItems ] = useState<ILibraryItem[]>([]);
useLayoutEffect(() => { useLayoutEffect(
const filter = filterParams; () => {
setItems(library.filter(filter)); setItems(library.filter(filter));
}, [library, filterParams]); }, [library, filter, filter.query]);
return ( return (
<div className='w-full'> <div className='w-full'>
@ -25,11 +25,12 @@ function LibraryPage() {
{ !library.loading && library.items && { !library.loading && library.items &&
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<SearchPanel <SearchPanel
filter={filterParams} total={library.items.length ?? 0}
setFilter={setFilterParams} filtered={items.length}
setFilter={setFilter}
/> />
<ViewLibrary <ViewLibrary
cleanQuery={() => setFilterParams({})} cleanQuery={() => setFilter({})}
items={items} items={items}
/> />
</div> </div>

View File

@ -34,28 +34,34 @@ function LoginPage() {
username: username, username: username,
password: password password: password
}; };
login(data, () => { navigate('/library?filter=personal'); }); login(data, () => navigate('/library'));
} }
} }
return ( return (
<div className='w-full py-4'> { user <div className='flex justify-center w-full'>
<div className='py-2'> { user
? <b>{`Вы вошли в систему как ${user.username}`}</b> ? <b>{`Вы вошли в систему как ${user.username}`}</b>
: <Form title='Ввод данных пользователя' onSubmit={handleSubmit} widthClass='w-[21rem]'> :
<Form
title='Ввод данных пользователя'
onSubmit={handleSubmit}
widthClass='w-[24rem]'
>
<TextInput id='username' <TextInput id='username'
label='Имя пользователя' label='Имя пользователя'
required required
type='text' type='text'
value={username} value={username}
autoFocus autoFocus
onChange={event => { setUsername(event.target.value); }} onChange={event => setUsername(event.target.value)}
/> />
<TextInput id='password' <TextInput id='password'
label='Пароль' label='Пароль'
required required
type='password' type='password'
value={password} value={password}
onChange={event => { setPassword(event.target.value); }} onChange={event => setPassword(event.target.value)}
/> />
<div className='flex justify-center w-full gap-2 mt-4'> <div className='flex justify-center w-full gap-2 mt-4'>
@ -72,6 +78,7 @@ function LoginPage() {
{ error && <BackendError error={error} />} { error && <BackendError error={error} />}
</Form> </Form>
}</div> }</div>
</div>
); );
} }

View File

@ -65,21 +65,21 @@ function DlgCloneRSForm({ hideWindow }: DlgCloneRSFormProps) {
<TextInput id='title' label='Полное название' type='text' <TextInput id='title' label='Полное название' type='text'
required required
value={title} value={title}
onChange={event => { setTitle(event.target.value); }} onChange={event => setTitle(event.target.value)}
/> />
<TextInput id='alias' label='Сокращение' type='text' <TextInput id='alias' label='Сокращение' type='text'
required required
value={alias} value={alias}
widthClass='max-w-sm' widthClass='max-w-sm'
onChange={event => { setAlias(event.target.value); }} onChange={event => setAlias(event.target.value)}
/> />
<TextArea id='comment' label='Комментарий' <TextArea id='comment' label='Комментарий'
value={comment} value={comment}
onChange={event => { setComment(event.target.value); }} onChange={event => setComment(event.target.value)}
/> />
<Checkbox id='common' label='Общедоступная схема' <Checkbox id='common' label='Общедоступная схема'
value={common} value={common}
onChange={event => { setCommon(event.target.checked); }} onChange={event => setCommon(event.target.checked)}
/> />
</Modal> </Modal>
); );

View File

@ -34,9 +34,7 @@ function DlgCreateCst({ hideWindow, initial, onCreate }: DlgCreateCstProps) {
} }
} }
const handleSubmit = () => { const handleSubmit = () => onCreate(getData());
onCreate(getData());
};
useEffect(() => { useEffect(() => {
if (initial) { if (initial) {
@ -48,10 +46,7 @@ function DlgCreateCst({ hideWindow, initial, onCreate }: DlgCreateCstProps) {
} }
}, [initial]); }, [initial]);
useEffect(() => { useEffect(() => setValidated(selectedType !== undefined), [selectedType]);
setValidated(selectedType !== undefined);
}, [selectedType]
);
return ( return (
<Modal <Modal
@ -67,7 +62,7 @@ function DlgCreateCst({ hideWindow, initial, onCreate }: DlgCreateCstProps) {
options={CstTypeSelector} options={CstTypeSelector}
placeholder='Выберите тип' placeholder='Выберите тип'
values={selectedType ? [{ value: selectedType, label: getCstTypeLabel(selectedType) }] : []} values={selectedType ? [{ value: selectedType, label: getCstTypeLabel(selectedType) }] : []}
onChange={data => { setSelectedType(data.length > 0 ? data[0].value : CstType.BASE); }} onChange={data => setSelectedType(data.length > 0 ? data[0].value : CstType.BASE)}
/> />
</div> </div>
<TextArea id='term' label='Термин' <TextArea id='term' label='Термин'
@ -89,14 +84,14 @@ function DlgCreateCst({ hideWindow, initial, onCreate }: DlgCreateCstProps) {
rows={2} rows={2}
value={textDefinition} value={textDefinition}
spellCheck spellCheck
onChange={event => { setTextDefinition(event.target.value); }} onChange={event => setTextDefinition(event.target.value)}
/> />
<TextArea id='convention' label='Конвенция / Комментарий' <TextArea id='convention' label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию' placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию'
rows={2} rows={2}
value={convention} value={convention}
spellCheck spellCheck
onChange={event => { setConvention(event.target.value); }} onChange={event => setConvention(event.target.value)}
/> />
</div> </div>
</Modal> </Modal>

View File

@ -31,11 +31,11 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
const handleSubmit = () => onRename(getData()); const handleSubmit = () => onRename(getData());
useLayoutEffect( useLayoutEffect(
() => { () => {
if (schema && initial && cstType !== initial.cst_type) { if (schema && initial && cstType !== initial.cst_type) {
setAlias(createAliasFor(cstType, schema)); setAlias(createAliasFor(cstType, schema));
} }
}, [initial, cstType, schema]); }, [initial, cstType, schema]);
useLayoutEffect( useLayoutEffect(
() => { () => {
@ -66,13 +66,13 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
submitInvalidTooltip={'Введите имя, соответствующее типу и отсутствующее в схеме'} submitInvalidTooltip={'Введите имя, соответствующее типу и отсутствующее в схеме'}
submitText='Переименовать' submitText='Переименовать'
> >
<div className='flex items-center gap-4 px-2 mb-2 h-fit min-w-[25rem]'> <div className='flex items-center gap-4 px-2 my-2 h-fit min-w-[25rem]'>
<ConceptSelect <ConceptSelect
className='mt-2 min-w-[14rem] self-center' className='min-w-[14rem] self-center'
options={CstTypeSelector} options={CstTypeSelector}
placeholder='Выберите тип' placeholder='Выберите тип'
values={cstType ? [{ value: cstType, label: getCstTypeLabel(cstType) }] : []} values={cstType ? [{ value: cstType, label: getCstTypeLabel(cstType) }] : []}
onChange={data => { setCstType(data.length > 0 ? data[0].value : CstType.BASE); }} onChange={data => setCstType(data.length > 0 ? data[0].value : CstType.BASE)}
/> />
<div> <div>
<TextInput id='alias' label='Имя' <TextInput id='alias' label='Имя'

View File

@ -32,7 +32,7 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
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);
} }
} }
@ -53,7 +53,7 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
<Checkbox <Checkbox
label='Загружать название и комментарий' label='Загружать название и комментарий'
value={loadMetadata} value={loadMetadata}
onChange={event => { setLoadMetadata(event.target.checked); }} onChange={event => setLoadMetadata(event.target.checked)}
/> />
</div> </div>
</Modal> </Modal>

View File

@ -45,7 +45,8 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
const isEnabled = useMemo(() => activeCst && isEditable, [activeCst, isEditable]); const isEnabled = useMemo(() => activeCst && isEditable, [activeCst, isEditable]);
useLayoutEffect(() => { useLayoutEffect(
() => {
if (!activeCst) { if (!activeCst) {
setIsModified(false); setIsModified(false);
return; return;
@ -60,7 +61,8 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
activeCst?.definition.text.raw, activeCst?.convention, activeCst?.definition.text.raw, activeCst?.convention,
term, textDefinition, expression, convention]); term, textDefinition, expression, convention]);
useLayoutEffect(() => { useLayoutEffect(
() => {
if (activeCst) { if (activeCst) {
setAlias(activeCst.alias); setAlias(activeCst.alias);
setConvention(activeCst.convention ?? ''); setConvention(activeCst.convention ?? '');
@ -73,8 +75,10 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
} }
}, [activeCst, onOpenEdit, schema]); }, [activeCst, onOpenEdit, schema]);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) { function handleSubmit(event?: React.FormEvent<HTMLFormElement>) {
event.preventDefault(); if (event) {
event.preventDefault();
}
if (!activeID || processing) { if (!activeID || processing) {
return; return;
} }
@ -86,7 +90,7 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
definition_raw: textDefinition, definition_raw: textDefinition,
term_raw: term term_raw: term
}; };
cstUpdate(data, () => { toast.success('Изменения сохранены'); }); cstUpdate(data, () => toast.success('Изменения сохранены'));
} }
function handleDelete() { function handleDelete() {
@ -126,13 +130,14 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
return ( return (
<div className='flex items-stretch w-full gap-2 mb-2 justify-stretch'> <div className='flex items-stretch w-full gap-2 mb-2 justify-stretch'>
<form onSubmit={handleSubmit} className='min-w-[50rem] max-w-min px-4 py-2 border'> <form onSubmit={handleSubmit} className='min-w-[50rem] max-w-min px-4 py-2 border-y border-r clr-border'>
<div className='relative'> <div className='relative'>
<div className='absolute top-0 left-0'> <div className='absolute top-0 left-0'>
<MiniButton <MiniButton
tooltip='Сохранить изменения' tooltip='Сохранить изменения'
disabled={!isModified || !isEnabled} disabled={!isModified || !isEnabled}
icon={<SaveIcon size={6} color={isModified && isEnabled ? 'text-primary' : ''}/>} icon={<SaveIcon size={6} color={isModified && isEnabled ? 'text-primary' : ''}/>}
onClick={() => handleSubmit()}
> >
</MiniButton> </MiniButton>
</div> </div>
@ -208,8 +213,8 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
resolved={activeCst?.definition.text.resolved ?? ''} resolved={activeCst?.definition.text.resolved ?? ''}
disabled={!isEnabled} disabled={!isEnabled}
spellCheck spellCheck
onChange={event => { setTextDefinition(event.target.value); }} onChange={event => setTextDefinition(event.target.value)}
onFocus={() => { setEditMode(EditMode.TEXT); }} onFocus={() => setEditMode(EditMode.TEXT)}
/> />
<TextArea id='convention' label='Конвенция / Комментарий' <TextArea id='convention' label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию' placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию'
@ -217,10 +222,10 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
value={convention} value={convention}
disabled={!isEnabled} disabled={!isEnabled}
spellCheck spellCheck
onChange={event => { setConvention(event.target.value); }} onChange={event => setConvention(event.target.value)}
onFocus={() => { setEditMode(EditMode.TEXT); }} onFocus={() => setEditMode(EditMode.TEXT)}
/> />
<div className='flex justify-center w-full mt-2'> <div className='flex justify-center w-full mt-4 mb-2'>
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
disabled={!isModified || !isEnabled} disabled={!isModified || !isEnabled}

View File

@ -8,9 +8,10 @@ import Divider from '../../components/Common/Divider';
import HelpRSFormItems from '../../components/Help/HelpRSFormItems'; import HelpRSFormItems from '../../components/Help/HelpRSFormItems';
import { ArrowDownIcon, ArrowsRotateIcon, ArrowUpIcon, DumpBinIcon, HelpIcon, SmallPlusIcon } from '../../components/Icons'; import { ArrowDownIcon, ArrowsRotateIcon, ArrowUpIcon, DumpBinIcon, HelpIcon, SmallPlusIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import { prefixes } from '../../utils/constants'; import { prefixes } from '../../utils/constants';
import { CstType, IConstituenta, ICstCreateData, ICstMovetoData } from '../../utils/models' import { CstType, IConstituenta, ICstCreateData, ICstMovetoData } from '../../utils/models'
import { getCstTypePrefix, getCstTypeShortcut, getCstTypificationLabel, mapStatusInfo } from '../../utils/staticUI'; import { getCstStatusColor, getCstTypePrefix, getCstTypeShortcut, getCstTypificationLabel, mapStatusInfo } from '../../utils/staticUI';
interface EditorItemsProps { interface EditorItemsProps {
onOpenEdit: (cstID: number) => void onOpenEdit: (cstID: number) => void
@ -19,6 +20,7 @@ interface EditorItemsProps {
} }
function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps) { function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps) {
const { colors } = useConceptTheme();
const { schema, isEditable, cstMoveTo, resetAliases } = useRSForm(); const { schema, isEditable, cstMoveTo, resetAliases } = useRSForm();
const [selected, setSelected] = useState<number[]>([]); const [selected, setSelected] = useState<number[]>([]);
const nothingSelected = useMemo(() => selected.length === 0, [selected]); const nothingSelected = useMemo(() => selected.length === 0, [selected]);
@ -51,7 +53,9 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
}, -1); }, -1);
const target = Math.max(0, currentIndex - 1) + 1 const target = Math.max(0, currentIndex - 1) + 1
const data = { const data = {
items: selected.map(id => { return { id: id }; }), items: selected.map(id => {
return { id: id };
}),
move_to: target move_to: target
} }
cstMoveTo(data); cstMoveTo(data);
@ -76,7 +80,9 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
}, -1); }, -1);
const target = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1 const target = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1
const data: ICstMovetoData = { const data: ICstMovetoData = {
items: selected.map(id => { return { id: id }; }), items: selected.map(id => {
return { id: id };
}),
move_to: target move_to: target
} }
cstMoveTo(data); cstMoveTo(data);
@ -87,7 +93,6 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
resetAliases(() => toast.success('Переиндексация конституент успешна')); resetAliases(() => toast.success('Переиндексация конституент успешна'));
} }
// Add new constituenta
function handleCreateCst(type?: CstType) { function handleCreateCst(type?: CstType) {
if (!schema) { if (!schema) {
return; return;
@ -180,7 +185,8 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
return (<> return (<>
<div <div
id={`${prefixes.cst_list}${cst.alias}`} id={`${prefixes.cst_list}${cst.alias}`}
className={`w-full rounded-md text-center ${info.color}`} className='w-full text-center rounded-md'
style={{backgroundColor: getCstStatusColor(cst.status, colors)}}
> >
{cst.alias} {cst.alias}
</div> </div>
@ -248,7 +254,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
reorder: true, reorder: true,
hide: 1800 hide: 1800
} }
], []); ], [colors]);
return ( return (
<div className='w-full'> <div className='w-full'>

View File

@ -27,7 +27,7 @@ function EditorRSForm({ onDestroy, onClaim, onShare, onDownload }: EditorRSFormP
const { getUserLabel } = useUsers(); const { getUserLabel } = useUsers();
const { const {
schema, update, isForceAdmin, schema, update, isForceAdmin,
isEditable, isOwned, isClaimable, processing isEditable, isClaimable, processing
} = useRSForm(); } = useRSForm();
const { user } = useAuth(); const { user } = useAuth();
@ -79,7 +79,7 @@ function EditorRSForm({ onDestroy, onClaim, onShare, onDownload }: EditorRSFormP
}; };
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-[35.3rem] px-4 py-2 border-y border-r clr-border min-w-fit'>
<div className='relative w-full'> <div className='relative w-full'>
<div className='absolute top-0 right-0 flex'> <div className='absolute top-0 right-0 flex'>
<MiniButton <MiniButton
@ -93,8 +93,8 @@ function EditorRSForm({ onDestroy, onClaim, onShare, onDownload }: EditorRSFormP
onClick={onDownload} onClick={onDownload}
/> />
<MiniButton <MiniButton
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' } tooltip={isClaimable ? 'Стать владельцем' : 'Невозможно стать владельцем' }
icon={<CrownIcon size={5} color={isOwned ? '' : 'text-green'}/>} icon={<CrownIcon size={5} color={!isClaimable ? '' : 'text-green'}/>}
disabled={!isClaimable || !user} disabled={!isClaimable || !user}
onClick={onClaim} onClick={onClaim}
/> />

View File

@ -1,6 +1,6 @@
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, import { GraphCanvas, GraphCanvasRef, GraphEdge,
GraphNode, LayoutTypes, lightTheme, Sphere, useSelection GraphNode, LayoutTypes, Sphere, useSelection
} from 'reagraph'; } from 'reagraph';
import Button from '../../components/Common/Button'; import Button from '../../components/Common/Button';
@ -15,6 +15,7 @@ import { ArrowsRotateIcon, DumpBinIcon, FilterCogIcon, HelpIcon, SmallPlusIcon }
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import useLocalStorage from '../../hooks/useLocalStorage'; import useLocalStorage from '../../hooks/useLocalStorage';
import { graphDarkT, graphLightT, IColorTheme } from '../../utils/color';
import { prefixes, resources } from '../../utils/constants'; import { prefixes, resources } from '../../utils/constants';
import { Graph } from '../../utils/Graph'; import { Graph } from '../../utils/Graph';
import { CstType, IConstituenta, ICstCreateData } from '../../utils/models'; import { CstType, IConstituenta, ICstCreateData } from '../../utils/models';
@ -28,14 +29,14 @@ import ConstituentaTooltip from './elements/ConstituentaTooltip';
export type ColoringScheme = 'none' | 'status' | 'type'; export type ColoringScheme = 'none' | 'status' | 'type';
const TREE_SIZE_MILESTONE = 50; const TREE_SIZE_MILESTONE = 50;
function getCstNodeColor(cst: IConstituenta, coloringScheme: ColoringScheme, darkMode: boolean): string { function getCstNodeColor(cst: IConstituenta, coloringScheme: ColoringScheme, colors: IColorTheme): string {
if (coloringScheme === 'type') { if (coloringScheme === 'type') {
return getCstClassColor(cst.cstClass, darkMode); return getCstClassColor(cst.cstClass, colors);
} }
if (coloringScheme === 'status') { if (coloringScheme === 'status') {
return getCstStatusColor(cst.status, darkMode); return getCstStatusColor(cst.status, colors);
} }
return (darkMode ? '#7a8c9e' :'#7ca0ab'); return '';
} }
export interface GraphEditorParams { export interface GraphEditorParams {
@ -62,7 +63,7 @@ interface EditorTermGraphProps {
function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGraphProps) { function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGraphProps) {
const { schema, isEditable } = useRSForm(); const { schema, isEditable } = useRSForm();
const { darkMode, noNavigation } = useConceptTheme(); const { darkMode, colors, noNavigation } = useConceptTheme();
const [ layout, setLayout ] = useLocalStorage<LayoutTypes>('graph_layout', 'treeTd2d'); const [ layout, setLayout ] = useLocalStorage<LayoutTypes>('graph_layout', 'treeTd2d');
const [ coloringScheme, setColoringScheme ] = useLocalStorage<ColoringScheme>('graph_coloring', 'none'); const [ coloringScheme, setColoringScheme ] = useLocalStorage<ColoringScheme>('graph_coloring', 'none');
@ -171,13 +172,13 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
if (cst) { if (cst) {
result.push({ result.push({
id: String(node.id), id: String(node.id),
fill: getCstNodeColor(cst, coloringScheme, darkMode), fill: getCstNodeColor(cst, coloringScheme, colors),
label: cst.term.resolved && !noTerms ? `${cst.alias}: ${cst.term.resolved}` : cst.alias label: cst.term.resolved && !noTerms ? `${cst.alias}: ${cst.term.resolved}` : cst.alias
}); });
} }
}); });
return result; return result;
}, [schema, coloringScheme, filtered.nodes, darkMode, noTerms]); }, [schema, coloringScheme, filtered.nodes, noTerms, colors]);
const edges: GraphEdge[] = useMemo( const edges: GraphEdge[] = useMemo(
() => { () => {
@ -337,8 +338,8 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
const canvasHeight = useMemo( const canvasHeight = useMemo(
() => { () => {
return !noNavigation ? return !noNavigation ?
'calc(100vh - 9.8rem)' 'calc(100vh - 10.1rem)'
: 'calc(100vh - 1.8rem)'; : 'calc(100vh - 2.1rem)';
}, [noNavigation]); }, [noNavigation]);
const dismissedStyle = useCallback( const dismissedStyle = useCallback(
@ -354,7 +355,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
initial={getOptions()} initial={getOptions()}
onConfirm={handleChangeOptions} onConfirm={handleChangeOptions}
/>} />}
<div className='flex flex-col border-t border-r max-w-[12.5rem] pr-2 pb-2 text-sm select-none' style={{height: canvasHeight}}> <div className='flex flex-col border-r border-b min-w-[13.5rem] max-w-min px-2 pb-2 text-sm select-none clr-border' style={{height: canvasHeight}}>
{hoverCst && {hoverCst &&
<div className='relative'> <div className='relative'>
<InfoConstituenta <InfoConstituenta
@ -394,12 +395,12 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
onClick={() => setShowOptions(true)} onClick={() => setShowOptions(true)}
/> />
<ConceptSelect <ConceptSelect
className='min-w-[9.3rem]' className='min-w-[9.8rem]'
options={GraphColoringSelector} options={GraphColoringSelector}
searchable={false} searchable={false}
placeholder='Выберите цвет' placeholder='Выберите цвет'
values={coloringScheme ? [{ value: coloringScheme, label: mapColoringLabels.get(coloringScheme) }] : []} values={coloringScheme ? [{ value: coloringScheme, label: mapColoringLabels.get(coloringScheme) }] : []}
onChange={data => { setColoringScheme(data.length > 0 ? data[0].value : GraphColoringSelector[0].value); }} onChange={data => setColoringScheme(data.length > 0 ? data[0].value : GraphColoringSelector[0].value)}
/> />
</div> </div>
@ -409,7 +410,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
searchable={false} searchable={false}
placeholder='Способ расположения' placeholder='Способ расположения'
values={layout ? [{ value: layout, label: mapLayoutLabels.get(layout) }] : []} values={layout ? [{ value: layout, label: mapLayoutLabels.get(layout) }] : []}
onChange={data => { setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value); }} onChange={data => setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value)}
/> />
<Checkbox <Checkbox
label='Скрыть текст' label='Скрыть текст'
@ -441,7 +442,10 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
key={`${cst.alias}`} key={`${cst.alias}`}
id={`${prefixes.cst_list}${cst.alias}`} id={`${prefixes.cst_list}${cst.alias}`}
className='w-fit min-w-[3rem] rounded-md text-center cursor-pointer' className='w-fit min-w-[3rem] rounded-md text-center cursor-pointer'
style={ { backgroundColor: getCstNodeColor(cst, adjustedColoring, darkMode), ...dismissedStyle(cstID) }} style={{
backgroundColor: getCstNodeColor(cst, adjustedColoring, colors),
...dismissedStyle(cstID)
}}
onClick={() => toggleDismissed(cstID)} onClick={() => toggleDismissed(cstID)}
onDoubleClick={() => onOpenEdit(cstID)} onDoubleClick={() => onOpenEdit(cstID)}
> >
@ -458,7 +462,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
</div> </div>
<div className='w-full h-full overflow-auto'> <div className='w-full h-full overflow-auto'>
<div <div
className='relative border-r border-y' className='relative'
style={{width: canvasWidth, height: canvasHeight, borderBottomWidth: noNavigation ? '1px': ''}} style={{width: canvasWidth, height: canvasHeight, borderBottomWidth: noNavigation ? '1px': ''}}
> >
<div className='relative top-0 right-0 z-10 flex mt-1 ml-2 flex-start'> <div className='relative top-0 right-0 z-10 flex mt-1 ml-2 flex-start'>
@ -492,7 +496,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
: undefined : undefined
} }
labelFontUrl={resources.graph_font} labelFontUrl={resources.graph_font}
theme={darkMode ? darkTheme : lightTheme} theme={darkMode ? graphDarkT : graphLightT}
renderNode={({ node, ...rest }) => ( renderNode={({ node, ...rest }) => (
<Sphere {...rest} node={node} /> <Sphere {...rest} node={node} />
)} )}

View File

@ -9,8 +9,9 @@ import ConceptTab from '../../components/Common/ConceptTab';
import { Loader } from '../../components/Common/Loader'; import { Loader } from '../../components/Common/Loader';
import { useLibrary } from '../../context/LibraryContext'; import { useLibrary } from '../../context/LibraryContext';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import { prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants'; import { prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants';
import { ICstCreateData, ICstRenameData, SyntaxTree } from '../../utils/models'; import { ICstCreateData, ICstRenameData, LibraryFilterStrategy, SyntaxTree } from '../../utils/models';
import { createAliasFor } from '../../utils/staticUI'; import { createAliasFor } from '../../utils/staticUI';
import DlgCloneRSForm from './DlgCloneRSForm'; import DlgCloneRSForm from './DlgCloneRSForm';
import DlgCreateCst from './DlgCreateCst'; import DlgCreateCst from './DlgCreateCst';
@ -25,7 +26,7 @@ import EditorTermGraph from './EditorTermGraph';
import RSFormStats from './elements/RSFormStats'; import RSFormStats from './elements/RSFormStats';
import RSTabsMenu from './RSTabsMenu'; import RSTabsMenu from './RSTabsMenu';
export enum RSTabsList { export enum RSTabID {
CARD = 0, CARD = 0,
CST_LIST = 1, CST_LIST = 1,
CST_EDIT = 2, CST_EDIT = 2,
@ -40,8 +41,9 @@ function RSTabs() {
cstCreate, cstDelete, cstRename, subscribe, unsubscribe cstCreate, cstDelete, cstRename, subscribe, unsubscribe
} = useRSForm(); } = useRSForm();
const { destroySchema } = useLibrary(); const { destroySchema } = useLibrary();
const { setNoFooter } = useConceptTheme();
const [activeTab, setActiveTab] = useState(RSTabsList.CARD); const [activeTab, setActiveTab] = useState<RSTabID>(RSTabID.CARD);
const [activeID, setActiveID] = useState<number | undefined>(undefined); const [activeID, setActiveID] = useState<number | undefined>(undefined);
const [showUpload, setShowUpload] = useState(false); const [showUpload, setShowUpload] = useState(false);
@ -72,21 +74,23 @@ function RSTabs() {
}, [schema]); }, [schema]);
useLayoutEffect(() => { useLayoutEffect(() => {
const activeTab = Number(new URLSearchParams(search).get('tab')) ?? RSTabsList.CARD; const activeTab = (Number(new URLSearchParams(search).get('tab')) ?? RSTabID.CARD) as RSTabID;
const cstQuery = new URLSearchParams(search).get('active'); const cstQuery = new URLSearchParams(search).get('active');
setActiveTab(activeTab); setActiveTab(activeTab);
setNoFooter(activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST);
setActiveID(Number(cstQuery) ?? ((schema && schema?.items.length > 0) ? schema.items[0].id : undefined)); setActiveID(Number(cstQuery) ?? ((schema && schema?.items.length > 0) ? schema.items[0].id : undefined));
}, [search, setActiveTab, setActiveID, schema]); return () => setNoFooter(false);
}, [search, setActiveTab, setActiveID, schema, setNoFooter]);
function onSelectTab(index: number) { function onSelectTab(index: number) {
navigateTo(index, activeID); navigateTo(index, activeID);
} }
const navigateTo = useCallback( const navigateTo = useCallback(
(tab: RSTabsList, activeID?: number) => { (tab: RSTabID, activeID?: number) => {
if (activeID) { if (activeID) {
navigate(`/rsforms/${schema!.id}?tab=${tab}&active=${activeID}`, { navigate(`/rsforms/${schema!.id}?tab=${tab}&active=${activeID}`, {
replace: tab === activeTab && tab !== RSTabsList.CST_EDIT replace: tab === activeTab && tab !== RSTabID.CST_EDIT
}); });
} else { } else {
navigate(`/rsforms/${schema!.id}?tab=${tab}`); navigate(`/rsforms/${schema!.id}?tab=${tab}`);
@ -102,7 +106,7 @@ function RSTabs() {
cstCreate(data, newCst => { cstCreate(data, newCst => {
toast.success(`Конституента добавлена: ${newCst.alias}`); toast.success(`Конституента добавлена: ${newCst.alias}`);
navigateTo(activeTab, newCst.id); navigateTo(activeTab, newCst.id);
if (activeTab === RSTabsList.CST_EDIT || activeTab === RSTabsList.CST_LIST) { if (activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST) {
setTimeout(() => { setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`); const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
if (element) { if (element) {
@ -144,14 +148,16 @@ function RSTabs() {
return; return;
} }
const data = { const data = {
items: deleted.map(id => { return { id: id }; }) items: deleted.map(id => {
return { id: id };
})
}; };
let activeIndex = schema.items.findIndex(cst => cst.id === activeID); let activeIndex = schema.items.findIndex(cst => cst.id === activeID);
cstDelete(data, () => { cstDelete(data, () => {
const deletedNames = deleted.map(id => schema.items.find(cst => cst.id === id)?.alias).join(', '); const deletedNames = deleted.map(id => schema.items.find(cst => cst.id === id)?.alias).join(', ');
toast.success(`Конституенты удалены: ${deletedNames}`); toast.success(`Конституенты удалены: ${deletedNames}`);
if (deleted.length === schema.items.length) { if (deleted.length === schema.items.length) {
navigateTo(RSTabsList.CST_LIST); navigateTo(RSTabID.CST_LIST);
} }
if (activeIndex) { if (activeIndex) {
while (activeIndex < schema.items.length && deleted.find(id => id === schema.items[activeIndex].id)) { while (activeIndex < schema.items.length && deleted.find(id => id === schema.items[activeIndex].id)) {
@ -182,7 +188,7 @@ function RSTabs() {
const onOpenCst = useCallback( const onOpenCst = useCallback(
(cstID: number) => { (cstID: number) => {
navigateTo(RSTabsList.CST_EDIT, cstID) navigateTo(RSTabID.CST_EDIT, cstID)
}, [navigateTo]); }, [navigateTo]);
const onDestroySchema = useCallback( const onDestroySchema = useCallback(
@ -192,7 +198,7 @@ function RSTabs() {
} }
destroySchema(schema.id, () => { destroySchema(schema.id, () => {
toast.success('Схема удалена'); toast.success('Схема удалена');
navigate('/library?filter=personal'); navigate(`/library?filter=${LibraryFilterStrategy.PERSONAL}`);
}); });
}, [schema, destroySchema, navigate]); }, [schema, destroySchema, navigate]);
@ -283,7 +289,7 @@ function RSTabs() {
defaultFocus={true} defaultFocus={true}
selectedTabClassName='font-bold' selectedTabClassName='font-bold'
> >
<TabList className='flex items-start pl-2 select-none w-fit clr-bg-pop'> <TabList className='flex items-start pl-2 border-b border-r-2 select-none w-fit clr-bg-pop clr-border'>
<RSTabsMenu <RSTabsMenu
onDownload={onDownloadSchema} onDownload={onDownloadSchema}
onDestroy={onDestroySchema} onDestroy={onDestroySchema}
@ -302,7 +308,7 @@ function RSTabs() {
<ConceptTab className='min-w-[6.5rem]'>Граф термов</ConceptTab> <ConceptTab className='min-w-[6.5rem]'>Граф термов</ConceptTab>
</TabList> </TabList>
<TabPanel className='flex items-start w-full gap-2 px-2'> <TabPanel className='flex w-full gap-4'>
<EditorRSForm <EditorRSForm
onDownload={onDownloadSchema} onDownload={onDownloadSchema}
onDestroy={onDestroySchema} onDestroy={onDestroySchema}
@ -320,7 +326,7 @@ function RSTabs() {
/> />
</TabPanel> </TabPanel>
<TabPanel className='pl-2'> <TabPanel>
<EditorConstituenta <EditorConstituenta
activeID={activeID} activeID={activeID}
onOpenEdit={onOpenCst} onOpenEdit={onOpenCst}
@ -331,7 +337,7 @@ function RSTabs() {
/> />
</TabPanel> </TabPanel>
<TabPanel className='pl-2'> <TabPanel>
<EditorTermGraph <EditorTermGraph
onOpenEdit={onOpenCst} onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst} onCreateCst={promptCreateCst}

View File

@ -52,14 +52,6 @@ function DependencyModePicker({ value, onChange }: DependencyModePickerProps) {
</Dropdown> </Dropdown>
} }
</div> </div>
// case DependencyMode.OUTPUTS: return 'потребители';
// case DependencyMode.INPUTS: return 'поставщики';
// case DependencyMode.EXPAND_INPUTS: return 'влияющие';
// case DependencyMode.EXPAND_OUTPUTS: return 'зависимые';
// }
// }
); );
} }

View File

@ -8,7 +8,7 @@ interface RSFormStatsProps {
function RSFormStats({ stats }: RSFormStatsProps) { function RSFormStats({ stats }: RSFormStatsProps) {
return ( return (
<div className='px-4 py-2 border'> <div className='flex flex-col gap-1 px-4 py-2 border min-w-[16rem]'>
<LabeledText id='count_all' <LabeledText id='count_all'
label='Всего конституент ' label='Всего конституент '
text={stats.count_all} text={stats.count_all}
@ -27,12 +27,20 @@ function RSFormStats({ stats }: RSFormStatsProps) {
label='Невычислимы ' label='Невычислимы '
text={stats.count_incalc} text={stats.count_incalc}
/>} />}
<Divider margins='my-1' /> <Divider margins='my-2' />
<LabeledText id='count_termin' <LabeledText id='count_termin'
label='Термины ' label='Термины '
text={stats.count_termin} text={stats.count_termin}
/> />
<Divider margins='my-1' /> <LabeledText id='count_definition'
label='Определения '
text={stats.count_definition}
/>
<LabeledText id='count_convention'
label='Конвенции '
text={stats.count_convention}
/>
<Divider margins='my-2' />
{ stats.count_base > 0 && { stats.count_base > 0 &&
<LabeledText id='count_base' <LabeledText id='count_base'
label='Базисные множества ' label='Базисные множества '

View File

@ -12,7 +12,7 @@ function RSLocalButton({ text, tooltip, disabled, onInsert }: RSLocalButtonProps
<button <button
type='button' type='button'
disabled={disabled} disabled={disabled}
onClick={() => { onInsert(TokenID.ID_LOCAL, text); }} onClick={() => onInsert(TokenID.ID_LOCAL, text)}
title={tooltip} title={tooltip}
tabIndex={-1} tabIndex={-1}
className='w-[1.5rem] h-7 cursor-pointer border rounded-none clr-btn-clear' className='w-[1.5rem] h-7 cursor-pointer border rounded-none clr-btn-clear'

View File

@ -14,7 +14,7 @@ function RSTokenButton({ id, disabled, onInsert }: RSTokenButtonProps) {
<button <button
type='button' type='button'
disabled={disabled} disabled={disabled}
onClick={() => { onInsert(id); }} onClick={() => onInsert(id)}
title={data.tooltip} title={data.tooltip}
tabIndex={-1} tabIndex={-1}
className={`px-1 cursor-pointer border rounded-none h-7 ${width} clr-btn-clear`} className={`px-1 cursor-pointer border rounded-none h-7 ${width} clr-btn-clear`}

View File

@ -1,7 +1,8 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useConceptTheme } from '../../../context/ThemeContext';
import { ExpressionStatus, type IConstituenta, IExpressionParse,inferStatus, ParsingStatus } from '../../../utils/models'; import { ExpressionStatus, type IConstituenta, IExpressionParse,inferStatus, ParsingStatus } from '../../../utils/models';
import { mapStatusInfo } from '../../../utils/staticUI'; import { getCstStatusColor, mapStatusInfo } from '../../../utils/staticUI';
interface StatusBarProps { interface StatusBarProps {
isModified?: boolean isModified?: boolean
@ -10,6 +11,7 @@ interface StatusBarProps {
} }
function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) { function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) {
const { colors } = useConceptTheme();
const status = useMemo(() => { const status = useMemo(() => {
if (isModified) { if (isModified) {
return ExpressionStatus.UNKNOWN; return ExpressionStatus.UNKNOWN;
@ -24,7 +26,9 @@ function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) {
const data = mapStatusInfo.get(status)!; const data = mapStatusInfo.get(status)!;
return ( return (
<div title={data.tooltip} <div title={data.tooltip}
className={`text-sm h-[1.6rem] w-[10rem] font-semibold inline-flex border items-center select-none justify-center align-middle ${data.color}`}> className='text-sm h-[1.6rem] w-[10rem] font-semibold inline-flex border items-center select-none justify-center align-middle'
style={{backgroundColor: getCstStatusColor(status, colors)}}
>
Статус: [ {data.text} ] Статус: [ {data.text} ]
</div> </div>
) )

View File

@ -6,13 +6,13 @@ import { useConceptTheme } from '../../../context/ThemeContext';
import useLocalStorage from '../../../hooks/useLocalStorage'; import useLocalStorage from '../../../hooks/useLocalStorage';
import { prefixes } from '../../../utils/constants'; import { prefixes } from '../../../utils/constants';
import { applyGraphFilter, CstMatchMode, CstType, DependencyMode, extractGlobals, IConstituenta, matchConstituenta } from '../../../utils/models'; import { applyGraphFilter, CstMatchMode, CstType, DependencyMode, extractGlobals, IConstituenta, matchConstituenta } from '../../../utils/models';
import { getCstDescription, getMockConstituenta, mapStatusInfo } from '../../../utils/staticUI'; import { getCstDescription, getCstStatusColor, getMockConstituenta } from '../../../utils/staticUI';
import ConstituentaTooltip from './ConstituentaTooltip'; import ConstituentaTooltip from './ConstituentaTooltip';
import DependencyModePicker from './DependencyModePicker'; import DependencyModePicker from './DependencyModePicker';
import MatchModePicker from './MatchModePicker'; import MatchModePicker from './MatchModePicker';
// Height that should be left to accomodate navigation panel + bottom margin // Height that should be left to accomodate navigation panel + bottom margin
const LOCAL_NAVIGATION_H = '2.6rem'; const LOCAL_NAVIGATION_H = '2.1rem';
interface ViewSideConstituentsProps { interface ViewSideConstituentsProps {
expression: string expression: string
@ -26,7 +26,7 @@ function isMockCst(cst: IConstituenta) {
} }
function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }: ViewSideConstituentsProps) { function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }: ViewSideConstituentsProps) {
const { darkMode, noNavigation } = useConceptTheme(); const { noNavigation, colors } = useConceptTheme();
const { schema } = useRSForm(); const { schema } = useRSForm();
const [filterMatch, setFilterMatch] = useLocalStorage('side-filter-match', CstMatchMode.ALL); const [filterMatch, setFilterMatch] = useLocalStorage('side-filter-match', CstMatchMode.ALL);
@ -81,10 +81,10 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
{ {
when: (cst: IConstituenta) => cst.id === activeID, when: (cst: IConstituenta) => cst.id === activeID,
style: { style: {
backgroundColor: darkMode ? '#0068b3' : '#def1ff', backgroundColor: colors.selection,
}, },
} }
], [activeID, darkMode]); ], [activeID, colors]);
const columns = useMemo( const columns = useMemo(
() => [ () => [
@ -97,11 +97,11 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
name: 'ID', name: 'ID',
id: 'alias', id: 'alias',
cell: (cst: IConstituenta) => { cell: (cst: IConstituenta) => {
const info = mapStatusInfo.get(cst.status)!;
return (<> return (<>
<div <div
id={`${prefixes.cst_list}${cst.alias}`} id={`${prefixes.cst_list}${cst.alias}`}
className={`w-full rounded-md text-center ${info.color}`} className='w-full text-center rounded-md'
style={{backgroundColor: getCstStatusColor(cst.status, colors)}}
> >
{cst.alias} {cst.alias}
</div> </div>
@ -113,7 +113,7 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
conditionalCellStyles: [ conditionalCellStyles: [
{ {
when: (cst: IConstituenta) => isMockCst(cst), when: (cst: IConstituenta) => isMockCst(cst),
classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]'] style: {backgroundColor: colors.selectionError}
} }
] ]
}, },
@ -126,7 +126,7 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
conditionalCellStyles: [ conditionalCellStyles: [
{ {
when: (cst: IConstituenta) => isMockCst(cst), when: (cst: IConstituenta) => isMockCst(cst),
classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]'] style: {backgroundColor: colors.selectionError}
} }
] ]
}, },
@ -141,11 +141,11 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
conditionalCellStyles: [ conditionalCellStyles: [
{ {
when: (cst: IConstituenta) => isMockCst(cst), when: (cst: IConstituenta) => isMockCst(cst),
classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]'] style: {backgroundColor: colors.selectionError}
} }
] ]
} }
], []); ], [colors]);
const maxHeight = useMemo( const maxHeight = useMemo(
() => { () => {
@ -156,13 +156,13 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
}, [noNavigation, baseHeight]); }, [noNavigation, baseHeight]);
return (<> return (<>
<div className='sticky top-0 left-0 right-0 z-10 flex items-start justify-between w-full gap-1 px-2 py-1 bg-white border-b rounded clr-bg-pop clr-border'> <div className='sticky top-0 left-0 right-0 z-10 flex items-start justify-between w-full gap-1 px-2 py-1 border-b rounded clr-input clr-border'>
<MatchModePicker <MatchModePicker
value={filterMatch} value={filterMatch}
onChange={setFilterMatch} onChange={setFilterMatch}
/> />
<input type='text' <input type='text'
className='w-full px-2 bg-white outline-none select-none hover:text-clip clr-bg-pop' className='w-full px-2 outline-none select-none hover:text-clip clr-input'
placeholder='наберите текст фильтра' placeholder='наберите текст фильтра'
value={filterText} value={filterText}
onChange={event => setFilterText(event.target.value)} onChange={event => setFilterText(event.target.value)}

View File

@ -47,26 +47,30 @@ function RegisterPage() {
{ user && { user &&
<b>{`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`}</b>} <b>{`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`}</b>}
{ !user && { !user &&
<Form title='Регистрация пользователя' onSubmit={handleSubmit}> <Form
title='Регистрация пользователя'
onSubmit={handleSubmit}
widthClass='w-[24rem]'
>
<TextInput id='username' label='Имя пользователя' type='text' <TextInput id='username' label='Имя пользователя' type='text'
required required
value={username} value={username}
onChange={event => { setUsername(event.target.value); }} onChange={event => setUsername(event.target.value)}
/> />
<TextInput id='password' label='Пароль' type='password' <TextInput id='password' label='Пароль' type='password'
required required
value={password} value={password}
onChange={event => { setPassword(event.target.value); }} onChange={event => setPassword(event.target.value)}
/> />
<TextInput id='password2' label='Повторите пароль' type='password' <TextInput id='password2' label='Повторите пароль' type='password'
required required
value={password2} value={password2}
onChange={event => { setPassword2(event.target.value); }} onChange={event => setPassword2(event.target.value)}
/> />
<div className='text-sm'> <div className='text-sm'>
<p>- используйте уникальный пароль</p> <p>- используйте уникальный пароль</p>
<p>- портал функционирует в тестовом режиме</p> <p>- портал функционирует в тестовом режиме</p>
<p>- безопасность информации пользователей не гарантируется</p> <p className='font-semibold text-red'>- безопасность информации не гарантируется</p>
{/* <p>- минимум 8 символов</p> {/* <p>- минимум 8 символов</p>
<p>- большие, маленькие буквы, цифры</p> <p>- большие, маленькие буквы, цифры</p>
<p>- минимум 1 спец. символ</p> */} <p>- минимум 1 спец. символ</p> */}
@ -74,18 +78,18 @@ function RegisterPage() {
<TextInput id='email' label='email' type='text' <TextInput id='email' label='email' type='text'
required required
value={email} value={email}
onChange={event => { setEmail(event.target.value); }} onChange={event => setEmail(event.target.value)}
/> />
<TextInput id='first_name' label='Имя' type='text' <TextInput id='first_name' label='Имя' type='text'
value={firstName} value={firstName}
onChange={event => { setFirstName(event.target.value); }} onChange={event => setFirstName(event.target.value)}
/> />
<TextInput id='last_name' label='Фамилия' type='text' <TextInput id='last_name' label='Фамилия' type='text'
value={lastName} value={lastName}
onChange={event => { setLastName(event.target.value); }} onChange={event => setLastName(event.target.value)}
/> />
<div className='flex items-center justify-between my-4'> <div className='flex items-center justify-center w-full my-4'>
<SubmitButton text='Регистрировать' loading={loading}/> <SubmitButton text='Регистрировать' loading={loading}/>
</div> </div>
{ error && <BackendError error={error} />} { error && <BackendError error={error} />}

View File

@ -14,6 +14,20 @@ function EditorProfile() {
const [first_name, setFirstName] = useState(''); const [first_name, setFirstName] = useState('');
const [last_name, setLastName] = useState(''); const [last_name, setLastName] = useState('');
const [isModified, setIsModified] = useState(true);
useLayoutEffect(() => {
if (!user) {
setIsModified(false);
return;
}
setIsModified(
user.email !== email ||
user.first_name !== first_name ||
user.last_name !== last_name
);
}, [user, user?.email, user?.first_name, user?.last_name, email, first_name, last_name]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (user) { if (user) {
setUsername(user.username); setUsername(user.username);
@ -57,6 +71,7 @@ function EditorProfile() {
<SubmitButton <SubmitButton
text='Сохранить данные' text='Сохранить данные'
loading={processing} loading={processing}
disabled={!isModified}
/> />
</div> </div>
</form> </form>

View File

@ -16,7 +16,7 @@ function UserTabs() {
const { user: auth } = useAuth(); const { user: auth } = useAuth();
const { items } = useLibrary(); const { items } = useLibrary();
const [showSubs, setShowSubs] = useState(true); const [showSubs, setShowSubs] = useState(false);
const subscriptions = useMemo( const subscriptions = useMemo(
() => { () => {

View File

@ -0,0 +1,219 @@
export interface IColorTheme {
red: string
green: string
blue: string
teal: string
orange: string
text: string
input: string
inputDisabled: string
selection: string
selectionError: string
// bg100: string
// bg70: string
// bg50: string
// fg100: string
// fg70: string
// fg50: string
// primary: string
// secondary: string
}
// =========== GENERAL THEMES =========
export const lightT: IColorTheme = {
red: '#ffc9c9',
green: '#aaff80',
blue: '#b3bdff',
teal: '#a5e9fa',
orange: '#ffbb80',
text: '#000000',
input: '#ffffff',
inputDisabled: '#f0f2f7',
selection: '#def1ff',
selectionError: '#ffc9c9'
};
export const darkT: IColorTheme = {
red: '#bf0d00',
green: '#2b8000',
blue: '#394bbf',
teal: '#0099bf',
orange: '#964600',
text: '#e4e4e7',
input: '#070b12',
inputDisabled: '#374151', // bg-gray-700
selection: '#8c6000',
selectionError: '#592b2b'
};
// ========= DATA TABLE THEMES ========
export const dataTableLightT = {
divider: {
default: '#d1d5db'
},
striped: {
default: '#f0f2f7'
},
}
export const dataTableDarkT = {
text: {
primary: 'rgba(228, 228, 231, 1)',
secondary: 'rgba(228, 228, 231, 0.87)',
disabled: 'rgba(228, 228, 231, 0.54)'
},
background: {
default: '#111827'
},
highlightOnHover: {
default: '#4d6080',
text: 'rgba(228, 228, 231, 1)'
},
divider: {
default: '#6b6b6b'
},
striped: {
default: '#374151',
text: 'rgba(228, 228, 231, 1)'
},
selected: {
default: '#4d6080',
text: 'rgba(228, 228, 231, 1)'
}
};
// ============ GRAPH THEMES ==========
export const graphLightT = {
canvas: {
background: '#f9fafb',
},
node: {
fill: '#7ca0ab',
activeFill: '#1DE9AC',
opacity: 1,
selectedOpacity: 1,
inactiveOpacity: 0.2,
label: {
color: '#2A6475',
stroke: '#fff',
activeColor: '#1DE9AC'
}
},
lasso: {
border: '1px solid #55aaff',
background: 'rgba(75, 160, 255, 0.1)'
},
ring: {
fill: '#D8E6EA',
activeFill: '#1DE9AC'
},
edge: {
fill: '#D8E6EA',
activeFill: '#1DE9AC',
opacity: 1,
selectedOpacity: 1,
inactiveOpacity: 0.1,
label: {
stroke: '#fff',
color: '#2A6475',
activeColor: '#1DE9AC'
}
},
arrow: {
fill: '#D8E6EA',
activeFill: '#1DE9AC'
},
cluster: {
stroke: '#D8E6EA',
label: {
stroke: '#fff',
color: '#2A6475'
}
}
}
export const graphDarkT = {
canvas: {
background: '#1f2937'
},
node: {
fill: '#7a8c9e',
activeFill: '#1DE9AC',
opacity: 1,
selectedOpacity: 1,
inactiveOpacity: 0.2,
label: {
stroke: '#1E2026',
color: '#ACBAC7',
activeColor: '#1DE9AC'
}
},
lasso: {
border: '1px solid #55aaff',
background: 'rgba(75, 160, 255, 0.1)'
},
ring: {
fill: '#54616D',
activeFill: '#1DE9AC'
},
edge: {
fill: '#474B56',
activeFill: '#1DE9AC',
opacity: 1,
selectedOpacity: 1,
inactiveOpacity: 0.1,
label: {
stroke: '#1E2026',
color: '#ACBAC7',
activeColor: '#1DE9AC'
}
},
arrow: {
fill: '#474B56',
activeFill: '#1DE9AC'
},
cluster: {
stroke: '#474B56',
label: {
stroke: '#1E2026',
color: '#ACBAC7'
}
}
}
// ======== Bracket Matching Themes ===========
export const bracketsLightT = {
'.cc-matchingBracket': {
fontWeight: 600,
},
'.cc-nonmatchingBracket': {
color: '#ef4444',
fontWeight: 700,
},
'&.cm-focused .cc-matchingBracket': {
backgroundColor: '#dae6f2',
},
};
export const bracketsDarkT = {
'.cc-matchingBracket': {
fontWeight: 600,
},
'.cc-nonmatchingBracket': {
color: '#ef4444',
fontWeight: 700,
},
'&.cm-focused .cc-matchingBracket': {
backgroundColor: '#734f00',
},
};

View File

@ -247,6 +247,8 @@ export interface IRSFormStats {
count_incalc: number count_incalc: number
count_termin: number count_termin: number
count_definition: number
count_convention: number
count_base: number count_base: number
count_constant: number count_constant: number
@ -281,10 +283,22 @@ export interface IRSFormUploadData {
} }
// ========== Library ===== // ========== Library =====
export enum LibraryFilterStrategy {
MANUAL = 'manual',
PERSONAL = 'personal',
COMMON = 'common',
SUBSCRIBE = 'subscribe',
CANONICAL = 'canonical',
OWNED = 'owned'
}
export interface ILibraryFilter { export interface ILibraryFilter {
ownedBy?: number query?: string
is_personal?: boolean
is_owned?: boolean
is_common?: boolean is_common?: boolean
queryMeta?: string is_canonical?: boolean
is_subscribed?: boolean
} }
// ================ Misc types ================ // ================ Misc types ================
@ -392,6 +406,8 @@ export function LoadRSFormData(schema: IRSFormData): IRSForm {
count_incalc: 0, count_incalc: 0,
count_termin: 0, count_termin: 0,
count_definition: 0,
count_convention: 0,
count_base: 0, count_base: 0,
count_constant: 0, count_constant: 0,
@ -416,6 +432,10 @@ export function LoadRSFormData(schema: IRSFormData): IRSForm {
count_termin: result.items.reduce( count_termin: result.items.reduce(
(sum, cst) => (sum + (cst.term?.raw ? 1 : 0) || 0), 0), (sum, cst) => (sum + (cst.term?.raw ? 1 : 0) || 0), 0),
count_definition: result.items.reduce(
(sum, cst) => (sum + (cst.definition?.text.raw ? 1 : 0) || 0), 0),
count_convention: result.items.reduce(
(sum, cst) => (sum + (cst.convention ? 1 : 0) || 0), 0),
count_base: result.items.reduce( count_base: result.items.reduce(
(sum, cst) => sum + (cst.cstType === CstType.BASE ? 1 : 0), 0), (sum, cst) => sum + (cst.cstType === CstType.BASE ? 1 : 0), 0),

View File

@ -1,6 +1,7 @@
import { LayoutTypes } from 'reagraph'; import { LayoutTypes } from 'reagraph';
import { ColoringScheme } from '../pages/RSFormPage/EditorTermGraph'; import { ColoringScheme } from '../pages/RSFormPage/EditorTermGraph';
import { IColorTheme } from './color';
import { resolveErrorClass,RSErrorClass, RSErrorType, TokenID } from './enums'; import { resolveErrorClass,RSErrorClass, RSErrorType, TokenID } from './enums';
import { import {
CstClass, CstMatchMode, CstType, CstClass, CstMatchMode, CstType,
@ -9,18 +10,7 @@ import {
ISyntaxTreeNode, ParsingStatus, ValueClass ISyntaxTreeNode, ParsingStatus, ValueClass
} from './models'; } from './models';
export interface IRSButtonData { export interface IDescriptor {
text: string
tooltip: string
}
export interface IFormatInfo {
text: string
color: string
tooltip: string
}
export interface ITopicInfo {
text: string text: string
tooltip: string tooltip: string
} }
@ -64,7 +54,7 @@ export function getCstExpressionPrefix(cst: IConstituenta): string {
return cst.alias + (cst.cstType === CstType.STRUCTURED ? '::=' : ':=='); return cst.alias + (cst.cstType === CstType.STRUCTURED ? '::=' : ':==');
} }
export function getRSButtonData(id: TokenID): IRSButtonData { export function getRSButtonData(id: TokenID): IDescriptor {
switch (id) { switch (id) {
case TokenID.BOOLEAN: return { case TokenID.BOOLEAN: return {
text: '()', text: '()',
@ -323,51 +313,45 @@ export const GraphColoringSelector: {value: ColoringScheme, label: string}[] = [
{ value: 'type', label: 'Цвет: класс'}, { value: 'type', label: 'Цвет: класс'},
]; ];
export function getCstStatusColor(status: ExpressionStatus, darkMode: boolean): string { export function getCstStatusColor(status: ExpressionStatus, colors: IColorTheme): string {
switch (status) { switch (status) {
case ExpressionStatus.VERIFIED: return darkMode ? '#2b8000': '#aaff80'; case ExpressionStatus.VERIFIED: return colors.green;
case ExpressionStatus.INCORRECT: return darkMode ? '#592b2b': '#ffc9c9'; case ExpressionStatus.INCORRECT: return colors.red;
case ExpressionStatus.INCALCULABLE: return darkMode ? '#964600': '#ffbb80'; case ExpressionStatus.INCALCULABLE: return colors.orange;
case ExpressionStatus.PROPERTY: return darkMode ? '#36899e': '#a5e9fa'; case ExpressionStatus.PROPERTY: return colors.teal;
case ExpressionStatus.UNKNOWN: return darkMode ? '#1e00b3': '#b3bdff'; case ExpressionStatus.UNKNOWN: return colors.blue;
case ExpressionStatus.UNDEFINED: return darkMode ? '#1e00b3': '#b3bdff'; case ExpressionStatus.UNDEFINED: return colors.blue;
} }
} }
export const mapStatusInfo: Map<ExpressionStatus, IFormatInfo> = new Map([ export const mapStatusInfo: Map<ExpressionStatus, IDescriptor> = new Map([
[ ExpressionStatus.VERIFIED, { [ ExpressionStatus.VERIFIED, {
text: 'ок', text: 'ок',
color: 'bg-[#aaff80] dark:bg-[#2b8000]',
tooltip: 'выражение корректно и вычислимо' tooltip: 'выражение корректно и вычислимо'
}], }],
[ ExpressionStatus.INCORRECT, { [ ExpressionStatus.INCORRECT, {
text: 'ошибка', text: 'ошибка',
color: 'bg-[#ffc9c9] dark:bg-[#592b2b]',
tooltip: 'ошибка в выражении' tooltip: 'ошибка в выражении'
}], }],
[ ExpressionStatus.INCALCULABLE, { [ ExpressionStatus.INCALCULABLE, {
text: 'невыч', text: 'невыч',
color: 'bg-[#ffbb80] dark:bg-[#964600]',
tooltip: 'выражение не вычислимо' tooltip: 'выражение не вычислимо'
}], }],
[ ExpressionStatus.PROPERTY, { [ ExpressionStatus.PROPERTY, {
text: 'св-во', text: 'св-во',
color: 'bg-[#a5e9fa] dark:bg-[#36899e]',
tooltip: 'можно проверить принадлежность, но нельзя получить значение' tooltip: 'можно проверить принадлежность, но нельзя получить значение'
}], }],
[ ExpressionStatus.UNKNOWN, { [ ExpressionStatus.UNKNOWN, {
text: 'неизв', text: 'неизв',
color: 'bg-[#b3bdff] dark:bg-[#1e00b3]',
tooltip: 'требует проверки выражения' tooltip: 'требует проверки выражения'
}], }],
[ ExpressionStatus.UNDEFINED, { [ ExpressionStatus.UNDEFINED, {
text: 'N/A', text: 'N/A',
color: 'bg-[#b3bdff] dark:bg-[#1e00b3]',
tooltip: 'произошла ошибка при проверке выражения' tooltip: 'произошла ошибка при проверке выражения'
}] }]
]); ]);
export const mapTopicInfo: Map<HelpTopic, ITopicInfo> = new Map([ export const mapTopicInfo: Map<HelpTopic, IDescriptor> = new Map([
[ HelpTopic.MAIN, { [ HelpTopic.MAIN, {
text: 'Портал', text: 'Портал',
tooltip: 'Общая справка по порталу' tooltip: 'Общая справка по порталу'
@ -406,34 +390,30 @@ export const mapTopicInfo: Map<HelpTopic, ITopicInfo> = new Map([
}], }],
]); ]);
export function getCstClassColor(cstClass: CstClass, darkMode: boolean): string { export function getCstClassColor(cstClass: CstClass, colors: IColorTheme): string {
switch (cstClass) { switch (cstClass) {
case CstClass.TEMPLATE: return darkMode ? '#36899e': '#a5e9fa'; case CstClass.BASIC: return colors.green;
case CstClass.BASIC: return darkMode ? '#2b8000': '#aaff80'; case CstClass.DERIVED: return colors.blue;
case CstClass.DERIVED: return darkMode ? '#1e00b3': '#b3bdff'; case CstClass.STATEMENT: return colors.red;
case CstClass.STATEMENT: return darkMode ? '#592b2b': '#ffc9c9'; case CstClass.TEMPLATE: return colors.teal;
} }
} }
export const mapCstClassInfo: Map<CstClass, IFormatInfo> = new Map([ export const mapCstClassInfo: Map<CstClass, IDescriptor> = new Map([
[ CstClass.BASIC, { [ CstClass.BASIC, {
text: 'базовый', text: 'базовый',
color: 'bg-[#aaff80] dark:bg-[#2b8000]',
tooltip: 'неопределяемое понятие, требует конвенции' tooltip: 'неопределяемое понятие, требует конвенции'
}], }],
[ CstClass.DERIVED, { [ CstClass.DERIVED, {
text: 'производный', text: 'производный',
color: 'bg-[#b3bdff] dark:bg-[#1e00b3]',
tooltip: 'выводимое понятие, задаваемое определением' tooltip: 'выводимое понятие, задаваемое определением'
}], }],
[ CstClass.STATEMENT, { [ CstClass.STATEMENT, {
text: 'утверждение', text: 'утверждение',
color: 'bg-[#ffc9c9] dark:bg-[#592b2b]',
tooltip: 'неопределяемое понятие, требует конвенции' tooltip: 'неопределяемое понятие, требует конвенции'
}], }],
[ CstClass.TEMPLATE, { [ CstClass.TEMPLATE, {
text: 'шаблон', text: 'шаблон',
color: 'bg-[#a5e9fa] dark:bg-[#36899e]',
tooltip: 'параметризованный шаблон определения' tooltip: 'параметризованный шаблон определения'
}], }],
]); ]);
@ -500,7 +480,7 @@ export function getTypificationLabel({isValid, resultType, args}: {
if (!isValid) { if (!isValid) {
return 'N/A'; return 'N/A';
} }
if (resultType === '') { if (resultType === '' || resultType === 'LOGIC') {
resultType = 'Логический' resultType = 'Логический'
} }
if (args.length === 0) { if (args.length === 0) {