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
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();
Comment thread
wusteven815 marked this conversation as 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