From 0eac46530aea58e04f297c1042e045016ec86cf1 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Mon, 18 Oct 2021 17:27:53 +0300 Subject: [PATCH] Pin icon for Catalog list items (#3810) --- src/common/__tests__/hotbar-store.test.ts | 9 ++++ src/common/hotbar-store.ts | 10 ++++- .../+catalog/catalog-entity-drawer-menu.tsx | 17 ++++---- .../components/+catalog/catalog.module.css | 32 ++++++++++++-- .../components/+catalog/catalog.test.tsx | 6 +++ src/renderer/components/+catalog/catalog.tsx | 39 ++++++++++++++--- .../+catalog/hotbar-toggle-menu-item.tsx | 42 +++++++++++++++++++ .../components/hotbar/hotbar-entity-icon.tsx | 21 ++-------- .../components/hotbar/hotbar-menu.tsx | 2 +- src/renderer/components/icon/unpin.svg | 6 +++ 10 files changed, 147 insertions(+), 37 deletions(-) create mode 100644 src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx create mode 100644 src/renderer/components/icon/unpin.svg diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index 8207aaafac..e029835006 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -324,6 +324,15 @@ describe("HotbarStore", () => { console.error = error; console.warn = warn; }); + + it("checks if entity already pinned to hotbar", () => { + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.addToHotbar(testCluster); + + expect(hotbarStore.isAddedToActive(testCluster)).toBeTruthy(); + expect(hotbarStore.isAddedToActive(awsCluster)).toBeFalsy(); + }); }); describe("pre beta-5 migrations", () => { diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts index 5bfb1ac73a..f7b7de642d 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.ts @@ -171,7 +171,7 @@ export class HotbarStore extends BaseStore { }}; - if (hotbar.items.find(i => i?.entity.uid === uid)) { + if (this.isAddedToActive(item)) { return; } @@ -274,6 +274,14 @@ export class HotbarStore extends BaseStore { hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id; } + + /** + * Checks if entity already pinned to hotbar + * @returns boolean + */ + isAddedToActive(entity: CatalogEntity) { + return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid); + } } /** diff --git a/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx index ac4d31092a..2d180bf9db 100644 --- a/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx +++ b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx @@ -28,9 +28,9 @@ import { makeObservable, observable } from "mobx"; import { navigate } from "../../navigation"; import { MenuItem } from "../menu"; import { ConfirmDialog } from "../confirm-dialog"; -import { HotbarStore } from "../../../common/hotbar-store"; import { Icon } from "../icon"; import type { CatalogEntityItem } from "./catalog-entity-item"; +import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item"; export interface CatalogEntityDrawerMenuProps extends MenuActionsProps { item: CatalogEntityItem | null | undefined; @@ -70,10 +70,6 @@ export class CatalogEntityDrawerMenu extends React.Comp } } - addToHotbar(entity: CatalogEntity): void { - HotbarStore.getInstance().addToHotbar(entity); - } - getMenuItems(entity: T): React.ReactChild[] { if (!entity) { return []; @@ -99,9 +95,12 @@ export class CatalogEntityDrawerMenu extends React.Comp } items.push( - this.addToHotbar(entity) }> - - + } + removeContent={} + /> ); return items; @@ -109,7 +108,7 @@ export class CatalogEntityDrawerMenu extends React.Comp render() { const { className, item: entity, ...menuProps } = this.props; - + if (!this.contextMenu || !entity.enabled) { return null; } diff --git a/src/renderer/components/+catalog/catalog.module.css b/src/renderer/components/+catalog/catalog.module.css index 4c529a0934..fd730ae1e7 100644 --- a/src/renderer/components/+catalog/catalog.module.css +++ b/src/renderer/components/+catalog/catalog.module.css @@ -19,10 +19,36 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +.list :global(.TableRow) { + .entityName { + position: relative; + width: min-content; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 24px; + + .pinIcon { + position: absolute; + right: 0; + opacity: 0; + transition: none; + + &:hover { + /* Drop styles defined for */ + background-color: transparent; + box-shadow: none; + } + } + } + + &:hover .pinIcon { + opacity: 1; + } +} + .iconCell { - max-width: 40px; - display: flex; - align-items: center; + @apply flex items-center max-w-[40px]; } .iconCell > div * { diff --git a/src/renderer/components/+catalog/catalog.test.tsx b/src/renderer/components/+catalog/catalog.test.tsx index 417496a038..3357a7e7f3 100644 --- a/src/renderer/components/+catalog/catalog.test.tsx +++ b/src/renderer/components/+catalog/catalog.test.tsx @@ -46,6 +46,12 @@ jest.mock("@electron/remote", () => { }; }); +jest.mock("./hotbar-toggle-menu-item", () => { + return { + HotbarToggleMenuItem: () =>
menu item
+ }; +}); + class MockCatalogEntity extends CatalogEntity { public apiVersion = "api"; public kind = "kind"; diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 5a479d437d..594bb610e3 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -37,13 +37,15 @@ import { CatalogAddButton } from "./catalog-add-button"; import type { RouteComponentProps } from "react-router"; import { Notifications } from "../notifications"; import { MainLayout } from "../layout/main-layout"; -import { createAppStorage, cssNames } from "../../utils"; +import { createAppStorage, cssNames, prevDefault } from "../../utils"; import { makeCss } from "../../../common/utils/makeCss"; import { CatalogEntityDetails } from "./catalog-entity-details"; import { browseCatalogTab, catalogURL, CatalogViewRouteParam } from "../../../common/routes"; import { CatalogMenu } from "./catalog-menu"; import { HotbarIcon } from "../hotbar/hotbar-icon"; import { RenderDelay } from "../render-delay/render-delay"; +import { Icon } from "../icon"; +import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item"; export const previousActiveTab = createAppStorage("catalog-previous-active-tab", browseCatalogTab); @@ -129,6 +131,10 @@ export class Catalog extends React.Component { HotbarStore.getInstance().addToHotbar(item.entity); } + removeFromHotbar(item: CatalogEntityItem): void { + HotbarStore.getInstance().removeFromHotbar(item.getId()); + } + onDetails = (item: CatalogEntityItem) => { if (this.catalogEntityStore.selectedItemId) { this.catalogEntityStore.selectedItemId = null; @@ -194,13 +200,34 @@ export class Catalog extends React.Component { )) } - this.addToHotbar(item)}> - Pin to Hotbar - + ); }; + renderName(item: CatalogEntityItem) { + const isItemInHotbar = HotbarStore.getInstance().isAddedToActive(item.entity); + + return ( +
+ {item.name} + isItemInHotbar ? this.removeFromHotbar(item) : this.addToHotbar(item))} + /> +
+ ); + } + renderIcon(item: CatalogEntityItem) { return ( @@ -231,7 +258,7 @@ export class Catalog extends React.Component { renderHeaderTitle={activeCategory?.metadata.name || "Browse All"} isSelectable={false} isConfigurable={true} - className="CatalogItemList" + className={styles.list} store={this.catalogEntityStore} sortingCallbacks={{ [sortBy.name]: item => item.name, @@ -255,7 +282,7 @@ export class Catalog extends React.Component { })} renderTableContents={item => [ this.renderIcon(item), - item.name, + this.renderName(item), !activeCategory && item.kind, item.source, item.getLabelBadges(), diff --git a/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx b/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx new file mode 100644 index 0000000000..7d679e4d81 --- /dev/null +++ b/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import React, { ReactNode, useState } from "react"; + +import { HotbarStore } from "../../../common/hotbar-store"; +import { MenuItem } from "../menu"; + +import type { CatalogEntity } from "../../api/catalog-entity"; + +export function HotbarToggleMenuItem(props: { entity: CatalogEntity, addContent: ReactNode, removeContent: ReactNode }) { + const store = HotbarStore.getInstance(false); + const add = () => store.addToHotbar(props.entity); + const remove = () => store.removeFromHotbar(props.entity.getId()); + const [itemInHotbar, setItemInHotbar] = useState(store.isAddedToActive(props.entity)); + + return ( + { + itemInHotbar ? remove() : add(); + setItemInHotbar(!itemInHotbar); + }}> + {itemInHotbar ? props.removeContent : props.addContent } + + ); +} diff --git a/src/renderer/components/hotbar/hotbar-entity-icon.tsx b/src/renderer/components/hotbar/hotbar-entity-icon.tsx index 05ce960a23..24405da556 100644 --- a/src/renderer/components/hotbar/hotbar-entity-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-entity-icon.tsx @@ -30,7 +30,6 @@ import { navigate } from "../../navigation"; import { cssNames, IClassName } from "../../utils"; import { Icon } from "../icon"; import { HotbarIcon } from "./hotbar-icon"; -import { HotbarStore } from "../../../common/hotbar-store"; import { LensKubernetesClusterStatus } from "../../../common/catalog-entities/kubernetes-cluster"; interface Props extends DOMAttributes { @@ -88,10 +87,6 @@ export class HotbarEntityIcon extends React.Component { return catalogEntityRegistry.activeEntity?.metadata?.uid == item.getId(); } - isPersisted(entity: CatalogEntity) { - return HotbarStore.getInstance().getActive().items.find((item) => item?.entity?.uid === entity.metadata.uid) !== undefined; - } - render() { if (!this.contextMenu) { return null; @@ -107,21 +102,13 @@ export class HotbarEntityIcon extends React.Component { disabled: !entity }); - const isPersisted = this.isPersisted(entity); const onOpen = async () => { const menuItems: CatalogEntityContextMenu[] = []; - if (!isPersisted) { - menuItems.unshift({ - title: "Pin to Hotbar", - onClick: () => add(entity, index) - }); - } else { - menuItems.unshift({ - title: "Unpin from Hotbar", - onClick: () => remove(entity.metadata.uid) - }); - } + menuItems.unshift({ + title: "Remove from Hotbar", + onClick: () => remove(entity.metadata.uid) + }); this.contextMenu.menuItems = menuItems; diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index 9055279437..571b978a79 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -138,7 +138,7 @@ export class HotbarMenu extends React.Component { tooltip={`${item.entity.name} (${item.entity.source})`} menuItems={[ { - title: "Unpin from Hotbar", + title: "Remove from Hotbar", onClick: () => this.removeItem(item.entity.uid) } ]} diff --git a/src/renderer/components/icon/unpin.svg b/src/renderer/components/icon/unpin.svg new file mode 100644 index 0000000000..f685eb5e92 --- /dev/null +++ b/src/renderer/components/icon/unpin.svg @@ -0,0 +1,6 @@ + + + + +