diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts index 64aadb1597..8e6f9610e4 100644 --- a/hassio/src/dialogs/network/dialog-hassio-network.ts +++ b/hassio/src/dialogs/network/dialog-hassio-network.ts @@ -18,7 +18,6 @@ import { } from "lit-element"; import { cache } from "lit-html/directives/cache"; import { fireEvent } from "../../../../src/common/dom/fire_event"; -import "../../../../src/components/ha-chips"; import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-expansion-panel"; diff --git a/src/common/string/filter/sequence-matching.ts b/src/common/string/filter/sequence-matching.ts index dac81672a8..6e4a5bf2e5 100644 --- a/src/common/string/filter/sequence-matching.ts +++ b/src/common/string/filter/sequence-matching.ts @@ -42,7 +42,7 @@ export const fuzzySequentialMatch = (filter: string, ...words: string[]) => { export interface ScorableTextItem { score?: number; - text: string; + filterText: string; altText?: string; } @@ -55,8 +55,8 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => { return items .map((item) => { item.score = item.altText - ? fuzzySequentialMatch(filter, item.text, item.altText) - : fuzzySequentialMatch(filter, item.text); + ? fuzzySequentialMatch(filter, item.filterText, item.altText) + : fuzzySequentialMatch(filter, item.filterText); return item; }) .filter((item) => item.score !== undefined && item.score > 0) diff --git a/src/components/ha-chips.ts b/src/components/ha-chip-set.ts similarity index 54% rename from src/components/ha-chips.ts rename to src/components/ha-chip-set.ts index 8a1363cf8f..353493941b 100644 --- a/src/components/ha-chips.ts +++ b/src/components/ha-chip-set.ts @@ -1,6 +1,5 @@ // @ts-ignore import chipStyles from "@material/chips/dist/mdc.chips.min.css"; -import { ripple } from "@material/mwc-ripple/ripple-directive"; import { css, CSSResult, @@ -12,6 +11,7 @@ import { unsafeCSS, } from "lit-element"; import { fireEvent } from "../common/dom/fire_event"; +import "./ha-chip"; declare global { // for fire event @@ -20,8 +20,8 @@ declare global { } } -@customElement("ha-chips") -export class HaChips extends LitElement { +@customElement("ha-chip-set") +export class HaChipSet extends LitElement { @property() public items = []; protected render(): TemplateResult { @@ -33,18 +33,9 @@ export class HaChips extends LitElement { ${this.items.map( (item, idx) => html` -
-
- - - ${item} - - -
+ + ${item} + ` )} @@ -60,16 +51,12 @@ export class HaChips extends LitElement { static get styles(): CSSResult { return css` ${unsafeCSS(chipStyles)} - .mdc-chip { - background-color: rgba(var(--rgb-primary-text-color), 0.15); - color: var(--primary-text-color); - } `; } } declare global { interface HTMLElementTagNameMap { - "ha-chips": HaChips; + "ha-chip-set": HaChipSet; } } diff --git a/src/components/ha-chip.ts b/src/components/ha-chip.ts new file mode 100644 index 0000000000..50e04dee7a --- /dev/null +++ b/src/components/ha-chip.ts @@ -0,0 +1,75 @@ +// @ts-ignore +import chipStyles from "@material/chips/dist/mdc.chips.min.css"; +import { ripple } from "@material/mwc-ripple/ripple-directive"; +import "./ha-icon"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, + unsafeCSS, +} from "lit-element"; + +declare global { + // for fire event + interface HASSDomEvents { + "chip-clicked": { index: string }; + } +} + +@customElement("ha-chip") +export class HaChip extends LitElement { + @property() public index = 0; + + @property({type: Boolean}) public hasIcon = false; + + protected render(): TemplateResult { + return html` +
+ ${this.hasIcon + ? html`
+ +
` + : null} +
+ + + + + +
+ `; + } + + static get styles(): CSSResult { + return css` + ${unsafeCSS(chipStyles)} + .mdc-chip { + margin: 4px; + background-color: var( + --ha-chip-background-color, + rgba(var(--rgb-primary-text-color), 0.15) + ); + color: var(--ha-chip-text-color, var(--primary-text-color)); + } + + .mdc-chip:hover { + color: var(--ha-chip-text-color, var(--primary-text-color)); + } + + .mdc-chip__icon--leading { + --mdc-icon-size: 20px; + color: var(--ha-chip-icon-color, var(--ha-chip-text-color)); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-chip": HaChip; + } +} diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 9789157691..c8b04598f7 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -3,7 +3,7 @@ import type { List } from "@material/mwc-list/mwc-list"; import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import type { ListItem } from "@material/mwc-list/mwc-list-item"; -import { mdiConsoleLine } from "@mdi/js"; +import { mdiConsoleLine, mdiEarth, mdiReload, mdiServerNetwork } from "@mdi/js"; import { css, customElement, @@ -36,7 +36,7 @@ import "../../components/ha-circular-progress"; import "../../components/ha-dialog"; import "../../components/ha-header-bar"; import { domainToName } from "../../data/integration"; -import { getPanelIcon, getPanelNameTranslationKey } from "../../data/panel"; +import { getPanelNameTranslationKey } from "../../data/panel"; import { PageNavigation } from "../../layouts/hass-tabs-subpage"; import { configSections } from "../../panels/config/ha-panel-config"; import { haStyleDialog } from "../../resources/styles"; @@ -46,31 +46,44 @@ import { showConfirmationDialog, } from "../generic/show-dialog-box"; import { QuickBarParams } from "./show-dialog-quick-bar"; - -const DEFAULT_NAVIGATION_ICON = "hass:arrow-right-circle"; -const DEFAULT_SERVER_ICON = "hass:server"; +import "../../components/ha-chip"; interface QuickBarItem extends ScorableTextItem { - icon?: string; + primaryText: string; iconPath?: string; action(data?: any): void; } -interface QuickBarNavigationItem extends QuickBarItem { +interface CommandItem extends QuickBarItem { + categoryKey: "reload" | "navigation" | "server_control"; + categoryText: string; +} + +interface EntityItem extends QuickBarItem { + icon?: string; +} + +const isCommandItem = (item: EntityItem | CommandItem): item is CommandItem => { + return (item as CommandItem).categoryKey !== undefined; +}; + +interface QuickBarNavigationItem extends CommandItem { path: string; } -interface NavigationInfo extends PageNavigation { - text: string; -} +type NavigationInfo = PageNavigation & Pick; +type BaseNavigationCommand = Pick< + QuickBarNavigationItem, + "primaryText" | "path" +>; @customElement("ha-quick-bar") export class QuickBar extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @internalProperty() private _commandItems?: QuickBarItem[]; + @internalProperty() private _commandItems?: CommandItem[]; - @internalProperty() private _entityItems?: QuickBarItem[]; + @internalProperty() private _entityItems?: EntityItem[]; @internalProperty() private _items?: QuickBarItem[] = []; @@ -201,6 +214,12 @@ export class QuickBar extends LitElement { } private _renderItem(item: QuickBarItem, index?: number) { + return isCommandItem(item) + ? this._renderCommandItem(item, index) + : this._renderEntityItem(item, index); + } + + private _renderEntityItem(item: EntityItem, index?: number) { return html` ` - : html``} - ${item.text} + : html``} + ${item.primaryText} + ${item.altText + ? html` + ${item.altText} + ` + : null} + + `; + } + + private _renderCommandItem(item: CommandItem, index?: number) { + return html` + + + + ${item.iconPath + ? html`` + : ""} + ${item.categoryText} + + + ${item.primaryText} ${item.altText ? html` ({ - text: computeStateName(this.hass.states[entityId]), - altText: entityId, - icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]), - action: () => fireEvent(this, "hass-more-info", { entityId }), - })) - .sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase())); + .map((entityId) => { + const primaryText = computeStateName(this.hass.states[entityId]); + return { + primaryText, + filterText: primaryText, + altText: entityId, + icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]), + action: () => fireEvent(this, "hass-more-info", { entityId }), + }; + }) + .sort((a, b) => + compare(a.primaryText.toLowerCase(), b.primaryText.toLowerCase()) + ); } - private _generateCommandItems(): QuickBarItem[] { + private _generateCommandItems(): CommandItem[] { return [ ...this._generateReloadCommands(), ...this._generateServerControlCommands(), ...this._generateNavigationCommands(), - ].sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase())); + ].sort((a, b) => + compare(a.filterText.toLowerCase(), b.filterText.toLowerCase()) + ); } - private _generateReloadCommands(): QuickBarItem[] { + private _generateReloadCommands(): CommandItem[] { const reloadableDomains = componentsWithService(this.hass, "reload").sort(); - return reloadableDomains.map((domain) => ({ - text: + return reloadableDomains.map((domain) => { + const categoryText = this.hass.localize( + `ui.dialogs.quick-bar.commands.types.reload` + ); + const primaryText = this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) || this.hass.localize( "ui.dialogs.quick-bar.commands.reload.reload", "domain", domainToName(this.hass.localize, domain) - ), - icon: domainIcon(domain), - action: () => this.hass.callService(domain, "reload"), - })); + ); + + return { + primaryText, + filterText: `${categoryText} ${primaryText}`, + action: () => this.hass.callService(domain, "reload"), + categoryKey: "reload", + iconPath: mdiReload, + categoryText, + }; + }); } - private _generateServerControlCommands(): QuickBarItem[] { + private _generateServerControlCommands(): CommandItem[] { const serverActions = ["restart", "stop"]; - return serverActions.map((action) => - this._generateConfirmationCommand( + return serverActions.map((action) => { + const categoryKey = "server_control"; + const categoryText = this.hass.localize( + `ui.dialogs.quick-bar.commands.types.${categoryKey}` + ); + const primaryText = this.hass.localize( + "ui.dialogs.quick-bar.commands.server_control.perform_action", + "action", + this.hass.localize( + `ui.dialogs.quick-bar.commands.server_control.${action}` + ) + ); + + return this._generateConfirmationCommand( { - text: this.hass.localize( - "ui.dialogs.quick-bar.commands.server_control.perform_action", - "action", - this.hass.localize( - `ui.dialogs.quick-bar.commands.server_control.${action}` - ) - ), - icon: DEFAULT_SERVER_ICON, + primaryText, + filterText: `${categoryText} ${primaryText}`, + categoryKey, + iconPath: mdiServerNetwork, + categoryText, action: () => this.hass.callService("homeassistant", action), }, this.hass.localize("ui.dialogs.generic.ok") - ) - ); + ); + }); } - private _generateNavigationCommands(): QuickBarItem[] { + private _generateNavigationCommands(): CommandItem[] { const panelItems = this._generateNavigationPanelCommands(); const sectionItems = this._generateNavigationConfigSectionCommands(); - return this._withNavigationActions([...panelItems, ...sectionItems]); + return this._finalizeNavigationCommands([...panelItems, ...sectionItems]); } - private _generateNavigationPanelCommands(): Omit< - QuickBarNavigationItem, - "action" - >[] { + private _generateNavigationPanelCommands(): BaseNavigationCommand[] { return Object.keys(this.hass.panels) .filter((panelKey) => panelKey !== "_my_redirect") .map((panelKey) => { const panel = this.hass.panels[panelKey]; const translationKey = getPanelNameTranslationKey(panel); - const text = this.hass.localize( - "ui.dialogs.quick-bar.commands.navigation.navigate_to", - "panel", - this.hass.localize(translationKey) || panel.title || panel.url_path - ); + const primaryText = + this.hass.localize(translationKey) || panel.title || panel.url_path; return { - text, - icon: getPanelIcon(panel) || DEFAULT_NAVIGATION_ICON, + primaryText, path: `/${panel.url_path}`, }; }); } - private _generateNavigationConfigSectionCommands(): Partial< - QuickBarNavigationItem - >[] { + private _generateNavigationConfigSectionCommands(): BaseNavigationCommand[] { const items: NavigationInfo[] = []; for (const sectionKey of Object.keys(configSections)) { @@ -426,18 +503,12 @@ export class QuickBar extends LitElement { page: PageNavigation ): NavigationInfo | undefined { if (page.component) { - const shortCaption = this.hass.localize( + const caption = this.hass.localize( `ui.dialogs.quick-bar.commands.navigation.${page.component}` ); - if (page.translationKey && shortCaption) { - const caption = this.hass.localize( - "ui.dialogs.quick-bar.commands.navigation.navigate_to", - "panel", - shortCaption - ); - - return { ...page, text: caption }; + if (page.translationKey && caption) { + return { ...page, primaryText: caption }; } } @@ -445,9 +516,9 @@ export class QuickBar extends LitElement { } private _generateConfirmationCommand( - item: QuickBarItem, + item: CommandItem, confirmText: ConfirmationDialogParams["confirmText"] - ): QuickBarItem { + ): CommandItem { return { ...item, action: () => @@ -458,13 +529,24 @@ export class QuickBar extends LitElement { }; } - private _withNavigationActions(items) { - return items.map(({ text, icon, iconPath, path }) => ({ - text, - icon, - iconPath, - action: () => navigate(this, path), - })); + private _finalizeNavigationCommands( + items: BaseNavigationCommand[] + ): CommandItem[] { + return items.map((item) => { + const categoryKey = "navigation"; + const categoryText = this.hass.localize( + `ui.dialogs.quick-bar.commands.types.${categoryKey}` + ); + + return { + ...item, + categoryKey, + iconPath: mdiEarth, + categoryText, + filterText: `${categoryText} ${item.primaryText}`, + action: () => navigate(this, item.path), + }; + }); } private _toggleIfAlreadyOpened() { @@ -506,8 +588,8 @@ export class QuickBar extends LitElement { } } - ha-icon, - ha-svg-icon { + ha-icon.entity, + ha-svg-icon.entity { margin-left: 20px; } @@ -516,6 +598,29 @@ export class QuickBar extends LitElement { color: var(--primary-text-color); } + span.command-category { + font-weight: bold; + padding: 3px; + display: inline-flex; + border-radius: 6px; + color: black; + } + + .command-category.reload { + --ha-chip-background-color: #cddc39; + --ha-chip-text-color: black; + } + + .command-category.navigation { + --ha-chip-background-color: var(--light-primary-color); + --ha-chip-text-color: black; + } + + .command-category.server_control { + --ha-chip-background-color: var(--warning-color); + --ha-chip-text-color: black; + } + .uni-virtualizer-host { display: block; position: relative; diff --git a/src/panels/config/devices/device-detail/ha-device-automation-card.ts b/src/panels/config/devices/device-detail/ha-device-automation-card.ts index fabd999d65..7cb25de0c6 100644 --- a/src/panels/config/devices/device-detail/ha-device-automation-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-automation-card.ts @@ -7,7 +7,7 @@ import { TemplateResult, } from "lit-element"; import "../../../../components/ha-card"; -import "../../../../components/ha-chips"; +import "../../../../components/ha-chip-set"; import { showAutomationEditor } from "../../../../data/automation"; import { DeviceAction, @@ -65,13 +65,13 @@ export abstract class HaDeviceAutomationCard< ${this.hass.localize(this.headerKey)}
- this._localizeDeviceAutomation(this.hass, automation) )} > - +
`; } diff --git a/src/resources/styles.ts b/src/resources/styles.ts index 89f0463042..952193d6e9 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -92,6 +92,7 @@ export const derivedStyles = { "mdc-button-disabled-ink-color": "var(--disabled-text-color)", "mdc-button-outline-color": "var(--divider-color)", "mdc-dialog-scroll-divider-color": "var(--divider-color)", + "chip-background-color": "rgba(var(--rgb-primary-text-color), 0.15)", }; export const buttonLinkStyle = css` diff --git a/src/translations/en.json b/src/translations/en.json index bcd659530a..92651cccf7 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -546,12 +546,16 @@ "rpi_gpio": "[%key:ui::panel::config::server_control::section::reloading::rpi_gpio%]" }, "server_control": { - "perform_action": "{action} Server", + "perform_action": "{action} server", "restart": "[%key:ui::panel::config::server_control::section::server_management::restart%]", "stop": "[%key:ui::panel::config::server_control::section::server_management::stop%]" }, + "types": { + "reload": "Reload", + "navigation": "Navigate", + "server_control": "Server" + }, "navigation": { - "navigate_to": "Navigate to {panel}", "logs": "[%key:ui::panel::config::logs::caption%]", "automation": "[%key:ui::panel::config::automation::caption%]", "script": "[%key:ui::panel::config::script::caption%]", @@ -1107,37 +1111,37 @@ "reloading": { "heading": "YAML configuration reloading", "introduction": "Some parts of Home Assistant can reload without requiring a restart. Hitting reload will unload their current YAML configuration and load the new one.", - "reload": "Reload {domain}", - "core": "Reload location & customizations", - "group": "Reload groups, group entities, and group notify services", - "automation": "Reload automations", - "script": "Reload scripts", - "scene": "Reload scenes", - "person": "Reload people", - "zone": "Reload zones", - "input_boolean": "Reload input booleans", - "input_text": "Reload input texts", - "input_number": "Reload input numbers", - "input_datetime": "Reload input date times", - "input_select": "Reload input selects", - "template": "Reload template entities", - "universal": "Reload universal media player entities", - "rest": "Reload rest entities, and rest notify services", - "command_line": "Reload command line entities", - "filter": "Reload filter entities", - "statistics": "Reload statistics entities", - "generic": "Reload generic IP camera entities", - "generic_thermostat": "Reload generic thermostat entities", - "homekit": "Reload HomeKit", - "min_max": "Reload min/max entities", - "history_stats": "Reload history stats entities", - "trend": "Reload trend entities", - "ping": "Reload ping binary sensor entities", - "filesize": "Reload file size entities", - "telegram": "Reload telegram notify services", - "smtp": "Reload SMTP notify services", - "mqtt": "Reload manually configured MQTT entities", - "rpi_gpio": "Reload Raspberry Pi GPIO entities" + "reload": "{domain}", + "core": "Location & customizations", + "group": "Groups, group entities, and notify services", + "automation": "Automations", + "script": "Scripts", + "scene": "Scenes", + "person": "People", + "zone": "Zones", + "input_boolean": "Input booleans", + "input_text": "Input texts", + "input_number": "Input numbers", + "input_datetime": "Input date times", + "input_select": "Input selects", + "template": "Template entities", + "universal": "Universal media player entities", + "rest": "Rest entities and notify services", + "command_line": "Command line entities", + "filter": "Filter entities", + "statistics": "Statistics entities", + "generic": "Generic IP camera entities", + "generic_thermostat": "Generic thermostat entities", + "homekit": "HomeKit", + "min_max": "Min/max entities", + "history_stats": "History stats entities", + "trend": "Trend entities", + "ping": "Ping binary sensor entities", + "filesize": "File size entities", + "telegram": "Telegram notify services", + "smtp": "SMTP notify services", + "mqtt": "Manually configured MQTT entities", + "rpi_gpio": "Raspberry Pi GPIO entities" }, "server_management": { "heading": "Server management", @@ -3848,4 +3852,4 @@ } } } -} +} \ No newline at end of file diff --git a/test-mocha/common/string/sequence_matching.test.ts b/test-mocha/common/string/sequence_matching.test.ts index 8621a36274..48400606ea 100644 --- a/test-mocha/common/string/sequence_matching.test.ts +++ b/test-mocha/common/string/sequence_matching.test.ts @@ -80,10 +80,14 @@ describe("fuzzySequentialMatch", () => { describe("fuzzyFilterSort", () => { const filter = "ticker"; - const item1 = { text: "automation.ticker", altText: "Stocks", score: 0 }; - const item2 = { text: "sensor.ticker", altText: "Stocks up", score: 0 }; + const item1 = { + filterText: "automation.ticker", + altText: "Stocks", + score: 0, + }; + const item2 = { filterText: "sensor.ticker", altText: "Stocks up", score: 0 }; const item3 = { - text: "automation.check_router", + filterText: "automation.check_router", altText: "Timer Check Router", score: 0, };