mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-13 20:30:36 +03:00
F: Prepare Association backend api
This commit is contained in:
parent
593b483936
commit
41e0ba64ba
|
@ -4,7 +4,7 @@
|
|||
from typing import Optional
|
||||
|
||||
from apps.library.models import LibraryItem
|
||||
from apps.rsform.models import Constituenta, CstType, OrderManager, RSFormCached
|
||||
from apps.rsform.models import Association, Constituenta, CstType, OrderManager, RSFormCached
|
||||
|
||||
from .Argument import Argument
|
||||
from .Inheritance import Inheritance
|
||||
|
@ -283,15 +283,15 @@ class OperationSchemaCached:
|
|||
mapping=alias_mapping
|
||||
)
|
||||
|
||||
def before_delete_cst(self, sourceID: int, target: list[int]) -> None:
|
||||
def before_delete_cst(self, operationID: int, target: list[int]) -> None:
|
||||
''' Trigger cascade resolutions before Constituents are deleted. '''
|
||||
operation = self.cache.get_operation(sourceID)
|
||||
operation = self.cache.get_operation(operationID)
|
||||
self.engine.on_delete_inherited(operation.pk, target)
|
||||
|
||||
def before_substitute(self, schemaID: int, substitutions: CstSubstitution) -> None:
|
||||
''' Trigger cascade resolutions before Constituents are substituted. '''
|
||||
operation = self.cache.get_operation(schemaID)
|
||||
self.engine.on_before_substitute(substitutions, operation)
|
||||
self.engine.on_before_substitute(operation.pk, substitutions)
|
||||
|
||||
def before_delete_arguments(self, target: Operation, arguments: list[Operation]) -> None:
|
||||
''' Trigger cascade resolutions before arguments are deleted. '''
|
||||
|
@ -318,6 +318,17 @@ class OperationSchemaCached:
|
|||
mapping={}
|
||||
)
|
||||
|
||||
def after_create_association(self, schemaID: int, associations: list[Association],
|
||||
exclude: Optional[list[int]] = None) -> None:
|
||||
''' Trigger cascade resolutions when association is created. '''
|
||||
operation = self.cache.get_operation(schemaID)
|
||||
self.engine.on_inherit_association(operation.pk, associations, exclude)
|
||||
|
||||
def before_delete_association(self, schemaID: int, associations: list[Association]) -> None:
|
||||
''' Trigger cascade resolutions when association is deleted. '''
|
||||
operation = self.cache.get_operation(schemaID)
|
||||
self.engine.on_delete_association(operation.pk, associations)
|
||||
|
||||
def _on_add_substitutions(self, schema: Optional[RSFormCached], added: list[Substitution]) -> None:
|
||||
''' Trigger cascade resolutions when Constituenta substitution is added. '''
|
||||
if not added:
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import Optional
|
|||
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from apps.rsform.models import INSERT_LAST, Constituenta, CstType, RSFormCached
|
||||
from apps.rsform.models import INSERT_LAST, Association, Constituenta, CstType, RSFormCached
|
||||
|
||||
from .Inheritance import Inheritance
|
||||
from .Operation import Operation
|
||||
|
@ -126,9 +126,41 @@ class PropagationEngine:
|
|||
mapping=new_mapping
|
||||
)
|
||||
|
||||
def on_before_substitute(self, substitutions: CstSubstitution, operation: Operation) -> None:
|
||||
def on_inherit_association(self, operationID: int,
|
||||
items: list[Association],
|
||||
exclude: Optional[list[int]] = None) -> None:
|
||||
''' Trigger cascade resolutions when association is inherited. '''
|
||||
children = self.cache.extend_graph.outputs[operationID]
|
||||
if not children:
|
||||
return
|
||||
for child_id in children:
|
||||
if not exclude or child_id not in exclude:
|
||||
self.inherit_association(child_id, items)
|
||||
|
||||
def inherit_association(self, target: int, items: list[Association]) -> None:
|
||||
''' Execute inheritance of Associations. '''
|
||||
operation = self.cache.operation_by_id[target]
|
||||
if operation.result is None:
|
||||
return
|
||||
|
||||
self.cache.ensure_loaded_subs()
|
||||
new_associations: list[Association] = []
|
||||
for assoc in items:
|
||||
new_container = self.cache.get_inheritor(assoc.container_id, target)
|
||||
new_associate = self.cache.get_inheritor(assoc.associate_id, target)
|
||||
if new_container is None or new_associate is None:
|
||||
continue
|
||||
new_associations.append(Association(
|
||||
container_id=new_container,
|
||||
associate_id=new_associate
|
||||
))
|
||||
if new_associations:
|
||||
new_associations = Association.objects.bulk_create(new_associations)
|
||||
self.on_inherit_association(target, new_associations)
|
||||
|
||||
def on_before_substitute(self, operationID: int, substitutions: CstSubstitution) -> None:
|
||||
''' Trigger cascade resolutions when Constituenta substitution is executed. '''
|
||||
children = self.cache.extend_graph.outputs[operation.pk]
|
||||
children = self.cache.extend_graph.outputs[operationID]
|
||||
if not children:
|
||||
return
|
||||
self.cache.ensure_loaded_subs()
|
||||
|
@ -140,9 +172,37 @@ class PropagationEngine:
|
|||
new_substitutions = self._transform_substitutions(substitutions, child_id, child_schema)
|
||||
if not new_substitutions:
|
||||
continue
|
||||
self.on_before_substitute(new_substitutions, child_operation)
|
||||
self.on_before_substitute(child_operation.pk, new_substitutions)
|
||||
child_schema.substitute(new_substitutions)
|
||||
|
||||
def on_delete_association(self, operationID: int, associations: list[Association]) -> None:
|
||||
''' Trigger cascade resolutions when association is deleted. '''
|
||||
children = self.cache.extend_graph.outputs[operationID]
|
||||
if not children:
|
||||
return
|
||||
self.cache.ensure_loaded_subs()
|
||||
for child_id in children:
|
||||
child_operation = self.cache.operation_by_id[child_id]
|
||||
child_schema = self.cache.get_schema(child_operation)
|
||||
if child_schema is None:
|
||||
continue
|
||||
|
||||
deleted: list[Association] = []
|
||||
for assoc in associations:
|
||||
new_container = self.cache.get_inheritor(assoc.container_id, child_id)
|
||||
new_associate = self.cache.get_inheritor(assoc.associate_id, child_id)
|
||||
if new_container is None or new_associate is None:
|
||||
continue
|
||||
deleted_assoc = Association.objects.filter(
|
||||
container=new_container,
|
||||
associate=new_associate
|
||||
)
|
||||
if deleted_assoc.exists():
|
||||
deleted.append(deleted_assoc[0])
|
||||
if deleted:
|
||||
self.on_delete_association(child_id, deleted)
|
||||
Association.objects.filter(pk__in=[assoc.pk for assoc in deleted]).delete()
|
||||
|
||||
def on_delete_inherited(self, operation: int, target: list[int]) -> None:
|
||||
''' Trigger cascade resolutions when Constituenta inheritance is deleted. '''
|
||||
children = self.cache.extend_graph.outputs[operation]
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from typing import Optional
|
||||
|
||||
from apps.library.models import LibraryItem, LibraryItemType
|
||||
from apps.rsform.models import Constituenta, CstType, RSFormCached
|
||||
from apps.rsform.models import Association, Constituenta, CstType, RSFormCached
|
||||
|
||||
from .OperationSchemaCached import CstSubstitution, OperationSchemaCached
|
||||
|
||||
|
@ -80,3 +80,22 @@ class PropagationFacade:
|
|||
for host in hosts:
|
||||
if exclude is None or host.pk not in exclude:
|
||||
OperationSchemaCached(host).before_delete_cst(item.pk, ids)
|
||||
|
||||
@staticmethod
|
||||
def after_create_association(sourceID: int, associations: list[Association],
|
||||
exclude: Optional[list[int]] = None) -> None:
|
||||
''' Trigger cascade resolutions when association is created. '''
|
||||
hosts = _get_oss_hosts(sourceID)
|
||||
for host in hosts:
|
||||
if exclude is None or host.pk not in exclude:
|
||||
OperationSchemaCached(host).after_create_association(sourceID, associations)
|
||||
|
||||
@staticmethod
|
||||
def before_delete_association(sourceID: int,
|
||||
associations: list[Association],
|
||||
exclude: Optional[list[int]] = None) -> None:
|
||||
''' Trigger cascade resolutions before association is deleted. '''
|
||||
hosts = _get_oss_hosts(sourceID)
|
||||
for host in hosts:
|
||||
if exclude is None or host.pk not in exclude:
|
||||
OperationSchemaCached(host).before_delete_association(sourceID, associations)
|
||||
|
|
|
@ -12,6 +12,7 @@ from .basics import (
|
|||
WordFormSerializer
|
||||
)
|
||||
from .data_access import (
|
||||
AssociationDataSerializer,
|
||||
CrucialUpdateSerializer,
|
||||
CstCreateSerializer,
|
||||
CstInfoSerializer,
|
||||
|
|
|
@ -30,6 +30,25 @@ class AssociationSerializer(StrictModelSerializer):
|
|||
fields = ('container', 'associate')
|
||||
|
||||
|
||||
class AssociationDataSerializer(StrictSerializer):
|
||||
''' Serializer: Association data. '''
|
||||
container = PKField(many=False, queryset=Constituenta.objects.all().only('schema_id'))
|
||||
associate = PKField(many=False, queryset=Constituenta.objects.all().only('schema_id'))
|
||||
|
||||
def validate(self, attrs):
|
||||
schema = cast(LibraryItem, self.context['schema'])
|
||||
if schema and attrs['container'].schema_id != schema.id:
|
||||
raise serializers.ValidationError({
|
||||
'container': msg.constituentaNotInRSform(schema.title)
|
||||
})
|
||||
if schema and attrs['associate'].schema_id != schema.id:
|
||||
raise serializers.ValidationError({
|
||||
'associate': msg.constituentaNotInRSform(schema.title)
|
||||
})
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class CstBaseSerializer(StrictModelSerializer):
|
||||
''' Serializer: Constituenta all data. '''
|
||||
class Meta:
|
||||
|
|
|
@ -49,6 +49,9 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
'restore_order',
|
||||
'reset_aliases',
|
||||
'produce_structure',
|
||||
'add_association',
|
||||
'delete_association',
|
||||
'clear_associations'
|
||||
]:
|
||||
permission_list = [permissions.ItemEditor]
|
||||
elif self.action in [
|
||||
|
@ -281,6 +284,104 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
data=s.RSFormParseSerializer(item).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
summary='create Association',
|
||||
tags=['Constituenta'],
|
||||
request=s.AssociationDataSerializer,
|
||||
responses={
|
||||
c.HTTP_201_CREATED: s.RSFormParseSerializer,
|
||||
c.HTTP_400_BAD_REQUEST: None,
|
||||
c.HTTP_403_FORBIDDEN: None,
|
||||
c.HTTP_404_NOT_FOUND: None
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='create-association')
|
||||
def create_association(self, request: Request, pk) -> HttpResponse:
|
||||
''' Create Association. '''
|
||||
item = self._get_item()
|
||||
serializer = s.AssociationDataSerializer(data=request.data, context={'schema': item})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
with transaction.atomic():
|
||||
new_association = m.Association.objects.create(
|
||||
container=serializer.validated_data['container'],
|
||||
associate=serializer.validated_data['associate']
|
||||
)
|
||||
PropagationFacade.after_create_association(item.pk, [new_association])
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data=s.RSFormParseSerializer(item).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
summary='delete Association',
|
||||
tags=['RSForm'],
|
||||
request=s.AssociationDataSerializer,
|
||||
responses={
|
||||
c.HTTP_200_OK: s.RSFormParseSerializer,
|
||||
c.HTTP_400_BAD_REQUEST: None,
|
||||
c.HTTP_403_FORBIDDEN: None,
|
||||
c.HTTP_404_NOT_FOUND: None
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='delete-association')
|
||||
def delete_association(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Delete Association. '''
|
||||
item = self._get_item()
|
||||
serializer = s.AssociationDataSerializer(data=request.data, context={'schema': item})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
with transaction.atomic():
|
||||
target = list(m.Association.objects.filter(
|
||||
container=serializer.validated_data['container'],
|
||||
associate=serializer.validated_data['associate']
|
||||
))
|
||||
if not target:
|
||||
raise ValidationError({
|
||||
'container': msg.invalidAssociation()
|
||||
})
|
||||
|
||||
PropagationFacade.before_delete_association(item.pk, target)
|
||||
m.Association.objects.filter(pk__in=[assoc.pk for assoc in target]).delete()
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(item).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
summary='delete all associations for target constituenta',
|
||||
tags=['RSForm'],
|
||||
request=s.CstTargetSerializer,
|
||||
responses={
|
||||
c.HTTP_200_OK: s.RSFormParseSerializer,
|
||||
c.HTTP_400_BAD_REQUEST: None,
|
||||
c.HTTP_403_FORBIDDEN: None,
|
||||
c.HTTP_404_NOT_FOUND: None
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='clear-associations')
|
||||
def clear_associations(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Delete Associations for target Constituenta. '''
|
||||
item = self._get_item()
|
||||
serializer = s.CstTargetSerializer(data=request.data, context={'schema': item})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
with transaction.atomic():
|
||||
target = list(m.Association.objects.filter(container=serializer.validated_data['target']))
|
||||
if target:
|
||||
PropagationFacade.before_delete_association(item.pk, target)
|
||||
m.Association.objects.filter(pk__in=[assoc.pk for assoc in target]).delete()
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(item).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
summary='move constituenta',
|
||||
tags=['RSForm'],
|
||||
|
|
|
@ -142,6 +142,10 @@ def typificationInvalidStr():
|
|||
return 'Invalid typification string'
|
||||
|
||||
|
||||
def invalidAssociation():
|
||||
return f'Ассоциация не найдена'
|
||||
|
||||
|
||||
def exteorFileVersionNotSupported():
|
||||
return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии'
|
||||
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { type Edge, MarkerType, type Node, useEdgesState, useNodesState } from 'reactflow';
|
||||
|
||||
import { type IConstituenta, type IRSForm } from '@/features/rsform';
|
||||
import { TGEdgeTypes } from '@/features/rsform/components/term-graph/graph/tg-edge-types';
|
||||
import { TGNodeTypes } from '@/features/rsform/components/term-graph/graph/tg-node-types';
|
||||
import { SelectColoring } from '@/features/rsform/components/term-graph/select-coloring';
|
||||
import { ToolbarFocusedCst } from '@/features/rsform/components/term-graph/toolbar-focused-cst';
|
||||
import { applyLayout, produceFilteredGraph, type TGNodeData } from '@/features/rsform/models/graph-api';
|
||||
import { type IConstituenta, type IRSForm } from '@/features/rsform/models/rsform';
|
||||
import { useTermGraphStore } from '@/features/rsform/stores/term-graph';
|
||||
|
||||
import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow';
|
||||
|
|
|
@ -5,6 +5,8 @@ import { DELAYS, KEYS } from '@/backend/configuration';
|
|||
import { infoMsg } from '@/utils/labels';
|
||||
|
||||
import {
|
||||
type IAssociationDataDTO,
|
||||
type IAssociationTargetDTO,
|
||||
type ICheckConstituentaDTO,
|
||||
type IConstituentaCreatedResponse,
|
||||
type IConstituentaList,
|
||||
|
@ -150,5 +152,33 @@ export const rsformsApi = {
|
|||
schema: schemaExpressionParse,
|
||||
endpoint: `/api/rsforms/${itemID}/check-constituenta`,
|
||||
request: { data: data }
|
||||
}),
|
||||
|
||||
createAssociation: ({ itemID, data }: { itemID: number; data: IAssociationDataDTO }) =>
|
||||
axiosPost<IAssociationDataDTO, IRSFormDTO>({
|
||||
schema: schemaRSForm,
|
||||
endpoint: `/api/rsforms/${itemID}/create-association`,
|
||||
request: {
|
||||
data: data,
|
||||
successMessage: infoMsg.changesSaved
|
||||
}
|
||||
}),
|
||||
deleteAssociation: ({ itemID, data }: { itemID: number; data: IAssociationDataDTO }) =>
|
||||
axiosPatch<IAssociationDataDTO, IRSFormDTO>({
|
||||
schema: schemaRSForm,
|
||||
endpoint: `/api/rsforms/${itemID}/delete-association`,
|
||||
request: {
|
||||
data: data,
|
||||
successMessage: infoMsg.changesSaved
|
||||
}
|
||||
}),
|
||||
clearAssociations: ({ itemID, data }: { itemID: number; data: IAssociationTargetDTO }) =>
|
||||
axiosPatch<IAssociationTargetDTO, IRSFormDTO>({
|
||||
schema: schemaRSForm,
|
||||
endpoint: `/api/rsforms/${itemID}/clear-associations`,
|
||||
request: {
|
||||
data: data,
|
||||
successMessage: infoMsg.changesSaved
|
||||
}
|
||||
})
|
||||
} as const;
|
||||
|
|
|
@ -94,6 +94,12 @@ export interface ICheckConstituentaDTO {
|
|||
/** Represents data, used in merging multiple {@link IConstituenta}. */
|
||||
export type ISubstitutionsDTO = z.infer<typeof schemaSubstitutions>;
|
||||
|
||||
/** Represents data for creating or deleting an association. */
|
||||
export type IAssociationDataDTO = z.infer<typeof schemaAssociationData>;
|
||||
|
||||
/** Represents data for clearing all associations for a target constituenta. */
|
||||
export type IAssociationTargetDTO = z.infer<typeof schemaAssociationTarget>;
|
||||
|
||||
/** Represents Constituenta list. */
|
||||
export interface IConstituentaList {
|
||||
items: number[];
|
||||
|
@ -386,6 +392,15 @@ export const schemaSubstitutions = z.strictObject({
|
|||
substitutions: z.array(schemaSubstituteConstituents).min(1, { message: errorMsg.emptySubstitutions })
|
||||
});
|
||||
|
||||
export const schemaAssociationData = z.strictObject({
|
||||
container: z.number(),
|
||||
associate: z.number()
|
||||
});
|
||||
|
||||
export const schemaAssociationTarget = z.strictObject({
|
||||
target: z.number()
|
||||
});
|
||||
|
||||
export const schemaInlineSynthesis = z.strictObject({
|
||||
receiver: z.number(),
|
||||
source: z.number().nullable(),
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { rsformsApi } from './api';
|
||||
import { type IAssociationTargetDTO } from './types';
|
||||
|
||||
export const useClearAssociations = () => {
|
||||
const client = useQueryClient();
|
||||
const { updateTimestamp } = useUpdateTimestamp();
|
||||
const mutation = useMutation({
|
||||
mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'clear-associations'],
|
||||
mutationFn: rsformsApi.clearAssociations,
|
||||
onSuccess: async data => {
|
||||
updateTimestamp(data.id, data.time_update);
|
||||
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
|
||||
await client.invalidateQueries({
|
||||
queryKey: [rsformsApi.baseKey],
|
||||
predicate: query => query.queryKey.length > 2 && query.queryKey[2] !== String(data.id)
|
||||
});
|
||||
},
|
||||
onError: () => client.invalidateQueries()
|
||||
});
|
||||
return {
|
||||
clearAssociations: (data: { itemID: number; data: IAssociationTargetDTO }) => mutation.mutateAsync(data)
|
||||
};
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { rsformsApi } from './api';
|
||||
import { type IAssociationDataDTO } from './types';
|
||||
|
||||
export const useCreateAssociation = () => {
|
||||
const client = useQueryClient();
|
||||
const { updateTimestamp } = useUpdateTimestamp();
|
||||
const mutation = useMutation({
|
||||
mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'create-association'],
|
||||
mutationFn: rsformsApi.createAssociation,
|
||||
onSuccess: async data => {
|
||||
updateTimestamp(data.id, data.time_update);
|
||||
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
|
||||
await client.invalidateQueries({
|
||||
queryKey: [rsformsApi.baseKey],
|
||||
predicate: query => query.queryKey.length > 2 && query.queryKey[2] !== String(data.id)
|
||||
});
|
||||
},
|
||||
onError: () => client.invalidateQueries()
|
||||
});
|
||||
return {
|
||||
createAssociation: (data: { itemID: number; data: IAssociationDataDTO }) => mutation.mutateAsync(data)
|
||||
};
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { rsformsApi } from './api';
|
||||
import { type IAssociationDataDTO } from './types';
|
||||
|
||||
export const useDeleteAssociation = () => {
|
||||
const client = useQueryClient();
|
||||
const { updateTimestamp } = useUpdateTimestamp();
|
||||
const mutation = useMutation({
|
||||
mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'delete-association'],
|
||||
mutationFn: rsformsApi.deleteAssociation,
|
||||
onSuccess: async data => {
|
||||
updateTimestamp(data.id, data.time_update);
|
||||
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
|
||||
await client.invalidateQueries({
|
||||
queryKey: [rsformsApi.baseKey],
|
||||
predicate: query => query.queryKey.length > 2 && query.queryKey[2] !== String(data.id)
|
||||
});
|
||||
},
|
||||
onError: () => client.invalidateQueries()
|
||||
});
|
||||
return {
|
||||
deleteAssociation: (data: { itemID: number; data: IAssociationDataDTO }) => mutation.mutateAsync(data)
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user