mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Multiple UI improvements for RSForm edit
This commit is contained in:
parent
6bb034ae51
commit
47564c9d91
|
@ -1,3 +1,5 @@
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
export interface CheckboxProps {
|
export interface CheckboxProps {
|
||||||
|
@ -11,21 +13,34 @@ export interface CheckboxProps {
|
||||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement disabled={disabled}
|
|
||||||
function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-full', value, onChange }: CheckboxProps) {
|
function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-full', value, onChange }: CheckboxProps) {
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const cursor = disabled ? 'cursor-not-allowed' : 'cursor-pointer';
|
||||||
|
|
||||||
|
function handleLabelClick(event: React.MouseEvent<HTMLLabelElement, MouseEvent>): void {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!disabled) {
|
||||||
|
inputRef.current?.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex gap-2 [&:not(:first-child)]:mt-3 ' + widthClass} title={tooltip}>
|
<div className={'flex gap-2 [&:not(:first-child)]:mt-3 ' + widthClass} title={tooltip}>
|
||||||
<input id={id} type='checkbox'
|
<input id={id} type='checkbox' ref={inputRef}
|
||||||
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'
|
className={`relative peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none clr-checkbox ${cursor}`}
|
||||||
required={required}
|
required={required}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
checked={value}
|
checked={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
{ label && <Label
|
{ label &&
|
||||||
|
<Label
|
||||||
|
className={`${cursor}`}
|
||||||
text={label}
|
text={label}
|
||||||
required={required}
|
required={required}
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
|
onClick={handleLabelClick}
|
||||||
/>}
|
/>}
|
||||||
<svg
|
<svg
|
||||||
className='absolute hidden w-3 h-3 mt-1 ml-0.5 text-white pointer-events-none peer-checked:block'
|
className='absolute hidden w-3 h-3 mt-1 ml-0.5 text-white pointer-events-none peer-checked:block'
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
interface LabelProps {
|
import { LabelHTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
interface LabelProps
|
||||||
|
extends Omit<React.DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>, 'children'> {
|
||||||
text: string
|
text: string
|
||||||
htmlFor?: string
|
|
||||||
required?: boolean
|
required?: boolean
|
||||||
title?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Label({ text, htmlFor, required = false, title }: LabelProps) {
|
function Label({ text, required, title, className, ...props }: LabelProps) {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className='text-sm font-semibold'
|
className={`${className} text-sm font-semibold`}
|
||||||
htmlFor={htmlFor}
|
|
||||||
title={ (required && !title) ? 'обязательное поле' : title }
|
title={ (required && !title) ? 'обязательное поле' : title }
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
{text + (required ? '*' : '')}
|
{text + (required ? '*' : '')}
|
||||||
</label>
|
</label>
|
||||||
|
|
|
@ -9,15 +9,13 @@ import {
|
||||||
patchRSForm,
|
patchRSForm,
|
||||||
patchUploadTRS, postClaimRSForm, postCloneRSForm,postNewConstituenta} from '../utils/backendAPI'
|
patchUploadTRS, postClaimRSForm, postCloneRSForm,postNewConstituenta} from '../utils/backendAPI'
|
||||||
import {
|
import {
|
||||||
IConstituenta, IConstituentaList, IConstituentaMeta, ICstCreateData,
|
IConstituentaList, IConstituentaMeta, ICstCreateData,
|
||||||
ICstMovetoData, ICstUpdateData, IRSForm, IRSFormCreateData, IRSFormData, IRSFormMeta, IRSFormUpdateData, IRSFormUploadData
|
ICstMovetoData, ICstUpdateData, IRSForm, IRSFormCreateData, IRSFormData, IRSFormMeta, IRSFormUpdateData, IRSFormUploadData
|
||||||
} from '../utils/models'
|
} from '../utils/models'
|
||||||
import { useAuth } from './AuthContext'
|
import { useAuth } from './AuthContext'
|
||||||
|
|
||||||
interface IRSFormContext {
|
interface IRSFormContext {
|
||||||
schema?: IRSForm
|
schema?: IRSForm
|
||||||
activeCst?: IConstituenta
|
|
||||||
activeID?: number
|
|
||||||
|
|
||||||
error: ErrorInfo
|
error: ErrorInfo
|
||||||
loading: boolean
|
loading: boolean
|
||||||
|
@ -30,7 +28,6 @@ interface IRSFormContext {
|
||||||
isTracking: boolean
|
isTracking: boolean
|
||||||
isForceAdmin: boolean
|
isForceAdmin: boolean
|
||||||
|
|
||||||
setActiveID: React.Dispatch<React.SetStateAction<number | undefined>>
|
|
||||||
toggleForceAdmin: () => void
|
toggleForceAdmin: () => void
|
||||||
toggleReadonly: () => void
|
toggleReadonly: () => void
|
||||||
toggleTracking: () => void
|
toggleTracking: () => void
|
||||||
|
@ -69,7 +66,6 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
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)
|
||||||
const [activeID, setActiveID] = useState<number | undefined>(undefined)
|
|
||||||
|
|
||||||
const [isForceAdmin, setIsForceAdmin] = useState(false)
|
const [isForceAdmin, setIsForceAdmin] = useState(false)
|
||||||
const [isReadonly, setIsReadonly] = useState(false)
|
const [isReadonly, setIsReadonly] = useState(false)
|
||||||
|
@ -84,11 +80,6 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
)
|
)
|
||||||
}, [user?.is_staff, isReadonly, isForceAdmin, isOwned, loading])
|
}, [user?.is_staff, isReadonly, isForceAdmin, isOwned, loading])
|
||||||
|
|
||||||
const activeCst = useMemo(
|
|
||||||
() => {
|
|
||||||
return schema?.items?.find((cst) => cst.id === activeID)
|
|
||||||
}, [schema?.items, activeID])
|
|
||||||
|
|
||||||
const isTracking = useMemo(
|
const isTracking = useMemo(
|
||||||
() => {
|
() => {
|
||||||
return true
|
return true
|
||||||
|
@ -272,7 +263,6 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
<RSFormContext.Provider value={{
|
<RSFormContext.Provider value={{
|
||||||
schema,
|
schema,
|
||||||
error, loading, processing,
|
error, loading, processing,
|
||||||
activeID, activeCst, setActiveID,
|
|
||||||
isForceAdmin, isReadonly, isOwned, isEditable,
|
isForceAdmin, isReadonly, isOwned, isEditable,
|
||||||
isClaimable, isTracking,
|
isClaimable, isTracking,
|
||||||
toggleForceAdmin: () => { setIsForceAdmin(prev => !prev) },
|
toggleForceAdmin: () => { setIsForceAdmin(prev => !prev) },
|
||||||
|
|
62
rsconcept/frontend/src/pages/RSFormPage/DlgDeleteCst.tsx
Normal file
62
rsconcept/frontend/src/pages/RSFormPage/DlgDeleteCst.tsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import Checkbox from '../../components/Common/Checkbox';
|
||||||
|
import Modal from '../../components/Common/Modal';
|
||||||
|
import { useRSForm } from '../../context/RSFormContext';
|
||||||
|
import { getCstLabel } from '../../utils/staticUI';
|
||||||
|
|
||||||
|
interface DlgDeleteCstProps {
|
||||||
|
hideWindow: () => void
|
||||||
|
selected: number[]
|
||||||
|
onDelete: (items: number[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function DlgDeleteCst({ hideWindow, selected, onDelete }: DlgDeleteCstProps) {
|
||||||
|
const { schema } = useRSForm();
|
||||||
|
|
||||||
|
const [ expandOut, setExpandOut ] = useState(false);
|
||||||
|
const expansion: number[] = useMemo(() => schema?.graph.expandOutputs(selected) ?? [], [selected, schema?.graph]);
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
hideWindow();
|
||||||
|
if (expandOut) {
|
||||||
|
onDelete(selected.concat(expansion));
|
||||||
|
} else {
|
||||||
|
onDelete(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title='Удаление конституент'
|
||||||
|
hideWindow={hideWindow}
|
||||||
|
canSubmit={true}
|
||||||
|
submitText={expandOut ? 'Удалить с зависимыми' : 'Удалить'}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<div className='max-w-[60vw] min-w-[20rem]'>
|
||||||
|
<p>Выбраны к удалению: <b>{selected.length}</b></p>
|
||||||
|
<div className='px-3 border h-[9rem] overflow-y-auto whitespace-nowrap'>
|
||||||
|
{selected.map(id => {
|
||||||
|
const cst = schema!.items.find(cst => cst.id === id);
|
||||||
|
return (cst && <p>{getCstLabel(cst)}</p>);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<p className='mt-4'>Зависимые конституенты: <b>{expansion.length}</b></p>
|
||||||
|
<div className='px-3 border h-[9rem] overflow-y-auto whitespace-nowrap'>
|
||||||
|
{expansion.map(id => {
|
||||||
|
const cst = schema!.items.find(cst => cst.id === id);
|
||||||
|
return (cst && <p>{getCstLabel(cst)}</p>);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
label='Удалить зависимые конституенты'
|
||||||
|
value={expandOut}
|
||||||
|
onChange={data => setExpandOut(data.target.checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DlgDeleteCst;
|
|
@ -1,5 +1,4 @@
|
||||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import MiniButton from '../../components/Common/MiniButton';
|
import MiniButton from '../../components/Common/MiniButton';
|
||||||
|
@ -11,19 +10,21 @@ import { type CstType, EditMode, ICstUpdateData, SyntaxTree } from '../../utils/
|
||||||
import { getCstTypeLabel } from '../../utils/staticUI';
|
import { getCstTypeLabel } from '../../utils/staticUI';
|
||||||
import EditorRSExpression from './EditorRSExpression';
|
import EditorRSExpression from './EditorRSExpression';
|
||||||
import ViewSideConstituents from './elements/ViewSideConstituents';
|
import ViewSideConstituents from './elements/ViewSideConstituents';
|
||||||
import { RSTabsList } from './RSTabs';
|
|
||||||
|
|
||||||
interface EditorConstituentaProps {
|
interface EditorConstituentaProps {
|
||||||
|
activeID?: number
|
||||||
|
onOpenEdit: (cstID: number) => void
|
||||||
onShowAST: (expression: string, ast: SyntaxTree) => void
|
onShowAST: (expression: string, ast: SyntaxTree) => void
|
||||||
onShowCreateCst: (selectedID: number | undefined, type: CstType | undefined) => void
|
onCreateCst: (selectedID: number | undefined, type: CstType | undefined) => void
|
||||||
|
onDeleteCst: (selected: number[], callback?: (items: number[]) => void) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorConstituenta({ onShowAST, onShowCreateCst }: EditorConstituentaProps) {
|
function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDeleteCst }: EditorConstituentaProps) {
|
||||||
const navigate = useNavigate();
|
const { schema, processing, isEditable, cstUpdate } = useRSForm();
|
||||||
const {
|
const activeCst = useMemo(
|
||||||
activeCst, activeID, schema, setActiveID, processing, isEditable,
|
() => {
|
||||||
cstDelete, cstUpdate
|
return schema?.items?.find((cst) => cst.id === activeID);
|
||||||
} = useRSForm();
|
}, [schema?.items, activeID]);
|
||||||
|
|
||||||
const [isModified, setIsModified] = useState(false);
|
const [isModified, setIsModified] = useState(false);
|
||||||
const [editMode, setEditMode] = useState(EditMode.TEXT);
|
const [editMode, setEditMode] = useState(EditMode.TEXT);
|
||||||
|
@ -38,12 +39,6 @@ function EditorConstituenta({ onShowAST, onShowCreateCst }: EditorConstituentaPr
|
||||||
|
|
||||||
const isEnabled = useMemo(() => activeCst && isEditable, [activeCst, isEditable]);
|
const isEnabled = useMemo(() => activeCst && isEditable, [activeCst, isEditable]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (schema && schema?.items.length > 0) {
|
|
||||||
setActiveID((prev) => (prev ?? schema.items[0].id ?? undefined));
|
|
||||||
}
|
|
||||||
}, [schema, setActiveID]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!activeCst) {
|
if (!activeCst) {
|
||||||
setIsModified(false);
|
setIsModified(false);
|
||||||
|
@ -71,8 +66,7 @@ function EditorConstituenta({ onShowAST, onShowCreateCst }: EditorConstituentaPr
|
||||||
}
|
}
|
||||||
}, [activeCst]);
|
}, [activeCst]);
|
||||||
|
|
||||||
const handleSubmit =
|
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
(event: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!activeID || processing) {
|
if (!activeID || processing) {
|
||||||
return;
|
return;
|
||||||
|
@ -86,40 +80,29 @@ function EditorConstituenta({ onShowAST, onShowCreateCst }: EditorConstituentaPr
|
||||||
term_raw: term
|
term_raw: term
|
||||||
};
|
};
|
||||||
cstUpdate(data, () => { toast.success('Изменения сохранены'); });
|
cstUpdate(data, () => { toast.success('Изменения сохранены'); });
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDelete = useCallback(
|
function handleDelete() {
|
||||||
() => {
|
if (!schema || !activeID) {
|
||||||
if (!activeID || !schema?.items || !window.confirm('Вы уверены, что хотите удалить конституенту?')) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = {
|
onDeleteCst([activeID]);
|
||||||
items: [{ id: activeID }]
|
|
||||||
}
|
}
|
||||||
const index = schema.items.findIndex((cst) => cst.id === activeID);
|
|
||||||
let newActive: number | undefined = undefined
|
|
||||||
if (index !== -1 && index + 1 < schema.items.length) {
|
|
||||||
newActive = schema.items[index + 1].id;
|
|
||||||
}
|
|
||||||
cstDelete(data, () => toast.success('Конституента удалена'));
|
|
||||||
if (newActive) navigate(`/rsforms/${schema.id}?tab=${RSTabsList.CST_EDIT}&active=${newActive}`);
|
|
||||||
}, [activeID, schema, cstDelete, navigate]);
|
|
||||||
|
|
||||||
const handleAddNew = useCallback(
|
function handleCreateCst() {
|
||||||
() => {
|
|
||||||
if (!activeID || !schema) {
|
if (!activeID || !schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onShowCreateCst(activeID, activeCst?.cstType);
|
onCreateCst(activeID, activeCst?.cstType);
|
||||||
}, [activeID, activeCst?.cstType, schema, onShowCreateCst]);
|
}
|
||||||
|
|
||||||
const handleRename = useCallback(() => {
|
function handleRename() {
|
||||||
toast.info('Переименование в разработке');
|
toast.info('Переименование в разработке');
|
||||||
}, []);
|
}
|
||||||
|
|
||||||
const handleChangeType = useCallback(() => {
|
function handleChangeType() {
|
||||||
toast.info('Изменение типа в разработке');
|
toast.info('Изменение типа в разработке');
|
||||||
}, []);
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-start w-full gap-2'>
|
<div className='flex items-start w-full gap-2'>
|
||||||
|
@ -158,7 +141,7 @@ function EditorConstituenta({ onShowAST, onShowCreateCst }: EditorConstituentaPr
|
||||||
<MiniButton
|
<MiniButton
|
||||||
tooltip='Создать конституенты после данной'
|
tooltip='Создать конституенты после данной'
|
||||||
disabled={!isEnabled}
|
disabled={!isEnabled}
|
||||||
onClick={handleAddNew}
|
onClick={handleCreateCst}
|
||||||
icon={<SmallPlusIcon size={5} color={isEnabled ? 'text-green' : ''} />}
|
icon={<SmallPlusIcon size={5} color={isEnabled ? 'text-green' : ''} />}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
@ -220,7 +203,11 @@ function EditorConstituenta({ onShowAST, onShowCreateCst }: EditorConstituentaPr
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<ViewSideConstituents expression={expression}/>
|
<ViewSideConstituents
|
||||||
|
expression={expression}
|
||||||
|
activeID={activeID}
|
||||||
|
onOpenEdit={onOpenEdit}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,56 +13,32 @@ import { CstType, IConstituenta, ICstMovetoData } from '../../utils/models'
|
||||||
import { getCstTypePrefix, getCstTypeShortcut, getTypeLabel, mapStatusInfo } from '../../utils/staticUI';
|
import { getCstTypePrefix, getCstTypeShortcut, getTypeLabel, mapStatusInfo } from '../../utils/staticUI';
|
||||||
|
|
||||||
interface EditorItemsProps {
|
interface EditorItemsProps {
|
||||||
onOpenEdit: (cst: IConstituenta) => void
|
onOpenEdit: (cstID: number) => void
|
||||||
onShowCreateCst: (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => void
|
onCreateCst: (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => void
|
||||||
|
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) {
|
function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps) {
|
||||||
const {
|
const { schema, isEditable, cstMoveTo, resetAliases } = useRSForm();
|
||||||
schema, isEditable,
|
|
||||||
cstDelete, cstMoveTo, resetAliases
|
|
||||||
} = useRSForm();
|
|
||||||
const { noNavigation } = useConceptTheme();
|
const { noNavigation } = useConceptTheme();
|
||||||
const [selected, setSelected] = useState<number[]>([]);
|
const [selected, setSelected] = useState<number[]>([]);
|
||||||
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
||||||
|
|
||||||
const [toggledClearRows, setToggledClearRows] = useState(false);
|
const [toggledClearRows, setToggledClearRows] = useState(false);
|
||||||
|
|
||||||
const handleRowClicked = useCallback(
|
|
||||||
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
|
|
||||||
if (event.altKey) {
|
|
||||||
onOpenEdit(cst);
|
|
||||||
}
|
|
||||||
}, [onOpenEdit]);
|
|
||||||
|
|
||||||
const handleSelectionChange = useCallback(
|
|
||||||
({ selectedRows }: {
|
|
||||||
allSelected: boolean
|
|
||||||
selectedCount: number
|
|
||||||
selectedRows: IConstituenta[]
|
|
||||||
}) => {
|
|
||||||
setSelected(selectedRows.map(cst => cst.id));
|
|
||||||
}, [setSelected]);
|
|
||||||
|
|
||||||
// Delete selected constituents
|
// Delete selected constituents
|
||||||
const handleDelete = useCallback(() => {
|
function handleDelete() {
|
||||||
if (!schema?.items || !window.confirm('Вы уверены, что хотите удалить выбранные конституенты?')) {
|
if (!schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = {
|
onDeleteCst(selected, () => {
|
||||||
items: selected.map(id => { return { id }; })
|
|
||||||
}
|
|
||||||
const deletedNames = selected.map(id => schema.items?.find(cst => cst.id === id)?.alias).join(', ');
|
|
||||||
cstDelete(data, () => {
|
|
||||||
toast.success(`Конституенты удалены: ${deletedNames}`);
|
|
||||||
setToggledClearRows(prev => !prev);
|
setToggledClearRows(prev => !prev);
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
});
|
});
|
||||||
}, [selected, schema?.items, cstDelete]);
|
}
|
||||||
|
|
||||||
// Move selected cst up
|
// Move selected cst up
|
||||||
const handleMoveUp = useCallback(
|
function handleMoveUp() {
|
||||||
() => {
|
|
||||||
if (!schema?.items || selected.length === 0) {
|
if (!schema?.items || selected.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -80,11 +56,10 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) {
|
||||||
move_to: target
|
move_to: target
|
||||||
}
|
}
|
||||||
cstMoveTo(data);
|
cstMoveTo(data);
|
||||||
}, [selected, schema?.items, cstMoveTo]);
|
}
|
||||||
|
|
||||||
// Move selected cst down
|
// Move selected cst down
|
||||||
const handleMoveDown = useCallback(
|
function handleMoveDown() {
|
||||||
() => {
|
|
||||||
if (!schema?.items || selected.length === 0) {
|
if (!schema?.items || selected.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -106,16 +81,15 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) {
|
||||||
move_to: target
|
move_to: target
|
||||||
}
|
}
|
||||||
cstMoveTo(data);
|
cstMoveTo(data);
|
||||||
}, [selected, schema?.items, cstMoveTo]);
|
}
|
||||||
|
|
||||||
// Generate new names for all constituents
|
// Generate new names for all constituents
|
||||||
const handleReindex = useCallback(() => {
|
function handleReindex() {
|
||||||
resetAliases(() => toast.success('Переиндексация конституент успешна'));
|
resetAliases(() => toast.success('Переиндексация конституент успешна'));
|
||||||
}, [resetAliases]);
|
}
|
||||||
|
|
||||||
// Add new constituent
|
// Add new constituenta
|
||||||
const handleAddNew = useCallback(
|
function handleCreateCst(type?: CstType){
|
||||||
(type?: CstType) => {
|
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -124,8 +98,8 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) {
|
||||||
return Math.max(position, prev);
|
return Math.max(position, prev);
|
||||||
}, -1);
|
}, -1);
|
||||||
const insert_where = selectedPosition >= 0 ? schema.items[selectedPosition].id : undefined;
|
const insert_where = selectedPosition >= 0 ? schema.items[selectedPosition].id : undefined;
|
||||||
onShowCreateCst(insert_where, type, type !== undefined);
|
onCreateCst(insert_where, type, type !== undefined);
|
||||||
}, [schema, onShowCreateCst, selected]);
|
}
|
||||||
|
|
||||||
// Implement hotkeys for working with constituents table
|
// Implement hotkeys for working with constituents table
|
||||||
function handleTableKey(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleTableKey(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
|
@ -154,18 +128,34 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case '1': handleAddNew(CstType.BASE); return true;
|
case '1': handleCreateCst(CstType.BASE); return true;
|
||||||
case '2': handleAddNew(CstType.STRUCTURED); return true;
|
case '2': handleCreateCst(CstType.STRUCTURED); return true;
|
||||||
case '3': handleAddNew(CstType.TERM); return true;
|
case '3': handleCreateCst(CstType.TERM); return true;
|
||||||
case '4': handleAddNew(CstType.AXIOM); return true;
|
case '4': handleCreateCst(CstType.AXIOM); return true;
|
||||||
case 'q': handleAddNew(CstType.FUNCTION); return true;
|
case 'q': handleCreateCst(CstType.FUNCTION); return true;
|
||||||
case 'w': handleAddNew(CstType.PREDICATE); return true;
|
case 'w': handleCreateCst(CstType.PREDICATE); return true;
|
||||||
case '5': handleAddNew(CstType.CONSTANT); return true;
|
case '5': handleCreateCst(CstType.CONSTANT); return true;
|
||||||
case '6': handleAddNew(CstType.THEOREM); return true;
|
case '6': handleCreateCst(CstType.THEOREM); return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRowClicked = useCallback(
|
||||||
|
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
|
||||||
|
if (event.altKey) {
|
||||||
|
onOpenEdit(cst.id);
|
||||||
|
}
|
||||||
|
}, [onOpenEdit]);
|
||||||
|
|
||||||
|
const handleSelectionChange = useCallback(
|
||||||
|
({ selectedRows }: {
|
||||||
|
allSelected: boolean
|
||||||
|
selectedCount: number
|
||||||
|
selectedRows: IConstituenta[]
|
||||||
|
}) => {
|
||||||
|
setSelected(selectedRows.map(cst => cst.id));
|
||||||
|
}, [setSelected]);
|
||||||
|
|
||||||
const columns = useMemo(() =>
|
const columns = useMemo(() =>
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
@ -300,7 +290,7 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) {
|
||||||
tooltip='Новая конституента'
|
tooltip='Новая конституента'
|
||||||
icon={<SmallPlusIcon color='text-green' size={6}/>}
|
icon={<SmallPlusIcon color='text-green' size={6}/>}
|
||||||
dense
|
dense
|
||||||
onClick={() => { handleAddNew(); }}
|
onClick={() => handleCreateCst()}
|
||||||
/>
|
/>
|
||||||
{(Object.values(CstType)).map(
|
{(Object.values(CstType)).map(
|
||||||
(typeStr) => {
|
(typeStr) => {
|
||||||
|
@ -309,7 +299,7 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) {
|
||||||
text={`${getCstTypePrefix(type)}`}
|
text={`${getCstTypePrefix(type)}`}
|
||||||
tooltip={getCstTypeShortcut(type)}
|
tooltip={getCstTypeShortcut(type)}
|
||||||
dense
|
dense
|
||||||
onClick={() => { handleAddNew(type); }}
|
onClick={() => handleCreateCst(type)}
|
||||||
/>;
|
/>;
|
||||||
})}
|
})}
|
||||||
<div id='items-table-help'>
|
<div id='items-table-help'>
|
||||||
|
@ -359,7 +349,7 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) {
|
||||||
selectableRows
|
selectableRows
|
||||||
selectableRowsHighlight
|
selectableRowsHighlight
|
||||||
onSelectedRowsChange={handleSelectionChange}
|
onSelectedRowsChange={handleSelectionChange}
|
||||||
onRowDoubleClicked={onOpenEdit}
|
onRowDoubleClicked={cst => onOpenEdit(cst.id)}
|
||||||
onRowClicked={handleRowClicked}
|
onRowClicked={handleRowClicked}
|
||||||
clearSelectedRows={toggledClearRows}
|
clearSelectedRows={toggledClearRows}
|
||||||
dense
|
dense
|
||||||
|
|
|
@ -30,7 +30,7 @@ function EditorTermGraph() {
|
||||||
const result: GraphEdge[] = [];
|
const result: GraphEdge[] = [];
|
||||||
let edgeID = 1;
|
let edgeID = 1;
|
||||||
schema?.graph.nodes.forEach(source => {
|
schema?.graph.nodes.forEach(source => {
|
||||||
source.adjacent.forEach(target => {
|
source.outputs.forEach(target => {
|
||||||
result.push({
|
result.push({
|
||||||
id: String(edgeID),
|
id: String(edgeID),
|
||||||
source: String(source.id),
|
source: String(source.id),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
@ -7,12 +7,12 @@ import BackendError from '../../components/BackendError';
|
||||||
import ConceptTab from '../../components/Common/ConceptTab';
|
import ConceptTab from '../../components/Common/ConceptTab';
|
||||||
import { Loader } from '../../components/Common/Loader';
|
import { Loader } from '../../components/Common/Loader';
|
||||||
import { useRSForm } from '../../context/RSFormContext';
|
import { useRSForm } from '../../context/RSFormContext';
|
||||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
import { prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants';
|
||||||
import { prefixes, timeout_updateUI } from '../../utils/constants';
|
import { CstType, ICstCreateData, SyntaxTree } from '../../utils/models';
|
||||||
import { CstType,type IConstituenta, ICstCreateData, 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';
|
||||||
|
import DlgDeleteCst from './DlgDeleteCst';
|
||||||
import DlgShowAST from './DlgShowAST';
|
import DlgShowAST from './DlgShowAST';
|
||||||
import DlgUploadRSForm from './DlgUploadRSForm';
|
import DlgUploadRSForm from './DlgUploadRSForm';
|
||||||
import EditorConstituenta from './EditorConstituenta';
|
import EditorConstituenta from './EditorConstituenta';
|
||||||
|
@ -31,73 +31,63 @@ export enum RSTabsList {
|
||||||
|
|
||||||
function RSTabs() {
|
function RSTabs() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { setActiveID, activeID, error, schema, loading, cstCreate } = useRSForm();
|
const search = useLocation().search;
|
||||||
|
const {
|
||||||
|
error, schema, loading,
|
||||||
|
cstCreate, cstDelete
|
||||||
|
} = useRSForm();
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useLocalStorage('rsform_edit_tab', RSTabsList.CARD);
|
const [activeTab, setActiveTab] = useState(RSTabsList.CARD);
|
||||||
|
const [activeID, setActiveID] = useState<number | undefined>(undefined)
|
||||||
const [init, setInit] = useState(false);
|
|
||||||
|
|
||||||
const [showUpload, setShowUpload] = useState(false);
|
const [showUpload, setShowUpload] = useState(false);
|
||||||
const [showClone, setShowClone] = useState(false);
|
const [showClone, setShowClone] = useState(false);
|
||||||
|
|
||||||
const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]);
|
const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]);
|
||||||
const [expression, setExpression] = useState('');
|
const [expression, setExpression] = useState('');
|
||||||
const [showAST, setShowAST] = useState(false);
|
const [showAST, setShowAST] = useState(false);
|
||||||
|
|
||||||
|
const [afterDelete, setAfterDelete] = useState<((items: number[]) => void) | undefined>(undefined);
|
||||||
|
const [toBeDeleted, setToBeDeleted] = useState<number[]>([]);
|
||||||
|
const [showDeleteCst, setShowDeleteCst] = useState(false);
|
||||||
|
|
||||||
const [defaultType, setDefaultType] = useState<CstType | undefined>(undefined);
|
const [defaultType, setDefaultType] = useState<CstType | undefined>(undefined);
|
||||||
const [insertWhere, setInsertWhere] = useState<number | undefined>(undefined);
|
const [insertWhere, setInsertWhere] = useState<number | undefined>(undefined);
|
||||||
const [showCreateCst, setShowCreateCst] = useState(false);
|
const [showCreateCst, setShowCreateCst] = useState(false);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (schema) {
|
if (schema) {
|
||||||
const url = new URL(window.location.href);
|
|
||||||
const activeQuery = url.searchParams.get('active');
|
|
||||||
const activeCst = schema.items.find((cst) => cst.id === Number(activeQuery));
|
|
||||||
setActiveID(activeCst?.id);
|
|
||||||
setInit(true);
|
|
||||||
|
|
||||||
const oldTitle = document.title
|
const oldTitle = document.title
|
||||||
document.title = schema.title
|
document.title = schema.title
|
||||||
return () => {
|
return () => {
|
||||||
document.title = oldTitle
|
document.title = oldTitle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [setActiveID, schema, schema?.title, setInit]);
|
}, [schema]);
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const url = new URL(window.location.href);
|
const activeTab = Number(new URLSearchParams(search).get('tab')) ?? RSTabsList.CARD;
|
||||||
const tabQuery = url.searchParams.get('tab');
|
const cstQuery = new URLSearchParams(search).get('active');
|
||||||
setActiveTab(Number(tabQuery) || RSTabsList.CARD);
|
setActiveTab(activeTab);
|
||||||
}, [setActiveTab]);
|
setActiveID(Number(cstQuery) ?? (schema && schema?.items.length > 0 && schema?.items[0]));
|
||||||
|
}, [search, setActiveTab, setActiveID, schema]);
|
||||||
useEffect(() => {
|
|
||||||
if (init) {
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
const currentActive = url.searchParams.get('active');
|
|
||||||
const currentTab = url.searchParams.get('tab');
|
|
||||||
const saveHistory = activeTab === RSTabsList.CST_EDIT && currentActive !== String(activeID);
|
|
||||||
if (currentTab !== String(activeTab)) {
|
|
||||||
url.searchParams.set('tab', String(activeTab));
|
|
||||||
}
|
|
||||||
if (activeID) {
|
|
||||||
if (currentActive !== String(activeID)) {
|
|
||||||
url.searchParams.set('active', String(activeID));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
url.searchParams.delete('active');
|
|
||||||
}
|
|
||||||
if (saveHistory) {
|
|
||||||
window.history.pushState(null, '', url.toString());
|
|
||||||
} else {
|
|
||||||
window.history.replaceState(null, '', url.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [activeTab, activeID, init]);
|
|
||||||
|
|
||||||
function onSelectTab(index: number) {
|
function onSelectTab(index: number) {
|
||||||
setActiveTab(index);
|
navigateTo(index, activeID);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddNew = useCallback(
|
const navigateTo = useCallback(
|
||||||
|
(tab: RSTabsList, activeID?: number) => {
|
||||||
|
if (activeID) {
|
||||||
|
navigate(`/rsforms/${schema!.id}?tab=${tab}&active=${activeID}`, {
|
||||||
|
replace: tab === activeTab && tab !== RSTabsList.CST_EDIT
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
navigate(`/rsforms/${schema!.id}?tab=${tab}`);
|
||||||
|
}
|
||||||
|
}, [navigate, schema, activeTab]);
|
||||||
|
|
||||||
|
const handleCreateCst = useCallback(
|
||||||
(type: CstType, selectedCst?: number) => {
|
(type: CstType, selectedCst?: number) => {
|
||||||
if (!schema?.items) {
|
if (!schema?.items) {
|
||||||
return;
|
return;
|
||||||
|
@ -109,32 +99,68 @@ function RSTabs() {
|
||||||
}
|
}
|
||||||
cstCreate(data, newCst => {
|
cstCreate(data, newCst => {
|
||||||
toast.success(`Конституента добавлена: ${newCst.alias}`);
|
toast.success(`Конституента добавлена: ${newCst.alias}`);
|
||||||
navigate(`/rsforms/${schema.id}?tab=${activeTab}&active=${newCst.id}`);
|
navigateTo(activeTab, newCst.id);
|
||||||
if (activeTab === RSTabsList.CST_EDIT || activeTab == RSTabsList.CST_LIST) {
|
if (activeTab === RSTabsList.CST_EDIT || activeTab == RSTabsList.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) {
|
||||||
element.scrollIntoView({
|
element.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: "end",
|
block: 'end',
|
||||||
inline: "nearest"
|
inline: 'nearest'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, timeout_updateUI);
|
}, TIMEOUT_UI_REFRESH);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [schema, cstCreate, insertWhere, navigate, activeTab]);
|
}, [schema, cstCreate, insertWhere, navigateTo, activeTab]);
|
||||||
|
|
||||||
const onShowCreateCst = useCallback(
|
const promptCreateCst = useCallback(
|
||||||
(selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => {
|
(selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => {
|
||||||
if (skipDialog && type) {
|
if (skipDialog && type) {
|
||||||
handleAddNew(type, selectedID);
|
handleCreateCst(type, selectedID);
|
||||||
} else {
|
} else {
|
||||||
setDefaultType(type);
|
setDefaultType(type);
|
||||||
setInsertWhere(selectedID);
|
setInsertWhere(selectedID);
|
||||||
setShowCreateCst(true);
|
setShowCreateCst(true);
|
||||||
}
|
}
|
||||||
}, [handleAddNew]);
|
}, [handleCreateCst]);
|
||||||
|
|
||||||
|
const handleDeleteCst = useCallback(
|
||||||
|
(deleted: number[]) => {
|
||||||
|
if (!schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
items: deleted.map(id => { return { id: id }; })
|
||||||
|
};
|
||||||
|
let activeIndex = schema.items.findIndex(cst => cst.id === activeID);
|
||||||
|
cstDelete(data, () => {
|
||||||
|
const deletedNames = deleted.map(id => schema.items.find(cst => cst.id === id)?.alias).join(', ');
|
||||||
|
toast.success(`Конституенты удалены: ${deletedNames}`);
|
||||||
|
if (deleted.length === schema.items.length) {
|
||||||
|
navigateTo(RSTabsList.CST_LIST);
|
||||||
|
}
|
||||||
|
if (activeIndex) {
|
||||||
|
while (activeIndex < schema.items.length && deleted.find(id => id === schema.items[activeIndex].id)) {
|
||||||
|
++activeIndex;
|
||||||
|
}
|
||||||
|
navigateTo(activeTab, schema.items[activeIndex].id);
|
||||||
|
}
|
||||||
|
if (afterDelete) afterDelete(deleted);
|
||||||
|
});
|
||||||
|
}, [afterDelete, cstDelete, schema, activeID, activeTab, navigateTo]);
|
||||||
|
|
||||||
|
|
||||||
|
const promptDeleteCst = useCallback(
|
||||||
|
(selected: number[], callback?: (items: number[]) => void) => {
|
||||||
|
setAfterDelete(() => (
|
||||||
|
(items: number[]) => {
|
||||||
|
if (callback) callback(items);
|
||||||
|
}));
|
||||||
|
setToBeDeleted(selected);
|
||||||
|
setShowDeleteCst(true)
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onShowAST = useCallback(
|
const onShowAST = useCallback(
|
||||||
(expression: string, ast: SyntaxTree) => {
|
(expression: string, ast: SyntaxTree) => {
|
||||||
|
@ -143,32 +169,42 @@ function RSTabs() {
|
||||||
setShowAST(true);
|
setShowAST(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onEditCst = useCallback(
|
const onOpenCst = useCallback(
|
||||||
(cst: IConstituenta) => {
|
(cstID: number) => {
|
||||||
setActiveID(cst.id);
|
navigateTo(RSTabsList.CST_EDIT, cstID)
|
||||||
setActiveTab(RSTabsList.CST_EDIT)
|
}, [navigateTo]);
|
||||||
}, [setActiveID, setActiveTab]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
{ loading && <Loader /> }
|
{ loading && <Loader /> }
|
||||||
{ error && <BackendError error={error} />}
|
{ error && <BackendError error={error} />}
|
||||||
{ schema && !loading &&
|
{ schema && !loading && <>
|
||||||
<>
|
{showUpload &&
|
||||||
{showUpload && <DlgUploadRSForm hideWindow={() => { setShowUpload(false); }}/>}
|
<DlgUploadRSForm
|
||||||
{showClone && <DlgCloneRSForm hideWindow={() => { setShowClone(false); }}/>}
|
hideWindow={() => setShowUpload(false)}
|
||||||
|
/>}
|
||||||
|
{showClone &&
|
||||||
|
<DlgCloneRSForm
|
||||||
|
hideWindow={() => setShowClone(false)}
|
||||||
|
/>}
|
||||||
{showAST &&
|
{showAST &&
|
||||||
<DlgShowAST
|
<DlgShowAST
|
||||||
expression={expression}
|
expression={expression}
|
||||||
syntaxTree={syntaxTree}
|
syntaxTree={syntaxTree}
|
||||||
hideWindow={() => { setShowAST(false); }}
|
hideWindow={() => setShowAST(false)}
|
||||||
/>}
|
/>}
|
||||||
{showCreateCst &&
|
{showCreateCst &&
|
||||||
<DlgCreateCst
|
<DlgCreateCst
|
||||||
hideWindow={() => { setShowCreateCst(false); }}
|
hideWindow={() => setShowCreateCst(false)}
|
||||||
onCreate={handleAddNew}
|
onCreate={handleCreateCst}
|
||||||
defaultType={defaultType}
|
defaultType={defaultType}
|
||||||
/>}
|
/>}
|
||||||
|
{showDeleteCst &&
|
||||||
|
<DlgDeleteCst
|
||||||
|
hideWindow={() => setShowDeleteCst(false)}
|
||||||
|
onDelete={handleDeleteCst}
|
||||||
|
selected={toBeDeleted}
|
||||||
|
/>}
|
||||||
<Tabs
|
<Tabs
|
||||||
selectedIndex={activeTab}
|
selectedIndex={activeTab}
|
||||||
onSelect={onSelectTab}
|
onSelect={onSelectTab}
|
||||||
|
@ -195,19 +231,28 @@ function RSTabs() {
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel className='w-full'>
|
<TabPanel className='w-full'>
|
||||||
<EditorItems onOpenEdit={onEditCst} onShowCreateCst={onShowCreateCst} />
|
<EditorItems
|
||||||
|
onOpenEdit={onOpenCst}
|
||||||
|
onCreateCst={promptCreateCst}
|
||||||
|
onDeleteCst={promptDeleteCst}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<EditorConstituenta onShowAST={onShowAST} onShowCreateCst={onShowCreateCst} />
|
<EditorConstituenta
|
||||||
|
activeID={activeID}
|
||||||
|
onOpenEdit={onOpenCst}
|
||||||
|
onShowAST={onShowAST}
|
||||||
|
onCreateCst={promptCreateCst}
|
||||||
|
onDeleteCst={promptDeleteCst}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<EditorTermGraph />
|
<EditorTermGraph />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</>
|
</>}
|
||||||
}
|
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,16 +7,18 @@ 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 { CstType, extractGlobals,type IConstituenta, matchConstituenta } from '../../../utils/models';
|
import { CstType, extractGlobals,type IConstituenta, matchConstituenta } from '../../../utils/models';
|
||||||
import { getMockConstituenta, mapStatusInfo } from '../../../utils/staticUI';
|
import { getCstDescription, getMockConstituenta, mapStatusInfo } from '../../../utils/staticUI';
|
||||||
import ConstituentaTooltip from './ConstituentaTooltip';
|
import ConstituentaTooltip from './ConstituentaTooltip';
|
||||||
|
|
||||||
interface ViewSideConstituentsProps {
|
interface ViewSideConstituentsProps {
|
||||||
expression: string
|
expression: string
|
||||||
|
activeID?: number
|
||||||
|
onOpenEdit: (cstID: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ViewSideConstituents({ expression }: ViewSideConstituentsProps) {
|
function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideConstituentsProps) {
|
||||||
const { darkMode } = useConceptTheme();
|
const { darkMode } = useConceptTheme();
|
||||||
const { schema, setActiveID, activeID } = useRSForm();
|
const { schema } = useRSForm();
|
||||||
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
|
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
|
||||||
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '')
|
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '')
|
||||||
const [onlyExpression, setOnlyExpression] = useLocalStorage('side-filter-flag', false);
|
const [onlyExpression, setOnlyExpression] = useLocalStorage('side-filter-flag', false);
|
||||||
|
@ -46,14 +48,16 @@ function ViewSideConstituents({ expression }: ViewSideConstituentsProps) {
|
||||||
const handleRowClicked = useCallback(
|
const handleRowClicked = useCallback(
|
||||||
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
|
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
|
||||||
if (event.altKey && cst.id > 0) {
|
if (event.altKey && cst.id > 0) {
|
||||||
setActiveID(cst.id);
|
onOpenEdit(cst.id);
|
||||||
}
|
}
|
||||||
}, [setActiveID]);
|
}, [onOpenEdit]);
|
||||||
|
|
||||||
const handleDoubleClick = useCallback(
|
const handleDoubleClick = useCallback(
|
||||||
(cst: IConstituenta) => {
|
(cst: IConstituenta) => {
|
||||||
if (cst.id > 0) setActiveID(cst.id);
|
if (cst.id > 0) {
|
||||||
}, [setActiveID]);
|
onOpenEdit(cst.id);
|
||||||
|
}
|
||||||
|
}, [onOpenEdit]);
|
||||||
|
|
||||||
const conditionalRowStyles = useMemo(() =>
|
const conditionalRowStyles = useMemo(() =>
|
||||||
[
|
[
|
||||||
|
@ -99,8 +103,7 @@ function ViewSideConstituents({ expression }: ViewSideConstituentsProps) {
|
||||||
{
|
{
|
||||||
name: 'Описание',
|
name: 'Описание',
|
||||||
id: 'description',
|
id: 'description',
|
||||||
selector: (cst: IConstituenta) =>
|
selector: (cst: IConstituenta) => getCstDescription(cst),
|
||||||
cst.term.resolved || cst.definition.text.resolved || cst.definition.formal || cst.convention,
|
|
||||||
minWidth: '350px',
|
minWidth: '350px',
|
||||||
wrap: true,
|
wrap: true,
|
||||||
conditionalCellStyles: [
|
conditionalCellStyles: [
|
||||||
|
|
|
@ -14,4 +14,33 @@ describe('Testing Graph constuction', () => {
|
||||||
graph.addEdge(13, 38);
|
graph.addEdge(13, 38);
|
||||||
expect([... graph.nodes.keys()]).toStrictEqual([13, 37, 38]);
|
expect([... graph.nodes.keys()]).toStrictEqual([13, 37, 38]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('creating from array', () => {
|
||||||
|
const graph = new Graph([[1, 2], [3], [4, 1]]);
|
||||||
|
expect([... graph.nodes.keys()]).toStrictEqual([1, 2, 3, 4]);
|
||||||
|
expect([... graph.nodes.get(1)!.outputs]).toStrictEqual([2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Testing Graph queries', () => {
|
||||||
|
test('expand outputs', () => {
|
||||||
|
const graph = new Graph([[1, 2], [2, 3], [2, 5], [5, 6], [6, 1], [7]]);
|
||||||
|
expect(graph.expandOutputs([])).toStrictEqual([]);
|
||||||
|
expect(graph.expandOutputs([3])).toStrictEqual([]);
|
||||||
|
expect(graph.expandOutputs([7])).toStrictEqual([]);
|
||||||
|
expect(graph.expandOutputs([2, 5])).toStrictEqual([3, 6, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('expand into unique array', () => {
|
||||||
|
const graph = new Graph([[1, 2], [1, 3], [2, 5], [3, 5]]);
|
||||||
|
expect(graph.expandOutputs([1])).toStrictEqual([2, 3 ,5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('expand inputs', () => {
|
||||||
|
const graph = new Graph([[1, 2], [2, 3], [2, 5], [5, 6], [6, 1], [7]]);
|
||||||
|
expect(graph.expandInputs([])).toStrictEqual([]);
|
||||||
|
expect(graph.expandInputs([7])).toStrictEqual([]);
|
||||||
|
expect(graph.expandInputs([6])).toStrictEqual([5, 2, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
|
@ -1,30 +1,49 @@
|
||||||
// Graph class with basic comparison. Does not work for objects
|
// ======== ID based fast Graph implementation =============
|
||||||
export class GraphNode {
|
export class GraphNode {
|
||||||
id: number;
|
id: number;
|
||||||
adjacent: number[];
|
outputs: number[];
|
||||||
|
inputs: number[];
|
||||||
|
|
||||||
constructor(id: number) {
|
constructor(id: number) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.adjacent = [];
|
this.outputs = [];
|
||||||
|
this.inputs = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
addAdjacent(node: number): void {
|
addOutput(node: number): void {
|
||||||
this.adjacent.push(node);
|
this.outputs.push(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAdjacent(target: number): number | null {
|
addInput(node: number): void {
|
||||||
const index = this.adjacent.findIndex(node => node === target);
|
this.inputs.push(node);
|
||||||
if (index > -1) {
|
|
||||||
return this.adjacent.splice(index, 1)[0];
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
removeInput(target: number): number | null {
|
||||||
|
const index = this.inputs.findIndex(node => node === target);
|
||||||
|
return index > -1 ? this.inputs.splice(index, 1)[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeOutput(target: number): number | null {
|
||||||
|
const index = this.outputs.findIndex(node => node === target);
|
||||||
|
return index > -1 ? this.outputs.splice(index, 1)[0] : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Graph {
|
export class Graph {
|
||||||
nodes: Map<number, GraphNode> = new Map();
|
nodes: Map<number, GraphNode> = new Map();
|
||||||
|
|
||||||
constructor() {}
|
constructor(arr?: number[][]) {
|
||||||
|
if (!arr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
arr.forEach(edge => {
|
||||||
|
if (edge.length == 1) {
|
||||||
|
this.addNode(edge[0]);
|
||||||
|
} else {
|
||||||
|
this.addEdge(edge[0], edge[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
addNode(target: number): GraphNode {
|
addNode(target: number): GraphNode {
|
||||||
let node = this.nodes.get(target);
|
let node = this.nodes.get(target);
|
||||||
|
@ -40,8 +59,9 @@ export class Graph {
|
||||||
if (!nodeToRemove) {
|
if (!nodeToRemove) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
this.nodes.forEach((node) => {
|
this.nodes.forEach(node => {
|
||||||
node.removeAdjacent(nodeToRemove.id);
|
node.removeInput(nodeToRemove.id);
|
||||||
|
node.removeOutput(nodeToRemove.id);
|
||||||
});
|
});
|
||||||
this.nodes.delete(target);
|
this.nodes.delete(target);
|
||||||
return nodeToRemove;
|
return nodeToRemove;
|
||||||
|
@ -50,38 +70,99 @@ export class Graph {
|
||||||
addEdge(source: number, destination: number): void {
|
addEdge(source: number, destination: number): void {
|
||||||
const sourceNode = this.addNode(source);
|
const sourceNode = this.addNode(source);
|
||||||
const destinationNode = this.addNode(destination);
|
const destinationNode = this.addNode(destination);
|
||||||
sourceNode.addAdjacent(destinationNode.id);
|
sourceNode.addOutput(destinationNode.id);
|
||||||
|
destinationNode.addInput(sourceNode.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeEdge(source: number, destination: number): void {
|
removeEdge(source: number, destination: number): void {
|
||||||
const sourceNode = this.nodes.get(source);
|
const sourceNode = this.nodes.get(source);
|
||||||
const destinationNode = this.nodes.get(destination);
|
const destinationNode = this.nodes.get(destination);
|
||||||
if (sourceNode && destinationNode) {
|
if (sourceNode && destinationNode) {
|
||||||
sourceNode.removeAdjacent(destination);
|
sourceNode.removeOutput(destination);
|
||||||
|
destinationNode.removeInput(source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expandOutputs(origin: number[]): number[] {
|
||||||
|
const result: number[] = [];
|
||||||
|
const marked = new Map<number, boolean>();
|
||||||
|
origin.forEach(id => marked.set(id, true));
|
||||||
|
origin.forEach(id => {
|
||||||
|
const node = this.nodes.get(id);
|
||||||
|
if (node) {
|
||||||
|
node.outputs.forEach(child => {
|
||||||
|
if (!marked.get(child) && !result.find(id => id === child)) {
|
||||||
|
result.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let position = 0;
|
||||||
|
while (position < result.length) {
|
||||||
|
const node = this.nodes.get(result[position]);
|
||||||
|
if (node && !marked.get(node.id)) {
|
||||||
|
marked.set(node.id, true);
|
||||||
|
node.outputs.forEach(child => {
|
||||||
|
if (!marked.get(child) && !result.find(id => id === child)) {
|
||||||
|
result.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
position += 1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
expandInputs(origin: number[]): number[] {
|
||||||
|
const result: number[] = [];
|
||||||
|
const marked = new Map<number, boolean>();
|
||||||
|
origin.forEach(id => marked.set(id, true));
|
||||||
|
origin.forEach(id => {
|
||||||
|
const node = this.nodes.get(id);
|
||||||
|
if (node) {
|
||||||
|
node.inputs.forEach(child => {
|
||||||
|
if (!marked.get(child) && !result.find(id => id === child)) {
|
||||||
|
result.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let position = 0;
|
||||||
|
while (position < result.length) {
|
||||||
|
const node = this.nodes.get(result[position]);
|
||||||
|
if (node && !marked.get(node.id)) {
|
||||||
|
marked.set(node.id, true);
|
||||||
|
node.inputs.forEach(child => {
|
||||||
|
if (!marked.get(child) && !result.find(id => id === child)) {
|
||||||
|
result.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
position += 1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
visitDFS(visitor: (node: GraphNode) => void) {
|
visitDFS(visitor: (node: GraphNode) => void) {
|
||||||
const visited: Map<number, boolean> = new Map();
|
const visited: Map<number, boolean> = new Map();
|
||||||
this.nodes.forEach(node => {
|
this.nodes.forEach(node => {
|
||||||
if (!visited.has(node.id)) {
|
if (!visited.has(node.id)) {
|
||||||
this.depthFirstSearchAux(node, visited, visitor);
|
this.depthFirstSearch(node, visited, visitor);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private depthFirstSearchAux(node: GraphNode, visited: Map<number, boolean>, visitor: (node: GraphNode) => void): void {
|
private depthFirstSearch(
|
||||||
if (!node) {
|
node: GraphNode,
|
||||||
return;
|
visited: Map<number, boolean>,
|
||||||
}
|
visitor: (node: GraphNode) => void)
|
||||||
|
: void {
|
||||||
visited.set(node.id, true);
|
visited.set(node.id, true);
|
||||||
|
|
||||||
visitor(node);
|
visitor(node);
|
||||||
|
node.outputs.forEach((item) => {
|
||||||
node.adjacent.forEach((item) => {
|
|
||||||
if (!visited.has(item)) {
|
if (!visited.has(item)) {
|
||||||
const childNode = this.nodes.get(item);
|
const childNode = this.nodes.get(item)!;
|
||||||
if (childNode) this.depthFirstSearchAux(childNode, visited, visitor);
|
this.depthFirstSearch(childNode, visited, visitor);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ const dev = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const config = process.env.NODE_ENV === 'production' ? prod : dev;
|
export const config = process.env.NODE_ENV === 'production' ? prod : dev;
|
||||||
export const timeout_updateUI = 100;
|
export const TIMEOUT_UI_REFRESH = 100;
|
||||||
|
|
||||||
export const urls = {
|
export const urls = {
|
||||||
concept: 'https://www.acconcept.ru/',
|
concept: 'https://www.acconcept.ru/',
|
||||||
|
|
|
@ -14,7 +14,7 @@ export interface IStatusInfo {
|
||||||
tooltip: string
|
tooltip: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTypeLabel(cst: IConstituenta) {
|
export function getTypeLabel(cst: IConstituenta): string {
|
||||||
if (cst.parse?.typification) {
|
if (cst.parse?.typification) {
|
||||||
return cst.parse.typification;
|
return cst.parse.typification;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,28 @@ export function getTypeLabel(cst: IConstituenta) {
|
||||||
return 'Логический';
|
return 'Логический';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCstDescription(cst: IConstituenta): string {
|
||||||
|
if (cst.cstType === CstType.STRUCTURED) {
|
||||||
|
return (
|
||||||
|
cst.term.resolved || cst.term.raw ||
|
||||||
|
cst.definition.text.resolved || cst.definition.text.raw ||
|
||||||
|
cst.convention ||
|
||||||
|
cst.definition.formal
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
cst.term.resolved || cst.term.raw ||
|
||||||
|
cst.definition.text.resolved || cst.definition.text.raw ||
|
||||||
|
cst.definition.formal ||
|
||||||
|
cst.convention
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCstLabel(cst: IConstituenta) {
|
||||||
|
return `${cst.alias}: ${getCstDescription(cst)}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function getCstTypePrefix(type: CstType) {
|
export function getCstTypePrefix(type: CstType) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case CstType.BASE: return 'X';
|
case CstType.BASE: return 'X';
|
||||||
|
|
Loading…
Reference in New Issue
Block a user