Refactor and transform "Application update", "Preferences" and "Application menu" into Features (#6437)

* Move some code for application update to feature directory

The rest of the code could not be moved yet because of work-in-progress refactorings for OCP compliance.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce helper to get a global override for a function-injectable

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract global override-files from bloated getDiForUnitTesting

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Move some more code for application update to feature directory

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce competition for top bar items to achieve OCP-compliance

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract top menu item for opening context menu using the new competition

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract top menu item for navigating to home as OCP

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract top menu item for navigating to back as OCP

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract top menu item for navigating to forward as OCP

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract top menu item for application update as OCP

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Format code to make ongoing refactoring a bit easier

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Add missing unit tests for top bar extendability using extension API

This makes ongoing refactoring easier.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Replace implementation for old top-bar items for losing competition

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract top menu item for window controls as OCP

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce reusable component for spacing between other components

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce helper component to render list of React elements

Features:
- Placeholder for empty list
- Separators between items
- No boilerplate for "key" prop in React

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix double-clicking and dragging of window from top bar

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update snapshots

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix stuff broken in rebase

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Update snapshots after rebase

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make fake time have a default value for "now"

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Migrate some application menu items to injectables

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Consolidate separators of application menu in single file

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove duplication from separators in application menu

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract some operation system actions from application menu as injectables

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract menu item for quitting application as injectable

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce way to type narrow a string property

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make the di's for unit testing able to auto-register also named exports

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make global override less strict to simplify setup of many unit tests

There's a better solution for this in the horizon, as this overridden thing is better faked than stubbed.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify a test

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce way to create hierarchical composites from a flat array

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Implement hierarchy of application menu items using "many-root" composite

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Migrate more application menu items to injectables

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Simplify hierarchy of application menu items using "single-root" composite

Also solve composed typing of application menu by using Discriminated Unions of TypeScript, see: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update snapshot

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify creation of composite

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make composite able to have custom handler for missing parent ids

This will be useful next for application menu items, where a missing parent id cannot be fatal.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Defend against self-referencing composites

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Reintroduce non-fatal handling of orphan application menu items

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make faked, yet weak, typing a bit stronger

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify getting of composite paths

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make registrator for application menu items support all known scenarios

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Add logging for unrecognizable application menu item types

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Rename id of menu item to keep it discoverable by existing extensions

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Consolidate code to check for updates closer to feature

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce reusable horizontal line

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for preferences as a Feature

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for terminal preference tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for editor preference tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for proxy preference tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for telemetry preference tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for application preference tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move code related to helm chart preferences under related Feature in preparation for competition

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for kubernetes preference tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for application preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for editor preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for kubernetes preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for proxy preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing observer to make sure component updates

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for telemetry preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for terminal preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak UI for preferences

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove dead code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak more UI of preferences

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Revert "Introduce reusable horizontal line"

This reverts commit 4d8c147fe0f1a14bd884f73cf345e7d3a28b954a.

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove dead code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce utility to find exactly one item from array

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for preferences navigation

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move code under a Feature

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove usage of old code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove dead code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add extensions tab group to preferences

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce way to find out if composite has a descendant

This will serve eg. hiding of empty preference tab groups.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make tab groups and tabs in preferences not render when there is no content

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove code made redundant with hiding of preference tabs without content

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Tweak UI for preference navigation tab groups

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce test helper to abstract discovery of HTML elements

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Kill dead code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate discovery of HTML elements in some tests

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update snapshots

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Adapt application builder to changes in preference navigation

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Adapt test setup to changes in preference navigation

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove uninteresting technical tests that are covered by behavioural ones

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate discovery of HTML elements in a test

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove test ID made redundant by consolidating discovery of HTML elements

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove duplication from preference pages

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix import

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make queries in element discovery return matching attribute values for easier testing

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make element discovery able to do nested discovery

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make element discovery able to discover without value for attribute

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Implement registrator for preference items

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update snapshots

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make styling less brittle by not relying on static HTML-element structures with CSS-rules

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Add todo

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove "group" from preference types, as it is exact replica of "item"

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Update snapshots

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Replace usages of react-component factory with actual components for simplicity

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Start considering application preferences as default tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate naming

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make sense in horizontal lines

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Clean public interface of a normalize composite

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate name of function

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate directory structure of composite

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move utility functions to common

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Rename a preference item type to better communicate intent in UI

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move shared UI component to more common place

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate sizing of horizontal line to t-shirts

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move application update related preferences under application update feature

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make winner of competition to use original route for preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make HTML element discovery require less parameters

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract "composable-responsibilities" for Discriminable, Labelable, Orderable, and Showable

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Update snapshot

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove dead code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move code under a feature

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate navigating to preferences

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move code under a feature

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Relocate code under a sub-feature

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add TODO

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove dead code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix merge conflicts

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update snapshots after rebase

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix import path after rebase

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove duplication from exhaustiveness checks for discriminating unions

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add TODO

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update link to a more recent article

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify static Showability of a PreferenceItem

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Move general function to general directory

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract responsibility of "separability"

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make separator in Map-component know left and right item

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove additional separators when separated items are not shown for having no content

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Update snapshots

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix lint error

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Adapt integration test to recent changes

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Adapt more integration tests to recent changes

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make composite not care about in formatting of ids

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Adapt application builder and tests to array-like paths over string-like paths

Array-like paths do not have weakness for special characters as part of id, such as ".".

Also note: the error messaging for clicking of application menu in application builder is a bit worse now I think, but the simplification of the test code is worth it in this case IMHO.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make composite not care about formatting of ids

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Adapt application builder and tests to array-like paths over string-like paths

Array-like paths do not have weakness for special characters as part of id, such as ".".

Also note: the error messaging for clicking of application menu in application builder is a bit worse now I think, but the simplification of the test code is worth it in this case IMHO.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Consolidate output of get-composite-path to match find-composite's input

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make global overrides for functions log args of the call for devability

Also make the thrown error suggest how to fix the problem.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make attempts to log error throw in unit tests

Errors cannot be allowed to happen without a unit test explicitly causing it. Errors cannot be allowed to happen without author of unit test knowing it.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make composite not know about how children are transformed

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove some duplication from tests of composite

Also make the thrown error suggest how to fix the problem.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make composite unit test an unrealistic test scenario about undefined ids

Also make the thrown error suggest how to fix the problem.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Kill dead code

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify unit tests for composite

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Consolidate tests to now point-free composite

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify "Showable"

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make non-shown application menu items not break composite structure

This was made apparent by adding related unit tests for all known environments.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify usages of Orderable and Showable

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract being maybe Showable as explicit composable responsibility

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make more showables maybe showable

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Consolidate logic for application menu for Windows to be based on composite

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Start using named export for composite

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Rename type for accuracy

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove unnecessary type and value

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make composable responsibilities readonly to nudge towards immutability

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove a bit of duplication to create TS-constants

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make a comment and test name make more sense

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify fallthrough in a switch/case

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Replace inline styles with proper CSS

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Switch to correct type to indicate "object which might not contain a property"

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify overriding of platform in a unit test

Also make typing of platforms more strict, and remove some magic strings.
Also add a TODO for further OCP-ification.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Consolidate some "maybe-types" and arguments using them

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Update snapshots

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Iku-tursoandJanne Savolainen authored and GitHub committed 2022-10-26 11:16:23 -04:00
1 parent 38feab813c
commit 37eb236948
418 files changed
+39647 -27024

No files matched your search

@@ -0,0 +1,132 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`application-menu, given platform is 'darwin' given enough time passes populates application menu 1`] = `
Array [
"root",
"root -> mac",
"root -> mac -> about",
"root -> mac -> separator-1",
"root -> mac -> navigate-to-preferences",
"root -> mac -> navigate-to-extensions",
"root -> mac -> separator-2",
"root -> mac -> services",
"root -> mac -> separator-3",
"root -> mac -> hide",
"root -> mac -> hide-others",
"root -> mac -> unhide",
"root -> mac -> separator-4",
"root -> mac -> quit",
"root -> file",
"root -> file -> add-cluster",
"root -> file -> separator-1-for-file",
"root -> file -> close-window",
"root -> edit",
"root -> edit -> undo",
"root -> edit -> redo",
"root -> edit -> separator-1-in-edit",
"root -> edit -> cut",
"root -> edit -> copy",
"root -> edit -> paste",
"root -> edit -> delete",
"root -> edit -> separator-2-in-edit",
"root -> edit -> selectAll",
"root -> view",
"root -> view -> navigate-to-catalog",
"root -> view -> open-command-palette",
"root -> view -> separator-1-for-view",
"root -> view -> go-back",
"root -> view -> go-forward",
"root -> view -> reload",
"root -> view -> toggle-dev-tools",
"root -> view -> separator-2-for-view",
"root -> view -> reset-zoom",
"root -> view -> zoom-in",
"root -> view -> zoom-out",
"root -> view -> separator-3-for-view",
"root -> view -> toggle-full-screen",
"root -> help",
"root -> help -> navigate-to-welcome",
"root -> help -> open-documentation",
"root -> help -> open-support",
]
`;
exports[`application-menu, given platform is 'linux' given enough time passes populates application menu 1`] = `
Array [
"root",
"root -> file",
"root -> file -> add-cluster",
"root -> file -> navigate-to-preferences",
"root -> file -> navigate-to-extensions",
"root -> file -> quit",
"root -> edit",
"root -> edit -> undo",
"root -> edit -> redo",
"root -> edit -> separator-1-in-edit",
"root -> edit -> cut",
"root -> edit -> copy",
"root -> edit -> paste",
"root -> edit -> delete",
"root -> edit -> separator-2-in-edit",
"root -> edit -> selectAll",
"root -> view",
"root -> view -> navigate-to-catalog",
"root -> view -> open-command-palette",
"root -> view -> separator-1-for-view",
"root -> view -> go-back",
"root -> view -> go-forward",
"root -> view -> reload",
"root -> view -> toggle-dev-tools",
"root -> view -> separator-2-for-view",
"root -> view -> reset-zoom",
"root -> view -> zoom-in",
"root -> view -> zoom-out",
"root -> view -> separator-3-for-view",
"root -> view -> toggle-full-screen",
"root -> help",
"root -> help -> navigate-to-welcome",
"root -> help -> open-documentation",
"root -> help -> open-support",
"root -> help -> about",
]
`;
exports[`application-menu, given platform is 'win32' given enough time passes populates application menu 1`] = `
Array [
"root",
"root -> file",
"root -> file -> add-cluster",
"root -> file -> navigate-to-preferences",
"root -> file -> navigate-to-extensions",
"root -> file -> quit",
"root -> edit",
"root -> edit -> undo",
"root -> edit -> redo",
"root -> edit -> separator-1-in-edit",
"root -> edit -> cut",
"root -> edit -> copy",
"root -> edit -> paste",
"root -> edit -> delete",
"root -> edit -> separator-2-in-edit",
"root -> edit -> selectAll",
"root -> view",
"root -> view -> navigate-to-catalog",
"root -> view -> open-command-palette",
"root -> view -> separator-1-for-view",
"root -> view -> go-back",
"root -> view -> go-forward",
"root -> view -> reload",
"root -> view -> toggle-dev-tools",
"root -> view -> separator-2-for-view",
"root -> view -> reset-zoom",
"root -> view -> zoom-in",
"root -> view -> zoom-out",
"root -> view -> separator-3-for-view",
"root -> view -> toggle-full-screen",
"root -> help",
"root -> help -> navigate-to-welcome",
"root -> help -> open-documentation",
"root -> help -> open-support",
"root -> help -> about",
]
`;
@@ -0,0 +1,226 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { noop } from "lodash/fp";
import { runInAction } from "mobx";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import type { FakeExtensionOptions } from "../../renderer/components/test-utils/get-extension-fake";
import applicationMenuItemInjectionToken from "./main/menu-items/application-menu-item-injection-token";
import logErrorInjectable from "../../common/log-error.injectable";
describe("application-menu-in-legacy-extension-api", () => {
let builder: ApplicationBuilder;
let logErrorMock: jest.Mock;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.beforeApplicationStart(
(mainDi) => {
runInAction(() => {
mainDi.register(
someTopMenuItemInjectable,
someNonExtensionBasedMenuItemInjectable,
);
});
logErrorMock = jest.fn();
mainDi.override(logErrorInjectable, () => logErrorMock);
},
);
await builder.startHidden();
});
describe("when extension with application menu items is enabled", () => {
let onClickMock: jest.Mock;
let testExtensionOptions: FakeExtensionOptions;
beforeEach(() => {
onClickMock = jest.fn();
testExtensionOptions = {
id: "some-test-extension",
name: "some-extension-name",
mainOptions: {
appMenus: [
{
id: "some-non-shown-item",
parentId: "some-top-menu-item",
click: noop,
label: "Irrelevant",
visible: false,
},
{
id: "some-clickable-item",
parentId: "some-top-menu-item",
click: onClickMock,
},
{
parentId: "some-top-menu-item",
type: "separator",
},
{
id: "some-os-action-menu-item-id",
parentId: "some-top-menu-item",
role: "help",
},
{
id: "some-submenu-with-explicit-children",
parentId: "some-top-menu-item",
submenu: [
{ id: "some-explicit-child", label: "Some explicit child", click: noop },
],
},
],
},
};
builder.extensions.enable(testExtensionOptions);
});
it("related menu items exist", () => {
const menuItemPathsForExtension = builder.applicationMenu.items.filter(
(x) =>
x.join(".").startsWith("root.some-top-menu-item.some-extension-name"),
);
expect(menuItemPathsForExtension).toEqual([
["root", "some-top-menu-item", "some-extension-name/some-clickable-item"],
// Note: anonymous index "1" is used by the non-visible menu item.
["root", "some-top-menu-item", "some-extension-name/2-separator"],
["root", "some-top-menu-item", "some-extension-name/some-os-action-menu-item-id"],
["root", "some-top-menu-item", "some-extension-name/some-submenu-with-explicit-children"],
["root", "some-top-menu-item", "some-extension-name/some-submenu-with-explicit-children", "some-extension-name/some-submenu-with-explicit-children/some-explicit-child"],
]);
});
it("when the extension-based clickable menu item is clicked, does so", () => {
builder.applicationMenu.click(
"root", "some-top-menu-item", "some-extension-name/some-clickable-item",
);
expect(onClickMock).toHaveBeenCalled();
});
describe("when the extension is disabled", () => {
beforeEach(() => {
builder.extensions.disable(testExtensionOptions);
});
it("when related menu items no longer exist", () => {
const menuItemPathsForExtension = builder.applicationMenu.items.filter(
(x) =>
x.join(".").startsWith("root.some-top-menu-item.some-extension-name"),
);
expect(menuItemPathsForExtension).toEqual([]);
});
it("when the extension is enabled again, also related menu items exist again", () => {
builder.extensions.enable(testExtensionOptions);
const menuItemPathsForExtension = builder.applicationMenu.items.filter(
(x) =>
x.join(".").startsWith("root.some-top-menu-item.some-extension-name"),
);
expect(menuItemPathsForExtension).toEqual([
["root", "some-top-menu-item", "some-extension-name/some-clickable-item"],
["root", "some-top-menu-item", "some-extension-name/2-separator"],
["root", "some-top-menu-item", "some-extension-name/some-os-action-menu-item-id"],
["root", "some-top-menu-item", "some-extension-name/some-submenu-with-explicit-children"],
["root", "some-top-menu-item", "some-extension-name/some-submenu-with-explicit-children", "some-extension-name/some-submenu-with-explicit-children/some-explicit-child"],
]);
});
});
});
describe("when extension with unrecognizable application menu items is enabled", () => {
beforeEach(() => {
const testExtensionOptions: FakeExtensionOptions = {
id: "some-test-extension",
name: "some-extension-name",
mainOptions: {
appMenus: [
{
id: "some-recognizable-item",
parentId: "some-top-menu-item",
click: noop,
},
{
id: "some-unrecognizable-item",
parentId: "some-top-menu-item",
// Note: there is no way to recognize this
// click: noop,
// role: "help"
// submenu: []
// type: "separator"
},
],
},
};
builder.extensions.enable(testExtensionOptions);
});
it("only recognizable menu items from extension exist", () => {
const menuItemPathsForExtension = builder.applicationMenu.items.filter(
(x) =>
x.join(".").startsWith("root.some-top-menu-item.some-extension-name"),
);
expect(menuItemPathsForExtension).toEqual([
["root", "some-top-menu-item", "some-extension-name/some-recognizable-item"],
]);
});
it("logs about the unrecognizable item", () => {
expect(logErrorMock).toHaveBeenCalledWith(
'[MENU]: Tried to register menu item "some-extension-name/some-unrecognizable-item" but it is not recognizable as any of ApplicationMenuItemTypes',
);
});
});
});
const someTopMenuItemInjectable = getInjectable({
id: "some-top-menu-item",
instantiate: () => ({
id: "some-top-menu-item",
parentId: "root" as const,
kind: "top-level-menu" as const,
label: "Some existing root menu item",
orderNumber: 42,
}),
injectionToken: applicationMenuItemInjectionToken,
});
const someNonExtensionBasedMenuItemInjectable = getInjectable({
id: "some-non-extension-based-menu-item",
instantiate: () => ({
id: "some-non-extension-based-menu-item",
parentId: "some-top-menu-item",
kind: "clickable-menu-item" as const,
label: "Some menu item",
onClick: () => {},
orderNumber: 42,
}),
injectionToken: applicationMenuItemInjectionToken,
});
@@ -0,0 +1,59 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import populateApplicationMenuInjectable from "./main/populate-application-menu.injectable";
import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time";
import { getCompositePaths } from "../../common/utils/composite/get-composite-paths/get-composite-paths";
import platformInjectable, { allPlatforms } from "../../common/vars/platform.injectable";
describe.each(allPlatforms)("application-menu, given platform is '%s'", (platform) => {
let builder: ApplicationBuilder;
let populateApplicationMenuMock: jest.Mock;
beforeEach(async () => {
useFakeTime();
populateApplicationMenuMock = jest.fn();
builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => {
mainDi.override(platformInjectable, () => platform);
mainDi.override(
populateApplicationMenuInjectable,
() => populateApplicationMenuMock,
);
});
await builder.startHidden();
});
it("when insufficient time passes, does not populate menu items yet", () => {
advanceFakeTime(99);
expect(populateApplicationMenuMock).not.toHaveBeenCalled();
});
describe("given enough time passes", () => {
let applicationMenuPaths: string[][];
beforeEach(() => {
advanceFakeTime(100);
applicationMenuPaths = getCompositePaths(
populateApplicationMenuMock.mock.calls[0][0],
);
});
it("populates application menu with at least something", () => {
expect(applicationMenuPaths.length).toBeGreaterThan(0);
});
it("populates application menu", () => {
expect(applicationMenuPaths.map(x => x.join(" -> "))).toMatchSnapshot();
});
});
});
@@ -0,0 +1,81 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import populateApplicationMenuInjectable from "./main/populate-application-menu.injectable";
import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time";
import { getCompositePaths } from "../../common/utils/composite/get-composite-paths/get-composite-paths";
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "./main/menu-items/application-menu-item-injection-token";
import { runInAction } from "mobx";
import logErrorInjectable from "../../common/log-error.injectable";
describe("handling-of-orphan-application-menu-items, given orphan menu item", () => {
let builder: ApplicationBuilder;
let populateApplicationMenuMock: jest.Mock;
let logErrorMock: jest.Mock;
beforeEach(async () => {
useFakeTime();
populateApplicationMenuMock = jest.fn();
logErrorMock = jest.fn();
builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => {
const someOrphanMenuItemInjectable = getInjectable({
id: "some-orphan-menu-item",
instantiate: () => ({
kind: "sub-menu" as const,
id: "some-item-id",
// Note: unknown id makes this item an orphan.
parentId: "some-unknown-parent-id",
orderNumber: 0,
label: "irrelevant",
}),
injectionToken: applicationMenuItemInjectionToken,
});
runInAction(() => {
mainDi.register(someOrphanMenuItemInjectable);
});
mainDi.override(logErrorInjectable, () => logErrorMock);
mainDi.override(
populateApplicationMenuInjectable,
() => populateApplicationMenuMock,
);
});
await builder.startHidden();
});
describe("given some time passes", () => {
let applicationMenuPaths: string[][];
beforeEach(() => {
advanceFakeTime(100);
applicationMenuPaths = getCompositePaths(
populateApplicationMenuMock.mock.calls[0][0],
);
});
it("keeps showing the other application menu items without throwing", () => {
expect(applicationMenuPaths.length).toBeGreaterThan(0);
});
it("does not show orphan application menu item", () => {
expect(applicationMenuPaths.find(x => x.join(".").endsWith("some-item-id")));
});
it("logs about bad menu item", () => {
expect(logErrorMock).toHaveBeenCalledWith('[MENU]: cannot render menu item for missing parentIds: "some-unknown-parent-id"');
});
});
});
@@ -0,0 +1,64 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemsInjectable from "./application-menu-items.injectable";
import type { Composite } from "../../../common/utils/composite/get-composite/get-composite";
import { getCompositeFor } from "../../../common/utils/composite/get-composite/get-composite";
import { computed } from "mobx";
import { pipeline } from "@ogre-tools/fp";
import type { ApplicationMenuItemTypes } from "./menu-items/application-menu-item-injection-token";
import type { RootComposite } from "../../../common/utils/composite/interfaces";
import type { Discriminable } from "../../../common/utils/composable-responsibilities/discriminable/discriminable";
import { orderByOrderNumber } from "../../../common/utils/composable-responsibilities/orderable/orderable";
import logErrorInjectable from "../../../common/log-error.injectable";
import { isShown } from "../../../common/utils/composable-responsibilities/showable/showable";
export type MenuItemRoot = Discriminable<"root"> & RootComposite<"root">;
const applicationMenuItemCompositeInjectable = getInjectable({
id: "application-menu-item-composite",
instantiate: (di) => {
const menuItems = di.inject(applicationMenuItemsInjectable);
const logError = di.inject(logErrorInjectable);
return computed((): Composite<ApplicationMenuItemTypes | MenuItemRoot> => {
const items = menuItems.get();
return pipeline(
[
{
parentId: undefined,
id: "root",
kind: "root",
} as const,
...items,
],
getCompositeFor({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
transformChildren: (children) =>
pipeline(
children,
orderByOrderNumber,
(children) => children.filter(isShown),
),
handleMissingParentIds: ({ missingParentIds }) => {
logError(
`[MENU]: cannot render menu item for missing parentIds: "${missingParentIds.join(
'", "',
)}"`,
);
},
}),
);
});
},
});
export default applicationMenuItemCompositeInjectable;
@@ -0,0 +1,158 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensExtension } from "../../../extensions/lens-extension";
import type { LensMainExtension } from "../../../extensions/lens-main-extension";
import type {
ApplicationMenuItemTypes,
ClickableMenuItem,
OsActionMenuItem,
Separator,
} from "./menu-items/application-menu-item-injection-token";
import applicationMenuItemInjectionToken from "./menu-items/application-menu-item-injection-token";
import type { MenuRegistration } from "./menu-registration";
import logErrorInjectable from "../../../common/log-error.injectable";
const applicationMenuItemRegistratorInjectable = getInjectable({
id: "application-menu-item-registrator",
instantiate: (di) => {
const logError = di.inject(logErrorInjectable);
const toRecursedInjectables = toRecursedInjectablesFor(logError);
return (ext: LensExtension) => {
const extension = ext as LensMainExtension;
return extension.appMenus.flatMap(
toRecursedInjectables([extension.sanitizedExtensionId]),
);
};
},
injectionToken: extensionRegistratorInjectionToken,
});
export default applicationMenuItemRegistratorInjectable;
const toRecursedInjectablesFor = (logError: (errorMessage: string) => void) => {
const toRecursedInjectables = (previousIdPath: string[]) =>
(
registration: MenuRegistration,
index: number,
// Todo: new version of injectable would require less type parameters with defaults.
): Injectable<
ApplicationMenuItemTypes,
ApplicationMenuItemTypes,
void
>[] => {
const previousIdPathString = previousIdPath.join("/");
const registrationId = registration.id || index.toString();
const currentIdPath = [...previousIdPath, registrationId];
const currentIdPathString = currentIdPath.join("/");
const parentId = registration.parentId || previousIdPathString;
const menuItem = getApplicationMenuItem({
registration,
parentId,
currentIdPathString,
index,
});
if (!menuItem) {
logError(`[MENU]: Tried to register menu item "${currentIdPathString}" but it is not recognizable as any of ApplicationMenuItemTypes`);
return [];
}
return [
getInjectable({
id: `${currentIdPathString}/application-menu-item`,
instantiate: () => menuItem,
injectionToken: applicationMenuItemInjectionToken,
}),
...((registration.submenu as MenuRegistration[])
? (registration.submenu as MenuRegistration[]).flatMap(
toRecursedInjectables(currentIdPath),
)
: []),
];
};
return toRecursedInjectables;
};
const getApplicationMenuItem = ({
registration,
index,
currentIdPathString,
parentId,
}: {
registration: MenuRegistration;
index: number;
currentIdPathString: string;
parentId: string;
}): ApplicationMenuItemTypes | undefined => {
const orderNumber = 1000 + index * 10;
if (registration.type === "separator") {
return {
kind: "separator" as const,
id: `${currentIdPathString}-separator`,
parentId,
orderNumber,
} as Separator;
}
if (registration.submenu) {
return {
kind: "sub-menu" as const,
id: currentIdPathString,
parentId,
isShown: registration.visible ?? true,
orderNumber,
label: registration.label || "",
};
}
if (registration.click) {
return {
kind: "clickable-menu-item" as const,
id: currentIdPathString,
parentId,
// Todo: hide electron events from this abstraction.
onClick: registration.click,
label: registration.label,
isShown: registration.visible ?? true,
orderNumber,
...(registration.accelerator
? { keyboardShortcut: registration.accelerator as string }
: {}),
} as ClickableMenuItem;
}
if (registration.role) {
return {
kind: "os-action-menu-item" as const,
id: currentIdPathString,
parentId,
label: registration.label,
isShown: registration.visible ?? true,
orderNumber,
actionName: registration.role,
...(registration.accelerator
? { keyboardShortcut: registration.accelerator as string }
: {}),
} as OsActionMenuItem;
}
return undefined;
};
@@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { MenuItemConstructorOptions } from "electron";
import { computed } from "mobx";
import applicationMenuItemInjectionToken from "./menu-items/application-menu-item-injection-token";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
export interface MenuItemOpts extends MenuItemConstructorOptions {
submenu?: MenuItemConstructorOptions[];
}
const applicationMenuItemsInjectable = getInjectable({
id: "application-menu-items",
instantiate: (di) => {
const computedInjectMany = di.inject(computedInjectManyInjectable);
return computed(() =>
computedInjectMany(applicationMenuItemInjectionToken).get(),
);
},
});
export default applicationMenuItemsInjectable;
@@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { autorun } from "mobx";
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
import populateApplicationMenuInjectable from "./populate-application-menu.injectable";
import applicationMenuItemCompositeInjectable from "./application-menu-item-composite.injectable";
const applicationMenuReactivityInjectable = getInjectable({
id: "application-menu-reactivity",
instantiate: (di) => {
const applicationMenuItemComposite = di.inject(applicationMenuItemCompositeInjectable);
const populateApplicationMenu = di.inject(populateApplicationMenuInjectable);
return getStartableStoppable("application-menu-reactivity", () =>
autorun(() => populateApplicationMenu(applicationMenuItemComposite.get()), {
delay: 100,
}),
);
},
});
export default applicationMenuReactivityInjectable;
@@ -0,0 +1,91 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
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 { MaybeShowable } 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 ElectronClickable {
// TODO: This leaky abstraction is exposed in Extension API, therefore cannot be updated
onClick: (menuItem: ElectronMenuItem, browserWindow: (BrowserWindow) | (undefined), event: KeyboardEvent) => void;
}
export interface Labeled {
label: string;
}
export interface MaybeLabeled extends SetOptional<Labeled, "label"> {}
type ApplicationMenuItemType<T extends string> =
// 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
& Discriminable<T>
& ParentOfChildComposite
& ChildOfParentComposite
& MaybeShowable
& Orderable;
export type TopLevelMenu =
& ApplicationMenuItemType<"top-level-menu">
& { parentId: "root" }
& Labeled
& MayHaveElectronRole;
interface MayHaveElectronRole {
role?: ElectronRoles;
}
type ElectronRoles = Exclude<MenuItemConstructorOptions["role"], undefined>;
export type SubMenu =
& ApplicationMenuItemType<"sub-menu">
& Labeled
& ChildOfParentComposite;
export type ClickableMenuItem =
& ApplicationMenuItemType<"clickable-menu-item">
& MenuItem
& Labeled
& ElectronClickable;
export type OsActionMenuItem =
& ApplicationMenuItemType<"os-action-menu-item">
& MenuItem
& MaybeLabeled
& TriggersElectronAction;
type MenuItem =
& ChildOfParentComposite
& MayHaveKeyboardShortcut;
interface TriggersElectronAction {
actionName: ElectronRoles;
}
// Todo: SeparatorMenuItem
export type Separator =
& ApplicationMenuItemType<"separator">
& ChildOfParentComposite;
export type ApplicationMenuItemTypes =
| TopLevelMenu
| SubMenu
| OsActionMenuItem
| ClickableMenuItem
| Separator
;
const applicationMenuItemInjectionToken = getInjectionToken<ApplicationMenuItemTypes>({
id: "application-menu-item-injection-token",
});
export default applicationMenuItemInjectionToken;
@@ -0,0 +1,22 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
const editMenuItemInjectable = getInjectable({
id: "edit-application-menu-item",
instantiate: () => ({
kind: "top-level-menu" as const,
id: "edit",
parentId: "root" as const,
orderNumber: 30,
label: "Edit",
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default editMenuItemInjectable;
@@ -0,0 +1,69 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getApplicationMenuOperationSystemActionInjectable } from "../../get-application-menu-operation-system-action-injectable";
import { getApplicationMenuSeparatorInjectable } from "../../get-application-menu-separator-injectable";
export const actionForUndo = getApplicationMenuOperationSystemActionInjectable({
id: "undo",
parentId: "edit",
orderNumber: 10,
actionName: "undo",
});
export const actionForRedo = getApplicationMenuOperationSystemActionInjectable({
id: "redo",
parentId: "edit",
orderNumber: 20,
actionName: "redo",
});
export const separator1 = getApplicationMenuSeparatorInjectable({
id: "separator-1-in-edit",
parentId: "edit",
orderNumber: 30,
});
export const actionForCut = getApplicationMenuOperationSystemActionInjectable({
id: "cut",
parentId: "edit",
orderNumber: 40,
actionName: "cut",
});
export const actionForCopy = getApplicationMenuOperationSystemActionInjectable({
id: "copy",
parentId: "edit",
orderNumber: 50,
actionName: "copy",
});
export const actionForPaste = getApplicationMenuOperationSystemActionInjectable({
id: "paste",
parentId: "edit",
orderNumber: 60,
actionName: "paste",
});
export const actionForDelete = getApplicationMenuOperationSystemActionInjectable({
id: "delete",
parentId: "edit",
orderNumber: 70,
actionName: "delete",
});
export const separator2 = getApplicationMenuSeparatorInjectable({
id: "separator-2-in-edit",
parentId: "edit",
orderNumber: 80,
});
export const actionForSelectAll = getApplicationMenuOperationSystemActionInjectable({
id: "selectAll",
parentId: "edit",
orderNumber: 90,
actionName: "selectAll",
});
@@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import navigateToAddClusterInjectable from "../../../../../../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable";
const addClusterMenuItemInjectable = getInjectable({
id: "add-cluster-application-menu-item",
instantiate: (di) => {
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "file",
id: "add-cluster",
orderNumber: 10,
label: "Add Cluster",
keyboardShortcut: "CmdOrCtrl+Shift+A",
onClick: () => {
navigateToAddCluster();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default addClusterMenuItemInjectable;
@@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import isMacInjectable from "../../../../../../common/vars/is-mac.injectable";
const closeWindowMenuItemInjectable = getInjectable({
id: "close-window-application-menu-item",
instantiate: (di) => {
const isMac = di.inject(isMacInjectable);
return {
id: "close-window",
kind: "os-action-menu-item" as const,
parentId: "file",
orderNumber: 60,
actionName: "close" as const,
label: "Close Window",
keyboardShortcut: "Shift+Cmd+W",
isShown: isMac,
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default closeWindowMenuItemInjectable;
@@ -0,0 +1,22 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
const fileMenuItemInjectable = getInjectable({
id: "file-application-menu-item",
instantiate: () => ({
kind: "top-level-menu" as const,
id: "file",
parentId: "root" as const,
orderNumber: 20,
label: "File",
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default fileMenuItemInjectable;
@@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuSeparatorInjectable,
} from "../../get-application-menu-separator-injectable";
export const separator1 = getApplicationMenuSeparatorInjectable({
id: "separator-1-for-file",
parentId: "file",
orderNumber: 20,
isShownOnlyOnMac: true,
});
@@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { OsActionMenuItem } from "./application-menu-item-injection-token";
import applicationMenuItemInjectionToken from "./application-menu-item-injection-token";
const getApplicationMenuOperationSystemActionInjectable = ({
id,
...rest
}: Omit<OsActionMenuItem, "kind" >) =>
getInjectable({
id: `application-menu-operation-system-action/${id}`,
instantiate: () => ({
...rest,
id,
kind: "os-action-menu-item" as const,
}),
injectionToken: applicationMenuItemInjectionToken,
});
export { getApplicationMenuOperationSystemActionInjectable };
@@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { Separator } from "./application-menu-item-injection-token";
import applicationMenuItemInjectionToken from "./application-menu-item-injection-token";
import isMacInjectable from "../../../../common/vars/is-mac.injectable";
const getApplicationMenuSeparatorInjectable = ({
id,
isShownOnlyOnMac = false,
...rest
}: { isShownOnlyOnMac?: boolean } & Omit<
Separator,
"kind" | "isShown"
>) =>
getInjectable({
id: `application-menu-separator/${id}`,
instantiate: (di) => {
const isMac = di.inject(isMacInjectable);
const isShown = isShownOnlyOnMac ? isMac : true;
return {
...rest,
id,
kind: "separator" as const,
isShown,
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export { getApplicationMenuSeparatorInjectable };
@@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
const helpMenuItemInjectable = getInjectable({
id: "help-application-menu-item",
instantiate: () => ({
kind: "top-level-menu" as const,
id: "help",
parentId: "root" as const,
orderNumber: 50,
label: "Help",
role: "help" as const,
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default helpMenuItemInjectable;
@@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import navigateToWelcomeInjectable from "../../../../../../common/front-end-routing/routes/welcome/navigate-to-welcome.injectable";
const navigateToWelcomeMenuItem = getInjectable({
id: "navigate-to-welcome-menu-item",
instantiate: (di) => {
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "help",
id: "navigate-to-welcome",
orderNumber: 10,
label: "Welcome",
onClick: () => {
navigateToWelcome();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default navigateToWelcomeMenuItem;
@@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import { docsUrl } from "../../../../../../common/vars";
import openLinkInBrowserInjectable from "../../../../../../common/utils/open-link-in-browser.injectable";
import loggerInjectable from "../../../../../../common/logger.injectable";
const openDocumentationMenuItemInjectable = getInjectable({
id: "open-documentation-menu-item",
instantiate: (di) => {
const openLinkInBrowser = di.inject(openLinkInBrowserInjectable);
const logger = di.inject(loggerInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "help",
id: "open-documentation",
orderNumber: 20,
label: "Documentation",
// TODO: Convert to async/await
onClick: () => {
openLinkInBrowser(docsUrl).catch((error) => {
logger.error("[MENU]: failed to open browser", { error });
});
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default openDocumentationMenuItemInjectable;
@@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import { supportUrl } from "../../../../../../common/vars";
import openLinkInBrowserInjectable from "../../../../../../common/utils/open-link-in-browser.injectable";
import loggerInjectable from "../../../../../../common/logger.injectable";
const openSupportItemInjectable = getInjectable({
id: "open-support-menu-item",
instantiate: (di) => {
const openLinkInBrowser = di.inject(openLinkInBrowserInjectable);
const logger = di.inject(loggerInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "help",
id: "open-support",
orderNumber: 30,
label: "Support",
// TODO: Convert to async/await
onClick: () => {
openLinkInBrowser(supportUrl).catch((error) => {
logger.error("[MENU]: failed to open browser", { error });
});
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default openSupportItemInjectable;
@@ -0,0 +1,34 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import navigateToExtensionsInjectable from "../../../../../../common/front-end-routing/routes/extensions/navigate-to-extensions.injectable";
import isMacInjectable from "../../../../../../common/vars/is-mac.injectable";
const navigateToExtensionsMenuItem = getInjectable({
id: "navigate-to-extensions-menu-item",
instantiate: (di) => {
const navigateToExtensions = di.inject(navigateToExtensionsInjectable);
const isMac = di.inject(isMacInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: isMac ? "mac" : "file",
id: "navigate-to-extensions",
orderNumber: isMac ? 50 : 40,
label: "Extensions",
keyboardShortcut: isMac ? "CmdOrCtrl+Shift+E" : "Ctrl+Shift+E",
onClick: () => {
navigateToExtensions();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default navigateToExtensionsMenuItem;
@@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuOperationSystemActionInjectable,
} from "../../get-application-menu-operation-system-action-injectable";
export const actionForServices = getApplicationMenuOperationSystemActionInjectable({
id: "services",
parentId: "mac",
orderNumber: 80,
actionName: "services",
});
export const actionForHide = getApplicationMenuOperationSystemActionInjectable({
id: "hide",
parentId: "mac",
orderNumber: 100,
actionName: "hide",
});
export const actionForHideOthers = getApplicationMenuOperationSystemActionInjectable({
id: "hide-others",
parentId: "mac",
orderNumber: 110,
actionName: "hideOthers",
});
export const actionForUnhide = getApplicationMenuOperationSystemActionInjectable({
id: "unhide",
parentId: "mac",
orderNumber: 120,
actionName: "unhide",
});
@@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
import appNameInjectable from "../../../../../common/vars/app-name.injectable";
import isMacInjectable from "../../../../../common/vars/is-mac.injectable";
const primaryMenuItemInjectable = getInjectable({
id: "primary-application-menu-item",
instantiate: (di) => {
const appName = di.inject(appNameInjectable);
const isMac = di.inject(isMacInjectable);
return {
kind: "top-level-menu" as const,
parentId: "root" as const,
id: "mac",
orderNumber: 10,
label: appName,
isShown: isMac,
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default primaryMenuItemInjectable;
@@ -0,0 +1,35 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import stopServicesAndExitAppInjectable from "../../../../../../main/stop-services-and-exit-app.injectable";
import isMacInjectable from "../../../../../../common/vars/is-mac.injectable";
const quitApplicationMenuItemInjectable = getInjectable({
id: "quit-application-menu-item",
instantiate: (di) => {
const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable);
const isMac = di.inject(isMacInjectable);
return {
kind: "clickable-menu-item" as const,
id: "quit",
label: "Quit",
parentId: isMac ? "mac" : "file",
orderNumber: isMac ? 140 : 70,
keyboardShortcut: isMac ? "Cmd+Q" : "Alt+F4",
onClick: () => {
stopServicesAndExitApp();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default quitApplicationMenuItemInjectable;
@@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuSeparatorInjectable,
} from "../../get-application-menu-separator-injectable";
export const separator1 = getApplicationMenuSeparatorInjectable({
id: "separator-1",
parentId: "mac",
orderNumber: 30,
});
export const separator2 = getApplicationMenuSeparatorInjectable({
id: "separator-2",
parentId: "mac",
orderNumber: 70,
});
export const separator3 = getApplicationMenuSeparatorInjectable({
id: "separator-3",
parentId: "mac",
orderNumber: 90,
});
export const separator4 = getApplicationMenuSeparatorInjectable({
id: "separator-4",
parentId: "mac",
orderNumber: 130,
});
@@ -0,0 +1,35 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import productNameInjectable from "../../../../../../common/vars/product-name.injectable";
import showAboutInjectable from "./show-about.injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import isMacInjectable from "../../../../../../common/vars/is-mac.injectable";
const aboutMenuItemInjectable = getInjectable({
id: "about-menu-item",
instantiate: (di) => {
const productName = di.inject(productNameInjectable);
const showAbout = di.inject(showAboutInjectable);
const isMac = di.inject(isMacInjectable);
return {
kind: "clickable-menu-item" as const,
id: "about",
parentId: isMac ? "mac" : "help",
orderNumber: isMac ? 10 : 40,
label: `About ${productName}`,
onClick() {
showAbout();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default aboutMenuItemInjectable;
@@ -0,0 +1,45 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import showMessagePopupInjectable from "../../../../../../main/electron-app/features/show-message-popup.injectable";
import isWindowsInjectable from "../../../../../../common/vars/is-windows.injectable";
import appNameInjectable from "../../../../../../common/vars/app-name.injectable";
import productNameInjectable from "../../../../../../common/vars/product-name.injectable";
import buildVersionInjectable from "../../../../../../main/vars/build-version/build-version.injectable";
import extensionApiVersionInjectable from "../../../../../../common/vars/extension-api-version.injectable";
import applicationCopyrightInjectable from "../../../../../../common/vars/application-copyright.injectable";
const showAboutInjectable = getInjectable({
id: "show-about",
instantiate: (di) => {
const buildVersion = di.inject(buildVersionInjectable);
const extensionApiVersion = di.inject(extensionApiVersionInjectable);
const showMessagePopup = di.inject(showMessagePopupInjectable);
const isWindows = di.inject(isWindowsInjectable);
const appName = di.inject(appNameInjectable);
const productName = di.inject(productNameInjectable);
const applicationCopyright = di.inject(applicationCopyrightInjectable);
return () => {
const appInfo = [
`${appName}: ${buildVersion.get()}`,
`Extension API: ${extensionApiVersion}`,
`Electron: ${process.versions.electron}`,
`Chrome: ${process.versions.chrome}`,
`Node: ${process.versions.node}`,
applicationCopyright,
];
showMessagePopup(
`${isWindows ? " ".repeat(2) : ""}${appName}`,
productName,
appInfo.join("\r\n"),
);
};
},
});
export default showAboutInjectable;
@@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import { webContents } from "electron";
const goBackMenuItemInjectable = getInjectable({
id: "go-back-menu-item",
instantiate: () => ({
kind: "clickable-menu-item" as const,
parentId: "view",
id: "go-back",
orderNumber: 40,
label: "Back",
keyboardShortcut: "CmdOrCtrl+[",
onClick: () => {
webContents
.getAllWebContents()
.filter((wc) => wc.getType() === "window")
.forEach((wc) => wc.goBack());
},
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default goBackMenuItemInjectable;
@@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import { webContents } from "electron";
const goForwardMenuItemInjectable = getInjectable({
id: "go-forward-menu-item",
instantiate: () => ({
kind: "clickable-menu-item" as const,
parentId: "view",
id: "go-forward",
orderNumber: 50,
label: "Forward",
keyboardShortcut: "CmdOrCtrl+]",
onClick: () => {
webContents
.getAllWebContents()
.filter((wc) => wc.getType() === "window")
.forEach((wc) => wc.goForward());
},
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default goForwardMenuItemInjectable;
@@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import navigateToCatalogInjectable from "../../../../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
const navigateToCatalogMenuItemInjectable = getInjectable({
id: "navigate-to-catalog-menu-item",
instantiate: (di) => {
const navigateToCatalog = di.inject(navigateToCatalogInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "view",
id: "navigate-to-catalog",
orderNumber: 10,
label: "Catalog",
keyboardShortcut: "Shift+CmdOrCtrl+C",
onClick: () => {
navigateToCatalog();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default navigateToCatalogMenuItemInjectable;
@@ -0,0 +1,40 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import broadcastMessageInjectable from "../../../../../../common/ipc/broadcast-message.injectable";
const openCommandPaletteMenuItemInjectable = getInjectable({
id: "open-command-palette-menu-item",
instantiate: (di) => {
const broadcastMessage = di.inject(broadcastMessageInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "view",
id: "open-command-palette",
orderNumber: 20,
label: "Command Palette...",
keyboardShortcut: "Shift+CmdOrCtrl+P",
onClick(_m, _b, event) {
/**
* Don't broadcast unless it was triggered by menu iteration so that
* there aren't double events in renderer
*
* NOTE: this `?` is required because of a bug in playwright. https://github.com/microsoft/playwright/issues/10554
*/
if (!event?.triggeredByAccelerator) {
broadcastMessage("command-palette:open");
}
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default openCommandPaletteMenuItemInjectable;
@@ -0,0 +1,42 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuOperationSystemActionInjectable,
} from "../../get-application-menu-operation-system-action-injectable";
export const actionForToggleDevTools = getApplicationMenuOperationSystemActionInjectable({
id: "toggle-dev-tools",
parentId: "view",
orderNumber: 70,
actionName: "toggleDevTools",
});
export const actionForResetZoom = getApplicationMenuOperationSystemActionInjectable({
id: "reset-zoom",
parentId: "view",
orderNumber: 90,
actionName: "resetZoom",
});
export const actionForZoomIn = getApplicationMenuOperationSystemActionInjectable({
id: "zoom-in",
parentId: "view",
orderNumber: 100,
actionName: "zoomIn",
});
export const actionForZoomOut = getApplicationMenuOperationSystemActionInjectable({
id: "zoom-out",
parentId: "view",
orderNumber: 110,
actionName: "zoomOut",
});
export const actionForToggleFullScreen = getApplicationMenuOperationSystemActionInjectable({
id: "toggle-full-screen",
parentId: "view",
orderNumber: 130,
actionName: "togglefullscreen",
});
@@ -0,0 +1,34 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import reloadCurrentApplicationWindowInjectable from "../../../../../../main/start-main-application/lens-window/reload-current-application-window.injectable";
const reloadMenuItemInjectable = getInjectable({
id: "reload-menu-item",
instantiate: (di) => {
const reloadApplicationWindow = di.inject(
reloadCurrentApplicationWindowInjectable,
);
return {
kind: "clickable-menu-item" as const,
parentId: "view",
id: "reload",
orderNumber: 60,
label: "Reload",
keyboardShortcut: "CmdOrCtrl+R",
onClick: () => {
reloadApplicationWindow();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default reloadMenuItemInjectable;
@@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuSeparatorInjectable,
} from "../../get-application-menu-separator-injectable";
export const separator1 = getApplicationMenuSeparatorInjectable({
id: "separator-1-for-view",
parentId: "view",
orderNumber: 30,
});
export const separator2 = getApplicationMenuSeparatorInjectable({
id: "separator-2-for-view",
parentId: "view",
orderNumber: 80,
});
export const separator3 = getApplicationMenuSeparatorInjectable({
id: "separator-3-for-view",
parentId: "view",
orderNumber: 120,
});
@@ -0,0 +1,22 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
const viewMenuItemInjectable = getInjectable({
id: "view-application-menu-item",
instantiate: () => ({
kind: "top-level-menu" as const,
parentId: "root" as const,
id: "view",
orderNumber: 40,
label: "View",
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default viewMenuItemInjectable;
@@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { MenuItemConstructorOptions } from "electron";
export interface MenuRegistration extends MenuItemConstructorOptions {
parentId: string;
}
@@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { Menu } from "electron";
import type { MenuItemOpts } from "./application-menu-items.injectable";
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
export function buildMenu(applicationMenuItems: MenuItemOpts[]) {
Menu.setApplicationMenu(
Menu.buildFromTemplate(applicationMenuItems),
);
}
@@ -0,0 +1,8 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import populateApplicationMenuInjectable from "./populate-application-menu.injectable";
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
export default getGlobalOverride(populateApplicationMenuInjectable, () => () => {});
@@ -0,0 +1,110 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { Menu } from "electron";
import type { MenuItemOpts } from "./application-menu-items.injectable";
import type { Composite } from "../../../common/utils/composite/get-composite/get-composite";
import type { ApplicationMenuItemTypes } from "./menu-items/application-menu-item-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { map, sortBy } from "lodash/fp";
import type { MenuItemRoot } from "./application-menu-item-composite.injectable";
import { checkThatAllDiscriminablesAreExhausted } from "../../../common/utils/composable-responsibilities/discriminable/discriminable";
const populateApplicationMenuInjectable = getInjectable({
id: "populate-application-menu",
instantiate:
() => (composite: Composite<ApplicationMenuItemTypes | MenuItemRoot>) => {
const electronTemplate = getApplicationMenuTemplate(composite);
const menu = Menu.buildFromTemplate(electronTemplate);
Menu.setApplicationMenu(menu);
},
causesSideEffects: true,
});
export default populateApplicationMenuInjectable;
export const getApplicationMenuTemplate = (composite: Composite<ApplicationMenuItemTypes | MenuItemRoot>) => {
const topLevelMenus = composite.children.filter(
(x): x is Composite<ApplicationMenuItemTypes> => x.value.kind !== "root",
);
return topLevelMenus.map(toHierarchicalElectronMenuItem);
};
const toHierarchicalElectronMenuItem = (
composite: Composite<ApplicationMenuItemTypes>,
): MenuItemOpts => {
const value = composite.value;
switch (value.kind) {
case "top-level-menu": {
const { id } = composite;
const { label, role } = value;
return {
...(id ? { id } : {}),
...(role ? { role } : {}),
label,
submenu: pipeline(
composite.children,
sortBy((childComposite) => childComposite.value.orderNumber),
map(toHierarchicalElectronMenuItem),
),
};
}
case "sub-menu": {
const { id } = composite;
const { label } = value;
return {
...(id ? { id } : {}),
label,
submenu: pipeline(
composite.children,
sortBy((childComposite) => childComposite.value.orderNumber),
map(toHierarchicalElectronMenuItem),
),
};
}
case "clickable-menu-item": {
const { id } = composite;
const { label, onClick, keyboardShortcut } = value;
return {
...(id ? { id } : {}),
...(label ? { label } : {}),
...(keyboardShortcut ? { accelerator: keyboardShortcut }: {}),
click: onClick,
};
}
case "os-action-menu-item": {
const { label, keyboardShortcut, actionName } = value;
return {
...(label ? { label } : {}),
...(keyboardShortcut ? { accelerator: keyboardShortcut } : {}),
role: actionName,
};
}
case "separator": {
return {
type: "separator",
};
}
default: {
throw checkThatAllDiscriminablesAreExhausted(value);
}
}
};
@@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuReactivityInjectable from "./application-menu-reactivity.injectable";
import { onLoadOfApplicationInjectionToken } from "../../../main/start-main-application/runnable-tokens/on-load-of-application-injection-token";
const startApplicationMenuInjectable = getInjectable({
id: "start-application-menu",
instantiate: (di) => {
const applicationMenu = di.inject(
applicationMenuReactivityInjectable,
);
return {
id: "start-application-menu",
run: async () => {
await applicationMenu.start();
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default startApplicationMenuInjectable;
@@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuReactivityInjectable from "./application-menu-reactivity.injectable";
import { beforeQuitOfBackEndInjectionToken } from "../../../main/start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
const stopApplicationMenuInjectable = getInjectable({
id: "stop-application-menu",
instantiate: (di) => {
const applicationMenu = di.inject(
applicationMenuReactivityInjectable,
);
return {
id: "stop-application-menu",
run: async () => {
await applicationMenu.stop();
},
};
},
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default stopApplicationMenuInjectable;