TabsManager in react (#500)
This commit is contained in:
parent
19cf203606
commit
f2585bba14
|
@ -20,7 +20,6 @@ import QueryTab from "./Tabs/QueryTab";
|
||||||
import QueryTablesTab from "./Tabs/QueryTablesTab";
|
import QueryTablesTab from "./Tabs/QueryTablesTab";
|
||||||
import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2";
|
import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2";
|
||||||
import StoredProcedureTab from "./Tabs/StoredProcedureTab";
|
import StoredProcedureTab from "./Tabs/StoredProcedureTab";
|
||||||
import TabsManagerTemplate from "./Tabs/TabsManager.html";
|
|
||||||
import TerminalTab from "./Tabs/TerminalTab";
|
import TerminalTab from "./Tabs/TerminalTab";
|
||||||
import TriggerTab from "./Tabs/TriggerTab";
|
import TriggerTab from "./Tabs/TriggerTab";
|
||||||
import UserDefinedFunctionTab from "./Tabs/UserDefinedFunctionTab";
|
import UserDefinedFunctionTab from "./Tabs/UserDefinedFunctionTab";
|
||||||
|
@ -34,7 +33,6 @@ ko.components.register("json-editor", new JsonEditorComponent());
|
||||||
ko.components.register("diff-editor", new DiffEditorComponent());
|
ko.components.register("diff-editor", new DiffEditorComponent());
|
||||||
ko.components.register("dynamic-list", DynamicListComponent);
|
ko.components.register("dynamic-list", DynamicListComponent);
|
||||||
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
|
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
|
||||||
ko.components.register("tabs-manager", { template: TabsManagerTemplate });
|
|
||||||
|
|
||||||
// Collection Tabs
|
// Collection Tabs
|
||||||
[
|
[
|
||||||
|
@ -58,7 +56,6 @@ ko.components.register("tabs-manager", { template: TabsManagerTemplate });
|
||||||
// Panes
|
// Panes
|
||||||
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
|
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
|
||||||
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
|
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
|
||||||
|
|
||||||
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
|
ko.components.register("graph-new-vertex-pane", new PaneComponents.GraphNewVertexPaneComponent());
|
||||||
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
|
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
|
||||||
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
|
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
import ko from "knockout";
|
||||||
|
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 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} />)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="tabPanesContainer">
|
||||||
|
{...tabs.map((tab) => <TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
|
||||||
|
const [hovering, setHovering] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
onMouseOver={() => setHovering(true)}
|
||||||
|
onMouseLeave={() => setHovering(false)}
|
||||||
|
onClick={() => tab.onTabClick()}
|
||||||
|
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressActivate(undefined, e)}
|
||||||
|
className={active ? "active tabList" : "tabList"}
|
||||||
|
title={useObservable(tab.tabPath)}
|
||||||
|
aria-selected={active}
|
||||||
|
aria-expanded={active}
|
||||||
|
aria-controls={tab.tabId}
|
||||||
|
tabIndex={0}
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
<span className="tabNavContentContainer">
|
||||||
|
<a data-toggle="tab" href={"#" + tab.tabId} tabIndex={-1}>
|
||||||
|
<div className="tab_Content">
|
||||||
|
<span className="statusIconContainer">
|
||||||
|
{useObservable(tab.isExecutionError) && <ErrorIcon tab={tab} active={active} />}
|
||||||
|
{useObservable(tab.isExecuting) && (
|
||||||
|
<img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="tabNavText">{useObservable(tab.tabTitle)}</span>
|
||||||
|
<span className="tabIconSection">
|
||||||
|
<CloseButton tab={tab} active={active} hovering={hovering} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CloseButton = ({ tab, active, hovering }: { tab: Tab; active: boolean; hovering: boolean }) => (
|
||||||
|
<span
|
||||||
|
style={{ display: hovering || active ? undefined : "none" }}
|
||||||
|
title="Close"
|
||||||
|
role="button"
|
||||||
|
aria-label="Close Tab"
|
||||||
|
className="cancelButton"
|
||||||
|
onClick={() => tab.onCloseTabButtonClick()}
|
||||||
|
tabIndex={active ? 0 : undefined}
|
||||||
|
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
|
||||||
|
>
|
||||||
|
<span className="tabIcon close-Icon">
|
||||||
|
<img src={errorIcon} title="Close" alt="Close" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ErrorIcon = ({ tab, active }: { tab: Tab; active: boolean }) => (
|
||||||
|
<div
|
||||||
|
id="errorStatusIcon"
|
||||||
|
role="button"
|
||||||
|
title="Click to view more details"
|
||||||
|
tabIndex={active ? 0 : undefined}
|
||||||
|
className={active ? "actionsEnabled errorIconContainer" : "errorIconContainer"}
|
||||||
|
onClick={({ nativeEvent: e }) => tab.onErrorDetailsClick(undefined, e)}
|
||||||
|
onKeyPress={({ nativeEvent: e }) => tab.onErrorDetailsKeyPress(undefined, e)}
|
||||||
|
>
|
||||||
|
<span className="errorIcon" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
|
||||||
|
const ref = useRef<HTMLDivElement>();
|
||||||
|
const attrs = {
|
||||||
|
style: { display: active ? undefined : "none" },
|
||||||
|
className: "tabs-container",
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect((): (() => void) | void => {
|
||||||
|
const { current: element } = ref;
|
||||||
|
if (element) {
|
||||||
|
ko.applyBindings(tab, element);
|
||||||
|
const ctx = ko.contextFor(element).createChildContext(tab);
|
||||||
|
ko.applyBindingsToDescendants(ctx, element);
|
||||||
|
return () => ko.cleanNode(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("render" in tab) {
|
||||||
|
tab.isTemplateReady(true);
|
||||||
|
}
|
||||||
|
}, [ref, tab]);
|
||||||
|
|
||||||
|
if ("render" in tab) {
|
||||||
|
return <div {...attrs}>{tab.render()}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div {...attrs} ref={ref} data-bind="html: constructor.component.template" />;
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as ThemeUtility from "../../Common/ThemeUtility";
|
import * as ThemeUtility from "../../Common/ThemeUtility";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
@ -21,15 +20,13 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||||
public collection: ViewModels.CollectionBase;
|
public collection: ViewModels.CollectionBase;
|
||||||
public database: ViewModels.Database;
|
public database: ViewModels.Database;
|
||||||
public rid: string;
|
public rid: string;
|
||||||
public hasFocus: ko.Observable<boolean>;
|
|
||||||
public isMouseOver: ko.Observable<boolean>;
|
|
||||||
public tabId = `tab${TabsBase.id++}`;
|
public tabId = `tab${TabsBase.id++}`;
|
||||||
public tabKind: ViewModels.CollectionTabKind;
|
public tabKind: ViewModels.CollectionTabKind;
|
||||||
public tabTitle: ko.Observable<string>;
|
public tabTitle: ko.Observable<string>;
|
||||||
public tabPath: ko.Observable<string>;
|
public tabPath: ko.Observable<string>;
|
||||||
public hashLocation: ko.Observable<string>;
|
public hashLocation: ko.Observable<string>;
|
||||||
public isExecutionError: ko.Observable<boolean>;
|
public isExecutionError = ko.observable(false);
|
||||||
public isExecuting: ko.Observable<boolean>;
|
public isExecuting = ko.observable(false);
|
||||||
public pendingNotification?: ko.Observable<DataModels.Notification>;
|
public pendingNotification?: ko.Observable<DataModels.Notification>;
|
||||||
public manager?: TabsManager;
|
public manager?: TabsManager;
|
||||||
protected _theme: string;
|
protected _theme: string;
|
||||||
|
@ -42,16 +39,12 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||||
this.collection = options.collection;
|
this.collection = options.collection;
|
||||||
this.database = options.database;
|
this.database = options.database;
|
||||||
this.rid = options.rid || (this.collection && this.collection.rid) || "";
|
this.rid = options.rid || (this.collection && this.collection.rid) || "";
|
||||||
this.hasFocus = ko.observable<boolean>(false);
|
|
||||||
this.isMouseOver = ko.observable<boolean>(false);
|
|
||||||
this.tabKind = options.tabKind;
|
this.tabKind = options.tabKind;
|
||||||
this.tabTitle = ko.observable<string>(options.title);
|
this.tabTitle = ko.observable<string>(options.title);
|
||||||
this.tabPath =
|
this.tabPath =
|
||||||
ko.observable(options.tabPath ?? "") ||
|
ko.observable(options.tabPath ?? "") ||
|
||||||
(this.collection &&
|
(this.collection &&
|
||||||
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`));
|
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`));
|
||||||
this.isExecutionError = ko.observable<boolean>(false);
|
|
||||||
this.isExecuting = ko.observable<boolean>(false);
|
|
||||||
this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
|
this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
|
||||||
this.onLoadStartKey = options.onLoadStartKey;
|
this.onLoadStartKey = options.onLoadStartKey;
|
||||||
this.hashLocation = ko.observable<string>(options.hashLocation || "");
|
this.hashLocation = ko.observable<string>(options.hashLocation || "");
|
||||||
|
@ -117,22 +110,12 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||||
|
|
||||||
public onActivate(): void {
|
public onActivate(): void {
|
||||||
this.updateSelectedNode();
|
this.updateSelectedNode();
|
||||||
if (!!this.collection) {
|
this.collection?.selectedSubnodeKind(this.tabKind);
|
||||||
this.collection.selectedSubnodeKind(this.tabKind);
|
this.database?.selectedSubnodeKind(this.tabKind);
|
||||||
}
|
|
||||||
|
|
||||||
if (!!this.database) {
|
|
||||||
this.database.selectedSubnodeKind(this.tabKind);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hasFocus(true);
|
|
||||||
this.updateGlobalHash(this.hashLocation());
|
this.updateGlobalHash(this.hashLocation());
|
||||||
|
|
||||||
this.updateNavbarWithTabsButtons();
|
this.updateNavbarWithTabsButtons();
|
||||||
|
|
||||||
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Open, {
|
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Open, {
|
||||||
tabName: this.constructor.name,
|
tabName: this.constructor.name,
|
||||||
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
tabTitle: this.tabTitle(),
|
tabTitle: this.tabTitle(),
|
||||||
tabId: this.tabId,
|
tabId: this.tabId,
|
||||||
|
@ -140,13 +123,8 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {
|
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {
|
||||||
if (this.collection && this.collection.container) {
|
this.collection?.container?.expandConsole();
|
||||||
this.collection.container.expandConsole();
|
this.database?.container?.expandConsole();
|
||||||
}
|
|
||||||
|
|
||||||
if (this.database && this.database.container) {
|
|
||||||
this.database.container.expandConsole();
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -159,9 +137,8 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
public refresh(): Q.Promise<any> {
|
public refresh() {
|
||||||
location.reload();
|
location.reload();
|
||||||
return Q();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getContainer(): Explorer {
|
public getContainer(): Explorer {
|
||||||
|
@ -190,8 +167,3 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditorPosition {
|
|
||||||
line: number;
|
|
||||||
column: number;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
<div
|
|
||||||
id="content"
|
|
||||||
class="flexContainer hideOverflows"
|
|
||||||
data-bind="visible: activeTab() && openedTabs && openedTabs().length > 0"
|
|
||||||
>
|
|
||||||
<!-- Tabs - Start -->
|
|
||||||
<div class="nav-tabs-margin">
|
|
||||||
<ul class="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
|
||||||
<!-- ko foreach: openedTabs -->
|
|
||||||
<li
|
|
||||||
class="tabList"
|
|
||||||
data-bind="
|
|
||||||
attr: {
|
|
||||||
title: $data.tabPath,
|
|
||||||
'aria-selected': $parent.activeTab() === $data,
|
|
||||||
'aria-expanded': $parent.activeTab() === $data,
|
|
||||||
'aria-controls': $data.tabId
|
|
||||||
},
|
|
||||||
css:{
|
|
||||||
active: $parent.activeTab() === $data
|
|
||||||
},
|
|
||||||
hasFocus: $data.hasFocus,
|
|
||||||
event: { keypress: onKeyPressActivate },
|
|
||||||
click: $data.onTabClick,"
|
|
||||||
tabindex="0"
|
|
||||||
role="tab"
|
|
||||||
>
|
|
||||||
<span class="tabNavContentContainer">
|
|
||||||
<a data-toggle="tab" data-bind="attr: { href: '#' + $data.tabId }" tabindex="-1">
|
|
||||||
<div class="tab_Content">
|
|
||||||
<span class="statusIconContainer">
|
|
||||||
<div
|
|
||||||
class="errorIconContainer"
|
|
||||||
id="errorStatusIcon"
|
|
||||||
title="Click to view more details"
|
|
||||||
role="button"
|
|
||||||
data-bind="
|
|
||||||
click: onErrorDetailsClick,
|
|
||||||
event: { keypress: onErrorDetailsKeyPress },
|
|
||||||
attr: { tabindex: $parent.activeTab() === $data ? 0 : null },
|
|
||||||
css: { actionsEnabled: $parent.activeTab() === $data },
|
|
||||||
visible: isExecutionError"
|
|
||||||
>
|
|
||||||
<span class="errorIcon"></span>
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
class="loadingIcon"
|
|
||||||
title="Loading"
|
|
||||||
src="/circular_loader_black_16x16.gif"
|
|
||||||
data-bind="visible: $data.isExecuting"
|
|
||||||
alt="Loading"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span class="tabNavText" data-bind="text: $data.tabTitle"></span>
|
|
||||||
<span class="tabIconSection">
|
|
||||||
<span
|
|
||||||
aria-label="Close Tab"
|
|
||||||
role="button"
|
|
||||||
class="cancelButton"
|
|
||||||
data-bind="
|
|
||||||
click: $data.onCloseTabButtonClick,
|
|
||||||
event: { keypress: onKeyPressClose },
|
|
||||||
attr: { tabindex: $parent.activeTab() === $data ? 0 : null },
|
|
||||||
visible: $parent.activeTab() === $data || $data.isMouseOver()"
|
|
||||||
title="Close"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="tabIcon close-Icon"
|
|
||||||
data-bind="visible: $parent.activeTab() === $data || $data.isMouseOver()"
|
|
||||||
>
|
|
||||||
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<!-- /ko -->
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<!-- Tabs -- End -->
|
|
||||||
|
|
||||||
<!-- Tabs Panes -- Start -->
|
|
||||||
<div class="tabPanesContainer">
|
|
||||||
<!-- ko foreach: openedTabs -->
|
|
||||||
<div class="tabs-container" data-bind="visible: $parent.activeTab() === $data">
|
|
||||||
<span data-bind="component: { name: $data.constructor.component.name, params: $data }"></span>
|
|
||||||
</div>
|
|
||||||
<!-- /ko -->
|
|
||||||
</div>
|
|
||||||
<!-- Tabs Panes - End -->
|
|
||||||
</div>
|
|
|
@ -50,6 +50,7 @@ import { PanelContainerComponent } from "./Explorer/Panes/PanelContainerComponen
|
||||||
import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen";
|
import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen";
|
||||||
import "./Explorer/SplashScreen/SplashScreen.less";
|
import "./Explorer/SplashScreen/SplashScreen.less";
|
||||||
import "./Explorer/Tabs/QueryTab.less";
|
import "./Explorer/Tabs/QueryTab.less";
|
||||||
|
import { Tabs } from "./Explorer/Tabs/Tabs";
|
||||||
import { useConfig } from "./hooks/useConfig";
|
import { useConfig } from "./hooks/useConfig";
|
||||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||||
import { useSidePanel } from "./hooks/useSidePanel";
|
import { useSidePanel } from "./hooks/useSidePanel";
|
||||||
|
@ -79,7 +80,7 @@ const App: React.FunctionComponent = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel();
|
const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel();
|
||||||
const { tabs, tabsManager } = useTabs();
|
const { tabs, activeTab, tabsManager } = useTabs();
|
||||||
|
|
||||||
const explorerParams: ExplorerParams = {
|
const explorerParams: ExplorerParams = {
|
||||||
setIsNotificationConsoleExpanded,
|
setIsNotificationConsoleExpanded,
|
||||||
|
@ -205,7 +206,7 @@ const App: React.FunctionComponent = () => {
|
||||||
</div>
|
</div>
|
||||||
{/* Collections Tree - End */}
|
{/* Collections Tree - End */}
|
||||||
{tabs.length === 0 && <SplashScreen explorer={explorer} />}
|
{tabs.length === 0 && <SplashScreen explorer={explorer} />}
|
||||||
<div className="tabsManagerContainer" data-bind='component: { name: "tabs-manager", params: tabsManager }' />
|
<Tabs tabs={tabs} activeTab={activeTab} />
|
||||||
</div>
|
</div>
|
||||||
{/* Collections Tree and Tabs - End */}
|
{/* Collections Tree and Tabs - End */}
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Observable } from "knockout";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useObservable<T>(observable: Pick<Observable<T>, "subscribe" | "peek">): T {
|
||||||
|
const [, setValue] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = observable.subscribe(() => setValue((n) => 1 + n), undefined, "change");
|
||||||
|
return () => subscription.dispose();
|
||||||
|
}, [observable]);
|
||||||
|
|
||||||
|
return observable.peek();
|
||||||
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
import { isObservableArray, Observable, ObservableArray } from "knockout";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export function useObservableState<T>(observable: Observable<T>): [T, (s: T) => void];
|
|
||||||
export function useObservableState<T>(observable: ObservableArray<T>): [T[], (s: T[]) => void];
|
|
||||||
export function useObservableState<T>(observable: ObservableArray<T> | Observable<T>): [T | T[], (s: T | T[]) => void] {
|
|
||||||
const [value, setValue] = useState(observable());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
isObservableArray(observable)
|
|
||||||
? observable.subscribe((values) => setValue([...values]))
|
|
||||||
: observable.subscribe(setValue);
|
|
||||||
}, [observable]);
|
|
||||||
|
|
||||||
return [value, observable];
|
|
||||||
}
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import TabsBase from "../Explorer/Tabs/TabsBase";
|
import TabsBase from "../Explorer/Tabs/TabsBase";
|
||||||
import { TabsManager } from "../Explorer/Tabs/TabsManager";
|
import { TabsManager } from "../Explorer/Tabs/TabsManager";
|
||||||
import { useObservableState } from "./useObservableState";
|
import { useObservable } from "./useObservable";
|
||||||
|
|
||||||
export type UseTabs = {
|
export type UseTabs = {
|
||||||
tabs: readonly TabsBase[];
|
tabs: readonly TabsBase[];
|
||||||
|
activeTab: TabsBase;
|
||||||
tabsManager: TabsManager;
|
tabsManager: TabsManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useTabs(): UseTabs {
|
export function useTabs(): UseTabs {
|
||||||
const [tabsManager] = useState(() => new TabsManager());
|
const [tabsManager] = useState(() => new TabsManager());
|
||||||
const [tabs] = useObservableState(tabsManager.openedTabs);
|
const tabs = useObservable(tabsManager.openedTabs);
|
||||||
|
const activeTab = useObservable(tabsManager.activeTab);
|
||||||
|
|
||||||
return { tabs, tabsManager };
|
return { tabs, activeTab, tabsManager };
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue