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
1 change: 1 addition & 0 deletions packages/components/src/context-actions/ContextActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class ContextActions extends Component<
global: 100000,

edit: 100,
reopen: 9000,
wusteven815 marked this conversation as resolved.
Show resolved Hide resolved
};

static triggerMenu(
Expand Down
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
54 changes: 51 additions & 3 deletions packages/dashboard-core-plugins/src/panels/PanelContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import React, { PureComponent, ReactElement } from 'react';
import { ContextAction, ContextActions } from '@deephaven/components';
import type { Container, Tab } from '@deephaven/golden-layout';
import type {
Container,
EventEmitter,
ReactComponentConfig,
Tab,
} from '@deephaven/golden-layout';
import {
CustomizableWorkspace,
RootState,
getWorkspace,
setWorkspace as setWorkspaceAction,
} from '@deephaven/redux';
import { connect } from 'react-redux';
import { LayoutUtils, PanelEvent } from '@deephaven/dashboard';

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

interface HasContainer {
Expand All @@ -26,6 +41,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 +82,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 @@ -102,11 +123,26 @@ class PanelContextMenu extends PureComponent<
}

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

const contextActions: (ContextAction | (() => ContextAction))[] = [
...additionalActions,
];
const stackId = LayoutUtils.getStackForConfig(
glContainer.layoutManager.root,
glContainer.getConfig()
)?.config.id;
const hasPanelToReopen = workspace.data.closed?.some(
panel => (panel as ReactComponentConfig).parentStackId === stackId
);

contextActions.push(() => ({
title: 'Re-open closed panel',
order: 10,
wusteven815 marked this conversation as resolved.
Show resolved Hide resolved
group: ContextActions.groups.reopen,
action: this.handleReopenLast,
disabled: !hasPanelToReopen,
}));
wusteven815 marked this conversation as resolved.
Show resolved Hide resolved

const closable = glContainer.tab?.contentItem?.config?.isClosable;
contextActions.push({
Expand Down Expand Up @@ -138,4 +174,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;
39 changes: 36 additions & 3 deletions packages/dashboard/src/PanelManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,12 +291,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, is defined
wusteven815 marked this conversation as resolved.
Show resolved Hide resolved
*/
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,12 +350,17 @@ 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
);
dehydratedConfig.parentStackId = LayoutUtils.getStackForConfig(
root,
config
)?.config.id;
if (dehydratedConfig != null) {
this.closed.push(dehydratedConfig);
}
wusteven815 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
46 changes: 43 additions & 3 deletions packages/dashboard/src/layout/LayoutUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,42 @@ 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) {
return item as Stack;
}
wusteven815 marked this conversation as resolved.
Show resolved Hide resolved

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 +502,7 @@ class LayoutUtils {
static openComponent({
root,
config: configParam,
stack: stackParam = undefined,
replaceExisting = true,
replaceConfig = undefined,
createNewStack = false,
Expand All @@ -474,6 +511,7 @@ class LayoutUtils {
}: {
root?: ContentItem;
config?: Partial<ReactComponentConfig>;
stack?: Stack;
replaceExisting?: boolean;
replaceConfig?: Partial<ItemConfigType>;
createNewStack?: boolean;
Expand All @@ -498,9 +536,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
5 changes: 5 additions & 0 deletions packages/golden-layout/src/config/ItemConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export interface ReactComponentConfig extends ItemConfig {
* Properties that will be passed to the component and accessible using this.props.
*/
props?: any;

/**
* The stack the component is in.
*/
parentStackId?: string | string[];
wusteven815 marked this conversation as resolved.
Show resolved Hide resolved
}

export function isGLComponentConfig(
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
29 changes: 11 additions & 18 deletions packages/golden-layout/test/id-tests.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
describe('Dynamic ids work properly', function () {
var layout, item;
var layout, item, id;

it('creates a layout', function () {
layout = testTools.createLayout({
Expand All @@ -17,45 +17,38 @@ describe('Dynamic ids work properly', function () {
expect(item.isComponent).toBe(true);
});

it('has no id initially', function () {
expect(item.config.id).toBe(undefined);
it('has an id', function () {
id = item.config.id;
expect(id).not.toBe(undefined);
expect(item.hasId('id_1')).toBe(false);
expect(item.hasId('id_2')).toBe(false);
});

it('adds the first id as a string', function () {
item.addId('id_1');
expect(item.hasId('id_1')).toBe(true);
expect(item.hasId('id_2')).toBe(false);
expect(item.config.id).toBe('id_1');
expect(layout.root.getItemsById('id_1')[0]).toBe(item);
expect(item.hasId(undefined)).toBe(false);
});

it('adds the second id to an array', function () {
item.addId('id_2');
expect(item.config.id instanceof Array).toBe(true);
expect(item.config.id.length).toBe(2);
expect(item.config.id[0]).toBe('id_1');
expect(item.config.id[0]).toBe(id);
expect(item.config.id[1]).toBe('id_2');
expect(item.hasId('id_1')).toBe(true);
expect(item.hasId(id)).toBe(true);
expect(item.hasId('id_2')).toBe(true);
expect(layout.root.getItemsById('id_1')[0]).toBe(item);
expect(layout.root.getItemsById(id)[0]).toBe(item);
expect(layout.root.getItemsById('id_2')[0]).toBe(item);
});

it('doesn\t add duplicated ids', function () {
item.addId('id_2');
expect(item.config.id instanceof Array).toBe(true);
expect(item.config.id.length).toBe(2);
expect(item.config.id[0]).toBe('id_1');
expect(item.config.id[0]).toBe(id);
expect(item.config.id[1]).toBe('id_2');
expect(layout.root.getItemsById('id_1')[0]).toBe(item);
expect(layout.root.getItemsById(id)[0]).toBe(item);
expect(layout.root.getItemsById('id_2')[0]).toBe(item);
});

it('removes ids', function () {
item.removeId('id_2');
expect(item.hasId('id_1')).toBe(true);
expect(item.hasId(id)).toBe(true);
expect(item.hasId('id_2')).toBe(false);
expect(item.config.id.length).toBe(1);
});
Expand Down
Loading
Loading