Portal/rsconcept/frontend/src/features/rsform/components/PickSubstitutions.tsx

301 lines
10 KiB
TypeScript
Raw Normal View History

2024-06-07 20:17:03 +03:00
'use client';
import { useState } from 'react';
import { toast } from 'react-toastify';
2025-02-12 21:36:03 +03:00
import clsx from 'clsx';
2025-02-20 20:22:05 +03:00
import { type ILibraryItem, SelectLibraryItem } from '@/features/library';
2024-06-07 20:17:03 +03:00
import { MiniButton } from '@/components/Control';
2025-02-20 20:22:05 +03:00
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/DataTable';
2025-01-28 23:23:03 +03:00
import { IconAccept, IconPageLeft, IconPageRight, IconRemove, IconReplace } from '@/components/Icons';
2025-02-20 20:22:05 +03:00
import { type Styling } from '@/components/props';
import { NoData } from '@/components/View';
2025-02-11 21:07:06 +03:00
import { APP_COLORS } from '@/styling/colors';
import { errorMsg } from '@/utils/labels';
2024-06-07 20:17:03 +03:00
2025-02-20 20:22:05 +03:00
import { type ICstSubstitute } from '../backend/types';
import { type IConstituenta, type IRSForm } from '../models/rsform';
2025-02-12 21:36:03 +03:00
import { BadgeConstituenta } from './BadgeConstituenta';
import { SelectConstituenta } from './SelectConstituenta';
2024-06-07 20:17:03 +03:00
interface IMultiSubstitution {
original_source: ILibraryItem;
original: IConstituenta;
substitution: IConstituenta;
substitution_source: ILibraryItem;
is_suggestion: boolean;
}
2025-02-20 20:22:05 +03:00
interface PickSubstitutionsProps extends Styling {
value: ICstSubstitute[];
2025-02-12 00:14:18 +03:00
onChange: (newValue: ICstSubstitute[]) => void;
suggestions?: ICstSubstitute[];
2024-06-07 20:17:03 +03:00
rows?: number;
allowSelfSubstitution?: boolean;
2024-06-07 20:17:03 +03:00
schemas: IRSForm[];
filterCst?: (cst: IConstituenta) => boolean;
2024-06-07 20:17:03 +03:00
}
2024-07-29 16:55:48 +03:00
const columnHelper = createColumnHelper<IMultiSubstitution>();
2024-06-07 20:17:03 +03:00
export function PickSubstitutions({
value,
onChange,
suggestions,
2024-06-07 20:17:03 +03:00
rows,
schemas,
filterCst,
allowSelfSubstitution,
className,
...restProps
2024-06-07 20:17:03 +03:00
}: 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
2024-07-29 16:55:48 +03:00
);
const [leftCst, setLeftCst] = useState<IConstituenta | null>(null);
const [rightCst, setRightCst] = useState<IConstituenta | null>(null);
2024-07-29 16:55:48 +03:00
2024-06-07 20:17:03 +03:00
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;
}
2024-06-07 20:17:03 +03:00
function getConstituenta(id: number): IConstituenta | undefined {
for (const schema of schemas) {
const cst = schema.cstByID.get(id);
if (cst) {
return cst;
2024-07-29 16:55:48 +03:00
}
}
return undefined;
}
2024-07-29 16:55:48 +03:00
2024-06-07 20:17:03 +03:00
function addSubstitution() {
if (!leftCst || !rightCst) {
return;
}
2024-07-29 16:55:48 +03:00
const newSubstitution: ICstSubstitute = {
original: deleteRight ? rightCst.id : leftCst.id,
substitution: deleteRight ? leftCst.id : rightCst.id
2024-06-07 20:17:03 +03:00
};
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;
}
2024-08-01 11:55:45 +03:00
if (leftArgument === rightArgument) {
if ((deleteRight && rightCst?.is_inherited) || (!deleteRight && leftCst?.is_inherited)) {
toast.error(errorMsg.substituteInherited);
2024-08-01 11:55:45 +03:00
return;
}
}
2025-02-12 00:14:18 +03:00
onChange([...value, newSubstitution]);
setLeftCst(null);
setRightCst(null);
2024-06-07 20:17:03 +03:00
}
function handleDeclineSuggestion(item: IMultiSubstitution) {
2025-02-12 00:14:18 +03:00
setIgnores([...value, { original: item.original.id, substitution: item.substitution.id }]);
}
function handleAcceptSuggestion(item: IMultiSubstitution) {
2025-02-12 00:14:18 +03:00
onChange([...value, { original: item.original.id, substitution: item.substitution.id }]);
}
function handleDeleteSubstitution(target: IMultiSubstitution) {
handleDeclineSuggestion(target);
2025-02-12 00:14:18 +03:00
onChange(
value.filter(item => item.original !== target.original.id || item.substitution !== target.substitution.id)
);
}
2024-06-07 20:17:03 +03:00
const columns = [
columnHelper.accessor(item => item.substitution_source.alias, {
id: 'left_schema',
size: 100,
cell: props => <div className='min-w-[10.5rem] 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-[8rem] 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
noHover
title='Принять предложение'
icon={<IconAccept size='1rem' className='icon-green' />}
onClick={() => handleAcceptSuggestion(props.row.original)}
/>
<MiniButton
noHover
title='Игнорировать предложение'
icon={<IconRemove size='1rem' className='icon-red' />}
onClick={() => handleDeclineSuggestion(props.row.original)}
/>
</div>
) : (
<div className='max-w-fit'>
<MiniButton
noHover
title='Удалить'
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 }
}
];
2024-06-07 20:17:03 +03:00
return (
<div className={clsx('flex flex-col', className)} {...restProps}>
2024-06-07 20:17:03 +03:00
<div className='flex items-end gap-3 justify-stretch'>
2025-02-20 20:22:05 +03:00
<div className='grow flex flex-col basis-1/2 gap-[0.125rem] border-x border-t clr-input rounded-t-md'>
2024-09-21 20:03:49 +03:00
<SelectLibraryItem
noBorder
placeholder='Выберите аргумент'
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== rightArgument?.id)}
value={leftArgument}
onChange={setLeftArgument}
2024-09-21 20:03:49 +03:00
/>
<SelectConstituenta
noBorder
items={(leftArgument as IRSForm)?.items.filter(
cst => !value.find(item => item.original === cst.id) && (!filterCst || filterCst(cst))
2024-09-21 20:03:49 +03:00
)}
value={leftCst}
onChange={setLeftCst}
2024-09-21 20:03:49 +03:00
/>
2024-06-07 20:17:03 +03:00
</div>
<div className='flex flex-col gap-1'>
<MiniButton
title={deleteRight ? 'Заменить правую' : 'Заменить левую'}
onClick={toggleDelete}
icon={
deleteRight ? (
2024-12-17 10:52:36 +03:00
<IconPageRight size='1.5rem' className='text-sec-600' />
) : (
2024-12-17 10:52:36 +03:00
<IconPageLeft size='1.5rem' className='text-sec-600' />
)
}
/>
<MiniButton
title='Добавить в таблицу отождествлений'
className='mb-[0.375rem] grow-0'
icon={<IconReplace size='1.5rem' className='icon-primary' />}
disabled={!leftCst || !rightCst || (leftCst === rightCst && !allowSelfSubstitution)}
onClick={addSubstitution}
/>
</div>
2024-06-07 20:17:03 +03:00
2025-02-20 20:22:05 +03:00
<div className='grow basis-1/2 flex flex-col gap-[0.125rem] border-x border-t clr-input rounded-t-md'>
2024-09-21 20:03:49 +03:00
<SelectLibraryItem
noBorder
placeholder='Выберите аргумент'
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== leftArgument?.id)}
value={rightArgument}
onChange={setRightArgument}
2024-09-21 20:03:49 +03:00
/>
<SelectConstituenta
noBorder
items={(rightArgument as IRSForm)?.items.filter(
cst => !value.find(item => item.original === cst.id) && (!filterCst || filterCst(cst))
2024-09-21 20:03:49 +03:00
)}
value={rightCst}
onChange={setRightCst}
2024-09-21 20:03:49 +03:00
/>
2024-06-07 20:17:03 +03:00
</div>
</div>
<DataTable
dense
noHeader
noFooter
className='text-sm border rounded-t-none select-none cc-scroll-y'
2024-06-07 20:17:03 +03:00
rows={rows}
contentHeight='1.3rem'
2024-07-29 16:55:48 +03:00
data={substitutionData}
2024-06-07 20:17:03 +03:00
columns={columns}
headPosition='0'
noDataComponent={
2024-06-21 19:16:41 +03:00
<NoData className='min-h-[2rem]'>
2024-06-07 20:17:03 +03:00
<p>Список пуст</p>
<p>Добавьте отождествление</p>
2024-06-21 19:16:41 +03:00
</NoData>
2024-06-07 20:17:03 +03:00
}
conditionalRowStyles={conditionalRowStyles}
2024-06-07 20:17:03 +03:00
/>
</div>
);
}