mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Implementing inline synthesis pt3. Frontend done
This commit is contained in:
parent
0d551422b9
commit
38bbf04de6
|
@ -94,11 +94,11 @@ function ConstituentaMultiPicker({ id, schema, prefixID, rows, selected, setSele
|
|||
|
||||
return (
|
||||
<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'>
|
||||
Выбраны {selected.length} из {schema?.items.length ?? 0}
|
||||
</span>
|
||||
<div className='flex gap-6 w-full text-sm'>
|
||||
<div className='flex w-full gap-6 text-sm'>
|
||||
<Button
|
||||
text='Поставщики'
|
||||
title='Добавить все конституенты, от которых зависят выбранные'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { CstMatchMode } from '@/models/miscellaneous';
|
||||
|
@ -7,15 +8,16 @@ import { ConstituentaID, IConstituenta } from '@/models/rsform';
|
|||
import { matchConstituenta } from '@/models/rsformAPI';
|
||||
import { describeConstituenta, describeConstituentaTerm } from '@/utils/labels';
|
||||
|
||||
import { CProps } from '../props';
|
||||
import SelectSingle from '../ui/SelectSingle';
|
||||
|
||||
interface ConstituentaSelectorProps {
|
||||
interface ConstituentaSelectorProps extends CProps.Styling {
|
||||
items?: IConstituenta[];
|
||||
value?: IConstituenta;
|
||||
onSelectValue: (newValue?: IConstituenta) => void;
|
||||
}
|
||||
|
||||
function ConstituentaSelector({ items, value, onSelectValue }: ConstituentaSelectorProps) {
|
||||
function ConstituentaSelector({ className, items, value, onSelectValue, ...restProps }: ConstituentaSelectorProps) {
|
||||
const options = useMemo(() => {
|
||||
return (
|
||||
items?.map(cst => ({
|
||||
|
@ -35,12 +37,13 @@ function ConstituentaSelector({ items, value, onSelectValue }: ConstituentaSelec
|
|||
|
||||
return (
|
||||
<SelectSingle
|
||||
className='w-[20rem] text-ellipsis'
|
||||
className={clsx('text-ellipsis', className)}
|
||||
options={options}
|
||||
value={{ value: value?.id, label: value ? `${value.alias}: ${describeConstituentaTerm(value)}` : '' }}
|
||||
onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))}
|
||||
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
||||
filterOption={filter}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ function ConstituentsTab({ schema, error, loading, selected, setSelected }: Cons
|
|||
<DataLoader id='dlg-constituents-tab' isLoading={loading} error={error} hasNoData={!schema}>
|
||||
<ConstituentaMultiPicker
|
||||
schema={schema}
|
||||
rows={16}
|
||||
rows={14}
|
||||
prefixID={prefixes.cst_inline_synth_list}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import TabLabel from '@/components/ui/TabLabel';
|
||||
import useRSFormDetails from '@/hooks/useRSFormDetails';
|
||||
import { LibraryItemID } from '@/models/library';
|
||||
import { ICstSubstituteData, IRSForm, IRSFormInlineData } from '@/models/rsform';
|
||||
import { IRSForm, IRSFormInlineData, ISubstitution } from '@/models/rsform';
|
||||
|
||||
import ConstituentsTab from './ConstituentsTab';
|
||||
import SchemaTab from './SchemaTab';
|
||||
|
@ -30,11 +30,11 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
|
|||
|
||||
const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined);
|
||||
const [selected, setSelected] = useState<LibraryItemID[]>([]);
|
||||
const [substitutions, setSubstitutions] = useState<ICstSubstituteData[]>([]);
|
||||
const [substitutions, setSubstitutions] = useState<ISubstitution[]>([]);
|
||||
|
||||
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() {
|
||||
if (!source.schema) {
|
||||
|
@ -44,16 +44,22 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
|
|||
source: source.schema?.id,
|
||||
receiver: receiver.id,
|
||||
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);
|
||||
}
|
||||
|
||||
useEffect(() => setSelected(source.schema ? source.schema?.items.map(cst => cst.id) : []), [source.schema]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
header='Импорт концептуальной схем'
|
||||
submitText='Добавить конституенты'
|
||||
className='w-[40rem] h-[40rem] px-6'
|
||||
className='w-[40rem] h-[36rem] px-6'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={validated}
|
||||
onSubmit={handleSubmit}
|
||||
|
@ -88,6 +94,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
|
|||
<SubstitutionsTab
|
||||
receiver={receiver}
|
||||
source={source.schema}
|
||||
selected={selected}
|
||||
loading={source.loading}
|
||||
substitutions={substitutions}
|
||||
setSubstitutions={setSubstitutions}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { LibraryItemID } from '@/models/library';
|
|||
|
||||
interface SchemaTabProps {
|
||||
selected?: LibraryItemID;
|
||||
setSelected: React.Dispatch<LibraryItemID | undefined>;
|
||||
setSelected: (newValue: LibraryItemID) => void;
|
||||
}
|
||||
|
||||
function SchemaTab({ selected, setSelected }: SchemaTabProps) {
|
||||
|
@ -18,7 +18,7 @@ function SchemaTab({ selected, setSelected }: SchemaTabProps) {
|
|||
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex gap-6 items-center'>
|
||||
<div className='flex items-center gap-6'>
|
||||
<span className='select-none'>Выбрана</span>
|
||||
<TextInput
|
||||
id='dlg_selected_schema_title'
|
||||
|
@ -30,7 +30,7 @@ function SchemaTab({ selected, setSelected }: SchemaTabProps) {
|
|||
dense
|
||||
/>
|
||||
</div>
|
||||
<SchemaPicker rows={16} value={selected} onSelectValue={setSelected} />
|
||||
<SchemaPicker rows={15} value={selected} onSelectValue={setSelected} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,18 +1,165 @@
|
|||
'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 {
|
||||
receiver?: IRSForm;
|
||||
source?: IRSForm;
|
||||
selected: ConstituentaID[];
|
||||
|
||||
loading?: boolean;
|
||||
substitutions: ICstSubstituteData[];
|
||||
setSubstitutions: React.Dispatch<ICstSubstituteData[]>;
|
||||
error?: ErrorData;
|
||||
|
||||
substitutions: ISubstitution[];
|
||||
setSubstitutions: React.Dispatch<React.SetStateAction<ISubstitution[]>>;
|
||||
}
|
||||
|
||||
// { source, receiver, loading, substitutions, setSubstitutions }: SubstitutionsTabProps
|
||||
function SubstitutionsTab(props: SubstitutionsTabProps) {
|
||||
return <>3 - {props.loading}</>;
|
||||
function SubstitutionsTab({
|
||||
source,
|
||||
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;
|
||||
|
|
|
@ -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;
|
|
@ -44,18 +44,28 @@ function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) {
|
|||
hideWindow={hideWindow}
|
||||
canSubmit={canSubmit}
|
||||
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>
|
||||
<Label text='Удаляемая конституента' />
|
||||
<ConstituentaSelector items={schema?.items} value={original} onSelectValue={setOriginal} />
|
||||
<ConstituentaSelector
|
||||
className='w-[20rem]'
|
||||
items={schema?.items}
|
||||
value={original}
|
||||
onSelectValue={setOriginal}
|
||||
/>
|
||||
</FlexColumn>
|
||||
|
||||
<LuReplace size='3rem' className='icon-primary' />
|
||||
|
||||
<FlexColumn>
|
||||
<Label text='Подставляемая конституента' />
|
||||
<ConstituentaSelector items={schema?.items} value={substitution} onSelectValue={setSubstitution} />
|
||||
<ConstituentaSelector
|
||||
className='w-[20rem]'
|
||||
items={schema?.items}
|
||||
value={substitution}
|
||||
onSelectValue={setSubstitution}
|
||||
/>
|
||||
</FlexColumn>
|
||||
<Checkbox
|
||||
className='mt-3'
|
||||
|
|
|
@ -154,6 +154,16 @@ export interface ICstSubstituteData {
|
|||
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}.
|
||||
*/
|
||||
|
|
|
@ -93,7 +93,8 @@ export const globalIDs = {
|
|||
export const prefixes = {
|
||||
page_size: 'page_size_',
|
||||
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_hidden_list: 'cst_hidden_list_',
|
||||
cst_modal_list: 'cst_modal_list_',
|
||||
|
|
Loading…
Reference in New Issue
Block a user