From e033122baf7390ad516b3b3d96af258431318061 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Mon, 24 May 2021 13:48:45 +0300 Subject: [PATCH] Add icon column to catalog list (#2852) * add icon column to catalog list Signed-off-by: Jari Kolehmainen * Create generic Avatar component Signed-off-by: Alex Andreev Co-authored-by: Alex Andreev --- src/renderer/components/+catalog/catalog.scss | 16 ++ src/renderer/components/+catalog/catalog.tsx | 21 +++ src/renderer/components/avatar/avatar.tsx | 92 ++++++++++ .../components/hotbar/hotbar-icon.scss | 173 +++++++++--------- .../components/hotbar/hotbar-icon.tsx | 56 +----- 5 files changed, 223 insertions(+), 135 deletions(-) create mode 100644 src/renderer/components/avatar/avatar.tsx diff --git a/src/renderer/components/+catalog/catalog.scss b/src/renderer/components/+catalog/catalog.scss index 5f8131dfaa..e76606567b 100644 --- a/src/renderer/components/+catalog/catalog.scss +++ b/src/renderer/components/+catalog/catalog.scss @@ -69,7 +69,18 @@ } } + .TableCell.icon { + max-width: 40px; + display: flex; + align-items: center; + } + + .TableCell.source { + max-width: 100px; + } + .TableCell.status { + max-width: 100px; &.connected { color: var(--colorSuccess); } @@ -97,4 +108,9 @@ } } } + + .catalogIcon { + font-size: 10px; + -webkit-font-smoothing: auto; + } } diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 5211e034c2..7f5d10b015 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -40,6 +40,7 @@ import { CatalogAddButton } from "./catalog-add-button"; import type { RouteComponentProps } from "react-router"; import type { ICatalogViewRouteParam } from "./catalog.route"; import { Notifications } from "../notifications"; +import { Avatar } from "../avatar/avatar"; enum sortBy { name = "name", @@ -174,6 +175,24 @@ export class Catalog extends React.Component { ); } + renderIcon(item: CatalogEntityItem) { + const category = catalogCategoryRegistry.getCategoryForEntity(item.entity); + + if (!category) { + return null; + } + + return ( + + ); + } + render() { if (!this.catalogEntityStore) { return null; @@ -202,12 +221,14 @@ export class Catalog extends React.Component { (entity: CatalogEntityItem) => entity.searchFields, ]} renderTableHeader={[ + { title: "", className: "icon" }, { title: "Name", className: "name", sortBy: sortBy.name }, { title: "Source", className: "source", sortBy: sortBy.source }, { title: "Labels", className: "labels" }, { title: "Status", className: "status", sortBy: sortBy.status }, ]} renderTableContents={(item: CatalogEntityItem) => [ + this.renderIcon(item), item.name, item.source, item.labels.map((label) => ), diff --git a/src/renderer/components/avatar/avatar.tsx b/src/renderer/components/avatar/avatar.tsx new file mode 100644 index 0000000000..2cf78866f4 --- /dev/null +++ b/src/renderer/components/avatar/avatar.tsx @@ -0,0 +1,92 @@ +/** + * 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, { DOMAttributes } from "react"; +import randomColor from "randomcolor"; +import GraphemeSplitter from "grapheme-splitter"; +import { Avatar as MaterialAvatar, AvatarTypeMap } from "@material-ui/core"; +import { iter } from "../../utils"; + +interface Props extends DOMAttributes, Partial { + title: string; + colorHash?: string; + width?: number; + height?: number; + src?: string; + className?: string; +} + +function getNameParts(name: string): string[] { + const byWhitespace = name.split(/\s+/); + + if (byWhitespace.length > 1) { + return byWhitespace; + } + + const byDashes = name.split(/[-_]+/); + + if (byDashes.length > 1) { + return byDashes; + } + + return name.split(/@+/); +} + +function getIconString(title: string) { + if (!title) { + return "??"; + } + + const [rawFirst, rawSecond, rawThird] = getNameParts(title); + const splitter = new GraphemeSplitter(); + const first = splitter.iterateGraphemes(rawFirst); + const second = rawSecond ? splitter.iterateGraphemes(rawSecond): first; + const third = rawThird ? splitter.iterateGraphemes(rawThird) : iter.newEmpty(); + + return [ + ...iter.take(first, 1), + ...iter.take(second, 1), + ...iter.take(third, 1), + ].filter(Boolean).join(""); +} + +export function Avatar(props: Props) { + const { title, src, width = 32, height = 32, colorHash, ...settings } = props; + + const generateAvatarStyle = (): React.CSSProperties => { + return { + backgroundColor: randomColor({ seed: colorHash, luminosity: "dark" }), + width, + height, + textTransform: "uppercase" + }; + }; + + return ( + + {getIconString(title)} + + ); +} diff --git a/src/renderer/components/hotbar/hotbar-icon.scss b/src/renderer/components/hotbar/hotbar-icon.scss index 018faf9085..d573e86eb5 100644 --- a/src/renderer/components/hotbar/hotbar-icon.scss +++ b/src/renderer/components/hotbar/hotbar-icon.scss @@ -20,92 +20,6 @@ */ .HotbarMenu { - .HotbarIcon { - --size: 37px; - - border-radius: 6px; - user-select: none; - cursor: pointer; - transition: none; - text-shadow: 0 0 4px #0000008f; - position: relative; - - div.MuiAvatar-colorDefault { - font-weight:500; - text-transform: uppercase; - border-radius: 6px; - } - - &.disabled { - opacity: 0.4; - cursor: default; - filter: grayscale(0.7); - - &:hover { - &:not(.active) { - box-shadow: none; - } - } - } - - &.isDragging { - box-shadow: none; - } - - div.MuiAvatar-root { - &.active { - box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent); - transition: all 0s 0.8s; - } - - &:hover { - &:not(.active) { - box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff30; - } - } - } - - .led { - position: absolute; - left: 3px; - top: 3px; - background-color: var(--layoutBackground); - border: 1px solid var(--clusterMenuBackground); - border-radius: 50%; - padding: 0px; - width: 8px; - height: 8px; - - &.online { - background-color: var(--primary); - box-shadow: 0 0 5px var(--clusterMenuBackground), 0 0 5px var(--primary); - } - } - - .badge { - position: absolute; - right: -2px; - bottom: -3px; - margin: -8px; - font-size: var(--font-size-small); - background: var(--clusterMenuBackground); - color: var(--textColorAccent); - padding: 0px; - border-radius: 50%; - border: 2px solid var(--clusterMenuBackground); - width: 15px; - height: 15px; - - svg { - width: 13px; - } - } - - img { - width: var(--size); - height: var(--size); - } - } .HotbarIconMenu { left: 30px; @@ -116,3 +30,90 @@ } } } + +.HotbarIcon { + border-radius: 6px; + user-select: none; + cursor: pointer; + transition: none; + text-shadow: 0 0 4px #0000008f; + position: relative; + + div.MuiAvatar-colorDefault { + font-weight:500; + text-transform: uppercase; + border-radius: 6px; + } + + &.disabled { + opacity: 0.4; + cursor: default; + filter: grayscale(0.7); + + &:hover { + &:not(.active) { + box-shadow: none; + } + } + } + + &.isDragging { + box-shadow: none; + } + + div.MuiAvatar-root { + width: var(--size); + height: var(--size); + + &.active { + box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent); + } + + &:hover { + &:not(.active) { + box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff30; + } + } + } + + .led { + position: absolute; + left: 3px; + top: 3px; + background-color: var(--layoutBackground); + border: 1px solid var(--clusterMenuBackground); + border-radius: 50%; + padding: 0px; + width: 8px; + height: 8px; + + &.online { + background-color: var(--primary); + box-shadow: 0 0 5px var(--clusterMenuBackground), 0 0 5px var(--primary); + } + } + + .badge { + position: absolute; + right: -2px; + bottom: -3px; + margin: -8px; + font-size: var(--font-size-small); + background: var(--clusterMenuBackground); + color: var(--textColorAccent); + padding: 0px; + border-radius: 50%; + border: 2px solid var(--clusterMenuBackground); + width: 15px; + height: 15px; + + svg { + width: 13px; + } + } + + img { + width: var(--size); + height: var(--size); + } +} diff --git a/src/renderer/components/hotbar/hotbar-icon.tsx b/src/renderer/components/hotbar/hotbar-icon.tsx index 7c567ff681..f2193bfb8b 100644 --- a/src/renderer/components/hotbar/hotbar-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-icon.tsx @@ -22,16 +22,14 @@ import "./hotbar-icon.scss"; import React, { DOMAttributes, useState } from "react"; -import { Avatar } from "@material-ui/core"; -import randomColor from "randomcolor"; -import GraphemeSplitter from "grapheme-splitter"; import type { CatalogEntityContextMenu } from "../../../common/catalog"; -import { cssNames, IClassName, iter } from "../../utils"; +import { cssNames, IClassName } from "../../utils"; import { ConfirmDialog } from "../confirm-dialog"; import { Menu, MenuItem } from "../menu"; import { MaterialTooltip } from "../+catalog/material-tooltip/material-tooltip"; import { observer } from "mobx-react"; +import { Avatar } from "../avatar/avatar"; interface Props extends DOMAttributes { uid: string; @@ -44,12 +42,6 @@ interface Props extends DOMAttributes { disabled?: boolean; } -function generateAvatarStyle(seed: string): React.CSSProperties { - return { - "backgroundColor": randomColor({ seed, luminosity: "dark" }) - }; -} - function onMenuItemClick(menuItem: CatalogEntityContextMenu) { if (menuItem.confirm) { ConfirmDialog.open({ @@ -67,22 +59,6 @@ function onMenuItemClick(menuItem: CatalogEntityContextMenu) { } } -function getNameParts(name: string): string[] { - const byWhitespace = name.split(/\s+/); - - if (byWhitespace.length > 1) { - return byWhitespace; - } - - const byDashes = name.split(/[-_]+/); - - if (byDashes.length > 1) { - return byDashes; - } - - return name.split(/@+/); -} - export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => { const { uid, title, active, className, source, disabled, onMenuOpen, children, ...rest } = props; const id = `hotbarIcon-${uid}`; @@ -92,36 +68,18 @@ export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => { setMenuOpen(!menuOpen); }; - const getIconString = () => { - if (!title) { - return "??"; - } - - const [rawFirst, rawSecond, rawThird] = getNameParts(title); - const splitter = new GraphemeSplitter(); - const first = splitter.iterateGraphemes(rawFirst); - const second = rawSecond ? splitter.iterateGraphemes(rawSecond): first; - const third = rawThird ? splitter.iterateGraphemes(rawThird) : iter.newEmpty(); - - return [ - ...iter.take(first, 1), - ...iter.take(second, 1), - ...iter.take(third, 1), - ].filter(Boolean).join(""); - }; - return (
- {getIconString()} - + width={40} + height={40} + /> {children}