R: Restructuring layout data pt2

This commit is contained in:
Ivan 2025-04-06 15:47:40 +03:00
parent 3271d9244c
commit 5efce874b2
27 changed files with 161 additions and 174 deletions

View File

@ -172,7 +172,7 @@ class SetOperationInputSerializer(serializers.Serializer):
class OperationSchemaSerializer(serializers.ModelSerializer): class OperationSchemaSerializer(serializers.ModelSerializer):
''' Serializer: Detailed data for OSS. ''' ''' Serializer: Detailed data for OSS. '''
items = serializers.ListField( operations = serializers.ListField(
child=OperationSerializer() child=OperationSerializer()
) )
arguments = serializers.ListField( arguments = serializers.ListField(
@ -193,9 +193,9 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
del result['versions'] del result['versions']
oss = OperationSchema(instance) oss = OperationSchema(instance)
result['layout'] = oss.layout().data result['layout'] = oss.layout().data
result['items'] = [] result['operations'] = []
for operation in oss.operations().order_by('pk'): for operation in oss.operations().order_by('pk'):
result['items'].append(OperationSerializer(operation).data) result['operations'].append(OperationSerializer(operation).data)
result['arguments'] = [] result['arguments'] = []
for argument in oss.arguments().order_by('order'): for argument in oss.arguments().order_by('order'):
result['arguments'].append(ArgumentSerializer(argument).data) result['arguments'].append(ArgumentSerializer(argument).data)

View File

@ -98,7 +98,7 @@ class TestOssOperations(EndpointTester):
self.executeNotFound(data=data, item=self.invalid_id) self.executeNotFound(data=data, item=self.invalid_id)
response = self.executeCreated(data=data, item=self.owned_id) response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(len(response.data['oss']['items']), 4) self.assertEqual(len(response.data['oss']['operations']), 4)
new_operation = response.data['new_operation'] new_operation = response.data['new_operation']
layout = response.data['oss']['layout'] layout = response.data['oss']['layout']
item = [item for item in layout['operations'] if item['id'] == new_operation['id']][0] item = [item for item in layout['operations'] if item['id'] == new_operation['id']][0]
@ -210,9 +210,9 @@ class TestOssOperations(EndpointTester):
self.login() self.login()
response = self.executeOK(data=data) response = self.executeOK(data=data)
layout = response.data['layout'] layout = response.data['layout']
items = [item for item in layout['operations'] if item['id'] == data['target']] deleted_items = [item for item in layout['operations'] if item['id'] == data['target']]
self.assertEqual(len(response.data['items']), 2) self.assertEqual(len(response.data['operations']), 2)
self.assertEqual(len(items), 0) self.assertEqual(len(deleted_items), 0)
@decl_endpoint('/api/oss/{item}/create-input', method='patch') @decl_endpoint('/api/oss/{item}/create-input', method='patch')
def test_create_input(self): def test_create_input(self):

View File

@ -82,9 +82,9 @@ class TestOssViewset(EndpointTester):
self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA) self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA)
self.assertEqual(len(response.data['items']), 3) self.assertEqual(len(response.data['operations']), 3)
self.assertEqual(response.data['items'][0]['id'], self.operation1.pk) self.assertEqual(response.data['operations'][0]['id'], self.operation1.pk)
self.assertEqual(response.data['items'][0]['operation_type'], self.operation1.operation_type) self.assertEqual(response.data['operations'][0]['operation_type'], self.operation1.operation_type)
self.assertEqual(len(response.data['substitutions']), 1) self.assertEqual(len(response.data['substitutions']), 1)
sub = response.data['substitutions'][0] sub = response.data['substitutions'][0]
@ -125,11 +125,13 @@ class TestOssViewset(EndpointTester):
data = {'operations': [], 'blocks': []} data = {'operations': [], 'blocks': []}
self.executeOK(data=data) self.executeOK(data=data)
data = {'operations': [ data = {
{'id': self.operation1.pk, 'x': 42.1, 'y': 1337}, 'operations': [
{'id': self.operation2.pk, 'x': 36.1, 'y': 1437}, {'id': self.operation1.pk, 'x': 42.1, 'y': 1337},
{'id': self.operation3.pk, 'x': 36.1, 'y': 1435} {'id': self.operation2.pk, 'x': 36.1, 'y': 1437},
], 'blocks': []} {'id': self.operation3.pk, 'x': 36.1, 'y': 1435}
], 'blocks': []
}
self.toggle_admin(True) self.toggle_admin(True)
self.executeOK(data=data, item=self.unowned_id) self.executeOK(data=data, item=self.unowned_id)

View File

