From 89c08dd76fa327facc369fd4f066afa6778c9eaf Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Mon, 17 Apr 2023 14:53:09 +0300 Subject: [PATCH] feat: Make dock tabs able to be created dynamically Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen --- packages/business-features/dock/package.json | 3 +- .../dock/src/__snapshots__/dock.test.tsx.snap | 68 ++++---- .../src/{dock-tab.ts => dock-tab-type.ts} | 6 +- .../business-features/dock/src/dock.test.tsx | 152 ++++++++++++------ .../src/dock/activate-dock-tab.injectable.ts | 10 +- .../src/dock/active-dock-tab.injectable.ts | 19 ++- .../src/dock/create-dock-tab.injectable.ts | 43 +++++ .../dock/src/dock/dock-host.tsx | 13 +- .../src/dock/dock-tab-state.injectable.ts | 12 ++ .../src/dock/dock-tabs-types.injectable.ts | 4 +- .../dock/src/dock/dock-tabs.injectable.ts | 41 +++-- .../dock/src/dock/get-random-id.injectable.ts | 18 +++ 12 files changed, 270 insertions(+), 119 deletions(-) rename packages/business-features/dock/src/{dock-tab.ts => dock-tab-type.ts} (58%) create mode 100644 packages/business-features/dock/src/dock/create-dock-tab.injectable.ts create mode 100644 packages/business-features/dock/src/dock/dock-tab-state.injectable.ts create mode 100644 packages/business-features/dock/src/dock/get-random-id.injectable.ts diff --git a/packages/business-features/dock/package.json b/packages/business-features/dock/package.json index 0522d09f8b..c2a4b4d104 100644 --- a/packages/business-features/dock/package.json +++ b/packages/business-features/dock/package.json @@ -42,7 +42,8 @@ "mobx-react": "^7.6.0", "lodash": "^4.17.21", "react": "^17", - "react-dom": "^17" + "react-dom": "^17", + "uuid": "^8.3.2" }, "devDependencies": { "@async-fn/jest": "^1.6.4", diff --git a/packages/business-features/dock/src/__snapshots__/dock.test.tsx.snap b/packages/business-features/dock/src/__snapshots__/dock.test.tsx.snap index bbf89358a7..e7106d02eb 100644 --- a/packages/business-features/dock/src/__snapshots__/dock.test.tsx.snap +++ b/packages/business-features/dock/src/__snapshots__/dock.test.tsx.snap @@ -1,44 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DockHost, given rendered given implementations of dock tabs emerge renders 1`] = ` +exports[`DockHost, given rendered given implementations of dock tab types emerge renders 1`] = `
-
-
- Some title 1 -
-
-
-
- Some title 2 -
-
-
-
-
- Some content 1 -
-
+ /> +
`; -exports[`DockHost, given rendered given implementations of dock tabs emerge when the second dock tab is clicked renders 1`] = ` +exports[`DockHost, given rendered given implementations of dock tab types emerge when dock tab of one of the types is created given another dock tab is created when the second dock tab is clicked renders 1`] = `
@@ -46,7 +21,7 @@ exports[`DockHost, given rendered given implementations of dock tabs emerge when class="flex align-center" >
`; -exports[`DockHost, given rendered given no implementations of dock tabs, renders 1`] = ` +exports[`DockHost, given rendered given implementations of dock tab types emerge when dock tab of one of the types is created renders 1`] = ` + +
+
+
+
+
+ Some title 1 +
+
+
+
+
+ Some content 1 +
+
+
+
+ +`; + +exports[`DockHost, given rendered given no implementations of dock tab types, renders 1`] = `
diff --git a/packages/business-features/dock/src/dock-tab.ts b/packages/business-features/dock/src/dock-tab-type.ts similarity index 58% rename from packages/business-features/dock/src/dock-tab.ts rename to packages/business-features/dock/src/dock-tab-type.ts index 617bd115b2..c17951eb60 100644 --- a/packages/business-features/dock/src/dock-tab.ts +++ b/packages/business-features/dock/src/dock-tab-type.ts @@ -1,12 +1,12 @@ import type React from "react"; import { getInjectionToken } from "@ogre-tools/injectable"; -export type DockTab = { +export type DockTabType = { id: string; TitleComponent: React.ComponentType; ContentComponent: React.ComponentType; }; -export const dockTabInjectionToken = getInjectionToken({ - id: "dock-tab-injection-token", +export const dockTabTypeInjectionToken = getInjectionToken({ + id: "dock-tab-type-injection-token", }); diff --git a/packages/business-features/dock/src/dock.test.tsx b/packages/business-features/dock/src/dock.test.tsx index d54c85663b..296071d59f 100644 --- a/packages/business-features/dock/src/dock.test.tsx +++ b/packages/business-features/dock/src/dock.test.tsx @@ -5,10 +5,38 @@ import { DockHost } from "./dock/dock-host"; import React from "react"; import type { RenderResult } from "@testing-library/react"; import { act } from "@testing-library/react"; -import { dockTabInjectionToken } from "./dock-tab"; +import { dockTabTypeInjectionToken } from "./dock-tab-type"; import { Discover, discoverFor } from "@k8slens/react-testing-library-discovery"; import { registerFeature } from "@k8slens/feature-core"; import { dockFeature } from "./feature"; +import { createDockTabInjectionToken } from "./dock/create-dock-tab.injectable"; +import { getRandomIdInjectionToken } from "./dock/get-random-id.injectable"; + +const dockTabTypeInjectable1 = getInjectable({ + id: "some-dock-tab-type-1", + + instantiate: () => ({ + id: "some-dock-tab-type-1", + TitleComponent: () =>
Some title 1
, + + ContentComponent: () =>
Some content 1
, + }), + + injectionToken: dockTabTypeInjectionToken, +}); + +const dockTabTypeInjectable2 = getInjectable({ + id: "some-dock-tab-type-2", + + instantiate: () => ({ + id: "some-dock-tab-type-2", + TitleComponent: () =>
Some title 2
, + + ContentComponent: () =>
Some content 2
, + }), + + injectionToken: dockTabTypeInjectionToken, +}); describe("DockHost, given rendered", () => { let di: DiContainer; @@ -20,50 +48,26 @@ describe("DockHost, given rendered", () => { registerFeature(di, dockFeature); + di.override(getRandomIdInjectionToken, () => { + let index = 0; + + return () => `some-random-id-${index++}`; + }); + const render = renderFor(di); rendered = render(); discover = discoverFor(() => rendered); }); - it("given no implementations of dock tabs, renders", () => { + it("given no implementations of dock tab types, renders", () => { expect(rendered.baseElement).toMatchSnapshot(); }); - describe("given implementations of dock tabs emerge", () => { + describe("given implementations of dock tab types emerge", () => { beforeEach(() => { - const dockTabInjectable1 = getInjectable({ - id: "some-dock-tab-1", - - instantiate: () => ({ - id: "some-dock-tab-1", - TitleComponent: () =>
Some title 1
, - - ContentComponent: () => ( -
Some content 1
- ), - }), - - injectionToken: dockTabInjectionToken, - }); - - const dockTabInjectable2 = getInjectable({ - id: "some-dock-tab-2", - - instantiate: () => ({ - id: "some-dock-tab-2", - TitleComponent: () =>
Some title 2
, - - ContentComponent: () => ( -
Some content 2
- ), - }), - - injectionToken: dockTabInjectionToken, - }); - runInAction(() => { - di.register(dockTabInjectable1, dockTabInjectable2); + di.register(dockTabTypeInjectable1, dockTabTypeInjectable2); }); }); @@ -71,35 +75,83 @@ describe("DockHost, given rendered", () => { expect(rendered.baseElement).toMatchSnapshot(); }); - it("renders the titles of all the dock tabs in order", () => { - expect(discover.queryAllElements("dock-tab-title").attributeValues).toEqual([ - "some-title-1", - "some-title-2", - ]); + it("renders no tabs", () => { + const { discovered } = discover.querySingleElement("dock-tab"); + + expect(discovered).toBeNull(); }); - it("renders only the content of the first dock tab", () => { - expect(discover.queryAllElements("dock-tab-content").attributeValues).toEqual([ - "some-content-1", - ]); + it("renders no content", () => { + const { discovered } = discover.querySingleElement("dock-tab-content"); + + expect(discovered).toBeNull(); }); - describe("when the second dock tab is clicked", () => { + describe("when dock tab of one of the types is created", () => { beforeEach(() => { - act(() => { - discover.getSingleElement("dock-tab", "some-dock-tab-2").click(); - }); + const dockTabType1 = di.inject(dockTabTypeInjectable1); + + const createDockTab = di.inject(createDockTabInjectionToken); + + createDockTab({ type: dockTabType1 }); }); it("renders", () => { expect(rendered.baseElement).toMatchSnapshot(); }); - it("renders only the content of the second dock tab", () => { - expect(discover.queryAllElements("dock-tab-content").attributeValues).toEqual([ - "some-content-2", + it("renders the title for the dock tab", () => { + expect(discover.queryAllElements("dock-tab-title").attributeValues).toEqual([ + "some-title-1", ]); }); + + it("renders the content of the first dock tab", () => { + expect(discover.queryAllElements("dock-tab-content").attributeValues).toEqual([ + "some-content-1", + ]); + }); + + describe("given another dock tab is created", () => { + beforeEach(() => { + const dockTabType2 = di.inject(dockTabTypeInjectable2); + + const createDockTab = di.inject(createDockTabInjectionToken); + + createDockTab({ type: dockTabType2 }); + }); + + it("renders the titles of all the dock tabs in order of creating", () => { + expect(discover.queryAllElements("dock-tab-title").attributeValues).toEqual([ + "some-title-1", + "some-title-2", + ]); + }); + + it("renders only the content of the just created dock tab", () => { + expect(discover.queryAllElements("dock-tab-content").attributeValues).toEqual([ + "some-content-2", + ]); + }); + + describe("when the second dock tab is clicked", () => { + beforeEach(() => { + act(() => { + discover.getSingleElement("dock-tab", "some-random-id-1").click(); + }); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("renders only the content of the second dock tab", () => { + expect(discover.queryAllElements("dock-tab-content").attributeValues).toEqual([ + "some-content-2", + ]); + }); + }); + }); }); }); }); diff --git a/packages/business-features/dock/src/dock/activate-dock-tab.injectable.ts b/packages/business-features/dock/src/dock/activate-dock-tab.injectable.ts index 80d68e103a..eedac4d5a3 100644 --- a/packages/business-features/dock/src/dock/activate-dock-tab.injectable.ts +++ b/packages/business-features/dock/src/dock/activate-dock-tab.injectable.ts @@ -1,19 +1,15 @@ -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { getInjectable } from "@ogre-tools/injectable"; import { action } from "mobx"; import activeDockTabIdInjectable from "./active-dock-tab-id.injectable"; const activateDockTabInjectable = getInjectable({ id: "activate-dock-tab", - instantiate: (di, tabId) => { + instantiate: (di) => { const activeDockTabId = di.inject(activeDockTabIdInjectable); - return action(() => activeDockTabId.set(tabId)); + return action((tabId: string) => activeDockTabId.set(tabId)); }, - - lifecycle: lifecycleEnum.keyedSingleton({ - getInstanceKey: (di, tabId: string) => tabId, - }), }); export default activateDockTabInjectable; diff --git a/packages/business-features/dock/src/dock/active-dock-tab.injectable.ts b/packages/business-features/dock/src/dock/active-dock-tab.injectable.ts index 02d634eabf..b17078e0ce 100644 --- a/packages/business-features/dock/src/dock/active-dock-tab.injectable.ts +++ b/packages/business-features/dock/src/dock/active-dock-tab.injectable.ts @@ -3,24 +3,29 @@ import { computed } from "mobx"; import activeDockTabIdInjectable from "./active-dock-tab-id.injectable"; import { pipeline } from "@ogre-tools/fp"; import { defaults, find, first } from "lodash/fp"; -import type { DockTab } from "../dock-tab"; -import dockTabTypesInjectable from "./dock-tabs-types.injectable"; +import dockTabsInjectable, { DockTabViewModel } from "./dock-tabs.injectable"; -const nullTab: DockTab = { +const nullTab: DockTabViewModel = { id: "no-active-dock-tab", - TitleComponent: () => null, - ContentComponent: () => null, + + type: { + id: "no-active-dock-tab-type", + TitleComponent: () => null, + ContentComponent: () => null, + }, + + activate: () => {}, }; const activeDockTabInjectable = getInjectable({ id: "active-dock-tab", instantiate: (di) => { - const dockTabTypes = di.inject(dockTabTypesInjectable); + const dockTabs = di.inject(dockTabsInjectable); const activeDockTabId = di.inject(activeDockTabIdInjectable); return computed(() => { - const tabs = dockTabTypes.get(); + const tabs = dockTabs.get(); return pipeline( tabs, diff --git a/packages/business-features/dock/src/dock/create-dock-tab.injectable.ts b/packages/business-features/dock/src/dock/create-dock-tab.injectable.ts new file mode 100644 index 0000000000..239c16e408 --- /dev/null +++ b/packages/business-features/dock/src/dock/create-dock-tab.injectable.ts @@ -0,0 +1,43 @@ +import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; +import { runInAction } from "mobx"; +import type { DockTabType } from "../dock-tab-type"; +import { dockTabStateInjectable } from "./dock-tab-state.injectable"; +import { getRandomIdInjectionToken } from "./get-random-id.injectable"; +import activateDockTabInjectable from "./activate-dock-tab.injectable"; + +export interface CreateDockTabParams { + type: DockTabType; +} + +export type CreateDockTab = ({ type }: CreateDockTabParams) => void; + +export const createDockTabInjectionToken = getInjectionToken({ + id: "create-dock-tab-injection-token", +}); + +const createDockTabInjectable = getInjectable({ + id: "create-dock-tab", + + instantiate: (di) => { + const dockTabState = di.inject(dockTabStateInjectable); + const getRandomId = di.inject(getRandomIdInjectionToken); + const activateDockTab = di.inject(activateDockTabInjectable); + + return ({ type }) => { + runInAction(() => { + const newTabId = getRandomId(); + + dockTabState.add({ + id: newTabId, + typeId: type.id, + }); + + activateDockTab(newTabId); + }); + }; + }, + + injectionToken: createDockTabInjectionToken, +}); + +export default createDockTabInjectable; diff --git a/packages/business-features/dock/src/dock/dock-host.tsx b/packages/business-features/dock/src/dock/dock-host.tsx index 47709e5800..783a716398 100644 --- a/packages/business-features/dock/src/dock/dock-host.tsx +++ b/packages/business-features/dock/src/dock/dock-host.tsx @@ -4,18 +4,19 @@ import { withInjectables } from "@ogre-tools/injectable-react"; import React from "react"; import { Tabs } from "./tabs"; import { Div, Map } from "@k8slens/ui-components"; -import dockTabsInjectable, { ActivatableDockTab } from "./dock-tabs.injectable"; -import type { DockTab } from "../dock-tab"; +import dockTabsInjectable, { DockTabViewModel } from "./dock-tabs.injectable"; import activeDockTabInjectable from "./active-dock-tab.injectable"; const NonInjectedDockHost = observer(({ dockTabs, activeDockTab }: Dependencies) => { - const { ContentComponent: DockTabContent } = activeDockTab.get(); + const { + type: { ContentComponent: DockTabContent }, + } = activeDockTab.get(); return (
- {({ id, TitleComponent, activate }) => ( + {({ id, type: { TitleComponent }, activate }) => ( @@ -31,8 +32,8 @@ const NonInjectedDockHost = observer(({ dockTabs, activeDockTab }: Dependencies) }); interface Dependencies { - dockTabs: IComputedValue; - activeDockTab: IComputedValue; + dockTabs: IComputedValue; + activeDockTab: IComputedValue; } export const DockHost = withInjectables( diff --git a/packages/business-features/dock/src/dock/dock-tab-state.injectable.ts b/packages/business-features/dock/src/dock/dock-tab-state.injectable.ts new file mode 100644 index 0000000000..c589543407 --- /dev/null +++ b/packages/business-features/dock/src/dock/dock-tab-state.injectable.ts @@ -0,0 +1,12 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import { observable } from "mobx"; + +export interface DockTabInState { + id: string; + typeId: string; +} + +export const dockTabStateInjectable = getInjectable({ + id: "dock-tab-state", + instantiate: () => observable.set(), +}); diff --git a/packages/business-features/dock/src/dock/dock-tabs-types.injectable.ts b/packages/business-features/dock/src/dock/dock-tabs-types.injectable.ts index b62b2d5fc9..19b41f77f6 100644 --- a/packages/business-features/dock/src/dock/dock-tabs-types.injectable.ts +++ b/packages/business-features/dock/src/dock/dock-tabs-types.injectable.ts @@ -1,6 +1,6 @@ import { getInjectable } from "@ogre-tools/injectable"; import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; -import { dockTabInjectionToken } from "../dock-tab"; +import { dockTabTypeInjectionToken } from "../dock-tab-type"; const dockTabTypesInjectable = getInjectable({ id: "dock-tab-types", @@ -8,7 +8,7 @@ const dockTabTypesInjectable = getInjectable({ instantiate: (di) => { const computedInjectMany = di.inject(computedInjectManyInjectable); - return computedInjectMany(dockTabInjectionToken); + return computedInjectMany(dockTabTypeInjectionToken); }, }); diff --git a/packages/business-features/dock/src/dock/dock-tabs.injectable.ts b/packages/business-features/dock/src/dock/dock-tabs.injectable.ts index 1799e0a046..4e14e03212 100644 --- a/packages/business-features/dock/src/dock/dock-tabs.injectable.ts +++ b/packages/business-features/dock/src/dock/dock-tabs.injectable.ts @@ -1,27 +1,46 @@ +import { pipeline } from "@ogre-tools/fp"; import { getInjectable } from "@ogre-tools/injectable"; +import { filter, map } from "lodash/fp"; import { computed } from "mobx"; -import type { DockTab } from "../dock-tab"; +import type { DockTabType } from "../dock-tab-type"; +import { dockTabStateInjectable } from "./dock-tab-state.injectable"; import activateDockTabInjectable from "./activate-dock-tab.injectable"; import dockTabTypesInjectable from "./dock-tabs-types.injectable"; -type Activatable = { activate: () => void }; - -export type ActivatableDockTab = DockTab & Activatable; +export interface DockTabViewModel { + id: string; + type: DockTabType; + activate: () => void; +} const dockTabsInjectable = getInjectable({ id: "dock-tabs", instantiate: (di) => { const dockTabTypes = di.inject(dockTabTypesInjectable); - const activateFor = di.injectFactory(activateDockTabInjectable); + const dockTabState = di.inject(dockTabStateInjectable); + const activateDockTab = di.inject(activateDockTabInjectable); - return computed(() => - dockTabTypes.get().map((tab) => ({ - ...tab, + return computed((): DockTabViewModel[] => { + const dereferencedDockTabTypes = dockTabTypes.get(); - activate: activateFor(tab.id), - })), - ); + return pipeline( + [...dockTabState.values()], + + map((tab) => ({ + tab, + type: dereferencedDockTabTypes.find((type) => type.id === tab.typeId), + })), + + filter(({ type }) => !!type), + + map(({ tab, type }) => ({ + id: tab.id, + type: type as DockTabType, + activate: () => activateDockTab(tab.id), + })), + ); + }); }, }); diff --git a/packages/business-features/dock/src/dock/get-random-id.injectable.ts b/packages/business-features/dock/src/dock/get-random-id.injectable.ts new file mode 100644 index 0000000000..882f73fc27 --- /dev/null +++ b/packages/business-features/dock/src/dock/get-random-id.injectable.ts @@ -0,0 +1,18 @@ +import { getInjectable } from "@ogre-tools/injectable"; +import { v4 as getRandomId } from "uuid"; +import { getInjectionToken } from "@ogre-tools/injectable"; + +export type GetRandomId = () => string; + +export const getRandomIdInjectionToken = getInjectionToken({ + id: "get-random-id-injection-token", +}); + +const getRandomIdInjectable = getInjectable({ + id: "get-random-id", + instantiate: () => () => getRandomId(), + causesSideEffects: true, + injectionToken: getRandomIdInjectionToken, +}); + +export default getRandomIdInjectable;