From 9fc4374df306c9f02bf1afb5be5c4c1685812586 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sun, 21 Jun 2026 20:49:17 +0100 Subject: [PATCH] Implemented the floor-level ordering pass using the existing integer SortOrder as the user-facing Level. --- PlotLine/Services/CoreServices.cs | 16 +++++++--- PlotLine/Sql/076_FloorPlanFloorLevels.sql | 31 ++++++++++++++++++ PlotLine/ViewModels/CoreViewModels.cs | 2 ++ PlotLine/Views/FloorPlans/Edit.cshtml | 39 ++++++++++++++++++++--- 4 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 PlotLine/Sql/076_FloorPlanFloorLevels.sql diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index 1632a2e..8db386a 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -7563,6 +7563,7 @@ public sealed class FloorPlanService( } var projectLocations = await locations.ListByProjectAsync(project.ProjectID); + var orderedFloors = data.Floors.OrderBy(x => x.SortOrder).ThenBy(x => x.Name).ToList(); return new FloorPlanEditorViewModel { Project = project, @@ -7576,27 +7577,27 @@ public sealed class FloorPlanService( Name = data.FloorPlan.Name, Description = data.FloorPlan.Description }, - Floors = data.Floors.Select(ToFloorEdit).ToList(), + Floors = orderedFloors.Select(ToFloorEdit).ToList(), Blocks = data.Blocks.Select(ToBlockEdit).ToList(), Transitions = data.Transitions.Select(ToTransitionEdit).ToList(), NewFloor = new FloorPlanFloorEditViewModel { FloorPlanID = data.FloorPlan.FloorPlanID, Name = "New floor", - SortOrder = data.Floors.Any() ? data.Floors.Max(x => x.SortOrder) + 10 : 10, + SortOrder = orderedFloors.Any() ? orderedFloors.Max(x => x.SortOrder) + 1 : 0, GridWidth = 24, GridHeight = 16 }, NewBlock = new FloorPlanBlockEditViewModel { - FloorPlanFloorID = data.Floors.FirstOrDefault()?.FloorPlanFloorID ?? 0, + FloorPlanFloorID = orderedFloors.FirstOrDefault()?.FloorPlanFloorID ?? 0, BlockType = FloorPlanBlockTypes.Room, WidthCells = 2, HeightCells = 1 }, NewTransition = new FloorPlanTransitionEditViewModel { - FloorPlanFloorID = data.Floors.FirstOrDefault()?.FloorPlanFloorID ?? 0, + FloorPlanFloorID = orderedFloors.FirstOrDefault()?.FloorPlanFloorID ?? 0, TransitionType = FloorPlanTransitionTypes.Door }, LocationOptions = ToOptionalLocationOptions(projectLocations, "Create/link automatically"), @@ -7627,7 +7628,7 @@ public sealed class FloorPlanService( FloorPlanID = floorPlanId, LocationID = floorLocationId, Name = "Ground Floor", - SortOrder = 10, + SortOrder = 0, GridWidth = 24, GridHeight = 16 }); @@ -8005,6 +8006,11 @@ public sealed class FloorPlanService( private static void ValidateFloor(FloorPlanFloorEditViewModel model) { + if (model.SortOrder is < -10 or > 50) + { + throw new InvalidOperationException("Floor level must be between -10 and 50."); + } + if (model.GridWidth is < 8 or > 80 || model.GridHeight is < 8 or > 80) { throw new InvalidOperationException("Grid width and height must be between 8 and 80."); diff --git a/PlotLine/Sql/076_FloorPlanFloorLevels.sql b/PlotLine/Sql/076_FloorPlanFloorLevels.sql new file mode 100644 index 0000000..f0f087a --- /dev/null +++ b/PlotLine/Sql/076_FloorPlanFloorLevels.sql @@ -0,0 +1,31 @@ +-- Reinterpret the existing FloorPlanFloors.SortOrder integer as user-facing floor Level. +-- This preserves current relative order for legacy 10-step SortOrder data without adding schema. + +IF OBJECT_ID(N'dbo.FloorPlanFloors', N'U') IS NOT NULL +BEGIN + ;WITH LegacyPlans AS + ( + SELECT FloorPlanID + FROM dbo.FloorPlanFloors + GROUP BY FloorPlanID + HAVING COUNT(*) > 0 + AND MIN(SortOrder) >= 10 + AND SUM(CASE WHEN SortOrder % 10 = 0 THEN 0 ELSE 1 END) = 0 + ), + OrderedFloors AS + ( + SELECT + fpf.FloorPlanFloorID, + ROW_NUMBER() OVER (PARTITION BY fpf.FloorPlanID ORDER BY fpf.SortOrder, fpf.Name, fpf.FloorPlanFloorID) - 1 AS FloorLevel + FROM dbo.FloorPlanFloors fpf + INNER JOIN LegacyPlans lp ON lp.FloorPlanID = fpf.FloorPlanID + ) + UPDATE fpf + SET + SortOrder = ordered.FloorLevel, + ModifiedDate = SYSUTCDATETIME() + FROM dbo.FloorPlanFloors fpf + INNER JOIN OrderedFloors ordered ON ordered.FloorPlanFloorID = fpf.FloorPlanFloorID + WHERE fpf.SortOrder <> ordered.FloorLevel; +END; +GO diff --git a/PlotLine/ViewModels/CoreViewModels.cs b/PlotLine/ViewModels/CoreViewModels.cs index 0a1de89..fe5964f 100644 --- a/PlotLine/ViewModels/CoreViewModels.cs +++ b/PlotLine/ViewModels/CoreViewModels.cs @@ -991,6 +991,8 @@ public sealed class TimelineMetricSettingOptionViewModel public int MetricTypeID { get; set; } public string MetricName { get; set; } = string.Empty; public string? Description { get; set; } + [Display(Name = "Level")] + [Range(-10, 50)] public int SortOrder { get; set; } public bool IsSelected { get; set; } } diff --git a/PlotLine/Views/FloorPlans/Edit.cshtml b/PlotLine/Views/FloorPlans/Edit.cshtml index 138b858..14873a3 100644 --- a/PlotLine/Views/FloorPlans/Edit.cshtml +++ b/PlotLine/Views/FloorPlans/Edit.cshtml @@ -113,12 +113,15 @@

Add floor

-
+
+ + +
@@ -131,6 +134,7 @@
+

Used to order floors and later to connect stairs up/down.

@@ -564,11 +568,15 @@

Add floor

-
+
+ + +
Used to order floors and later to connect stairs up/down.
+
@@ -590,12 +598,16 @@
-
+
+ + +
Used to order floors and later to connect stairs up/down.
+

Location: @(string.IsNullOrWhiteSpace(floor.LocationPath) ? "A child Location will be created or linked on save." : floor.LocationPath)

@@ -907,6 +919,7 @@ let saveTimer = null; let saveInFlight = false; let saveAgain = false; + let refreshAfterSave = false; const backgroundTimers = new Map(); function setToolboxTab(tabName) { @@ -1068,6 +1081,10 @@ } setSaveStatus("saved"); + if (refreshAfterSave) { + refreshAfterSave = false; + await refreshWorkspaceAsync(workspaceState()); + } } catch (error) { setSaveStatus("failed", error.message || "Save failed"); } finally { @@ -1831,11 +1848,25 @@ }); }); + editor.querySelectorAll("[data-floor-level]").forEach(input => { + const updateFloorLevel = (clampValue = false) => { + const parsed = Number(input.value); + if (!Number.isFinite(parsed)) return; + if (clampValue && Number.isFinite(parsed)) { + input.value = Math.max(-10, Math.min(parsed, 50)); + } + refreshAfterSave = true; + markDirty(); + }; + input.addEventListener("input", () => updateFloorLevel()); + input.addEventListener("change", () => updateFloorLevel(true)); + }); + editor.querySelectorAll("input, select, textarea").forEach(input => { if (input.matches("[form='floor-plan-add-block-form'], [form='floor-plan-add-floor-form']")) return; if (input.matches("[form^='floor-plan-background-'], [form^='floor-plan-transition-add-']")) return; if (input.matches("[data-background-field]")) return; - if (input.matches("[data-floor-width],[data-floor-height]")) return; + if (input.matches("[data-floor-width],[data-floor-height],[data-floor-level]")) return; if (input.matches("[data-block-field='x'],[data-block-field='y'],[data-block-field='w'],[data-block-field='h'],[data-block-field='type'],[data-block-field='location']")) return; const eventName = input.tagName === "TEXTAREA" || input.type === "text" ? "input" : "change"; input.addEventListener(eventName, markDirty);