mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-05-14 09:17:29 +01:00
duplicate tab changes
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { createCollectionContextMenuButton } from "Explorer/ContextMenuButtonFactory";
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
|
||||
jest.mock("UserContext", () => ({
|
||||
userContext: { apiType: "SQL", fabricContext: undefined, features: {} },
|
||||
}));
|
||||
|
||||
jest.mock("ConfigContext", () => ({
|
||||
configContext: { platform: "Hosted" },
|
||||
Platform: { Fabric: "Fabric" },
|
||||
}));
|
||||
|
||||
jest.mock("Platform/Fabric/FabricUtil", () => ({
|
||||
isFabric: () => false,
|
||||
isFabricNative: () => false,
|
||||
}));
|
||||
|
||||
jest.mock("Common/DatabaseAccountUtility", () => ({
|
||||
isGlobalSecondaryIndexEnabled: () => false,
|
||||
}));
|
||||
|
||||
jest.mock("Explorer/Notebook/useNotebook", () => ({
|
||||
useNotebook: { getState: jest.fn(() => ({ isShellEnabled: false })) },
|
||||
}));
|
||||
|
||||
jest.mock("Explorer/useDatabases", () => ({
|
||||
useDatabases: { getState: jest.fn(() => ({ isPinned: () => false })) },
|
||||
}));
|
||||
|
||||
jest.mock("hooks/useSidePanel", () => ({
|
||||
useSidePanel: { getState: jest.fn(() => ({ openSidePanel: jest.fn() })) },
|
||||
}));
|
||||
|
||||
jest.mock("hooks/useTabs", () => ({
|
||||
useTabs: { getState: jest.fn() },
|
||||
}));
|
||||
|
||||
jest.mock("Explorer/useSelectedNode", () => ({
|
||||
useSelectedNode: { getState: jest.fn(() => ({ setSelectedNode: jest.fn() })) },
|
||||
}));
|
||||
|
||||
jest.mock("Platform/Hosted/extractFeatures", () => ({
|
||||
extractFeatures: () => ({}),
|
||||
}));
|
||||
|
||||
const { useTabs } = require("hooks/useTabs");
|
||||
|
||||
const mockContainer = {} as any;
|
||||
|
||||
const mockCollection = {
|
||||
id: ko.observable<string>("testContainer"),
|
||||
databaseId: "testDb",
|
||||
partitionKey: { paths: ["/pk"], kind: "Hash", version: 2 },
|
||||
materializedViewDefinition: ko.observable(undefined),
|
||||
onNewQueryClick: jest.fn(),
|
||||
onNewStoredProcedureClick: jest.fn(),
|
||||
onNewUserDefinedFunctionClick: jest.fn(),
|
||||
onNewTriggerClick: jest.fn(),
|
||||
onDocumentDBDocumentsClick: jest.fn(),
|
||||
} as unknown as ViewModels.Collection;
|
||||
|
||||
describe("createCollectionContextMenuButton - Duplicate tab", () => {
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it("includes a 'Duplicate tab' menu item", () => {
|
||||
useTabs.getState.mockReturnValue({ activeTab: null });
|
||||
const items = createCollectionContextMenuButton(mockContainer, mockCollection);
|
||||
const labels = items.map((i) => i.label);
|
||||
expect(labels).toContain("Duplicate tab");
|
||||
});
|
||||
|
||||
it("calls duplicateTab() on the active tab when it belongs to this collection", () => {
|
||||
const duplicateTab = jest.fn();
|
||||
const mockActiveTab = {
|
||||
duplicateTab,
|
||||
collection: mockCollection,
|
||||
};
|
||||
useTabs.getState.mockReturnValue({ activeTab: mockActiveTab });
|
||||
|
||||
const items = createCollectionContextMenuButton(mockContainer, mockCollection);
|
||||
const duplicateItem = items.find((i) => i.label === "Duplicate tab");
|
||||
duplicateItem.onClick();
|
||||
|
||||
expect(duplicateTab).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("opens a new Items tab when no active tab belongs to this collection", () => {
|
||||
useTabs.getState.mockReturnValue({ activeTab: null });
|
||||
|
||||
const items = createCollectionContextMenuButton(mockContainer, mockCollection);
|
||||
const duplicateItem = items.find((i) => i.label === "Duplicate tab");
|
||||
duplicateItem.onClick();
|
||||
|
||||
expect(mockCollection.onDocumentDBDocumentsClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("opens a new Items tab when the active tab belongs to a different collection", () => {
|
||||
const otherCollection = { ...mockCollection, id: ko.observable("other") } as unknown as ViewModels.Collection;
|
||||
useTabs.getState.mockReturnValue({
|
||||
activeTab: { duplicateTab: jest.fn(), collection: otherCollection },
|
||||
});
|
||||
|
||||
const items = createCollectionContextMenuButton(mockContainer, mockCollection);
|
||||
const duplicateItem = items.find((i) => i.label === "Duplicate tab");
|
||||
duplicateItem.onClick();
|
||||
|
||||
expect(mockCollection.onDocumentDBDocumentsClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
|
||||
import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg";
|
||||
import AddTriggerIcon from "../../images/AddTrigger.svg";
|
||||
import AddUdfIcon from "../../images/AddUdf.svg";
|
||||
import CopyIcon from "../../images/Copy.svg";
|
||||
import DeleteCollectionIcon from "../../images/DeleteCollection.svg";
|
||||
import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg";
|
||||
import DeleteSprocIcon from "../../images/DeleteSproc.svg";
|
||||
@@ -27,6 +28,7 @@ import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
import Explorer from "./Explorer";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
||||
@@ -175,6 +177,22 @@ export const createCollectionContextMenuButton = (
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
iconSrc: CopyIcon,
|
||||
onClick: () => {
|
||||
const activeTab = useTabs.getState().activeTab;
|
||||
if (
|
||||
activeTab?.collection?.databaseId === selectedCollection.databaseId &&
|
||||
activeTab?.collection?.id() === selectedCollection.id()
|
||||
) {
|
||||
activeTab.duplicateTab();
|
||||
} else {
|
||||
selectedCollection.onDocumentDBDocumentsClick();
|
||||
}
|
||||
},
|
||||
label: t(Keys.contextMenu.duplicateTab),
|
||||
});
|
||||
|
||||
if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) {
|
||||
items.push({
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import {
|
||||
Button,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
MessageBarTitle,
|
||||
TableRowId,
|
||||
makeStyles,
|
||||
shorthands,
|
||||
Button,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
MessageBarTitle,
|
||||
TableRowId,
|
||||
makeStyles,
|
||||
shorthands,
|
||||
} from "@fluentui/react-components";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import MongoUtility from "Common/MongoUtility";
|
||||
import { createDocument } from "Common/dataAccess/createDocument";
|
||||
import {
|
||||
deleteDocument as deleteNoSqlDocument,
|
||||
deleteDocuments as deleteNoSqlDocuments,
|
||||
deleteDocument as deleteNoSqlDocument,
|
||||
deleteDocuments as deleteNoSqlDocuments,
|
||||
} from "Common/dataAccess/deleteDocument";
|
||||
import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
||||
import { readDocument } from "Common/dataAccess/readDocument";
|
||||
@@ -28,13 +28,13 @@ import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
|
||||
import {
|
||||
ColumnsSelection,
|
||||
FilterHistory,
|
||||
SubComponentName,
|
||||
TabDivider,
|
||||
deleteDocumentsTabSubComponentState,
|
||||
readDocumentsTabSubComponentState,
|
||||
saveDocumentsTabSubComponentState,
|
||||
ColumnsSelection,
|
||||
FilterHistory,
|
||||
SubComponentName,
|
||||
TabDivider,
|
||||
deleteDocumentsTabSubComponentState,
|
||||
readDocumentsTabSubComponentState,
|
||||
saveDocumentsTabSubComponentState,
|
||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||
@@ -49,6 +49,8 @@ import { userContext } from "UserContext";
|
||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||
import { Allotment } from "allotment";
|
||||
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 { format } from "react-string-format";
|
||||
import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg";
|
||||
@@ -176,6 +178,21 @@ export class DocumentsTabV2 extends TabsBase {
|
||||
};
|
||||
}
|
||||
|
||||
public duplicateTab(): void {
|
||||
const newTab = new DocumentsTabV2({
|
||||
partitionKey: this.partitionKey,
|
||||
documentIds: ko.observableArray([]),
|
||||
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 {
|
||||
return (
|
||||
<DocumentsTabComponent
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { 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 any };
|
||||
|
||||
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 * as DataModels from "../../../Contracts/DataModels";
|
||||
import type { QueryTabOptions } from "../../../Contracts/ViewModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import Explorer from "../../Explorer";
|
||||
import { IQueryTabComponentProps, ITabAccessor, QueryTabComponent } from "../../Tabs/QueryTab/QueryTabComponent";
|
||||
@@ -72,6 +73,26 @@ export class NewQueryTab extends TabsBase {
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
return <QueryTabComponent {...this.iQueryTabComponentProps} />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Spinner, SpinnerSize, TooltipHost } from "@fluentui/react";
|
||||
import { Menu, MenuItem, MenuList, MenuPopover, MenuTrigger } from "@fluentui/react-components";
|
||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
@@ -86,13 +87,15 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
||||
}
|
||||
}, [active]);
|
||||
return (
|
||||
<li
|
||||
onMouseOver={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
className={active ? "active tabList" : "tabList"}
|
||||
style={active ? { fontWeight: "bolder" } : {}}
|
||||
role="presentation"
|
||||
>
|
||||
<Menu openOnContext>
|
||||
<MenuTrigger disableButtonEnhancement>
|
||||
<li
|
||||
onMouseOver={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
className={active ? "active tabList" : "tabList"}
|
||||
style={active ? { fontWeight: "bolder" } : {}}
|
||||
role="presentation"
|
||||
>
|
||||
<span className="tabNavContentContainer">
|
||||
<div className="tab_Content">
|
||||
<TooltipHost content={useObservable(tab?.tabPath || ko.observable(""))}>
|
||||
@@ -153,7 +156,23 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</li>
|
||||
</li>
|
||||
</MenuTrigger>
|
||||
<MenuPopover>
|
||||
<MenuList>
|
||||
{tab && (
|
||||
<MenuItem onClick={() => tab.duplicateTab()}>Duplicate tab</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
|
||||
}}
|
||||
>
|
||||
Close tab
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</MenuPopover>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,10 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
public getPersistedState = (): OpenTab | null => this.persistedState;
|
||||
public triggerPersistState: () => void = undefined;
|
||||
|
||||
public duplicateTab(): void {
|
||||
// Subclasses override this to support tab duplication
|
||||
}
|
||||
|
||||
public onCloseTabButtonClick(): void {
|
||||
useTabs.getState().closeTab(this);
|
||||
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
||||
|
||||
@@ -313,7 +313,8 @@
|
||||
"newTrigger": "New Trigger",
|
||||
"deleteStoredProcedure": "Delete Stored Procedure",
|
||||
"deleteTrigger": "Delete Trigger",
|
||||
"deleteUdf": "Delete User Defined Function"
|
||||
"deleteUdf": "Delete User Defined Function",
|
||||
"duplicateTab": "Duplicate tab"
|
||||
},
|
||||
"tabs": {
|
||||
"documents": {
|
||||
|
||||
Reference in New Issue
Block a user