From 256973bfa0de1027e67f6ff96af81168b49c243f Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Tue, 2 Jun 2026 14:21:53 -0500 Subject: [PATCH 01/16] Add unmerging functionality to the editor --- .../layoutEditor/EditorContextMenu.tsx | 61 +++++++++---- .../context/LayoutEditorContextManager.tsx | 88 ++++++++++++++++--- .../client/types/layoutEditorContextTypes.ts | 1 + 3 files changed, 120 insertions(+), 30 deletions(-) diff --git a/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx b/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx index 31604f82d..629d1a975 100644 --- a/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx +++ b/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx @@ -17,13 +17,13 @@ */ import * as React from 'react'; -import { FC, ReactElement, useEffect, useRef } from 'react'; +import { FC, ReactElement, useEffect, useRef, useState } from 'react'; import '../../cageui.scss'; import { Button } from 'react-bootstrap'; import { parseRoomItemType, stringToRoomItem } from '../../utils/helpers'; import { Cage, - DefaultRackTypes, + DefaultRackTypes, Rack, RackGroup, RackStringType, RackTypes, RoomItemType, @@ -31,6 +31,8 @@ import { RoomObjectTypes } from '../../types/typings'; import { SelectedObj } from '../../types/layoutEditorTypes'; +import { useLayoutEditorContext } from '../../context/LayoutEditorContextManager'; +import { findCageInGroup } from '../../utils/LayoutEditorHelpers'; interface Option { label: string; @@ -64,23 +66,19 @@ export const EditorContextMenu: FC = (props) => { type } = props; + const {localRoom, unmergeRacks} = useLayoutEditorContext(); const menuRef = useRef(null); - // Delete object for room objects - const handleDeleteObject = (e: React.MouseEvent) => { - e.stopPropagation(); - onClickDelete(); - }; + const [selectedRack, setSelectedRack] = useState(); + const [selectedRackGroup, setSelectedRackGroup] = useState(); - // Delete cage and rack for caging units - const handleDeleteCage = (e: React.MouseEvent) => { - e.stopPropagation(); - onClickDelete('cage'); - }; - const handleDeleteRack = (e: React.MouseEvent) => { - e.stopPropagation(); - onClickDelete('rack'); - }; + useEffect(() => { + if(selectedObj.selectionType === 'cage'){ + const {rack, rackGroup} = findCageInGroup((selectedObj as Cage).svgId, localRoom.rackGroups); + setSelectedRack(rack); + setSelectedRackGroup(rackGroup); + } + }, [selectedObj]); useEffect(() => { const handleClickOutside = (event) => { @@ -136,12 +134,34 @@ export const EditorContextMenu: FC = (props) => { menu.style.top = `${adjustedTop}px`; }, [ctxMenuStyle.display, ctxMenuStyle.left, ctxMenuStyle.top]); + + // Delete object for room objects + const handleDeleteObject = (e: React.MouseEvent) => { + e.stopPropagation(); + onClickDelete(); + }; + + // Delete cage and rack for caging units + const handleDeleteCage = (e: React.MouseEvent) => { + e.stopPropagation(); + onClickDelete('cage'); + }; + const handleDeleteRack = (e: React.MouseEvent) => { + e.stopPropagation(); + onClickDelete('rack'); + }; + + const handleUnmergeRack = (e: React.MouseEvent) => { + e.stopPropagation(); + unmergeRacks(selectedRackGroup, selectedRack); + } + return (
{menuItems && menuItems.map((item, index) => { @@ -186,6 +206,13 @@ export const EditorContextMenu: FC = (props) => { > Delete Rack + +
} diff --git a/CageUI/src/client/context/LayoutEditorContextManager.tsx b/CageUI/src/client/context/LayoutEditorContextManager.tsx index 607b9115c..8415d034a 100644 --- a/CageUI/src/client/context/LayoutEditorContextManager.tsx +++ b/CageUI/src/client/context/LayoutEditorContextManager.tsx @@ -289,6 +289,15 @@ export const LayoutEditorContextProvider: FC = ({children, p }); }; + const getNewGroupId = () => { + const newId = nextAvailGroup; + setNextAvailGroup(prevState => { + const nextId = parseLongId(prevState) + 1; + return `rack-group-${nextId}` as GroupId; + }); + return newId; + } + // This only adds default racks/cages to the layout, it is not used in loading in previous layouts const addRack = async (id: number, x: number, y: number, newScale: number, rackType: RackTypes): Promise => { const newCageNum: CageNumber = `${roomItemToString(rackType) as RackStringType}-${getNextCageNum(roomItemToString(rackType) as RackStringType)}`; @@ -367,7 +376,7 @@ export const LayoutEditorContextProvider: FC = ({children, p const newRackGroup: RackGroup = { selectionType: 'rackGroup', - groupId: nextAvailGroup, + groupId: getNewGroupId(), racks: [newRack], rotation: GroupRotation.Quarter, x: x, @@ -375,10 +384,6 @@ export const LayoutEditorContextProvider: FC = ({children, p scale: newScale, }; - setNextAvailGroup(prevState => { - const nextId = parseLongId(prevState) + 1; - return `rack-group-${nextId}` as GroupId; - }); setLocalRoom(prevRoom => ({ ...prevRoom, rackGroups: [...prevRoom.rackGroups, newRackGroup] @@ -844,7 +849,6 @@ export const LayoutEditorContextProvider: FC = ({children, p // 6. Split groups based on components let finalGroups = updatedGroups.filter(g => g.groupId !== location.rackGroup.groupId); - let nextGroupId = nextAvailGroup; // In the group splitting logic: if (components.size > 1) { @@ -948,15 +952,11 @@ export const LayoutEditorContextProvider: FC = ({children, p finalGroups.push({ ...affectedGroup, - groupId: nextGroupId, + groupId: getNewGroupId(), x: minX, y: minY, racks: newRacks }); - - // Update next group ID - const nextIdNum = parseInt(nextGroupId.split('-')[2]) + 1; - nextGroupId = `rack-group-${nextIdNum}` as GroupId; } } else { // No splitting needed, keep the modified group @@ -964,7 +964,6 @@ export const LayoutEditorContextProvider: FC = ({children, p } // 7. Update state - setNextAvailGroup(nextGroupId); setLocalRoom(prev => ({ ...prev, rackGroups: finalGroups @@ -1137,6 +1136,68 @@ export const LayoutEditorContextProvider: FC = ({children, p setCageNumChange({before: numBefore, after: numAfter}); }; + /* + Effectively unconnects the selectedRack from any connections with other racks. It does this by removing it from + the current rack group and creating a new rack group for the selected rack. + + //TODO how to handle unconnecting when the selected rack is in the middle of multiple racks? + */ + const unmergeRacks = (rackGroup: RackGroup, selectedRack: Rack) => { + const newRoom: Room = { ...localRoom }; + + // 1. Find the index of the rack group that contains the selected rack + const rackGroupIndex = newRoom.rackGroups.findIndex(group => + group.groupId === rackGroup.groupId + ); + + if (rackGroupIndex === -1) return; + + // 2. Create the updated original rack group (without the selected rack) + let updatedOriginalRacks = rackGroup.racks.filter(rack => rack.objectId !== selectedRack.objectId); + let updatedOriginalGroup = { ...rackGroup }; + + if (updatedOriginalRacks.length > 0) { + // Normalize the original group: find the new top-left corner + const minX = Math.min(...updatedOriginalRacks.map(r => r.x)); + const minY = Math.min(...updatedOriginalRacks.map(r => r.y)); + + updatedOriginalGroup = { + ...rackGroup, + x: rackGroup.x + minX, + y: rackGroup.y + minY, + racks: updatedOriginalRacks.map(r => ({ + ...r, + x: r.x - minX, + y: r.y - minY + })) + }; + newRoom.rackGroups[rackGroupIndex] = updatedOriginalGroup; + } else { + // If no racks left, remove the group entirely + newRoom.rackGroups.splice(rackGroupIndex, 1); + } + + // 3. Create the new rack group for the unmerged rack + // The new group starts at the global position of the selected rack + const newRackGroup: RackGroup = { + ...rackGroup, + groupId: getNewGroupId(), + x: rackGroup.x + selectedRack.x, + y: rackGroup.y + selectedRack.y, + racks: [{ + ...selectedRack, + x: 0, // Reset local coordinates to 0,0 in the new group + y: 0 + }] + }; + + // 4. Update the room state + newRoom.rackGroups = [...newRoom.rackGroups, newRackGroup]; + + setLocalRoom(newRoom); + setReloadRoom(newRoom); + }; + const getNextCageNum = (rackType: RackStringType) => { const cages = unitLocs[rackType]; @@ -1211,7 +1272,8 @@ export const LayoutEditorContextProvider: FC = ({children, p user, getAdjCages, reloadRoom, - setReloadRoom + setReloadRoom, + unmergeRacks }}> {!isLoading ? children : null} diff --git a/CageUI/src/client/types/layoutEditorContextTypes.ts b/CageUI/src/client/types/layoutEditorContextTypes.ts index 74661d169..7fb6a245e 100644 --- a/CageUI/src/client/types/layoutEditorContextTypes.ts +++ b/CageUI/src/client/types/layoutEditorContextTypes.ts @@ -70,4 +70,5 @@ export interface LayoutContextType { getAdjCages: (cage: Cage, cageLoc: LocationCoords) => LocationCoords[]; reloadRoom: Room, setReloadRoom: React.Dispatch>, + unmergeRacks: (rackGroup: RackGroup, selectedRack: Rack) => void; } \ No newline at end of file From e2266bac85dba710918127f451a275cfc7ab8d68 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Tue, 2 Jun 2026 16:28:28 -0500 Subject: [PATCH 02/16] Update unmerging to fix mods for adjacent cages --- .../context/LayoutEditorContextManager.tsx | 50 ++++++- .../src/client/utils/LayoutEditorHelpers.ts | 2 +- CageUI/src/client/utils/helpers.ts | 136 ++++++++++++++---- 3 files changed, 155 insertions(+), 33 deletions(-) diff --git a/CageUI/src/client/context/LayoutEditorContextManager.tsx b/CageUI/src/client/context/LayoutEditorContextManager.tsx index 8415d034a..1ff6041de 100644 --- a/CageUI/src/client/context/LayoutEditorContextManager.tsx +++ b/CageUI/src/client/context/LayoutEditorContextManager.tsx @@ -58,11 +58,14 @@ import { getTranslation, isRackEnum, showLayoutEditorError, + checkAdjacent } from '../utils/LayoutEditorHelpers'; import * as d3 from 'd3'; import { + cageDirectionToModLocation, generateCageId, generateUUID, + getAdjLocation, getNextDefaultRackId, getSvgSize, parseLongId, @@ -1152,8 +1155,53 @@ export const LayoutEditorContextProvider: FC = ({children, p if (rackGroupIndex === -1) return; + const removedModIds: string[] = []; + const otherRacks = rackGroup.racks.filter(r => r.objectId !== selectedRack.objectId); + + // Process modifications between selectedRack and other racks in the group + selectedRack.cages.forEach(selectedCage => { + const selectedCageLoc = getCageLoc(selectedCage.svgId, selectedCage.cageNum); + if (!selectedCageLoc) return; + + otherRacks.forEach(otherRack => { + otherRack.cages.forEach(otherCage => { + const otherCageLoc = getCageLoc(otherCage.svgId, otherCage.cageNum); + if (!otherCageLoc) return; + + const adjResult = checkAdjacent(otherCageLoc, selectedCageLoc, selectedCage.size, otherCage.size); + if (adjResult.isAdjacent) { + const location = cageDirectionToModLocation(adjResult.direction, rackGroup.rotation); + const adjLocation = getAdjLocation(location); + + // Remove from selectedCage + if (selectedCage.mods && selectedCage.mods[location]) { + selectedCage.mods[location].forEach(mod => { + mod.modKeys.forEach(key => removedModIds.push(key.modId)); + }); + selectedCage.mods[location] = []; + } + + // Remove from otherCage + if (otherCage.mods && otherCage.mods[adjLocation]) { + otherCage.mods[adjLocation].forEach(mod => { + mod.modKeys.forEach(key => removedModIds.push(key.modId)); + }); + otherCage.mods[adjLocation] = []; + } + } + }); + }); + }); + + // Remove collected modIds from room.mods + if (newRoom.mods) { + removedModIds.forEach(id => { + delete newRoom.mods[id]; + }); + } + // 2. Create the updated original rack group (without the selected rack) - let updatedOriginalRacks = rackGroup.racks.filter(rack => rack.objectId !== selectedRack.objectId); + let updatedOriginalRacks = otherRacks; let updatedOriginalGroup = { ...rackGroup }; if (updatedOriginalRacks.length > 0) { diff --git a/CageUI/src/client/utils/LayoutEditorHelpers.ts b/CageUI/src/client/utils/LayoutEditorHelpers.ts index d339357a0..0f8e4fa85 100644 --- a/CageUI/src/client/utils/LayoutEditorHelpers.ts +++ b/CageUI/src/client/utils/LayoutEditorHelpers.ts @@ -830,7 +830,7 @@ export function checkAdjacent(targetCage: LocationCoords, draggedCage: LocationC } } - return {isAdjacent: false, direction: '0'}; + return {isAdjacent: false, direction: null}; } //Offset for the top left corner of the layout, without doing this objects will randomly jump when dragging and placing diff --git a/CageUI/src/client/utils/helpers.ts b/CageUI/src/client/utils/helpers.ts index 58555b31f..f169eaa78 100644 --- a/CageUI/src/client/utils/helpers.ts +++ b/CageUI/src/client/utils/helpers.ts @@ -19,6 +19,7 @@ import { AllHistoryData, Cage, + CageDirection, CageModification, CageModificationsType, CageMods, @@ -865,7 +866,7 @@ export const buildNewLocalRoom = async (prevRoom: PrevRoom): Promise<[Room, Unit x: rackItem.xCoord - rack.x - group.x, // get cage coords by subtracting from both rack and group y: rackItem.yCoord - rack.y - group.y, size: svgSize, - mods: cageMods + mods: cageMods, }; newUnitLocs[cageNumType].push({ @@ -881,6 +882,7 @@ export const buildNewLocalRoom = async (prevRoom: PrevRoom): Promise<[Room, Unit const rackGroup: RackGroup = findOrAddGroup(rackItem); const rack: Rack = await findOrAddRack(rackGroup, rackItem); await addCageToRack(rack, rackItem, rackGroup); + }; // generates room object state for room objects from layout history data @@ -928,6 +930,55 @@ export const getAdjLocation = (loc: ModLocations): ModLocations => { } }; +export const cageDirectionToModLocation = (loc: CageDirection, rotation: GroupRotation): ModLocations => { + if(rotation === GroupRotation.Origin){ // 0 + switch (loc) { + case CageDirection.Left: + return ModLocations.Left; + case CageDirection.Right: + return ModLocations.Right; + case CageDirection.Top: + return ModLocations.Top; + case CageDirection.Bottom: + return ModLocations.Bottom; + } + }else if(rotation === GroupRotation.Quarter){ // 90 + switch (loc) { + case CageDirection.Left: + return ModLocations.Bottom; + case CageDirection.Right: + return ModLocations.Top; + case CageDirection.Top: + return ModLocations.Right; + case CageDirection.Bottom: + return ModLocations.Left; + } + }else if(rotation === GroupRotation.Half){ // 180 + switch (loc) { + case CageDirection.Left: + return ModLocations.Right; + case CageDirection.Right: + return ModLocations.Left; + case CageDirection.Top: + return ModLocations.Bottom; + case CageDirection.Bottom: + return ModLocations.Top; + } + }else if(rotation === GroupRotation.ThreeQuarter){ // 270 + switch (loc) { + case CageDirection.Left: + return ModLocations.Top; + case CageDirection.Right: + return ModLocations.Bottom; + case CageDirection.Top: + return ModLocations.Left; + case CageDirection.Bottom: + return ModLocations.Right; + } + } + +}; + export const getDefaultMod = (loc: ModLocations): ModTypes | null => { if (loc === ModLocations.Top || loc === ModLocations.Bottom) { return ModTypes.StandardFloor; @@ -1304,43 +1355,66 @@ export const saveRoomHelper = async (room: Room, sessionLog: SessionLog, oldTemp // Create default mods for new rooms. if (isRoomNonDefault) { - const usedMap = new Map(); room.rackGroups.forEach((group) => { group.racks.forEach((r) => { r.cages.forEach((c) => { - if (c.mods === undefined || c.mods === null) { - const connectedCages = findConnectedCages(r, group.rotation, c); - Object.entries(connectedCages).forEach(([direction, connections]) => { - if (connections.length === 0) { - return; + const connectedCages = findConnectedCages(r, group.rotation, c); + const connectedRacks = findConnectedRacks(group, r, c); + + // Combine all potential connection directions from both adjacent cages and racks + const allDirections = new Set([ + ...Object.keys(connectedCages), + ...Object.keys(connectedRacks) + ]); + + allDirections.forEach((direction) => { + const locDir = parseInt(direction) as ModLocations; + const cageConnections = connectedCages[locDir] || []; + const rackConnections = connectedRacks[locDir] || []; + + // Only proceed if there is a connection in this direction + if (cageConnections.length > 0 || rackConnections.length > 0) { + if (c.mods && c.mods[locDir] && c.mods[locDir].length > 0) { + // If existing mods exist for this direction, add them + c.mods[locDir].forEach(section => { + section.modKeys.forEach(key => { + newModData.push({ + cage: c.objectId, + location: locDir, + modId: key.modId, + modification: room.mods[key.modId].value, + parentModId: key.parentModId, + rack: r.objectId, + subId: section.subId, + }); + }); + }); + } else { + // If no mods exist for this connection, add default ones + if (cageConnections.length > 0) { + addModEntries(cageConnections, locDir, r, false, newModData, usedMap); + } + if (rackConnections.length > 0) { + addModEntries(rackConnections, locDir, r, true, newModData, usedMap); + } } - const locDir = parseInt(direction) as ModLocations; - addModEntries(connections, locDir, r, false, newModData, usedMap); - }); + } + }); - const connectedRacks = findConnectedRacks(group, r, c); - Object.entries(connectedRacks).forEach(([direction, connections]) => { - if (connections.length === 0) { - return; - } - const locDir = parseInt(direction) as ModLocations; - addModEntries(connections, locDir, r, true, newModData, usedMap); - }); - } else { - Object.entries(c.mods).forEach(([direction, modSubsections]: [string, CageModification[]]) => { - modSubsections.forEach(section => { - section.modKeys.forEach(key => { - newModData.push({ - cage: c.objectId, - location: parseInt(direction), - modId: key.modId, - modification: room.mods[key.modId].value, - parentModId: key.parentModId, - rack: r.objectId, - subId: section.subId, - }); + // Handle Direct location mods (not used in connections) + if (c.mods && c.mods[ModLocations.Direct] && c.mods[ModLocations.Direct].length > 0) { + c.mods[ModLocations.Direct].forEach(section => { + section.modKeys.forEach(key => { + newModData.push({ + cage: c.objectId, + location: ModLocations.Direct, + modId: key.modId, + modification: room.mods[key.modId].value, + parentModId: key.parentModId, + rack: r.objectId, + subId: section.subId, }); }); }); From e4f3189009c18ad8d578fd5df1bc5edc431efd75 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Tue, 2 Jun 2026 16:31:38 -0500 Subject: [PATCH 03/16] Close context menu after merge --- CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx b/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx index 629d1a975..0db953cbd 100644 --- a/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx +++ b/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx @@ -154,6 +154,7 @@ export const EditorContextMenu: FC = (props) => { const handleUnmergeRack = (e: React.MouseEvent) => { e.stopPropagation(); unmergeRacks(selectedRackGroup, selectedRack); + closeMenu(); } return ( From 751a9a2fa2490dbe7e2a518c22f20dc8a502297b Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Wed, 3 Jun 2026 12:57:33 -0500 Subject: [PATCH 04/16] disable unmerging if the racks are not connected --- .../components/layoutEditor/EditorContextMenu.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx b/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx index 0db953cbd..9990e64c3 100644 --- a/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx +++ b/CageUI/src/client/components/layoutEditor/EditorContextMenu.tsx @@ -208,12 +208,14 @@ export const EditorContextMenu: FC = (props) => { Delete Rack - + {(selectedRackGroup && selectedRackGroup.racks.length > 1) && + + } } From fc97c844fecd60cd4e7a27ac1f5e6adfeaaf8f6d Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Wed, 3 Jun 2026 14:25:19 -0500 Subject: [PATCH 05/16] Added automatic scroll to view for the room list component based on the url --- .../src/client/components/home/RoomList.tsx | 66 +++++++++++++++++-- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/CageUI/src/client/components/home/RoomList.tsx b/CageUI/src/client/components/home/RoomList.tsx index a6a58c633..3ccc9691e 100644 --- a/CageUI/src/client/components/home/RoomList.tsx +++ b/CageUI/src/client/components/home/RoomList.tsx @@ -17,26 +17,29 @@ */ import * as React from 'react'; -import { FC, useEffect, useState } from 'react'; +import { FC, useEffect, useRef, useState } from 'react'; import '../../cageui.scss'; import { Room } from '../../types/typings'; import { ExpandedRooms, ListCage, ListRack, ListRoom } from '../../types/homeTypes'; import { labkeyActionSelectWithPromise } from '../../api/labkeyActions'; import { buildNewLocalRoom, fetchRoomData } from '../../utils/helpers'; import { useHomeNavigationContext } from '../../context/HomeNavigationContextManager'; -import { Filter } from '@labkey/api'; +import { ActionURL, Filter } from '@labkey/api'; export const RoomList: FC = () => { - const {navigateTo} = useHomeNavigationContext(); + const {navigateTo, selectedPage} = useHomeNavigationContext(); // keeps track of which rooms have already been fetched from layout_history const [expandedRooms, setExpandedRooms] = useState({}); - const [expandedRacks, setExpandedRacks] = useState([]); + const [expandedRacks, setExpandedRacks] = useState>({}); const [allRooms, setAllRooms] = useState([]); // Stores all items fetched on load const [visibleRooms, setVisibleRooms] = useState([]); // Items currently visible const [searchQuery, setSearchQuery] = useState(''); + const roomRefs = useRef>({}); + const listContainerRef = useRef(null); + const handleSearch = (e) => { setSearchQuery(e.target.value); }; @@ -144,6 +147,51 @@ export const RoomList: FC = () => { })); }; + // Auto-expand and scroll based on URL parameters + useEffect(() => { + const roomName = ActionURL.getParameter("room"); + const rackId = ActionURL.getParameter("rack"); + + if (roomName) { + if (!expandedRooms[roomName]) { + toggleExpandRoom(roomName); + } + + if (rackId) { + const rackKey = `${roomName}_${rackId}`; + if (!expandedRacks[rackKey]) { + setExpandedRacks(prev => ({ + ...prev, + [rackKey]: true + })); + } + } + + // Scroll room into view + if (roomRefs.current[roomName] && listContainerRef.current) { + const container = listContainerRef.current; + const element = roomRefs.current[roomName]; + + // Use a short timeout to ensure the DOM has updated (expanded) before we calculate the offset + setTimeout(() => { + if (element && container) { + const containerRect = container.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + // elementRect.top is the distance from viewport top to element top + // containerRect.top is the distance from viewport top to container top + // relativeTop is the distance from container top to element top within the scrollable area + const relativeTop = elementRect.top - containerRect.top + container.scrollTop; + + container.scrollTo({ + top: relativeTop, + behavior: 'auto' + }); + } + }, 100); + } + } + }, [selectedPage, allRooms, visibleRooms]); + const handleRoomClick = (room: ListRoom) => { navigateTo({selected: 'Room', room: room.name}) }; @@ -165,9 +213,15 @@ export const RoomList: FC = () => { className={'room-search'} onChange={handleSearch} /> -
    +
      {visibleRooms.map((room, index) => ( -
      +
      { + if (el) roomRefs.current[room.name] = el; + }} + >
      handleRoomClick(room)} className={`room-dir-header ${expandedRooms[room.name] ? 'open' : ''}`} From 64a49bd571f799538a48a634a30523e7bef6d03d Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Wed, 3 Jun 2026 14:33:14 -0500 Subject: [PATCH 06/16] Remove container as required by labkey 26.3 --- CageUI/src/client/components/layoutEditor/Editor.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CageUI/src/client/components/layoutEditor/Editor.tsx b/CageUI/src/client/components/layoutEditor/Editor.tsx index b83ce70f0..6d4bec36f 100644 --- a/CageUI/src/client/components/layoutEditor/Editor.tsx +++ b/CageUI/src/client/components/layoutEditor/Editor.tsx @@ -750,8 +750,8 @@ const Editor: FC = ({roomSize}) => { if (loadTemplate) { window.location.href = ActionURL.buildURL( ActionURL.getController(), - 'cageui-editLayout', - ActionURL.getContainer(), + 'editLayout', + undefined, {room: localRoom.name} ); } From 27288fdd92be167087760a1806ab64b0fe62c5c2 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Wed, 3 Jun 2026 14:46:22 -0500 Subject: [PATCH 07/16] Fix bug with rack ids not assigning correctly when saving templates --- CageUI/src/org/labkey/cageui/CageUIManager.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/CageUI/src/org/labkey/cageui/CageUIManager.java b/CageUI/src/org/labkey/cageui/CageUIManager.java index ff7804ce4..c01e49eb8 100644 --- a/CageUI/src/org/labkey/cageui/CageUIManager.java +++ b/CageUI/src/org/labkey/cageui/CageUIManager.java @@ -965,17 +965,12 @@ private void submitTemplateLayout(Room room, String historyId, BundledForms bund ArrayList templateForms = new ArrayList<>(); // Process rack groups - int rackGroupIndex = 0; for (RackGroup rackGroup : room.getRackGroups()) { - rackGroupIndex++; // Process racks in this group - int rackIndex = 0; for (Rack rack : rackGroup.getRacks()) { - rackIndex++; - // Process cages in this rack if (rack.getCages() != null) { @@ -983,9 +978,9 @@ private void submitTemplateLayout(Room room, String historyId, BundledForms bund { TemplateLayoutHistoryForm form = new TemplateLayoutHistoryForm(); form.setHistoryId(historyId); - form.setRackGroup(rackGroupIndex); + form.setRackGroup(findLastNumberAfterDash(rackGroup.getGroupId())); form.setGroupRotation(rackGroup.getRotation()); - form.setRack(rackIndex); + form.setRack(rack.getItemId()); form.setCage(findLastNumberAfterDash(cage.getCageNum())); form.setObjectType(rack.getType().getEffectiveRackType().getNumericValue()); form.setExtraContext(cage.getExtraContext() != null ? From 416ae48c6caec834e514c454730d9fd8404b5420 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Wed, 3 Jun 2026 16:22:02 -0500 Subject: [PATCH 08/16] Add new locked divider mod --- CageUI/resources/web/CageUI/static/legend.svg | 131 +++++++++--------- CageUI/src/client/types/typings.ts | 1 + CageUI/src/client/utils/constants.ts | 14 ++ .../src/org/labkey/cageui/model/ModTypes.java | 1 + 4 files changed, 85 insertions(+), 62 deletions(-) diff --git a/CageUI/resources/web/CageUI/static/legend.svg b/CageUI/resources/web/CageUI/static/legend.svg index a64e32c8e..51a97407f 100644 --- a/CageUI/resources/web/CageUI/static/legend.svg +++ b/CageUI/resources/web/CageUI/static/legend.svg @@ -16,112 +16,119 @@ - * limitations under the License. - */ --> - - - + + - - - - - - + + - - - - - + + + + - - - - - + - - + - - - - - - - + + + - Solid Divider + Solid Divider - - Protected Contact Divider + Protected Contact Divider - - Visual Contact Divider + Visual Contact Divider - - Privacy Divider + Privacy Divider - - Standard Floor + Standard Floor - - Mesh Floor + Mesh Floor - - Mesh Floor x2 + Mesh Floor x2 - Extension - - + C-Tunnel - - - - + - - - + - Social Panel Divider + Social Panel Divider - - + - Restraint + Restraint - - - + - Window Blind + Window Blind - - - + + + + + + Locked Divider + \ No newline at end of file diff --git a/CageUI/src/client/types/typings.ts b/CageUI/src/client/types/typings.ts index cd307d423..60e33a206 100644 --- a/CageUI/src/client/types/typings.ts +++ b/CageUI/src/client/types/typings.ts @@ -69,6 +69,7 @@ export enum ModTypes { PCDivider = 'pcd', // protected contact VCDivider = 'vcd', // visual contact PrivacyDivider = 'pd', + LockedDivider = 'ld', NoDivider = 'nd', CTunnel = 'ct', Extension = 'ex', diff --git a/CageUI/src/client/utils/constants.ts b/CageUI/src/client/utils/constants.ts index 8ef19e5cf..b28eef1e5 100644 --- a/CageUI/src/client/utils/constants.ts +++ b/CageUI/src/client/utils/constants.ts @@ -214,6 +214,20 @@ export const Modifications: ModRecord = { value: '4' }] }, + [ModTypes.LockedDivider]: { + name: 'Locked Divider', + svgIds: { + [ModLocations.Left]: LocationWithRotationMap[ModLocations.Left], + [ModLocations.Right]: LocationWithRotationMap[ModLocations.Right], + }, + styles: [{ + property: 'stroke', + value: '#ed1c24' + }, { + property: 'stroke-width', + value: '2' + }] + }, [ModTypes.NoDivider]: { name: 'No Divider', svgIds: { diff --git a/CageUI/src/org/labkey/cageui/model/ModTypes.java b/CageUI/src/org/labkey/cageui/model/ModTypes.java index 6497ec2ef..6eb02f82a 100644 --- a/CageUI/src/org/labkey/cageui/model/ModTypes.java +++ b/CageUI/src/org/labkey/cageui/model/ModTypes.java @@ -31,6 +31,7 @@ public enum ModTypes PCDivider("pcd"), VCDivider("vcd"), PrivacyDivider("pd"), + LockedDivider("ld"), NoDivider("nd"), CTunnel("ct"), Extension("ex"), From 1794ad2feca92cf8a32a5eb9ebac120b24f80511 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Wed, 3 Jun 2026 16:22:13 -0500 Subject: [PATCH 09/16] Add screen loading when changing rooms --- CageUI/src/client/components/LoadingScreen.tsx | 5 +++-- CageUI/src/client/components/home/RoomContent.tsx | 10 ++++++++-- CageUI/src/client/components/home/RoomList.tsx | 5 ++++- .../components/home/rackView/ChangeRackPopup.tsx | 1 + .../client/components/home/roomView/RoomLayout.tsx | 8 ++++++-- .../src/client/components/layoutEditor/Editor.tsx | 1 + .../client/context/HomeNavigationContextManager.tsx | 13 ++++++++++++- CageUI/src/client/pages/home/RoomHome.tsx | 4 ++-- .../src/client/types/homeNavigationContextTypes.ts | 2 ++ 9 files changed, 39 insertions(+), 10 deletions(-) diff --git a/CageUI/src/client/components/LoadingScreen.tsx b/CageUI/src/client/components/LoadingScreen.tsx index 821ffdc42..d7c2d91ad 100644 --- a/CageUI/src/client/components/LoadingScreen.tsx +++ b/CageUI/src/client/components/LoadingScreen.tsx @@ -22,11 +22,12 @@ import { createPortal } from 'react-dom'; interface LoadingScreenProps { isVisible: boolean; + message: string; targetElement?: HTMLElement | null; } export const LoadingScreen: FC = (props) => { - const {isVisible, targetElement} = props; + const {isVisible, message, targetElement} = props; const [container, setContainer] = useState(null); @@ -44,7 +45,7 @@ export const LoadingScreen: FC = (props) => {
      -

      Saving...

      +

      {message}

      , container diff --git a/CageUI/src/client/components/home/RoomContent.tsx b/CageUI/src/client/components/home/RoomContent.tsx index 750d30bdc..e057356fe 100644 --- a/CageUI/src/client/components/home/RoomContent.tsx +++ b/CageUI/src/client/components/home/RoomContent.tsx @@ -24,9 +24,10 @@ import { CageViewContent } from './cageView/CageViewContent'; import { RackViewContent } from './rackView/RackViewContent'; import { HomeViewContent } from './HomeViewContent'; import { useHomeNavigationContext } from '../../context/HomeNavigationContextManager'; +import { LoadingScreen } from '../LoadingScreen'; export const RoomContent: FC = () => { - const {selectedPage} = useHomeNavigationContext(); + const {selectedPage, isNavLoading} = useHomeNavigationContext(); const renderContent = () => { switch (selectedPage?.selected) { @@ -43,7 +44,12 @@ export const RoomContent: FC = () => { return (
      - {renderContent()} + + {!isNavLoading && renderContent()}
      ); }; \ No newline at end of file diff --git a/CageUI/src/client/components/home/RoomList.tsx b/CageUI/src/client/components/home/RoomList.tsx index 3ccc9691e..ea0e9552b 100644 --- a/CageUI/src/client/components/home/RoomList.tsx +++ b/CageUI/src/client/components/home/RoomList.tsx @@ -27,7 +27,7 @@ import { useHomeNavigationContext } from '../../context/HomeNavigationContextMan import { ActionURL, Filter } from '@labkey/api'; export const RoomList: FC = () => { - const {navigateTo, selectedPage} = useHomeNavigationContext(); + const {navigateTo, selectedPage, setIsNavLoading} = useHomeNavigationContext(); // keeps track of which rooms have already been fetched from layout_history const [expandedRooms, setExpandedRooms] = useState({}); const [expandedRacks, setExpandedRacks] = useState>({}); @@ -193,14 +193,17 @@ export const RoomList: FC = () => { }, [selectedPage, allRooms, visibleRooms]); const handleRoomClick = (room: ListRoom) => { + setIsNavLoading(true); navigateTo({selected: 'Room', room: room.name}) }; const handleRackClick = (room: ListRoom, rack: ListRack) => { + setIsNavLoading(true); navigateTo({selected: 'Rack', room: room.name, rack: rack.id}); }; const handleCageClick = (room: ListRoom, rack: ListRack, cage: ListCage) => { + setIsNavLoading(true); navigateTo({selected: 'Cage', room: room.name, rack: rack.id, cage: cage.id}); }; diff --git a/CageUI/src/client/components/home/rackView/ChangeRackPopup.tsx b/CageUI/src/client/components/home/rackView/ChangeRackPopup.tsx index 3fa788b35..14ce67738 100644 --- a/CageUI/src/client/components/home/rackView/ChangeRackPopup.tsx +++ b/CageUI/src/client/components/home/rackView/ChangeRackPopup.tsx @@ -167,6 +167,7 @@ export const ChangeRackPopup: FC = (props) => { {isSaving && } diff --git a/CageUI/src/client/components/home/roomView/RoomLayout.tsx b/CageUI/src/client/components/home/roomView/RoomLayout.tsx index a5931267d..cadb12a8e 100644 --- a/CageUI/src/client/components/home/roomView/RoomLayout.tsx +++ b/CageUI/src/client/components/home/roomView/RoomLayout.tsx @@ -122,8 +122,12 @@ export const RoomLayout: FC = (props) => { return (
      - {isSaving && }
      {showChangesMenu && diff --git a/CageUI/src/client/components/layoutEditor/Editor.tsx b/CageUI/src/client/components/layoutEditor/Editor.tsx index 6d4bec36f..b1f8f7c11 100644 --- a/CageUI/src/client/components/layoutEditor/Editor.tsx +++ b/CageUI/src/client/components/layoutEditor/Editor.tsx @@ -874,6 +874,7 @@ const Editor: FC = ({roomSize}) => { {startSaving && } diff --git a/CageUI/src/client/context/HomeNavigationContextManager.tsx b/CageUI/src/client/context/HomeNavigationContextManager.tsx index eace2551b..416fa3392 100644 --- a/CageUI/src/client/context/HomeNavigationContextManager.tsx +++ b/CageUI/src/client/context/HomeNavigationContextManager.tsx @@ -58,6 +58,8 @@ export const HomeNavigationContextProvider: FC = ({u const [selectedRack, setSelectedRack] = useState(null); const [selectedCage, setSelectedCage] = useState(null); + const [isNavLoading, setIsNavLoading] = useState(false); + useEffect(() => { setSelectedLocalRoom(selectedRoom); }, [selectedRoom]); @@ -122,11 +124,14 @@ export const HomeNavigationContextProvider: FC = ({u setSelectedRackGroup(null); setSelectedRack(null); setSelectedCage(null); + setIsNavLoading(false); break; case 'Room': if (page.room) { - loadRoomData(page.room); + loadRoomData(page.room).then((newRoom) => { + setIsNavLoading(false); + }); } break; @@ -138,11 +143,13 @@ export const HomeNavigationContextProvider: FC = ({u const { rack: currRack, rackGroup: currGroup } = findRackInGroup(page.rack, newRoom?.rackGroups || []); setSelectedRack(currRack); setSelectedRackGroup(currGroup); + setIsNavLoading(false); }); } else { const { rack: currRack, rackGroup: currGroup } = findRackInGroup(page.rack, selectedRoom?.rackGroups || []); setSelectedRack(currRack); setSelectedRackGroup(currGroup); + setIsNavLoading(false); } } break; @@ -160,6 +167,7 @@ export const HomeNavigationContextProvider: FC = ({u setSelectedRackGroup(currGroup); setSelectedRack(currRack); setSelectedCage(currCage); + setIsNavLoading(false); }); } else { const { @@ -170,6 +178,7 @@ export const HomeNavigationContextProvider: FC = ({u setSelectedRackGroup(currGroup); setSelectedRack(currRack); setSelectedCage(currCage); + setIsNavLoading(false); } } break; @@ -232,6 +241,8 @@ export const HomeNavigationContextProvider: FC = ({u navigateTo, setSelectedLocalRoom, userProfile, + isNavLoading, + setIsNavLoading }}> {children} diff --git a/CageUI/src/client/pages/home/RoomHome.tsx b/CageUI/src/client/pages/home/RoomHome.tsx index 372707cda..d99dcf000 100644 --- a/CageUI/src/client/pages/home/RoomHome.tsx +++ b/CageUI/src/client/pages/home/RoomHome.tsx @@ -22,7 +22,7 @@ import '../../cageui.scss'; import { RoomList } from '../../components/home/RoomList'; import { RoomNavbar } from '../../components/home/RoomNavbar'; import { RoomContent } from '../../components/home/RoomContent'; -import { HomeNavigationContextProvider } from '../../context/HomeNavigationContextManager'; +import { HomeNavigationContextProvider, useHomeNavigationContext } from '../../context/HomeNavigationContextManager'; import { RoomContextProvider } from '../../context/RoomContextManager'; import { labkeyGetUserPermissions } from '../../api/labkeyActions'; import { GetUserPermissionsResponse } from '@labkey/api/dist/labkey/security/Permission'; @@ -45,7 +45,7 @@ export const RoomHome: FC = () => { return (user?.container && -
      +
      diff --git a/CageUI/src/client/types/homeNavigationContextTypes.ts b/CageUI/src/client/types/homeNavigationContextTypes.ts index 7f6d1a87d..816a746b8 100644 --- a/CageUI/src/client/types/homeNavigationContextTypes.ts +++ b/CageUI/src/client/types/homeNavigationContextTypes.ts @@ -33,4 +33,6 @@ export interface HomeNavigationContextType { selectedCage: Cage; navigateTo: (page: SelectedPage) => void; userProfile: GetUserPermissionsResponse; + setIsNavLoading: React.Dispatch>; + isNavLoading: boolean; } \ No newline at end of file From 98fb938c3dd4488e805890dbeb89305eaf129102 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 4 Jun 2026 12:24:42 -0500 Subject: [PATCH 10/16] Fix dropdowns so that they are able to close in room list --- CageUI/src/client/cageui.scss | 20 ++++++++++++---- .../src/client/components/home/RoomList.tsx | 24 +++++++++++-------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/CageUI/src/client/cageui.scss b/CageUI/src/client/cageui.scss index 8f9f57618..059c9bebb 100644 --- a/CageUI/src/client/cageui.scss +++ b/CageUI/src/client/cageui.scss @@ -1166,13 +1166,19 @@ border: 1px solid black; } +.room-dir-header-container { + display: flex; + align-items: center; + justify-content: space-between; +} + .room-dir-header { cursor: pointer; font-weight: bold; display: flex; align-items: center; - justify-content: space-between; font-size: x-large; + flex-grow: 1; } .room-dir-room-obj { @@ -1180,14 +1186,20 @@ border-bottom: 1px solid lightgrey; } +.room-dir-rack-obj-container { + display: flex; + align-items: center; + justify-content: space-between; +} + .room-dir-rack-obj { cursor: pointer; font-size: large; font-weight: bold; display: flex; align-items: center; - justify-content: space-between; margin: 15px 10px 15px 5px; + flex-grow: 1; } .room-dir-cage-obj { @@ -1199,12 +1211,12 @@ margin: 15px 10px 15px 5px; } -.room-dir-header.open .arrow { +.room-dir-header-container.open .arrow { transform: rotate(135deg); } -.room-dir-rack-obj.open .arrow { +.room-dir-rack-obj-container.open .arrow { transform: rotate(135deg); } diff --git a/CageUI/src/client/components/home/RoomList.tsx b/CageUI/src/client/components/home/RoomList.tsx index ea0e9552b..0c15bfdd1 100644 --- a/CageUI/src/client/components/home/RoomList.tsx +++ b/CageUI/src/client/components/home/RoomList.tsx @@ -225,22 +225,26 @@ export const RoomList: FC = () => { if (el) roomRefs.current[room.name] = el; }} > -
      handleRoomClick(room)} - className={`room-dir-header ${expandedRooms[room.name] ? 'open' : ''}`} - > - {room.name} +
      +
      handleRoomClick(room)} + className={`room-dir-header`} + > + {room.name} +
      toggleExpandRoom(room.name)}>
      {expandedRooms[room.name] && (
        {room?.racks?.map((rack) => (
      • -
        handleRackClick(room, rack)} - className={`room-dir-rack-obj ${expandedRacks[`${room.name}_${rack.id}`] ? 'open' : ''}`} - > - {rack.name} +
        +
        handleRackClick(room, rack)} + className={`room-dir-rack-obj`} + > + {rack.name} +
        toggleExpandRack(room.name, rack.id)}>
        From a547dab357d1bb1403ddaee0a7e59e662f26979e Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 4 Jun 2026 12:28:03 -0500 Subject: [PATCH 11/16] Fix room list padding so it looks nicer with the scroll bar showing --- CageUI/src/client/cageui.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CageUI/src/client/cageui.scss b/CageUI/src/client/cageui.scss index 059c9bebb..a3b6bc49a 100644 --- a/CageUI/src/client/cageui.scss +++ b/CageUI/src/client/cageui.scss @@ -1148,7 +1148,7 @@ .room-list-items { overflow-y: auto; - padding: 5px; + padding: 5px 15px 5px 5px; } .arrow { From 8f834afa03390c4b49a7f51ce3718763507e64c1 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 4 Jun 2026 13:43:20 -0500 Subject: [PATCH 12/16] Sort cages and racks in room list in ascending order --- CageUI/src/client/components/home/RoomList.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CageUI/src/client/components/home/RoomList.tsx b/CageUI/src/client/components/home/RoomList.tsx index 0c15bfdd1..98bbed5f9 100644 --- a/CageUI/src/client/components/home/RoomList.tsx +++ b/CageUI/src/client/components/home/RoomList.tsx @@ -123,6 +123,18 @@ export const RoomList: FC = () => { }); }); }); + + // Sort cages within each rack and then sort racks by their first cage + tempRacks.forEach((rack) => { + rack.cages.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true })); + }); + tempRacks.sort((a, b) => { + if (a.cages.length > 0 && b.cages.length > 0) { + return a.cages[0].name.localeCompare(b.cages[0].name, undefined, { numeric: true }); + } + return 0; + }); + return { ...prevRoom, racks: tempRacks, From bc553a198dd2b08d2be6dfc6a89d67b8aa5f5f85 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 4 Jun 2026 14:56:13 -0500 Subject: [PATCH 13/16] Fix controller for 26.3 actionURL --- CageUI/src/client/components/home/rackView/ChangeRackPopup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CageUI/src/client/components/home/rackView/ChangeRackPopup.tsx b/CageUI/src/client/components/home/rackView/ChangeRackPopup.tsx index 14ce67738..821258ec0 100644 --- a/CageUI/src/client/components/home/rackView/ChangeRackPopup.tsx +++ b/CageUI/src/client/components/home/rackView/ChangeRackPopup.tsx @@ -147,7 +147,7 @@ export const ChangeRackPopup: FC = (props) => { //navigateTo({selected: 'Room', room: selectedRoom.name}); window.location.href = ActionURL.buildURL( ActionURL.getController(), - 'cageui-home', + 'home', ActionURL.getContainer(), {room: res.roomName, rack: res.rack}); From a67023cf46fd4dbf24ee68c50964da36d749ddc6 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 4 Jun 2026 15:40:20 -0500 Subject: [PATCH 14/16] Add svg preloading so each svg is only ever fetched once --- .../postgresql/cageui-26.000-26.001.sql | 57 ++++++++++++++ .../client/components/layoutEditor/Editor.tsx | 7 +- CageUI/src/client/types/typings.ts | 3 + CageUI/src/client/utils/helpers.ts | 75 ++++++++++++------- .../src/org/labkey/cageui/CageUIModule.java | 2 +- 5 files changed, 115 insertions(+), 29 deletions(-) create mode 100644 CageUI/resources/schemas/dbscripts/postgresql/cageui-26.000-26.001.sql diff --git a/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.000-26.001.sql b/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.000-26.001.sql new file mode 100644 index 000000000..e995899e5 --- /dev/null +++ b/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.000-26.001.sql @@ -0,0 +1,57 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +INSERT INTO ehr_lookups.lookup_sets (setname, label, description, keyField, container) +select 'cageui_svg_urls' as setname, + 'SVG Urls Field Values' as label, + 'List of URLS for room items' as description, + 'value' as keyField, + container from ehr_lookups.lookup_sets where setname='ancestry'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'cage' as value, '/cageui/static/cage.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'pen' as value, '/cageui/static/pen.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'tempCage' as value, '/cageui/static/cage.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'playCage' as value, '/cageui/static/pen.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'roomDivider' as value, '/cageui/static/roomDivider.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'drain' as value, '/cageui/static/drain.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'door' as value, '/cageui/static/door.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'gateClosed' as value, '/cageui/static/gateClosed.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'gateOpen' as value, '/cageui/static/gateOpen.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'top' as value, '/cageui/static/top.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'bottom' as value, '/cageui/static/bottom.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; diff --git a/CageUI/src/client/components/layoutEditor/Editor.tsx b/CageUI/src/client/components/layoutEditor/Editor.tsx index b1f8f7c11..7f6b165f6 100644 --- a/CageUI/src/client/components/layoutEditor/Editor.tsx +++ b/CageUI/src/client/components/layoutEditor/Editor.tsx @@ -689,8 +689,9 @@ const Editor: FC = ({roomSize}) => { } }); // loads grid with new room - addPrevRoomSvgs(user, 'edit', reloadRoom, layoutSvg, undefined, undefined, setSelectedObj, contextMenuRef, setCtxMenuStyle, closeMenuThenDrag); - setReloadRoom(null); + addPrevRoomSvgs(user, 'edit', reloadRoom, layoutSvg, undefined, undefined, setSelectedObj, contextMenuRef, setCtxMenuStyle, closeMenuThenDrag).then(() => { + setReloadRoom(null); + }); }, [reloadRoom]); // Effect attaches an observer to the border_template svg. after it is injected into the dom it will run @@ -751,7 +752,7 @@ const Editor: FC = ({roomSize}) => { window.location.href = ActionURL.buildURL( ActionURL.getController(), 'editLayout', - undefined, + ActionURL.getController(), {room: localRoom.name} ); } diff --git a/CageUI/src/client/types/typings.ts b/CageUI/src/client/types/typings.ts index 60e33a206..18076e486 100644 --- a/CageUI/src/client/types/typings.ts +++ b/CageUI/src/client/types/typings.ts @@ -185,6 +185,9 @@ export type Modification = { export type ModRecord = Record; +export interface LoadedSvgs { + [key: RoomItemStringType]: SVGElement; +} export interface FetchRoomData { selectedSize: SelectorOptions; diff --git a/CageUI/src/client/utils/helpers.ts b/CageUI/src/client/utils/helpers.ts index f169eaa78..fff87219e 100644 --- a/CageUI/src/client/utils/helpers.ts +++ b/CageUI/src/client/utils/helpers.ts @@ -33,7 +33,7 @@ import { GroupId, GroupRotation, LayoutData, - LayoutHistoryData, + LayoutHistoryData, LoadedSvgs, ModData, ModLocations, ModTypes, @@ -75,7 +75,7 @@ import { setupEditCageEvent } from './LayoutEditorHelpers'; import { SelectDistinctOptions } from '@labkey/api/dist/labkey/query/SelectDistinctRows'; -import { selectDistinctRows } from '@labkey/components'; +import { selectDistinctRows, selectRows } from '@labkey/components'; import { CELL_SIZE, Modifications, roomSizeOptions, SVG_HEIGHT, SVG_WIDTH } from './constants'; import { ExtraContext, LayoutSaveResult } from '../types/layoutEditorTypes'; import { SelectRowsOptions } from '@labkey/api/dist/labkey/query/SelectRows'; @@ -496,11 +496,45 @@ export const fetchRoomData = async (roomName: string, abortSignal?: AbortSignal) return prevRoomData; }; +const loadSvgs = async (): Promise => { + const loadedSvgs: LoadedSvgs = {}; + + const config: SelectRowsOptions = { + schemaName: "ehr_lookups", + queryName: "cageui_svg_urls", + columns: ["value", "title"] + } + + const res = await labkeyActionSelectWithPromise(config); + if(res.rowCount > 0){ + + // Create all promises first + const promises = res.rows.map(row => { + return d3.svg(`${ActionURL.getContextPath()}${row.title}`).then((d) => { + if(!loadedSvgs[row.value]){ // cage templates + loadedSvgs[row.value] = d.querySelector(`svg[id*=template]`); + } + if(!loadedSvgs[row.value]){ // room objects + loadedSvgs[row.value] = d.querySelector('svg'); + } + }); + }); + + // Wait for all promises to complete + await Promise.all(promises); + }else{ + console.error("Error finding cageUI Svgs") + } + + return loadedSvgs; +} + // Adds the svgs from the saved layouts to the DOM. Mode edit is version displayed in the layout editor and view is the one in the home views. // roomForMods is passed if the unitsToRender is not room but needs access to the room object. This is for loading mods. -export const addPrevRoomSvgs = (user: GetUserPermissionsResponse, mode: 'edit' | 'view', unitsToRender: Room | RackGroup | Rack | Cage, layoutSvg: d3.Selection, currRoom?: Room, modsToLoad?: RoomMods, setSelectedObj?, contextMenuRef?: MutableRefObject, setCtxMenuStyle?, closeMenuThenDrag?) => { +export const addPrevRoomSvgs = async (user: GetUserPermissionsResponse, mode: 'edit' | 'view', unitsToRender: Room | RackGroup | Rack | Cage, layoutSvg: d3.Selection, currRoom?: Room, modsToLoad?: RoomMods, setSelectedObj?, contextMenuRef?: MutableRefObject, setCtxMenuStyle?, closeMenuThenDrag?) => { let renderType: 'room' | 'group' | 'rack' | 'cage'; + const loadedSvgs: LoadedSvgs = await loadSvgs(); if ((unitsToRender as Room)?.rackGroups) { renderType = 'room'; @@ -554,16 +588,13 @@ export const addPrevRoomSvgs = (user: GetUserPermissionsResponse, mode: 'edit' | .style('pointer-events', 'bounding-box'); // This is where the cage svg group is created. - rack.cages.forEach(async (cage) => { + rack.cages.forEach((cage) => { const cageGroup = rackGroup.append('g') .attr('id', cage.svgId) .attr('name', cage.cageNum) .attr('transform', `translate(${cage.x},${cage.y})`); - let unitSvg: SVGElement; - await d3.svg(`${ActionURL.getContextPath()}/cageui/static/${rackTypeString}.svg`).then((d) => { - unitSvg = d.querySelector(`svg[id*=template]`); - }); + const unitSvg: SVGElement = loadedSvgs[rackTypeString].cloneNode(true) as SVGElement; // Only needed for layout editor to attach context menus const shape = d3.select(unitSvg); @@ -599,9 +630,9 @@ export const addPrevRoomSvgs = (user: GetUserPermissionsResponse, mode: 'edit' | .attr('id', group.groupId) .attr('class', 'draggable rack-group'); - group.racks.forEach(async rack => { + group.racks.forEach( rack => { // Use parent group as rackGroup if only 1 rack, otherwise create a new rack group - await createRackGroup(parentGroup, rack, isSingleRack, group.rotation); + createRackGroup(parentGroup, rack, isSingleRack, group.rotation); }); let groupX = renderType === 'room' ? group.x : group.racks[0].x; let groupY = renderType === 'room' ? group.y : group.racks[0].y; @@ -621,7 +652,7 @@ export const addPrevRoomSvgs = (user: GetUserPermissionsResponse, mode: 'edit' | }); // Render room objects - (unitsToRender as Room).objects.forEach(async (roomObj) => { + (unitsToRender as Room).objects.forEach( (roomObj) => { const wrapperGroup = layoutSvg.append('g') .attr('id', roomObj.itemId + '-wrapper') .attr('class', 'draggable room-obj') @@ -632,10 +663,7 @@ export const addPrevRoomSvgs = (user: GetUserPermissionsResponse, mode: 'edit' | .attr('id', roomObj.itemId) .attr('transform', `translate(0,0)`) - let objSvg: SVGElement; - await d3.svg(`${ActionURL.getContextPath()}/cageui/static/${roomItemToString(roomObj.type)}.svg`).then((d) => { - objSvg = d.querySelector('svg'); - }); + const objSvg: SVGElement = loadedSvgs[roomItemToString(roomObj.type)].cloneNode(true) as SVGElement; const shape = d3.select(objSvg) .classed('draggable', false) @@ -665,18 +693,15 @@ export const addPrevRoomSvgs = (user: GetUserPermissionsResponse, mode: 'edit' | const cageGroup = layoutSvg.append('g') .attr('id', cage.cageNum) .attr('transform', `translate(0,0)`); - let unitSvg: SVGElement; + const unitSvg: SVGElement = loadedSvgs[parseRoomItemType((unitsToRender as Cage).cageNum)].cloneNode(true) as SVGElement; - d3.svg(`${ActionURL.getContextPath()}/cageui/static/${parseRoomItemType((unitsToRender as Cage).cageNum)}.svg`).then((d) => { - unitSvg = d.querySelector(`svg[id*=template]`); - const shape = d3.select(unitSvg); - (shape.select('tspan').node() as SVGTSpanElement).textContent = `${parseRoomItemNum((unitsToRender as Cage).cageNum)}`; + const shape = d3.select(unitSvg); + (shape.select('tspan').node() as SVGTSpanElement).textContent = `${parseRoomItemNum((unitsToRender as Cage).cageNum)}`; - if (mode === 'view') { - loadCageMods(cage, shape, rackGroup.rotation); - } - cageGroup.append(() => shape.node()); - }); + if (mode === 'view') { + loadCageMods(cage, shape, rackGroup.rotation); + } + cageGroup.append(() => shape.node()); } }; diff --git a/CageUI/src/org/labkey/cageui/CageUIModule.java b/CageUI/src/org/labkey/cageui/CageUIModule.java index 6bde122a0..0ccbe402a 100644 --- a/CageUI/src/org/labkey/cageui/CageUIModule.java +++ b/CageUI/src/org/labkey/cageui/CageUIModule.java @@ -59,7 +59,7 @@ public String getName() @Override public @Nullable Double getSchemaVersion() { - return 25.003; + return 26.001; } @Override From f513f89eb7c21dcfd2da825b246dd7afc1d7ecc3 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Fri, 5 Jun 2026 10:18:28 -0500 Subject: [PATCH 15/16] Remove TODO --- CageUI/src/client/context/LayoutEditorContextManager.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/CageUI/src/client/context/LayoutEditorContextManager.tsx b/CageUI/src/client/context/LayoutEditorContextManager.tsx index 1ff6041de..58f6ba7ad 100644 --- a/CageUI/src/client/context/LayoutEditorContextManager.tsx +++ b/CageUI/src/client/context/LayoutEditorContextManager.tsx @@ -1142,8 +1142,6 @@ export const LayoutEditorContextProvider: FC = ({children, p /* Effectively unconnects the selectedRack from any connections with other racks. It does this by removing it from the current rack group and creating a new rack group for the selected rack. - - //TODO how to handle unconnecting when the selected rack is in the middle of multiple racks? */ const unmergeRacks = (rackGroup: RackGroup, selectedRack: Rack) => { const newRoom: Room = { ...localRoom }; From c5055ff8e40d0ba81c8b4fc626e7ebc079c190e6 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Fri, 5 Jun 2026 10:19:02 -0500 Subject: [PATCH 16/16] remove debugging logs --- CageUI/src/client/components/home/roomView/CagePopup.tsx | 1 - .../src/client/components/home/roomView/RoomObjectPopup.tsx | 4 ---- 2 files changed, 5 deletions(-) diff --git a/CageUI/src/client/components/home/roomView/CagePopup.tsx b/CageUI/src/client/components/home/roomView/CagePopup.tsx index 6d8ce82d3..c206d5d86 100644 --- a/CageUI/src/client/components/home/roomView/CagePopup.tsx +++ b/CageUI/src/client/components/home/roomView/CagePopup.tsx @@ -100,7 +100,6 @@ export const CagePopup: FC = (props) => { // This submission updates the room mods with the current selections. const handleSaveMods = () => { - console.log("SaveMods: ", currCageMods); validateAndApplyDefaults(currCageMods).then((res) => { const result = saveCageMods(prevCage, res); diff --git a/CageUI/src/client/components/home/roomView/RoomObjectPopup.tsx b/CageUI/src/client/components/home/roomView/RoomObjectPopup.tsx index 748a58de8..40dad79c8 100644 --- a/CageUI/src/client/components/home/roomView/RoomObjectPopup.tsx +++ b/CageUI/src/client/components/home/roomView/RoomObjectPopup.tsx @@ -43,10 +43,6 @@ export const RoomObjectPopup: FC = (props) => { const [prevRoomObjId, setPrevRoomObjId] = useState((selectedObj as RoomObject).itemId); const menuRef = useRef(null); - useEffect(() => { - console.log('roomObj: ', roomObj); - }, [roomObj]); - useEffect(() => { // Check if the click was outside the menu const handleClickOutside = (event) => {