// What saving the machine form should do about its controlling-PC link. // // Pulled out of MachineForm.vue because the component decided this inline and // got it wrong in a way nothing could catch: it treated "I know the id of the // existing link" as "there IS an existing link". The id was read from // `rel.assetrelationshipid`, a key the API has never emitted - the column is // `relationshipid` and to_dict serialises column names - so it was undefined on // every load, and the two questions gave opposite answers on a machine that // plainly had a controlling PC. // // Both failures followed from that single conflation: // - Saving ANY change, a map position included, re-POSTed the link that was // already there. The server answered 409 "this relationship already // exists", correctly, and the form surfaced it as a failed save. // - Clearing the controlling PC did nothing at all, because "no selection and // no known id" reads as "nothing to do". // // So THE LOADED PC IS THE AUTHORITY on whether a link exists, and the // relationship id is only ever used to delete a row. If the id goes missing // again the worst case is a link that cannot be removed, never a duplicate // create. /** * Decide what to do with the controlling-PC link on save. * * @param {object} state * @param {number|string|null} state.selected - PC id chosen in the form now. * @param {number|string|null} state.loaded - PC id the link named on load. * @param {number|null} state.existingRelationshipId - row id to delete, if known. * @returns {{action: 'none'|'unlink'|'link'|'relink', deleteId: number|null}} */ export function planControllingPcChange({ selected, loaded, existingRelationshipId } = {}) { const now = normalize(selected) const before = normalize(loaded) const deleteId = existingRelationshipId ?? null // Unchanged is not a change. Somebody moving a marker on the map is not // asking to be re-linked to the same PC, and delete-then-create on an // untouched link opens a window where the machine has no controlling PC - // which resolve_asset_position walks across, so a failure between the two // calls would lose the machine's inherited position along with the link. if (now === before) { return { action: 'none', deleteId: null } } if (now === null) { // Cleared. Nothing to delete means nothing to do, rather than an error: // the link is already absent, which is the state being asked for. return deleteId === null ? { action: 'none', deleteId: null } : { action: 'unlink', deleteId } } if (before === null) { return { action: 'link', deleteId: null } } // Swapped to a different PC. The old row goes first: 'controls' is singular // per source, not per target, so leaving it would leave the machine with two. return { action: 'relink', deleteId } } // Ids arrive as numbers from the API and strings from a