F: Constituenta relocation: final part
Some checks failed
Frontend CI / build (22.x) (push) Waiting to run
Backend CI / build (3.12) (push) Has been cancelled

This commit is contained in:
Ivan 2024-10-28 23:55:25 +03:00
parent 0d14151469
commit 9761cc2c9d
7 changed files with 122 additions and 55 deletions

View File

@ -16,4 +16,4 @@ djangorestframework-stubs==3.15.1
django-extensions==3.2.3 django-extensions==3.2.3
mypy==1.11.2 mypy==1.11.2
pylint==3.3.1 pylint==3.3.1
coverage==7.6.3 coverage==7.6.4

View File

@ -1,14 +1,14 @@
tzdata==2024.1 tzdata==2024.2
Django==5.1.1 Django==5.1.2
djangorestframework==3.15.2 djangorestframework==3.15.2
django-cors-headers==4.4.0 django-cors-headers==4.5.0
django-filter==24.3 django-filter==24.3
drf-spectacular==0.27.2 drf-spectacular==0.27.2
drf-spectacular-sidecar==2024.7.1 drf-spectacular-sidecar==2024.7.1
coreapi==2.3.3 coreapi==2.3.3
django-rest-passwordreset==1.4.1 django-rest-passwordreset==1.4.2
cctext==0.1.4 cctext==0.1.4
pyconcept==0.1.10 pyconcept==0.1.11
psycopg2-binary==2.9.9 psycopg2-binary==2.9.10
gunicorn==23.0.0 gunicorn==23.0.0

View File

@ -20,6 +20,8 @@ import {
IconGraphInputs, IconGraphInputs,
IconGraphOutputs, IconGraphOutputs,
IconHide, IconHide,
IconMoveDown,
IconMoveUp,
IconOSS, IconOSS,
IconPrivate, IconPrivate,
IconProps, IconProps,
@ -159,3 +161,11 @@ export function CstTypeIcon({ value, size = '1.25rem', className }: DomIconProps
return <IconCstTheorem size={size} className={className ?? 'clr-text-red'} />; return <IconCstTheorem size={size} className={className ?? 'clr-text-red'} />;
} }
} }
export function RelocateUpIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
if (value) {
return <IconMoveUp size={size} className={className ?? 'clr-text-primary'} />;
} else {
return <IconMoveDown size={size} className={className ?? 'clr-text-primary'} />;
}
}

View File

