F: Rework RelocateConstituents dialog
Some checks are pending
Frontend CI / build (22.x) (push) Waiting to run

This commit is contained in:
Ivan 2025-02-10 15:49:56 +03:00
parent a967e957b3
commit c5b73612f3
7 changed files with 87 additions and 66 deletions

View File

@ -12,7 +12,7 @@ import {
OperationID, OperationID,
OperationType OperationType
} from '@/features/oss/models/oss'; } from '@/features/oss/models/oss';
import { ConstituentaID, IConstituentaReference, ITargetCst } from '@/features/rsform/models/rsform'; import { IConstituentaReference, ITargetCst } from '@/features/rsform/models/rsform';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
/** /**
@ -130,10 +130,15 @@ export interface IOperationUpdateDTO extends ITargetOperation {
/** /**
* Represents data, used relocating {@link IConstituenta}s between {@link ILibraryItem}s. * Represents data, used relocating {@link IConstituenta}s between {@link ILibraryItem}s.
*/ */
export interface ICstRelocateDTO { export const schemaCstRelocate = z.object({
destination: LibraryItemID; destination: z.number(),
items: ConstituentaID[]; items: z.array(z.number()).refine(data => data.length > 0)
} });
/**
* Represents data, used relocating {@link IConstituenta}s between {@link ILibraryItem}s.
*/
export type ICstRelocateDTO = z.infer<typeof schemaCstRelocate>;
export const ossApi = { export const ossApi = {
baseKey: 'oss', baseKey: 'oss',
@ -218,9 +223,9 @@ export const ossApi = {
} }
}), }),
relocateConstituents: ({ itemID, data }: { itemID: LibraryItemID; data: ICstRelocateDTO }) => relocateConstituents: (data: ICstRelocateDTO) =>
axiosPost<ICstRelocateDTO, IOperationSchemaDTO>({ axiosPost<ICstRelocateDTO, IOperationSchemaDTO>({
endpoint: `/api/oss/${itemID}/relocate-constituents`, endpoint: `/api/oss/relocate-constituents`,
request: { request: {
data: data, data: data,
successMessage: information.changesSaved successMessage: information.changesSaved

View File

@ -1,7 +1,6 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/features/library/backend/api'; import { libraryApi } from '@/features/library/backend/api';
import { LibraryItemID } from '@/features/library/models/library';
import { rsformsApi } from '@/features/rsform/backend/api'; import { rsformsApi } from '@/features/rsform/backend/api';
import { ICstRelocateDTO, ossApi } from './api'; import { ICstRelocateDTO, ossApi } from './api';
@ -20,6 +19,6 @@ export const useRelocateConstituents = () => {
} }
}); });
return { return {
relocateConstituents: (data: { itemID: LibraryItemID; data: ICstRelocateDTO }) => mutation.mutate(data) relocateConstituents: (data: ICstRelocateDTO) => mutation.mutate(data)
}; };
}; };

View File

