Move tabs manager to zustand (#915)

This commit is contained in:
victor-meng
2021-07-08 21:32:22 -07:00
committed by GitHub
parent f4eef1b61b
commit f8ab0a82e0
42 changed files with 609 additions and 663 deletions

View File

@@ -1,6 +1,7 @@
import React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import type { TabOptions } from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer";
import TabsBase from "../TabsBase";
import MongoShellTabComponent, { IMongoShellTabAccessor, IMongoShellTabComponentProps } from "./MongoShellTabComponent";
@@ -33,7 +34,7 @@ export class NewMongoShellTab extends TabsBase {
}
public onTabClick(): void {
this.manager?.activateTab(this);
useTabs.getState().activateTab(this);
this.iMongoShellTabAccessor.onTabClickEvent();
}
}

View File

@@ -1,6 +1,7 @@
import React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import type { QueryTabOptions } from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer";
import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent";
import TabsBase from "../TabsBase";
@@ -40,12 +41,12 @@ export class NewQueryTab extends TabsBase {
}
public onTabClick(): void {
this.manager?.activateTab(this);
useTabs.getState().activateTab(this);
this.iTabAccessor.onTabClickEvent();
}
public onCloseTabButtonClick(): void {
this.manager?.closeTab(this);
useTabs.getState().closeTab(this);
if (this.iTabAccessor) {
this.iTabAccessor.onCloseClickEvent(true);
}

View File

@@ -29,7 +29,6 @@ import { EditorReact } from "../../Controls/Editor/EditorReact";
import Explorer from "../../Explorer";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import TabsBase from "../TabsBase";
import { TabsManager } from "../TabsManager";
import "./QueryTabComponent.less";
enum ToggleState {
@@ -65,7 +64,6 @@ export interface IQueryTabComponentProps {
partitionKey: DataModels.PartitionKey;
container: Explorer;
activeTab?: TabsBase;
tabManager?: TabsManager;
onTabAccessor: (instance: ITabAccessor) => void;
isPreferredApiMongoDB?: boolean;
monacoEditorSetting?: string;

View File

@@ -2,6 +2,7 @@ import React from "react";
import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer";
import StoredProcedure from "../../Tree/StoredProcedure";
import ScriptTabBase from "../ScriptTabBase";
@@ -51,12 +52,12 @@ export class NewStoredProcedureTab extends ScriptTabBase {
}
public onTabClick(): void {
this.manager?.activateTab(this);
useTabs.getState().activateTab(this);
this.iStoreProcAccessor.onTabClickEvent();
}
public onCloseTabButtonClick(): void {
this.manager?.closeTab(this);
useTabs.getState().closeTab(this);
}
public onExecuteSprocsResult(result: ExecuteSprocResult): void {

View File

@@ -10,6 +10,7 @@ import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProc
import { updateStoredProcedure } from "../../../Common/dataAccess/updateStoredProcedure";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
import { useTabs } from "../../../hooks/useTabs";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "../../Controls/Editor/EditorReact";
import Explorer from "../../Explorer";
@@ -144,7 +145,7 @@ export default class StoredProcedureTabComponent extends React.Component<
}
public onTabClick(): void {
if (this.props.container.tabsManager.openedTabs().length > 0) {
if (useTabs.getState().openedTabs.length > 0) {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
}
@@ -396,10 +397,8 @@ export default class StoredProcedureTabComponent extends React.Component<
editorModel && editorModel.setValue(createdResource.body as string);
this.props.scriptTabBaseInstance.editorContent.setBaseline(createdResource.body as string);
this.node = this.collection.createStoredProcedureNode(createdResource);
this.props.container.tabsManager.openedTabs()[
this.props.container.tabsManager.openedTabs().length - 1
].node = this.node;
this.props.scriptTabBaseInstance.node = this.node;
useTabs.getState().updateTab(this.props.scriptTabBaseInstance);
this.props.scriptTabBaseInstance.editorState(ViewModels.ScriptEditorState.exisitingNoEdits);
this.setState({

View File

@@ -3,28 +3,32 @@ import React, { useEffect, useRef, useState } from "react";
import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
import errorIcon from "../../../images/close-black.svg";
import { useObservable } from "../../hooks/useObservable";
import { useTabs } from "../../hooks/useTabs";
import TabsBase from "./TabsBase";
type Tab = TabsBase | (TabsBase & { render: () => JSX.Element });
export const Tabs = ({ tabs, activeTab }: { tabs: readonly Tab[]; activeTab: Tab }): JSX.Element => (
<div className="tabsManagerContainer">
<div id="content" className="flexContainer hideOverflows">
<div className="nav-tabs-margin">
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
{tabs.map((tab) => (
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
export const Tabs = (): JSX.Element => {
const { openedTabs, activeTab } = useTabs();
return (
<div className="tabsManagerContainer">
<div id="content" className="flexContainer hideOverflows">
<div className="nav-tabs-margin">
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
{openedTabs.map((tab) => (
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
))}
</ul>
</div>
<div className="tabPanesContainer">
{openedTabs.map((tab) => (
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
))}
</ul>
</div>
<div className="tabPanesContainer">
{tabs.map((tab) => (
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
))}
</div>
</div>
</div>
</div>
);
);
};
function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
const [hovering, setHovering] = useState(false);

View File

@@ -4,6 +4,7 @@ import * as ThemeUtility from "../../Common/ThemeUtility";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
@@ -11,7 +12,6 @@ import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { useSelectedNode } from "../useSelectedNode";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import { TabsManager } from "./TabsManager";
// TODO: Use specific actions for logging telemetry data
export default class TabsBase extends WaitsForTemplateViewModel {
private static id = 0;
@@ -28,7 +28,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
public isExecutionError = ko.observable(false);
public isExecuting = ko.observable(false);
public pendingNotification?: ko.Observable<DataModels.Notification>;
public manager?: TabsManager;
protected _theme: string;
public onLoadStartKey: number;
@@ -60,7 +59,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}
public onCloseTabButtonClick(): void {
this.manager?.closeTab(this);
useTabs.getState().closeTab(this);
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
tabName: this.constructor.name,
dataExplorerArea: Constants.Areas.Tab,
@@ -70,7 +69,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}
public onTabClick(): void {
this.manager?.activateTab(this);
useTabs.getState().activateTab(this);
}
protected updateSelectedNode(): void {
@@ -105,7 +104,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
/** @deprecated this is no longer observable, bind to comparisons with manager.activeTab() instead */
public isActive() {
return this === this.manager?.activeTab();
return this === useTabs.getState().activeTab;
}
public onActivate(): void {

View File

@@ -1,54 +0,0 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import TabsBase from "./TabsBase";
export class TabsManager {
public openedTabs = ko.observableArray<TabsBase>([]);
public activeTab = ko.observable<TabsBase>();
public activateNewTab(tab: TabsBase): void {
this.openedTabs.push(tab);
this.activateTab(tab);
}
public activateTab(tab: TabsBase): void {
if (this.openedTabs().includes(tab)) {
tab.manager = this;
this.activeTab(tab);
tab.onActivate();
}
}
public getTabs(tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean): TabsBase[] {
return this.openedTabs().filter((tab) => tab.tabKind === tabKind && (!comparator || comparator(tab)));
}
public refreshActiveTab(comparator: (tab: TabsBase) => boolean): void {
// ensures that the tab selects/highlights the right node based on resource tree expand/collapse state
this.activeTab() && comparator(this.activeTab()) && this.activeTab().onActivate();
}
public closeTabsByComparator(comparator: (tab: TabsBase) => boolean): void {
this.openedTabs()
.filter(comparator)
.forEach((tab) => tab.onCloseTabButtonClick());
}
public closeTab(tab: TabsBase): void {
const tabIndex = this.openedTabs().indexOf(tab);
if (tabIndex !== -1) {
this.openedTabs.remove(tab);
tab.manager = undefined;
if (this.openedTabs().length === 0) {
this.activeTab(undefined);
}
if (tab === this.activeTab()) {
const tabToTheRight = this.openedTabs()[tabIndex];
const lastOpenTab = this.openedTabs()[this.openedTabs().length - 1];
this.activateTab(tabToTheRight ?? lastOpenTab);
}
}
}
}

View File

@@ -1,23 +1,19 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import { container } from "../Controls/Settings/TestUtils";
import DocumentId from "../Tree/DocumentId";
import { container } from "./../Controls/Settings/TestUtils";
import DocumentsTab from "./DocumentsTab";
import { NewQueryTab } from "./QueryTab/QueryTab";
import { TabsManager } from "./TabsManager";
describe("Tabs manager tests", () => {
let tabsManager: TabsManager;
let explorer: Explorer;
describe("useTabs tests", () => {
let database: ViewModels.Database;
let collection: ViewModels.Collection;
let queryTab: NewQueryTab;
let documentsTab: DocumentsTab;
beforeEach(() => {
explorer = new Explorer();
updateUserContext({
databaseAccount: {
id: "test",
@@ -30,7 +26,6 @@ describe("Tabs manager tests", () => {
});
database = {
container: explorer,
id: ko.observable<string>("test"),
isDatabaseShared: () => false,
} as ViewModels.Database;
@@ -38,7 +33,6 @@ describe("Tabs manager tests", () => {
database.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
collection = {
container: explorer,
databaseId: "test",
id: ko.observable<string>("test"),
} as ViewModels.Collection;
@@ -76,63 +70,70 @@ describe("Tabs manager tests", () => {
documentsTab.tabId = "2";
});
beforeEach(() => (tabsManager = new TabsManager()));
beforeEach(() => useTabs.setState({ openedTabs: [], activeTab: undefined }));
it("open new tabs", () => {
tabsManager.activateNewTab(queryTab);
expect(tabsManager.openedTabs().length).toBe(1);
expect(tabsManager.openedTabs()[0]).toEqual(queryTab);
expect(tabsManager.activeTab()).toEqual(queryTab);
const { activateNewTab } = useTabs.getState();
activateNewTab(queryTab);
let tabsState = useTabs.getState();
expect(tabsState.openedTabs.length).toBe(1);
expect(tabsState.openedTabs[0]).toEqual(queryTab);
expect(tabsState.activeTab).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true);
tabsManager.activateNewTab(documentsTab);
expect(tabsManager.openedTabs().length).toBe(2);
expect(tabsManager.openedTabs()[1]).toEqual(documentsTab);
expect(tabsManager.activeTab()).toEqual(documentsTab);
activateNewTab(documentsTab);
tabsState = useTabs.getState();
expect(tabsState.openedTabs.length).toBe(2);
expect(tabsState.openedTabs[1]).toEqual(documentsTab);
expect(tabsState.activeTab).toEqual(documentsTab);
expect(queryTab.isActive()).toBe(false);
expect(documentsTab.isActive()).toBe(true);
});
it("open existing tabs", () => {
tabsManager.activateNewTab(queryTab);
tabsManager.activateNewTab(documentsTab);
tabsManager.activateTab(queryTab);
expect(tabsManager.openedTabs().length).toBe(2);
expect(tabsManager.activeTab()).toEqual(queryTab);
const { activateNewTab, activateTab } = useTabs.getState();
activateNewTab(queryTab);
activateNewTab(documentsTab);
activateTab(queryTab);
const { openedTabs, activeTab } = useTabs.getState();
expect(openedTabs.length).toBe(2);
expect(activeTab).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true);
expect(documentsTab.isActive()).toBe(false);
});
it("get tabs", () => {
tabsManager.activateNewTab(queryTab);
tabsManager.activateNewTab(documentsTab);
const { activateNewTab, getTabs } = useTabs.getState();
activateNewTab(queryTab);
activateNewTab(documentsTab);
const queryTabs = tabsManager.getTabs(ViewModels.CollectionTabKind.Query);
const queryTabs = getTabs(ViewModels.CollectionTabKind.Query);
expect(queryTabs.length).toBe(1);
expect(queryTabs[0]).toEqual(queryTab);
const documentsTabs = tabsManager.getTabs(
ViewModels.CollectionTabKind.Documents,
(tab) => tab.tabId === documentsTab.tabId
);
const documentsTabs = getTabs(ViewModels.CollectionTabKind.Documents, (tab) => tab.tabId === documentsTab.tabId);
expect(documentsTabs.length).toBe(1);
expect(documentsTabs[0]).toEqual(documentsTab);
});
it("close tabs", () => {
tabsManager.activateNewTab(queryTab);
tabsManager.activateNewTab(documentsTab);
const { activateNewTab, closeTab, closeTabsByComparator } = useTabs.getState();
activateNewTab(queryTab);
activateNewTab(documentsTab);
closeTab(documentsTab);
tabsManager.closeTab(documentsTab);
expect(tabsManager.openedTabs().length).toBe(1);
expect(tabsManager.openedTabs()[0]).toEqual(queryTab);
expect(tabsManager.activeTab()).toEqual(queryTab);
let tabsState = useTabs.getState();
expect(tabsState.openedTabs.length).toBe(1);
expect(tabsState.openedTabs[0]).toEqual(queryTab);
expect(tabsState.activeTab).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true);
expect(documentsTab.isActive()).toBe(false);
tabsManager.closeTabsByComparator((tab) => tab.tabId === queryTab.tabId);
expect(tabsManager.openedTabs().length).toBe(0);
expect(tabsManager.activeTab()).toEqual(undefined);
closeTabsByComparator((tab) => tab.tabId === queryTab.tabId);
tabsState = useTabs.getState();
expect(tabsState.openedTabs.length).toBe(0);
expect(tabsState.activeTab).toEqual(undefined);
expect(queryTab.isActive()).toBe(false);
});
});