ConceptPortal-public/rsconcept/frontend/src/components/RefsInput/DlgEditReference.tsx
2023-09-29 15:33:32 +03:00

305 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createColumnHelper } from '@tanstack/react-table';
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { useConceptTheme } from '../../context/ThemeContext';
import { getCompatibleGrams, Grammeme, parseEntityReference,parseGrammemes,parseSyntacticReference,ReferenceType } from '../../models/language';
import { CstMatchMode } from '../../models/miscelanious';
import { IConstituenta, matchConstituenta } from '../../models/rsform';
import ConstituentaTooltip from '../../pages/RSFormPage/elements/ConstituentaTooltip';
import { colorfgCstStatus } from '../../utils/color';
import { prefixes } from '../../utils/constants';
import { labelReferenceType } from '../../utils/labels';
import { compareGrammemeOptions, IGrammemeOption, PremadeWordForms, SelectorGrammems, SelectorReferenceType } from '../../utils/selectors';
import Label from '../Common/Label';
import Modal from '../Common/Modal';
import SelectMulti from '../Common/SelectMulti';
import SelectSingle from '../Common/SelectSingle';
import TextInput from '../Common/TextInput';
import DataTable, { IConditionalStyle } from '../DataTable';
import TermformButton from './TermformButton';
interface DlgEditReferenceProps {
hideWindow: () => void
items: IConstituenta[]
initialType: ReferenceType
initialRef?: string
initialText?: string
onSave: (newRef: string) => void
}
const constituentaHelper = createColumnHelper<IConstituenta>();
function DlgEditReference({ hideWindow, items, initialRef, initialText, initialType, onSave }: DlgEditReferenceProps) {
const { colors } = useConceptTheme();
const [type, setType] = useState<ReferenceType>(ReferenceType.ENTITY);
const [nominal, setNominal] = useState('');
const [offset, setOffset] = useState(1);
const [alias, setAlias] = useState('');
const [term, setTerm] = useState('');
const [filter, setFilter] = useState('');
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);
const [selectedGrams, setSelectedGrams] = useState<IGrammemeOption[]>([]);
const [gramOptions, setGramOptions] = useState<IGrammemeOption[]>([]);
const isValid = useMemo(
() => {
if (type === ReferenceType.ENTITY) {
return alias !== '' && selectedGrams.length > 0;
} else if (type === ReferenceType.SYNTACTIC) {
return nominal !== '' && offset !== 0;
} else {
return false;
}
}, [type, alias, selectedGrams, nominal, offset]);
function produceReference(): string {
if (type === ReferenceType.ENTITY) {
return `@{${alias}|${selectedGrams.map(gram => gram.value).join(',')}}`;
} else if (type === ReferenceType.SYNTACTIC) {
return `@{${offset}|${nominal}}`;
} else {
return '';
}
}
// Initialization
useLayoutEffect(
() => {
setType(initialType);
if (initialRef) {
if (initialType === ReferenceType.ENTITY) {
const ref = parseEntityReference(initialRef);
setAlias(ref.entity);
const grams = parseGrammemes(ref.form);
setSelectedGrams(SelectorGrammems.filter(data => grams.includes(data.value)));
} else if (initialType === ReferenceType.SYNTACTIC) {
const ref = parseSyntacticReference(initialRef);
setOffset(ref.offset);
setNominal(ref.nominal);
}
} else if (initialText) {
setNominal(initialText ?? '');
setFilter(initialText);
}
}, [initialRef, initialText, initialType, items]);
// Filter constituents
useEffect(
() => {
if (filter === '') {
setFilteredData(items.filter(
(cst) => cst.term_resolved !== '')
);
} else {
setFilteredData(items.filter(
(cst) => matchConstituenta(filter, cst, CstMatchMode.TERM))
);
}
}, [filter, items]);
// Filter grammemes when input changes
useEffect(
() => {
const compatible = getCompatibleGrams(
selectedGrams
.filter(data => Object.values(Grammeme).includes(data.value as Grammeme))
.map(data => data.value as Grammeme)
);
setGramOptions(SelectorGrammems.filter(({value}) => compatible.includes(value as Grammeme)));
}, [selectedGrams]);
// Update term when alias changes
useEffect(
() => {
const cst = items.find(item => item.alias === alias)
setTerm(cst?.term_resolved ?? '')
}, [alias, term, items]);
const handleSubmit = () => onSave(produceReference());
function handleSelectConstituenta(cst: IConstituenta) {
setAlias(cst.alias);
}
const handleSelectGrams = useCallback(
(grams: Grammeme[]) => {
setSelectedGrams(SelectorGrammems.filter(({value}) => grams.includes(value as Grammeme)));
}, []);
const FormButtons = useMemo(() => {
return (
<div className='flex flex-col items-center w-full text-sm'>
<div className='flex flex-start'>
{PremadeWordForms.slice(0, 6).map(
(data, index) =>
<TermformButton id={`${prefixes.wordform_list}${index}`}
text={data.text} example={data.example} grams={data.grams}
isSelected={data.grams.every(gram => selectedGrams.find(item => item.value as Grammeme === gram))}
onSelectGrams={handleSelectGrams}
/>
)}
</div>
<div className='flex flex-start'>
{PremadeWordForms.slice(6, 12).map(
(data, index) =>
<TermformButton id={`${prefixes.wordform_list}${index}`}
text={data.text} example={data.example} grams={data.grams}
isSelected={data.grams.every(gram => selectedGrams.find(item => item.value as Grammeme === gram))}
onSelectGrams={handleSelectGrams}
/>
)}
</div>
</div>);
}, [handleSelectGrams, selectedGrams]);
const columnsConstituenta = useMemo(
() => [
constituentaHelper.accessor('alias', {
id: 'alias',
header: 'Имя',
size: 65,
minSize: 65,
cell: props => {
const cst = props.row.original;
return (<>
<div
id={`${prefixes.cst_list}${cst.alias}`}
className='min-w-[3.1rem] max-w-[3.1rem] px-1 text-center rounded-md whitespace-nowrap'
style={{
borderWidth: '1px',
borderColor: colorfgCstStatus(cst.status, colors),
color: colorfgCstStatus(cst.status, colors),
fontWeight: 600
}}
>
{cst.alias}
</div>
<ConstituentaTooltip data={cst} anchor={`#${prefixes.cst_list}${cst.alias}`} />
</>);
}
}),
constituentaHelper.accessor('term_resolved', {
id: 'term',
header: 'Термин',
size: 600,
minSize: 350,
maxSize: 600
})
], [colors]);
const conditionalRowStyles = useMemo(
(): IConditionalStyle<IConstituenta>[] => [
{
when: (cst: IConstituenta) => cst.alias === alias,
style: {
backgroundColor: colors.bgSelected
},
}
], [alias, colors]);
return (
<Modal
title='Редактирование ссылки'
hideWindow={hideWindow}
submitText='Сохранить ссылку'
canSubmit={isValid}
onSubmit={handleSubmit}
>
<div className='min-w-[40rem] flex flex-col gap-4 mb-4 mt-2'>
<SelectSingle
className='z-modal-top min-w-[20rem] w-fit self-center'
options={SelectorReferenceType}
isSearchable={false}
placeholder='Тип ссылки'
value={{ value: type, label: labelReferenceType(type) }}
onChange={data => setType(data?.value ?? ReferenceType.ENTITY)}
/>
{type === ReferenceType.SYNTACTIC &&
<div className='flex gap-4 flex-start'>
<TextInput id='offset' type='number'
label='Смещение'
dimensions='max-w-[10rem]'
singleRow
value={offset}
onChange={event => setOffset(event.target.valueAsNumber)}
/>
<TextInput id='nominal' type='text'
dimensions='w-full'
label='Начальная форма'
placeholder='зависимое слово в начальной форме'
spellCheck
singleRow
value={nominal}
onChange={event => setNominal(event.target.value)}
/>
</div>}
{type === ReferenceType.ENTITY &&
<div className='flex flex-col gap-2'>
<TextInput
dimensions='w-full'
placeholder='текст фильтра'
value={filter}
onChange={event => setFilter(event.target.value)}
/>
<div className='border min-h-[15.5rem] max-h-[15.5rem] text-sm overflow-y-auto'>
<DataTable
data={filteredData}
columns={columnsConstituenta}
conditionalRowStyles={conditionalRowStyles}
dense
noDataComponent={
<span className='flex flex-col justify-center p-2 text-center min-h-[5rem]'>
<p>Список конституент пуст</p>
<p>Измените параметры фильтра</p>
</span>
}
onRowClicked={handleSelectConstituenta}
/>
</div>
<div className='flex gap-4 flex-start'>
<TextInput
label='Отсылаемый идентификатор'
dimensions='max-w-[18rem] min-w-[18rem] whitespace-nowrap'
singleRow
value={alias}
onChange={event => setAlias(event.target.value)}
/>
<TextInput
label='Термин'
singleRow
disabled
noBorder
value={term}
tooltip={term}
dimensions='w-full'
/>
</div>
{FormButtons}
<div className='flex items-center gap-10 flex-start'>
<Label text='Отсылаемая словоформа'/>
<SelectMulti
className='flex-grow h-full z-modal-top'
options={gramOptions}
placeholder='Выберите граммемы'
value={selectedGrams}
onChange={newValue => setSelectedGrams([...newValue].sort(compareGrammemeOptions))}
/>
</div>
</div>}
</div>
</Modal>);
}
export default DlgEditReference;