Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: context menu reopen for stack only #1932

Merged
merged 16 commits into from
Apr 26, 2024
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 @@ -513,6 +513,7 @@ export class LayoutManager extends EventEmitter {
};
}

config.id = config.id ?? getUniqueId();
wusteven815 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading