Implement search setup for EditConsituenta

This commit is contained in:
IRBorisov 2023-08-02 21:35:24 +03:00
parent 47564c9d91
commit b8fe9953a9
8 changed files with 259 additions and 44 deletions

View File

@ -1,13 +1,15 @@
import { useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import ConceptTooltip from '../../components/Common/ConceptTooltip';
import Divider from '../../components/Common/Divider';
import MiniButton from '../../components/Common/MiniButton';
import SubmitButton from '../../components/Common/SubmitButton';
import TextArea from '../../components/Common/TextArea';
import { DumpBinIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
import { DumpBinIcon, HelpIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext';
import { type CstType, EditMode, ICstUpdateData, SyntaxTree } from '../../utils/models';
import { getCstTypeLabel } from '../../utils/staticUI';
import { getCstTypeLabel, mapStatusInfo } from '../../utils/staticUI';
import EditorRSExpression from './EditorRSExpression';
import ViewSideConstituents from './elements/ViewSideConstituents';
@ -150,6 +152,36 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
onClick={handleDelete}
icon={<DumpBinIcon size={5} color={isEnabled ? 'text-red' : ''} />}
/>
<div id='cst-help' className='flex items-center ml-[0.25rem]'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip anchorSelect='#cst-help'>
<div className='max-w-[35rem]'>
<h1>Подсказки</h1>
<p><b className='text-red'>Изменения сохраняются ПОСЛЕ нажатия на кнопку снизу или слева вверху</b></p>
<p><b>Клик на формальное выражение</b> - обратите внимание на кнопки снизу.<br/>Для каждой есть горячая клавиша в подсказке</p>
<p><b>Список конституент справа</b> - обратите внимание на настройки фильтрации</p>
<p>- слева от ввода текста настраивается набор атрибутов конституенты</p>
<p>- справа от ввода текста настраивается список конституент, которые фильтруются</p>
<p>- текущая конституента выделена цветом строки</p>
<p>- двойнок клин / Alt + клик - выбор редактируемой конституенты</p>
<p>- при наведении на ID конституенты отображаются ее атрибуты</p>
<p>- столбец "Описание" содержит один из непустых текстовых атрибутов</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>
</div>
<TextArea id='term' label='Термин'
@ -167,6 +199,7 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
disabled
/>
<EditorRSExpression id='expression' label='Формальное выражение'
activeCst={activeCst}
placeholder='Родоструктурное выражение, задающее формальное определение'
value={expression}
disabled={!isEnabled}

View File

@ -7,7 +7,7 @@ import { Loader } from '../../components/Common/Loader';
import { useRSForm } from '../../context/RSFormContext';
import useCheckExpression from '../../hooks/useCheckExpression';
import { TokenID } from '../../utils/enums';
import { IRSErrorDescription, SyntaxTree } from '../../utils/models';
import { IConstituenta, IRSErrorDescription, SyntaxTree } from '../../utils/models';
import { getCstExpressionPrefix } from '../../utils/staticUI';
import ParsingResult from './elements/ParsingResult';
import RSLocalButton from './elements/RSLocalButton';
@ -17,6 +17,7 @@ import { getSymbolSubstitute, TextWrapper } from './elements/textEditing';
interface EditorRSExpressionProps {
id: string
activeCst?: IConstituenta
label: string
isActive: boolean
disabled?: boolean
@ -30,10 +31,10 @@ interface EditorRSExpressionProps {
}
function EditorRSExpression({
id, label, disabled, isActive, placeholder, value, setValue, onShowAST,
id, activeCst, label, disabled, isActive, placeholder, value, setValue, onShowAST,
toggleEditMode, setTypification, onChange
}: EditorRSExpressionProps) {
const { schema, activeCst } = useRSForm();
const { schema } = useRSForm();
const [isModified, setIsModified] = useState(false);
const { parseData, checkExpression, resetParse, loading } = useCheckExpression({ schema });
const expressionCtrl = useRef<HTMLTextAreaElement>(null);

View File

@ -28,7 +28,6 @@ function RSTabsMenu({showUploadDialog, showCloneDialog}: RSTabsMenuProps) {
const schemaMenu = useDropdown();
const editMenu = useDropdown();
const handleClaimOwner = useCallback(() => {
editMenu.hide();
claimOwnershipProc(claim)

View File

@ -0,0 +1,66 @@
import { useCallback } from 'react';
import Dropdown from '../../../components/Common/Dropdown';
import DropdownButton from '../../../components/Common/DropdownButton';
import useDropdown from '../../../hooks/useDropdown';
import { DependencyMode } from '../../../utils/models';
import { getDependencyLabel } from '../../../utils/staticUI';
interface DependencyModePickerProps {
value: DependencyMode
onChange: (value: DependencyMode) => void
}
function DependencyModePicker({ value, onChange }: DependencyModePickerProps) {
const pickerMenu = useDropdown();
const handleChange = useCallback(
(newValue: DependencyMode) => {
pickerMenu.hide();
onChange(newValue);
}, [pickerMenu, onChange]);
return (
<div ref={pickerMenu.ref}>
<span
className='text-sm font-semibold underline cursor-pointer select-none whitespace-nowrap'
tabIndex={-1}
onClick={pickerMenu.toggle}
>
{getDependencyLabel(value)}
</span>
{ pickerMenu.isActive &&
<Dropdown stretchLeft >
<DropdownButton onClick={() => handleChange(DependencyMode.ALL)}>
<p><b>вся схема:</b> список всех конституент схемы</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(DependencyMode.EXPRESSION)}>
<p><b>выражение:</b> список идентификаторов из выражения</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(DependencyMode.OUTPUTS)}>
<p><b>потребители:</b> конституенты, ссылающиеся на данную</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(DependencyMode.INPUTS)}>
<p><b>поставщики:</b> конституенты, на которые ссылается данная</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(DependencyMode.EXPAND_OUTPUTS)}>
<p><b>зависимые:</b> конституенты, зависящие по цепочке</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(DependencyMode.EXPAND_INPUTS)}>
<p><b>влияющие:</b> конституенты, влияющие на данную (цепочка)</p>
</DropdownButton>
</Dropdown>
}
</div>
// case DependencyMode.OUTPUTS: return 'потребители';
// case DependencyMode.INPUTS: return 'поставщики';
// case DependencyMode.EXPAND_INPUTS: return 'влияющие';
// case DependencyMode.EXPAND_OUTPUTS: return 'зависимые';
// }
// }
);
}
export default DependencyModePicker;

View File

@ -0,0 +1,55 @@
import { useCallback } from 'react';
import Dropdown from '../../../components/Common/Dropdown';
import DropdownButton from '../../../components/Common/DropdownButton';
import useDropdown from '../../../hooks/useDropdown';
import { CstMatchMode } from '../../../utils/models';
import { getCstCompareLabel } from '../../../utils/staticUI';
interface MatchModePickerProps {
value: CstMatchMode
onChange: (value: CstMatchMode) => void
}
function MatchModePicker({ value, onChange }: MatchModePickerProps) {
const pickerMenu = useDropdown();
const handleChange = useCallback(
(newValue: CstMatchMode) => {
pickerMenu.hide();
onChange(newValue);
}, [pickerMenu, onChange]);
return (
<div ref={pickerMenu.ref}>
<span
className='text-sm font-semibold underline cursor-pointer select-none whitespace-nowrap'
tabIndex={-1}
onClick={pickerMenu.toggle}
>
{getCstCompareLabel(value)}
</span>
{ pickerMenu.isActive &&
<Dropdown>
<DropdownButton onClick={() => handleChange(CstMatchMode.ALL)}>
<p><b>везде:</b> искать во всех атрибутах</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(CstMatchMode.EXPR)}>
<p><b>ФВ:</b> искать в формальных выражениях</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(CstMatchMode.TERM)}>
<p><b>термин:</b> искать в терминах</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(CstMatchMode.TEXT)}>
<p><b>текст:</b> искать в определениях и конвенциях</p>
</DropdownButton>
<DropdownButton onClick={() => handleChange(CstMatchMode.NAME)}>
<p><b>ID:</b> искать в идентификаторах конституент</p>
</DropdownButton>
</Dropdown>
}
</div>
);
}
export default MatchModePicker;

