Refactoring and multiple minor UI improvements

This commit is contained in:
IRBorisov 2023-07-31 22:38:58 +03:00
parent 19a02b435f
commit f03e61e3c1
26 changed files with 330 additions and 259 deletions

View File

@ -6,15 +6,17 @@ export interface CheckboxProps {
required?: boolean
disabled?: boolean
widthClass?: string
tooltip?: string
value?: boolean
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
}
function Checkbox({ id, required, disabled, label, widthClass = 'w-full', value, onChange }: CheckboxProps) {
// TODO: implement disabled={disabled}
function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-full', value, onChange }: CheckboxProps) {
return (
<div className={'flex gap-2 [&:not(:first-child)]:mt-3 ' + widthClass}>
<div className={'flex gap-2 [&:not(:first-child)]:mt-3 ' + widthClass} title={tooltip}>
<input id={id} type='checkbox'
className='relative cursor-pointer peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none clr-checkbox'
className='relative cursor-pointer disabled:cursor-not-allowed peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none clr-checkbox'
required={required}
disabled={disabled}
checked={value}

View File

@ -1,29 +0,0 @@
import { Ref } from 'react';
import { darkTheme, GraphCanvas, GraphCanvasProps, GraphCanvasRef, lightTheme } from 'reagraph';
import { useConceptTheme } from '../../context/ThemeContext';
import { resources } from '../../utils/constants';
interface ConceptGraphProps
extends Omit<GraphCanvasProps, 'theme' | 'labelFontUrl'> {
ref?: Ref<GraphCanvasRef>
sizeClass: string
}
function ConceptGraph({ sizeClass, ...props }: ConceptGraphProps) {
const { darkMode } = useConceptTheme();
return (
<div className='flex-wrap w-full h-full overflow-auto'>
<div className={`relative border ${sizeClass}`}>
<GraphCanvas
theme={darkMode ? darkTheme : lightTheme}
labelFontUrl={resources.graph_font}
{...props}
/>
</div>
</div>
);
}
export default ConceptGraph;

View File

@ -6,9 +6,10 @@ extends Omit<PropsWithRef<SelectProps<T>>, 'noDataLabel'> {
}
function ConceptSelect<T extends object | string>({ ...props }: ConceptSelectProps<T>) {
function ConceptSelect<T extends object | string>({ className, ...props }: ConceptSelectProps<T>) {
return (
<Select
className={`overflow-ellipsis whitespace-nowrap ${className}`}
{...props}
noDataLabel='Список пуст'
/>

View File

@ -3,18 +3,18 @@ import { ITooltip, Tooltip } from 'react-tooltip';
import { useConceptTheme } from '../../context/ThemeContext';
interface ConceptTooltipProps
extends Omit<ITooltip, 'variant' | 'place'> {
extends Omit<ITooltip, 'variant'> {
}
function ConceptTooltip({ className, ...props }: ConceptTooltipProps) {
function ConceptTooltip({ className, place='bottom', ...props }: ConceptTooltipProps) {
const { darkMode } = useConceptTheme();
return (
<Tooltip
className={`overflow-auto border shadow-md z-20 ${className}`}
variant={(darkMode ? 'dark' : 'light')}
place='bottom'
place={place}
{...props}
/>
);

View File

@ -3,11 +3,11 @@ interface DividerProps {
margins?: string
}
function Divider({ vertical, margins = '2' }: DividerProps) {
function Divider({ vertical, margins = 'mx-2' }: DividerProps) {
return (
<>
{vertical && <div className={`mx-${margins} border-x-2`} />}
{!vertical && <div className={`my-${margins} border-y-2`} />}
{vertical && <div className={`${margins} border-x-2`} />}
{!vertical && <div className={`${margins} border-y-2`} />}
</>
);
}

View File

@ -7,7 +7,7 @@ interface DropdownProps {
function Dropdown({ children, widthClass = 'w-fit', stretchLeft }: DropdownProps) {
return (
<div className='relative'>
<div className={`absolute ${stretchLeft ? 'right-0' : 'left-0'} py-2 z-10 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'} 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}`}>
{children}
</div>
</div>

View File

@ -1,6 +1,5 @@
import { useRef } from 'react';
import useClickedOutside from '../../hooks/useClickedOutside';
import useEscapeKey from '../../hooks/useEscapeKey';
import Button from './Button';
@ -16,7 +15,6 @@ interface ModalProps {
function Modal({ title, hideWindow, onSubmit, onCancel, canSubmit, children, submitText = 'Продолжить' }: ModalProps) {
const ref = useRef(null);
useClickedOutside({ ref, callback: hideWindow });
useEscapeKey(hideWindow);
const handleCancel = () => {
@ -34,7 +32,7 @@ function Modal({ title, hideWindow, onSubmit, onCancel, canSubmit, children, sub
<div className='fixed top-0 left-0 z-50 w-full h-full opacity-50 clr-modal'>
</div>
<div ref={ref} className='fixed bottom-1/2 left-1/2 translate-y-1/2 -translate-x-1/2 px-6 py-4 flex flex-col w-fit h-fit z-[60] clr-card border shadow-md mb-[5rem]'>
{ title && <h1 className='mb-4 text-xl font-bold text-center'>{title}</h1> }
{ title && <h1 className='mb-2 text-xl font-bold text-center'>{title}</h1> }
<div className='py-2'>
{children}
</div>

View File

@ -16,7 +16,6 @@ export function useRSFormDetails({ target }: { target?: string }) {
}
const schema = LoadRSFormData(data);
setInnerSchema(schema);
console.log('Loaded schema: ', schema);
}
const reload = useCallback(

View File

@ -9,7 +9,7 @@ export function useUserProfile() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined);
const fetchUser = useCallback(
const reload = useCallback(
() => {
setError(undefined);
setUser(undefined);
@ -29,14 +29,17 @@ export function useUserProfile() {
showError: true,
setLoading: setLoading,
onError: error => { setError(error); },
onSuccess: newData => { setUser(newData); if (callback) callback(newData) }
onSuccess: newData => {
setUser(newData);
if (callback) callback(newData);
}
});
}, [setUser]
)
);
useEffect(() => {
fetchUser();
}, [fetchUser])
reload();
}, [reload])
return { user, fetchUser, updateUser, error, loading };
return { user, updateUser, error, loading };
}

View File

@ -39,7 +39,7 @@ function DlgCreateCst({ hideWindow, defaultType, onCreate }: DlgCreateCstProps)
options={CstTypeSelector}
placeholder='Выберите тип'
values={selectedType ? [{ value: selectedType, label: getCstTypeLabel(selectedType) }] : []}
onChange={data => { setSelectedType(data[0].value); }}
onChange={data => { setSelectedType(data.length > 0 ? data[0].value : undefined); }}
/>
</Modal>
);

View File

@ -38,17 +38,20 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
return (
<Modal
title='Загрузка схемы из Экстеор'
title='Импорт схемы из Экстеора'
hideWindow={hideWindow}
canSubmit={!!file}
onSubmit={handleSubmit}
submitText='Загрузить'
>
<div className='max-w-[20rem]'>
<FileInput label='Загрузить файл'
<FileInput
label='Выбрать файл'
acceptType='.trs'
onChange={handleFile}
/>
<Checkbox label='Загружать метаданные'
<Checkbox
label='Загружать название и комментарий'
value={loadMetadata}
onChange={event => { setLoadMetadata(event.target.checked); }}
/>

View File

@ -15,7 +15,7 @@ import { RSTabsList } from './RSTabs';
interface EditorConstituentaProps {
onShowAST: (ast: SyntaxTree) => void
onShowCreateCst: (position: number | undefined, type: CstType | undefined) => void
onShowCreateCst: (selectedID: number | undefined, type: CstType | undefined) => void
}
function EditorConstituenta({ onShowAST, onShowCreateCst }: EditorConstituentaProps) {

View File

@ -8,12 +8,13 @@ import Divider from '../../components/Common/Divider';
import { ArrowDownIcon, ArrowsRotateIcon, ArrowUpIcon, DumpBinIcon, HelpIcon, SmallPlusIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import { CstType, type IConstituenta, ICstMovetoData, inferStatus, ParsingStatus, ValueClass } from '../../utils/models'
import { getCstTypePrefix, getCstTypeShortcut, getStatusInfo, getTypeLabel } from '../../utils/staticUI';
import { prefixes } from '../../utils/constants';
import { CstType, IConstituenta, ICstMovetoData } from '../../utils/models'
import { getCstTypePrefix, getCstTypeShortcut, getTypeLabel, mapStatusInfo } from '../../utils/staticUI';
interface EditorItemsProps {
onOpenEdit: (cst: IConstituenta) => void
onShowCreateCst: (position: number | undefined, type: CstType | undefined, skipDialog?: boolean) => void
onShowCreateCst: (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => void
}
function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) {
@ -121,8 +122,8 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) {
const selectedPosition = selected.reduce((prev, cstID) => {
const position = schema.items.findIndex(cst => cst.id === cstID);
return Math.max(position, prev);
}, -1) + 1;
const insert_where = selectedPosition > 0 ? selectedPosition : undefined;
}, -1);
const insert_where = selectedPosition >= 0 ? schema.items[selectedPosition].id : undefined;
onShowCreateCst(insert_where, type, type !== undefined);
}, [schema, onShowCreateCst, selected]);
@ -173,53 +174,30 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) {
selector: (cst: IConstituenta) => cst.id,
omit: true
},
{
name: 'Статус',
id: 'status',
cell: (cst: IConstituenta) =>
<div style={{ fontSize: 12 }}>
{getStatusInfo(inferStatus(cst.parse?.status, cst.parse?.valueClass)).text}
</div>,
width: '80px',
maxWidth: '80px',
reorder: true,
hide: 1280,
conditionalCellStyles: [
{
when: (cst: IConstituenta) => cst.parse?.status !== ParsingStatus.VERIFIED,
classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]']
},
{
when: (cst: IConstituenta) => cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.INVALID,
classNames: ['bg-[#beeefa]', 'dark:bg-[#286675]']
},
{
when: (cst: IConstituenta) => cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.PROPERTY,
classNames: ['bg-[#a5e9fa]', 'dark:bg-[#36899e]']
}
]
},
{
name: 'Имя',
id: 'alias',
selector: (cst: IConstituenta) => cst.alias,
cell: (cst: IConstituenta) => {
const info = mapStatusInfo.get(cst.status)!;
return (<>
<div
id={`${prefixes.cst_list}${cst.alias}`}
className={`w-full rounded-md text-center ${info.color}`}
>
{cst.alias}
</div>
<ConceptTooltip
anchorSelect={`#${prefixes.cst_list}${cst.alias}`}
place='right'
>
<p><b>Статус: </b> {info.tooltip}</p>
</ConceptTooltip>
</>);
},
width: '65px',
maxWidth: '65px',
reorder: true,
conditionalCellStyles: [
{
when: (cst: IConstituenta) => cst.parse?.status !== ParsingStatus.VERIFIED,
classNames: ['bg-[#ff8080]', 'dark:bg-[#800000]']
},
{
when: (cst: IConstituenta) => cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.INVALID,
classNames: ['bg-[#ffbb80]', 'dark:bg-[#964600]']
},
{
when: (cst: IConstituenta) => cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.PROPERTY,
classNames: ['bg-[#a5e9fa]', 'dark:bg-[#36899e]']
}
]
},
{
name: 'Тип',
@ -311,7 +289,7 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) {
dense
onClick={handleDelete}
/>
<Divider vertical margins='1' />
<Divider vertical margins='my-1' />
<Button
tooltip='Переиндексировать имена'
icon={<ArrowsRotateIcon color='text-primary' size={6}/>}
@ -341,9 +319,23 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) {
<div>
<h1>Горячие клавиши</h1>
<p><b>Двойной клик / Alt + клик</b> - редактирование конституенты</p>
<p><b>Клик на квадрат слева</b> - выделение конституенты</p>
<p><b>Alt + вверх/вниз</b> - движение конституент</p>
<p><b>Delete</b> - удаление конституент</p>
<p><b>Alt + 1-6, Q,W</b> - добавление конституент</p>
<Divider margins='mt-2' />
<h1>Статусы</h1>
{ [... mapStatusInfo.values()].map(info => {
return (<p className='py-1'>
<span className={`inline-block font-semibold min-w-[4rem] text-center border ${info.color}`}>
{info.text}
</span>
<span> - </span>
<span>
{info.tooltip}
</span>
</p>);
})}
</div>
</ConceptTooltip>
</div>}

View File

@ -7,7 +7,8 @@ import { Loader } from '../../components/Common/Loader';
import { useRSForm } from '../../context/RSFormContext';
import useCheckExpression from '../../hooks/useCheckExpression';
import { TokenID } from '../../utils/enums';
import { CstType, SyntaxTree } from '../../utils/models';
import { IRSErrorDescription, SyntaxTree } from '../../utils/models';
import { getCstExpressionPrefix } from '../../utils/staticUI';
import ParsingResult from './elements/ParsingResult';
import RSLocalButton from './elements/RSLocalButton';
import RSTokenButton from './elements/RSTokenButton';
@ -42,23 +43,43 @@ function EditorRSExpression({
resetParse();
}, [activeCst, resetParse]);
const handleCheckExpression = useCallback(() => {
function handleFocusIn() {
toggleEditMode()
}
function handleChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
onChange(event);
setIsModified(true);
}
function handleCheckExpression() {
if (!activeCst) {
return;
}
const prefix = activeCst?.alias + (activeCst?.cstType === CstType.STRUCTURED ? '::=' : ':==');
const prefix = getCstExpressionPrefix(activeCst);
const expression = prefix + value;
checkExpression(expression, parse => {
if (!parse.parseResult && parse.errors.length > 0) {
if (parse.errors.length > 0) {
const errorPosition = parse.errors[0].position - prefix.length
expressionCtrl.current!.selectionStart = errorPosition;
expressionCtrl.current!.selectionEnd = errorPosition;
expressionCtrl.current!.focus();
}
expressionCtrl.current!.focus();
setIsModified(false);
setTypification(parse.typification);
});
}, [value, checkExpression, activeCst, setTypification]);
}
const onShowError = useCallback(
(error: IRSErrorDescription) => {
if (!activeCst || !expressionCtrl.current) {
return;
}
const errorPosition = error.position - getCstExpressionPrefix(activeCst).length
expressionCtrl.current.selectionStart = errorPosition;
expressionCtrl.current.selectionEnd = errorPosition;
expressionCtrl.current.focus();
}, [activeCst]);
const handleEdit = useCallback((id: TokenID, key?: string) => {
if (!expressionCtrl.current) {
@ -77,12 +98,8 @@ function EditorRSExpression({
setIsModified(true);
}, [setValue]);
const handleChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange(event);
setIsModified(true);
}, [setIsModified, onChange]);
const handleInput = useCallback((event: React.KeyboardEvent<HTMLTextAreaElement>) => {
const handleInput = useCallback(
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (!expressionCtrl.current) {
return;
}
@ -108,10 +125,6 @@ function EditorRSExpression({
setIsModified(true);
}, [expressionCtrl, setValue]);
const handleFocusIn = useCallback(() => {
toggleEditMode()
}, [toggleEditMode]);
const EditButtons = useMemo(() => {
return (<div className='flex items-center justify-between w-full'>
<div className='text-sm w-fit'>
@ -188,7 +201,7 @@ function EditorRSExpression({
</div>
</div>
</div>);
}, [handleEdit])
}, [handleEdit]);
return (
<div className='flex flex-col items-start [&:not(:first-child)]:mt-3 w-full'>
@ -206,6 +219,7 @@ function EditorRSExpression({
onFocus={handleFocusIn}
onKeyDown={handleInput}
disabled={disabled}
spellCheck={false}
/>
<div className='flex w-full gap-4 py-1 mt-1 justify-stretch'>
<div className='flex flex-col gap-2'>
@ -230,7 +244,12 @@ function EditorRSExpression({
{ (loading || parseData) &&
<div className='w-full overflow-y-auto border mt-2 max-h-[14rem] min-h-[7rem]'>
{ loading && <Loader />}
{ !loading && parseData && <ParsingResult data={parseData} onShowAST={onShowAST} />}
{ !loading && parseData &&
<ParsingResult
data={parseData}
onShowAST={onShowAST}
onShowError={onShowError}
/>}
</div>}
</div>
);

View File

@ -1,15 +1,20 @@
import { useMemo, useRef } from 'react';
import { GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, useSelection } from 'reagraph';
import { useCallback, useMemo, useRef, useState } from 'react';
import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, lightTheme, useSelection } from 'reagraph';
import ConceptGraph from '../../components/Common/ConceptGraph';
import Button from '../../components/Common/Button';
import Checkbox from '../../components/Common/Checkbox';
import ConceptSelect from '../../components/Common/ConceptSelect';
import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import useLocalStorage from '../../hooks/useLocalStorage';
import { GraphLayoutSelector } from '../../utils/staticUI';
import { resources } from '../../utils/constants';
import { GraphLayoutSelector,mapLayoutLabels } from '../../utils/staticUI';
function EditorTermGraph() {
const { schema } = useRSForm();
const { darkMode } = useConceptTheme();
const [ layout, setLayout ] = useLocalStorage<LayoutTypes>('graph_layout', 'forceatlas2');
const [ orbit, setOrbit ] = useState(false);
const graphRef = useRef<GraphCanvasRef | null>(null);
const nodes: GraphNode[] = useMemo(() => {
@ -37,6 +42,11 @@ function EditorTermGraph() {
return result;
}, [schema?.graph]);
const handleCenter = useCallback(() => {
graphRef.current?.resetControls();
graphRef.current?.centerGraph();
}, []);
const {
selections, actives,
onNodeClick,
@ -44,44 +54,58 @@ function EditorTermGraph() {
onNodePointerOver,
onNodePointerOut
} = useSelection({
type: 'multi', // 'single' | 'multi' | 'multiModifier'
ref: graphRef,
nodes,
edges,
type: 'multi', // 'single' | 'multi' | 'multiModifier'
pathSelectionType: 'all',
focusOnSelect: 'singleOnly'
focusOnSelect: false
});
return (
<div>
return (<>
<div className='relative w-full'>
<div className='absolute top-0 left-0 z-20 px-3 py-2'>
<div className='absolute top-0 left-0 z-20 px-3 py-2 w-[12rem] flex flex-col gap-2'>
<ConceptSelect
className='w-[10rem]'
options={GraphLayoutSelector}
placeholder='Выберите тип'
values={layout ? [{ value: layout, label: String(layout) }] : []}
onChange={data => { data && setLayout(data[0].value); }}
values={layout ? [{ value: layout, label: mapLayoutLabels.get(layout) }] : []}
onChange={data => { setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value); }}
/>
<Checkbox
label='Анимация вращения'
widthClass='w-full'
value={orbit}
onChange={ event => setOrbit(event.target.checked) }/>
<Button
text='Центрировать'
dense
onClick={handleCenter}
/>
</div>
</div>
<ConceptGraph ref={graphRef}
sizeClass='w-[1240px] h-[800px] 2xl:w-[1880px] 2xl:h-[800px]'
<div className='flex-wrap w-full h-full overflow-auto'>
<div className='relative w-[1240px] h-[800px] 2xl:w-[1880px] 2xl:h-[800px]'>
<GraphCanvas
draggable
ref={graphRef}
nodes={nodes}
edges={edges}
draggable
layoutType={layout}
selections={selections}
actives={actives}
onCanvasClick={onCanvasClick}
onNodeClick={onNodeClick}
onCanvasClick={onCanvasClick}
defaultNodeSize={5}
onNodePointerOver={onNodePointerOver}
onNodePointerOut={onNodePointerOut}
// sizingType="default" // 'none' | 'pagerank' | 'centrality' | 'attribute' | 'default';
cameraMode={ layout.includes('3d') ? 'rotate' : 'pan'}
cameraMode={ orbit ? 'orbit' : layout.includes('3d') ? 'rotate' : 'pan'}
layoutOverrides={ layout.includes('tree') ? { nodeLevelRatio: 1 } : undefined }
labelFontUrl={resources.graph_font}
theme={darkMode ? darkTheme : lightTheme}
/>
</div>
);
</div>
</>);
}

View File

@ -8,6 +8,7 @@ import ConceptTab from '../../components/Common/ConceptTab';
import { Loader } from '../../components/Common/Loader';
import { useRSForm } from '../../context/RSFormContext';
import useLocalStorage from '../../hooks/useLocalStorage';
import { prefixes, timeout_updateUI } from '../../utils/constants';
import { CstType,type IConstituenta, ICstCreateData, SyntaxTree } from '../../utils/models';
import { createAliasFor } from '../../utils/staticUI';
import DlgCloneRSForm from './DlgCloneRSForm';
@ -42,34 +43,44 @@ function RSTabs() {
const [showAST, setShowAST] = useState(false);
const [defaultType, setDefaultType] = useState<CstType | undefined>(undefined);
const [insertPosition, setInsertPosition] = useState<number | undefined>(undefined);
const [insertWhere, setInsertWhere] = useState<number | undefined>(undefined);
const [showCreateCst, setShowCreateCst] = useState(false);
const handleAddNew = useCallback(
(type: CstType) => {
(type: CstType, selectedCst?: number) => {
if (!schema?.items) {
return;
}
const data: ICstCreateData = {
cst_type: type,
alias: createAliasFor(type, schema),
insert_after: insertPosition ?? null
insert_after: selectedCst ?? insertWhere ?? null
}
cstCreate(data, newCst => {
toast.success(`Конституента добавлена: ${newCst.alias}`);
if (activeTab === RSTabsList.CST_EDIT) {
navigate(`/rsforms/${schema.id}?tab=${RSTabsList.CST_EDIT}&active=${newCst.id}`);
navigate(`/rsforms/${schema.id}?tab=${activeTab}&active=${newCst.id}`);
if (activeTab === RSTabsList.CST_EDIT || activeTab == RSTabsList.CST_LIST) {
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: "end",
inline: "nearest"
});
}
}, timeout_updateUI);
}
});
}, [schema, cstCreate, insertPosition, navigate, activeTab]);
}, [schema, cstCreate, insertWhere, navigate, activeTab]);
const onShowCreateCst = useCallback(
(position: number | undefined, type: CstType | undefined, skipDialog?: boolean) => {
(selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => {
if (skipDialog && type) {
handleAddNew(type);
handleAddNew(type, selectedID);
} else {
setDefaultType(type);
setInsertPosition(position);
setInsertWhere(selectedID);
setShowCreateCst(true);
}
}, [handleAddNew]);

View File

@ -87,7 +87,7 @@ function RSTabsMenu({showUploadDialog, showCloneDialog}: RSTabsMenuProps) {
<DropdownButton onClick={handleDownload}>
<div className='inline-flex items-center justify-start gap-2'>
<DownloadIcon color='text-primary' size={4}/>
<p>Выгрузить файл Экстеор</p>
<p>Выгрузить в Экстеор</p>
</div>
</DropdownButton>
<DropdownButton disabled={!isEditable} onClick={handleUpload}>
@ -123,9 +123,14 @@ function RSTabsMenu({showUploadDialog, showCloneDialog}: RSTabsMenuProps) {
</p>
</div>
</DropdownButton>
{(isOwned || user?.is_staff) &&
<DropdownButton onClick={toggleReadonly}>
<Checkbox value={readonly} label='только чтение'/>
</DropdownButton>
<Checkbox
value={readonly}
label='Я — читатель!'
tooltip='Режим чтения'
/>
</DropdownButton>}
{user?.is_staff &&
<DropdownButton onClick={toggleForceAdmin}>
<Checkbox value={forceAdmin} label='режим администратора'/>

View File

@ -1,5 +1,6 @@
import ConceptTooltip from '../../../components/Common/ConceptTooltip';
import { IConstituenta } from '../../../utils/models';
import { getTypeLabel } from '../../../utils/staticUI';
interface ConstituentaTooltipProps {
data: IConstituenta
@ -10,11 +11,11 @@ function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
return (
<ConceptTooltip
anchorSelect={anchor}
className='max-w-[20rem] min-w-[20rem]'
className='max-w-[25rem] min-w-[25rem]'
>
<h1>Конституента {data.alias}</h1>
<p><b>Типизация: </b>{data.parse.typification}</p>
<p><b>Тремин: </b>{data.term.resolved || data.term.raw}</p>
<p><b>Типизация: </b>{getTypeLabel(data)}</p>
<p><b>Термин: </b>{data.term.resolved || data.term.raw}</p>
{data.definition.formal && <p><b>Выражение: </b>{data.definition.formal}</p>}
{data.definition.text.resolved && <p><b>Определение: </b>{data.definition.text.resolved}</p>}
{data.convention && <p><b>Конвенция: </b>{data.convention}</p>}

View File

@ -1,12 +1,13 @@
import { IExpressionParse, SyntaxTree } from '../../../utils/models';
import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '../../../utils/models';
import { getRSErrorMessage, getRSErrorPrefix } from '../../../utils/staticUI';
interface ParsingResultProps {
data: IExpressionParse
onShowAST: (ast: SyntaxTree) => void
onShowError: (error: IRSErrorDescription) => void
}
function ParsingResult({ data, onShowAST }: ParsingResultProps) {
function ParsingResult({ data, onShowAST, onShowError }: ParsingResultProps) {
const errorCount = data.errors.reduce((total, error) => (error.isCritical ? total + 1 : total), 0);
const warningsCount = data.errors.length - errorCount;
@ -19,9 +20,9 @@ function ParsingResult({ data, onShowAST }: ParsingResultProps) {
<p>Ошибок: <b>{errorCount}</b> | Предупреждений: <b>{warningsCount}</b></p>
{data.errors.map(error => {
return (
<p className='text-red'>
<span className='font-semibold'>{error.isCritical ? 'Ошибка' : 'Предупреждение'} {getRSErrorPrefix(error)}: </span>
<span>{getRSErrorMessage(error)}</span>
<p className='cursor-pointer text-red' onClick={() => onShowError(error)}>
<span className='mr-1 font-semibold underline'>{error.isCritical ? 'Ошибка' : 'Предупреждение'} {getRSErrorPrefix(error)}:</span>
<span> {getRSErrorMessage(error)}</span>
</p>
);
})}

View File

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { ExpressionStatus, type IConstituenta, IExpressionParse,inferStatus, ParsingStatus } from '../../../utils/models';
import { getStatusInfo } from '../../../utils/staticUI';
import { mapStatusInfo } from '../../../utils/staticUI';
interface StatusBarProps {
isModified?: boolean
@ -21,7 +21,7 @@ function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) {
return inferStatus(constituenta?.parse?.status, constituenta?.parse?.valueClass);
}, [isModified, constituenta, parseData]);
const data = getStatusInfo(status);
const data = mapStatusInfo.get(status)!;
return (
<div title={data.tooltip}
className={'min-h-[2rem] min-w-[6rem] font-semibold inline-flex border rounded-lg items-center justify-center align-middle ' + data.color}>

View File

@ -3,9 +3,11 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import Checkbox from '../../../components/Common/Checkbox';
import ConceptDataTable from '../../../components/Common/ConceptDataTable';
import { useRSForm } from '../../../context/RSFormContext';
import { useConceptTheme } from '../../../context/ThemeContext';
import useLocalStorage from '../../../hooks/useLocalStorage';
import { prefixes } from '../../../utils/constants';
import { CstType, extractGlobals,type IConstituenta, matchConstituenta } from '../../../utils/models';
import { getMockConstituenta } from '../../../utils/staticUI';
import { getMockConstituenta, mapStatusInfo } from '../../../utils/staticUI';
import ConstituentaTooltip from './ConstituentaTooltip';
interface ViewSideConstituentsProps {
@ -13,7 +15,8 @@ interface ViewSideConstituentsProps {
}
function ViewSideConstituents({ expression }: ViewSideConstituentsProps) {
const { schema, setActiveID } = useRSForm();
const { darkMode } = useConceptTheme();
const { schema, setActiveID, activeID } = useRSForm();
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '')
const [onlyExpression, setOnlyExpression] = useLocalStorage('side-filter-flag', false);
@ -52,6 +55,16 @@ function ViewSideConstituents({ expression }: ViewSideConstituentsProps) {
if (cst.id > 0) setActiveID(cst.id);
}, [setActiveID]);
const conditionalRowStyles = useMemo(() =>
[
{
when: (cst: IConstituenta) => cst.id === activeID,
style: {
backgroundColor: darkMode ? '#0068b3' : '#def1ff',
},
}
], [activeID, darkMode]);
const columns = useMemo(() =>
[
{
@ -63,13 +76,19 @@ function ViewSideConstituents({ expression }: ViewSideConstituentsProps) {
name: 'ID',
id: 'alias',
cell: (cst: IConstituenta) => {
return (<div>
<span id={cst.alias}>{cst.alias}</span>
<ConstituentaTooltip data={cst} anchor={`#${cst.alias}`} />
</div>);
const info = mapStatusInfo.get(cst.status)!;
return (<>
<div
id={`${prefixes.cst_list}${cst.alias}`}
className={`w-full rounded-md text-center ${info.color}`}
>
{cst.alias}
</div>
<ConstituentaTooltip data={cst} anchor={`#${prefixes.cst_list}${cst.alias}`} />
</>);
},
width: '62px',
maxWidth: '62px',
width: '65px',
maxWidth: '65px',
conditionalCellStyles: [
{
when: (cst: IConstituenta) => cst.id <= 0,
@ -132,6 +151,7 @@ function ViewSideConstituents({ expression }: ViewSideConstituentsProps) {
<ConceptDataTable
data={filteredData}
columns={columns}
conditionalRowStyles={conditionalRowStyles}
keyField='id'
noContextMenu
noDataComponent={<span className='flex flex-col justify-center p-2 text-center'>

View File

@ -101,9 +101,9 @@ export class TextWrapper implements IManagedText {
insertToken(tokenID: TokenID): boolean {
switch (tokenID) {
case TokenID.NT_DECLARATIVE_EXPR: this.envelopeWith('D{ξ∈X1 | ', '}'); return true;
case TokenID.NT_IMPERATIVE_EXPR: this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; ', '}'); return true;
case TokenID.NT_RECURSIVE_FULL: this.envelopeWith('R{ ξ:=D1 | 1=1 | ', '}'); return true;
case TokenID.NT_DECLARATIVE_EXPR: this.envelopeWith('D{ξ∈X1 | P1[ξ]', '}'); return true;
case TokenID.NT_IMPERATIVE_EXPR: this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; P1[σ, γ]', '}'); return true;
case TokenID.NT_RECURSIVE_FULL: this.envelopeWith('R{ ξ:=D1 | F1[ξ]≠∅ | ξF1[ξ]', '}'); return true;
case TokenID.BIGPR: this.envelopeWith('Pr1(', ')'); return true;
case TokenID.SMALLPR: this.envelopeWith('pr1(', ')'); return true;
case TokenID.FILTER: this.envelopeWith('Fi1[α](', ')'); return true;

View File

@ -71,7 +71,7 @@ export function postLogout(request: FrontAction) {
});
}
export function postSignup(request: IFrontRequest<IUserSignupData, IUserProfile>) {
export function postSignup(request: FrontExchange<IUserSignupData, IUserProfile>) {
AxiosPost({
title: 'Register user',
endpoint: `${config.url.AUTH}signup`,

View File

@ -13,6 +13,9 @@ const dev = {
}
};
export const config = process.env.NODE_ENV === 'production' ? prod : dev;
export const timeout_updateUI = 100;
export const urls = {
concept: 'https://www.acconcept.ru/',
exteor32: 'https://drive.google.com/open?id=1IHlMMwaYlAUBRSxU1RU_hXM5mFU9-oyK&usp=drive_fs',
@ -26,4 +29,6 @@ export const resources = {
graph_font: 'https://ey2pz3.csb.app/NotoSansSC-Regular.ttf'
}
export const config = process.env.NODE_ENV === 'production' ? prod : dev;
export const prefixes = {
cst_list: 'cst-list-'
}

View File

@ -21,8 +21,8 @@ export interface IUserSignupData extends Omit<IUser, 'is_staff' | 'id'> {
password: string
password2: string
}
export interface IUserProfile extends Omit<IUser, 'is_staff'> {}
export interface IUserUpdateData extends Omit<IUser, 'is_staff' | 'id'> {}
export interface IUserProfile extends Omit<IUser, 'is_staff'> {}
export interface IUserInfo extends Omit<IUserProfile, 'email'> {}
// ======== RS Parsing ============
@ -104,6 +104,7 @@ export interface IConstituenta {
resolved: string
}
}
status: ExpressionStatus
parse: {
status: ParsingStatus
valueClass: ValueClass
@ -216,10 +217,6 @@ export enum ExpressionStatus {
}
// ========== Model functions =================
export function extractGlobals(expression: string): Set<string> {
return new Set(expression.match(/[XCSADFPT]\d+/g) ?? []);
}
export function inferStatus(parse?: ParsingStatus, value?: ValueClass): ExpressionStatus {
if (!parse || !value) {
return ExpressionStatus.UNDEFINED;
@ -239,6 +236,10 @@ export function inferStatus(parse?: ParsingStatus, value?: ValueClass): Expressi
return ExpressionStatus.VERIFIED
}
export function extractGlobals(expression: string): Set<string> {
return new Set(expression.match(/[XCSADFPT]\d+/g) ?? []);
}
export function LoadRSFormData(schema: IRSFormData): IRSForm {
const result = schema as IRSForm
result.graph = new Graph;
@ -293,6 +294,7 @@ export function LoadRSFormData(schema: IRSFormData): IRSForm {
(sum, cst) => sum + (cst.cstType === CstType.THEOREM ? 1 : 0), 0)
}
result.items.forEach(cst => {
cst.status = inferStatus(cst.parse.status, cst.parse.valueClass);
result.graph.addNode(cst.id);
const dependencies = extractGlobals(cst.definition.formal);
dependencies.forEach(value => {

View File

@ -24,6 +24,23 @@ export function getTypeLabel(cst: IConstituenta) {
return 'Логический';
}
export function getCstTypePrefix(type: CstType) {
switch (type) {
case CstType.BASE: return 'X';
case CstType.CONSTANT: return 'C';
case CstType.STRUCTURED: return 'S';
case CstType.AXIOM: return 'A';
case CstType.TERM: return 'D';
case CstType.FUNCTION: return 'F';
case CstType.PREDICATE: return 'P';
case CstType.THEOREM: return 'T';
}
}
export function getCstExpressionPrefix(cst: IConstituenta): string {
return cst.alias + (cst.cstType === CstType.STRUCTURED ? '::=' : ':==');
}
export function getRSButtonData(id: TokenID): IRSButtonData {
switch (id) {
case TokenID.BOOLEAN: return {
@ -96,11 +113,11 @@ export function getRSButtonData(id: TokenID): IRSButtonData {
};
case TokenID.IN: return {
text: '∈',
tooltip: 'быть элементом (принадлежит) [Alt + \']'
tooltip: 'быть элементом (принадлежит) [Alt + 1]'
};
case TokenID.NOTIN: return {
text: '∉',
tooltip: 'не принадлежит [Alt + Shift + \']'
tooltip: 'не принадлежит [Alt + Shift + 1]'
};
case TokenID.SUBSET_OR_EQ: return {
text: '⊆',
@ -172,11 +189,11 @@ export function getRSButtonData(id: TokenID): IRSButtonData {
};
case TokenID.PUNC_ASSIGN: return {
text: ':=',
tooltip: 'присвоение (императивный синтаксис)'
tooltip: 'присвоение (императивный синтаксис) [Alt + Shift + 6]'
};
case TokenID.PUNC_ITERATE: return {
text: ':∈',
tooltip: 'перебор элементов множества (императивный синтаксис)'
tooltip: 'перебор элементов множества (императивный синтаксис) [Alt + 6]'
};
}
return {
@ -218,74 +235,70 @@ export const CstTypeSelector = (Object.values(CstType)).map(
return { value: type, label: getCstTypeLabel(type) };
});
export const mapLayoutLabels: Map<string, string> = new Map([
['forceatlas2', 'Атлас 2D'],
['forceDirected2d', 'Силы 2D'],
['forceDirected3d', 'Силы 3D'],
['treeTd2d', 'ДеревоВерт 2D'],
['treeTd3d', 'ДеревоВерт 3D'],
['treeLr2d', 'ДеревоГор 2D'],
['treeLr3d', 'ДеревоГор 3D'],
['radialOut2d', 'Радиальная 2D'],
['radialOut3d', 'Радиальная 3D'],
['circular2d', 'Круговая'],
['hierarchicalTd', 'ИерархияВерт'],
['hierarchicalLr', 'ИерархияГор'],
['nooverlap', 'Без перекрытия']
]);
export const GraphLayoutSelector: {value: LayoutTypes, label: string}[] = [
{ value: 'forceatlas2', label: 'forceatlas2'},
{ value: 'nooverlap', label: 'nooverlap'},
{ value: 'forceDirected2d', label: 'forceDirected2d'},
{ value: 'forceDirected3d', label: 'forceDirected3d'},
{ value: 'circular2d', label: 'circular2d'},
{ value: 'treeTd2d', label: 'treeTd2d'},
{ value: 'treeTd3d', label: 'treeTd3d'},
{ value: 'treeLr2d', label: 'treeLr2d'},
{ value: 'treeLr3d', label: 'treeLr3d'},
{ value: 'radialOut2d', label: 'radialOut2d'},
{ value: 'radialOut3d', label: 'radialOut3d'},
// { value: 'hierarchicalTd', label: 'hierarchicalTd'},
// { value: 'hierarchicalLr', label: 'hierarchicalLr'}
{ value: 'forceatlas2', label: 'Атлас 2D'},
{ value: 'forceDirected2d', label: 'Силы 2D'},
{ value: 'forceDirected3d', label: 'Силы 3D'},
{ value: 'treeTd2d', label: 'ДеревоВ 2D'},
{ value: 'treeTd3d', label: 'ДеревоВ 3D'},
{ value: 'treeLr2d', label: 'ДеревоГ 2D'},
{ value: 'treeLr3d', label: 'ДеревоГ 3D'},
{ value: 'radialOut2d', label: 'Радиальная 2D'},
{ value: 'radialOut3d', label: 'Радиальная 3D'},
// { value: 'circular2d', label: 'circular2d'},
// { value: 'nooverlap', label: 'nooverlap'},
// { value: 'hierarchicalTd', label: 'hierarchicalTd'},
// { value: 'hierarchicalLr', label: 'hierarchicalLr'}
];
export function getCstTypePrefix(type: CstType) {
switch (type) {
case CstType.BASE: return 'X';
case CstType.CONSTANT: return 'C';
case CstType.STRUCTURED: return 'S';
case CstType.AXIOM: return 'A';
case CstType.TERM: return 'D';
case CstType.FUNCTION: return 'F';
case CstType.PREDICATE: return 'P';
case CstType.THEOREM: return 'T';
}
}
export function getStatusInfo(status?: ExpressionStatus): IStatusInfo {
switch (status) {
case ExpressionStatus.UNDEFINED: return {
text: 'N/A',
color: 'bg-[#b3bdff] dark:bg-[#1e00b3]',
tooltip: 'произошла ошибка при проверке выражения'
};
case ExpressionStatus.UNKNOWN: return {
text: 'неизв',
color: 'bg-[#b3bdff] dark:bg-[#1e00b3]',
tooltip: 'требует проверки выражения'
};
case ExpressionStatus.INCORRECT: return {
text: 'ошибка',
color: 'bg-[#ff8080] dark:bg-[#800000]',
tooltip: 'ошибка в выражении'
};
case ExpressionStatus.INCALCULABLE: return {
text: 'невыч',
color: 'bg-[#ffbb80] dark:bg-[#964600]',
tooltip: 'выражение не вычислимо (экспоненциальная сложность)'
};
case ExpressionStatus.PROPERTY: return {
text: 'св-во',
color: 'bg-[#a5e9fa] dark:bg-[#36899e]',
tooltip: 'можно проверить принадлежность, но нельзя получить значение'
};
case ExpressionStatus.VERIFIED: return {
export const mapStatusInfo: Map<ExpressionStatus, IStatusInfo> = new Map([
[ ExpressionStatus.VERIFIED, {
text: 'ок',
color: 'bg-[#aaff80] dark:bg-[#2b8000]',
tooltip: 'выражение корректно и вычислимо'
};
}
return {
text: 'undefined',
color: '',
tooltip: '!ERROR!'
};
}
}],
[ ExpressionStatus.INCORRECT, {
text: 'ошибка',
color: 'bg-[#ffc9c9] dark:bg-[#592b2b]',
tooltip: 'ошибка в выражении'
}],
[ ExpressionStatus.INCALCULABLE, {
text: 'невыч',
color: 'bg-[#ffbb80] dark:bg-[#964600]',
tooltip: 'выражение не вычислимо (экспоненциальная сложность)'
}],
[ ExpressionStatus.PROPERTY, {
text: 'св-во',
color: 'bg-[#a5e9fa] dark:bg-[#36899e]',
tooltip: 'можно проверить принадлежность, но нельзя получить значение'
}],
[ ExpressionStatus.UNKNOWN, {
text: 'неизв',
color: 'bg-[#b3bdff] dark:bg-[#1e00b3]',
tooltip: 'требует проверки выражения'
}],
[ ExpressionStatus.UNDEFINED, {
text: 'N/A',
color: 'bg-[#b3bdff] dark:bg-[#1e00b3]',
tooltip: 'произошла ошибка при проверке выражения'
}],
]);
export function createAliasFor(type: CstType, schema: IRSForm): string {
const prefix = getCstTypePrefix(type);
@ -320,6 +333,7 @@ export function getMockConstituenta(id: number, alias: string, type: CstType, co
resolved: ''
}
},
status: ExpressionStatus.INCORRECT,
parse: {
status: ParsingStatus.INCORRECT,
valueClass: ValueClass.INVALID,
@ -365,7 +379,7 @@ export function getRSErrorMessage(error: IRSErrorDescription): string {
case RSErrorType.localDoubleDeclare:
return `Предупреждение! Повторное объявление локальной переменной ${error.params[0]}`;
case RSErrorType.localNotUsed:
return `Предупреждение! Переменная объявлена но не использована: ${error.params[0]}`;
return `Предупреждение! Переменная объявлена, но не использована: ${error.params[0]}`;
case RSErrorType.localShadowing:
return `Повторное объявление переменной: ${error.params[0]}`;