diff --git a/bun.lockb b/bun.lockb
index 0ffd08fccbc..e6df1f25624 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/extensions/default/src/Components/SidePanelWithServices.tsx b/extensions/default/src/Components/SidePanelWithServices.tsx
index c4fd32a1c0d..298f5ae03b9 100644
--- a/extensions/default/src/Components/SidePanelWithServices.tsx
+++ b/extensions/default/src/Components/SidePanelWithServices.tsx
@@ -7,47 +7,58 @@ export type SidePanelWithServicesProps = {
side: 'left' | 'right';
className?: string;
activeTabIndex: number;
- tabs: any;
+ tabs?: any;
expandedWidth?: number;
+ onClose: () => void;
+ onOpen: () => void;
+ isExpanded: boolean;
+ collapsedWidth?: number;
+ expandedInsideBorderSize?: number;
+ collapsedInsideBorderSize?: number;
+ collapsedOutsideBorderSize?: number;
};
const SidePanelWithServices = ({
servicesManager,
side,
activeTabIndex: activeTabIndexProp,
+ isExpanded,
tabs: tabsProp,
- expandedWidth,
+ onOpen,
+ onClose,
...props
}: SidePanelWithServicesProps) => {
const panelService = servicesManager?.services?.panelService;
// Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM.
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
- const [sidePanelOpen, setSidePanelOpen] = useState(activeTabIndexProp !== null);
- const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp);
+ const [sidePanelExpanded, setSidePanelExpanded] = useState(isExpanded);
+ const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0);
+ const [closedManually, setClosedManually] = useState(false);
const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side));
const handleActiveTabIndexChange = useCallback(({ activeTabIndex }) => {
setActiveTabIndex(activeTabIndex);
- setSidePanelOpen(activeTabIndex !== null);
}, []);
const handleOpen = useCallback(() => {
- setSidePanelOpen(true);
- // If panel is being opened but no tab is active, set first tab as active
- if (activeTabIndex === null && tabs.length > 0) {
- setActiveTabIndex(0);
- }
- }, [activeTabIndex, tabs]);
+ setSidePanelExpanded(true);
+ onOpen?.();
+ }, [onOpen]);
const handleClose = useCallback(() => {
- setSidePanelOpen(false);
- setActiveTabIndex(null);
- }, []);
+ setSidePanelExpanded(false);
+ setClosedManually(true);
+ onClose?.();
+ }, [onClose]);
+
+ useEffect(() => {
+ setSidePanelExpanded(isExpanded);
+ }, [isExpanded]);
/** update the active tab index from outside */
useEffect(() => {
- setActiveTabIndex(activeTabIndexProp);
+ setActiveTabIndex(activeTabIndexProp ?? 0);
}, [activeTabIndexProp]);
useEffect(() => {
@@ -71,9 +82,12 @@ const SidePanelWithServices = ({
const activatePanelSubscription = panelService.subscribe(
panelService.EVENTS.ACTIVATE_PANEL,
(activatePanelEvent: Types.ActivatePanelEvent) => {
- if (sidePanelOpen || activatePanelEvent.forceActive) {
+ if (sidePanelExpanded || activatePanelEvent.forceActive) {
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
if (tabIndex !== -1) {
+ if (!closedManually) {
+ setSidePanelExpanded(true);
+ }
setActiveTabIndex(tabIndex);
}
}
@@ -83,7 +97,7 @@ const SidePanelWithServices = ({
return () => {
activatePanelSubscription.unsubscribe();
};
- }, [tabs, sidePanelOpen, panelService]);
+ }, [tabs, sidePanelExpanded, panelService, closedManually]);
return (
);
};
diff --git a/extensions/default/src/ViewerLayout/ResizablePanelsHook.tsx b/extensions/default/src/ViewerLayout/ResizablePanelsHook.tsx
new file mode 100644
index 00000000000..9b7c377962a
--- /dev/null
+++ b/extensions/default/src/ViewerLayout/ResizablePanelsHook.tsx
@@ -0,0 +1,325 @@
+import { useState, useCallback, useLayoutEffect, useRef } from 'react';
+import { getPanelElement, getPanelGroupElement } from 'react-resizable-panels';
+
+// Id needed to grab the panel group for converting pixels to percentages
+const viewerLayoutResizablePanelGroupId = 'viewerLayoutResizablePanelGroup';
+const viewerLayoutResizableLeftPanelId = 'viewerLayoutResizableLeftPanel';
+const viewerLayoutResizableRightPanelId = 'viewerLayoutResizableRightPanel';
+
+const sidePanelExpandedDefaultWidth = 280;
+const sidePanelExpandedInsideBorderSize = 4;
+const sidePanelExpandedDefaultOffsetWidth =
+ sidePanelExpandedDefaultWidth + sidePanelExpandedInsideBorderSize;
+const sidePanelCollapsedInsideBorderSize = 4;
+const sidePanelCollapsedOutsideBorderSize = 8;
+const sidePanelCollapsedWidth = 25;
+const sidePanelCollapsedOffsetWidth =
+ sidePanelCollapsedWidth +
+ sidePanelCollapsedInsideBorderSize +
+ sidePanelCollapsedOutsideBorderSize;
+
+/**
+ * Set the minimum and maximum css style width attributes for the given element.
+ * The two style attributes are cleared whenever the width
+ * arguments is undefined.
+ *
+ * This utility is used as part of a HACK throughout the ViewerLayout component as
+ * the means of restricting the side panel widths during the resizing of the
+ * browser window. In general, the widths are always set unless the resize
+ * handle for either side panel is being dragged (i.e. a side panel is being resized).
+ *
+ * @param elem the element
+ * @param width the max and min width to set on the element
+ */
+const setMinMaxWidth = (elem, width?) => {
+ elem.style.minWidth = width === undefined ? '' : `${width}px`;
+ elem.style.maxWidth = elem.style.minWidth;
+};
+
+const useResizablePanels = (
+ leftPanelClosed,
+ setLeftPanelClosed,
+ rightPanelClosed,
+ setRightPanelClosed
+) => {
+ const [leftPanelExpandedWidth, setLeftPanelExpandedWidth] = useState(
+ sidePanelExpandedDefaultWidth
+ );
+ const [rightPanelExpandedWidth, setRightPanelExpandedWidth] = useState(
+ sidePanelExpandedDefaultWidth
+ );
+
+ // Percentage sizes.
+ const [resizablePanelCollapsedSize, setResizablePanelCollapsedSize] = useState(0);
+ const [resizablePanelDefaultSize, setResizablePanelDefaultSize] = useState(0);
+
+ const resizablePanelGroupElemRef = useRef(null);
+ const resizableLeftPanelElemRef = useRef(null);
+ const resizableRightPanelElemRef = useRef(null);
+ const resizableLeftPanelAPIRef = useRef(null);
+ const resizableRightPanelAPIRef = useRef(null);
+ const isResizableHandleDraggingRef = useRef(false);
+
+ // This useLayoutEffect is used to...
+ // - Grab a reference to the various resizable panel elements needed for
+ // converting between percentages and pixels in various callbacks.
+ // - Expand those panels that are initially expanded.
+ useLayoutEffect(() => {
+ const panelGroupElem = getPanelGroupElement(viewerLayoutResizablePanelGroupId);
+
+ resizablePanelGroupElemRef.current = panelGroupElem;
+ const { width: panelGroupWidth } = panelGroupElem.getBoundingClientRect();
+
+ const leftPanelElem = getPanelElement(viewerLayoutResizableLeftPanelId);
+ resizableLeftPanelElemRef.current = leftPanelElem;
+
+ const rightPanelElem = getPanelElement(viewerLayoutResizableRightPanelId);
+ resizableRightPanelElemRef.current = rightPanelElem;
+
+ const resizablePanelExpandedSize =
+ (sidePanelExpandedDefaultOffsetWidth / panelGroupWidth) * 100;
+
+ // Since both resizable panels are collapsed by default (i.e. their default size is zero),
+ // on the very first render check if either/both side panels should be expanded.
+ if (!leftPanelClosed) {
+ resizableLeftPanelAPIRef?.current?.expand(resizablePanelExpandedSize);
+ setMinMaxWidth(leftPanelElem, sidePanelExpandedDefaultOffsetWidth);
+ }
+
+ if (!rightPanelClosed) {
+ resizableRightPanelAPIRef?.current?.expand(resizablePanelExpandedSize);
+ setMinMaxWidth(rightPanelElem, sidePanelExpandedDefaultOffsetWidth);
+ }
+ }, []); // no dependencies because this useLayoutEffect is only needed on the very first render
+
+ // This useLayoutEffect follows the pattern prescribed by the react-resizable-panels
+ // readme for converting between pixel values and percentages. An example of
+ // the pattern can be found here:
+ // https://github.com/bvaughn/react-resizable-panels/issues/46#issuecomment-1368108416
+ // This useLayoutEffect is used to...
+ // - Ensure that the percentage size is up-to-date with the pixel sizes
+ // - Add a resize observer to the resizable panel group to reset various state
+ // values whenever the resizable panel group is resized (e.g. whenever the
+ // browser window is resized).
+ useLayoutEffect(() => {
+ const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
+
+ // Ensure the side panels' percentage size is in synch with the pixel width of the
+ // expanded side panels. In general the two get out-of-sync during a browser
+ // window resize. Note that this code is here and NOT in the ResizeObserver
+ // because it has to be done AFTER the minimum percentage size for a panel is
+ // updated which occurs only AFTER the render following a resize. And by virtue
+ // of the dependency on the `resizablePanelDefaultSize` state, this code
+ // is executed on the render following an update of the minimum percentage size
+ // for a panel.
+ if (!resizableLeftPanelAPIRef.current.isCollapsed()) {
+ const leftSize =
+ ((leftPanelExpandedWidth + sidePanelExpandedInsideBorderSize) / panelGroupWidth) * 100;
+ resizableLeftPanelAPIRef.current.resize(leftSize);
+ }
+
+ if (!resizableRightPanelAPIRef.current.isCollapsed()) {
+ const rightSize =
+ ((rightPanelExpandedWidth + sidePanelExpandedInsideBorderSize) / panelGroupWidth) * 100;
+ resizableRightPanelAPIRef.current.resize(rightSize);
+ }
+
+ // This observer kicks in when the ViewportLayout resizable panel group
+ // component is resized. This typically occurs when the browser window resizes.
+ const observer = new ResizeObserver(() => {
+ const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
+ const defaultSize = (sidePanelExpandedDefaultOffsetWidth / panelGroupWidth) * 100;
+
+ // Set the new default and collapsed resizable panel sizes.
+ setResizablePanelDefaultSize(Math.min(50, defaultSize));
+ setResizablePanelCollapsedSize((sidePanelCollapsedOffsetWidth / panelGroupWidth) * 100);
+
+ if (
+ resizableLeftPanelAPIRef.current.isCollapsed() &&
+ resizableRightPanelAPIRef.current.isCollapsed()
+ ) {
+ return;
+ }
+
+ // The code that follows is to handle cases when the group panel is resized to be
+ // too small to display either side panel at its current width.
+
+ // Determine the current widths of the two side panels.
+ let leftPanelOffsetWidth = resizableLeftPanelAPIRef.current.isCollapsed()
+ ? sidePanelCollapsedOffsetWidth
+ : leftPanelExpandedWidth + sidePanelExpandedInsideBorderSize;
+
+ let rightPanelOffsetWidth = resizableRightPanelAPIRef.current.isCollapsed()
+ ? sidePanelCollapsedOffsetWidth
+ : rightPanelExpandedWidth + sidePanelExpandedInsideBorderSize;
+
+ if (
+ !resizableLeftPanelAPIRef.current.isCollapsed() &&
+ leftPanelOffsetWidth + rightPanelOffsetWidth > panelGroupWidth
+ ) {
+ // There is not enough space to show both panels at their pre-resize widths.
+ // Note that at this point, the viewport grid component is zero width.
+ // Reduce the left panel width so that both panels might fit.
+ leftPanelOffsetWidth = Math.max(
+ panelGroupWidth - rightPanelOffsetWidth,
+ sidePanelExpandedDefaultOffsetWidth
+ );
+ setLeftPanelExpandedWidth(leftPanelOffsetWidth - sidePanelExpandedInsideBorderSize);
+ setMinMaxWidth(resizableLeftPanelElemRef.current, leftPanelOffsetWidth);
+ }
+
+ if (
+ !resizableRightPanelAPIRef.current.isCollapsed() &&
+ rightPanelOffsetWidth + leftPanelOffsetWidth > panelGroupWidth
+ ) {
+ // There is not enough space to show both panels at their pre-resize widths.
+ // Note that at this point, the viewport grid component is zero width.
+ // Reduce the right panel width so that both panels might fit.
+ rightPanelOffsetWidth = Math.max(
+ panelGroupWidth - leftPanelOffsetWidth,
+ sidePanelExpandedDefaultOffsetWidth
+ );
+ setRightPanelExpandedWidth(rightPanelOffsetWidth - sidePanelExpandedInsideBorderSize);
+ setMinMaxWidth(resizableRightPanelElemRef.current, rightPanelOffsetWidth);
+ }
+ });
+
+ observer.observe(resizablePanelGroupElemRef.current);
+
+ return () => {
+ observer.disconnect();
+ };
+ }, [leftPanelExpandedWidth, resizablePanelDefaultSize, rightPanelExpandedWidth]);
+
+ /**
+ * Handles dragging of either side panel resize handle.
+ */
+ const onHandleDragging = useCallback(
+ isStartDrag => {
+ if (isStartDrag) {
+ isResizableHandleDraggingRef.current = true;
+
+ setMinMaxWidth(resizableLeftPanelElemRef.current);
+ setMinMaxWidth(resizableRightPanelElemRef.current);
+ } else {
+ isResizableHandleDraggingRef.current = false;
+
+ if (resizableLeftPanelAPIRef?.current?.isExpanded()) {
+ setMinMaxWidth(
+ resizableLeftPanelElemRef.current,
+ leftPanelExpandedWidth + sidePanelExpandedInsideBorderSize
+ );
+ }
+
+ if (resizableRightPanelAPIRef?.current?.isExpanded()) {
+ setMinMaxWidth(
+ resizableRightPanelElemRef.current,
+ rightPanelExpandedWidth + sidePanelExpandedInsideBorderSize
+ );
+ }
+ }
+ },
+ [leftPanelExpandedWidth, rightPanelExpandedWidth]
+ );
+
+ const onLeftPanelClose = useCallback(() => {
+ setLeftPanelClosed(true);
+ setMinMaxWidth(resizableLeftPanelElemRef.current);
+ resizableLeftPanelAPIRef?.current?.collapse();
+ }, [setLeftPanelClosed]);
+
+ const onLeftPanelOpen = useCallback(() => {
+ resizableLeftPanelAPIRef?.current?.expand();
+ if (!isResizableHandleDraggingRef.current) {
+ setMinMaxWidth(
+ resizableLeftPanelElemRef.current,
+ leftPanelExpandedWidth + sidePanelExpandedInsideBorderSize
+ );
+ }
+ setLeftPanelClosed(false);
+ }, [leftPanelExpandedWidth, setLeftPanelClosed]);
+
+ const onLeftPanelResize = useCallback(size => {
+ if (resizableLeftPanelAPIRef.current.isCollapsed()) {
+ return;
+ }
+
+ const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
+ setLeftPanelExpandedWidth((size / 100) * panelGroupWidth - sidePanelExpandedInsideBorderSize);
+ }, []);
+
+ const onRightPanelClose = useCallback(() => {
+ setRightPanelClosed(true);
+ setMinMaxWidth(resizableRightPanelElemRef.current);
+ resizableRightPanelAPIRef?.current?.collapse();
+ }, [setRightPanelClosed]);
+
+ const onRightPanelOpen = useCallback(() => {
+ resizableRightPanelAPIRef?.current?.expand();
+ if (!isResizableHandleDraggingRef.current) {
+ setMinMaxWidth(
+ resizableRightPanelElemRef.current,
+ rightPanelExpandedWidth + sidePanelExpandedInsideBorderSize
+ );
+ }
+ setRightPanelClosed(false);
+ }, [rightPanelExpandedWidth, setRightPanelClosed]);
+
+ const onRightPanelResize = useCallback(size => {
+ if (resizableRightPanelAPIRef?.current?.isCollapsed()) {
+ return;
+ }
+ const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
+ setRightPanelExpandedWidth((size / 100) * panelGroupWidth - sidePanelExpandedInsideBorderSize);
+ }, []);
+
+ return [
+ {
+ expandedWidth: leftPanelExpandedWidth,
+ collapsedWidth: sidePanelCollapsedWidth,
+ collapsedInsideBorderSize: sidePanelCollapsedInsideBorderSize,
+ collapsedOutsideBorderSize: sidePanelCollapsedOutsideBorderSize,
+ expandedInsideBorderSize: sidePanelExpandedInsideBorderSize,
+ onClose: onLeftPanelClose,
+ onOpen: onLeftPanelOpen,
+ },
+ {
+ expandedWidth: rightPanelExpandedWidth,
+ collapsedWidth: sidePanelCollapsedWidth,
+ collapsedInsideBorderSize: sidePanelCollapsedInsideBorderSize,
+ collapsedOutsideBorderSize: sidePanelCollapsedOutsideBorderSize,
+ expandedInsideBorderSize: sidePanelExpandedInsideBorderSize,
+ onClose: onRightPanelClose,
+ onOpen: onRightPanelOpen,
+ },
+ { direction: 'horizontal', id: viewerLayoutResizablePanelGroupId },
+ {
+ defaultSize: resizablePanelDefaultSize,
+ minSize: resizablePanelDefaultSize,
+ onResize: onLeftPanelResize,
+ collapsible: true,
+ collapsedSize: resizablePanelCollapsedSize,
+ onCollapse: () => setLeftPanelClosed(true),
+ onExpand: () => setLeftPanelClosed(false),
+ ref: resizableLeftPanelAPIRef,
+ order: 0,
+ id: viewerLayoutResizableLeftPanelId,
+ },
+ { order: 1, id: 'viewerLayoutResizableViewportGridPanel' },
+ {
+ defaultSize: resizablePanelDefaultSize,
+ minSize: resizablePanelDefaultSize,
+ onResize: onRightPanelResize,
+ collapsible: true,
+ collapsedSize: resizablePanelCollapsedSize,
+ onCollapse: () => setRightPanelClosed(true),
+ onExpand: () => setRightPanelClosed(false),
+ ref: resizableRightPanelAPIRef,
+ order: 2,
+ id: viewerLayoutResizableRightPanelId,
+ },
+ onHandleDragging,
+ ];
+};
+
+export default useResizablePanels;
diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx
index e19a24c68ab..b11ec0da4cc 100644
--- a/extensions/default/src/ViewerLayout/index.tsx
+++ b/extensions/default/src/ViewerLayout/index.tsx
@@ -6,7 +6,8 @@ import { HangingProtocolService, CommandsManager } from '@ohif/core';
import { useAppConfig } from '@state';
import ViewerHeader from './ViewerHeader';
import SidePanelWithServices from '../Components/SidePanelWithServices';
-import { Onboarding } from '@ohif/ui-next';
+import { Onboarding, ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@ohif/ui-next';
+import useResizablePanels from './ResizablePanelsHook';
function ViewerLayout({
// From Extension Module Params
@@ -19,6 +20,8 @@ function ViewerLayout({
ViewportGridComp,
leftPanelClosed = false,
rightPanelClosed = false,
+ leftPanelResizable = false,
+ rightPanelResizable = false,
}: withAppTypes): React.FunctionComponent {
const [appConfig] = useAppConfig();
@@ -35,6 +38,21 @@ function ViewerLayout({
const [leftPanelClosedState, setLeftPanelClosed] = useState(leftPanelClosed);
const [rightPanelClosedState, setRightPanelClosed] = useState(rightPanelClosed);
+ const [
+ leftPanelProps,
+ rightPanelProps,
+ resizablePanelGroupProps,
+ resizableLeftPanelProps,
+ resizableViewportGridPanelProps,
+ resizableRightPanelProps,
+ onHandleDragging,
+ ] = useResizablePanels(
+ leftPanelClosed,
+ setLeftPanelClosed,
+ rightPanelClosed,
+ setRightPanelClosed
+ );
+
/**
* Set body classes (tailwindcss) that don't allow vertical
* or horizontal overflow (no scrolling). Also guarantee window
@@ -123,31 +141,56 @@ function ViewerLayout({
>
{showLoadingIndicator && }
- {/* LEFT SIDEPANELS */}
- {hasLeftPanels ? (
-
- ) : null}
- {/* TOOLBAR + GRID */}
-
- {hasRightPanels ? (
-
- ) : null}
+
+ {/* LEFT SIDEPANELS */}
+
+ {hasLeftPanels ? (
+ <>
+
+
+
+
+ >
+ ) : null}
+ {/* TOOLBAR + GRID */}
+
+
+
+ {hasRightPanels ? (
+ <>
+
+
+
+
+ >
+ ) : null}
+
diff --git a/modes/longitudinal/src/index.ts b/modes/longitudinal/src/index.ts
index 68ce4ae38e6..6afe135faa9 100644
--- a/modes/longitudinal/src/index.ts
+++ b/modes/longitudinal/src/index.ts
@@ -117,21 +117,30 @@ function modeFactory({ modeConfiguration }) {
// // ActivatePanel event trigger for when a segmentation or measurement is added.
// // Do not force activation so as to respect the state the user may have left the UI in.
_activatePanelTriggersSubscriptions = [
- ...panelService.addActivatePanelTriggers(cornerstone.segmentation, [
- {
- sourcePubSubService: segmentationService,
- sourceEvents: [segmentationService.EVENTS.SEGMENTATION_ADDED],
- },
- ]),
- ...panelService.addActivatePanelTriggers(tracked.measurements, [
- {
- sourcePubSubService: measurementService,
- sourceEvents: [
- measurementService.EVENTS.MEASUREMENT_ADDED,
- measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
- ],
- },
- ]),
+ ...panelService.addActivatePanelTriggers(
+ cornerstone.segmentation,
+ [
+ {
+ sourcePubSubService: segmentationService,
+ sourceEvents: [segmentationService.EVENTS.SEGMENTATION_ADDED],
+ },
+ ],
+ true
+ ),
+ ...panelService.addActivatePanelTriggers(
+ tracked.measurements,
+ [
+ {
+ sourcePubSubService: measurementService,
+ sourceEvents: [
+ measurementService.EVENTS.MEASUREMENT_ADDED,
+ measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
+ ],
+ },
+ ],
+ true
+ ),
+ true,
];
},
onModeExit: ({ servicesManager }: withAppTypes) => {
@@ -181,8 +190,10 @@ function modeFactory({ modeConfiguration }) {
id: ohif.layout,
props: {
leftPanels: [tracked.thumbnailList],
+ leftPanelResizable: true,
rightPanels: [cornerstone.segmentation, tracked.measurements],
rightPanelClosed: true,
+ rightPanelResizable: true,
viewports: [
{
namespace: tracked.viewport,
diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js
index ad68375641d..febbaf41ac4 100644
--- a/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js
+++ b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js
@@ -15,7 +15,7 @@ describe('OHIF Measurement Panel', function () {
cy.get('@RightCollapseBtn').click();
cy.get('@measurementsPanel').should('not.exist');
- cy.get('@RightCollapseBtn').click();
+ cy.get('@RightCollapseBtn').click({ force: true });
// segmentation panel should be visible
cy.get('@segmentationPanel').should('be.visible');
diff --git a/platform/core/src/services/PanelService/PanelService.tsx b/platform/core/src/services/PanelService/PanelService.tsx
index 4900283e99a..91a01ac9158 100644
--- a/platform/core/src/services/PanelService/PanelService.tsx
+++ b/platform/core/src/services/PanelService/PanelService.tsx
@@ -151,13 +151,7 @@ export default class PanelService extends PubSubService {
panelsIds.forEach(panelId => this.addPanel(position, panelId, options));
}
- public setPanels(
- panels: { [key in PanelPosition]: string[] },
- options: {
- rightPanelClosed?: boolean;
- leftPanelClosed?: boolean;
- }
- ): void {
+ public setPanels(panels: { [key in PanelPosition]: string[] }, options): void {
this.reset();
Object.keys(panels).forEach((position: PanelPosition) => {
diff --git a/platform/ui-next/package.json b/platform/ui-next/package.json
index e7f60255b37..30d7c33400d 100644
--- a/platform/ui-next/package.json
+++ b/platform/ui-next/package.json
@@ -53,6 +53,7 @@
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
+ "react-resizable-panels": "^2.1.7",
"react-shepherd": "6.1.1",
"shepherd.js": "13.0.3",
"sonner": "^1.5.0",
diff --git a/platform/ui-next/src/components/Resizable/Resizable.tsx b/platform/ui-next/src/components/Resizable/Resizable.tsx
new file mode 100644
index 00000000000..967152b6b18
--- /dev/null
+++ b/platform/ui-next/src/components/Resizable/Resizable.tsx
@@ -0,0 +1,43 @@
+'use client';
+import React from 'react';
+
+import { GripVertical } from 'lucide-react';
+import * as ResizablePrimitive from 'react-resizable-panels';
+
+import cn from 'classnames';
+
+const ResizablePanelGroup = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+);
+
+const ResizablePanel = ResizablePrimitive.Panel;
+
+const ResizableHandle = ({
+ withHandle,
+ className,
+ ...props
+}: React.ComponentProps & {
+ withHandle?: boolean;
+}) => (
+ div]:rotate-90',
+ className
+ )}
+ {...props}
+ >
+ {withHandle && (
+
+
+
+ )}
+
+);
+
+export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
diff --git a/platform/ui-next/src/components/Resizable/index.ts b/platform/ui-next/src/components/Resizable/index.ts
new file mode 100644
index 00000000000..88432781a98
--- /dev/null
+++ b/platform/ui-next/src/components/Resizable/index.ts
@@ -0,0 +1,3 @@
+import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './Resizable';
+
+export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
diff --git a/platform/ui-next/src/components/SidePanel/SidePanel.tsx b/platform/ui-next/src/components/SidePanel/SidePanel.tsx
index f331edb6a5d..5d8be607cd9 100644
--- a/platform/ui-next/src/components/SidePanel/SidePanel.tsx
+++ b/platform/ui-next/src/components/SidePanel/SidePanel.tsx
@@ -1,39 +1,64 @@
import classnames from 'classnames';
-import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useState } from 'react';
import { Icons } from '../Icons';
-import { TooltipTrigger, TooltipContent, TooltipProvider, Tooltip } from '../Tooltip';
+import { TooltipTrigger, TooltipContent, Tooltip } from '../Tooltip';
import { Separator } from '../Separator';
+/**
+ * SidePanel component properties.
+ * Note that the component monitors changes to the various widths and border sizes and will resize dynamically
+ * @property {boolean} isExpanded - boolean indicating if the side panel is expanded/open or collapsed
+ * @property {number} expandedWidth - the width of this side panel when expanded not including any borders or margins
+ * @property {number} collapsedWidth - the width of this side panel when collapsed not including any borders or margins
+ * @property {number} expandedInsideBorderSize - the width of the space between the expanded side panel content and viewport grid
+ * @property {number} collapsedInsideBorderSize - the width of the space between the collapsed side panel content and the viewport grid
+ * @property {number} collapsedOutsideBorderSize - the width of the space between the collapsed side panel content and the edge of the browser window
+ */
+type SidePanelProps = {
+ side: 'left' | 'right';
+ className: string;
+ activeTabIndex: number;
+ onOpen: () => void;
+ onClose: () => void;
+ onActiveTabIndexChange: () => void;
+ isExpanded: boolean;
+ expandedWidth: number;
+ collapsedWidth: number;
+ expandedInsideBorderSize: number;
+ collapsedInsideBorderSize: number;
+ collapsedOutsideBorderSize: number;
+ tabs: any;
+};
+
type StyleMap = {
open: {
- left: { marginLeft: string };
- right: { marginRight: string };
+ left: {
+ marginLeft: string; // the space between the expanded/open left side panel and the browser window left edge
+ marginRight: string; // the space between the expanded/open left side panel and the viewport grid
+ };
+ right: {
+ marginLeft: string; // the space between the expanded/open right side panel and the viewport grid
+ marginRight: string; // the space between the expanded/open right side panel and the browser window right edge
+ };
};
closed: {
- left: { marginLeft: string };
- right: { marginRight: string };
+ left: {
+ marginLeft: string; // the space between the collapsed/closed left panel and the browser window left edge
+ marginRight: string; // the space between the collapsed/closed left panel and the viewport grid
+ alignItems: 'flex-end'; // the flexbox layout align-items property
+ };
+ right: {
+ marginLeft: string; // the space between the collapsed/closed right panel and the viewport grid
+ marginRight: string; // the space between the collapsed/closed right panel and the browser window right edge
+ alignItems: 'flex-start'; // the flexbox layout align-items property
+ };
};
};
-const borderSize = 4;
-const collapsedWidth = 25;
const closeIconWidth = 30;
const gridHorizontalPadding = 10;
const tabSpacerWidth = 2;
-const baseClasses =
- 'transition-all duration-300 ease-in-out bg-black border-black justify-start box-content flex flex-col';
-
-const classesMap = {
- open: {
- left: `mr-1`,
- right: `ml-1`,
- },
- closed: {
- left: `mr-2 items-end`,
- right: `ml-2 items-start`,
- },
-};
+const baseClasses = 'bg-black border-black justify-start box-content flex flex-col';
const openStateIconName = {
left: 'SidePanelCloseLeft',
@@ -106,19 +131,29 @@ const getTabIconClassNames = (numTabs: number, isActiveTab: boolean) => {
};
const createStyleMap = (
expandedWidth: number,
- borderSize: number,
- collapsedWidth: number
+ expandedInsideBorderSize: number,
+ collapsedWidth: number,
+ collapsedInsideBorderSize: number,
+ collapsedOutsideBorderSize: number
): StyleMap => {
- const collapsedHideWidth = expandedWidth - collapsedWidth - borderSize;
+ const collapsedHideWidth = expandedWidth - collapsedWidth - collapsedInsideBorderSize;
return {
open: {
- left: { marginLeft: '0px' },
- right: { marginRight: '0px' },
+ left: { marginLeft: '0px', marginRight: `${expandedInsideBorderSize}px` },
+ right: { marginLeft: `${expandedInsideBorderSize}px`, marginRight: '0px' },
},
closed: {
- left: { marginLeft: `-${collapsedHideWidth}px` },
- right: { marginRight: `-${collapsedHideWidth}px` },
+ left: {
+ marginLeft: `-${collapsedHideWidth}px`,
+ marginRight: `${collapsedOutsideBorderSize}px`,
+ alignItems: `flex-end`,
+ },
+ right: {
+ marginLeft: `${collapsedOutsideBorderSize}px`,
+ marginRight: `-${collapsedHideWidth}px`,
+ alignItems: `flex-start`,
+ },
},
};
};
@@ -143,47 +178,67 @@ const createBaseStyle = (expandedWidth: number) => {
height: '99.8%',
};
};
+
const SidePanel = ({
side,
className,
- activeTabIndex: activeTabIndexProp = null,
+ activeTabIndex: activeTabIndexProp,
+ isExpanded,
tabs,
onOpen,
onClose,
- expandedWidth = 280,
onActiveTabIndexChange,
-}) => {
- const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
- const [activeTabIndex, setActiveTabIndex] = useState(0);
-
- const styleMap = createStyleMap(expandedWidth, borderSize, collapsedWidth);
- const baseStyle = createBaseStyle(expandedWidth);
- const gridAvailableWidth = expandedWidth - closeIconWidth - gridHorizontalPadding;
- const gridWidth = getGridWidth(tabs.length, gridAvailableWidth);
+ expandedWidth = 280,
+ collapsedWidth = 25,
+ expandedInsideBorderSize = 4,
+ collapsedInsideBorderSize = 8,
+ collapsedOutsideBorderSize = 4,
+}: SidePanelProps) => {
+ const [panelOpen, setPanelOpen] = useState(isExpanded);
+ const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0);
+
+ const [styleMap, setStyleMap] = useState(
+ createStyleMap(
+ expandedWidth,
+ expandedInsideBorderSize,
+ collapsedWidth,
+ collapsedInsideBorderSize,
+ collapsedOutsideBorderSize
+ )
+ );
+
+ const [baseStyle, setBaseStyle] = useState(createBaseStyle(expandedWidth));
+
+ const [gridAvailableWidth, setGridAvailableWidth] = useState(
+ expandedWidth - closeIconWidth - gridHorizontalPadding
+ );
+
+ const [gridWidth, setGridWidth] = useState(getGridWidth(tabs.length, gridAvailableWidth));
const openStatus = panelOpen ? 'open' : 'closed';
const style = Object.assign({}, styleMap[openStatus][side], baseStyle);
const updatePanelOpen = useCallback(
- (panelOpen: boolean) => {
- setPanelOpen(panelOpen);
- if (panelOpen && onOpen) {
- onOpen();
- } else if (onClose && !panelOpen) {
- onClose();
+ (isOpen: boolean) => {
+ setPanelOpen(isOpen);
+ if (isOpen !== panelOpen) {
+ // only fire events for changes
+ if (isOpen && onOpen) {
+ onOpen();
+ } else if (onClose && !isOpen) {
+ onClose();
+ }
}
},
- [onOpen, onClose]
+ [panelOpen, onOpen, onClose]
);
const updateActiveTabIndex = useCallback(
- (activeTabIndex: number) => {
- if (activeTabIndex === null) {
- updatePanelOpen(false);
- return;
+ (activeTabIndex: number, forceOpen: boolean = false) => {
+ if (forceOpen) {
+ updatePanelOpen(true);
}
setActiveTabIndex(activeTabIndex);
- updatePanelOpen(true);
if (onActiveTabIndexChange) {
onActiveTabIndexChange({ activeTabIndex });
@@ -193,7 +248,35 @@ const SidePanel = ({
);
useEffect(() => {
- updateActiveTabIndex(activeTabIndexProp);
+ updatePanelOpen(isExpanded);
+ }, [isExpanded, updatePanelOpen]);
+
+ useEffect(() => {
+ setStyleMap(
+ createStyleMap(
+ expandedWidth,
+ expandedInsideBorderSize,
+ collapsedWidth,
+ collapsedInsideBorderSize,
+ collapsedOutsideBorderSize
+ )
+ );
+ setBaseStyle(createBaseStyle(expandedWidth));
+
+ const gridAvailableWidth = expandedWidth - closeIconWidth - gridHorizontalPadding;
+ setGridAvailableWidth(gridAvailableWidth);
+ setGridWidth(getGridWidth(tabs.length, gridAvailableWidth));
+ }, [
+ collapsedInsideBorderSize,
+ collapsedWidth,
+ expandedWidth,
+ expandedInsideBorderSize,
+ tabs.length,
+ collapsedOutsideBorderSize,
+ ]);
+
+ useEffect(() => {
+ updateActiveTabIndex(activeTabIndexProp ?? 0);
}, [activeTabIndexProp, updateActiveTabIndex]);
const getCloseStateComponent = () => {
@@ -223,7 +306,7 @@ const SidePanel = ({
data-cy={`${childComponent.name}-btn`}
className="text-primary-active hover:cursor-pointer"
onClick={() => {
- return childComponent.disabled ? null : updateActiveTabIndex(index);
+ return childComponent.disabled ? null : updateActiveTabIndex(index, true);
}}
>
{React.createElement(Icons[childComponent.iconName] || Icons.MissingIcon, {
@@ -374,7 +457,7 @@ const SidePanel = ({
return (
{panelOpen ? (
@@ -394,14 +477,4 @@ const SidePanel = ({
);
};
-SidePanel.propTypes = {
- side: PropTypes.oneOf(['left', 'right']).isRequired,
- className: PropTypes.string,
- activeTabIndex: PropTypes.number,
- onOpen: PropTypes.func,
- onClose: PropTypes.func,
- onActiveTabIndexChange: PropTypes.func,
- expandedWidth: PropTypes.number,
-};
-
export { SidePanel };
diff --git a/platform/ui-next/src/components/index.ts b/platform/ui-next/src/components/index.ts
index 20e613921f4..c2d57b74832 100644
--- a/platform/ui-next/src/components/index.ts
+++ b/platform/ui-next/src/components/index.ts
@@ -27,6 +27,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
import { Clipboard } from './Clipboard';
import { Combobox } from './Combobox';
import { Popover, PopoverContent, PopoverTrigger, PopoverAnchor } from './Popover';
+import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from './Resizable';
import { Calendar } from './Calendar';
import { DatePickerWithRange } from './DateRange';
import { Separator } from './Separator';
@@ -121,6 +122,9 @@ export {
PopoverContent,
PopoverTrigger,
PopoverAnchor,
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
Calendar,
DatePickerWithRange,
Input,
diff --git a/platform/ui-next/src/index.ts b/platform/ui-next/src/index.ts
index 874f222da1f..a37eb59ca01 100644
--- a/platform/ui-next/src/index.ts
+++ b/platform/ui-next/src/index.ts
@@ -44,6 +44,9 @@ import {
PopoverAnchor,
PopoverContent,
PopoverTrigger,
+ ResizablePanelGroup,
+ ResizablePanel,
+ ResizableHandle,
Select,
SelectTrigger,
SelectContent,
@@ -150,6 +153,9 @@ export {
PopoverAnchor,
PopoverContent,
PopoverTrigger,
+ ResizablePanelGroup,
+ ResizablePanel,
+ ResizableHandle,
Select,
SelectTrigger,
SelectContent,
diff --git a/yarn.lock b/yarn.lock
index 54a49352d5d..e53f588a56c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1394,9 +1394,9 @@
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@cornerstonejs/adapters@^2.15.3":
- version "2.15.3"
- resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-2.15.3.tgz#0b29519748fd1a51522f5a0c362317d51af98a7f"
- integrity sha512-TeYSM+qUh+x1XrPR4Tqowp3+T6X+g+H9i2siF33gOw+cIDi4EQ14WNcvWSB2tHmqHvg/Rmta9Kvgha0dSoWTzA==
+ version "2.15.5"
+ resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-2.15.5.tgz#2fce5e7ad5a3e7b5f229c5a1f4e07d6c91f9836c"
+ integrity sha512-ohdp8waX52ZpPhnrJJnM64Dlc2+X+DCL8HvYB/SjvE/H1YsHk1Pa5trd5taEKq1GJks3LX19XIuWTpKj/wXTUA==
dependencies:
"@babel/runtime-corejs2" "^7.17.8"
buffer "^6.0.3"
@@ -1430,18 +1430,18 @@
integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg==
"@cornerstonejs/core@^2.15.3":
- version "2.15.3"
- resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-2.15.3.tgz#62b82ac8ebc6a5cf30f96e358bf7654f4eee71b4"
- integrity sha512-qQDvDUtONz7ydI6yf2RPI+p7/846IxnSPgCPgqLaVohjWjHeZkyhV5oY6lN50ZxgHkWmBvinw6l2wjvh4x1bKw==
+ version "2.15.5"
+ resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-2.15.5.tgz#2c81bf28a1e15dfbe25215a7df6ae29b70e7d621"
+ integrity sha512-P+Dzsdgp9uSqcIBRECHI0Kj1G2//2UacstRmsRc3uxXgJ0lJ7AaLS31F4xVNOf+5mHMsB8BUsxdDg6iujX9lQA==
dependencies:
"@kitware/vtk.js" "32.9.0"
comlink "^4.4.1"
gl-matrix "^3.4.3"
"@cornerstonejs/dicom-image-loader@^2.15.3":
- version "2.15.3"
- resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-2.15.3.tgz#1ab90d03fab2674cbdfd6a56f4d3da585f69abf6"
- integrity sha512-/njbngdKBV3DEcD5UrPD9B3QC/eXS8gx9U3hfAvABFihaSQFd7/rEgKte1EbShQ9Qdtx1FPiaX+uMmhGPxVsQA==
+ version "2.15.5"
+ resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-2.15.5.tgz#867636c95321dc03d5eb960266af300609f28d54"
+ integrity sha512-XbWHlfNe6/+W6xFYSSBcOQNTJfCg0FRduh+AXfMxES96xTSdK/1LObeOOtVq5QCslUU90ERHuUYSY8wDNiys7w==
dependencies:
"@cornerstonejs/codec-charls" "^1.2.3"
"@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2"
@@ -1453,9 +1453,9 @@
uuid "^9.0.0"
"@cornerstonejs/tools@^2.15.3":
- version "2.15.3"
- resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-2.15.3.tgz#b778f67f13faf62d625c6ce8bbe8eed656f29ba0"
- integrity sha512-NzEeII/xTc4kFjvCZ0HV3LNOtlkFvPobxwHePuMq9wa8/2M89+blDrbrm1IOkO0xKrKMNnTqt1wTzBjcy+TKGA==
+ version "2.15.5"
+ resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-2.15.5.tgz#e38ff0e5a7158f073f361068ff0bbc2fe337fe7b"
+ integrity sha512-uOphCv2ulmRijy72emPUkXYw1JXYYygTPKWNmaEA6PX17fE6NlQfYdJc4GHVkXiqj2YJCDzN1PwukI1dWfL9Vg==
dependencies:
"@types/offscreencanvas" "2019.7.3"
comlink "^4.4.1"
@@ -3731,10 +3731,10 @@
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a"
integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==
-"@remix-run/router@1.21.0":
- version "1.21.0"
- resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.21.0.tgz#c65ae4262bdcfe415dbd4f64ec87676e4a56e2b5"
- integrity sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==
+"@remix-run/router@1.19.2":
+ version "1.19.2"
+ resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.19.2.tgz#0c896535473291cb41f152c180bedd5680a3b273"
+ integrity sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==
"@rollup/plugin-babel@^5.2.0":
version "5.3.1"
@@ -5068,9 +5068,9 @@
"@types/node" "*"
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0":
- version "5.0.4"
- resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.4.tgz#88c29e3052cec3536d64b6ce5015a30dfcbefca7"
- integrity sha512-5kz9ScmzBdzTgB/3susoCgfqNDzBjvLL4taparufgSvlwjdLy6UyUy9T/tCpYd2GIdIilCatC4iSQS0QSYHt0w==
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz#91f06cda1049e8f17eeab364798ed79c97488a1c"
+ integrity sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==
dependencies:
"@types/node" "*"
"@types/qs" "*"
@@ -5480,9 +5480,9 @@
integrity sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg==
"@types/ws@^8.2.2":
- version "8.5.13"
- resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20"
- integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==
+ version "8.5.12"
+ resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e"
+ integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==
dependencies:
"@types/node" "*"
@@ -10099,44 +10099,7 @@ exponential-backoff@^3.1.1:
resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6"
integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==
-express@^4.17.1:
- version "4.21.2"
- resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32"
- integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==
- dependencies:
- accepts "~1.3.8"
- array-flatten "1.1.1"
- body-parser "1.20.3"
- content-disposition "0.5.4"
- content-type "~1.0.4"
- cookie "0.7.1"
- cookie-signature "1.0.6"
- debug "2.6.9"
- depd "2.0.0"
- encodeurl "~2.0.0"
- escape-html "~1.0.3"
- etag "~1.8.1"
- finalhandler "1.3.1"
- fresh "0.5.2"
- http-errors "2.0.0"
- merge-descriptors "1.0.3"
- methods "~1.1.2"
- on-finished "2.4.1"
- parseurl "~1.3.3"
- path-to-regexp "0.1.12"
- proxy-addr "~2.0.7"
- qs "6.13.0"
- range-parser "~1.2.1"
- safe-buffer "5.2.1"
- send "0.19.0"
- serve-static "1.16.2"
- setprototypeof "1.2.0"
- statuses "2.0.1"
- type-is "~1.6.18"
- utils-merge "1.0.1"
- vary "~1.1.2"
-
-express@^4.17.3:
+express@^4.17.1, express@^4.17.3:
version "4.21.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281"
integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==
@@ -10531,12 +10494,7 @@ flow-parser@0.*:
resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.247.1.tgz#4a512c882cd08126825fe00b1ee452637ef44371"
integrity sha512-DHwcm06fWbn2Z6uFD3NaBZ5lMOoABIQ4asrVA80IWvYjjT5WdbghkUOL1wIcbLcagnFTdCZYOlSNnKNp/xnRZQ==
-follow-redirects@^1.0.0:
- version "1.15.9"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
- integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
-
-follow-redirects@^1.14.9, follow-redirects@^1.15.0, follow-redirects@^1.15.6:
+follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0, follow-redirects@^1.15.6:
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
@@ -11468,9 +11426,9 @@ http-errors@~1.6.2:
statuses ">= 1.4.0 < 2"
http-parser-js@>=0.5.1:
- version "0.5.9"
- resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.9.tgz#b817b3ca0edea6236225000d795378707c169cec"
- integrity sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==
+ version "0.5.8"
+ resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3"
+ integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==
http-proxy-agent@^5.0.0:
version "5.0.0"
@@ -15701,11 +15659,6 @@ path-to-regexp@0.1.10:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b"
integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
-path-to-regexp@0.1.12:
- version "0.1.12"
- resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
- integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
-
path-to-regexp@3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b"
@@ -17160,6 +17113,11 @@ react-remove-scroll@^2.6.1:
use-callback-ref "^1.3.3"
use-sidecar "^1.1.2"
+react-resizable-panels@^2.1.7:
+ version "2.1.7"
+ resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz#afd29d8a3d708786a9f95183a38803c89f13c2e7"
+ integrity sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==
+
react-resize-detector@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-10.0.1.tgz#ae9a8c5b6b93c4c11e03b3eb87e57fd7b62f1020"
@@ -17168,19 +17126,19 @@ react-resize-detector@^10.0.1:
lodash "^4.17.21"
react-router-dom@^6.8.1:
- version "6.28.1"
- resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.28.1.tgz#b78fe452d2cd31919b80e57047a896bfa1509f8c"
- integrity sha512-YraE27C/RdjcZwl5UCqF/ffXnZDxpJdk9Q6jw38SZHjXs7NNdpViq2l2c7fO7+4uWaEfcwfGCv3RSg4e1By/fQ==
+ version "6.26.2"
+ resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.26.2.tgz#a6e3b0cbd6bfd508e42b9342099d015a0ac59680"
+ integrity sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==
dependencies:
- "@remix-run/router" "1.21.0"
- react-router "6.28.1"
+ "@remix-run/router" "1.19.2"
+ react-router "6.26.2"
-react-router@6.28.1, react-router@^6.23.1:
- version "6.28.1"
- resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.28.1.tgz#f82317ab24eee67d7beb7b304c0378b2b48fa178"
- integrity sha512-2omQTA3rkMljmrvvo6WtewGdVh45SpL9hGiCI9uUrwGGfNFDIvGK4gYJsKlJoNVi6AQZcopSCballL+QGOm7fA==
+react-router@6.26.2, react-router@^6.23.1:
+ version "6.26.2"
+ resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.26.2.tgz#2f0a68999168954431cdc29dd36cec3b6fa44a7e"
+ integrity sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==
dependencies:
- "@remix-run/router" "1.21.0"
+ "@remix-run/router" "1.19.2"
react-select@5.7.4:
version "5.7.4"
@@ -17406,9 +17364,9 @@ readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable
util-deprecate "^1.0.1"
readable-stream@^4.5.2:
- version "4.5.2"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09"
- integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91"
+ integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==
dependencies:
abort-controller "^3.0.0"
buffer "^6.0.3"
@@ -18699,7 +18657,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
-"string-width-cjs@npm:string-width@^4.2.0":
+"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -18717,15 +18675,6 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
@@ -18828,7 +18777,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -18849,13 +18798,6 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
strip-ansi@^7.0.0, strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -20687,7 +20629,7 @@ worker-loader@3.0.8, worker-loader@^3.0.8:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -20713,15 +20655,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"