Implementing inline synthesis pt2

This commit is contained in:
IRBorisov 2024-03-20 15:03:53 +03:00
parent bcbe35b436
commit 71e87ac9c5
11 changed files with 260 additions and 72 deletions

View File

@ -0,0 +1,145 @@
'use client';
import clsx from 'clsx';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import DataTable, { createColumnHelper, RowSelectionState } from '@/components/DataTable';
import { useConceptTheme } from '@/context/ThemeContext';
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
import { describeConstituenta } from '@/utils/labels';
import ConstituentaBadge from './ConstituentaBadge';
import Button from './ui/Button';
import FlexColumn from './ui/FlexColumn';
interface ConstituentaMultiPickerProps {
id?: string;
schema?: IRSForm;
prefixID: string;
rows?: number;
selected: ConstituentaID[];
setSelected: React.Dispatch<ConstituentaID[]>;
}
const columnHelper = createColumnHelper<IConstituenta>();
function ConstituentaMultiPicker({ id, schema, prefixID, rows, selected, setSelected }: ConstituentaMultiPickerProps) {
const { colors } = useConceptTheme();
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
useLayoutEffect(() => {
if (!schema || selected.length === 0) {
setRowSelection({});
} else {
const newRowSelection: RowSelectionState = {};
schema.items.forEach((cst, index) => {
newRowSelection[String(index)] = selected.includes(cst.id);
});
setRowSelection(newRowSelection);
}
}, [selected, schema]);
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
if (!schema) {
setSelected([]);
} else {
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
const newSelection: ConstituentaID[] = [];
schema.items.forEach((cst, index) => {
if (newRowSelection[String(index)] === true) {
newSelection.push(cst.id);
}
});
setSelected(newSelection);
}
}
const selectBasis = useCallback(() => {
if (!schema || selected.length === 0) {
return;
}
const addition = schema.graph.expandInputs(selected).filter(id => !selected.includes(id));
if (addition.length > 0) {
setSelected([...selected, ...addition]);
}
}, [schema, selected, setSelected]);
const selectDependant = useCallback(() => {
if (!schema || selected.length === 0) {
return;
}
const addition = schema.graph.expandOutputs(selected).filter(id => !selected.includes(id));
if (addition.length > 0) {
setSelected([...selected, ...addition]);
}
}, [schema, selected, setSelected]);
const columns = useMemo(
() => [
columnHelper.accessor('alias', {
id: 'alias',
header: 'Имя',
size: 65,
cell: props => <ConstituentaBadge theme={colors} value={props.row.original} prefixID={prefixID} />
}),
columnHelper.accessor(cst => describeConstituenta(cst), {
id: 'description',
size: 1000,
header: 'Описание'
})
],
[colors, prefixID]
);
return (
<div>
<div className='flex gap-3 items-end 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'>
<Button
text='Поставщики'
title='Добавить все конституенты, от которых зависят выбранные'
className='w-[7rem]'
onClick={selectBasis}
/>
<Button
text='Потребители'
title='Добавить все конституенты, которые зависят от выбранных'
className='w-[7rem]'
onClick={selectDependant}
/>
</div>
</div>
<DataTable
id={id}
dense
noFooter
rows={rows}
contentHeight='1.3rem'
className={clsx(
'min-h-[16rem]', // prettier: split lines
'overflow-y-auto',
'border',
'text-sm',
'select-none'
)}
data={schema?.items ?? []}
columns={columns}
headPosition='0rem'
enableRowSelection
rowSelection={rowSelection}
onRowSelectionChange={handleRowSelection}
noDataComponent={
<FlexColumn className='items-center p-3'>
<p>Список пуст</p>
</FlexColumn>
}
/>
</div>
);
}
export default ConstituentaMultiPicker;

View File