@ -20,7 +20,7 @@ export const useSetAccessPolicy = () => {
client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy }); client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy });
return Promise.allSettled([ return Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }), client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
...ossData.items ...ossData.operations
.map(item => { .map(item => {
if (!item.result) { if (!item.result) {
return; return;

View File

@ -18,7 +18,7 @@ export const useSetEditors = () => {
if (ossData) { if (ossData) {
client.setQueryData(ossKey, { ...ossData, editors: variables.editors }); client.setQueryData(ossKey, { ...ossData, editors: variables.editors });
return Promise.allSettled( return Promise.allSettled(
ossData.items ossData.operations
.map(item => { .map(item => {
if (!item.result) { if (!item.result) {
return; return;

View File

@ -20,7 +20,7 @@ export const useSetLocation = () => {
client.setQueryData(ossKey, { ...ossData, location: variables.location }); client.setQueryData(ossKey, { ...ossData, location: variables.location });
return Promise.allSettled([ return Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }), client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
...ossData.items ...ossData.operations
.map(item => { .map(item => {
if (!item.result) { if (!item.result) {
return; return;

View File

@ -20,7 +20,7 @@ export const useSetOwner = () => {
client.setQueryData(ossKey, { ...ossData, owner: variables.owner }); client.setQueryData(ossKey, { ...ossData, owner: variables.owner });
return Promise.allSettled([ return Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }), client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
...ossData.items ...ossData.operations
.map(item => { .map(item => {
if (!item.result) { if (!item.result) {
return; return;

View File

@ -12,9 +12,9 @@ import {
type IOperationCreatedResponse, type IOperationCreatedResponse,
type IOperationCreateDTO, type IOperationCreateDTO,
type IOperationDeleteDTO, type IOperationDeleteDTO,
type IOperationPosition,
type IOperationSchemaDTO, type IOperationSchemaDTO,
type IOperationUpdateDTO, type IOperationUpdateDTO,
type IOssLayout,
type ITargetOperation, type ITargetOperation,
schemaConstituentaReference, schemaConstituentaReference,
schemaOperationCreatedResponse, schemaOperationCreatedResponse,
@ -39,19 +39,11 @@ export const ossApi = {
}); });
}, },
updateLayout: ({ updateLayout: ({ itemID, data, isSilent }: { itemID: number; data: IOssLayout; isSilent?: boolean }) =>
itemID,
positions,
isSilent
}: {
itemID: number;
positions: IOperationPosition[];
isSilent?: boolean;
}) =>
axiosPatch({ axiosPatch({
endpoint: `/api/oss/${itemID}/update-layout`, endpoint: `/api/oss/${itemID}/update-layout`,
request: { request: {
data: { positions: positions }, data: data,
successMessage: isSilent ? undefined : infoMsg.changesSaved successMessage: isSilent ? undefined : infoMsg.changesSaved
} }
}), }),

View File

@ -41,7 +41,7 @@ export class OssLoader {
} }
private prepareLookups() { private prepareLookups() {
this.oss.items.forEach(operation => { this.oss.operations.forEach(operation => {
this.operationByID.set(operation.id, operation); this.operationByID.set(operation.id, operation);
this.graph.addNode(operation.id); this.graph.addNode(operation.id);
}); });
@ -52,13 +52,16 @@ export class OssLoader {
} }
private extractSchemas() { private extractSchemas() {
this.schemaIDs = this.oss.items.map(operation => operation.result).filter(item => item !== null); this.schemaIDs = this.oss.operations.map(operation => operation.result).filter(item => item !== null);
} }
private inferOperationAttributes() { private inferOperationAttributes() {
this.graph.topologicalOrder().forEach(operationID => { this.graph.topologicalOrder().forEach(operationID => {
const operation = this.operationByID.get(operationID)!; const operation = this.operationByID.get(operationID)!;
const schema = this.items.find(item => item.id === operation.result); const schema = this.items.find(item => item.id === operation.result);
const position = this.oss.layout.operations.find(item => item.id === operationID);
operation.x = position?.x ?? 0;
operation.y = position?.y ?? 0;
operation.is_consolidation = this.inferConsolidation(operationID); operation.is_consolidation = this.inferConsolidation(operationID);
operation.is_owned = !schema || (schema.owner === this.oss.owner && schema.location === this.oss.location); operation.is_owned = !schema || (schema.owner === this.oss.owner && schema.location === this.oss.location);
operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID); operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID);
@ -82,7 +85,7 @@ export class OssLoader {
} }
private calculateStats(): IOperationSchemaStats { private calculateStats(): IOperationSchemaStats {
const items = this.oss.items; const items = this.oss.operations;
return { return {
count_operations: items.length, count_operations: items.length,
count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length, count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length,

View File

@ -23,8 +23,8 @@ export type IOperationDTO = z.infer<typeof schemaOperation>;
/** Represents backend data for {@link IOperationSchema}. */ /** Represents backend data for {@link IOperationSchema}. */
export type IOperationSchemaDTO = z.infer<typeof schemaOperationSchema>; export type IOperationSchemaDTO = z.infer<typeof schemaOperationSchema>;
/** Represents {@link IOperation} position. */ /** Represents {@link schemaOperation} layout. */
export type IOperationPosition = z.infer<typeof schemaOperationPosition>; export type IOssLayout = z.infer<typeof schemaOssLayout>;
/** Represents {@link IOperation} data, used in creation process. */ /** Represents {@link IOperation} data, used in creation process. */
export type IOperationCreateDTO = z.infer<typeof schemaOperationCreate>; export type IOperationCreateDTO = z.infer<typeof schemaOperationCreate>;
@ -35,7 +35,7 @@ export type IOperationCreatedResponse = z.infer<typeof schemaOperationCreatedRes
* Represents target {@link IOperation}. * Represents target {@link IOperation}.
*/ */
export interface ITargetOperation { export interface ITargetOperation {
positions: IOperationPosition[]; layout: IOssLayout;
target: number; target: number;
} }
@ -69,9 +69,7 @@ export const schemaOperation = z.strictObject({
title: z.string(), title: z.string(),
description: z.string(), description: z.string(),
position_x: z.number(), parent: z.number().nullable(),
position_y: z.number(),
result: z.number().nullable() result: z.number().nullable()
}); });
@ -83,9 +81,21 @@ export const schemaCstSubstituteInfo = schemaCstSubstitute.extend({
substitution_term: z.string() substitution_term: z.string()
}); });
export const schemaPosition = z.strictObject({
id: z.number(),
x: z.number(),
y: z.number()
});
export const schemaOssLayout = z.strictObject({
operations: z.array(schemaPosition),
blocks: z.array(schemaPosition)
});
export const schemaOperationSchema = schemaLibraryItem.extend({ export const schemaOperationSchema = schemaLibraryItem.extend({
editors: z.number().array(), editors: z.number().array(),
items: z.array(schemaOperation), operations: z.array(schemaOperation),
layout: schemaOssLayout,
arguments: z arguments: z
.object({ .object({
operation: z.number(), operation: z.number(),
@ -95,23 +105,18 @@ export const schemaOperationSchema = schemaLibraryItem.extend({
substitutions: z.array(schemaCstSubstituteInfo) substitutions: z.array(schemaCstSubstituteInfo)
}); });
export const schemaOperationPosition = z.strictObject({
id: z.number(),
position_x: z.number(),
position_y: z.number()
});
export const schemaOperationCreate = z.strictObject({ export const schemaOperationCreate = z.strictObject({
positions: z.array(schemaOperationPosition), layout: schemaOssLayout,
item_data: z.strictObject({ item_data: z.strictObject({
alias: z.string().nonempty(), alias: z.string().nonempty(),
operation_type: schemaOperationType, operation_type: schemaOperationType,
title: z.string(), title: z.string(),
description: z.string(), description: z.string(),
position_x: z.number(), parent: z.number().nullable(),
position_y: z.number(),
result: z.number().nullable() result: z.number().nullable()
}), }),
position_x: z.number(),
position_y: z.number(),
arguments: z.array(z.number()), arguments: z.array(z.number()),
create_schema: z.boolean() create_schema: z.boolean()
}); });
@ -123,14 +128,14 @@ export const schemaOperationCreatedResponse = z.strictObject({
export const schemaOperationDelete = z.strictObject({ export const schemaOperationDelete = z.strictObject({
target: z.number(), target: z.number(),
positions: z.array(schemaOperationPosition), layout: schemaOssLayout,
keep_constituents: z.boolean(), keep_constituents: z.boolean(),
delete_schema: z.boolean() delete_schema: z.boolean()
}); });
export const schemaInputUpdate = z.strictObject({ export const schemaInputUpdate = z.strictObject({
target: z.number(), target: z.number(),
positions: z.array(schemaOperationPosition), layout: schemaOssLayout,
input: z.number().nullable() input: z.number().nullable()
}); });
@ -141,7 +146,7 @@ export const schemaInputCreatedResponse = z.strictObject({
export const schemaOperationUpdate = z.strictObject({ export const schemaOperationUpdate = z.strictObject({
target: z.number(), target: z.number(),
positions: z.array(schemaOperationPosition), layout: schemaOssLayout,
item_data: z.strictObject({ item_data: z.strictObject({
alias: z.string().nonempty(errorMsg.requiredField), alias: z.string().nonempty(errorMsg.requiredField),
title: z.string(), title: z.string(),

View File

@ -14,7 +14,7 @@ export const useOperationUpdate = () => {
mutationFn: ossApi.operationUpdate, mutationFn: ossApi.operationUpdate,
onSuccess: (data, variables) => { onSuccess: (data, variables) => {
client.setQueryData(KEYS.composite.ossItem({ itemID: data.id }), data); client.setQueryData(KEYS.composite.ossItem({ itemID: data.id }), data);
const schemaID = data.items.find(item => item.id === variables.data.target)?.result; const schemaID = data.operations.find(item => item.id === variables.data.target)?.result;
if (!schemaID) { if (!schemaID) {
return; return;
} }

View File

@ -5,7 +5,7 @@ import { useUpdateTimestamp } from '@/features/library/backend/use-update-timest
import { KEYS } from '@/backend/configuration'; import { KEYS } from '@/backend/configuration';
import { ossApi } from './api'; import { ossApi } from './api';
import { type IOperationPosition } from './types'; import { type IOperationSchemaDTO, type IOssLayout } from './types';
export const useUpdateLayout = () => { export const useUpdateLayout = () => {
const client = useQueryClient(); const client = useQueryClient();
@ -13,13 +13,25 @@ export const useUpdateLayout = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-layout'], mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-layout'],
mutationFn: ossApi.updateLayout, mutationFn: ossApi.updateLayout,
onSuccess: (_, variables) => updateTimestamp(variables.itemID), onSuccess: (_, variables) => {
updateTimestamp(variables.itemID);
client.setQueryData(
ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey,
(prev: IOperationSchemaDTO | undefined) =>
!prev
? prev
: {
...prev,
layout: variables.data
}
);
},
onError: () => client.invalidateQueries() onError: () => client.invalidateQueries()
}); });
return { return {
updateLayout: (data: { updateLayout: (data: {
itemID: number; // itemID: number; //
positions: IOperationPosition[]; data: IOssLayout;
isSilent?: boolean; isSilent?: boolean;
}) => mutation.mutateAsync(data) }) => mutation.mutateAsync(data)
}; };

View File

@ -13,7 +13,7 @@ import { Label } from '@/components/input';
import { ModalForm } from '@/components/modal'; import { ModalForm } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { type IInputUpdateDTO, type IOperationPosition, schemaInputUpdate } from '../backend/types'; import { type IInputUpdateDTO, type IOssLayout, schemaInputUpdate } from '../backend/types';
import { useInputUpdate } from '../backend/use-input-update'; import { useInputUpdate } from '../backend/use-input-update';
import { type IOperation, type IOperationSchema } from '../models/oss'; import { type IOperation, type IOperationSchema } from '../models/oss';
import { sortItemsForOSS } from '../models/oss-api'; import { sortItemsForOSS } from '../models/oss-api';
@ -21,18 +21,18 @@ import { sortItemsForOSS } from '../models/oss-api';
export interface DlgChangeInputSchemaProps { export interface DlgChangeInputSchemaProps {
oss: IOperationSchema; oss: IOperationSchema;
target: IOperation; target: IOperation;
positions: IOperationPosition[]; layout: IOssLayout;
} }
export function DlgChangeInputSchema() { export function DlgChangeInputSchema() {
const { oss, target, positions } = useDialogsStore(state => state.props as DlgChangeInputSchemaProps); const { oss, target, layout } = useDialogsStore(state => state.props as DlgChangeInputSchemaProps);
const { inputUpdate } = useInputUpdate(); const { inputUpdate } = useInputUpdate();
const { setValue, handleSubmit, control } = useForm<IInputUpdateDTO>({ const { setValue, handleSubmit, control } = useForm<IInputUpdateDTO>({
resolver: zodResolver(schemaInputUpdate), resolver: zodResolver(schemaInputUpdate),
defaultValues: { defaultValues: {
target: target.id, target: target.id,
positions: positions, layout: layout,
input: target.result input: target.result
} }
}); });

View File

@ -10,12 +10,7 @@ import { ModalForm } from '@/components/modal';
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs'; import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { import { type IOperationCreateDTO, type IOssLayout, OperationType, schemaOperationCreate } from '../../backend/types';
type IOperationCreateDTO,
type IOperationPosition,
OperationType,
schemaOperationCreate
} from '../../backend/types';
import { useOperationCreate } from '../../backend/use-operation-create'; import { useOperationCreate } from '../../backend/use-operation-create';
import { describeOperationType, labelOperationType } from '../../labels'; import { describeOperationType, labelOperationType } from '../../labels';
import { type IOperationSchema } from '../../models/oss'; import { type IOperationSchema } from '../../models/oss';
@ -26,7 +21,7 @@ import { TabSynthesisOperation } from './tab-synthesis-operation';
export interface DlgCreateOperationProps { export interface DlgCreateOperationProps {
oss: IOperationSchema; oss: IOperationSchema;
positions: IOperationPosition[]; layout: IOssLayout;
initialInputs: number[]; initialInputs: number[];
defaultX: number; defaultX: number;
defaultY: number; defaultY: number;
@ -42,7 +37,7 @@ export type TabID = (typeof TabID)[keyof typeof TabID];
export function DlgCreateOperation() { export function DlgCreateOperation() {
const { operationCreate } = useOperationCreate(); const { operationCreate } = useOperationCreate();
const { oss, positions, initialInputs, onCreate, defaultX, defaultY } = useDialogsStore( const { oss, layout, initialInputs, onCreate, defaultX, defaultY } = useDialogsStore(
state => state.props as DlgCreateOperationProps state => state.props as DlgCreateOperationProps
); );
@ -51,30 +46,31 @@ export function DlgCreateOperation() {
defaultValues: { defaultValues: {
item_data: { item_data: {
operation_type: initialInputs.length === 0 ? OperationType.INPUT : OperationType.SYNTHESIS, operation_type: initialInputs.length === 0 ? OperationType.INPUT : OperationType.SYNTHESIS,
result: null,
position_x: defaultX,
position_y: defaultY,
alias: '', alias: '',
title: '', title: '',
description: '' description: '',
result: null,
parent: null
}, },
position_x: defaultX,
position_y: defaultY,
arguments: initialInputs, arguments: initialInputs,
create_schema: false, create_schema: false,
positions: positions layout: layout
}, },
mode: 'onChange' mode: 'onChange'
}); });
const alias = useWatch({ control: methods.control, name: 'item_data.alias' }); const alias = useWatch({ control: methods.control, name: 'item_data.alias' });
const [activeTab, setActiveTab] = useState(initialInputs.length === 0 ? TabID.INPUT : TabID.SYNTHESIS); const [activeTab, setActiveTab] = useState(initialInputs.length === 0 ? TabID.INPUT : TabID.SYNTHESIS);
const isValid = !!alias && !oss.items.some(operation => operation.alias === alias); const isValid = !!alias && !oss.operations.some(operation => operation.alias === alias);
function onSubmit(data: IOperationCreateDTO) { function onSubmit(data: IOperationCreateDTO) {
const target = calculateInsertPosition(oss, data.arguments, positions, { const target = calculateInsertPosition(oss, data.arguments, layout, {
x: defaultX, x: defaultX,
y: defaultY y: defaultY
}); });
data.item_data.position_x = target.x; data.position_x = target.x;
data.item_data.position_y = target.y; data.position_y = target.y;
void operationCreate({ itemID: oss.id, data: data }).then(response => onCreate?.(response.new_operation.id)); void operationCreate({ itemID: oss.id, data: data }).then(response => onCreate?.(response.new_operation.id));
} }

View File

@ -50,7 +50,7 @@ export function TabSynthesisOperation() {
name='arguments' name='arguments'
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<PickMultiOperation items={oss.items} value={field.value} onChange={field.onChange} rows={6} /> <PickMultiOperation items={oss.operations} value={field.value} onChange={field.onChange} rows={6} />
)} )}
/> />
</div> </div>

View File

@ -9,25 +9,25 @@ import { Checkbox, TextInput } from '@/components/input';
import { ModalForm } from '@/components/modal'; import { ModalForm } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { type IOperationDeleteDTO, type IOperationPosition, schemaOperationDelete } from '../backend/types'; import { type IOperationDeleteDTO, type IOssLayout, schemaOperationDelete } from '../backend/types';
import { useOperationDelete } from '../backend/use-operation-delete'; import { useOperationDelete } from '../backend/use-operation-delete';
import { type IOperation, type IOperationSchema } from '../models/oss'; import { type IOperation, type IOperationSchema } from '../models/oss';
export interface DlgDeleteOperationProps { export interface DlgDeleteOperationProps {
oss: IOperationSchema; oss: IOperationSchema;
target: IOperation; target: IOperation;
positions: IOperationPosition[]; layout: IOssLayout;
} }
export function DlgDeleteOperation() { export function DlgDeleteOperation() {
const { oss, target, positions } = useDialogsStore(state => state.props as DlgDeleteOperationProps); const { oss, target, layout } = useDialogsStore(state => state.props as DlgDeleteOperationProps);
const { operationDelete } = useOperationDelete(); const { operationDelete } = useOperationDelete();
const { handleSubmit, control } = useForm<IOperationDeleteDTO>({ const { handleSubmit, control } = useForm<IOperationDeleteDTO>({
resolver: zodResolver(schemaOperationDelete), resolver: zodResolver(schemaOperationDelete),
defaultValues: { defaultValues: {
target: target.id, target: target.id,
positions: positions, layout: layout,
keep_constituents: false, keep_constituents: false,
delete_schema: false delete_schema: false
} }

View File

@ -11,12 +11,7 @@ import { ModalForm } from '@/components/modal';
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs'; import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { import { type IOperationUpdateDTO, type IOssLayout, OperationType, schemaOperationUpdate } from '../../backend/types';
type IOperationPosition,
type IOperationUpdateDTO,
OperationType,
schemaOperationUpdate
} from '../../backend/types';
import { useOperationUpdate } from '../../backend/use-operation-update'; import { useOperationUpdate } from '../../backend/use-operation-update';
import { type IOperation, type IOperationSchema } from '../../models/oss'; import { type IOperation, type IOperationSchema } from '../../models/oss';
@ -27,7 +22,7 @@ import { TabSynthesis } from './tab-synthesis';
export interface DlgEditOperationProps { export interface DlgEditOperationProps {
oss: IOperationSchema; oss: IOperationSchema;
target: IOperation; target: IOperation;
positions: IOperationPosition[]; layout: IOssLayout;
} }
export const TabID = { export const TabID = {
@ -38,7 +33,7 @@ export const TabID = {
export type TabID = (typeof TabID)[keyof typeof TabID]; export type TabID = (typeof TabID)[keyof typeof TabID];
export function DlgEditOperation() { export function DlgEditOperation() {
const { oss, target, positions } = useDialogsStore(state => state.props as DlgEditOperationProps); const { oss, target, layout } = useDialogsStore(state => state.props as DlgEditOperationProps);
const { operationUpdate } = useOperationUpdate(); const { operationUpdate } = useOperationUpdate();
const methods = useForm<IOperationUpdateDTO>({ const methods = useForm<IOperationUpdateDTO>({
@ -55,7 +50,7 @@ export function DlgEditOperation() {
original: sub.original, original: sub.original,
substitution: sub.substitution substitution: sub.substitution
})), })),
positions: positions layout: layout
}, },
mode: 'onChange' mode: 'onChange'
}); });

View File

@ -13,7 +13,7 @@ export function TabArguments() {
const { control, setValue } = useFormContext<IOperationUpdateDTO>(); const { control, setValue } = useFormContext<IOperationUpdateDTO>();
const { oss, target } = useDialogsStore(state => state.props as DlgEditOperationProps); const { oss, target } = useDialogsStore(state => state.props as DlgEditOperationProps);
const potentialCycle = [target.id, ...oss.graph.expandAllOutputs([target.id])]; const potentialCycle = [target.id, ...oss.graph.expandAllOutputs([target.id])];
const filtered = oss.items.filter(item => !potentialCycle.includes(item.id)); const filtered = oss.operations.filter(item => !potentialCycle.includes(item.id));
function handleChangeArguments(prev: number[], newValue: number[]) { function handleChangeArguments(prev: number[], newValue: number[]) {
setValue('arguments', newValue, { shouldValidate: true }); setValue('arguments', newValue, { shouldValidate: true });

View File

@ -16,7 +16,7 @@ import { Loader } from '@/components/loader';
import { ModalForm } from '@/components/modal'; import { ModalForm } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { type ICstRelocateDTO, type IOperationPosition, schemaCstRelocate } from '../backend/types'; import { type ICstRelocateDTO, type IOssLayout, schemaCstRelocate } from '../backend/types';
import { useRelocateConstituents } from '../backend/use-relocate-constituents'; import { useRelocateConstituents } from '../backend/use-relocate-constituents';
import { useUpdateLayout } from '../backend/use-update-layout'; import { useUpdateLayout } from '../backend/use-update-layout';
import { IconRelocationUp } from '../components/icon-relocation-up'; import { IconRelocationUp } from '../components/icon-relocation-up';
@ -26,11 +26,11 @@ import { getRelocateCandidates } from '../models/oss-api';
export interface DlgRelocateConstituentsProps { export interface DlgRelocateConstituentsProps {
oss: IOperationSchema; oss: IOperationSchema;
initialTarget?: IOperation; initialTarget?: IOperation;
positions: IOperationPosition[]; layout?: IOssLayout;
} }
export function DlgRelocateConstituents() { export function DlgRelocateConstituents() {
const { oss, initialTarget, positions } = useDialogsStore(state => state.props as DlgRelocateConstituentsProps); const { oss, initialTarget, layout } = useDialogsStore(state => state.props as DlgRelocateConstituentsProps);
const { items: libraryItems } = useLibrary(); const { items: libraryItems } = useLibrary();
const { updateLayout: updatePositions } = useUpdateLayout(); const { updateLayout: updatePositions } = useUpdateLayout();
const { relocateConstituents } = useRelocateConstituents(); const { relocateConstituents } = useRelocateConstituents();
@ -55,7 +55,7 @@ export function DlgRelocateConstituents() {
libraryItems.find(item => item.id === initialTarget?.result) ?? null libraryItems.find(item => item.id === initialTarget?.result) ?? null
); );
const operation = oss.items.find(item => item.result === source?.id); const operation = oss.operations.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));
const destinationSchemas = (() => { const destinationSchemas = (() => {
if (!operation) { if (!operation) {
@ -73,7 +73,7 @@ export function DlgRelocateConstituents() {
if (!sourceData.schema || !destinationItem || !operation) { if (!sourceData.schema || !destinationItem || !operation) {
return []; return [];
} }
const destinationOperation = oss.items.find(item => item.result === destination); const destinationOperation = oss.operations.find(item => item.result === destination);
return getRelocateCandidates(operation.id, destinationOperation!.id, sourceData.schema, oss); return getRelocateCandidates(operation.id, destinationOperation!.id, sourceData.schema, oss);
})(); })();
@ -98,17 +98,13 @@ export function DlgRelocateConstituents() {
} }
function onSubmit(data: ICstRelocateDTO) { function onSubmit(data: ICstRelocateDTO) {
const positionsUnchanged = positions.every(item => { if (!layout || JSON.stringify(layout) === JSON.stringify(oss.layout)) {
const operation = oss.operationByID.get(item.id)!;
return operation.position_x === item.position_x && operation.position_y === item.position_y;
});
if (positionsUnchanged) {
return relocateConstituents(data); return relocateConstituents(data);
} else { } else {
return updatePositions({ return updatePositions({
isSilent: true, isSilent: true,
itemID: oss.id, itemID: oss.id,
positions: positions data: layout
}).then(() => relocateConstituents(data)); }).then(() => relocateConstituents(data));
} }
} }

View File

@ -23,7 +23,7 @@ import { infoMsg } from '@/utils/labels';
import { TextMatcher } from '@/utils/utils'; import { TextMatcher } from '@/utils/utils';
import { Graph } from '../../../models/graph'; import { Graph } from '../../../models/graph';
import { type IOperationPosition } from '../backend/types'; import { type IOssLayout } from '../backend/types';
import { describeSubstitutionError } from '../labels'; import { describeSubstitutionError } from '../labels';
import { type IOperation, type IOperationSchema, SubstitutionErrorType } from './oss'; import { type IOperation, type IOperationSchema, SubstitutionErrorType } from './oss';
@ -494,40 +494,39 @@ export function getRelocateCandidates(
export function calculateInsertPosition( export function calculateInsertPosition(
oss: IOperationSchema, oss: IOperationSchema,
argumentsOps: number[], argumentsOps: number[],
positions: IOperationPosition[], layout: IOssLayout,
defaultPosition: Position2D defaultPosition: Position2D
): Position2D { ): Position2D {
const result = defaultPosition; const result = defaultPosition;
if (positions.length === 0) { const operations = layout.operations;
if (operations.length === 0) {
return result; return result;
} }
if (argumentsOps.length === 0) { if (argumentsOps.length === 0) {
let inputsPositions = positions.filter(pos => let inputsPositions = operations.filter(pos =>
oss.items.find(operation => operation.arguments.length === 0 && operation.id === pos.id) oss.operations.find(operation => operation.arguments.length === 0 && operation.id === pos.id)
); );
if (inputsPositions.length === 0) { if (inputsPositions.length === 0) {
inputsPositions = positions; inputsPositions = operations;
} }
const maxX = Math.max(...inputsPositions.map(node => node.position_x)); const maxX = Math.max(...inputsPositions.map(node => node.x));
const minY = Math.min(...inputsPositions.map(node => node.position_y)); const minY = Math.min(...inputsPositions.map(node => node.y));
result.x = maxX + DISTANCE_X; result.x = maxX + DISTANCE_X;
result.y = minY; result.y = minY;
} else { } else {
const argNodes = positions.filter(pos => argumentsOps.includes(pos.id)); const argNodes = operations.filter(pos => argumentsOps.includes(pos.id));
const maxY = Math.max(...argNodes.map(node => node.position_y)); const maxY = Math.max(...argNodes.map(node => node.y));
const minX = Math.min(...argNodes.map(node => node.position_x)); const minX = Math.min(...argNodes.map(node => node.x));
const maxX = Math.max(...argNodes.map(node => node.position_x)); const maxX = Math.max(...argNodes.map(node => node.x));
result.x = Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE; result.x = Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE;
result.y = maxY + DISTANCE_Y; result.y = maxY + DISTANCE_Y;
} }
let flagIntersect = false; let flagIntersect = false;
do { do {
flagIntersect = positions.some( flagIntersect = operations.some(
position => position => Math.abs(position.x - result.x) < MIN_DISTANCE && Math.abs(position.y - result.y) < MIN_DISTANCE
Math.abs(position.position_x - result.x) < MIN_DISTANCE &&
Math.abs(position.position_y - result.y) < MIN_DISTANCE
); );
if (flagIntersect) { if (flagIntersect) {
result.x += MIN_DISTANCE; result.x += MIN_DISTANCE;

View File

@ -8,6 +8,8 @@ import { type ICstSubstituteInfo, type IOperationDTO, type IOperationSchemaDTO }
/** Represents Operation. */ /** Represents Operation. */
export interface IOperation extends IOperationDTO { export interface IOperation extends IOperationDTO {
x: number;
y: number;
is_owned: boolean; is_owned: boolean;
is_consolidation: boolean; // aka 'diamond synthesis' is_consolidation: boolean; // aka 'diamond synthesis'
substitutions: ICstSubstituteInfo[]; substitutions: ICstSubstituteInfo[];
@ -25,7 +27,7 @@ export interface IOperationSchemaStats {
/** Represents OperationSchema. */ /** Represents OperationSchema. */
export interface IOperationSchema extends IOperationSchemaDTO { export interface IOperationSchema extends IOperationSchemaDTO {
items: IOperation[]; operations: IOperation[];
graph: Graph; graph: Graph;
schemas: number[]; schemas: number[];

View File

@ -27,7 +27,7 @@ import { useMutatingOss } from '../../../backend/use-mutating-oss';
import { type IOperation } from '../../../models/oss'; import { type IOperation } from '../../../models/oss';
import { useOssEdit } from '../oss-edit-context'; import { useOssEdit } from '../oss-edit-context';
import { useGetPositions } from './use-get-positions'; import { useGetLayout } from './use-get-layout';
// pixels - size of OSS context menu // pixels - size of OSS context menu
const MENU_WIDTH = 200; const MENU_WIDTH = 200;
@ -49,7 +49,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
const { items: libraryItems } = useLibrary(); const { items: libraryItems } = useLibrary();
const { schema, navigateOperationSchema, isMutable, canDeleteOperation: canDelete } = useOssEdit(); const { schema, navigateOperationSchema, isMutable, canDeleteOperation: canDelete } = useOssEdit();
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const getPositions = useGetPositions(); const getLayout = useGetLayout();
const { inputCreate } = useInputCreate(); const { inputCreate } = useInputCreate();
const { operationExecute } = useOperationExecute(); const { operationExecute } = useOperationExecute();
@ -104,7 +104,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
showEditInput({ showEditInput({
oss: schema, oss: schema,
target: operation, target: operation,
positions: getPositions() layout: getLayout()
}); });
} }
@ -116,7 +116,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
showEditOperation({ showEditOperation({
oss: schema, oss: schema,
target: operation, target: operation,
positions: getPositions() layout: getLayout()
}); });
} }
@ -128,7 +128,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
showDeleteOperation({ showDeleteOperation({
oss: schema, oss: schema,
target: operation, target: operation,
positions: getPositions() layout: getLayout()
}); });
} }
@ -139,7 +139,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
onHide(); onHide();
void operationExecute({ void operationExecute({
itemID: schema.id, // itemID: schema.id, //
data: { target: operation.id, positions: getPositions() } data: { target: operation.id, layout: getLayout() }
}); });
} }
@ -154,7 +154,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
onHide(); onHide();
void inputCreate({ void inputCreate({
itemID: schema.id, itemID: schema.id,
data: { target: operation.id, positions: getPositions() } data: { target: operation.id, layout: getLayout() }
}).then(new_schema => router.push({ path: urls.schema(new_schema.id), force: true })); }).then(new_schema => router.push({ path: urls.schema(new_schema.id), force: true }));
} }
@ -166,7 +166,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
showRelocateConstituents({ showRelocateConstituents({
oss: schema, oss: schema,
initialTarget: operation, initialTarget: operation,
positions: getPositions() layout: getLayout()
}); });
} }

View File

@ -26,7 +26,7 @@ import { useOssEdit } from '../oss-edit-context';
import { OssNodeTypes } from './graph/oss-node-types'; import { OssNodeTypes } from './graph/oss-node-types';
import { type ContextMenuData, NodeContextMenu } from './node-context-menu'; import { type ContextMenuData, NodeContextMenu } from './node-context-menu';
import { ToolbarOssGraph } from './toolbar-oss-graph'; import { ToolbarOssGraph } from './toolbar-oss-graph';
import { useGetPositions } from './use-get-positions'; import { useGetLayout } from './use-get-layout';
const ZOOM_MAX = 2; const ZOOM_MAX = 2;
const ZOOM_MIN = 0.5; const ZOOM_MIN = 0.5;
@ -52,7 +52,7 @@ export function OssFlow() {
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate); const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
const edgeStraight = useOSSGraphStore(state => state.edgeStraight); const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
const getPositions = useGetPositions(); const getLayout = useGetLayout();
const { updateLayout: updatePositions } = useUpdateLayout(); const { updateLayout: updatePositions } = useUpdateLayout();
const [nodes, setNodes, onNodesChange] = useNodesState([]); const [nodes, setNodes, onNodesChange] = useNodesState([]);
@ -78,10 +78,10 @@ export function OssFlow() {
useEffect(() => { useEffect(() => {
setNodes( setNodes(
schema.items.map(operation => ({ schema.operations.map(operation => ({
id: String(operation.id), id: String(operation.id),
data: { label: operation.alias, operation: operation }, data: { label: operation.alias, operation: operation },
position: { x: operation.position_x, y: operation.position_y }, position: { x: operation.x, y: operation.y },
type: operation.operation_type.toString() type: operation.operation_type.toString()
})) }))
); );
@ -93,8 +93,7 @@ export function OssFlow() {
type: edgeStraight ? 'straight' : 'simplebezier', type: edgeStraight ? 'straight' : 'simplebezier',
animated: edgeAnimate, animated: edgeAnimate,
targetHandle: targetHandle:
schema.operationByID.get(argument.argument)!.position_x > schema.operationByID.get(argument.argument)!.x > schema.operationByID.get(argument.operation)!.x
schema.operationByID.get(argument.operation)!.position_x
? 'right' ? 'right'
: 'left' : 'left'
})) }))
@ -103,16 +102,7 @@ export function OssFlow() {
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate, fitView]); }, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate, fitView]);
function handleSavePositions() { function handleSavePositions() {
const positions = getPositions(); void updatePositions({ itemID: schema.id, data: getLayout() });
void updatePositions({ itemID: schema.id, positions: positions }).then(() => {
positions.forEach(item => {
const operation = schema.operationByID.get(item.id);
if (operation) {
operation.position_x = item.position_x;
operation.position_y = item.position_y;
}
});
});
} }
function handleCreateOperation() { function handleCreateOperation() {
@ -121,7 +111,7 @@ export function OssFlow() {
oss: schema, oss: schema,
defaultX: targetPosition.x, defaultX: targetPosition.x,
defaultY: targetPosition.y, defaultY: targetPosition.y,
positions: getPositions(), layout: getLayout(),
initialInputs: selected, initialInputs: selected,
onCreate: () => onCreate: () =>
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout) setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout)
@ -139,7 +129,7 @@ export function OssFlow() {
showDeleteOperation({ showDeleteOperation({
oss: schema, oss: schema,
target: operation, target: operation,
positions: getPositions() layout: getLayout()
}); });
} }

View File

@ -34,7 +34,7 @@ import { useOSSGraphStore } from '../../../stores/oss-graph';
import { useOssEdit } from '../oss-edit-context'; import { useOssEdit } from '../oss-edit-context';
import { VIEW_PADDING } from './oss-flow'; import { VIEW_PADDING } from './oss-flow';
import { useGetPositions } from './use-get-positions'; import { useGetLayout } from './use-get-layout';
interface ToolbarOssGraphProps extends Styling { interface ToolbarOssGraphProps extends Styling {
onCreate: () => void; onCreate: () => void;
@ -53,7 +53,7 @@ export function ToolbarOssGraph({
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const { fitView } = useReactFlow(); const { fitView } = useReactFlow();
const selectedOperation = schema.operationByID.get(selected[0]); const selectedOperation = schema.operationByID.get(selected[0]);
const getPositions = useGetPositions(); const getLayout = useGetLayout();
const showGrid = useOSSGraphStore(state => state.showGrid); const showGrid = useOSSGraphStore(state => state.showGrid);
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate); const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
@ -93,16 +93,7 @@ export function ToolbarOssGraph({
} }
function handleSavePositions() { function handleSavePositions() {
const positions = getPositions(); void updatePositions({ itemID: schema.id, data: getLayout() });
void updatePositions({ itemID: schema.id, positions: positions }).then(() => {
positions.forEach(item => {
const operation = schema.operationByID.get(item.id);
if (operation) {
operation.position_x = item.position_x;
operation.position_y = item.position_y;
}
});
});
} }
function handleOperationExecute() { function handleOperationExecute() {
@ -111,7 +102,7 @@ export function ToolbarOssGraph({
} }
void operationExecute({ void operationExecute({
itemID: schema.id, // itemID: schema.id, //
data: { target: selectedOperation.id, positions: getPositions() } data: { target: selectedOperation.id, layout: getLayout() }
}); });
} }
@ -122,7 +113,7 @@ export function ToolbarOssGraph({
showEditOperation({ showEditOperation({
oss: schema, oss: schema,
target: selectedOperation, target: selectedOperation,
positions: getPositions() layout: getLayout()
}); });
} }

View File

@ -0,0 +1,17 @@
import { useReactFlow } from 'reactflow';
import { type IOssLayout } from '@/features/oss/backend/types';
export function useGetLayout() {
const { getNodes } = useReactFlow();
return function getLayout(): IOssLayout {
return {
operations: getNodes().map(node => ({
id: Number(node.id),
x: node.position.x,
y: node.position.y
})),
blocks: []
};
};
}

View File

@ -1,12 +0,0 @@
import { useReactFlow } from 'reactflow';
export function useGetPositions() {
const { getNodes } = useReactFlow();
return function getPositions() {
return getNodes().map(node => ({
id: Number(node.id),
position_x: node.position.x,
position_y: node.position.y
}));
};
}

View File

@ -21,8 +21,7 @@ export function MenuEditOss() {
menu.hide(); menu.hide();
showRelocateConstituents({ showRelocateConstituents({
oss: schema, oss: schema,
initialTarget: undefined, initialTarget: undefined
positions: []
}); });
} }