Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d879267
feat: add suffix unit dependency checking and fix async cleanup
BunnyBea83 Jul 21, 2026
b3b1b34
feat: add suffix unit dependency checking and fix async cleanup
BunnyBea83 Jul 21, 2026
4cb3205
feat: add DB layer methods for cascading suffix deletions
BunnyBea83 Jul 21, 2026
ff4f60e
feat: enhance conversion/unit deletion with dependency validation
BunnyBea83 Jul 21, 2026
e41845c
feat: enhance UI warnings for suffix unit deletions
BunnyBea83 Jul 21, 2026
041ca55
Revert: Omit DB layer methods for cascading suffix deletions.
BunnyBea83 Jul 27, 2026
26a0c8b
Refactor: Robust Suffix Unit detection.
BunnyBea83 Jul 27, 2026
0887fe1
Feat: Add safe unit deletion
BunnyBea83 Jul 27, 2026
5e4e541
Refactor: Suffix unit deletion instead of hiding
BunnyBea83 Jul 27, 2026
d0df4e3
Feat:
BunnyBea83 Aug 4, 2026
3077314
Feat:
BunnyBea83 Aug 4, 2026
b1ff92f
Refactor:
BunnyBea83 Aug 4, 2026
2355470
Feat:
BunnyBea83 Aug 4, 2026
aade23e
Refactor: Linted Code
BunnyBea83 Aug 5, 2026
4a5c31b
Merge branch 'OpenEnergyDashboard:development' into BunnyBea83/issue1448
BunnyBea83 Sep 17, 2026
8f8ea5c
Refactor: PR cleanup and SQL implementation.
BunnyBea83 Sep 17, 2026
1c095a4
Refactor: redoCik and orphan unit detection
BunnyBea83 Sep 19, 2026
2bcf354
Refactor: UI display and Unit Tests.
BunnyBea83 Sep 20, 2026
67061e9
Refactor: Naming conventions and formatting
BunnyBea83 Oct 5, 2026
f25a64e
Refactor: SQL naming conventions
BunnyBea83 Oct 5, 2026
542302d
Refactor: redoCik usage
BunnyBea83 Oct 5, 2026
24c0592
Refactor: Orphan Check
BunnyBea83 Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 132 additions & 7 deletions src/client/app/components/conversion/EditConversionModalComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,18 @@ export default function EditConversionModalComponent(props: EditConversionModalC
return source?.typeOfUnit === UnitType.meter;
};

// Determine whether the selected unit is a Suffix unit
// Including Suffix input with unit type Unit and Suffix is filled in
const isSuffixRelated = (unit? : UnitData): boolean =>
Comment thread
huss marked this conversation as resolved.
Outdated
!!unit && (
unit.typeOfUnit === UnitType.suffix || !!unit.suffix?.trim()
Comment thread
huss marked this conversation as resolved.
Outdated
);

// Determine whether the selected source or destination is a suffix unit
const isSuffixUsed = () => {
const source = unitDataById[state.sourceId];
const dest = unitDataById[state.destinationId];
return source?.typeOfUnit === UnitType.suffix || dest?.typeOfUnit === UnitType.suffix;
return isSuffixRelated(source) || isSuffixRelated(dest);
};

