mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
Implement search setup for EditConsituenta
This commit is contained in:
parent
47564c9d91
commit
b8fe9953a9
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -27,7 +27,6 @@ function RSTabsMenu({showUploadDialog, showCloneDialog}: RSTabsMenuProps) {
|
|||
} = useRSForm();
|
||||
const schemaMenu = useDropdown();
|
||||
const editMenu = useDropdown();
|
||||
|
||||
|
||||
const handleClaimOwner = useCallback(() => {
|
||||
editMenu.hide();
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>) => {
|
||||
|
@ -130,25 +138,19 @@ 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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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'},
|
||||
|
|
Loading…
Reference in New Issue
Block a user