mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Implementing inline synthesis pt2
This commit is contained in:
parent
bcbe35b436
commit
71e87ac9c5
145
rsconcept/frontend/src/components/ConstituentaMultiPicker.tsx
Normal file
145
rsconcept/frontend/src/components/ConstituentaMultiPicker.tsx
Normal 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;
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 }}>
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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 })}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue
Block a user