Skip to content

Commit c1780ef

Browse files
bjaggclaude
andcommitted
Issue #1028: UI creates attribute+association in one call; add axios timeout
Frontend (mdr-frontend): - ModelExplorer.handleCreateAttribute now passes EntityId to createAttribute when adding under an Entity, and no longer makes the separate createEntityAttributeAssociation request — the API creates both atomically (see attribute_service). Removes the two-request orphan window. (The separate-association dialog flow for EXISTING attributes is unchanged.) - api.ts: add a 30s request timeout so a stalled request fails fast/clearly instead of hanging into the ambiguous 'Network error' that triggered the orphan bug. - AttributeParams gains optional EntityId. Backend: - create_attribute sets the association's ExtendedByDataModelId for extension models (OrgLIF/PartnerLIF), mirroring the frontend template, so the single atomic call is correct for extension data models too. Type-checks clean (tsc). Refs #1028. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 53b788e commit c1780ef

4 files changed

Lines changed: 20 additions & 10 deletions

File tree

components/lif/mdr_services/attribute_service.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,16 @@ async def create_attribute(session: AsyncSession, data: CreateAttributeDTO):
110110

111111
if data.EntityId is not None:
112112
await session.flush() # assign attribute.Id without committing
113+
# For an extension model (OrgLIF/PartnerLIF — data.Extension is derived from the model's
114+
# BaseDataModelId above), the association is an extension placement onto this model, matching
115+
# the frontend's tmplCreateEntityAttributeAssociation (ExtendedByDataModelId = the model id).
113116
association = EntityAttributeAssociation(
114117
EntityId=data.EntityId,
115118
AttributeId=attribute.Id,
116119
Contributor=data.Contributor,
117120
ContributorOrganization=data.ContributorOrganization,
118121
Deleted=False,
122+
ExtendedByDataModelId=data.DataModelId if data.Extension else None,
119123
)
120124
session.add(association)
121125

frontends/mdr-frontend/src/components/ModelExplorer/ModelExplorer.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -407,21 +407,20 @@ const ModelTree: React.FC<ModelTreeProps> = ({
407407
const useEAA = parentNode?.type === 'entity';
408408
/** ^ Always want an association, but should also be under an Entity */
409409
const inclusionParams: any = useInclusion ? tmplCreateInclusion(model.Id, 0, 'Attribute') : {};
410-
const eAAParams: any = useEAA ? tmplCreateEntityAttributeAssociation(parentNode.id, model) : {};
411410

412411
const createdIds: any = {};
413412
try {
414-
const newAttribute: any = await createAttribute({ ...params, DataModelId: model.Id });
413+
// When adding under an Entity, pass EntityId so the API creates the attribute AND its
414+
// entity association in a single transaction (#1028). This replaces the old two-request
415+
// flow (create attribute, then create association), which orphaned the attribute if the
416+
// first response was lost — persisted-but-unassociated: invisible + blocking re-create.
417+
const newAttribute: any = await createAttribute({
418+
...params,
419+
DataModelId: model.Id,
420+
...(useEAA ? { EntityId: parentNode.id } : {}),
421+
});
415422
createdIds.Attribute = newAttribute?.Id;
416423
debugLog(">> Created Attribute:", newAttribute?.Id, newAttribute);
417-
if (newAttribute?.Id && useEAA) {
418-
eAAParams.AttributeId = newAttribute?.Id;
419-
eAAParams.Contributor = params.Contributor;
420-
eAAParams.ContributorOrganization = params.ContributorOrganization;
421-
const newEAA: any = await createEntityAttributeAssociation(eAAParams);
422-
createdIds.EntityAttributeAssociation = newEAA?.Id;
423-
debugLog(">> Created Entity Attribute Association:", newEAA?.Id, newEAA);
424-
}
425424
if (newAttribute?.Id && useInclusion) {
426425
inclusionParams.IncludedElementId = newAttribute?.Id;
427426
inclusionParams.Contributor = params.Contributor;

frontends/mdr-frontend/src/services/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ const api = axios.create({
1212
// config to set Access-Control-Allow-Credentials: true and an explicit
1313
// origin (not "*"), which the MDR API already does.
1414
withCredentials: true,
15+
// Fail a stalled request instead of hanging indefinitely. Without a timeout, a request
16+
// whose response is lost hangs until the browser/OS gives up, surfacing as an ambiguous
17+
// "Network error" — the trigger for the orphaned-attribute class of bugs (#1028).
18+
timeout: 30000,
1519
});
1620

1721
// Add a response interceptor to handle token refresh

frontends/mdr-frontend/src/services/attributesService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export interface AttributeParams {
2525
ContributorOrganization?: string | null;
2626
Extension?: boolean;
2727
ExtensionNotes?: string | null;
28+
// When set, the API creates the attribute AND its association to this entity in one
29+
// transaction, so a dropped response can't orphan the attribute (#1028).
30+
EntityId?: number;
2831
}
2932

3033
export interface AttributeDTO {

0 commit comments

Comments
 (0)