@ -1,3 +1,5 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/DataTable';
@ -14,7 +16,7 @@ import FlexColumn from './ui/FlexColumn';
interface ConstituentaPickerProps {
id?: string;
prefixID?: string;
prefixID: string;
data?: IConstituenta[];
rows?: number;
@ -74,8 +76,6 @@ function ConstituentaPicker({
[colors, prefixID, describeFunc]
);
const size = useMemo(() => `calc(2px + (2px + 1.8rem)*${rows})`, [rows]);
const conditionalRowStyles = useMemo(
(): IConditionalStyle<IConstituenta>[] => [
{
@ -96,11 +96,12 @@ function ConstituentaPicker({
/>
<DataTable
id={id}
rows={rows}
contentHeight='1.3rem'
dense
noHeader
noFooter
className='overflow-y-auto text-sm select-none'
style={{ maxHeight: size, minHeight: size }}
data={filteredData}
columns={columns}
conditionalRowStyles={conditionalRowStyles}

View File

@ -2,28 +2,34 @@ import { AnimatePresence } from 'framer-motion';
import AnimateFade from './AnimateFade';
import InfoError, { ErrorData } from './InfoError';
import { CProps } from './props';
import Loader from './ui/Loader';
interface DataLoaderProps {
interface DataLoaderProps extends CProps.AnimatedDiv {
id: string;
isLoading: boolean;
isLoading?: boolean;
error?: ErrorData;
hasNoData?: boolean;
children: React.ReactNode;
}
function DataLoader({ id, isLoading, hasNoData, error, children }: DataLoaderProps) {
function DataLoader({ id, isLoading, hasNoData, error, children, ...restProps }: DataLoaderProps) {
return (
<AnimatePresence mode='wait'>
{isLoading ? <Loader key={`${id}-loader`} /> : null}
{error ? <InfoError key={`${id}-error`} error={error} /> : null}
{!isLoading && !error && !hasNoData ? (
<AnimateFade id={id} key={`${id}-data`}>
<AnimateFade id={id} key={`${id}-data`} {...restProps}>
{children}
</AnimateFade>
) : null}
{!isLoading && !error && hasNoData ? (
<AnimateFade id={id} key={`${id}-data`} {...restProps}>
Данные не загружены
</AnimateFade>
) : null}
</AnimatePresence>
);
}

View File

@ -36,6 +36,7 @@ export interface DataTableProps<TData extends RowData>
id?: string;
dense?: boolean;
rows?: number;
contentHeight?: string;
headPosition?: string;
noHeader?: boolean;
noFooter?: boolean;
@ -73,6 +74,7 @@ function DataTable<TData extends RowData>({
className,
dense,
rows,
contentHeight = '1.1875rem',
headPosition,
conditionalRowStyles,
noFooter,
@ -130,11 +132,11 @@ function DataTable<TData extends RowData>({
return undefined;
}
if (dense) {
return `calc(2px + (2px + 1.6875rem)*${rows} + ${noHeader ? '0px' : '(2px + 2.1875rem)'})`;
return `calc(2px + (2px + ${contentHeight} + 0.5rem)*${rows} + ${noHeader ? '0px' : '(2px + 2.1875rem)'})`;
} else {
return `calc(2px + (2px + 2.1875rem)*${rows + (noHeader ? 0 : 1)})`;
return `calc(2px + (2px + ${contentHeight} + 1rem)*${rows + (noHeader ? 0 : 1)})`;
}
}, [rows, dense, noHeader]);
}, [rows, dense, noHeader, contentHeight]);
return (
<div id={id} className={className} style={{ minHeight: fixedSize, maxHeight: fixedSize, ...style }}>

View File

@ -19,34 +19,6 @@ import { RSLanguage } from './rslang';
import { getSymbolSubstitute, RSTextWrapper } from './textEditing';
import { rsHoverTooltip } from './tooltip';
const editorSetup: BasicSetupOptions = {
highlightSpecialChars: false,
history: true,
drawSelection: true,
syntaxHighlighting: false,
defaultKeymap: true,
historyKeymap: true,
lineNumbers: false,
highlightActiveLineGutter: false,
foldGutter: false,
dropCursor: true,
allowMultipleSelections: false,
indentOnInput: false,
bracketMatching: false,
closeBrackets: false,
autocompletion: false,
rectangularSelection: false,
crosshairCursor: false,
highlightActiveLine: false,
highlightSelectionMatches: false,
closeBracketsKeymap: false,
searchKeymap: false,
foldKeymap: false,
completionKeymap: false,
lintKeymap: false
};
interface RSInputProps
extends Pick<
ReactCodeMirrorProps,
@ -60,7 +32,22 @@ interface RSInputProps
}
const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
({ id, label, onChange, onAnalyze, disabled, noTooltip, className, style, ...restProps }, ref) => {
(
{
id, // prettier: split lines
label,
disabled,
noTooltip,
className,
style,
onChange,
onAnalyze,
...restProps
},
ref
) => {
const { darkMode, colors } = useConceptTheme();
const { schema } = useRSForm();
@ -167,3 +154,32 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
);
export default RSInput;
// ======= Internal ==========
const editorSetup: BasicSetupOptions = {
highlightSpecialChars: false,
history: true,
drawSelection: true,
syntaxHighlighting: false,
defaultKeymap: true,
historyKeymap: true,
lineNumbers: false,
highlightActiveLineGutter: false,
foldGutter: false,
dropCursor: true,
allowMultipleSelections: false,
indentOnInput: false,
bracketMatching: false,
closeBrackets: false,
autocompletion: false,
rectangularSelection: false,
crosshairCursor: false,
highlightActiveLine: false,
highlightSelectionMatches: false,
closeBracketsKeymap: false,
searchKeymap: false,
foldKeymap: false,
completionKeymap: false,
lintKeymap: false
};

View File

@ -76,7 +76,7 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
id='dlg_cst_term'
spellCheck
label='Термин'
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
placeholder='Обозначение, используемое в текстовых определениях'
rows={2}
value={cstData.term_raw}
onChange={event => updateCstData({ term_raw: event.target.value })}
@ -84,8 +84,7 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
<RSInput
id='dlg_cst_expression'
label='Формальное определение'
placeholder='Родоструктурное выражение, задающее формальное определение'
height='5.1rem'
placeholder='Родоструктурное выражение'
value={cstData.definition_formal}
onChange={value => updateCstData({ definition_formal: value })}
/>
@ -93,7 +92,7 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
id='dlg_cst_definition'
spellCheck
label='Текстовое определение'
placeholder='Лингвистическая интерпретация формального выражения'
placeholder='Текстовая интерпретация формального выражения'
rows={2}
value={cstData.definition_raw}
onChange={event => updateCstData({ definition_raw: event.target.value })}

View File

@ -1,18 +1,31 @@
'use client';
import { LibraryItemID } from '@/models/library';
import { IRSForm } from '@/models/rsform';
import ConstituentaMultiPicker from '@/components/ConstituentaMultiPicker';
import DataLoader from '@/components/DataLoader';
import { ErrorData } from '@/components/InfoError';
import { ConstituentaID, IRSForm } from '@/models/rsform';
import { prefixes } from '@/utils/constants';
interface ConstituentsTabProps {
schema?: IRSForm;
loading?: boolean;
selected: LibraryItemID[];
setSelected: React.Dispatch<LibraryItemID[]>;
error?: ErrorData;
selected: ConstituentaID[];
setSelected: React.Dispatch<ConstituentaID[]>;
}
// { schema, loading, selected, setSelected }: ConstituentsTabProps
function ConstituentsTab(props: ConstituentsTabProps) {
return <>2 - {props.loading}</>;
function ConstituentsTab({ schema, error, loading, selected, setSelected }: ConstituentsTabProps) {
return (
<DataLoader id='dlg-constituents-tab' isLoading={loading} error={error} hasNoData={!schema}>
<ConstituentaMultiPicker
schema={schema}
rows={16}
prefixID={prefixes.cst_inline_synth_list}
selected={selected}
setSelected={setSelected}
/>
</DataLoader>
);
}
export default ConstituentsTab;

View File

@ -3,7 +3,6 @@
import { useMemo } from 'react';
import SchemaPicker from '@/components/SchemaPicker';
import FlexColumn from '@/components/ui/FlexColumn';
import TextInput from '@/components/ui/TextInput';
import { useLibrary } from '@/context/LibraryContext';
import { LibraryItemID } from '@/models/library';
@ -18,18 +17,21 @@ function SchemaTab({ selected, setSelected }: SchemaTabProps) {
const selectedInfo = useMemo(() => library.items.find(item => item.id === selected), [selected, library.items]);
return (
<FlexColumn>
<TextInput
id='dlg_selected_schema_title'
label='Выбрана'
noBorder
placeholder='Выберите из списка ниже'
value={selectedInfo?.title}
disabled
dense
/>
<div className='flex flex-col'>
<div className='flex gap-6 items-center'>
<span className='select-none'>Выбрана</span>
<TextInput
id='dlg_selected_schema_title'
disabled
noBorder
className='w-full'
placeholder='Выберите из списка ниже'
value={selectedInfo?.title ?? ''}
dense
/>
</div>
<SchemaPicker rows={16} value={selected} onSelectValue={setSelected} />
</FlexColumn>
</div>
);
}

View File

@ -128,7 +128,7 @@ function FormConstituenta({
<RefsInput
id='cst_term'
label='Термин'
placeholder='Обозначение, используемое в текстовых определениях данной схемы'
placeholder='Обозначение, используемое в текстовых определениях'
items={schema?.items}
value={term}
initialValue={constituenta?.term_raw ?? ''}
@ -165,7 +165,7 @@ function FormConstituenta({
<RefsInput
id='cst_definition'
label='Текстовое определение'
placeholder='Текстовый вариант формального определения'
placeholder='Текстовая интерпретация формального выражения'
height='3.8rem'
items={schema?.items}
value={textDefinition}

View File

@ -1,10 +1,11 @@
'use client';
import clsx from 'clsx';
import { useLayoutEffect, useState } from 'react';
import { useLayoutEffect, useMemo, useState } from 'react';
import { type RowSelectionState } from '@/components/DataTable';
import SelectedCounter from '@/components/SelectedCounter';
import { useConceptTheme } from '@/context/ThemeContext';
import { ConstituentaID, CstType } from '@/models/rsform';
import { useRSEdit } from '../RSEditContext';
@ -18,6 +19,7 @@ interface EditorRSListProps {
}
function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps) {
const { calculateHeight } = useConceptTheme();
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const controller = useRSEdit();
@ -91,6 +93,8 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
return false;
}
const tableHeight = useMemo(() => calculateHeight('4.05rem + 5px'), [calculateHeight]);
return (
<div tabIndex={-1} className='outline-none' onKeyDown={handleTableKey}>
{controller.isContentEditable || controller.isProcessing ? (
@ -112,8 +116,9 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
/>
<RSTable
enableSelection={controller.isContentEditable || controller.isProcessing}
items={controller.schema?.items}
maxHeight={tableHeight}
enableSelection={controller.isContentEditable || controller.isProcessing}
selected={rowSelection}
setSelected={handleRowSelection}
onEdit={onOpenEdit}

View File

@ -15,6 +15,7 @@ import { labelCstTypification } from '@/utils/labels';
interface RSTableProps {
items?: IConstituenta[];
enableSelection: boolean;
maxHeight?: string;
selected: RowSelectionState;
setSelected: React.Dispatch<React.SetStateAction<RowSelectionState>>;
@ -29,8 +30,8 @@ const COLUMN_CONVENTION_HIDE_THRESHOLD = 1800;
const columnHelper = createColumnHelper<IConstituenta>();
function RSTable({ items, enableSelection, selected, setSelected, onEdit, onCreateNew }: RSTableProps) {
const { colors, calculateHeight } = useConceptTheme();
function RSTable({ items, maxHeight, enableSelection, selected, setSelected, onEdit, onCreateNew }: RSTableProps) {
const { colors } = useConceptTheme();
const windowSize = useWindowSize();
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
@ -115,14 +116,12 @@ function RSTable({ items, enableSelection, selected, setSelected, onEdit, onCrea
[colors]
);
const tableHeight = useMemo(() => calculateHeight('4.05rem + 5px'), [calculateHeight]);
return (
<DataTable
dense
noFooter
className={clsx('min-h-[16rem]', 'overflow-y-auto', 'text-sm', 'select-none')}
style={{ maxHeight: tableHeight }}
style={{ maxHeight: maxHeight }}
data={items ?? []}
columns={columns}
headPosition='0rem'