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 successMessage: infoMsg.changesSaved
} }
}), }),
deleteBlock: ({ itemID, data }: { itemID: number; data: IDeleteBlockDTO }) => deleteBlock: ({ itemID, data }: { itemID: number; data: IDeleteBlockDTO; beforeUpdate?: () => void }) =>
axiosPatch<IDeleteBlockDTO, IOperationSchemaDTO>({ axiosPatch<IDeleteBlockDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema, schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/delete-block`, 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>({ axiosPatch<IDeleteReferenceDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema, schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/delete-reference`, endpoint: `/api/oss/${itemID}/delete-reference`,
@ -168,7 +168,7 @@ export const ossApi = {
successMessage: infoMsg.changesSaved successMessage: infoMsg.changesSaved
} }
}), }),
deleteOperation: ({ itemID, data }: { itemID: number; data: IDeleteOperationDTO }) => deleteOperation: ({ itemID, data }: { itemID: number; data: IDeleteOperationDTO; beforeUpdate?: () => void }) =>
axiosPatch<IDeleteOperationDTO, IOperationSchemaDTO>({ axiosPatch<IDeleteOperationDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema, schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/delete-operation`, 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 { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
import { KEYS } from '@/backend/configuration'; import { KEYS } from '@/backend/configuration';
import { PARAMETER } from '@/utils/constants';
import { ossApi } from './api'; import { ossApi } from './api';
import { type IDeleteBlockDTO } from './types'; import { type IDeleteBlockDTO } from './types';
@ -13,7 +14,11 @@ export const useDeleteBlock = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-block'], mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-block'],
mutationFn: ossApi.deleteBlock, 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); updateTimestamp(data.id, data.time_update);
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
await Promise.allSettled([ await Promise.allSettled([
@ -24,6 +29,8 @@ export const useDeleteBlock = () => {
onError: () => client.invalidateQueries() onError: () => client.invalidateQueries()
}); });
return { 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 { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
import { KEYS } from '@/backend/configuration'; import { KEYS } from '@/backend/configuration';
import { PARAMETER } from '@/utils/constants';
import { ossApi } from './api'; import { ossApi } from './api';
import { type IDeleteOperationDTO } from './types'; import { type IDeleteOperationDTO } from './types';
@ -13,7 +14,11 @@ export const useDeleteOperation = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-operation'], mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-operation'],
mutationFn: ossApi.deleteOperation, 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); updateTimestamp(data.id, data.time_update);
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
await Promise.allSettled([ await Promise.allSettled([
@ -24,6 +29,8 @@ export const useDeleteOperation = () => {
onError: () => client.invalidateQueries() onError: () => client.invalidateQueries()
}); });
return { 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 { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
import { KEYS } from '@/backend/configuration'; import { KEYS } from '@/backend/configuration';
import { PARAMETER } from '@/utils/constants';
import { ossApi } from './api'; import { ossApi } from './api';
import { type IDeleteReferenceDTO } from './types'; import { type IDeleteReferenceDTO } from './types';
@ -13,7 +14,11 @@ export const useDeleteReference = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-reference'], mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-reference'],
mutationFn: ossApi.deleteReference, 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); updateTimestamp(data.id, data.time_update);
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
await Promise.allSettled([ await Promise.allSettled([
@ -24,6 +29,8 @@ export const useDeleteReference = () => {
onError: () => client.invalidateQueries() onError: () => client.invalidateQueries()
}); });
return { 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; oss: IOperationSchema;
target: IOperationInput | IOperationSynthesis; target: IOperationInput | IOperationSynthesis;
layout: IOssLayout; layout: IOssLayout;
beforeDelete?: () => void;
} }
export function DlgDeleteOperation() { 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 { deleteOperation } = useDeleteOperation();
const { handleSubmit, control } = useForm<IDeleteOperationDTO>({ const { handleSubmit, control } = useForm<IDeleteOperationDTO>({
@ -34,7 +35,7 @@ export function DlgDeleteOperation() {
}); });
function onSubmit(data: IDeleteOperationDTO) { function onSubmit(data: IDeleteOperationDTO) {
return deleteOperation({ itemID: oss.id, data: data }); return deleteOperation({ itemID: oss.id, data: data, beforeUpdate: beforeDelete });
} }
return ( return (

View File

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

View File

@ -38,7 +38,7 @@ interface MenuOperationProps {
export function MenuOperation({ operation, onHide }: MenuOperationProps) { export function MenuOperation({ operation, onHide }: MenuOperationProps) {
const { items: libraryItems } = useLibrary(); const { items: libraryItems } = useLibrary();
const { schema, setSelected, navigateOperationSchema, isMutable, canDeleteOperation } = useOssEdit(); const { schema, setSelected, navigateOperationSchema, isMutable, canDeleteOperation, deselectAll } = useOssEdit();
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const getLayout = useGetLayout(); const getLayout = useGetLayout();
@ -115,7 +115,8 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
showDeleteReference({ showDeleteReference({
oss: schema, oss: schema,
target: operation, target: operation,
layout: getLayout() layout: getLayout(),
beforeDelete: deselectAll
}); });
break; break;
case OperationType.INPUT: case OperationType.INPUT:
@ -123,7 +124,8 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
showDeleteOperation({ showDeleteOperation({
oss: schema, oss: schema,
target: operation, 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 [containMovement, setContainMovement] = useState(false);
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]); const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]); const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const prevSelected = useRef<string[]>([]);
function onSelectionChange({ nodes }: { nodes: Node[] }) { function onSelectionChange({ nodes }: { nodes: Node[] }) {
const ids = nodes.map(node => node.id); const ids = nodes.map(node => node.id);
@ -39,19 +40,21 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
onChange: onSelectionChange onChange: onSelectionChange
}); });
const resetGraph = useCallback(() => { const reloadData = useCallback(() => {
const newNodes: Node[] = schema.hierarchy.topologicalOrder().map(nodeID => { const newNodes: Node[] = schema.hierarchy.topologicalOrder().map(nodeID => {
const item = schema.itemByNodeID.get(nodeID)!; const item = schema.itemByNodeID.get(nodeID)!;
if (item.nodeType === NodeType.BLOCK) { if (item.nodeType === NodeType.BLOCK) {
return { return {
id: nodeID, id: nodeID,
type: 'block', type: 'block',
selected: prevSelected.current.includes(item.nodeID),
data: { label: item.title, block: item }, data: { label: item.title, block: item },
position: computeRelativePosition(schema, { x: item.x, y: item.y }, item.parent), position: computeRelativePosition(schema, { x: item.x, y: item.y }, item.parent),
style: { style: {
width: item.width, width: item.width,
height: item.height height: item.height
}, },
parentId: item.parent ? constructNodeID(NodeType.BLOCK, item.parent) : undefined, parentId: item.parent ? constructNodeID(NodeType.BLOCK, item.parent) : undefined,
zIndex: Z_BLOCK zIndex: Z_BLOCK
}; };
@ -59,6 +62,7 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
return { return {
id: item.nodeID, id: item.nodeID,
type: item.operation_type.toString(), type: item.operation_type.toString(),
selected: prevSelected.current.includes(item.nodeID),
data: { label: item.alias, operation: item }, data: { label: item.alias, operation: item },
position: computeRelativePosition(schema, { x: item.x, y: item.y }, item.parent), position: computeRelativePosition(schema, { x: item.x, y: item.y }, item.parent),
parentId: item.parent ? constructNodeID(NodeType.BLOCK, item.parent) : undefined, parentId: item.parent ? constructNodeID(NodeType.BLOCK, item.parent) : undefined,
@ -83,19 +87,23 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
setNodes(newNodes); setNodes(newNodes);
setEdges(newEdges); setEdges(newEdges);
}, [schema, setNodes, setEdges, edgeAnimate, edgeStraight]);
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
}, [schema, setNodes, setEdges, edgeAnimate, edgeStraight, fitView]);
useEffect(() => { useEffect(() => {
resetGraph(); reloadData();
}, [schema, edgeAnimate, edgeStraight, resetGraph]); }, [schema, edgeAnimate, edgeStraight, reloadData]);
function resetView() { function resetView() {
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout); setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
} }
const prevSelected = useRef<string[]>([]); function resetGraph() {
setSelected([]);
prevSelected.current = [];
reloadData();
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
}
if ( if (
viewportInitialized && viewportInitialized &&
(prevSelected.current.length !== selected.length || prevSelected.current.some((id, i) => id !== selected[i])) (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() { export function OssFlow() {
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
const { navigateOperationSchema, schema, selected, setSelected, selectedItems, isMutable, canDeleteOperation } = const {
useOssEdit(); navigateOperationSchema,
schema,
selected,
setSelected,
selectedItems,
isMutable,
deselectAll,
canDeleteOperation
} = useOssEdit();
const { screenToFlowPosition } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
const { containMovement, nodes, onNodesChange, edges, onEdgesChange, resetGraph, resetView } = useOssFlow(); const { containMovement, nodes, onNodesChange, edges, onEdgesChange, resetGraph, resetView } = useOssFlow();
const store = useStoreApi(); const store = useStoreApi();
@ -157,7 +165,8 @@ export function OssFlow() {
showDeleteReference({ showDeleteReference({
oss: schema, oss: schema,
target: item, target: item,
layout: getLayout() layout: getLayout(),
beforeDelete: deselectAll
}); });
break; break;
case OperationType.INPUT: case OperationType.INPUT:
@ -165,14 +174,19 @@ export function OssFlow() {
showDeleteOperation({ showDeleteOperation({
oss: schema, oss: schema,
target: item, target: item,
layout: getLayout() layout: getLayout(),
beforeDelete: deselectAll
}); });
} }
} else { } else {
if (!window.confirm(promptText.deleteBlock)) { if (!window.confirm(promptText.deleteBlock)) {
return; 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
});
} }
} }