Portal/rsconcept/frontend/src/features/rsform/components/pick-substitutions.tsx
2025-04-10 11:07:04 +03:00

300 lines
10 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.

'use client';
import { useState } from 'react';
import { toast } from 'react-toastify';
import clsx from 'clsx';
import { type ILibraryItem } from '@/features/library';
import { SelectLibraryItem } from '@/features/library/components';
import { MiniButton } from '@/components/control';
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/data-table';
import { IconAccept, IconPageLeft, IconPageRight, IconRemove, IconReplace } from '@/components/icons';
import { type Styling } from '@/components/props';
import { NoData } from '@/components/view';
import { APP_COLORS } from '@/styling/colors';
import { errorMsg } from '@/utils/labels';
import { type ICstSubstitute } from '../backend/types';
import { type IConstituenta, type IRSForm } from '../models/rsform';
import { BadgeConstituenta } from './badge-constituenta';
import { SelectConstituenta } from './select-constituenta';
interface IMultiSubstitution {
original_source: ILibraryItem;
original: IConstituenta;
substitution: IConstituenta;
substitution_source: ILibraryItem;
is_suggestion: boolean;
}
interface PickSubstitutionsProps extends Styling {
value: ICstSubstitute[];
onChange: (newValue: ICstSubstitute[]) => void;
suggestions?: ICstSubstitute[];
rows?: number;
allowSelfSubstitution?: boolean;
schemas: IRSForm[];
filterCst?: (cst: IConstituenta) => boolean;
}
const columnHelper = createColumnHelper<IMultiSubstitution>();
export function PickSubstitutions({
value,
onChange,
suggestions,
rows,
schemas,
filterCst,
allowSelfSubstitution,
className,
...restProps
}: PickSubstitutionsProps) {
const [leftArgument, setLeftArgument] = useState<ILibraryItem | null>(schemas.length === 1 ? schemas[0] : null);
const [rightArgument, setRightArgument] = useState<ILibraryItem | null>(
schemas.length === 1 && allowSelfSubstitution ? schemas[0] : null
);
const leftItems = !leftArgument
? []
: (leftArgument as IRSForm).items.filter(
cst => !value.find(item => item.original === cst.id) && (!filterCst || filterCst(cst))
);
const [leftCst, setLeftCst] = useState<IConstituenta | null>(null);
const [rightCst, setRightCst] = useState<IConstituenta | null>(null);
const rightItems = !rightArgument
? []
: (rightArgument as IRSForm).items.filter(
cst => !value.find(item => item.original === cst.id) && (!filterCst || filterCst(cst))
);
const [deleteRight, setDeleteRight] = useState(true);
const toggleDelete = () => setDeleteRight(prev => !prev);
const [ignores, setIgnores] = useState<ICstSubstitute[]>([]);
const filteredSuggestions =
suggestions?.filter(
item => !ignores.find(ignore => ignore.original === item.original && ignore.substitution === item.substitution)
) ?? [];
const substitutionData: IMultiSubstitution[] = [
...value.map(item => ({
original_source: getSchemaByCst(item.original)!,
original: getConstituenta(item.original)!,
substitution: getConstituenta(item.substitution)!,
substitution_source: getSchemaByCst(item.substitution)!,
is_suggestion: false
})),
...filteredSuggestions.map(item => ({
original_source: getSchemaByCst(item.original)!,
original: getConstituenta(item.original)!,
substitution: getConstituenta(item.substitution)!,
substitution_source: getSchemaByCst(item.substitution)!,
is_suggestion: true
}))
];
function getSchemaByCst(id: number): IRSForm | undefined {
for (const schema of schemas) {
const cst = schema.cstByID.get(id);
if (cst) {
return schema;
}
}
return undefined;
}
function getConstituenta(id: number): IConstituenta | undefined {
for (const schema of schemas) {
const cst = schema.cstByID.get(id);
if (cst) {
return cst;
}
}
return undefined;
}
function addSubstitution() {
if (!leftCst || !rightCst) {
return;
}
const newSubstitution: ICstSubstitute = {
original: deleteRight ? rightCst.id : leftCst.id,
substitution: deleteRight ? leftCst.id : rightCst.id
};
const toDelete = value.map(item => item.original);
const replacements = value.map(item => item.substitution);
if (
toDelete.includes(newSubstitution.original) ||
toDelete.includes(newSubstitution.substitution) ||
replacements.includes(newSubstitution.original)
) {
toast.error(errorMsg.reuseOriginal);
return;
}
if (leftArgument === rightArgument) {
if ((deleteRight && rightCst?.is_inherited) || (!deleteRight && leftCst?.is_inherited)) {
toast.error(errorMsg.substituteInherited);
return;
}
}
onChange([...value, newSubstitution]);
setLeftCst(null);
setRightCst(null);
}
function handleDeclineSuggestion(item: IMultiSubstitution) {
setIgnores([...value, { original: item.original.id, substitution: item.substitution.id }]);
}
function handleAcceptSuggestion(item: IMultiSubstitution) {
onChange([...value, { original: item.original.id, substitution: item.substitution.id }]);
}
function handleDeleteSubstitution(target: IMultiSubstitution) {
handleDeclineSuggestion(target);
onChange(
value.filter(item => item.original !== target.original.id || item.substitution !== target.substitution.id)
);
}
const columns = [
columnHelper.accessor(item => item.substitution_source.alias, {
id: 'left_schema',
size: 100,
cell: props => <div className='min-w-43 text-ellipsis text-left'>{props.getValue()}</div>
}),
columnHelper.accessor(item => item.substitution.alias, {
id: 'left_alias',
size: 65,
cell: props => <BadgeConstituenta value={props.row.original.substitution} />
}),
columnHelper.display({
id: 'status',
size: 0,
cell: () => <IconPageRight size='1.2rem' />
}),
columnHelper.accessor(item => item.original.alias, {
id: 'right_alias',
size: 65,
cell: props => <BadgeConstituenta value={props.row.original.original} />
}),
columnHelper.accessor(item => item.original_source.alias, {
id: 'right_schema',
size: 100,
cell: props => <div className='min-w-32 text-ellipsis text-right'>{props.getValue()}</div>
}),
columnHelper.display({
id: 'actions',
size: 0,
cell: props =>
props.row.original.is_suggestion ? (
<div className='max-w-fit'>
<MiniButton
title='Принять предложение'
noHover
icon={<IconAccept size='1rem' className='icon-green' />}
onClick={() => handleAcceptSuggestion(props.row.original)}
/>
<MiniButton
title='Игнорировать предложение'
noHover
icon={<IconRemove size='1rem' className='icon-red' />}
onClick={() => handleDeclineSuggestion(props.row.original)}
/>
</div>
) : (
<div className='max-w-fit'>
<MiniButton
title='Удалить'
noHover
icon={<IconRemove size='1rem' className='icon-red' />}
onClick={() => handleDeleteSubstitution(props.row.original)}
/>
</div>
)
})
];
const conditionalRowStyles: IConditionalStyle<IMultiSubstitution>[] = [
{
when: (item: IMultiSubstitution) => item.is_suggestion,
style: { backgroundColor: APP_COLORS.bgOrange50 }
}
];
return (
<div className={clsx('flex flex-col', className)} {...restProps}>
<div className='flex items-center gap-3'>
<div className='w-60 grow flex flex-col basis-1/2 gap-1 border-x border-t clr-input rounded-t-md'>
<SelectLibraryItem
id='substitute-left-schema'
noBorder
placeholder='Выберите аргумент'
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== rightArgument?.id)}
value={leftArgument}
onChange={setLeftArgument}
/>
<SelectConstituenta noBorder items={leftItems} value={leftCst} onChange={setLeftCst} />
</div>
<div className='flex flex-col justify-center gap-1'>
<MiniButton
title={deleteRight ? 'Заменить правую' : 'Заменить левую'}
onClick={toggleDelete}
icon={
deleteRight ? (
<IconPageRight size='1.5rem' className='text-sec-600' />
) : (
<IconPageLeft size='1.5rem' className='text-sec-600' />
)
}
/>
<MiniButton
title='Добавить в таблицу отождествлений'
className='grow-0'
icon={<IconReplace size='1.5rem' className='icon-primary' />}
onClick={addSubstitution}
disabled={!leftCst || !rightCst || (leftCst === rightCst && !allowSelfSubstitution)}
/>
</div>
<div className='w-60 grow basis-1/2 flex flex-col gap-1 border-x border-t clr-input rounded-t-md'>
<SelectLibraryItem
id='substitute-right-schema'
noBorder
placeholder='Выберите аргумент'
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== leftArgument?.id)}
value={rightArgument}
onChange={setRightArgument}
/>
<SelectConstituenta noBorder items={rightItems} value={rightCst} onChange={setRightCst} />
</div>
</div>
<DataTable
dense
noHeader
noFooter
className='text-sm border rounded-t-none select-none cc-scroll-y'
rows={rows}
contentHeight='1.3rem'
data={substitutionData}
columns={columns}
headPosition='0'
noDataComponent={
<NoData className='min-h-8'>
<p>Список пуст</p>
<p>Добавьте отождествление</p>
</NoData>
}
conditionalRowStyles={conditionalRowStyles}
/>
</div>
);
}