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}
+
`
)}
@@ -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,
};