ConceptPortal-public/rsconcept/frontend/src/components/RefsInput/DlgEditReference.tsx

320 lines
10 KiB
TypeScript
Raw Normal View History

2023-09-29 15:33:32 +03:00
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';
2023-09-29 16:22:49 +03:00
import ConceptTooltip from '../Common/ConceptTooltip';
2023-09-29 15:33:32 +03:00
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';
2023-09-29 16:22:49 +03:00
import HelpTerminologyControl from '../Help/HelpTerminologyControl';
import { HelpIcon } from '../Icons';
2023-09-29 15:33:32 +03:00
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'>
2023-09-29 16:22:49 +03:00
<div className='flex self-center flex-start'>
<SelectSingle
className='z-modal-top min-w-[20rem] w-fit'
options={SelectorReferenceType}
isSearchable={false}
placeholder='Тип ссылки'
value={{ value: type, label: labelReferenceType(type) }}
onChange={data => setType(data?.value ?? ReferenceType.ENTITY)}
/>
<div id='terminology-help' className='px-1 py-1'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip
anchorSelect='#terminology-help'
className='max-w-[30rem]'
offset={4}
>
<HelpTerminologyControl />
</ConceptTooltip>
</div>
2023-09-29 15:33:32 +03:00
{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;