F: Improve Graph selection and View retention

This commit is contained in:
Ivan 2025-08-05 16:54:52 +03:00
parent 935cd42306
commit 54073dbbed
9 changed files with 75 additions and 28 deletions

View File

@ -79,7 +79,7 @@ export const ossApi = {
successMessage: infoMsg.changesSaved
}
}),
deleteBlock: ({ itemID, data }: { itemID: number; data: IDeleteBlockDTO }) =>
deleteBlock: ({ itemID, data }: { itemID: number; data: IDeleteBlockDTO; beforeUpdate?: () => void }) =>
axiosPatch<IDeleteBlockDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/delete-block`,
@ -101,7 +101,7 @@ export const ossApi = {
}
}
}),
deleteReference: ({ itemID, data }: { itemID: number; data: IDeleteReferenceDTO }) =>
deleteReference: ({ itemID, data }: { itemID: number; data: IDeleteReferenceDTO; beforeUpdate?: () => void }) =>
axiosPatch<IDeleteReferenceDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/delete-reference`,
@ -168,7 +168,7 @@ export const ossApi = {
successMessage: infoMsg.changesSaved
}
}),
deleteOperation: ({ itemID, data }: { itemID: number; data: IDeleteOperationDTO }) =>
deleteOperation: ({ itemID, data }: { itemID: number; data: IDeleteOperationDTO; beforeUpdate?: () => void }) =>
axiosPatch<IDeleteOperationDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/delete-operation`,

View File

@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
import { KEYS } from '@/backend/configuration';
import { PARAMETER } from '@/utils/constants';
import { ossApi } from './api';
import { type IDeleteBlockDTO } from './types';
@ -13,7 +14,11 @@ export const useDeleteBlock = () => {
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-block'],
mutationFn: ossApi.deleteBlock,
onSuccess: async data => {
onSuccess: async (data, variables) => {
if (variables.beforeUpdate) {
variables.beforeUpdate();
await new Promise(resolve => setTimeout(resolve, PARAMETER.minimalTimeout));
}
updateTimestamp(data.id, data.time_update);
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
await Promise.allSettled([
@ -24,6 +29,8 @@ export const useDeleteBlock = () => {
onError: () => client.invalidateQueries()
});
return {
deleteBlock: (data: { itemID: number; data: IDeleteBlockDTO }) => mutation.mutateAsync(data)
deleteBlock: (data: { itemID: number; data: IDeleteBlockDTO; beforeUpdate?: () => void }) => {
mutation.mutate(data);
}
};
};

View File

@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
import { KEYS } from '@/backend/configuration';
import { PARAMETER } from '@/utils/constants';
import { ossApi } from './api';
import { type IDeleteOperationDTO } from './types';
@ -13,7 +14,11 @@ export const useDeleteOperation = () => {
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-operation'],
mutationFn: ossApi.deleteOperation,
onSuccess: async data => {
onSuccess: async (data, variables) => {
if (variables.beforeUpdate) {
variables.beforeUpdate();
await new Promise(resolve => setTimeout(resolve, PARAMETER.minimalTimeout));
}
updateTimestamp(data.id, data.time_update);
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
await Promise.allSettled([
@ -24,6 +29,8 @@ export const useDeleteOperation = () => {
onError: () => client.invalidateQueries()
});
return {
deleteOperation: (data: { itemID: number; data: IDeleteOperationDTO }) => mutation.mutateAsync(data)
deleteOperation: (data: { itemID: number; data: IDeleteOperationDTO; beforeUpdate?: () => void }) => {
mutation.mutate(data);
}
};
};

View File

@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
import { KEYS } from '@/backend/configuration';
import { PARAMETER } from '@/utils/constants';
import { ossApi } from './api';
import { type IDeleteReferenceDTO } from './types';
@ -13,7 +14,11 @@ export const useDeleteReference = () => {
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-reference'],
mutationFn: ossApi.deleteReference,
onSuccess: async data => {
onSuccess: async (data, variables) => {
if (variables.beforeUpdate) {
variables.beforeUpdate();
await new Promise(resolve => setTimeout(resolve, PARAMETER.minimalTimeout));
}
updateTimestamp(data.id, data.time_update);
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
await Promise.allSettled([
@ -24,6 +29,8 @@ export const useDeleteReference = () => {
onError: () => client.invalidateQueries()
});
return {
deleteReference: (data: { itemID: number; data: IDeleteReferenceDTO }) => mutation.mutateAsync(data)
deleteReference: (data: { itemID: number; data: IDeleteReferenceDTO; beforeUpdate?: () => void }) => {
mutation.mutate(data);
}
};
};

View File

@ -17,10 +17,11 @@ export interface DlgDeleteOperationProps {
oss: IOperationSchema;
target: IOperationInput | IOperationSynthesis;
layout: IOssLayout;
beforeDelete?: () => void;
}
export function DlgDeleteOperation() {
const { oss, target, layout } = useDialogsStore(state => state.props as DlgDeleteOperationProps);
const { oss, target, layout, beforeDelete } = useDialogsStore(state => state.props as DlgDeleteOperationProps);
const { deleteOperation } = useDeleteOperation();
const { handleSubmit, control } = useForm<IDeleteOperationDTO>({
@ -34,7 +35,7 @@ export function DlgDeleteOperation() {
});
function onSubmit(data: IDeleteOperationDTO) {
return deleteOperation({ itemID: oss.id, data: data });
return deleteOperation({ itemID: oss.id, data: data, beforeUpdate: beforeDelete });
}
return (

View File

@ -17,10 +17,11 @@ export interface DlgDeleteReferenceProps {
oss: IOperationSchema;
target: IOperationReference;
layout: IOssLayout;
beforeDelete?: () => void;
}
export function DlgDeleteReference() {
const { oss, target, layout } = useDialogsStore(state => state.props as DlgDeleteReferenceProps);
const { oss, target, layout, beforeDelete } = useDialogsStore(state => state.props as DlgDeleteReferenceProps);
const { deleteReference } = useDeleteReference();
const { handleSubmit, control } = useForm<IDeleteReferenceDTO>({
@ -35,7 +36,7 @@ export function DlgDeleteReference() {
const keep_connections = useWatch({ control, name: 'keep_connections' });
function onSubmit(data: IDeleteReferenceDTO) {
return deleteReference({ itemID: oss.id, data: data });
return deleteReference({ itemID: oss.id, data: data, beforeUpdate: beforeDelete });
}
return (

View File

@ -38,7 +38,7 @@ interface MenuOperationProps {
export function MenuOperation({ operation, onHide }: MenuOperationProps) {
const { items: libraryItems } = useLibrary();
const { schema, setSelected, navigateOperationSchema, isMutable, canDeleteOperation } = useOssEdit();
const { schema, setSelected, navigateOperationSchema, isMutable, canDeleteOperation, deselectAll } = useOssEdit();
const isProcessing = useMutatingOss();
const getLayout = useGetLayout();
@ -115,7 +115,8 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
showDeleteReference({
oss: schema,
target: operation,
layout: getLayout()
layout: getLayout(),
beforeDelete: deselectAll
});
break;
case OperationType.INPUT:
@ -123,7 +124,8 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
showDeleteOperation({
oss: schema,
target: operation,
layout: getLayout()
layout: getLayout(),
beforeDelete: deselectAll
});
}
}

View File

@ -26,6 +26,7 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
const [containMovement, setContainMovement] = useState(false);
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const prevSelected = useRef<string[]>([]);
function onSelectionChange({ nodes }: { nodes: Node[] }) {
const ids = nodes.map(node => node.id);
@ -39,19 +40,21 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
onChange: onSelectionChange
});
const resetGraph = useCallback(() => {
const reloadData = useCallback(() => {
const newNodes: Node[] = schema.hierarchy.topologicalOrder().map(nodeID => {
const item = schema.itemByNodeID.get(nodeID)!;
if (item.nodeType === NodeType.BLOCK) {
return {
id: nodeID,
type: 'block',
selected: prevSelected.current.includes(item.nodeID),
data: { label: item.title, block: item },
position: computeRelativePosition(schema, { x: item.x, y: item.y }, item.parent),
style: {
width: item.width,
height: item.height
},
parentId: item.parent ? constructNodeID(NodeType.BLOCK, item.parent) : undefined,
zIndex: Z_BLOCK
};
@ -59,6 +62,7 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
return {
id: item.nodeID,
type: item.operation_type.toString(),
selected: prevSelected.current.includes(item.nodeID),
data: { label: item.alias, operation: item },
position: computeRelativePosition(schema, { x: item.x, y: item.y }, item.parent),
parentId: item.parent ? constructNodeID(NodeType.BLOCK, item.parent) : undefined,
@ -83,19 +87,23 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
setNodes(newNodes);
setEdges(newEdges);
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
}, [schema, setNodes, setEdges, edgeAnimate, edgeStraight, fitView]);
}, [schema, setNodes, setEdges, edgeAnimate, edgeStraight]);
useEffect(() => {
resetGraph();
}, [schema, edgeAnimate, edgeStraight, resetGraph]);
reloadData();
}, [schema, edgeAnimate, edgeStraight, reloadData]);
function resetView() {
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
}
const prevSelected = useRef<string[]>([]);
function resetGraph() {
setSelected([]);
prevSelected.current = [];
reloadData();
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
}
if (
viewportInitialized &&
(prevSelected.current.length !== selected.length || prevSelected.current.some((id, i) => id !== selected[i]))

View File

@ -46,8 +46,16 @@ export const flowOptions = {
export function OssFlow() {
const mainHeight = useMainHeight();
const { navigateOperationSchema, schema, selected, setSelected, selectedItems, isMutable, canDeleteOperation } =
useOssEdit();
const {
navigateOperationSchema,
schema,
selected,
setSelected,
selectedItems,
isMutable,
deselectAll,
canDeleteOperation
} = useOssEdit();
const { screenToFlowPosition } = useReactFlow();
const { containMovement, nodes, onNodesChange, edges, onEdgesChange, resetGraph, resetView } = useOssFlow();
const store = useStoreApi();
@ -157,7 +165,8 @@ export function OssFlow() {
showDeleteReference({
oss: schema,
target: item,
layout: getLayout()
layout: getLayout(),
beforeDelete: deselectAll
});
break;
case OperationType.INPUT:
@ -165,14 +174,19 @@ export function OssFlow() {
showDeleteOperation({
oss: schema,
target: item,
layout: getLayout()
layout: getLayout(),
beforeDelete: deselectAll
});
}
} else {
if (!window.confirm(promptText.deleteBlock)) {
return;
}
void deleteBlock({ itemID: schema.id, data: { target: item.id, layout: getLayout() } });
void deleteBlock({
itemID: schema.id,
data: { target: item.id, layout: getLayout() },
beforeUpdate: deselectAll
});
}
}