/**
Expand Down Expand Up @@ -128,6 +135,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC
}
return count;
};

// Performs checks to warn the admin of the impact deleting a conversion will have on meter units and possible graphing units.
const checkState = async () => {
const source = unitDataById[state.sourceId];
Expand Down Expand Up @@ -205,16 +213,20 @@ export default function EditConversionModalComponent(props: EditConversionModalC
}

// Unit-to-unit orphan check
if (source.typeOfUnit === UnitType.unit && dest.typeOfUnit === UnitType.unit) {
if (source.typeOfUnit === UnitType.unit && dest.typeOfUnit === UnitType.unit
&& !isSuffixRelated(source) && !isSuffixRelated(dest)) {
// Find conversions to destination or source of bidirectional conversion
const destConversions = conversionDetails.filter(conversion =>
(conversion.destinationId === dest.id) ||
(conversion.bidirectional && conversion.sourceId === dest.id)
);

// Exclude conversion being deleted
// Find remaining connections to destination unit afterward
const remainingDestConversions = destConversions.filter(conversion =>
!(conversion.sourceId === source.id && conversion.destinationId === dest.id)
);

// Display units that will be orphaned by conversion deletion
if (remainingDestConversions.length === 0) {
msgElements.push(
<div key="unit-orphan">
Expand All @@ -223,10 +235,114 @@ export default function EditConversionModalComponent(props: EditConversionModalC
</div>
);
}
// Suffix unit to Suffix Unit deletion
} else if (
Comment thread
huss marked this conversation as resolved.
Outdated
isSuffixRelated(source) || isSuffixRelated(dest)) {
const suffixUnit = isSuffixRelated(source) ? source : dest;
// Find all conversions involving this suffix unit (as source or destination)
const suffixUnitConversions = conversionDetails.filter(c =>
c.sourceId === suffixUnit.id ||
(c.bidirectional && c.destinationId === suffixUnit.id)
);

// Find OED-created suffix-type units that would be cascade deleted
const suffixTypeUnitsToDelete = suffixUnitConversions
.map(c => {
Comment thread
huss marked this conversation as resolved.
Outdated
const otherId = c.sourceId === suffixUnit.id ? c.destinationId : c.sourceId;
return unitDataById[otherId];
})
.filter(isSuffixRelated);

// Get unique conversions that would be deleted
const conversionsToDelete = suffixUnitConversions.filter(c => {
Comment thread
huss marked this conversation as resolved.
const otherId = c.sourceId === suffixUnit.id ? c.destinationId : c.sourceId;
const otherUnit = unitDataById[otherId];
return isSuffixRelated(otherUnit);
});

// Check for meters/groups using affected suffix units
const affectedSuffixUnitIds = new Set(suffixTypeUnitsToDelete.map(u => u.id));
affectedSuffixUnitIds.add(suffixUnit.id); // Also check the main suffix unit
Comment thread
huss marked this conversation as resolved.
Outdated

// Check meters using these units (as unitId or defaultGraphicUnit)
const affectedMetersList = Object.values(meterDataById).filter(meter =>
affectedSuffixUnitIds.has(meter.unitId) ||
(meter.defaultGraphicUnit !== null && affectedSuffixUnitIds.has(meter.defaultGraphicUnit))
Comment thread
huss marked this conversation as resolved.
Outdated
);

// Check groups using these units (as defaultGraphicUnit)
const affectedGroupsList = Object.values(groupDataById).filter(group =>
group.defaultGraphicUnit !== null && affectedSuffixUnitIds.has(group.defaultGraphicUnit)
Comment thread
huss marked this conversation as resolved.
Outdated
);

// Display dependency warnings, affected meters and groups if any
if (affectedMetersList.length > 0 || affectedGroupsList.length > 0) {
msgElements.push(
<div key="suffix-dependencies-warning">
<span className="bold">{translate('conversion.delete.suffix.dependencies.warning')}</span>
{affectedMetersList.length > 0 && (
<div style={{ marginTop: '8px' }}>
Comment thread
huss marked this conversation as resolved.
Outdated
<span className="bold">{translate('conversion.delete.suffix.meters.affected')}:</span>
<ul>
{affectedMetersList.map(m => (
<li key={m.id}>"{m.name}"</li>
))}
</ul>
</div>
)}
{affectedGroupsList.length > 0 && (
<div style={{ marginTop: '8px' }}>
<span className="bold">{translate('conversion.delete.suffix.groups.affected')}:</span>
<ul>
{affectedGroupsList.map(g => (
<li key={g.id}>"{g.name}"</li>
))}
</ul>
</div>
)}
</div>
);
}

// Display units that will be cascade deleted
if (suffixTypeUnitsToDelete.length > 0) {
msgElements.push(
<div key="suffix-units-to-hide">
<span className="bold">{translate('conversion.delete.suffix.units.to.delete')}:</span>
<ul>
{suffixTypeUnitsToDelete.map(u => (
<li key={u.id}>"{u.name}" </li>
))}
</ul>
</div>
);
}

// List the conversions that will be cascade deleted
if (conversionsToDelete.length > 0) {
msgElements.push(
<div key="suffix-conversions-to-delete">
<span className="bold">{translate('conversion.delete.suffix.conversions.to.delete')}:</span>
<ul>
{conversionsToDelete.map((c, idx) => {
const s = unitDataById[c.sourceId]?.name || unitDataById[c.sourceId]?.identifier || c.sourceId;
Comment thread
huss marked this conversation as resolved.
Outdated
const d = unitDataById[c.destinationId]?.name || unitDataById[c.destinationId]?.identifier || c.destinationId;
const bidirectional = c.bidirectional ? ' ↔ ' : ' → ';
Comment thread
huss marked this conversation as resolved.
Outdated
return (
<li key={`${c.sourceId}-${c.destinationId}-${idx}`}>
"{s}"{bidirectional}"{d}"
</li>
);
})}
</ul>
</div>
);
}
}

// Only run simulation if the previous orphan check passed and it's unit-to-unit
if (source.typeOfUnit !== UnitType.suffix && dest.typeOfUnit === UnitType.unit && !cancel) {
// Run simulation if the previous orphan check passed and it's not cancelled
// Now supports suffix units since simulation has been enhanced
if (isSuffixRelated(source) && dest.typeOfUnit === UnitType.unit && !cancel) {
try {
const result = await triggerSimulate({
sourceId: state.sourceId,
Expand All @@ -249,6 +365,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC
cancel = true;
} else {
// Group meters by lostUnits
// Display all meters that are loosing the same set of units
Comment thread
huss marked this conversation as resolved.
Outdated
const meterLossMap = new Map<string, string[]>();
result.affectedMeters.forEach(meter => {
const key = JSON.stringify([...meter.lostUnits].sort());
Expand Down Expand Up @@ -277,6 +394,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC
});

// Group non-orphaned groups by lostUnits
// Display all groups loosing the same set of units
Comment thread
huss marked this conversation as resolved.
Outdated
const groupLossMap = new Map<string, string[]>();
const nonOrphanedGroups = result.affectedGroups?.filter(group => !group.orphaned);
nonOrphanedGroups.forEach(group => {
Expand Down Expand Up @@ -342,10 +460,12 @@ export default function EditConversionModalComponent(props: EditConversionModalC
);
}
});
// Save twhich meters.groups need their default unit cleared
Comment thread
huss marked this conversation as resolved.
Outdated
setMetersWithLostDefault(metersLostDefault);
setGroupsWithLostDefault(groupsLostDefault);
}
} catch (e) {
// If the simulation fails block the delete
msgElements.push(
<div key="simulation-error">
{translate('conversion.delete.simulation.error')}
Expand All @@ -354,7 +474,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC
cancel = true;
}
}

// Route to appropriate modal based on any above set cancle condition
Comment thread
huss marked this conversation as resolved.
Outdated
if (cancel) {
msgElements.push(
<div key="restricted">
Expand All @@ -365,6 +485,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC
setDeleteConfirmationMessage(msgElements);
handleCancelModalOpen();
} else {
// Confirm deletion if no blcoking issues
Comment thread
huss marked this conversation as resolved.
Outdated
msgElements.push(
Comment thread
huss marked this conversation as resolved.
<div key="final-confirm">
{translate('conversion.delete.conversion')} [{props.conversionIdentifier}] ?
Expand All @@ -373,6 +494,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC
setDeleteConfirmationMessage(msgElements);
handleDeleteConfirmationModalOpen();
}

};
/* Confirm Delete Modal */
// Separate from state comment to keep everything related to the warning confirmation modal together
Expand All @@ -399,14 +521,16 @@ export default function EditConversionModalComponent(props: EditConversionModalC
// Closes the warning modal
// Do not call the handler function because we do not want to open the parent modal
setShowDeleteConfirmationModal(false);

// Bundle the conversions, meters, and groups to delete
Comment thread
huss marked this conversation as resolved.
// Backend will cleat the references as part of the same delete
const payload = {
sourceId: state.sourceId,
destinationId: state.destinationId,
meterIds: metersWithLostDefault,
groupIds: groupsWithLostDefault
};
deleteConversion(payload)
// Send delete request
.unwrap()
.then(() => {
// Show source/destination identifiers (not numeric IDs)
Expand All @@ -416,6 +540,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC
', ' + translate('conversion.destination') + ' "' + unitDataById[payload.destinationId]?.identifier + '")'
);
}).catch(error => {
// Show any backend error messages alongside identifiers
showErrorNotification(
translate('conversion.delete.failure') +
' (' + translate('conversion.source') + ' "' + unitDataById[payload.sourceId]?.identifier + '"' +
Expand Down
7 changes: 7 additions & 0 deletions src/client/app/translations/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ const LocaleTranslationData = {
"conversion.delete.restricted": "This conversion cannot be deleted until the significant consequences of doing this are addressed.",
"conversion.delete.simulation.error": "There was an error simulating the deletion of this conversion. Please try again later.",
"conversion.delete.suffix.disable": "Deleting this suffix conversion will disable use of suffix",
"conversion.delete.suffix.conversions.to.delete": "The following conversions will also be deleted",
"conversion.delete.suffix.units.to.delete": "The following units will also be deleted",
"conversion.delete.suffix.dependencies.warning": "Warning: Some meters or groups depend on units that will be affected",
Comment thread
huss marked this conversation as resolved.
"conversion.delete.suffix.meters.affected": "Affected meters",
"conversion.delete.suffix.groups.affected": "Affected groups",
"conversion.delete.unit": "Deleting this conversion between two units of type unit may reduce the possible graphing units for some meter(s) but cannot be known until after the deletion happens.",
"conversion.delete.unit.orphan": "Deleting this unit conversion will orphan unit",
"conversion.destination": "Destination:",
Expand Down Expand Up @@ -700,6 +705,7 @@ const LocaleTranslationData = {
"conversion.delete.meter.reduce.graphable": "Deleting this meter conversion will reduce the number of graphable units for meters using meter unit\u{26A1}",
"conversion.delete.restricted": "This conversion cannot be deleted until the significant consequences of doing this are addressed.\u{26A1}",
"conversion.delete.simulation.error": "There was an error simulating the deletion of this conversion. Please try again later.\u{26A1}",
"conversion.delete.suffix.conversions.to.delete": "The following conversions will also be deleted.\u{26A1}",
"conversion.delete.suffix.disable": "Deleting this suffix conversion will disable use of suffix\u{26A1}",
"conversion.delete.unit": "Deleting this conversion between two units of type unit may reduce the possible graphing units for some meter(s) but cannot be known until after the deletion happens.\u{26A1}",
"conversion.delete.unit.orphan": "Deleting this unit conversion will orphan unit\u{26A1}",
Expand Down Expand Up @@ -1309,6 +1315,7 @@ const LocaleTranslationData = {
"conversion.delete.meter.reduce.graphable": "Deleting this meter conversion will reduce the number of graphable units for meters using meter unit\u{26A1}",
"conversion.delete.restricted": "This conversion cannot be deleted until the significant consequences of doing this are addressed.\u{26A1}",
"conversion.delete.simulation.error": "There was an error simulating the deletion of this conversion. Please try again later.\u{26A1}",
"conversion.delete.suffix.conversions.to.delete": "The following conversions will also be deleted\u{26A1}",
"conversion.delete.suffix.disable": "Deleting this suffix conversion will disable use of suffix\u{26A1}",
"conversion.delete.unit": "Deleting this conversion between two units of type unit may reduce the possible graphing units for some meter(s) but cannot be known until after the deletion happens.\u{26A1}",
"conversion.delete.unit.orphan": "Deleting this unit conversion will orphan unit\u{26A1}",
Expand Down
1 change: 1 addition & 0 deletions src/server/models/Conversion.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class Conversion {
destination: destination
});
}

Comment thread
huss marked this conversation as resolved.
Outdated
}

module.exports = Conversion;
Loading