mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-05-15 01:37:37 +01:00
feat: Add "Duplicate Tab" support for Items, Query, and Settings tabs
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
import * as ko from "knockout";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import { DocumentsTabV2 } from "./DocumentsTabV2";
|
||||||
|
|
||||||
|
jest.mock("hooks/useTabs", () => ({
|
||||||
|
useTabs: {
|
||||||
|
getState: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("UserContext", () => ({
|
||||||
|
userContext: { apiType: "SQL" },
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Explorer/Menus/CommandBar/CommandBarComponentAdapter", () => ({
|
||||||
|
useCommandBar: { getState: jest.fn(() => ({ setContextButtons: jest.fn() })) },
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
||||||
|
EditorReact: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockCollection = {
|
||||||
|
id: ko.observable<string>("testContainer"),
|
||||||
|
databaseId: "testDb",
|
||||||
|
partitionKey: { paths: ["/pk"], kind: "Hash", version: 2 },
|
||||||
|
selectedSubnodeKind: jest.fn(),
|
||||||
|
container: {},
|
||||||
|
} as unknown as ViewModels.Collection;
|
||||||
|
|
||||||
|
const buildTab = () =>
|
||||||
|
new DocumentsTabV2({
|
||||||
|
partitionKey: mockCollection.partitionKey,
|
||||||
|
documentIds: ko.observableArray([]),
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||||
|
title: "Items",
|
||||||
|
collection: mockCollection,
|
||||||
|
node: mockCollection,
|
||||||
|
tabPath: "testDb>testContainer>Documents",
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DocumentsTabV2.duplicateTab", () => {
|
||||||
|
let activateNewTab: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
activateNewTab = jest.fn();
|
||||||
|
(useTabs.getState as jest.Mock).mockReturnValue({ activateNewTab });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
it("calls activateNewTab with a new DocumentsTabV2 instance", () => {
|
||||||
|
const tab = buildTab();
|
||||||
|
tab.duplicateTab();
|
||||||
|
|
||||||
|
expect(activateNewTab).toHaveBeenCalledTimes(1);
|
||||||
|
const newTab = activateNewTab.mock.calls[0][0];
|
||||||
|
expect(newTab).toBeInstanceOf(DocumentsTabV2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a duplicate with the same collection", () => {
|
||||||
|
const tab = buildTab();
|
||||||
|
tab.duplicateTab();
|
||||||
|
|
||||||
|
const newTab = activateNewTab.mock.calls[0][0] as DocumentsTabV2;
|
||||||
|
expect(newTab.collection).toBe(mockCollection);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a duplicate with the same partitionKey", () => {
|
||||||
|
const tab = buildTab();
|
||||||
|
tab.duplicateTab();
|
||||||
|
|
||||||
|
const newTab = activateNewTab.mock.calls[0][0] as DocumentsTabV2;
|
||||||
|
expect(newTab.partitionKey).toEqual(mockCollection.partitionKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a distinct tab instance", () => {
|
||||||
|
const tab = buildTab();
|
||||||
|
tab.duplicateTab();
|
||||||
|
|
||||||
|
const newTab = activateNewTab.mock.calls[0][0];
|
||||||
|
expect(newTab).not.toBe(tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -49,6 +49,8 @@ import { userContext } from "UserContext";
|
|||||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
import { Allotment } from "allotment";
|
import { Allotment } from "allotment";
|
||||||
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
|
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
|
||||||
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
import ko from "knockout";
|
||||||
import React, { KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { format } from "react-string-format";
|
import { format } from "react-string-format";
|
||||||
import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg";
|
import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg";
|
||||||
@@ -176,6 +178,25 @@ export class DocumentsTabV2 extends TabsBase {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public canDuplicate(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public duplicateTab(): void {
|
||||||
|
const newTab = new DocumentsTabV2({
|
||||||
|
partitionKey: this.partitionKey,
|
||||||
|
documentIds: ko.observableArray<DocumentId>([]),
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||||
|
title: "Items",
|
||||||
|
collection: this.collection,
|
||||||
|
node: this.collection,
|
||||||
|
tabPath: `${this.collection.databaseId}>${this.collection.id()}>Documents`,
|
||||||
|
isPreferredApiMongoDB: userContext.apiType === "Mongo",
|
||||||
|
resourceTokenPartitionKey: this.resourceTokenPartitionKey,
|
||||||
|
});
|
||||||
|
useTabs.getState().activateNewTab(newTab);
|
||||||
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocumentsTabComponent
|
<DocumentsTabComponent
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ActionType, TabKind } from "Contracts/ActionContracts";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import MongoUtility from "../../../Common/MongoUtility";
|
import MongoUtility from "../../../Common/MongoUtility";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import { useTabs } from "../../../hooks/useTabs";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { NewQueryTab } from "../QueryTab/QueryTab";
|
import { NewQueryTab } from "../QueryTab/QueryTab";
|
||||||
import { IQueryTabComponentProps, ITabAccessor, QueryTabComponent } from "../QueryTab/QueryTabComponent";
|
import { IQueryTabComponentProps, ITabAccessor, QueryTabComponent } from "../QueryTab/QueryTabComponent";
|
||||||
@@ -67,6 +68,29 @@ export class NewMongoQueryTab extends NewQueryTab {
|
|||||||
return MongoUtility.tojson(value, undefined, false);
|
return MongoUtility.tojson(value, undefined, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public canDuplicate(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public duplicateTab(): void {
|
||||||
|
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||||
|
const newTab = new NewMongoQueryTab(
|
||||||
|
{
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Query,
|
||||||
|
title: `Query ${id}`,
|
||||||
|
tabPath: "",
|
||||||
|
collection: this.collection,
|
||||||
|
node: this.collection,
|
||||||
|
queryText: this.persistedState?.query?.text ?? "",
|
||||||
|
partitionKey: this.partitionKey,
|
||||||
|
splitterDirection: this.persistedState?.splitterDirection,
|
||||||
|
queryViewSizePercent: this.persistedState?.queryViewSizePercent,
|
||||||
|
},
|
||||||
|
this.mongoQueryTabProps,
|
||||||
|
);
|
||||||
|
useTabs.getState().activateNewTab(newTab);
|
||||||
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return <QueryTabComponent {...this.iMongoQueryTabComponentProps} />;
|
return <QueryTabComponent {...this.iMongoQueryTabComponentProps} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
import * as ko from "knockout";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import { IQueryTabProps, NewQueryTab } from "./QueryTab";
|
||||||
|
|
||||||
|
jest.mock("hooks/useTabs", () => ({
|
||||||
|
useTabs: {
|
||||||
|
getState: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Explorer/Menus/CommandBar/CommandBarComponentAdapter", () => ({
|
||||||
|
useCommandBar: { getState: jest.fn(() => ({ setContextButtons: jest.fn() })) },
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
||||||
|
loadState: jest.fn(),
|
||||||
|
AppStateComponentNames: {},
|
||||||
|
readSubComponentState: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Common/MessageHandler", () => ({
|
||||||
|
sendMessage: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockCollection = {
|
||||||
|
id: ko.observable<string>("testContainer"),
|
||||||
|
databaseId: "testDb",
|
||||||
|
partitionKey: { paths: ["/pk"], kind: "Hash", version: 2 },
|
||||||
|
selectedSubnodeKind: jest.fn(),
|
||||||
|
container: {},
|
||||||
|
} as unknown as ViewModels.Collection;
|
||||||
|
|
||||||
|
const mockProps = { container: {} as IQueryTabProps["container"] };
|
||||||
|
|
||||||
|
const buildTab = (queryText = "SELECT * FROM c") =>
|
||||||
|
new NewQueryTab(
|
||||||
|
{
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Query,
|
||||||
|
title: "Query 1",
|
||||||
|
tabPath: "",
|
||||||
|
collection: mockCollection,
|
||||||
|
node: mockCollection,
|
||||||
|
queryText,
|
||||||
|
partitionKey: mockCollection.partitionKey,
|
||||||
|
},
|
||||||
|
mockProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("NewQueryTab.duplicateTab", () => {
|
||||||
|
let activateNewTab: jest.Mock;
|
||||||
|
let getTabs: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
activateNewTab = jest.fn();
|
||||||
|
getTabs = jest.fn().mockReturnValue([]);
|
||||||
|
(useTabs.getState as jest.Mock).mockReturnValue({ activateNewTab, getTabs });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
it("calls activateNewTab with a new NewQueryTab instance", () => {
|
||||||
|
const tab = buildTab();
|
||||||
|
tab.duplicateTab();
|
||||||
|
|
||||||
|
expect(activateNewTab).toHaveBeenCalledTimes(1);
|
||||||
|
const newTab = activateNewTab.mock.calls[0][0];
|
||||||
|
expect(newTab).toBeInstanceOf(NewQueryTab);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves the current query text in the duplicate", () => {
|
||||||
|
const queryText = "SELECT * FROM c WHERE c.id = '123'";
|
||||||
|
const tab = buildTab(queryText);
|
||||||
|
tab.duplicateTab();
|
||||||
|
|
||||||
|
const newTab = activateNewTab.mock.calls[0][0] as NewQueryTab;
|
||||||
|
expect(newTab.iQueryTabComponentProps.queryText).toBe(queryText);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a duplicate with the same collection", () => {
|
||||||
|
const tab = buildTab();
|
||||||
|
tab.duplicateTab();
|
||||||
|
|
||||||
|
const newTab = activateNewTab.mock.calls[0][0] as NewQueryTab;
|
||||||
|
expect(newTab.collection).toBe(mockCollection);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a distinct tab instance", () => {
|
||||||
|
const tab = buildTab();
|
||||||
|
tab.duplicateTab();
|
||||||
|
|
||||||
|
const newTab = activateNewTab.mock.calls[0][0];
|
||||||
|
expect(newTab).not.toBe(tab);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("assigns an auto-incremented title based on existing query tabs", () => {
|
||||||
|
getTabs.mockReturnValue([{}, {}]); // 2 existing tabs → new title = "Query 3"
|
||||||
|
const tab = buildTab();
|
||||||
|
tab.duplicateTab();
|
||||||
|
|
||||||
|
const newTab = activateNewTab.mock.calls[0][0] as NewQueryTab;
|
||||||
|
expect(newTab.tabTitle()).toContain("Query 3");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import { MessageTypes } from "Contracts/MessageTypes";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import type { QueryTabOptions } from "../../../Contracts/ViewModels";
|
import type { QueryTabOptions } from "../../../Contracts/ViewModels";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { useTabs } from "../../../hooks/useTabs";
|
import { useTabs } from "../../../hooks/useTabs";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { IQueryTabComponentProps, ITabAccessor, QueryTabComponent } from "../../Tabs/QueryTab/QueryTabComponent";
|
import { IQueryTabComponentProps, ITabAccessor, QueryTabComponent } from "../../Tabs/QueryTab/QueryTabComponent";
|
||||||
@@ -72,6 +73,30 @@ export class NewQueryTab extends TabsBase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public canDuplicate(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public duplicateTab(): void {
|
||||||
|
const queryText = this.persistedState?.query?.text ?? "";
|
||||||
|
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||||
|
const newTab = new NewQueryTab(
|
||||||
|
{
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Query,
|
||||||
|
title: `Query ${id}`,
|
||||||
|
tabPath: "",
|
||||||
|
collection: this.collection,
|
||||||
|
node: this.collection,
|
||||||
|
queryText,
|
||||||
|
partitionKey: this.partitionKey,
|
||||||
|
splitterDirection: this.persistedState?.splitterDirection,
|
||||||
|
queryViewSizePercent: this.persistedState?.queryViewSizePercent,
|
||||||
|
},
|
||||||
|
this.props,
|
||||||
|
);
|
||||||
|
useTabs.getState().activateNewTab(newTab);
|
||||||
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return <QueryTabComponent {...this.iQueryTabComponentProps} />;
|
return <QueryTabComponent {...this.iQueryTabComponentProps} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
|
import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
|
||||||
import TabsBase from "./TabsBase";
|
import TabsBase from "./TabsBase";
|
||||||
|
|
||||||
@@ -23,6 +24,21 @@ export class CollectionSettingsTabV2 extends SettingsTabV2 {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public canDuplicate(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public duplicateTab(): void {
|
||||||
|
const newTab = new CollectionSettingsTabV2({
|
||||||
|
tabKind: ViewModels.CollectionTabKind.CollectionSettingsV2,
|
||||||
|
title: this.tabTitle(),
|
||||||
|
tabPath: "",
|
||||||
|
collection: this.collection,
|
||||||
|
node: this.collection,
|
||||||
|
});
|
||||||
|
useTabs.getState().activateNewTab(newTab);
|
||||||
|
}
|
||||||
|
|
||||||
public onActivate(): void {
|
public onActivate(): void {
|
||||||
super.onActivate();
|
super.onActivate();
|
||||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
|
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
|
||||||
|
|||||||
+90
-66
@@ -1,4 +1,5 @@
|
|||||||
import { Spinner, SpinnerSize, TooltipHost } from "@fluentui/react";
|
import { Spinner, SpinnerSize, TooltipHost } from "@fluentui/react";
|
||||||
|
import { Menu, MenuItem, MenuList, MenuPopover, MenuTrigger } from "@fluentui/react-components";
|
||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
@@ -10,6 +11,7 @@ import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
|||||||
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
||||||
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
|
import { Keys, t } from "Localization";
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
@@ -86,74 +88,96 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
|||||||
}
|
}
|
||||||
}, [active]);
|
}, [active]);
|
||||||
return (
|
return (
|
||||||
<li
|
<Menu openOnContext>
|
||||||
onMouseOver={() => setHovering(true)}
|
<MenuTrigger disableButtonEnhancement>
|
||||||
onMouseLeave={() => setHovering(false)}
|
<li
|
||||||
className={active ? "active tabList" : "tabList"}
|
onMouseOver={() => setHovering(true)}
|
||||||
style={active ? { fontWeight: "bolder" } : {}}
|
onMouseLeave={() => setHovering(false)}
|
||||||
role="presentation"
|
className={active ? "active tabList" : "tabList"}
|
||||||
>
|
style={active ? { fontWeight: "bolder" } : {}}
|
||||||
<span className="tabNavContentContainer">
|
role="presentation"
|
||||||
<div className="tab_Content">
|
>
|
||||||
<TooltipHost content={useObservable(tab?.tabPath || ko.observable(""))}>
|
<span className="tabNavContentContainer">
|
||||||
<span
|
<div className="tab_Content">
|
||||||
className="contentWrapper"
|
<TooltipHost content={useObservable(tab?.tabPath || ko.observable(""))}>
|
||||||
onClick={() => {
|
<span
|
||||||
if (tab) {
|
className="contentWrapper"
|
||||||
tab.onTabClick();
|
onClick={() => {
|
||||||
} else if (tabKind !== undefined) {
|
if (tab) {
|
||||||
useTabs.getState().activateReactTab(tabKind);
|
tab.onTabClick();
|
||||||
}
|
} else if (tabKind !== undefined) {
|
||||||
}}
|
useTabs.getState().activateReactTab(tabKind);
|
||||||
onKeyPress={({ nativeEvent: e }) => {
|
}
|
||||||
if (tab) {
|
}}
|
||||||
tab.onKeyPressActivate(undefined, e);
|
onKeyPress={({ nativeEvent: e }) => {
|
||||||
} else if (tabKind !== undefined) {
|
if (tab) {
|
||||||
onKeyPressReactTab(e, tabKind);
|
tab.onKeyPressActivate(undefined, e);
|
||||||
}
|
} else if (tabKind !== undefined) {
|
||||||
}}
|
onKeyPressReactTab(e, tabKind);
|
||||||
aria-selected={active}
|
}
|
||||||
aria-expanded={active}
|
}}
|
||||||
aria-controls={tabId}
|
aria-selected={active}
|
||||||
tabIndex={0}
|
aria-expanded={active}
|
||||||
role="tab"
|
aria-controls={tabId}
|
||||||
ref={focusTab}
|
tabIndex={0}
|
||||||
>
|
role="tab"
|
||||||
<span className="statusIconContainer" style={{ width: tabKind === ReactTabKind.Home ? 0 : 18 }}>
|
ref={focusTab}
|
||||||
{useObservable(tab?.isExecutionError || ko.observable(false)) && (
|
>
|
||||||
<ErrorIcon tab={tab} active={active} />
|
<span className="statusIconContainer" style={{ width: tabKind === ReactTabKind.Home ? 0 : 18 }}>
|
||||||
)}
|
{useObservable(tab?.isExecutionError || ko.observable(false)) && (
|
||||||
{useObservable(tab?.isExecutionWarning || ko.observable(false)) && (
|
<ErrorIcon tab={tab} active={active} />
|
||||||
<WarningIcon tab={tab} active={active} />
|
)}
|
||||||
)}
|
{useObservable(tab?.isExecutionWarning || ko.observable(false)) && (
|
||||||
{isTabExecuting(tab, tabKind) && (
|
<WarningIcon tab={tab} active={active} />
|
||||||
<Spinner
|
)}
|
||||||
size={SpinnerSize.small}
|
{isTabExecuting(tab, tabKind) && (
|
||||||
styles={{
|
<Spinner
|
||||||
circle: {
|
size={SpinnerSize.small}
|
||||||
borderTopColor: "var(--colorNeutralForeground1)",
|
styles={{
|
||||||
borderLeftColor: "var(--colorNeutralForeground1)",
|
circle: {
|
||||||
borderBottomColor: "var(--colorNeutralForeground1)",
|
borderTopColor: "var(--colorNeutralForeground1)",
|
||||||
borderRightColor: "var(--colorNeutralBackground1)",
|
borderLeftColor: "var(--colorNeutralForeground1)",
|
||||||
},
|
borderBottomColor: "var(--colorNeutralForeground1)",
|
||||||
}}
|
borderRightColor: "var(--colorNeutralBackground1)",
|
||||||
/>
|
},
|
||||||
)}
|
}}
|
||||||
{isQueryErrorThrown(tab, tabKind) && (
|
/>
|
||||||
<TooltipHost content="Error">
|
)}
|
||||||
<img src={errorQuery} alt="Error" style={{ marginTop: 4, marginLeft: 4, width: 10, height: 11 }} />
|
{isQueryErrorThrown(tab, tabKind) && (
|
||||||
</TooltipHost>
|
<TooltipHost content="Error">
|
||||||
)}
|
<img
|
||||||
|
src={errorQuery}
|
||||||
|
alt="Error"
|
||||||
|
style={{ marginTop: 4, marginLeft: 4, width: 10, height: 11 }}
|
||||||
|
/>
|
||||||
|
</TooltipHost>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="tabNavText">{tabTitle}</span>
|
||||||
|
</span>
|
||||||
|
</TooltipHost>
|
||||||
|
<span className="tabIconSection">
|
||||||
|
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} ariaLabel={tabTitle} />
|
||||||
</span>
|
</span>
|
||||||
<span className="tabNavText">{tabTitle}</span>
|
</div>
|
||||||
</span>
|
|
||||||
</TooltipHost>
|
|
||||||
<span className="tabIconSection">
|
|
||||||
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} ariaLabel={tabTitle} />
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</li>
|
||||||
</span>
|
</MenuTrigger>
|
||||||
</li>
|
<MenuPopover>
|
||||||
|
<MenuList>
|
||||||
|
{tab?.canDuplicate() && (
|
||||||
|
<MenuItem onClick={() => tab.duplicateTab()}>{t(Keys.tabs.tabMenu.duplicateTab)}</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(Keys.tabs.tabMenu.closeTab)}
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</MenuPopover>
|
||||||
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,14 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||||||
public getPersistedState = (): OpenTab | null => this.persistedState;
|
public getPersistedState = (): OpenTab | null => this.persistedState;
|
||||||
public triggerPersistState: () => void = undefined;
|
public triggerPersistState: () => void = undefined;
|
||||||
|
|
||||||
|
public canDuplicate(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public duplicateTab(): void {
|
||||||
|
// Subclasses override this to support tab duplication
|
||||||
|
}
|
||||||
|
|
||||||
public onCloseTabButtonClick(): void {
|
public onCloseTabButtonClick(): void {
|
||||||
useTabs.getState().closeTab(this);
|
useTabs.getState().closeTab(this);
|
||||||
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
||||||
|
|||||||
@@ -316,6 +316,10 @@
|
|||||||
"deleteUdf": "Delete User Defined Function"
|
"deleteUdf": "Delete User Defined Function"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
|
"tabMenu": {
|
||||||
|
"duplicateTab": "Duplicate tab",
|
||||||
|
"closeTab": "Close tab"
|
||||||
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
"newItem": "New Item",
|
"newItem": "New Item",
|
||||||
"newDocument": "New Document",
|
"newDocument": "New Document",
|
||||||
|
|||||||
Reference in New Issue
Block a user