@ -23,6 +23,7 @@ interface PickMultiConstituentaProps {
prefixID: string; prefixID: string;
rows?: number; rows?: number;
noBorder?: boolean;
selected: ConstituentaID[]; selected: ConstituentaID[];
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>; setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
@ -36,6 +37,7 @@ function PickMultiConstituenta({
data, data,
prefixID, prefixID,
rows, rows,
noBorder,
selected, selected,
setSelected setSelected
}: PickMultiConstituentaProps) { }: PickMultiConstituentaProps) {
@ -118,10 +120,10 @@ function PickMultiConstituenta({
); );
return ( return (
<div> <div className={noBorder ? '' : 'border'}>
<div className='flex justify-between items-center clr-input px-3 border-x border-t rounded-t-md'> <div className={clsx('px-3 flex justify-between items-center', 'clr-input', 'border-b', 'rounded-t-md')}>
<div className='w-[24ch] select-none whitespace-nowrap'> <div className='w-[24ch] select-none whitespace-nowrap'>
Выбраны {selected.length} из {data.length} {data.length > 0 ? `Выбраны ${selected.length} из ${data.length}` : 'Конституенты'}
</div> </div>
<SearchBar <SearchBar
id='dlg_constituents_search' id='dlg_constituents_search'
@ -145,7 +147,7 @@ function PickMultiConstituenta({
noFooter noFooter
rows={rows} rows={rows}
contentHeight='1.3rem' contentHeight='1.3rem'
className={clsx('cc-scroll-y', 'border', 'text-sm', 'select-none')} className={clsx('cc-scroll-y', 'text-sm', 'select-none')}
data={filtered} data={filtered}
columns={columns} columns={columns}
headPosition='0rem' headPosition='0rem'

View File

@ -1,10 +1,12 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { RelocateUpIcon } from '@/components/DomainIcons';
import PickMultiConstituenta from '@/components/select/PickMultiConstituenta'; import PickMultiConstituenta from '@/components/select/PickMultiConstituenta';
import SelectLibraryItem from '@/components/select/SelectLibraryItem'; import SelectLibraryItem from '@/components/select/SelectLibraryItem';
import MiniButton from '@/components/ui/MiniButton';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import DataLoader from '@/components/wrap/DataLoader'; import DataLoader from '@/components/wrap/DataLoader';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
@ -17,41 +19,57 @@ import { prefixes } from '@/utils/constants';
interface DlgRelocateConstituentsProps extends Pick<ModalProps, 'hideWindow'> { interface DlgRelocateConstituentsProps extends Pick<ModalProps, 'hideWindow'> {
oss: IOperationSchema; oss: IOperationSchema;
target: IOperation; initialTarget?: IOperation;
onSubmit: (data: ICstRelocateData) => void; onSubmit: (data: ICstRelocateData) => void;
} }
function DlgRelocateConstituents({ oss, hideWindow, target, onSubmit }: DlgRelocateConstituentsProps) { function DlgRelocateConstituents({ oss, hideWindow, initialTarget, onSubmit }: DlgRelocateConstituentsProps) {
const library = useLibrary(); 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, target.id]);
const [directionUp, setDirectionUp] = useState(true);
const [destination, setDestination] = useState<ILibraryItem | undefined>(undefined); const [destination, setDestination] = useState<ILibraryItem | undefined>(undefined);
const [selected, setSelected] = useState<ConstituentaID[]>([]); const [selected, setSelected] = useState<ConstituentaID[]>([]);
const [source, setSource] = useState<ILibraryItem | undefined>(
library.items.find(item => item.id === initialTarget?.result)
);
const source = useRSFormDetails({ target: String(target.result!) }); const operation = useMemo(() => oss.items.find(item => item.result === source?.id), [oss, source]);
const filtered = useMemo(() => { const sourceSchemas = useMemo(() => library.items.filter(item => oss.schemas.includes(item.id)), [library, oss]);
if (!source.schema || !destination) { const destinationSchemas = useMemo(() => {
if (!operation) {
return [];
}
const node = oss.graph.at(operation.id)!;
const ids: LibraryItemID[] = directionUp
? 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, operation, directionUp]);
const sourceData = useRSFormDetails({ target: source ? String(source.id) : undefined });
const filteredConstituents = useMemo(() => {
if (!sourceData.schema || !destination || !operation) {
return []; return [];
} }
const destinationOperation = oss.items.find(item => item.result === destination.id); const destinationOperation = oss.items.find(item => item.result === destination.id);
return getRelocateCandidates(target.id, destinationOperation!.id, source.schema, oss); return getRelocateCandidates(operation.id, destinationOperation!.id, sourceData.schema, oss);
}, [destination, target.id, source.schema, oss]); }, [destination, operation, sourceData.schema, oss]);
const isValid = useMemo(() => !!destination && selected.length > 0, [destination, selected]); const isValid = useMemo(() => !!destination && selected.length > 0, [destination, selected]);
const toggleDirection = useCallback(() => {
setDirectionUp(prev => !prev);
setDestination(undefined);
}, []);
useLayoutEffect(() => { const handleSelectSource = useCallback((newValue: ILibraryItem | undefined) => {
setSource(newValue);
setDestination(undefined);
setSelected([]); setSelected([]);
}, [destination]); }, []);
const handleSelectDestination = useCallback((newValue: ILibraryItem | undefined) => { const handleSelectDestination = useCallback((newValue: ILibraryItem | undefined) => {
setDestination(newValue); setDestination(newValue);
setSelected([]);
}, []); }, []);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
@ -74,24 +92,44 @@ function DlgRelocateConstituents({ oss, hideWindow, target, onSubmit }: DlgReloc
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={clsx('w-[40rem] h-[33rem]', 'py-3 px-6')} className={clsx('w-[40rem] h-[33rem]', 'py-3 px-6')}
> >
<DataLoader id='dlg-relocate-constituents' className='cc-column' isLoading={source.loading} error={source.error}> <div className='flex flex-col border'>
<SelectLibraryItem <div className='flex gap-1 items-center clr-input border-b rounded-t-md'>
placeholder='Выберите целевую схему' <SelectLibraryItem
items={schemas} noBorder
value={destination} className='w-1/2'
onSelectValue={handleSelectDestination} placeholder='Выберите исходную схему'
/> items={sourceSchemas}
{source.schema ? ( value={source}
<PickMultiConstituenta onSelectValue={handleSelectSource}
schema={source.schema}
data={filtered}
rows={12}
prefixID={prefixes.dlg_cst_constituents_list}
selected={selected}
setSelected={setSelected}
/> />
) : null} <MiniButton
</DataLoader> title='Направление перемещения'
icon={<RelocateUpIcon value={directionUp} />}
onClick={toggleDirection}
/>
<SelectLibraryItem
noBorder
className='w-1/2'
placeholder='Выберите целевую схему'
items={destinationSchemas}
value={destination}
onSelectValue={handleSelectDestination}
/>
</div>
<DataLoader id='dlg-relocate-constituents' isLoading={sourceData.loading} error={sourceData.error}>
{sourceData.schema ? (
<PickMultiConstituenta
noBorder
schema={sourceData.schema}
data={filteredConstituents}
rows={12}
prefixID={prefixes.dlg_cst_constituents_list}
selected={selected}
setSelected={setSelected}
/>
) : null}
</DataLoader>
</div>
</Modal> </Modal>
); );
} }

View File

@ -4,6 +4,7 @@ import { urls } from '@/app/urls';
import { import {
IconAdmin, IconAdmin,
IconAlert, IconAlert,
IconChild,
IconDestroy, IconDestroy,
IconEdit2, IconEdit2,
IconEditor, IconEditor,
@ -67,6 +68,11 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
router.push(urls.login); router.push(urls.login);
} }
function handleRelocate() {
editMenu.hide();
controller.promptRelocateConstituents(undefined, []);
}
return ( return (
<div className='flex'> <div className='flex'>
<div ref={schemaMenu.ref}> <div ref={schemaMenu.ref}>
@ -128,9 +134,11 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
/> />
<Dropdown isOpen={editMenu.isOpen}> <Dropdown isOpen={editMenu.isOpen}>
<DropdownButton <DropdownButton
text='см. Граф синтеза' text='Конституенты'
titleHtml='Редактирование доступно <br/>через Граф синтеза' titleHtml='Перемещение конституент</br>между схемами'
disabled icon={<IconChild size='1rem' className='icon-green' />}
disabled={controller.isProcessing}
onClick={handleRelocate}
/> />
</Dropdown> </Dropdown>
</div> </div>

View File

@ -76,7 +76,7 @@ export interface IOssEditContext extends ILibraryItemEditor {
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void; promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void; promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void;
executeOperation: (target: OperationID, positions: IOperationPosition[]) => void; executeOperation: (target: OperationID, positions: IOperationPosition[]) => void;
promptRelocateConstituents: (target: OperationID, positions: IOperationPosition[]) => void; promptRelocateConstituents: (target: OperationID | undefined, positions: IOperationPosition[]) => void;
} }
const OssEditContext = createContext<IOssEditContext | null>(null); const OssEditContext = createContext<IOssEditContext | null>(null);
@ -360,7 +360,7 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
[model] [model]
); );
const promptRelocateConstituents = useCallback((target: OperationID, positions: IOperationPosition[]) => { const promptRelocateConstituents = useCallback((target: OperationID | undefined, positions: IOperationPosition[]) => {
setPositions(positions); setPositions(positions);
setTargetOperationID(target); setTargetOperationID(target);
setShowRelocateConstituents(true); setShowRelocateConstituents(true);
@ -368,9 +368,18 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
const handleRelocateConstituents = useCallback( const handleRelocateConstituents = useCallback(
(data: ICstRelocateData) => { (data: ICstRelocateData) => {
model.savePositions({ positions: positions }, () => if (
model.relocateConstituents(data, () => toast.success(information.changesSaved)) positions.every(item => {
); const operation = model.schema!.operationByID.get(item.id)!;
return operation.position_x === item.position_x && operation.position_y === item.position_y;
})
) {
model.relocateConstituents(data, () => toast.success(information.changesSaved));
} else {
model.savePositions({ positions: positions }, () =>
model.relocateConstituents(data, () => toast.success(information.changesSaved))
);
}
}, },
[model, positions] [model, positions]
); );
@ -458,7 +467,7 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
{showRelocateConstituents ? ( {showRelocateConstituents ? (
<DlgRelocateConstituents <DlgRelocateConstituents
hideWindow={() => setShowRelocateConstituents(false)} hideWindow={() => setShowRelocateConstituents(false)}
target={targetOperation!} initialTarget={targetOperation}
oss={model.schema} oss={model.schema}
onSubmit={handleRelocateConstituents} onSubmit={handleRelocateConstituents}
/> />