Merge branch 'feat/auto-mobile-boards' into feat/homarr-assistant
This commit is contained in:
36 files changed
+1421
-642
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { and, like } from "@homarr/db";
|
||||
import { getImageMatchRank, normalizeImageName } from "@homarr/common";
|
||||
import { icons } from "@homarr/db/schema";
|
||||
import { iconsFindSchema } from "@homarr/validation/icons";
|
||||
|
||||
@@ -15,43 +15,44 @@ export const iconsRouter = createTRPCRouter({
|
||||
})
|
||||
.input(iconsFindSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const term = input.searchText?.toLowerCase().trim() ?? "";
|
||||
const keywords = (input.searchText ?? "").split(" ").filter((keyword) => keyword.length > 0);
|
||||
|
||||
let whereCondition = undefined;
|
||||
if (term.length > 0) {
|
||||
whereCondition = and(...keywords.map((keyword) => like(icons.name, `%${keyword}%`)));
|
||||
}
|
||||
|
||||
const term = normalizeImageName(input.searchText ?? "");
|
||||
const [repositories, countIcons] = await Promise.all([
|
||||
ctx.db.query.iconRepositories.findMany({
|
||||
orderBy: (table, { asc, sql }) => [sql`CASE WHEN ${table.slug} = 'local' THEN 0 ELSE 1 END`, asc(table.slug)],
|
||||
with: {
|
||||
icons: {
|
||||
columns: { id: true, name: true, url: true },
|
||||
where: whereCondition,
|
||||
orderBy: (table, { asc, sql }) => {
|
||||
const svgFirst = sql`CASE WHEN ${table.name} LIKE '%.svg' THEN 0 ELSE 1 END`;
|
||||
const nameAsc = asc(table.name);
|
||||
if (term.length === 0) {
|
||||
return [svgFirst, nameAsc];
|
||||
}
|
||||
const tier = sql`CASE
|
||||
WHEN LOWER(${table.name}) = ${term} THEN 0
|
||||
WHEN LOWER(${table.name}) LIKE ${term + "%"} THEN 1
|
||||
WHEN LOWER(${table.name}) LIKE ${`%${term}%`} THEN 2
|
||||
ELSE 3
|
||||
END`;
|
||||
return [tier, svgFirst, nameAsc];
|
||||
},
|
||||
limit: input.limitPerGroup,
|
||||
orderBy: (table, { asc, sql }) => [
|
||||
sql`CASE WHEN ${table.name} LIKE '%.svg' THEN 0 ELSE 1 END`,
|
||||
asc(table.name),
|
||||
],
|
||||
limit: term.length === 0 ? input.limitPerGroup : undefined,
|
||||
},
|
||||
},
|
||||
}),
|
||||
ctx.db.$count(icons),
|
||||
]);
|
||||
|
||||
const matchedRepositories = repositories.map((repository) => ({
|
||||
...repository,
|
||||
icons: repository.icons
|
||||
.flatMap((icon) => {
|
||||
const rank = getImageMatchRank(term, icon.name || icon.url);
|
||||
return rank === null ? [] : [{ icon, rank }];
|
||||
})
|
||||
.toSorted((a, b) =>
|
||||
a.rank !== b.rank
|
||||
? a.rank - b.rank
|
||||
: Number(b.icon.name.toLowerCase().endsWith(".svg")) -
|
||||
Number(a.icon.name.toLowerCase().endsWith(".svg")) || a.icon.name.localeCompare(b.icon.name),
|
||||
)
|
||||
.slice(0, input.limitPerGroup)
|
||||
.map(({ icon }) => icon),
|
||||
}));
|
||||
|
||||
return {
|
||||
icons: term.length > 0 ? repositories.filter((repository) => repository.icons.length > 0) : repositories,
|
||||
icons:
|
||||
term.length === 0 ? repositories : matchedRepositories.filter((repository) => repository.icons.length > 0),
|
||||
countIcons,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -74,4 +74,40 @@ describe("findIcons", () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test.each(["homeassistant", "home_assistant", "home assistant.png"])(
|
||||
"matches normalized search %s before applying the group limit",
|
||||
async (searchText) => {
|
||||
const db = createDb();
|
||||
const caller = iconsRouter.createCaller({ db, deviceType: undefined, session: null });
|
||||
|
||||
await db.insert(iconRepositories).values({ id: "repository", slug: "dashboard-icons" });
|
||||
await db.insert(icons).values([
|
||||
{
|
||||
id: "irrelevant",
|
||||
name: "000-unrelated.svg",
|
||||
url: "https://example.com/000-unrelated.svg",
|
||||
checksum: "irrelevant",
|
||||
iconRepositoryId: "repository",
|
||||
},
|
||||
{
|
||||
id: "home-assistant",
|
||||
name: "home-assistant.svg",
|
||||
url: "https://example.com/home-assistant.svg",
|
||||
checksum: "home-assistant",
|
||||
iconRepositoryId: "repository",
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await caller.findIcons({ searchText, limitPerGroup: 1 });
|
||||
|
||||
expect(result.icons[0]?.icons).toEqual([
|
||||
{
|
||||
id: "home-assistant",
|
||||
name: "home-assistant.svg",
|
||||
url: "https://example.com/home-assistant.svg",
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -113,17 +113,22 @@ async function setup(
|
||||
describe("Custom JSX v2 board router", () => {
|
||||
beforeEach(() => mocks.executeRequest.mockClear());
|
||||
|
||||
test("allows public queries only through the definition placed on an accessible board", async () => {
|
||||
test("loads placed widgets and public queries for anonymous visitors on public boards", async () => {
|
||||
const { db, itemId } = await setup();
|
||||
const caller = customApiRouter.createCaller({ db, deviceType: undefined, session: null });
|
||||
|
||||
await expect(caller.getData({ itemId })).resolves.toMatchObject({
|
||||
template: definition.template,
|
||||
data: { "load-status": { value: 42 } },
|
||||
status: { "load-status": { ok: true } },
|
||||
});
|
||||
await expect(caller.queryRequest({ itemId, requestId: "status", params: { name: "homarr" } })).resolves.toEqual(
|
||||
expect.objectContaining({ ok: true, data: { value: 42 } }),
|
||||
);
|
||||
await expect(caller.queryRequest({ itemId, requestId: "missing", params: {} })).rejects.toMatchObject({
|
||||
code: "NOT_FOUND",
|
||||
});
|
||||
expect(mocks.executeRequest).toHaveBeenCalledOnce();
|
||||
expect(mocks.executeRequest).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("uses current defaults when an updated option no longer accepts the stored value", async () => {
|
||||
@@ -150,6 +155,7 @@ describe("Custom JSX v2 board router", () => {
|
||||
await expect(
|
||||
anonymous.queryRequest({ itemId: privateSetup.itemId, requestId: "status", params: { name: "homarr" } }),
|
||||
).rejects.toMatchObject({ code: "NOT_FOUND" });
|
||||
await expect(anonymous.getData({ itemId: privateSetup.itemId })).rejects.toMatchObject({ code: "NOT_FOUND" });
|
||||
|
||||
const enabledSetup = await setup();
|
||||
await enabledSetup.db
|
||||
|
||||
Reference in new issue
Block a user