View File

@ -1,14 +1,15 @@
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 { applyGraphFilter, CstMatchMode, CstType, DependencyMode, extractGlobals, IConstituenta, matchConstituenta } from '../../../utils/models';
import { getCstDescription, getMockConstituenta, mapStatusInfo } from '../../../utils/staticUI';
import ConstituentaTooltip from './ConstituentaTooltip';
import DependencyModePicker from './DependencyModePicker';
import MatchModePicker from './MatchModePicker';
interface ViewSideConstituentsProps {
expression: string
@ -19,31 +20,38 @@ interface ViewSideConstituentsProps {
function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideConstituentsProps) {
const { darkMode } = useConceptTheme();
const { schema } = useRSForm();
const [filterMatch, setFilterMatch] = useLocalStorage('side-filter-match', CstMatchMode.ALL);
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '');
const [filterSource, setFilterSource] = useLocalStorage('side-filter-dependency', DependencyMode.ALL);
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '')
const [onlyExpression, setOnlyExpression] = useLocalStorage('side-filter-flag', false);
useEffect(() => {
if (!schema?.items) {
setFilteredData([]);
return;
}
if (onlyExpression) {
let filtered: IConstituenta[] = [];
if (filterSource === DependencyMode.EXPRESSION) {
const aliases = extractGlobals(expression);
const filtered = schema?.items.filter((cst) => aliases.has(cst.alias));
filtered = schema.items.filter((cst) => aliases.has(cst.alias));
const names = filtered.map(cst => cst.alias)
const diff = Array.from(aliases).filter(name => !names.includes(name));
if (diff.length > 0) {
diff.forEach(
(alias, index) => filtered.push(getMockConstituenta(-index, alias, CstType.BASE, 'Конституента отсутствует')));
}
setFilteredData(filtered);
} else if (!filterText) {
setFilteredData(schema?.items);
} else if (!activeID) {
filtered = schema.items
} else {
setFilteredData(schema?.items.filter((cst) => matchConstituenta(filterText, cst)));
filtered = applyGraphFilter(schema, activeID, filterSource);
}
}, [filterText, setFilteredData, onlyExpression, expression, schema]);
if (filterText) {
filtered = filtered.filter((cst) => matchConstituenta(filterText, cst, filterMatch));
}
setFilteredData(filtered);
}, [filterText, setFilteredData, filterSource, expression, schema, filterMatch, activeID]);
const handleRowClicked = useCallback(
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
@ -132,23 +140,17 @@ function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideCons
);
return (
<div className='max-h-[80vh] overflow-y-scroll border flex-grow w-full'>
<div className='max-h-[80vh] min-h-[40rem] overflow-y-scroll border flex-grow w-full'>
<div className='sticky top-0 left-0 right-0 z-10 flex items-center justify-between w-full gap-1 px-2 py-1 bg-white border-b-2 border-gray-400 rounded dark:bg-gray-700 dark:border-gray-300'>
<div className='w-full'>
<div className='flex items-center justify-between w-full'>
<MatchModePicker value={filterMatch} onChange={setFilterMatch}/>
<input type='text'
className='w-full px-2 outline-none dark:bg-gray-700 hover:text-clip'
placeholder='текст для фильтрации списка'
placeholder='наберите текст фильтра'
value={filterText}
onChange={event => { setFilterText(event.target.value); }}
disabled={onlyExpression}
/>
</div>
<div className='w-fit min-w-[8rem]'>
<Checkbox
label='из выражения'
value={onlyExpression}
onChange={event => { setOnlyExpression(event.target.checked); }}
/>
<DependencyModePicker value={filterSource} onChange={setFilterSource}/>
</div>
</div>
<ConceptDataTable

View File

@ -226,6 +226,25 @@ export enum ExpressionStatus {
VERIFIED
}
// Dependency mode for schema analysis
export enum DependencyMode {
ALL = 0,
EXPRESSION,
OUTPUTS,
INPUTS,
EXPAND_OUTPUTS,
EXPAND_INPUTS
}
// Constituent compare mode
export enum CstMatchMode {
ALL = 1,
EXPR,
TERM,
TEXT,
NAME
}
// ========== Model functions =================
export function inferStatus(parse?: ParsingStatus, value?: ValueClass): ExpressionStatus {
if (!parse || !value) {
@ -317,22 +336,23 @@ export function LoadRSFormData(schema: IRSFormData): IRSForm {
return result;
}
export function matchConstituenta(query: string, target?: IConstituenta) {
if (!target) {
return false;
} else if (target.alias.match(query)) {
export function matchConstituenta(query: string, target: IConstituenta, mode: CstMatchMode) {
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.NAME) &&
target.alias.match(query)) {
return true;
} else if (target.term.resolved.match(query)) {
return true;
} else if (target.definition.formal.match(query)) {
return true;
} else if (target.definition.text.resolved.match(query)) {
return true;
} else if (target.convention.match(query)) {
return true;
} else {
return false;
}
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.TERM) &&
target.term.resolved.match(query)) {
return true;
}
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.EXPR) &&
target.definition.formal.match(query)) {
return true;
}
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.TEXT)) {
return (target.definition.text.resolved.match(query) || target.convention.match(query));
}
return false;
}
export function matchRSFormMeta(query: string, target: IRSFormMeta) {
@ -345,3 +365,21 @@ export function matchRSFormMeta(query: string, target: IRSFormMeta) {
return false;
}
}
export function applyGraphFilter(schema: IRSForm, start: number, mode: DependencyMode): IConstituenta[] {
if (mode === DependencyMode.ALL) {
return schema.items;
}
let ids: number[] | undefined = undefined
switch (mode) {
case DependencyMode.OUTPUTS: { ids = schema.graph.nodes.get(start)?.outputs; break; }
case DependencyMode.INPUTS: { ids = schema.graph.nodes.get(start)?.inputs; break; }
case DependencyMode.EXPAND_OUTPUTS: { ids = schema.graph.expandOutputs([start]) ; break; }
case DependencyMode.EXPAND_INPUTS: { ids = schema.graph.expandInputs([start]) ; break; }
}
if (!ids) {
return schema.items;
} else {
return schema.items.filter(cst => ids!.find(id => id === cst.id));
}
}

