Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions plugins/ui/src/js/src/layout/ReactPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,56 @@ it('finds and closes existing panels from the layout root, but opens in the pare
});
});

it('re-attaches a detached parent to the root before opening the panel', () => {
const onOpen = jest.fn();
const onClose = jest.fn();
// A parent that is detached from the root (parent.parent === null)
const parent = TestUtils.createMockProxy<ContentItem>({ parent: null });

render(
<ParentItemContext.Provider value={parent}>
{makeTestComponent({ onOpen, onClose })}
</ParentItemContext.Provider>
);

const { root } = (useLayoutManager as jest.Mock).mock.results[0].value;

// parent is the topmost detached ancestor; root has no children so addChild is called on root
expect(root.addChild).toHaveBeenCalledWith(parent);
// Panel should still open in the parent stack after re-attachment
expect(LayoutUtils.openComponent).toHaveBeenCalledTimes(1);
expect(LayoutUtils.openComponent).toHaveBeenCalledWith(
expect.objectContaining({ root: parent })
);
});

it('re-attaches the topmost detached ancestor to the root before opening the panel', () => {
const onOpen = jest.fn();
const onClose = jest.fn();
// parent is a stack inside a detached row (grandparent.parent === null)
const grandparent = TestUtils.createMockProxy<ContentItem>({ parent: null });
const parent = TestUtils.createMockProxy<ContentItem>({
parent: grandparent,
});

render(
<ParentItemContext.Provider value={parent}>
{makeTestComponent({ onOpen, onClose })}
</ParentItemContext.Provider>
);

const { root } = (useLayoutManager as jest.Mock).mock.results[0].value;

// The topmost detached ancestor (grandparent) should be re-added, not just parent
expect(root.addChild).toHaveBeenCalledWith(grandparent);
expect(root.addChild).not.toHaveBeenCalledWith(parent);
// Panel should still open in the original parent (not grandparent)
expect(LayoutUtils.openComponent).toHaveBeenCalledTimes(1);
expect(LayoutUtils.openComponent).toHaveBeenCalledWith(
expect.objectContaining({ root: parent })
);
});

it('only calls open once if the panel has not closed and only children change', () => {
const onOpen = jest.fn();
const onClose = jest.fn();
Expand Down
18 changes: 17 additions & 1 deletion plugins/ui/src/js/src/layout/ReactPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,23 @@ function ReactPanel({
isClosable,
};

// If we didn't find it, we still want to open it in the same place in the layout as before (parent stack) instead of opening at the root
// If we didn't find it, we still want to open it in the same place in the layout as before (parent stack) instead of opening at the root.
// Walk up from parent, tracking the topmost item seen, to determine in one pass whether the parent
// is detached and, if so, which ancestor to re-add. Re-adding the topmost detached ancestor (e.g. a
// row containing multiple stacks) better preserves the layout and ensures stack headers are rendered.
let topDetached: typeof parent = parent;
let currentParent: typeof parent | null = parent.parent;
while (currentParent != null && currentParent !== root) {
topDetached = currentParent;
currentParent = currentParent.parent;
}
if (currentParent === null) {
// currentParent reached null without hitting root, so the parent is detached.
// Root can only have one direct child (a row/column container), so add to that instead.
const rootChild =
root.contentItems.length > 0 ? root.contentItems[0] : root;
rootChild.addChild(topDetached);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. a couple odd things when doing it like this:

  1. The row layout is still lost (all the panels are stacked in one column instead of across like in a row)
  2. The stack header for the last item does not seem to appear
Image

The row layout not being respected is kind of "well we're trying our best", but the stack header not appearing seems to be a bug (you can't drag to move that item anymore... though it does re-appear if you drag another panel...).

Right now we're just adding the parent if it's not attached; but I'm wondering if we should be re-adding the top most parent that isn't attached. A bit of a complication is that we would need to ensure it doesn't contain containers that have already been moved to somewhere else before re-attaching ... but I think would ultimately be more correct, and fix that header case. See if that's possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to add top most parent. This also fixed the header issue.

LayoutUtils.openComponent({ root: parent, config });
log.debug('Opened panel', panelId, config);
} else if (openedMetadataRef.current !== metadata) {
Expand Down
Loading