diff --git a/src/common/utils/composable-responsibilities/discriminable/discriminable.ts b/src/common/utils/composable-responsibilities/discriminable/discriminable.ts new file mode 100644 index 0000000000..2152d283d5 --- /dev/null +++ b/src/common/utils/composable-responsibilities/discriminable/discriminable.ts @@ -0,0 +1,7 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +// See: https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions +export interface Discriminable { kind: T } diff --git a/src/common/utils/composable-responsibilities/labelable/labelable.ts b/src/common/utils/composable-responsibilities/labelable/labelable.ts new file mode 100644 index 0000000000..47ab292432 --- /dev/null +++ b/src/common/utils/composable-responsibilities/labelable/labelable.ts @@ -0,0 +1,7 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +export interface Labelable { + label: string; +} diff --git a/src/common/utils/composable-responsibilities/orderable/orderable.ts b/src/common/utils/composable-responsibilities/orderable/orderable.ts new file mode 100644 index 0000000000..5067dc6e62 --- /dev/null +++ b/src/common/utils/composable-responsibilities/orderable/orderable.ts @@ -0,0 +1,8 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export interface Orderable { + orderNumber: number; +} diff --git a/src/common/utils/composable-responsibilities/showable/showable.ts b/src/common/utils/composable-responsibilities/showable/showable.ts new file mode 100644 index 0000000000..ab59288e40 --- /dev/null +++ b/src/common/utils/composable-responsibilities/showable/showable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { IComputedValue } from "mobx"; +import { isBoolean } from "../../type-narrowing"; + +export interface Showable< + T extends IComputedValue | boolean = + | IComputedValue + | boolean, +> { + isShown?: T; +} + +export const isShown = (showable: Showable) => { + if (showable.isShown === undefined) { + return true; + } + + if (isBoolean(showable.isShown)) { + return showable.isShown; + } + + return showable.isShown.get(); +}; diff --git a/src/common/utils/composite/interfaces.ts b/src/common/utils/composite/interfaces.ts new file mode 100644 index 0000000000..6e50e68c5f --- /dev/null +++ b/src/common/utils/composite/interfaces.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export interface ParentOfChildComposite { + id: Id; +} + +export interface ChildOfParentComposite { + parentId: Id; +} + +export type RootComposite = + & { parentId: undefined } + & ParentOfChildComposite; + diff --git a/src/features/application-menu/main/application-menu-item-composite.injectable.ts b/src/features/application-menu/main/application-menu-item-composite.injectable.ts index abf5609cc3..a9fa99a9e2 100644 --- a/src/features/application-menu/main/application-menu-item-composite.injectable.ts +++ b/src/features/application-menu/main/application-menu-item-composite.injectable.ts @@ -10,8 +10,14 @@ import { computed } from "mobx"; import { pipeline } from "@ogre-tools/fp"; import type { ApplicationMenuItemTypes } from "./menu-items/application-menu-item-injection-token"; import loggerInjectable from "../../../common/logger.injectable"; +import type { RootComposite } from "../../../common/utils/composite/interfaces"; +import type { Discriminable } from "../../../common/utils/composable-responsibilities/discriminable/discriminable"; +import type { Orderable } from "../../../common/utils/composable-responsibilities/orderable/orderable"; -export interface MenuItemRoot { id: "root"; parentId: undefined; kind: "root"; orderNumber: 0 } +export type MenuItemRoot = + & Discriminable<"root"> + & RootComposite<"root"> + & Orderable; const applicationMenuItemCompositeInjectable = getInjectable({ id: "application-menu-item-composite", diff --git a/src/features/application-menu/main/application-menu-items.injectable.ts b/src/features/application-menu/main/application-menu-items.injectable.ts index 3bd6ea9379..09c180526e 100644 --- a/src/features/application-menu/main/application-menu-items.injectable.ts +++ b/src/features/application-menu/main/application-menu-items.injectable.ts @@ -5,8 +5,9 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { MenuItemConstructorOptions } from "electron"; import { computed } from "mobx"; -import applicationMenuItemInjectionToken, { isShown } from "./menu-items/application-menu-item-injection-token"; +import applicationMenuItemInjectionToken from "./menu-items/application-menu-item-injection-token"; import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; +import { isShown } from "../../../common/utils/composable-responsibilities/showable/showable"; export interface MenuItemOpts extends MenuItemConstructorOptions { submenu?: MenuItemConstructorOptions[]; diff --git a/src/features/application-menu/main/menu-items/application-menu-item-injection-token.ts b/src/features/application-menu/main/menu-items/application-menu-item-injection-token.ts index ec64d63e7e..ebcfd5667f 100644 --- a/src/features/application-menu/main/menu-items/application-menu-item-injection-token.ts +++ b/src/features/application-menu/main/menu-items/application-menu-item-injection-token.ts @@ -5,17 +5,16 @@ import { getInjectionToken } from "@ogre-tools/injectable"; import type { BrowserWindow, KeyboardEvent, MenuItemConstructorOptions, MenuItem as ElectronMenuItem } from "electron"; import type { SetOptional } from "type-fest"; +import type { ChildOfParentComposite, ParentOfChildComposite } from "../../../../common/utils/composite/interfaces"; +import type { Showable } from "../../../../common/utils/composable-responsibilities/showable/showable"; +import type { Discriminable } from "../../../../common/utils/composable-responsibilities/discriminable/discriminable"; +import type { Orderable } from "../../../../common/utils/composable-responsibilities/orderable/orderable"; export interface MayHaveKeyboardShortcut { keyboardShortcut?: string; } -export interface Showable { - isShown?: boolean; -} -export const isShown = (showable: Showable) => showable.isShown !== false; - -export interface Clickable { +export interface ElectronClickable { // TODO: This leaky abstraction is exposed in Extension API, therefore cannot be updated onClick: (menuItem: ElectronMenuItem, browserWindow: (BrowserWindow) | (undefined), event: KeyboardEvent) => void; } @@ -26,29 +25,15 @@ export interface Labeled { export interface MaybeLabeled extends SetOptional {} -export interface CanBeChildOfParent { - parentId: string; -} - -export interface Orderable { - orderNumber: number; -} - -export interface Identifiable { - id: string; -} - type ApplicationMenuItemType = // Note: "kind" is being used for Discriminated unions of TypeScript to achieve type narrowing. // See: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions - & Kind - & Identifiable - & CanBeChildOfParent + & Discriminable + & ParentOfChildComposite + & ChildOfParentComposite & Showable & Orderable; -interface Kind { kind: T } - export type TopLevelMenu = & ApplicationMenuItemType<"top-level-menu"> & { parentId: "root" } @@ -64,13 +49,13 @@ type ElectronRoles = Exclude; export type SubMenu = & ApplicationMenuItemType<"sub-menu"> & Labeled - & CanBeChildOfParent; + & ChildOfParentComposite; export type ClickableMenuItem = & ApplicationMenuItemType<"clickable-menu-item"> & MenuItem & Labeled - & Clickable; + & ElectronClickable; export type OsActionMenuItem = & ApplicationMenuItemType<"os-action-menu-item"> @@ -79,7 +64,7 @@ export type OsActionMenuItem = & TriggersElectronAction; type MenuItem = - & CanBeChildOfParent + & ChildOfParentComposite & MayHaveKeyboardShortcut; interface TriggersElectronAction { @@ -89,7 +74,7 @@ interface TriggersElectronAction { // Todo: SeparatorMenuItem export type Separator = & ApplicationMenuItemType<"separator"> - & CanBeChildOfParent; + & ChildOfParentComposite; export type ApplicationMenuItemTypes = | TopLevelMenu diff --git a/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts b/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts index 44aad314b1..d382a8fb9b 100644 --- a/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts +++ b/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts @@ -3,52 +3,54 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectionToken } from "@ogre-tools/injectable"; -import type { IComputedValue } from "mobx"; import type React from "react"; +import type { ChildOfParentComposite, ParentOfChildComposite } from "../../../../common/utils/composite/interfaces"; +import type { Discriminable } from "../../../../common/utils/composable-responsibilities/discriminable/discriminable"; +import type { Labelable } from "../../../../common/utils/composable-responsibilities/labelable/labelable"; +import type { Showable } from "../../../../common/utils/composable-responsibilities/showable/showable"; +import type { Orderable } from "../../../../common/utils/composable-responsibilities/orderable/orderable"; export type PreferenceItemComponent = React.ComponentType<{ children: React.ReactElement; item: T; }>; -export interface PreferenceTab { - kind: "tab"; - id: string; - parentId: string; - pathId: string; - label: string; - orderNumber: number; - isShown?: IComputedValue | boolean; -} +export type PreferenceTab = + & Discriminable<"tab"> + & ParentOfChildComposite + & ChildOfParentComposite + & Showable + & Labelable + & Orderable + & { pathId: string }; -export interface PreferenceTabGroup { - kind: "tab-group"; - id: string; - parentId: "preference-tabs"; - label: string; - orderNumber: number; - isShown?: IComputedValue | boolean; - iconName?: string; -} +export type PreferenceTabGroup = + & Discriminable<"tab-group"> + & ParentOfChildComposite + & ChildOfParentComposite<"preference-tabs"> + & Showable + & Labelable + & Orderable + & { iconName? : string }; -export interface PreferencePage { - kind: "page"; - id: string; - parentId: string; - isShown?: IComputedValue | boolean; +interface RenderableWithSiblings { childSeparator?: () => React.ReactElement; - Component: PreferenceItemComponent; + Component: PreferenceItemComponent; } -export interface PreferenceBlock { - kind: "block"; - id: string; - parentId: string; - orderNumber: number; - isShown?: IComputedValue | boolean; - childSeparator?: () => React.ReactElement; - Component: PreferenceItemComponent; -} +export type PreferencePage = + & Discriminable<"page"> + & ParentOfChildComposite + & ChildOfParentComposite + & Showable + & RenderableWithSiblings; + +export type PreferenceBlock = + & Discriminable<"block"> + & ParentOfChildComposite + & ChildOfParentComposite + & Showable + & RenderableWithSiblings; export type PreferenceTypes = PreferenceTabGroup | PreferenceTab | PreferenceBlock | PreferencePage; diff --git a/src/features/preferences/renderer/preference-items/preference-tab-root.tsx b/src/features/preferences/renderer/preference-items/preference-tab-root.tsx index 5f5188799b..42e1293a84 100644 --- a/src/features/preferences/renderer/preference-items/preference-tab-root.tsx +++ b/src/features/preferences/renderer/preference-items/preference-tab-root.tsx @@ -6,14 +6,15 @@ import type { IComputedValue } from "mobx"; import { computed } from "mobx"; import React from "react"; import { HorizontalLine } from "../../../../renderer/components/horizontal-line/horizontal-line"; +import type { RootComposite } from "../../../../common/utils/composite/interfaces"; +import type { Discriminable } from "../../../../common/utils/composable-responsibilities/discriminable/discriminable"; +import type { Showable } from "../../../../common/utils/composable-responsibilities/showable/showable"; -export interface PreferenceTabsRoot { - kind: "preference-tabs-root"; - id: string; - parentId: undefined; - isShown: IComputedValue; - childSeparator: () => React.ReactElement; -} +export type PreferenceTabsRoot = + & Discriminable<"preference-tabs-root"> + & RootComposite + & Showable> + & { childSeparator: () => React.ReactElement }; export const preferenceTabsRoot: PreferenceTabsRoot = { kind: "preference-tabs-root" as const, diff --git a/src/features/preferences/renderer/preference-items/preferences-composite.injectable.ts b/src/features/preferences/renderer/preference-items/preferences-composite.injectable.ts index 5b58e3d84f..26e9b1ca01 100644 --- a/src/features/preferences/renderer/preference-items/preferences-composite.injectable.ts +++ b/src/features/preferences/renderer/preference-items/preferences-composite.injectable.ts @@ -12,7 +12,7 @@ import { filter } from "lodash/fp"; import { pipeline } from "@ogre-tools/fp"; import { preferenceTabsRoot } from "./preference-tab-root"; import logErrorInjectable from "../../../../common/log-error.injectable"; -import { isBoolean } from "../../../../common/utils"; +import { isShown } from "../../../../common/utils/composable-responsibilities/showable/showable"; const preferencesCompositeInjectable = getInjectable({ id: "preferences-composite", @@ -25,18 +25,7 @@ const preferencesCompositeInjectable = getInjectable({ return computed(() => pipeline( [preferenceTabsRoot, ...preferenceItems.get()], - - filter((item: PreferenceTypes) => { - if (item.isShown === undefined) { - return true; - } - - if (isBoolean(item.isShown)) { - return item.isShown; - } - - return item.isShown.get(); - }), + filter((item: PreferenceTypes) => isShown(item)), (items) => getComposite({