F: Implement constituenta relocation pt1

This commit is contained in:
Ivan 2024-10-23 15:31:24 +03:00
parent ed30714628
commit 5700462ad5
12 changed files with 285 additions and 45 deletions

View File

@ -5,6 +5,7 @@ import { useLayoutEffect, useMemo, useState } from 'react';
import DataTable, { createColumnHelper, RowSelectionState } from '@/components/ui/DataTable';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { Graph } from '@/models/Graph';
import { CstMatchMode } from '@/models/miscellaneous';
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
import { isBasicConcept, matchConstituenta } from '@/models/rsformAPI';
@ -17,7 +18,9 @@ import ToolbarGraphSelection from './ToolbarGraphSelection';
interface PickMultiConstituentaProps {
id?: string;
schema?: IRSForm;
schema: IRSForm;
data: IConstituenta[];
prefixID: string;
rows?: number;
@ -27,12 +30,39 @@ interface PickMultiConstituentaProps {
const columnHelper = createColumnHelper<IConstituenta>();
function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelected }: PickMultiConstituentaProps) {
function PickMultiConstituenta({
id,
schema,
data,
prefixID,
rows,
selected,
setSelected
}: PickMultiConstituentaProps) {
const { colors } = useConceptOptions();
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [filtered, setFiltered] = useState<IConstituenta[]>(schema?.items ?? []);
const [filtered, setFiltered] = useState<IConstituenta[]>(data);
const [filterText, setFilterText] = useState('');
const foldedGraph = useMemo(() => {
if (data.length === schema.items.length) {
return schema.graph;
}
const newGraph = new Graph();
schema.graph.nodes.forEach(node => {
newGraph.addNode(node.id);
node.outputs.forEach(output => {
newGraph.addEdge(node.id, output);
});
});
schema.items
.filter(item => data.find(cst => cst.id === item.id) === undefined)
.forEach(item => {
newGraph.foldNode(item.id);
});
return newGraph;
}, [schema.graph, data]);
useLayoutEffect(() => {
if (filtered.length === 0) {
setRowSelection({});
@ -46,17 +76,17 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
}, [filtered, setRowSelection, selected]);
useLayoutEffect(() => {
if (!schema || schema.items.length === 0) {
if (data.length === 0) {
setFiltered([]);
} else if (filterText) {
setFiltered(schema.items.filter(cst => matchConstituenta(cst, filterText, CstMatchMode.ALL)));
setFiltered(data.filter(cst => matchConstituenta(cst, filterText, CstMatchMode.ALL)));
} else {
setFiltered(schema.items);
setFiltered(data);
}
}, [filterText, schema?.items, schema]);
}, [filterText, data]);
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
if (!schema) {
if (!data) {
setSelected([]);
} else {
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
@ -91,7 +121,7 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
<div>
<div className='flex justify-between items-center clr-input px-3 border-x border-t rounded-t-md'>
<div className='w-[24ch] select-none whitespace-nowrap'>
Выбраны {selected.length} из {schema?.items.length ?? 0}
Выбраны {selected.length} из {data.length}
</div>
<SearchBar
id='dlg_constituents_search'
@ -100,16 +130,14 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
value={filterText}
onChange={setFilterText}
/>
{schema ? (
<ToolbarGraphSelection
graph={schema.graph}
graph={foldedGraph}
isCore={cstID => isBasicConcept(schema.cstByID.get(cstID)?.cst_type)}
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
setSelected={setSelected}
emptySelection={selected.length === 0}
className='w-fit'
/>
) : null}
</div>
<DataTable
id={id}

View File

@ -9,7 +9,7 @@ import TabLabel from '@/components/ui/TabLabel';
import useRSFormDetails from '@/hooks/useRSFormDetails';
import { LibraryItemID } from '@/models/library';
import { ICstSubstitute } from '@/models/oss';
import { IInlineSynthesisData, IRSForm } from '@/models/rsform';
import { ConstituentaID, IInlineSynthesisData, IRSForm } from '@/models/rsform';
import TabConstituents from './TabConstituents';
import TabSchema from './TabSchema';
@ -30,7 +30,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
const [activeTab, setActiveTab] = useState(TabID.SCHEMA);
const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined);
const [selected, setSelected] = useState<LibraryItemID[]>([]);
const [selected, setSelected] = useState<ConstituentaID[]>([]);
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>([]);
const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined });

View File

@ -17,13 +17,16 @@ interface TabConstituentsProps {
function TabConstituents({ schema, error, loading, selected, setSelected }: TabConstituentsProps) {
return (
<DataLoader id='dlg-constituents-tab' isLoading={loading} error={error} hasNoData={!schema}>
{schema ? (
<PickMultiConstituenta
schema={schema}
data={schema.items}
rows={13}
prefixID={prefixes.cst_inline_synth_list}
selected={selected}
setSelected={setSelected}
/>
) : null}
</DataLoader>
);
}

View File

@ -0,0 +1,96 @@
'use client';
import clsx from 'clsx';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import PickMultiConstituenta from '@/components/select/PickMultiConstituenta';
import SelectLibraryItem from '@/components/select/SelectLibraryItem';
import Modal, { ModalProps } from '@/components/ui/Modal';
import DataLoader from '@/components/wrap/DataLoader';
import { useLibrary } from '@/context/LibraryContext';
import useRSFormDetails from '@/hooks/useRSFormDetails';
import { ILibraryItem, LibraryItemID } from '@/models/library';
import { ICstRelocateData, IOperation, IOperationSchema } from '@/models/oss';
import { getRelocateCandidates } from '@/models/ossAPI';
import { ConstituentaID } from '@/models/rsform';
import { prefixes } from '@/utils/constants';
interface DlgRelocateConstituentsProps extends Pick<ModalProps, 'hideWindow'> {
oss: IOperationSchema;
target: IOperation;
onSubmit: (data: ICstRelocateData) => void;
}
function DlgRelocateConstituents({ oss, hideWindow, target, onSubmit }: DlgRelocateConstituentsProps) {
const library = useLibrary();
const schemas = useMemo(() => {
const node = oss.graph.at(target.id)!;
const ids: LibraryItemID[] = [
...node.inputs.map(id => oss.operationByID.get(id)!.result).filter(id => id !== null),
...node.outputs.map(id => oss.operationByID.get(id)!.result).filter(id => id !== null)
];
return ids.map(id => library.items.find(item => item.id === id)).filter(item => item !== undefined);
}, [oss, library.items]);
const [destination, setDestination] = useState<ILibraryItem | undefined>(undefined);
const [selected, setSelected] = useState<ConstituentaID[]>([]);
const source = useRSFormDetails({ target: String(target.result!) });
const filtered = useMemo(() => {
if (!source.schema || !destination) {
return [];
}
const destinationOperation = oss.items.find(item => item.result === destination.id);
return getRelocateCandidates(target.id, destinationOperation!.id, source.schema, oss);
}, [destination, source.schema?.items]);
const isValid = useMemo(() => !!destination && selected.length > 0, [destination, selected]);
useLayoutEffect(() => {
setSelected([]);
}, [destination]);
const handleSelectDestination = useCallback((newValue: ILibraryItem | undefined) => {
setDestination(newValue);
}, []);
const handleSubmit = useCallback(() => {
const data: ICstRelocateData = {
destination: target.result ?? 0,
items: []
};
onSubmit(data);
}, [target, onSubmit]);
return (
<Modal
header='Перемещение конституент'
submitText='Переместить'
hideWindow={hideWindow}
canSubmit={isValid}
onSubmit={handleSubmit}
className={clsx('w-[40rem] h-[33rem]', 'py-3 px-6')}
>
<DataLoader id='dlg-relocate-constituents' className='cc-column' isLoading={source.loading} error={source.error}>
<SelectLibraryItem
placeholder='Выберите целевую схему'
items={schemas}
value={destination}
onSelectValue={handleSelectDestination}
/>
{source.schema ? (
<PickMultiConstituenta
schema={source.schema}
data={filtered}
rows={12}
prefixID={prefixes.dlg_cst_constituents_list}
selected={selected}
setSelected={setSelected}
/>
) : null}
</DataLoader>
</Modal>
);
}
export default DlgRelocateConstituents;

View File

@ -86,30 +86,25 @@ export class Graph {
return !!this.nodes.get(target);
}
removeNode(target: number): GraphNode | null {
const nodeToRemove = this.nodes.get(target);
if (!nodeToRemove) {
return null;
}
removeNode(target: number): void {
this.nodes.forEach(node => {
node.removeInput(nodeToRemove.id);
node.removeOutput(nodeToRemove.id);
node.removeInput(target);
node.removeOutput(target);
});
this.nodes.delete(target);
return nodeToRemove;
}
foldNode(target: number): GraphNode | null {
foldNode(target: number): void {
const nodeToRemove = this.nodes.get(target);
if (!nodeToRemove) {
return null;
return;
}
nodeToRemove.inputs.forEach(input => {
nodeToRemove.outputs.forEach(output => {
this.addEdge(input, output);
});
});
return this.removeNode(target);
this.removeNode(target);
}
removeIsolated(): GraphNode[] {
@ -124,6 +119,9 @@ export class Graph {
}
addEdge(source: number, destination: number): void {
if (this.hasEdge(source, destination)) {
return;
}
const sourceNode = this.addNode(source);
const destinationNode = this.addNode(destination);
sourceNode.addOutput(destinationNode.id);

View File

@ -125,6 +125,14 @@ export interface ICstSubstituteData {
substitutions: ICstSubstitute[];
}
/**
* Represents data, used relocating {@link IConstituenta}s between {@link ILibraryItem}s.
*/
export interface ICstRelocateData {
destination: LibraryItemID;
items: ConstituentaID[];
}
/**
* Represents substitution for multi synthesis table.
*/

View File

@ -8,7 +8,7 @@ import { TextMatcher } from '@/utils/utils';
import { Graph } from './Graph';
import { ILibraryItem, LibraryItemID } from './library';
import { ICstSubstitute, IOperation, IOperationSchema, SubstitutionErrorType } from './oss';
import { ICstSubstitute, IOperation, IOperationSchema, OperationID, SubstitutionErrorType } from './oss';
import { ConstituentaID, CstClass, CstType, IConstituenta, IRSForm } from './rsform';
import { AliasMapping, ParsingStatus } from './rslang';
import { applyAliasMapping, applyTypificationMapping, extractGlobals, isSetTypification } from './rslangAPI';
@ -424,3 +424,45 @@ export class SubstitutionValidator {
return false;
}
}
/**
* Filter relocate candidates from gives schema.
*/
export function getRelocateCandidates(
source: OperationID,
destination: OperationID,
schema: IRSForm,
oss: IOperationSchema
): IConstituenta[] {
const destinationSchema = oss.operationByID.get(destination)?.result;
if (!destinationSchema) {
return [];
}
const node = oss.graph.at(source);
if (!node) {
return [];
}
const addedCst = schema.items.filter(item => !item.is_inherited);
if (node.outputs.includes(destination)) {
return addedCst;
}
const unreachableBases: ConstituentaID[] = [];
for (const cst of schema.items.filter(item => item.is_inherited)) {
if (cst.parent_schema == destinationSchema) {
continue;
}
const parent = schema.inheritance.find(item => item.child === cst.id && item.child_source === cst.schema)?.parent;
if (parent) {
const original = oss.substitutions.find(sub => sub.substitution === parent)?.original;
if (original) {
continue;
// TODO: test if original schema is destination schema
}
}
unreachableBases.push(cst.id);
}
const unreachable = schema.graph.expandAllOutputs(unreachableBases);
return addedCst.filter(cst => !unreachable.includes(cst.id));
}

View File

@ -2,7 +2,15 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { IconConnect, IconDestroy, IconEdit2, IconExecute, IconNewRSForm, IconRSForm } from '@/components/Icons';
import {
IconChild,
IconConnect,
IconDestroy,
IconEdit2,
IconExecute,
IconNewRSForm,
IconRSForm
} from '@/components/Icons';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import useClickedOutside from '@/hooks/useClickedOutside';
@ -25,6 +33,7 @@ interface NodeContextMenuProps extends ContextMenuData {
onEditSchema: (target: OperationID) => void;
onEditOperation: (target: OperationID) => void;
onExecuteOperation: (target: OperationID) => void;
onRelocateConstituents: (target: OperationID) => void;
}
function NodeContextMenu({
@ -36,7 +45,8 @@ function NodeContextMenu({
onCreateInput,
onEditSchema,
onEditOperation,
onExecuteOperation
onExecuteOperation,
onRelocateConstituents
}: NodeContextMenuProps) {
const controller = useOssEdit();
const [isOpen, setIsOpen] = useState(false);
@ -100,6 +110,11 @@ function NodeContextMenu({
onExecuteOperation(operation.id);
};
const handleRelocateConstituents = () => {
handleHide();
onRelocateConstituents(operation.id);
};
return (
<div ref={ref} className='absolute select-none' style={{ top: cursorY, left: cursorX }}>
<Dropdown
@ -156,6 +171,16 @@ function NodeContextMenu({
/>
) : null}
{controller.isMutable && operation.result ? (
<DropdownButton
text='Конституенты'
titleHtml='Перемещение конституент</br>между схемами'
icon={<IconChild size='1rem' className='icon-green' />}
disabled={controller.isProcessing}
onClick={handleRelocateConstituents}
/>
) : null}
<DropdownButton
text='Удалить операцию'
icon={<IconDestroy size='1rem' className='icon-red' />}

View File

@ -197,6 +197,13 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
handleExecuteOperation(controller.selected[0]);
}, [controller, handleExecuteOperation]);
const handleRelocateConstituents = useCallback(
(target: OperationID) => {
controller.promptRelocateConstituents(target);
},
[controller]
);
const handleFitView = useCallback(() => {
flow.fitView({ duration: PARAMETER.zoomDuration });
}, [flow]);
@ -376,6 +383,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
onEditSchema={handleEditSchema}
onEditOperation={handleEditOperation}
onExecuteOperation={handleExecuteOperation}
onRelocateConstituents={handleRelocateConstituents}
{...menuProps}
/>
) : null}

View File

@ -17,10 +17,12 @@ import DlgCreateOperation from '@/dialogs/DlgCreateOperation';
import DlgDeleteOperation from '@/dialogs/DlgDeleteOperation';
import DlgEditEditors from '@/dialogs/DlgEditEditors';
import DlgEditOperation from '@/dialogs/DlgEditOperation';
import DlgRelocateConstituents from '@/dialogs/DlgRelocateConstituents';
import { AccessPolicy, ILibraryItemEditor, LibraryItemID } from '@/models/library';
import { Position2D } from '@/models/miscellaneous';
import { calculateInsertPosition } from '@/models/miscellaneousAPI';
import {
ICstRelocateData,
IOperationCreateData,
IOperationDeleteData,
IOperationPosition,
@ -74,6 +76,7 @@ export interface IOssEditContext extends ILibraryItemEditor {
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void;
executeOperation: (target: OperationID, positions: IOperationPosition[]) => void;
promptRelocateConstituents: (target: OperationID) => void;
}
const OssEditContext = createContext<IOssEditContext | null>(null);
@ -110,6 +113,7 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
const [showEditInput, setShowEditInput] = useState(false);
const [showEditOperation, setShowEditOperation] = useState(false);
const [showDeleteOperation, setShowDeleteOperation] = useState(false);
const [showRelocateConstituents, setShowRelocateConstituents] = useState(false);
const [showCreateOperation, setShowCreateOperation] = useState(false);
const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 });
@ -359,6 +363,23 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
[model]
);
const promptRelocateConstituents = useCallback(
(target: OperationID) => {
setTargetOperationID(target);
setShowRelocateConstituents(true);
},
[model]
);
const handleRelocateConstituents = useCallback(
(data: ICstRelocateData) => {
// TODO: implement backed call
console.log(data);
toast.success('В разработке');
},
[model]
);
return (
<OssEditContext.Provider
value={{
@ -388,7 +409,8 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
createInput,
promptEditInput,
promptEditOperation,
executeOperation
executeOperation,
promptRelocateConstituents
}}
>
{model.schema ? (
@ -438,6 +460,15 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
onSubmit={deleteOperation}
/>
) : null}
{showRelocateConstituents ? (
<DlgRelocateConstituents
hideWindow={() => setShowRelocateConstituents(false)}
target={targetOperation!}
oss={model.schema}
onSubmit={handleRelocateConstituents}
/>
) : null}
)
</AnimatePresence>
) : null}

View File

@ -69,7 +69,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
);
return (
<div className='flex border-b clr-input'>
<div className='flex border-b clr-input rounded-t-md'>
<SearchBar
id='constituents_search'
noBorder

View File

@ -182,5 +182,6 @@ export const prefixes = {
user_editors: 'user_editors_',
wordform_list: 'wordform_list_',
rsedit_btn: 'rsedit_btn_',
dlg_cst_substitutes_list: 'dlg_cst_substitutes_list_'
dlg_cst_substitutes_list: 'dlg_cst_substitutes_list_',
dlg_cst_constituents_list: 'dlg_cst_constituents_list_'
};