feat: rebuild board grid experience
This commit is contained in:
1 parent
5dc4dbeaef
commit
0110a2ef63
122 files changed
+9582
-4063
No files matched your search
@@ -572,6 +572,35 @@ describe("getBoardByName should return board by name", () => {
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "view");
|
||||
});
|
||||
|
||||
it("normalizes legacy category behavior options in the response", async () => {
|
||||
const db = createDb();
|
||||
const caller = boardRouter.createCaller({ db, deviceType: undefined, session: defaultSession });
|
||||
const { boardId, sectionId } = await createFullBoardAsync(db, "default");
|
||||
await db
|
||||
.update(sections)
|
||||
.set({
|
||||
kind: "category",
|
||||
name: "Media",
|
||||
})
|
||||
.where(eq(sections.id, sectionId));
|
||||
|
||||
const result = await caller.getBoardByName({ name: "default" });
|
||||
const section = expectToBeDefined(result.sections[0]);
|
||||
|
||||
expect(result.id).toBe(boardId);
|
||||
expect(section.kind).toBe("category");
|
||||
if (section.kind === "category") {
|
||||
expect(section.collapsed).toBe(true);
|
||||
expect(section.options).toEqual({
|
||||
showLabel: true,
|
||||
collapsible: true,
|
||||
showOpenAll: true,
|
||||
railPlacement: "main",
|
||||
columnCount: 2,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw error when not present", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
@@ -882,6 +911,68 @@ describe("saveBoard should save full board", () => {
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "modify");
|
||||
},
|
||||
);
|
||||
it("should add and update category behavior options", async () => {
|
||||
const db = createDb();
|
||||
const caller = boardRouter.createCaller({ db, deviceType: undefined, session: defaultSession });
|
||||
const { boardId } = await createFullBoardAsync(db, "default");
|
||||
const sectionId = createId();
|
||||
|
||||
await caller.saveBoard({
|
||||
id: boardId,
|
||||
sections: [
|
||||
{
|
||||
id: sectionId,
|
||||
kind: "category",
|
||||
name: "Navigation",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
collapsed: false,
|
||||
options: {
|
||||
showLabel: true,
|
||||
collapsible: true,
|
||||
showOpenAll: true,
|
||||
railPlacement: "left",
|
||||
columnCount: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
items: [],
|
||||
});
|
||||
|
||||
await caller.saveBoard({
|
||||
id: boardId,
|
||||
sections: [
|
||||
{
|
||||
id: sectionId,
|
||||
kind: "category",
|
||||
name: "Navigation",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
collapsed: false,
|
||||
options: {
|
||||
showLabel: false,
|
||||
collapsible: false,
|
||||
showOpenAll: false,
|
||||
railPlacement: "right",
|
||||
columnCount: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
items: [],
|
||||
});
|
||||
|
||||
const section = await db.query.sections.findFirst({
|
||||
where: eq(sections.id, sectionId),
|
||||
});
|
||||
|
||||
expect(SuperJSON.parse(section?.options ?? "")).toEqual({
|
||||
showLabel: false,
|
||||
collapsible: false,
|
||||
showOpenAll: false,
|
||||
railPlacement: "right",
|
||||
columnCount: 4,
|
||||
});
|
||||
});
|
||||
it("should add item when present in input", async () => {
|
||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||
const db = createDb();
|
||||
@@ -1328,12 +1419,16 @@ const createExistingLayout = (id: string) => ({
|
||||
id,
|
||||
name: "Base",
|
||||
columnCount: 10,
|
||||
leftGutterColumnCount: 0,
|
||||
rightGutterColumnCount: 0,
|
||||
breakpoint: 0,
|
||||
});
|
||||
const createNewLayout = (columnCount: number) => ({
|
||||
id: createId(),
|
||||
name: "New layout",
|
||||
columnCount,
|
||||
leftGutterColumnCount: 0,
|
||||
rightGutterColumnCount: 0,
|
||||
breakpoint: 1400,
|
||||
});
|
||||
describe("saveLayouts should save layout changes", () => {
|
||||
@@ -1445,6 +1540,59 @@ describe("saveLayouts should save layout changes", () => {
|
||||
});
|
||||
await expectLayoutForDynamicSectionAsync(db, assignments.inRoot.f, layoutId, assignments.inDynamicSection);
|
||||
});
|
||||
test("should reserve gutter columns and return gutter contents to the canvas when disabled", async () => {
|
||||
const db = createDb();
|
||||
const caller = boardRouter.createCaller({ db, deviceType: undefined, session: defaultSession });
|
||||
const { boardId, layoutId, sectionId, itemId } = await createFullBoardAsync(db, "gutters");
|
||||
|
||||
await db
|
||||
.update(itemLayouts)
|
||||
.set({
|
||||
xOffset: 9,
|
||||
})
|
||||
.where(and(eq(itemLayouts.itemId, itemId), eq(itemLayouts.layoutId, layoutId)));
|
||||
|
||||
await caller.saveLayouts({
|
||||
id: boardId,
|
||||
layouts: [
|
||||
{
|
||||
...createExistingLayout(layoutId),
|
||||
leftGutterColumnCount: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const enabledLayout = expectToBeDefined(await db.query.layouts.findFirst({ where: eq(layouts.id, layoutId) }));
|
||||
expect(enabledLayout.leftGutterColumnCount).toBe(2);
|
||||
const leftRoot = expectToBeDefined(
|
||||
await db.query.sections.findFirst({
|
||||
where: and(eq(sections.boardId, boardId), eq(sections.kind, "empty"), eq(sections.xOffset, -1)),
|
||||
}),
|
||||
);
|
||||
const reflowedMainItem = expectToBeDefined(
|
||||
await db.query.itemLayouts.findFirst({
|
||||
where: and(eq(itemLayouts.itemId, itemId), eq(itemLayouts.layoutId, layoutId)),
|
||||
}),
|
||||
);
|
||||
expect(reflowedMainItem.xOffset + reflowedMainItem.width).toBeLessThanOrEqual(8);
|
||||
|
||||
const gutterItemId = await addItemAsync(db, {
|
||||
boardId,
|
||||
layoutId,
|
||||
sectionId: leftRoot.id,
|
||||
});
|
||||
await caller.saveLayouts({
|
||||
id: boardId,
|
||||
layouts: [createExistingLayout(layoutId)],
|
||||
});
|
||||
|
||||
const returnedItem = expectToBeDefined(
|
||||
await db.query.itemLayouts.findFirst({
|
||||
where: and(eq(itemLayouts.itemId, gutterItemId), eq(itemLayouts.layoutId, layoutId)),
|
||||
}),
|
||||
);
|
||||
expect(returnedItem.sectionId).toBe(sectionId);
|
||||
});
|
||||
test("should remove layout when not present in input", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { stringify } from "superjson";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { createId } from "@homarr/common";
|
||||
import { eq } from "@homarr/db";
|
||||
import { boards, sectionCollapseStates, sections, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { sectionRouter } from "../section/section-router";
|
||||
|
||||
const userId = createId();
|
||||
const session = {
|
||||
user: {
|
||||
id: userId,
|
||||
permissions: [],
|
||||
colorScheme: "light",
|
||||
},
|
||||
expires: new Date().toISOString(),
|
||||
} satisfies Session;
|
||||
|
||||
const createCallerAsync = async () => {
|
||||
const db = createDb();
|
||||
const boardId = createId();
|
||||
|
||||
await db.insert(users).values({ id: userId });
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: createId(),
|
||||
creatorId: userId,
|
||||
});
|
||||
|
||||
return {
|
||||
boardId,
|
||||
db,
|
||||
caller: sectionRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
describe("changeCollapsed", () => {
|
||||
it("preserves collapse support for legacy categories", async () => {
|
||||
const { boardId, caller, db } = await createCallerAsync();
|
||||
const sectionId = createId();
|
||||
await db.insert(sections).values({
|
||||
id: sectionId,
|
||||
boardId,
|
||||
kind: "category",
|
||||
name: "Media",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
});
|
||||
|
||||
await caller.changeCollapsed({ sectionId, collapsed: true });
|
||||
|
||||
const state = await db.query.sectionCollapseStates.findFirst({
|
||||
where: eq(sectionCollapseStates.sectionId, sectionId),
|
||||
});
|
||||
expect(state?.collapsed).toBe(true);
|
||||
});
|
||||
|
||||
it("creates and updates collapse state for an enabled dynamic section", async () => {
|
||||
const { boardId, caller, db } = await createCallerAsync();
|
||||
const sectionId = createId();
|
||||
await db.insert(sections).values({
|
||||
id: sectionId,
|
||||
boardId,
|
||||
kind: "dynamic",
|
||||
options: stringify({ collapsible: true }),
|
||||
});
|
||||
|
||||
await caller.changeCollapsed({ sectionId, collapsed: true });
|
||||
await caller.changeCollapsed({ sectionId, collapsed: false });
|
||||
|
||||
const states = await db.query.sectionCollapseStates.findMany({
|
||||
where: eq(sectionCollapseStates.sectionId, sectionId),
|
||||
});
|
||||
expect(states).toHaveLength(1);
|
||||
expect(states[0]?.collapsed).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a dynamic section when collapsing is disabled", async () => {
|
||||
const { boardId, caller, db } = await createCallerAsync();
|
||||
const sectionId = createId();
|
||||
await db.insert(sections).values({
|
||||
id: sectionId,
|
||||
boardId,
|
||||
kind: "dynamic",
|
||||
});
|
||||
|
||||
await expect(caller.changeCollapsed({ sectionId, collapsed: true })).rejects.toThrow("Section cannot be collapsed");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user