@ -103,7 +103,7 @@ function TabInputOperation({
{!createSchema ? ( {!createSchema ? (
<PickSchema <PickSchema
items={sortedItems} items={sortedItems}
value={attachedID} value={attachedID ?? null}
itemType={LibraryItemType.RSFORM} itemType={LibraryItemType.RSFORM}
onChange={onChangeAttachedID} onChange={onChangeAttachedID}
rows={8} rows={8}

View File

@ -1,7 +1,9 @@
'use client'; 'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx'; import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { RelocateUpIcon } from '@/components/DomainIcons'; import { RelocateUpIcon } from '@/components/DomainIcons';
@ -11,32 +13,50 @@ import { HelpTopic } from '@/features/help/models/helpTopic';
import { useLibrary } from '@/features/library/backend/useLibrary'; import { useLibrary } from '@/features/library/backend/useLibrary';
import SelectLibraryItem from '@/features/library/components/SelectLibraryItem'; import SelectLibraryItem from '@/features/library/components/SelectLibraryItem';
import { ILibraryItem, LibraryItemID } from '@/features/library/models/library'; import { ILibraryItem, LibraryItemID } from '@/features/library/models/library';
import { ICstRelocateDTO } from '@/features/oss/backend/api';
import { useRSForm } from '@/features/rsform/backend/useRSForm'; import { useRSForm } from '@/features/rsform/backend/useRSForm';
import PickMultiConstituenta from '@/features/rsform/components/PickMultiConstituenta'; import PickMultiConstituenta from '@/features/rsform/components/PickMultiConstituenta';
import { ConstituentaID } from '@/features/rsform/models/rsform';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { ICstRelocateDTO, IOperationPosition, schemaCstRelocate } from '../backend/api';
import { useRelocateConstituents } from '../backend/useRelocateConstituents';
import { useUpdatePositions } from '../backend/useUpdatePositions';
import { IOperation, IOperationSchema } from '../models/oss'; import { IOperation, IOperationSchema } from '../models/oss';
import { getRelocateCandidates } from '../models/ossAPI'; import { getRelocateCandidates } from '../models/ossAPI';
export interface DlgRelocateConstituentsProps { export interface DlgRelocateConstituentsProps {
oss: IOperationSchema; oss: IOperationSchema;
initialTarget?: IOperation; initialTarget?: IOperation;
onSubmit: (data: ICstRelocateDTO) => void; positions: IOperationPosition[];
} }
function DlgRelocateConstituents() { function DlgRelocateConstituents() {
const { oss, initialTarget, onSubmit } = useDialogsStore(state => state.props as DlgRelocateConstituentsProps); const { oss, initialTarget, positions } = useDialogsStore(state => state.props as DlgRelocateConstituentsProps);
const { items: libraryItems } = useLibrary(); const { items: libraryItems } = useLibrary();
const { updatePositions } = useUpdatePositions();
const { relocateConstituents } = useRelocateConstituents();
const {
handleSubmit,
control,
setValue,
resetField,
formState: { isValid }
} = useForm<ICstRelocateDTO>({
resolver: zodResolver(schemaCstRelocate),
defaultValues: {
items: []
},
mode: 'onChange'
});
const destination = useWatch({ control, name: 'destination' });
const destinationItem = destination ? libraryItems.find(item => item.id === destination) : undefined;
const [directionUp, setDirectionUp] = useState(true); const [directionUp, setDirectionUp] = useState(true);
const [destination, setDestination] = useState<ILibraryItem | undefined>(undefined);
const [selected, setSelected] = useState<ConstituentaID[]>([]);
const [source, setSource] = useState<ILibraryItem | undefined>( const [source, setSource] = useState<ILibraryItem | undefined>(
libraryItems.find(item => item.id === initialTarget?.result) libraryItems.find(item => item.id === initialTarget?.result)
); );
const isValid = !!destination && selected.length > 0;
console.log(isValid);
const operation = oss.items.find(item => item.result === source?.id); const operation = oss.items.find(item => item.result === source?.id);
const sourceSchemas = libraryItems.filter(item => oss.schemas.includes(item.id)); const sourceSchemas = libraryItems.filter(item => oss.schemas.includes(item.id));
@ -53,37 +73,50 @@ function DlgRelocateConstituents() {
const sourceData = useRSForm({ itemID: source?.id }); const sourceData = useRSForm({ itemID: source?.id });
const filteredConstituents = (() => { const filteredConstituents = (() => {
if (!sourceData.schema || !destination || !operation) { if (!sourceData.schema || !destinationItem || !operation) {
return []; return [];
} }
const destinationOperation = oss.items.find(item => item.result === destination.id); const destinationOperation = oss.items.find(item => item.result === destination);
return getRelocateCandidates(operation.id, destinationOperation!.id, sourceData.schema, oss); return getRelocateCandidates(operation.id, destinationOperation!.id, sourceData.schema, oss);
})(); })();
function toggleDirection() { function toggleDirection() {
setDirectionUp(prev => !prev); setDirectionUp(prev => !prev);
setDestination(undefined); resetField('destination');
} }
function handleSelectSource(newValue: ILibraryItem | undefined) { function handleSelectSource(newValue: ILibraryItem | undefined) {
setSource(newValue); setSource(newValue);
setDestination(undefined); resetField('destination');
setSelected([]); resetField('items');
} }
function handleSelectDestination(newValue: ILibraryItem | undefined) { function handleSelectDestination(newValue: ILibraryItem | undefined) {
setDestination(newValue); if (newValue) {
setSelected([]); setValue('destination', newValue.id);
} else {
resetField('destination');
}
resetField('items');
} }
function handleSubmit() { function onSubmit(data: ICstRelocateDTO) {
if (destination) { const positionsUnchanged = positions.every(item => {
onSubmit({ const operation = oss.operationByID.get(item.id)!;
destination: destination.id, return operation.position_x === item.position_x && operation.position_y === item.position_y;
items: selected });
}); if (positionsUnchanged) {
relocateConstituents(data);
} else {
updatePositions(
{
isSilent: true,
itemID: oss.id,
positions: positions
},
() => relocateConstituents(data)
);
} }
return true;
} }
return ( return (
@ -91,7 +124,7 @@ function DlgRelocateConstituents() {
header='Перенос конституент' header='Перенос конституент'
submitText='Переместить' submitText='Переместить'
canSubmit={isValid} canSubmit={isValid}
onSubmit={handleSubmit} onSubmit={event => void handleSubmit(onSubmit)(event)}
className={clsx('w-[40rem] h-[33rem]', 'py-3 px-6')} className={clsx('w-[40rem] h-[33rem]', 'py-3 px-6')}
helpTopic={HelpTopic.UI_RELOCATE_CST} helpTopic={HelpTopic.UI_RELOCATE_CST}
> >
@ -115,19 +148,25 @@ function DlgRelocateConstituents() {
className='w-1/2' className='w-1/2'
placeholder='Выберите целевую схему' placeholder='Выберите целевую схему'
items={destinationSchemas} items={destinationSchemas}
value={destination} value={destinationItem}
onChange={handleSelectDestination} onChange={handleSelectDestination}
/> />
</div> </div>
{sourceData.isLoading ? <Loader /> : null} {sourceData.isLoading ? <Loader /> : null}
{!sourceData.isLoading && sourceData.schema ? ( {!sourceData.isLoading && sourceData.schema ? (
<PickMultiConstituenta <Controller
noBorder name='items'
schema={sourceData.schema} control={control}
items={filteredConstituents} render={({ field }) => (
rows={12} <PickMultiConstituenta
value={selected} noBorder
onChange={setSelected} schema={sourceData.schema!}
items={filteredConstituents}
rows={12}
value={field.value}
onChange={field.onChange}
/>
)}
/> />
) : null} ) : null}
</div> </div>

View File

@ -19,8 +19,6 @@ import { IOperationPosition } from '../../backend/api';
import { useOperationCreate } from '../../backend/useOperationCreate'; import { useOperationCreate } from '../../backend/useOperationCreate';
import { useOperationUpdate } from '../../backend/useOperationUpdate'; import { useOperationUpdate } from '../../backend/useOperationUpdate';
import { useOssSuspense } from '../../backend/useOSS'; import { useOssSuspense } from '../../backend/useOSS';
import { useRelocateConstituents } from '../../backend/useRelocateConstituents';
import { useUpdatePositions } from '../../backend/useUpdatePositions';
import { IOperationSchema, OperationID, OperationType } from '../../models/oss'; import { IOperationSchema, OperationID, OperationType } from '../../models/oss';
import { calculateInsertPosition } from '../../models/ossAPI'; import { calculateInsertPosition } from '../../models/ossAPI';
@ -100,10 +98,8 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
const showCreateOperation = useDialogsStore(state => state.showCreateOperation); const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
const { deleteItem } = useDeleteItem(); const { deleteItem } = useDeleteItem();
const { updatePositions } = useUpdatePositions();
const { operationCreate } = useOperationCreate(); const { operationCreate } = useOperationCreate();
const { operationUpdate } = useOperationUpdate(); const { operationUpdate } = useOperationUpdate();
const { relocateConstituents } = useRelocateConstituents();
useEffect( useEffect(
() => () =>
@ -220,25 +216,7 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
showRelocateConstituents({ showRelocateConstituents({
oss: schema, oss: schema,
initialTarget: operation, initialTarget: operation,
onSubmit: data => { positions: positions
if (
positions.every(item => {
const operation = schema.operationByID.get(item.id)!;
return operation.position_x === item.position_x && operation.position_y === item.position_y;
})
) {
relocateConstituents({ itemID: schema.id, data });
} else {
updatePositions(
{
isSilent: true,
itemID: schema.id, //
positions: positions
},
() => relocateConstituents({ itemID: schema.id, data })
);
}
}
}); });
} }

View File

@ -19,7 +19,7 @@ import ToolbarGraphSelection from './ToolbarGraphSelection';
interface PickMultiConstituentaProps extends CProps.Styling { interface PickMultiConstituentaProps extends CProps.Styling {
id?: string; id?: string;
value: ConstituentaID[]; value: ConstituentaID[];
onChange: React.Dispatch<React.SetStateAction<ConstituentaID[]>>; onChange: (newValue: ConstituentaID[]) => void;
schema: IRSForm; schema: IRSForm;
items: IConstituenta[]; items: IConstituenta[];
@ -78,7 +78,7 @@ function PickMultiConstituenta({
newSelection.push(cst.id); newSelection.push(cst.id);
} }
}); });
onChange(prev => [...prev.filter(cst_id => !filtered.find(cst => cst.id === cst_id)), ...newSelection]); onChange([...value.filter(cst_id => !filtered.find(cst => cst.id === cst_id)), ...newSelection]);
} }
} }

View File

@ -25,7 +25,7 @@ function TabSource({ selected, receiver, setSelected }: TabSourceProps) {
items={sortedItems} items={sortedItems}
itemType={LibraryItemType.RSFORM} itemType={LibraryItemType.RSFORM}
rows={14} rows={14}
value={selected} value={selected ?? null}
onChange={setSelected} onChange={setSelected}
/> />
<div className='flex items-center gap-6 '> <div className='flex items-center gap-6 '>