wip: load board with automatic size and position for non base layouts
This commit is contained in:
4 files changed
+334
-248
No files matched your search
@@ -5,7 +5,7 @@ import { z } from "zod/v4";
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
import { createId } from "@homarr/common";
|
||||
import type { DeviceType } from "@homarr/common/server";
|
||||
import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db";
|
||||
import type { Database, InferInsertModel, InferSelectModel } from "@homarr/db";
|
||||
import { and, asc, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db";
|
||||
import { createDbInsertCollectionWithoutTransaction } from "@homarr/db/collection";
|
||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||
@@ -27,13 +27,11 @@ import {
|
||||
sections,
|
||||
users,
|
||||
} from "@homarr/db/schema";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import {
|
||||
emptySuperJSON,
|
||||
everyoneGroup,
|
||||
getPermissionsWithChildren,
|
||||
getPermissionsWithParents,
|
||||
widgetKinds,
|
||||
} from "@homarr/definitions";
|
||||
import { importOldmarrAsync } from "@homarr/old-import";
|
||||
import { importJsonFileSchema } from "@homarr/old-import/shared";
|
||||
@@ -50,13 +48,11 @@ import {
|
||||
boardSaveSchema,
|
||||
} from "@homarr/validation/board";
|
||||
import { byIdSchema } from "@homarr/validation/common";
|
||||
import { zodUnionFromArray } from "@homarr/validation/enums";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||
import { generateResponsiveGridFor } from "./board/grid-algorithm";
|
||||
import { applyAutoLayoutToBoard, getUpdatedBoardLayout } from "./board/board-layout";
|
||||
import { getFullBoardWithWhereAsync } from "./board/full-board-query";
|
||||
|
||||
export const boardRouter = createTRPCRouter({
|
||||
exists: permissionRequiredProcedure
|
||||
@@ -588,13 +584,15 @@ export const boardRouter = createTRPCRouter({
|
||||
|
||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
||||
|
||||
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||
const board = await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||
return applyAutoLayoutToBoard(board);
|
||||
}),
|
||||
getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => {
|
||||
const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
|
||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
||||
|
||||
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||
const board = await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||
return applyAutoLayoutToBoard(board);
|
||||
}),
|
||||
saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||
@@ -1414,244 +1412,6 @@ const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredId
|
||||
}
|
||||
};
|
||||
|
||||
const getUpdatedBoardLayout = (
|
||||
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
|
||||
options: {
|
||||
previous: {
|
||||
layoutId: string;
|
||||
columnCount: number;
|
||||
};
|
||||
current: {
|
||||
layoutId: string;
|
||||
columnCount: number;
|
||||
};
|
||||
},
|
||||
) => {
|
||||
const itemSectionLayoutsCollection: InferInsertModel<typeof itemLayouts>[] = [];
|
||||
const sectionLayoutsCollection: InferInsertModel<typeof sectionLayouts>[] = [];
|
||||
|
||||
const elements = getElementsForLayout(board, options.previous.layoutId);
|
||||
const rootSections = board.sections.filter((section) => section.kind !== "dynamic");
|
||||
|
||||
for (const rootSection of rootSections) {
|
||||
const result = generateResponsiveGridFor({
|
||||
items: elements,
|
||||
previousWidth: options.previous.columnCount,
|
||||
width: options.current.columnCount,
|
||||
sectionId: rootSection.id,
|
||||
});
|
||||
|
||||
itemSectionLayoutsCollection.push(
|
||||
...board.items
|
||||
.map((item): InferInsertModel<typeof itemLayouts> | null => {
|
||||
const currentElement = result.items.find((element) => element.type === "item" && element.id === item.id);
|
||||
|
||||
if (!currentElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
itemId: item.id,
|
||||
layoutId: options.current.layoutId,
|
||||
sectionId: currentElement.sectionId,
|
||||
height: currentElement.height,
|
||||
width: currentElement.width,
|
||||
xOffset: currentElement.xOffset,
|
||||
yOffset: currentElement.yOffset,
|
||||
};
|
||||
})
|
||||
.filter((item) => item !== null),
|
||||
);
|
||||
|
||||
sectionLayoutsCollection.push(
|
||||
...board.sections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.map((section): InferInsertModel<typeof sectionLayouts> | null => {
|
||||
const currentElement = result.items.find(
|
||||
(element) => element.type === "section" && element.id === section.id,
|
||||
);
|
||||
|
||||
if (!currentElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
layoutId: options.current.layoutId,
|
||||
sectionId: section.id,
|
||||
parentSectionId: currentElement.sectionId,
|
||||
height: currentElement.height,
|
||||
width: currentElement.width,
|
||||
xOffset: currentElement.xOffset,
|
||||
yOffset: currentElement.yOffset,
|
||||
};
|
||||
})
|
||||
.filter((section) => section !== null),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
itemSectionLayouts: itemSectionLayoutsCollection,
|
||||
sectionLayouts: sectionLayoutsCollection,
|
||||
};
|
||||
};
|
||||
|
||||
const getElementsForLayout = (board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>, layoutId: string) => {
|
||||
const sectionElements = board.sections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.map((section) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const clonedLayout = section.layouts.find((sectionLayout) => sectionLayout.layoutId === layoutId)!;
|
||||
|
||||
return {
|
||||
id: section.id,
|
||||
type: "section" as const,
|
||||
height: clonedLayout.height,
|
||||
width: clonedLayout.width,
|
||||
xOffset: clonedLayout.xOffset,
|
||||
yOffset: clonedLayout.yOffset,
|
||||
sectionId: clonedLayout.parentSectionId,
|
||||
};
|
||||
});
|
||||
|
||||
const itemElements = board.items.map((item) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const clonedLayout = item.layouts.find((itemLayout) => itemLayout.layoutId === layoutId)!;
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
type: "item" as const,
|
||||
height: clonedLayout.height,
|
||||
width: clonedLayout.width,
|
||||
xOffset: clonedLayout.xOffset,
|
||||
yOffset: clonedLayout.yOffset,
|
||||
sectionId: clonedLayout.sectionId,
|
||||
};
|
||||
});
|
||||
|
||||
return [...itemElements, ...sectionElements];
|
||||
};
|
||||
|
||||
const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, userId: string | null) => {
|
||||
const groupsOfCurrentUser = await db.query.groupMembers.findMany({
|
||||
where: eq(groupMembers.userId, userId ?? ""),
|
||||
});
|
||||
const board = await db.query.boards.findFirst({
|
||||
where,
|
||||
with: {
|
||||
creator: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
},
|
||||
},
|
||||
sections: {
|
||||
with: {
|
||||
collapseStates: {
|
||||
where: eq(sectionCollapseStates.userId, userId ?? ""),
|
||||
},
|
||||
layouts: true,
|
||||
},
|
||||
},
|
||||
items: {
|
||||
with: {
|
||||
integrations: {
|
||||
with: {
|
||||
integration: true,
|
||||
},
|
||||
},
|
||||
layouts: true,
|
||||
},
|
||||
},
|
||||
layouts: true,
|
||||
userPermissions: {
|
||||
where: eq(boardUserPermissions.userId, userId ?? ""),
|
||||
columns: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
groupPermissions: {
|
||||
where: inArray(boardGroupPermissions.groupId, groupsOfCurrentUser.map((group) => group.groupId).concat("")),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!board) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Board not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { sections, items, layouts, ...otherBoardProperties } = board;
|
||||
|
||||
return {
|
||||
...otherBoardProperties,
|
||||
layouts: layouts
|
||||
.map(({ boardId: _, ...layout }) => layout)
|
||||
.sort((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
|
||||
sections: sections.map(({ collapseStates, ...section }) =>
|
||||
parseSection({
|
||||
...section,
|
||||
xOffset: section.xOffset,
|
||||
yOffset: section.yOffset,
|
||||
options: superjson.parse(section.options ?? emptySuperJSON),
|
||||
layouts: section.layouts.map((layout) => ({
|
||||
xOffset: layout.xOffset,
|
||||
yOffset: layout.yOffset,
|
||||
width: layout.width,
|
||||
height: layout.height,
|
||||
parentSectionId: layout.parentSectionId,
|
||||
layoutId: layout.layoutId,
|
||||
})),
|
||||
collapsed: collapseStates.at(0)?.collapsed ?? false,
|
||||
}),
|
||||
),
|
||||
items: items.map(({ integrations: itemIntegrations, ...item }) =>
|
||||
parseItem({
|
||||
...item,
|
||||
layouts: item.layouts.map((layout) => ({
|
||||
xOffset: layout.xOffset,
|
||||
yOffset: layout.yOffset,
|
||||
width: layout.width,
|
||||
height: layout.height,
|
||||
layoutId: layout.layoutId,
|
||||
sectionId: layout.sectionId,
|
||||
})),
|
||||
integrationIds: itemIntegrations.map((item) => item.integration.id),
|
||||
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
|
||||
options: superjson.parse<Record<string, unknown>>(item.options),
|
||||
}),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const forKind = <T extends WidgetKind>(kind: T) =>
|
||||
z.object({
|
||||
kind: z.literal(kind),
|
||||
options: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
|
||||
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);
|
||||
|
||||
const parseItem = (item: unknown) => {
|
||||
const result = outputItemSchema.safeParse(item);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const parseSection = (section: unknown) => {
|
||||
const result = sectionSchema.safeParse(section);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const filterAddedItems = <TInput extends { id: string }>(inputArray: TInput[], dbArray: TInput[]) =>
|
||||
inputArray.filter((inputItem) => !dbArray.some((dbItem) => dbItem.id === inputItem.id));
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import type { Modify } from "@homarr/common/types";
|
||||
import type { InferInsertModel } from "@homarr/db";
|
||||
import type { itemLayouts, sectionLayouts } from "@homarr/db/schema";
|
||||
|
||||
import type { getFullBoardWithWhereAsync } from "./full-board-query";
|
||||
import { generateResponsiveGridFor } from "./grid-algorithm";
|
||||
|
||||
type DynamicSectionLayout = Modify<InferInsertModel<typeof sectionLayouts>, { parentSectionId: string }>;
|
||||
|
||||
export const getUpdatedBoardLayout = (
|
||||
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
|
||||
options: {
|
||||
previous: {
|
||||
layoutId: string;
|
||||
columnCount: number;
|
||||
};
|
||||
current: {
|
||||
layoutId: string;
|
||||
columnCount: number;
|
||||
};
|
||||
},
|
||||
) => {
|
||||
const itemSectionLayoutsCollection: InferInsertModel<typeof itemLayouts>[] = [];
|
||||
const sectionLayoutsCollection: DynamicSectionLayout[] = [];
|
||||
|
||||
const elements = getElementsForLayout(board, options.previous.layoutId);
|
||||
const rootSections = board.sections.filter((section) => section.kind !== "dynamic");
|
||||
|
||||
for (const rootSection of rootSections) {
|
||||
const result = generateResponsiveGridFor({
|
||||
items: elements,
|
||||
previousWidth: options.previous.columnCount,
|
||||
width: options.current.columnCount,
|
||||
sectionId: rootSection.id,
|
||||
});
|
||||
|
||||
itemSectionLayoutsCollection.push(
|
||||
...board.items
|
||||
.map((item): InferInsertModel<typeof itemLayouts> | null => {
|
||||
const currentElement = result.items.find((element) => element.type === "item" && element.id === item.id);
|
||||
|
||||
if (!currentElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
itemId: item.id,
|
||||
layoutId: options.current.layoutId,
|
||||
sectionId: currentElement.sectionId,
|
||||
height: currentElement.height,
|
||||
width: currentElement.width,
|
||||
xOffset: currentElement.xOffset,
|
||||
yOffset: currentElement.yOffset,
|
||||
};
|
||||
})
|
||||
.filter((item) => item !== null),
|
||||
);
|
||||
|
||||
sectionLayoutsCollection.push(
|
||||
...board.sections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.map((section): DynamicSectionLayout | null => {
|
||||
const currentElement = result.items.find(
|
||||
(element) => element.type === "section" && element.id === section.id,
|
||||
);
|
||||
|
||||
if (!currentElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
layoutId: options.current.layoutId,
|
||||
sectionId: section.id,
|
||||
parentSectionId: currentElement.sectionId,
|
||||
height: currentElement.height,
|
||||
width: currentElement.width,
|
||||
xOffset: currentElement.xOffset,
|
||||
yOffset: currentElement.yOffset,
|
||||
};
|
||||
})
|
||||
.filter((section) => section !== null),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
itemSectionLayouts: itemSectionLayoutsCollection,
|
||||
sectionLayouts: sectionLayoutsCollection,
|
||||
};
|
||||
};
|
||||
|
||||
const getElementsForLayout = (board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>, layoutId: string) => {
|
||||
const sectionElements = board.sections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.map((section) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const clonedLayout = section.layouts.find((sectionLayout) => sectionLayout.layoutId === layoutId)!;
|
||||
|
||||
return {
|
||||
id: section.id,
|
||||
type: "section" as const,
|
||||
height: clonedLayout.height,
|
||||
width: clonedLayout.width,
|
||||
xOffset: clonedLayout.xOffset,
|
||||
yOffset: clonedLayout.yOffset,
|
||||
sectionId: clonedLayout.parentSectionId,
|
||||
};
|
||||
});
|
||||
|
||||
const itemElements = board.items.map((item) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const clonedLayout = item.layouts.find((itemLayout) => itemLayout.layoutId === layoutId)!;
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
type: "item" as const,
|
||||
height: clonedLayout.height,
|
||||
width: clonedLayout.width,
|
||||
xOffset: clonedLayout.xOffset,
|
||||
yOffset: clonedLayout.yOffset,
|
||||
sectionId: clonedLayout.sectionId,
|
||||
};
|
||||
});
|
||||
|
||||
return [...itemElements, ...sectionElements];
|
||||
};
|
||||
|
||||
export const applyAutoLayoutToBoard = (
|
||||
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
|
||||
): Awaited<ReturnType<typeof getFullBoardWithWhereAsync>> => {
|
||||
if (board.layoutMode === "custom") return board;
|
||||
|
||||
const baseLayout = board.layouts.find((layout) => layout.id === board.baseLayoutId) ?? null;
|
||||
if (!baseLayout) return board;
|
||||
|
||||
const updatedLayoutMap = new Map<string, ReturnType<typeof getUpdatedBoardLayout>>();
|
||||
for (const layout of board.layouts) {
|
||||
if (layout.id === baseLayout.id) continue;
|
||||
|
||||
updatedLayoutMap.set(
|
||||
layout.id,
|
||||
getUpdatedBoardLayout(board, {
|
||||
previous: {
|
||||
layoutId: baseLayout.id,
|
||||
columnCount: baseLayout.columnCount,
|
||||
},
|
||||
current: {
|
||||
layoutId: layout.id,
|
||||
columnCount: layout.columnCount,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...board,
|
||||
items: board.items.map((item) => ({
|
||||
...item,
|
||||
layouts: item.layouts.map(
|
||||
(layoutSection) =>
|
||||
updatedLayoutMap
|
||||
.get(layoutSection.layoutId)
|
||||
?.itemSectionLayouts.find(
|
||||
(updatedLayoutSection) =>
|
||||
updatedLayoutSection.itemId === item.id && updatedLayoutSection.layoutId === layoutSection.layoutId,
|
||||
) ?? layoutSection,
|
||||
),
|
||||
})),
|
||||
sections: board.sections.map((section) =>
|
||||
"layouts" in section
|
||||
? {
|
||||
...section,
|
||||
layouts: section.layouts.map(
|
||||
(sectionLayout) =>
|
||||
updatedLayoutMap
|
||||
.get(sectionLayout.layoutId)
|
||||
?.sectionLayouts.find(
|
||||
(updatedSectionLayout) =>
|
||||
updatedSectionLayout.sectionId === section.id &&
|
||||
updatedSectionLayout.layoutId === sectionLayout.layoutId,
|
||||
) ?? sectionLayout,
|
||||
),
|
||||
}
|
||||
: section,
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
import z from "zod";
|
||||
|
||||
import type { Database, SQL } from "@homarr/db";
|
||||
import { eq, inArray } from "@homarr/db";
|
||||
import { boardGroupPermissions, boardUserPermissions, groupMembers, sectionCollapseStates } from "@homarr/db/schema";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { emptySuperJSON, widgetKinds } from "@homarr/definitions";
|
||||
import { zodUnionFromArray } from "@homarr/validation/enums";
|
||||
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||
|
||||
export const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, userId: string | null) => {
|
||||
const groupsOfCurrentUser = await db.query.groupMembers.findMany({
|
||||
where: eq(groupMembers.userId, userId ?? ""),
|
||||
});
|
||||
const board = await db.query.boards.findFirst({
|
||||
where,
|
||||
with: {
|
||||
creator: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
},
|
||||
},
|
||||
sections: {
|
||||
with: {
|
||||
collapseStates: {
|
||||
where: eq(sectionCollapseStates.userId, userId ?? ""),
|
||||
},
|
||||
layouts: true,
|
||||
},
|
||||
},
|
||||
items: {
|
||||
with: {
|
||||
integrations: {
|
||||
with: {
|
||||
integration: true,
|
||||
},
|
||||
},
|
||||
layouts: true,
|
||||
},
|
||||
},
|
||||
layouts: true,
|
||||
userPermissions: {
|
||||
where: eq(boardUserPermissions.userId, userId ?? ""),
|
||||
columns: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
groupPermissions: {
|
||||
where: inArray(boardGroupPermissions.groupId, groupsOfCurrentUser.map((group) => group.groupId).concat("")),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!board) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Board not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { sections, items, layouts, ...otherBoardProperties } = board;
|
||||
|
||||
return {
|
||||
...otherBoardProperties,
|
||||
layouts: layouts
|
||||
.map(({ boardId: _, ...layout }) => layout)
|
||||
.sort((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
|
||||
sections: sections.map(({ collapseStates, ...section }) =>
|
||||
parseSection({
|
||||
...section,
|
||||
xOffset: section.xOffset,
|
||||
yOffset: section.yOffset,
|
||||
options: superjson.parse(section.options ?? emptySuperJSON),
|
||||
layouts: section.layouts.map((layout) => ({
|
||||
xOffset: layout.xOffset,
|
||||
yOffset: layout.yOffset,
|
||||
width: layout.width,
|
||||
height: layout.height,
|
||||
parentSectionId: layout.parentSectionId,
|
||||
layoutId: layout.layoutId,
|
||||
})),
|
||||
collapsed: collapseStates.at(0)?.collapsed ?? false,
|
||||
}),
|
||||
),
|
||||
items: items.map(({ integrations: itemIntegrations, ...item }) =>
|
||||
parseItem({
|
||||
...item,
|
||||
layouts: item.layouts.map((layout) => ({
|
||||
xOffset: layout.xOffset,
|
||||
yOffset: layout.yOffset,
|
||||
width: layout.width,
|
||||
height: layout.height,
|
||||
layoutId: layout.layoutId,
|
||||
sectionId: layout.sectionId,
|
||||
})),
|
||||
integrationIds: itemIntegrations.map((item) => item.integration.id),
|
||||
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
|
||||
options: superjson.parse<Record<string, unknown>>(item.options),
|
||||
}),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const forKind = <T extends WidgetKind>(kind: T) =>
|
||||
z.object({
|
||||
kind: z.literal(kind),
|
||||
options: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
|
||||
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);
|
||||
|
||||
const parseItem = (item: unknown) => {
|
||||
const result = outputItemSchema.safeParse(item);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const parseSection = (section: unknown) => {
|
||||
const result = sectionSchema.safeParse(section);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
return result.data;
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { usePathname } from "next/navigation";
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
|
||||
import { useEditMode } from "./edit-mode";
|
||||
import { updateBoardName } from "./updater";
|
||||
|
||||
const BoardContext = createContext<{
|
||||
@@ -83,10 +84,12 @@ export const getCurrentLayout = (board: RouterOutputs["board"]["getBoardByName"]
|
||||
export const useCurrentLayout = () => {
|
||||
const board = useRequiredBoard();
|
||||
const [currentLayout, setCurrentLayout] = useState(getCurrentLayout(board));
|
||||
const [isEditMode] = useEditMode();
|
||||
|
||||
const onResize = useCallback(() => {
|
||||
if (isEditMode && board.layoutMode === "auto") return;
|
||||
setCurrentLayout(getCurrentLayout(board));
|
||||
}, [board]);
|
||||
}, [board, isEditMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
@@ -97,6 +100,10 @@ export const useCurrentLayout = () => {
|
||||
};
|
||||
}, [onResize]);
|
||||
|
||||
if (board.layoutMode === "auto" && isEditMode && board.baseLayoutId !== null) {
|
||||
return board.baseLayoutId;
|
||||
}
|
||||
|
||||
return currentLayout;
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user