Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/common/string/filter/sequence-matching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import { fuzzyScore } from "./filter";
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/

export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
export const fuzzySequentialMatch = (
filter: string,
item: ScorableTextItem
Comment thread
bramkragten marked this conversation as resolved.
) => {
let topScore = Number.NEGATIVE_INFINITY;

for (const word of words) {
for (const word of item.words) {
const scores = fuzzyScore(
filter,
filter.toLowerCase(),
Expand Down Expand Up @@ -51,8 +54,7 @@ export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {

export interface ScorableTextItem {
score?: number;
filterText: string;
altText?: string;
words: string[];
Comment thread
bramkragten marked this conversation as resolved.
Outdated
}

type FuzzyFilterSort = <T extends ScorableTextItem>(
Expand All @@ -63,9 +65,7 @@ type FuzzyFilterSort = <T extends ScorableTextItem>(
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
return items
.map((item) => {
item.score = item.altText
? fuzzySequentialMatch(filter, item.filterText, item.altText)
: fuzzySequentialMatch(filter, item.filterText);
item.score = fuzzySequentialMatch(filter, item);
Comment thread
bramkragten marked this conversation as resolved.
return item;
})
.filter((item) => item.score !== undefined)
Expand Down
129 changes: 72 additions & 57 deletions src/dialogs/quick-bar/ha-quick-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,18 @@ interface CommandItem extends QuickBarItem {
}

interface EntityItem extends QuickBarItem {
altText: string;
icon?: string;
}

const isCommandItem = (item: EntityItem | CommandItem): item is CommandItem => {
const isCommandItem = (item: QuickBarItem): item is CommandItem => {
return (item as CommandItem).categoryKey !== undefined;
};

const isEntityItem = (item: QuickBarItem): item is EntityItem => {
return !isCommandItem(item);
};

interface QuickBarNavigationItem extends CommandItem {
path: string;
}
Expand Down Expand Up @@ -228,9 +233,15 @@ export class QuickBar extends LitElement {
}

private _renderItem(item: QuickBarItem, index?: number) {
return isCommandItem(item)
? this._renderCommandItem(item, index)
: this._renderEntityItem(item, index);
if (isCommandItem(item)) {
return this._renderCommandItem(item, index);
}

if (isEntityItem(item)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just... we check if it is not a command item, we already know that, because we checked that above...?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_renderEntityItem(item) expects an EntityItem, but item is a QuickBarItem here. This would be okay except that the EntityItem interface now has the altText property.

So I'm no longer allowed to blindly send a QuickBarItem to the _renderEntityItem method due to mismatching types.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as EntityItem ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could do, but I try to avoid X as Y as it weakens type safety.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but I rather do that than to add unneeded runtime checks 😄

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okie doke

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

return this._renderEntityItem(item, index);
}

return html``;
}

private _renderEntityItem(item: EntityItem, index?: number) {
Expand Down Expand Up @@ -289,13 +300,6 @@ export class QuickBar extends LitElement {
</span>

<span class="command-text">${item.primaryText}</span>
${item.altText
? html`
<span slot="secondary" class="item-text secondary"
>${item.altText}</span
>
`
: null}
</mwc-list-item>
`;
}
Expand Down Expand Up @@ -389,17 +393,20 @@ export class QuickBar extends LitElement {
}
}

private _generateEntityItems(): QuickBarItem[] {
private _generateEntityItems(): EntityItem[] {
return Object.keys(this.hass.states)
.map((entityId) => {
const primaryText = computeStateName(this.hass.states[entityId]);
return {
primaryText,
filterText: primaryText,
const entityItem = {
primaryText: computeStateName(this.hass.states[entityId]),
altText: entityId,
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
action: () => fireEvent(this, "hass-more-info", { entityId }),
};

return {
...entityItem,
words: [entityItem.primaryText, entityItem.altText],
};
})
.sort((a, b) =>
compare(a.primaryText.toLowerCase(), b.primaryText.toLowerCase())
Expand All @@ -412,32 +419,35 @@ export class QuickBar extends LitElement {
...this._generateServerControlCommands(),
...this._generateNavigationCommands(),
].sort((a, b) =>
compare(a.filterText.toLowerCase(), b.filterText.toLowerCase())
compare(a.words.join(" ").toLowerCase(), b.words.join(" ").toLowerCase())
);
}

private _generateReloadCommands(): CommandItem[] {
const reloadableDomains = componentsWithService(this.hass, "reload").sort();

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)
);
const commandItem = {
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)
),
action: () => this.hass.callService(domain, "reload"),
iconPath: mdiReload,
categoryText: this.hass.localize(
`ui.dialogs.quick-bar.commands.types.reload`
),
};

return {
primaryText,
filterText: `${categoryText} ${primaryText}`,
action: () => this.hass.callService(domain, "reload"),
...commandItem,
categoryKey: "reload",
iconPath: mdiReload,
categoryText,
words: [`${commandItem.categoryText} ${commandItem.primaryText}`],
Comment thread
bramkragten marked this conversation as resolved.
Outdated
};
});
}
Expand All @@ -446,26 +456,28 @@ export class QuickBar extends LitElement {
const serverActions = ["restart", "stop"];

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}`
)
);
const categoryKey: CommandItem["categoryKey"] = "server_control";

const item = {
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}`
)
),
iconPath: mdiServerNetwork,
categoryText: this.hass.localize(
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
),
categoryKey,
action: () => this.hass.callService("homeassistant", action),
};

return this._generateConfirmationCommand(
{
primaryText,
filterText: `${categoryText} ${primaryText}`,
categoryKey,
iconPath: mdiServerNetwork,
categoryText,
action: () => this.hass.callService("homeassistant", action),
...item,
words: [`${item.categoryText} ${item.primaryText}`],
},
this.hass.localize("ui.dialogs.generic.ok")
);
Expand Down Expand Up @@ -550,19 +562,22 @@ export class QuickBar extends LitElement {
items: BaseNavigationCommand[]
): CommandItem[] {
return items.map((item) => {
const categoryKey = "navigation";
const categoryText = this.hass.localize(
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
);
const categoryKey: CommandItem["categoryKey"] = "navigation";

return {
const navItem = {
...item,
categoryKey,
iconPath: mdiEarth,
categoryText,
filterText: `${categoryText} ${item.primaryText}`,
categoryText: this.hass.localize(
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
),
action: () => navigate(this, item.path),
};

return {
...navItem,
words: [`${navItem.categoryText} ${navItem.primaryText}`],
categoryKey,
};
});
}

Expand Down
34 changes: 12 additions & 22 deletions test-mocha/common/string/sequence_matching.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { assert } from "chai";
import {
fuzzyFilterSort,
fuzzySequentialMatch,
ScorableTextItem,
} from "../../../src/common/string/filter/sequence-matching";

describe("fuzzySequentialMatch", () => {
const entity = { entity_id: "automation.ticker", friendly_name: "Stocks" };
const item: ScorableTextItem = {
words: ["automation.ticker", "Stocks"],
};

const createExpectation: (
pattern,
Expand Down Expand Up @@ -53,25 +56,17 @@ describe("fuzzySequentialMatch", () => {
"stox",
];

describe(`Entity '${entity.entity_id}'`, () => {
describe(`Entity '${item.words[0]}'`, () => {
for (const expectation of shouldMatchEntity) {
it(`matches '${expectation.pattern}' with return of '${expectation.expected}'`, () => {
const res = fuzzySequentialMatch(
expectation.pattern,
entity.entity_id,
entity.friendly_name
);
const res = fuzzySequentialMatch(expectation.pattern, item);
assert.equal(res, expectation.expected);
});
}

for (const badFilter of shouldNotMatchEntity) {
it(`fails to match with '${badFilter}'`, () => {
const res = fuzzySequentialMatch(
badFilter,
entity.entity_id,
entity.friendly_name
);
const res = fuzzySequentialMatch(badFilter, item);
assert.equal(res, undefined);
});
}
Expand All @@ -81,28 +76,23 @@ describe("fuzzySequentialMatch", () => {
describe("fuzzyFilterSort", () => {
const filter = "ticker";
const automationTicker = {
filterText: "automation.ticker",
altText: "Stocks",
words: ["automation.ticker", "Stocks"],
score: 0,
};
const ticker = {
filterText: "ticker",
altText: "Just ticker",
words: ["ticker", "Just ticker"],
score: 0,
};
const sensorTicker = {
filterText: "sensor.ticker",
altText: "Stocks up",
words: ["sensor.ticker", "Stocks up"],
score: 0,
};
const timerCheckRouter = {
filterText: "automation.check_router",
altText: "Timer Check Router",
words: ["automation.check_router", "Timer Check Router"],
score: 0,
};
const badMatch = {
filterText: "light.chandelier",
altText: "Chandelier",
words: ["light.chandelier", "Chandelier"],
score: 0,
};
const itemsBeforeFilter = [
Expand Down