View File

@ -1,7 +1,7 @@
import { LayoutTypes } from 'reagraph';
import { resolveErrorClass,RSErrorClass, RSErrorType, TokenID } from './enums';
import { CstType, ExpressionStatus, type IConstituenta, IRSErrorDescription,type IRSForm, ISyntaxTreeNode,ParsingStatus, ValueClass } from './models';
import { CstMatchMode,CstType, DependencyMode,ExpressionStatus, type IConstituenta, IRSErrorDescription,type IRSForm, ISyntaxTreeNode,ParsingStatus, ValueClass } from './models';
export interface IRSButtonData {
text: string
@ -273,6 +273,27 @@ export const mapLayoutLabels: Map<string, string> = new Map([
['nooverlap', 'Без перекрытия']
]);
export function getCstCompareLabel(mode: CstMatchMode): string {
switch(mode) {
case CstMatchMode.ALL: return 'везде';
case CstMatchMode.EXPR: return 'ФВ';
case CstMatchMode.TERM: return 'термин';
case CstMatchMode.TEXT: return 'текст';
case CstMatchMode.NAME: return 'ID';
}
}
export function getDependencyLabel(mode: DependencyMode): string {
switch(mode) {
case DependencyMode.ALL: return 'вся схема';
case DependencyMode.EXPRESSION: return 'выражение';
case DependencyMode.OUTPUTS: return 'потребители';
case DependencyMode.INPUTS: return 'поставщики';
case DependencyMode.EXPAND_INPUTS: return 'влияющие';
case DependencyMode.EXPAND_OUTPUTS: return 'зависимые';
}
}
export const GraphLayoutSelector: {value: LayoutTypes, label: string}[] = [
{ value: 'forceatlas2', label: 'Атлас 2D'},
{ value: 'forceDirected2d', label: 'Силы 2D'},