Skip to content

Commit

Permalink
feat: context menu reopen for stack only (#1932)
Browse files Browse the repository at this point in the history
- Adds #1931 
  - Add new context menu option for panels
  - Give IDs to stacks and helper function to get stacks by ID
  - Add ability to open component to specific stack
  - Fix unit tests related to IDs and layouts, added E2E tests

---------

Co-authored-by: Mike Bender <mikebender@deephaven.io>
  • Loading branch information
wusteven815 and mofojed authored Apr 26, 2024
1 parent 5f49761 commit 6a9a6a4
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 34 deletions.
2 changes: 2 additions & 0 deletions packages/dashboard-core-plugins/src/panels/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ class Panel extends PureComponent<PanelProps, PanelState> {
className,
renderTabTooltip,
glContainer,
glEventHub,
additionalActions,
errorMessage,
isLoaded,
Expand Down Expand Up @@ -362,6 +363,7 @@ class Panel extends PureComponent<PanelProps, PanelState> {
<>
<PanelContextMenu
glContainer={glContainer}
glEventHub={glEventHub}
additionalActions={this.getAdditionActions(
additionalActions,
isClonable,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React from 'react';
import { render } from '@testing-library/react';
import type { Container } from '@deephaven/golden-layout';
import { EventEmitter, type Container } from '@deephaven/golden-layout';
import { createMockStore } from '@deephaven/redux';
import { ApiContext } from '@deephaven/jsapi-bootstrap';
import dh from '@deephaven/jsapi-shim';
import { Provider } from 'react-redux';
import PanelContextMenu from './PanelContextMenu';

function makeGlComponent({
Expand All @@ -9,13 +13,25 @@ function makeGlComponent({
emit = jest.fn(),
unbind = jest.fn(),
trigger = jest.fn(),
layoutManager = {
root: {},
},
getConfig = jest.fn(),
} = {}) {
return { on, off, emit, unbind, trigger };
return { on, off, emit, unbind, trigger, layoutManager, getConfig };
}

function mountPanelContextMenu() {
const store = createMockStore();
return render(
<PanelContextMenu glContainer={makeGlComponent() as unknown as Container} />
<ApiContext.Provider value={dh}>
<Provider store={store}>
<PanelContextMenu
glContainer={makeGlComponent() as unknown as Container}
glEventHub={new EventEmitter()}
/>
</Provider>
</ApiContext.Provider>
);
}

Expand Down
51 changes: 49 additions & 2 deletions packages/dashboard-core-plugins/src/panels/PanelContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import React, { PureComponent, ReactElement } from 'react';
import { ContextAction, ContextActions } from '@deephaven/components';
import type { Container, Tab } from '@deephaven/golden-layout';
import type { Container, EventEmitter, Tab } from '@deephaven/golden-layout';
import {
CustomizableWorkspace,
RootState,
getWorkspace,
setWorkspace as setWorkspaceAction,
} from '@deephaven/redux';
import { connect } from 'react-redux';
import { ClosedPanel, LayoutUtils, PanelEvent } from '@deephaven/dashboard';

interface PanelContextMenuProps {
additionalActions: ContextAction[];
glContainer: Container;
glEventHub: EventEmitter;
workspace: CustomizableWorkspace;
}

interface HasContainer {
Expand All @@ -26,6 +36,7 @@ class PanelContextMenu extends PureComponent<
this.handleCloseTab = this.handleCloseTab.bind(this);
this.handleCloseTabsRight = this.handleCloseTabsRight.bind(this);
this.handleCloseTabsAll = this.handleCloseTabsAll.bind(this);
this.handleReopenLast = this.handleReopenLast.bind(this);
}

getAllTabs(): Tab[] {
Expand Down Expand Up @@ -66,6 +77,11 @@ class PanelContextMenu extends PureComponent<
}
}

handleReopenLast(): void {
const { glContainer, glEventHub } = this.props;
glEventHub.emit(PanelEvent.REOPEN_LAST, glContainer);
}

canCloseTabsRight(): boolean {
const { glContainer } = this.props;
const tabs = this.getAllTabs();
Expand Down Expand Up @@ -101,13 +117,32 @@ class PanelContextMenu extends PureComponent<
return disabled;
}

canReopenLast(): boolean {
const { workspace, glContainer } = this.props;
const stackId = LayoutUtils.getStackForConfig(
glContainer.layoutManager.root,
glContainer.getConfig()
)?.config.id;

return !workspace.data.closed?.some(
panel => (panel as ClosedPanel).parentStackId === stackId
);
}

render(): ReactElement {
const { additionalActions, glContainer } = this.props;

const contextActions: (ContextAction | (() => ContextAction))[] = [
...additionalActions,
];

contextActions.push(() => ({
title: 'Re-open closed panel',
group: ContextActions.groups.medium + 2004,
action: this.handleReopenLast,
disabled: this.canReopenLast(),
}));

const closable = glContainer.tab?.contentItem?.config?.isClosable;
contextActions.push({
title: 'Close',
Expand Down Expand Up @@ -138,4 +173,16 @@ class PanelContextMenu extends PureComponent<
}
}

export default PanelContextMenu;
const mapStateToProps = (
state: RootState
): {
workspace: CustomizableWorkspace;
} => ({
workspace: getWorkspace(state),
});

const ConnectedPanelContextMenu = connect(mapStateToProps, {
setWorkspace: setWorkspaceAction,
})(PanelContextMenu);

export default ConnectedPanelContextMenu;
48 changes: 41 additions & 7 deletions packages/dashboard/src/PanelManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export type PanelDehydraterFunction = (
config: ReactComponentConfig
) => ReactComponentConfig;

export type ClosedPanel = ReactComponentConfig;
export type ClosedPanel = ReactComponentConfig & {
/**
* The stack the component is in.
*/
parentStackId?: string | string[];
};

export type ClosedPanels = ClosedPanel[];

Expand Down Expand Up @@ -291,12 +296,40 @@ class PanelManager {
};

const { root } = this.layout;
LayoutUtils.openComponent({ root, config, replaceConfig });
const stack =
panelConfig.parentStackId === undefined
? undefined
: LayoutUtils.getStackById(root, panelConfig.parentStackId);
LayoutUtils.openComponent({
root,
config,
replaceConfig,
stack: stack ?? undefined,
});
}

handleReopenLast(): void {
/**
*
* @param glContainer Only reopen panels that were closed from the stack of this container, if defined
*/
handleReopenLast(glContainer?: Container): void {
if (this.closed.length === 0) return;
this.handleReopen(this.closed[this.closed.length - 1]);
if (glContainer === undefined) {
this.handleReopen(this.closed[this.closed.length - 1]);
return;
}

const stackId = LayoutUtils.getStackForConfig(
this.layout.root,
glContainer.getConfig()
)?.config.id;
for (let i = this.closed.length - 1; i >= 0; i -= 1) {
const panelConfig = this.closed[i];
if (panelConfig.parentStackId === stackId) {
this.handleReopen(panelConfig);
return;
}
}
}

handleDeleted(panelConfig: ClosedPanel): void {
Expand All @@ -322,15 +355,16 @@ class PanelManager {
}

addClosedPanel(glContainer: Container): void {
const { root } = this.layout;
const config = LayoutUtils.getComponentConfigFromContainer(glContainer);
if (config && isReactComponentConfig(config)) {
const dehydratedConfig = this.dehydrateComponent(
config.component,
config
);
if (dehydratedConfig != null) {
this.closed.push(dehydratedConfig);
}
(dehydratedConfig as ClosedPanel).parentStackId =
LayoutUtils.getStackForConfig(root, config)?.config.id;
this.closed.push(dehydratedConfig);
}
}

Expand Down
49 changes: 46 additions & 3 deletions packages/dashboard/src/layout/LayoutUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,45 @@ class LayoutUtils {
return this.addStack(newParent, !columnPreferred);
}

/**
* Gets a stack by its ID
* @param item Golden layout content item to search for the stack
* @param searchId the ID
*/
static getStackById(
item: ContentItem,
searchId: string | string[],
allowEmptyStack = false
): Stack | null {
if (allowEmptyStack && isStack(item) && item.contentItems.length === 0) {
return item;
}

if (searchId === item.config?.id) {
if (isStack(item)) {
return item as Stack;
}
throw new Error(`Item with ID ${searchId} is not a stack`);
}

if (item.contentItems == null) {
return null;
}

for (let i = 0; i < item.contentItems.length; i += 1) {
const stack = this.getStackById(
item.contentItems[i],
searchId,
allowEmptyStack
);
if (stack) {
return stack;
}
}

return null;
}

/**
* Gets the first stack which contains a contentItem with the given config values
* @param item Golden layout content item to search for the stack
Expand Down Expand Up @@ -466,6 +505,7 @@ class LayoutUtils {
static openComponent({
root,
config: configParam,
stack: stackParam = undefined,
replaceExisting = true,
replaceConfig = undefined,
createNewStack = false,
Expand All @@ -474,6 +514,7 @@ class LayoutUtils {
}: {
root?: ContentItem;
config?: Partial<ReactComponentConfig>;
stack?: Stack;
replaceExisting?: boolean;
replaceConfig?: Partial<ItemConfigType>;
createNewStack?: boolean;
Expand All @@ -498,9 +539,11 @@ class LayoutUtils {
component: config.component,
};
assertNotNull(root);
const stack = createNewStack
? LayoutUtils.addStack(root)
: LayoutUtils.getStackForRoot(root, searchConfig);
const stack =
stackParam ??
(createNewStack
? LayoutUtils.addStack(root)
: LayoutUtils.getStackForRoot(root, searchConfig));

assertNotNull(stack);
const oldContentItem = LayoutUtils.getContentItemInStack(
Expand Down
1 change: 1 addition & 0 deletions packages/golden-layout/src/LayoutManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ export class LayoutManager extends EventEmitter {
};
}

config.id = config.id ?? getUniqueId();
contentItem = new this._typeToItem[config.type](this, config, parent);
return contentItem;
}
Expand Down
9 changes: 9 additions & 0 deletions packages/golden-layout/src/container/ItemContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ export default class ItemContainer<
return this._config.componentState;
}

/**
* Returns the object's config
*
* @returns id
*/
getConfig() {
return this._config;
}

/**
* Merges the provided state into the current one
*
Expand Down
3 changes: 2 additions & 1 deletion packages/golden-layout/src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import $ from 'jquery';
import shortid from 'shortid';

export function getHashValue(key: string) {
var matches = location.hash.match(new RegExp(key + '=([^&]*)'));
Expand Down Expand Up @@ -40,7 +41,7 @@ export function removeFromArray<T>(item: T, array: T[]) {
}

export function getUniqueId() {
return (Math.random() * 1000000000000000).toString(36).replace('.', '');
return shortid();
}

/**
Expand Down
Loading

0 comments on commit 6a9a6a4

Please sign in to comment.