Implementing inline synthesis pt3. Frontend done

This commit is contained in:
IRBorisov 2024-03-21 17:48:42 +03:00
parent 0d551422b9
commit 38bbf04de6
10 changed files with 333 additions and 25 deletions

View File

@ -94,11 +94,11 @@ function ConstituentaMultiPicker({ id, schema, prefixID, rows, selected, setSele
return ( return (
<div> <div>
<div className='flex gap-3 items-end mb-3'> <div className='flex items-end gap-3 mb-3'>
<span className='w-[24ch] select-none whitespace-nowrap'> <span className='w-[24ch] select-none whitespace-nowrap'>
Выбраны {selected.length} из {schema?.items.length ?? 0} Выбраны {selected.length} из {schema?.items.length ?? 0}
</span> </span>
<div className='flex gap-6 w-full text-sm'> <div className='flex w-full gap-6 text-sm'>
<Button <Button
text='Поставщики' text='Поставщики'
title='Добавить все конституенты, от которых зависят выбранные' title='Добавить все конституенты, от которых зависят выбранные'

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { CstMatchMode } from '@/models/miscellaneous'; import { CstMatchMode } from '@/models/miscellaneous';
@ -7,15 +8,16 @@ import { ConstituentaID, IConstituenta } from '@/models/rsform';
import { matchConstituenta } from '@/models/rsformAPI'; import { matchConstituenta } from '@/models/rsformAPI';
import { describeConstituenta, describeConstituentaTerm } from '@/utils/labels'; import { describeConstituenta, describeConstituentaTerm } from '@/utils/labels';
import { CProps } from '../props';
import SelectSingle from '../ui/SelectSingle'; import SelectSingle from '../ui/SelectSingle';
interface ConstituentaSelectorProps { interface ConstituentaSelectorProps extends CProps.Styling {
items?: IConstituenta[]; items?: IConstituenta[];
value?: IConstituenta; value?: IConstituenta;
onSelectValue: (newValue?: IConstituenta) => void; onSelectValue: (newValue?: IConstituenta) => void;
} }
function ConstituentaSelector({ items, value, onSelectValue }: ConstituentaSelectorProps) { function ConstituentaSelector({ className, items, value, onSelectValue, ...restProps }: ConstituentaSelectorProps) {
const options = useMemo(() => { const options = useMemo(() => {
return ( return (
items?.map(cst => ({ items?.map(cst => ({
@ -35,12 +37,13 @@ function ConstituentaSelector({ items, value, onSelectValue }: ConstituentaSelec
return ( return (
<SelectSingle <SelectSingle
className='w-[20rem] text-ellipsis' className={clsx('text-ellipsis', className)}
options={options} options={options}
value={{ value: value?.id, label: value ? `${value.alias}: ${describeConstituentaTerm(value)}` : '' }} value={{ value: value?.id, label: value ? `${value.alias}: ${describeConstituentaTerm(value)}` : '' }}
onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))} onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))}
// @ts-expect-error: TODO: use type definitions from react-select in filter object // @ts-expect-error: TODO: use type definitions from react-select in filter object
filterOption={filter} filterOption={filter}
{...restProps}
/> />
); );
} }

View File

@ -19,7 +19,7 @@ function ConstituentsTab({ schema, error, loading, selected, setSelected }: Cons
<DataLoader id='dlg-constituents-tab' isLoading={loading} error={error} hasNoData={!schema}> <DataLoader id='dlg-constituents-tab' isLoading={loading} error={error} hasNoData={!schema}>
<ConstituentaMultiPicker <ConstituentaMultiPicker
schema={schema} schema={schema}
rows={16} rows={14}
prefixID={prefixes.cst_inline_synth_list} prefixID={prefixes.cst_inline_synth_list}
selected={selected} selected={selected}
setSelected={setSelected} setSelected={setSelected}

View File

@ -1,14 +1,14 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import TabLabel from '@/components/ui/TabLabel'; import TabLabel from '@/components/ui/TabLabel';
import useRSFormDetails from '@/hooks/useRSFormDetails'; import useRSFormDetails from '@/hooks/useRSFormDetails';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { ICstSubstituteData, IRSForm, IRSFormInlineData } from '@/models/rsform'; import { IRSForm, IRSFormInlineData, ISubstitution } from '@/models/rsform';
import ConstituentsTab from './ConstituentsTab'; import ConstituentsTab from './ConstituentsTab';
import SchemaTab from './SchemaTab'; import SchemaTab from './SchemaTab';
@ -30,11 +30,11 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined); const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined);
const [selected, setSelected] = useState<LibraryItemID[]>([]); const [selected, setSelected] = useState<LibraryItemID[]>([]);
const [substitutions, setSubstitutions] = useState<ICstSubstituteData[]>([]); const [substitutions, setSubstitutions] = useState<ISubstitution[]>([]);
const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined }); const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined });
const validated = useMemo(() => false, []); const validated = useMemo(() => !!source.schema && selected.length > 0, [source.schema, selected]);
function handleSubmit() { function handleSubmit() {
if (!source.schema) { if (!source.schema) {
@ -44,16 +44,22 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
source: source.schema?.id, source: source.schema?.id,
receiver: receiver.id, receiver: receiver.id,
items: selected, items: selected,
substitutions: substitutions substitutions: substitutions.map(item => ({
original: item.deleteRight ? item.rightCst.id : item.leftCst.id,
substitution: item.deleteRight ? item.leftCst.id : item.rightCst.id,
transfer_term: !item.deleteRight && item.takeLeftTerm
}))
}; };
onInlineSynthesis(data); onInlineSynthesis(data);
} }
useEffect(() => setSelected(source.schema ? source.schema?.items.map(cst => cst.id) : []), [source.schema]);
return ( return (
<Modal <Modal
header='Импорт концептуальной схем' header='Импорт концептуальной схем'
submitText='Добавить конституенты' submitText='Добавить конституенты'
className='w-[40rem] h-[40rem] px-6' className='w-[40rem] h-[36rem] px-6'
hideWindow={hideWindow} hideWindow={hideWindow}
canSubmit={validated} canSubmit={validated}
onSubmit={handleSubmit} onSubmit={handleSubmit}
@ -88,6 +94,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
<SubstitutionsTab <SubstitutionsTab
receiver={receiver} receiver={receiver}
source={source.schema} source={source.schema}
selected={selected}
loading={source.loading} loading={source.loading}
substitutions={substitutions} substitutions={substitutions}
setSubstitutions={setSubstitutions} setSubstitutions={setSubstitutions}

View File

@ -9,7 +9,7 @@ import { LibraryItemID } from '@/models/library';
interface SchemaTabProps { interface SchemaTabProps {
selected?: LibraryItemID; selected?: LibraryItemID;
setSelected: React.Dispatch<LibraryItemID | undefined>; setSelected: (newValue: LibraryItemID) => void;
} }
function SchemaTab({ selected, setSelected }: SchemaTabProps) { function SchemaTab({ selected, setSelected }: SchemaTabProps) {
@ -18,7 +18,7 @@ function SchemaTab({ selected, setSelected }: SchemaTabProps) {
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>
<div className='flex gap-6 items-center'> <div className='flex items-center gap-6'>
<span className='select-none'>Выбрана</span> <span className='select-none'>Выбрана</span>
<TextInput <TextInput
id='dlg_selected_schema_title' id='dlg_selected_schema_title'
@ -30,7 +30,7 @@ function SchemaTab({ selected, setSelected }: SchemaTabProps) {
dense dense
/> />
</div> </div>
<SchemaPicker rows={16} value={selected} onSelectValue={setSelected} /> <SchemaPicker rows={15} value={selected} onSelectValue={setSelected} />
</div> </div>
); );
} }

View File

@ -1,18 +1,165 @@
'use client'; 'use client';
import { ICstSubstituteData, IRSForm } from '@/models/rsform'; import { useState } from 'react';
import { LuLocate, LuLocateOff, LuPower, LuPowerOff, LuReplace } from 'react-icons/lu';
import { ErrorData } from '@/components/info/InfoError';
import ConstituentaSelector from '@/components/select/ConstituentaSelector';
import Button from '@/components/ui/Button';
import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import DataLoader from '@/components/wrap/DataLoader';
import { ConstituentaID, IConstituenta, IRSForm, ISubstitution } from '@/models/rsform';
import { prefixes } from '@/utils/constants';
import SubstitutionsTable from './SubstitutionsTable';
interface SubstitutionsTabProps { interface SubstitutionsTabProps {
receiver?: IRSForm; receiver?: IRSForm;
source?: IRSForm; source?: IRSForm;
selected: ConstituentaID[];
loading?: boolean; loading?: boolean;
substitutions: ICstSubstituteData[]; error?: ErrorData;
setSubstitutions: React.Dispatch<ICstSubstituteData[]>;
substitutions: ISubstitution[];
setSubstitutions: React.Dispatch<React.SetStateAction<ISubstitution[]>>;
} }
// { source, receiver, loading, substitutions, setSubstitutions }: SubstitutionsTabProps function SubstitutionsTab({
function SubstitutionsTab(props: SubstitutionsTabProps) { source,
return <>3 - {props.loading}</>; receiver,
selected,
error,
loading,
substitutions,
setSubstitutions
}: SubstitutionsTabProps) {
const [leftCst, setLeftCst] = useState<IConstituenta | undefined>(undefined);
const [rightCst, setRightCst] = useState<IConstituenta | undefined>(undefined);
const [deleteRight, setDeleteRight] = useState(false);
const [takeLeftTerm, setTakeLeftTerm] = useState(false);
const toggleDelete = () => setDeleteRight(prev => !prev);
const toggleTerm = () => setTakeLeftTerm(prev => !prev);
function addSubstitution() {
if (!leftCst || !rightCst) {
return;
}
const newSubstitution: ISubstitution = {
leftCst: leftCst,
rightCst: rightCst,
deleteRight: deleteRight,
takeLeftTerm: takeLeftTerm
};
setSubstitutions([
newSubstitution,
...substitutions.filter(
item =>
(!item.deleteRight && item.leftCst.id !== leftCst.id) ||
(item.deleteRight && item.rightCst.id !== rightCst.id)
)
]);
}
return (
<DataLoader id='dlg-substitutions-tab' className='cc-column' isLoading={loading} error={error} hasNoData={!source}>
<div className='flex items-end justify-between'>
<div>
<Overlay className='flex select-none'>
<MiniButton
title='Сохранить конституенту'
noHover
onClick={toggleDelete}
icon={
deleteRight ? (
<LuPower size='1rem' className='clr-text-green' />
) : (
<LuPowerOff size='1rem' className='clr-text-red' />
)
}
/>
<MiniButton
title='Сохранить термин'
noHover
onClick={toggleTerm}
icon={
takeLeftTerm ? (
<LuLocate size='1rem' className='clr-text-green' />
) : (
<LuLocateOff size='1rem' className='clr-text-red' />
)
}
/>
</Overlay>
<Label text='Импортируемая схема' />
<ConstituentaSelector
className='w-[15rem] mt-1'
items={source?.items.filter(cst => selected.includes(cst.id))}
value={leftCst}
onSelectValue={setLeftCst}
/>
</div>
<Button
title='Добавить в таблицу отождествлений'
className='h-[2.4rem] w-[5rem]'
icon={<LuReplace size='1.25rem' className='icon-primary' />}
disabled={!leftCst || !rightCst}
onClick={addSubstitution}
/>
<div>
<Overlay className='flex select-none'>
<MiniButton
title='Сохранить конституенту'
noHover
onClick={toggleDelete}
icon={
!deleteRight ? (
<LuPower size='1rem' className='clr-text-green' />
) : (
<LuPowerOff size='1rem' className='clr-text-red' />
)
}
/>
<MiniButton
title='Сохранить термин'
noHover
onClick={toggleTerm}
icon={
!takeLeftTerm ? (
<LuLocate size='1rem' className='clr-text-green' />
) : (
<LuLocateOff size='1rem' className='clr-text-red' />
)
}
/>
</Overlay>
<Label text='Текущая схема' />
<ConstituentaSelector
className='w-[15rem] mt-1'
items={receiver?.items}
value={rightCst}
onSelectValue={setRightCst}
/>
</div>
</div>
<h2>Таблица отождествлений</h2>
<SubstitutionsTable
items={substitutions}
setItems={setSubstitutions}
rows={10}
prefixID={prefixes.cst_inline_synth_substitutes}
/>
</DataLoader>
);
} }
export default SubstitutionsTab; export default SubstitutionsTab;

View File

@ -0,0 +1,130 @@
'use client';
import { useCallback, useMemo } from 'react';
import { BiChevronLeft, BiChevronRight, BiFirstPage, BiLastPage, BiX } from 'react-icons/bi';
import ConstituentaBadge from '@/components/info/ConstituentaBadge';
import DataTable, { createColumnHelper } from '@/components/ui/DataTable';
import MiniButton from '@/components/ui/MiniButton';
import { useConceptTheme } from '@/context/ThemeContext';
import { ISubstitution } from '@/models/rsform';
import { describeConstituenta } from '@/utils/labels';
interface SubstitutionsTableProps {
prefixID: string;
rows?: number;
items: ISubstitution[];
setItems: React.Dispatch<React.SetStateAction<ISubstitution[]>>;
}
function SubstitutionIcon({ item }: { item: ISubstitution }) {
if (item.deleteRight) {
if (item.takeLeftTerm) {
return <BiChevronRight size='1.2rem' />;
} else {
return <BiLastPage size='1.2rem' />;
}
} else {
if (item.takeLeftTerm) {
return <BiFirstPage size='1.2rem' />;
} else {
return <BiChevronLeft size='1.2rem' />;
}
}
}
const columnHelper = createColumnHelper<ISubstitution>();
function SubstitutionsTable({ items, rows, setItems, prefixID }: SubstitutionsTableProps) {
const { colors } = useConceptTheme();
const handleDeleteRow = useCallback(
(row: number) => {
setItems(prev => {
const newItems: ISubstitution[] = [];
prev.forEach((item, index) => {
if (index !== row) {
newItems.push(item);
}
});
return newItems;
});
},
[setItems]
);
const columns = useMemo(
() => [
columnHelper.accessor(item => describeConstituenta(item.leftCst), {
id: 'left_text',
header: 'Описание',
size: 1000,
cell: props => <div className='text-xs text-ellipsis'>{props.getValue()}</div>
}),
columnHelper.accessor(item => item.leftCst.alias, {
id: 'left_alias',
header: 'Имя',
size: 65,
cell: props => (
<ConstituentaBadge theme={colors} value={props.row.original.leftCst} prefixID={`${prefixID}_1_`} />
)
}),
columnHelper.display({
id: 'status',
header: '',
size: 40,
cell: props => <SubstitutionIcon item={props.row.original} />
}),
columnHelper.accessor(item => item.rightCst.alias, {
id: 'right_alias',
header: 'Имя',
size: 65,
cell: props => (
<ConstituentaBadge theme={colors} value={props.row.original.rightCst} prefixID={`${prefixID}_2_`} />
)
}),
columnHelper.accessor(item => describeConstituenta(item.rightCst), {
id: 'right_text',
header: 'Описание',
size: 1000,
cell: props => <div className='text-xs text-ellipsis'>{props.getValue()}</div>
}),
columnHelper.display({
id: 'actions',
size: 50,
minSize: 50,
maxSize: 50,
cell: props => (
<MiniButton
noHover
title='Удалить'
icon={<BiX size='1rem' className='icon-red' />}
onClick={() => handleDeleteRow(props.row.index)}
/>
)
})
],
[handleDeleteRow, colors, prefixID]
);
return (
<DataTable
dense
noFooter
className='mb-2 overflow-y-auto border select-none'
rows={rows}
contentHeight='1.3rem'
data={items}
columns={columns}
headPosition='0'
noDataComponent={
<span className='p-2 text-center min-h-[2rem]'>
<p>Список пуст</p>
<p>Добавьте отождествление</p>
</span>
}
/>
);
}
export default SubstitutionsTable;

View File

@ -44,18 +44,28 @@ function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) {
hideWindow={hideWindow} hideWindow={hideWindow}
canSubmit={canSubmit} canSubmit={canSubmit}
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={clsx('w-[30rem]', 'px-6 py-3 flex flex-col gap-3 justify-center items-center')} className={clsx('w-[25rem]', 'px-6 py-3 flex flex-col gap-3 justify-center items-center')}
> >
<FlexColumn> <FlexColumn>
<Label text='Удаляемая конституента' /> <Label text='Удаляемая конституента' />
<ConstituentaSelector items={schema?.items} value={original} onSelectValue={setOriginal} /> <ConstituentaSelector
className='w-[20rem]'
items={schema?.items}
value={original}
onSelectValue={setOriginal}
/>
</FlexColumn> </FlexColumn>
<LuReplace size='3rem' className='icon-primary' /> <LuReplace size='3rem' className='icon-primary' />
<FlexColumn> <FlexColumn>
<Label text='Подставляемая конституента' /> <Label text='Подставляемая конституента' />
<ConstituentaSelector items={schema?.items} value={substitution} onSelectValue={setSubstitution} /> <ConstituentaSelector
className='w-[20rem]'
items={schema?.items}
value={substitution}
onSelectValue={setSubstitution}
/>
</FlexColumn> </FlexColumn>
<Checkbox <Checkbox
className='mt-3' className='mt-3'

View File

@ -154,6 +154,16 @@ export interface ICstSubstituteData {
transfer_term: boolean; transfer_term: boolean;
} }
/**
* Represents single substitution for synthesis table.
*/
export interface ISubstitution {
leftCst: IConstituenta;
rightCst: IConstituenta;
deleteRight: boolean;
takeLeftTerm: boolean;
}
/** /**
* Represents data response when creating {@link IConstituenta}. * Represents data response when creating {@link IConstituenta}.
*/ */

View File

@ -93,7 +93,8 @@ export const globalIDs = {
export const prefixes = { export const prefixes = {
page_size: 'page_size_', page_size: 'page_size_',
cst_list: 'cst_list_', cst_list: 'cst_list_',
cst_inline_synth_list: 'cst_inline_synth_list', cst_inline_synth_list: 'cst_inline_synth_list_',
cst_inline_synth_substitutes: 'cst_inline_synth_substitutes_',
cst_side_table: 'cst_side_table_', cst_side_table: 'cst_side_table_',
cst_hidden_list: 'cst_hidden_list_', cst_hidden_list: 'cst_hidden_list_',
cst_modal_list: 'cst_modal_list_', cst_modal_list: 'cst_modal_list_',