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
@@ -0,0 +1,103 @@
|
||||
/** @vitest-environment node */
|
||||
|
||||
import { parse, stringify } from "superjson";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, eq } from "@homarr/db";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import { categorySectionOptionsSchema, dynamicSectionOptionsSchema } from "@homarr/validation/shared";
|
||||
|
||||
import {
|
||||
boards,
|
||||
itemLayouts,
|
||||
items,
|
||||
layouts,
|
||||
sectionCollapseStates,
|
||||
sectionLayouts,
|
||||
sections,
|
||||
users,
|
||||
} from "../../schema";
|
||||
import { migrateCategoriesToUnifiedSectionsAsync } from "./0004_unify_sections_and_gutters";
|
||||
|
||||
describe("unified section migration", () => {
|
||||
test("converts a rail category into a resizable section inside a board gutter", async () => {
|
||||
const db = createDb();
|
||||
const userId = createId();
|
||||
const boardId = createId();
|
||||
const layoutId = createId();
|
||||
const mainRootId = createId();
|
||||
const categoryId = createId();
|
||||
const itemId = createId();
|
||||
|
||||
await db.insert(users).values({ id: userId });
|
||||
await db.insert(boards).values({ id: boardId, name: "migration", creatorId: userId });
|
||||
await db.insert(layouts).values({ id: layoutId, name: "Base", boardId, columnCount: 8, breakpoint: 0 });
|
||||
await db.insert(sections).values([
|
||||
{ id: mainRootId, boardId, kind: "empty", xOffset: 0, yOffset: 0 },
|
||||
{
|
||||
id: categoryId,
|
||||
boardId,
|
||||
kind: "category",
|
||||
name: "Pinned apps",
|
||||
xOffset: 0,
|
||||
yOffset: 1,
|
||||
options: stringify(
|
||||
categorySectionOptionsSchema.parse({
|
||||
railPlacement: "left",
|
||||
columnCount: 2,
|
||||
}),
|
||||
),
|
||||
},
|
||||
]);
|
||||
await db.insert(items).values({ id: itemId, boardId, kind: "clock" });
|
||||
await db.insert(sectionCollapseStates).values({
|
||||
sectionId: categoryId,
|
||||
userId,
|
||||
// Legacy categories stored true when the section was open.
|
||||
collapsed: true,
|
||||
});
|
||||
await db.insert(itemLayouts).values({
|
||||
itemId,
|
||||
layoutId,
|
||||
sectionId: categoryId,
|
||||
xOffset: 1,
|
||||
yOffset: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
});
|
||||
|
||||
await migrateCategoriesToUnifiedSectionsAsync(db as unknown as Database);
|
||||
|
||||
const migratedLayout = await db.query.layouts.findFirst({ where: eq(layouts.id, layoutId) });
|
||||
expect(migratedLayout?.leftGutterColumnCount).toBe(2);
|
||||
|
||||
const gutterRoot = await db.query.sections.findFirst({
|
||||
where: and(eq(sections.boardId, boardId), eq(sections.kind, "empty"), eq(sections.xOffset, -1)),
|
||||
});
|
||||
expect(gutterRoot).toBeDefined();
|
||||
|
||||
const migratedCategory = await db.query.sections.findFirst({ where: eq(sections.id, categoryId) });
|
||||
expect(migratedCategory?.kind).toBe("dynamic");
|
||||
expect(dynamicSectionOptionsSchema.parse(parse(migratedCategory?.options ?? "{}")).title).toBe("Pinned apps");
|
||||
const migratedCollapseState = await db.query.sectionCollapseStates.findFirst({
|
||||
where: and(eq(sectionCollapseStates.sectionId, categoryId), eq(sectionCollapseStates.userId, userId)),
|
||||
});
|
||||
expect(migratedCollapseState?.collapsed).toBe(false);
|
||||
|
||||
const categoryLayout = await db.query.sectionLayouts.findFirst({
|
||||
where: and(eq(sectionLayouts.sectionId, categoryId), eq(sectionLayouts.layoutId, layoutId)),
|
||||
});
|
||||
expect(categoryLayout).toMatchObject({
|
||||
parentSectionId: gutterRoot?.id,
|
||||
width: 2,
|
||||
height: 1,
|
||||
});
|
||||
|
||||
const nestedItem = await db.query.itemLayouts.findFirst({
|
||||
where: and(eq(itemLayouts.itemId, itemId), eq(itemLayouts.layoutId, layoutId)),
|
||||
});
|
||||
expect(nestedItem?.sectionId).toBe(categoryId);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,499 @@
|
||||
import { parse, stringify } from "superjson";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
import type { BoardLane } from "@homarr/definitions";
|
||||
import { emptySuperJSON, getRootSectionLane, rootSectionOffsets } from "@homarr/definitions";
|
||||
import { categorySectionOptionsSchema, dynamicSectionOptionsSchema } from "@homarr/validation/shared";
|
||||
|
||||
import { and, eq, handleTransactionsAsync, inArray } from "../..";
|
||||
import type { Database } from "../..";
|
||||
import type { HomarrDatabase } from "../../driver";
|
||||
import { itemLayouts, layouts, sectionCollapseStates, sectionLayouts, sections } from "../../schema";
|
||||
|
||||
interface MigrationPlacement {
|
||||
id: string;
|
||||
type: "item" | "section";
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
interface MigrationBoard {
|
||||
items: {
|
||||
id: string;
|
||||
layouts: {
|
||||
layoutId: string;
|
||||
sectionId: string;
|
||||
xOffset: number;
|
||||
yOffset: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}[];
|
||||
}[];
|
||||
sections: {
|
||||
id: string;
|
||||
kind: string;
|
||||
layouts: {
|
||||
layoutId: string;
|
||||
parentSectionId: string | null;
|
||||
xOffset: number;
|
||||
yOffset: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
interface MigrationOperation {
|
||||
applyAsync(db: Database): Promise<void>;
|
||||
applySync(db: HomarrDatabase): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Categories and dynamic sections used to be separate concepts. Categories
|
||||
* were full-width roots and also doubled as side rails. Convert every category
|
||||
* into the same resizable section used by the canvas, and create internal
|
||||
* gutter roots for the board-level left/right sidebar settings.
|
||||
*/
|
||||
export async function migrateCategoriesToUnifiedSectionsAsync(db: Database) {
|
||||
const existingBoards = await db.query.boards.findMany({
|
||||
with: {
|
||||
layouts: true,
|
||||
sections: {
|
||||
with: {
|
||||
collapseStates: true,
|
||||
layouts: true,
|
||||
},
|
||||
},
|
||||
items: {
|
||||
with: {
|
||||
layouts: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let migratedCategoryCount = 0;
|
||||
|
||||
for (const board of existingBoards) {
|
||||
const categories = board.sections.filter((section) => section.kind === "category");
|
||||
if (categories.length === 0) continue;
|
||||
const operations: MigrationOperation[] = [];
|
||||
|
||||
const categoryDetails = categories.map((category) => {
|
||||
const options = categorySectionOptionsSchema.parse(parse(category.options ?? emptySuperJSON));
|
||||
return {
|
||||
category,
|
||||
lane: options.railPlacement as BoardLane,
|
||||
options,
|
||||
};
|
||||
});
|
||||
const emptyRoots = board.sections.filter((section) => section.kind === "empty");
|
||||
let mainRoot = emptyRoots.find((section) => getRootSectionLane(section.xOffset) === "main");
|
||||
if (!mainRoot) {
|
||||
const id = createId();
|
||||
operations.push(
|
||||
insertSectionsOperation([
|
||||
{
|
||||
id,
|
||||
boardId: board.id,
|
||||
kind: "empty",
|
||||
xOffset: rootSectionOffsets.main,
|
||||
yOffset: 0,
|
||||
options: emptySuperJSON,
|
||||
},
|
||||
]),
|
||||
);
|
||||
mainRoot = {
|
||||
id,
|
||||
boardId: board.id,
|
||||
kind: "empty",
|
||||
xOffset: rootSectionOffsets.main,
|
||||
yOffset: 0,
|
||||
name: null,
|
||||
options: emptySuperJSON,
|
||||
collapseStates: [],
|
||||
layouts: [],
|
||||
};
|
||||
emptyRoots.push(mainRoot);
|
||||
}
|
||||
|
||||
const rootByLane = new Map<BoardLane, (typeof emptyRoots)[number]>([["main", mainRoot]]);
|
||||
for (const lane of ["left", "right"] as const) {
|
||||
const existing = emptyRoots.find((section) => getRootSectionLane(section.xOffset) === lane);
|
||||
if (existing) {
|
||||
rootByLane.set(lane, existing);
|
||||
continue;
|
||||
}
|
||||
if (!categoryDetails.some((detail) => detail.lane === lane)) continue;
|
||||
|
||||
const id = createId();
|
||||
operations.push(
|
||||
insertSectionsOperation([
|
||||
{
|
||||
id,
|
||||
boardId: board.id,
|
||||
kind: "empty",
|
||||
xOffset: rootSectionOffsets[lane],
|
||||
yOffset: 0,
|
||||
options: emptySuperJSON,
|
||||
},
|
||||
]),
|
||||
);
|
||||
rootByLane.set(lane, {
|
||||
id,
|
||||
boardId: board.id,
|
||||
kind: "empty",
|
||||
xOffset: rootSectionOffsets[lane],
|
||||
yOffset: 0,
|
||||
name: null,
|
||||
options: emptySuperJSON,
|
||||
collapseStates: [],
|
||||
layouts: [],
|
||||
});
|
||||
}
|
||||
|
||||
for (const layout of board.layouts) {
|
||||
const requestedLeft = Math.max(
|
||||
0,
|
||||
...categoryDetails.filter((detail) => detail.lane === "left").map((detail) => detail.options.columnCount),
|
||||
);
|
||||
const requestedRight = Math.max(
|
||||
0,
|
||||
...categoryDetails.filter((detail) => detail.lane === "right").map((detail) => detail.options.columnCount),
|
||||
);
|
||||
const left = Math.min(3, requestedLeft, Math.max(0, layout.columnCount - 1));
|
||||
const right = Math.min(3, requestedRight, Math.max(0, layout.columnCount - left - 1));
|
||||
const widths = {
|
||||
left,
|
||||
main: Math.max(1, layout.columnCount - left - right),
|
||||
right,
|
||||
} satisfies Record<BoardLane, number>;
|
||||
|
||||
operations.push(updateLayoutGuttersOperation(layout.id, left, right));
|
||||
|
||||
for (const lane of ["left", "main", "right"] as const) {
|
||||
const targetRoot = rootByLane.get(lane);
|
||||
const laneWidth = widths[lane];
|
||||
if (!targetRoot || laneWidth === 0) continue;
|
||||
|
||||
const sourceRoots = emptyRoots
|
||||
.filter((section) => getRootSectionLane(section.xOffset) === lane)
|
||||
.toSorted((first, second) => (first.yOffset ?? 0) - (second.yOffset ?? 0));
|
||||
const sourceBlocks = [
|
||||
...sourceRoots.map((root) => ({
|
||||
id: root.id,
|
||||
yOffset: root.yOffset ?? 0,
|
||||
type: "root" as const,
|
||||
})),
|
||||
...categoryDetails
|
||||
.filter((detail) => detail.lane === lane)
|
||||
.map((detail) => ({
|
||||
id: detail.category.id,
|
||||
yOffset: detail.category.yOffset ?? 0,
|
||||
type: "category" as const,
|
||||
detail,
|
||||
})),
|
||||
].toSorted(
|
||||
(first, second) =>
|
||||
first.yOffset - second.yOffset ||
|
||||
(first.type === second.type ? first.id.localeCompare(second.id) : first.type === "category" ? -1 : 1),
|
||||
);
|
||||
|
||||
let nextRow = 0;
|
||||
for (const block of sourceBlocks) {
|
||||
if (block.type === "category") {
|
||||
const content = getDirectPlacements(board, layout.id, block.id);
|
||||
const packedContent = packPlacements(content, laneWidth);
|
||||
operations.push(...persistPlacementOperations(layout.id, block.id, packedContent));
|
||||
const height = Math.max(1, getPlacementRowCount(packedContent));
|
||||
const existingLayout = block.detail.category.layouts.find((candidate) => candidate.layoutId === layout.id);
|
||||
const values = {
|
||||
sectionId: block.id,
|
||||
layoutId: layout.id,
|
||||
parentSectionId: targetRoot.id,
|
||||
xOffset: 0,
|
||||
yOffset: nextRow,
|
||||
width: laneWidth,
|
||||
height,
|
||||
};
|
||||
if (existingLayout) {
|
||||
operations.push(updateSectionLayoutOperation(block.id, layout.id, values));
|
||||
} else {
|
||||
operations.push(insertSectionLayoutOperation(values));
|
||||
}
|
||||
nextRow += height;
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = getDirectPlacements(board, layout.id, block.id);
|
||||
const relativePackedContent = packPlacements(content, laneWidth);
|
||||
const packedContent = relativePackedContent.map((placement) => ({
|
||||
...placement,
|
||||
y: placement.y + nextRow,
|
||||
}));
|
||||
operations.push(...persistPlacementOperations(layout.id, targetRoot.id, packedContent));
|
||||
nextRow += getPlacementRowCount(relativePackedContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const { category, options } of categoryDetails) {
|
||||
const legacyCollapseStates = category.collapseStates;
|
||||
for (const state of legacyCollapseStates) {
|
||||
operations.push(updateCollapseStateOperation(category.id, state.userId, !state.collapsed));
|
||||
}
|
||||
|
||||
operations.push(
|
||||
updateSectionOperation(category.id, {
|
||||
kind: "dynamic",
|
||||
xOffset: null,
|
||||
yOffset: null,
|
||||
name: null,
|
||||
options: stringify(
|
||||
dynamicSectionOptionsSchema.parse({
|
||||
title: (category.name ?? "").slice(0, 64),
|
||||
customCssClasses: [],
|
||||
borderColor: "",
|
||||
showLabel: options.showLabel,
|
||||
collapsible: options.collapsible,
|
||||
showOpenAll: options.showOpenAll,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
migratedCategoryCount += 1;
|
||||
}
|
||||
|
||||
const retainedRootIds = new Set(Array.from(rootByLane.values(), (root) => root.id));
|
||||
const obsoleteRootIds = emptyRoots.filter((root) => !retainedRootIds.has(root.id)).map((root) => root.id);
|
||||
if (obsoleteRootIds.length > 0) {
|
||||
operations.push(deleteSectionsOperation(obsoleteRootIds));
|
||||
}
|
||||
|
||||
await executeBoardMigrationOperationsAsync(db, operations);
|
||||
}
|
||||
|
||||
if (migratedCategoryCount > 0) {
|
||||
console.log(`Migrated categories to unified sections count="${migratedCategoryCount}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const getDirectPlacements = (board: MigrationBoard, layoutId: string, sectionId: string): MigrationPlacement[] => [
|
||||
...board.items.flatMap((item) => {
|
||||
const layout = item.layouts.find(
|
||||
(candidate) => candidate.layoutId === layoutId && candidate.sectionId === sectionId,
|
||||
);
|
||||
return layout
|
||||
? [
|
||||
{
|
||||
id: item.id,
|
||||
type: "item" as const,
|
||||
x: layout.xOffset,
|
||||
y: layout.yOffset,
|
||||
w: layout.width,
|
||||
h: layout.height,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}),
|
||||
...board.sections.flatMap((section) => {
|
||||
if (section.kind !== "dynamic") return [];
|
||||
const layout = section.layouts.find(
|
||||
(candidate) => candidate.layoutId === layoutId && candidate.parentSectionId === sectionId,
|
||||
);
|
||||
return layout
|
||||
? [
|
||||
{
|
||||
id: section.id,
|
||||
type: "section" as const,
|
||||
x: layout.xOffset,
|
||||
y: layout.yOffset,
|
||||
w: layout.width,
|
||||
h: layout.height,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}),
|
||||
];
|
||||
|
||||
const executeBoardMigrationOperationsAsync = async (db: Database, operations: readonly MigrationOperation[]) => {
|
||||
await handleTransactionsAsync(db, {
|
||||
async handleAsync(transactionDb) {
|
||||
await transactionDb.transaction(async (transaction) => {
|
||||
for (const operation of operations) {
|
||||
await operation.applyAsync(transaction as unknown as Database);
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSync(sqliteDb) {
|
||||
sqliteDb.transaction((transaction) => {
|
||||
for (const operation of operations) {
|
||||
operation.applySync(transaction);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const insertSectionsOperation = (values: (typeof sections.$inferInsert)[]): MigrationOperation => ({
|
||||
async applyAsync(db) {
|
||||
await db.insert(sections).values(values);
|
||||
},
|
||||
applySync(db) {
|
||||
db.insert(sections).values(values).run();
|
||||
},
|
||||
});
|
||||
|
||||
const updateLayoutGuttersOperation = (
|
||||
layoutId: string,
|
||||
leftGutterColumnCount: number,
|
||||
rightGutterColumnCount: number,
|
||||
): MigrationOperation => ({
|
||||
async applyAsync(db) {
|
||||
await db.update(layouts).set({ leftGutterColumnCount, rightGutterColumnCount }).where(eq(layouts.id, layoutId));
|
||||
},
|
||||
applySync(db) {
|
||||
db.update(layouts).set({ leftGutterColumnCount, rightGutterColumnCount }).where(eq(layouts.id, layoutId)).run();
|
||||
},
|
||||
});
|
||||
|
||||
const persistPlacementOperations = (
|
||||
layoutId: string,
|
||||
sectionId: string,
|
||||
placements: readonly MigrationPlacement[],
|
||||
): MigrationOperation[] =>
|
||||
placements.map((placement) => {
|
||||
const values = {
|
||||
xOffset: placement.x,
|
||||
yOffset: placement.y,
|
||||
width: placement.w,
|
||||
height: placement.h,
|
||||
};
|
||||
if (placement.type === "item") {
|
||||
return {
|
||||
async applyAsync(db) {
|
||||
await db
|
||||
.update(itemLayouts)
|
||||
.set({ ...values, sectionId })
|
||||
.where(and(eq(itemLayouts.itemId, placement.id), eq(itemLayouts.layoutId, layoutId)));
|
||||
},
|
||||
applySync(db) {
|
||||
db.update(itemLayouts)
|
||||
.set({ ...values, sectionId })
|
||||
.where(and(eq(itemLayouts.itemId, placement.id), eq(itemLayouts.layoutId, layoutId)))
|
||||
.run();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
async applyAsync(db) {
|
||||
await db
|
||||
.update(sectionLayouts)
|
||||
.set({ ...values, parentSectionId: sectionId })
|
||||
.where(and(eq(sectionLayouts.sectionId, placement.id), eq(sectionLayouts.layoutId, layoutId)));
|
||||
},
|
||||
applySync(db) {
|
||||
db.update(sectionLayouts)
|
||||
.set({ ...values, parentSectionId: sectionId })
|
||||
.where(and(eq(sectionLayouts.sectionId, placement.id), eq(sectionLayouts.layoutId, layoutId)))
|
||||
.run();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const updateSectionLayoutOperation = (
|
||||
sectionId: string,
|
||||
layoutId: string,
|
||||
values: typeof sectionLayouts.$inferInsert,
|
||||
): MigrationOperation => ({
|
||||
async applyAsync(db) {
|
||||
await db
|
||||
.update(sectionLayouts)
|
||||
.set(values)
|
||||
.where(and(eq(sectionLayouts.sectionId, sectionId), eq(sectionLayouts.layoutId, layoutId)));
|
||||
},
|
||||
applySync(db) {
|
||||
db.update(sectionLayouts)
|
||||
.set(values)
|
||||
.where(and(eq(sectionLayouts.sectionId, sectionId), eq(sectionLayouts.layoutId, layoutId)))
|
||||
.run();
|
||||
},
|
||||
});
|
||||
|
||||
const insertSectionLayoutOperation = (values: typeof sectionLayouts.$inferInsert): MigrationOperation => ({
|
||||
async applyAsync(db) {
|
||||
await db.insert(sectionLayouts).values(values);
|
||||
},
|
||||
applySync(db) {
|
||||
db.insert(sectionLayouts).values(values).run();
|
||||
},
|
||||
});
|
||||
|
||||
const updateCollapseStateOperation = (sectionId: string, userId: string, collapsed: boolean): MigrationOperation => ({
|
||||
async applyAsync(db) {
|
||||
await db
|
||||
.update(sectionCollapseStates)
|
||||
.set({ collapsed })
|
||||
.where(and(eq(sectionCollapseStates.sectionId, sectionId), eq(sectionCollapseStates.userId, userId)));
|
||||
},
|
||||
applySync(db) {
|
||||
db.update(sectionCollapseStates)
|
||||
.set({ collapsed })
|
||||
.where(and(eq(sectionCollapseStates.sectionId, sectionId), eq(sectionCollapseStates.userId, userId)))
|
||||
.run();
|
||||
},
|
||||
});
|
||||
|
||||
const updateSectionOperation = (
|
||||
sectionId: string,
|
||||
values: Partial<typeof sections.$inferInsert>,
|
||||
): MigrationOperation => ({
|
||||
async applyAsync(db) {
|
||||
await db.update(sections).set(values).where(eq(sections.id, sectionId));
|
||||
},
|
||||
applySync(db) {
|
||||
db.update(sections).set(values).where(eq(sections.id, sectionId)).run();
|
||||
},
|
||||
});
|
||||
|
||||
const deleteSectionsOperation = (sectionIds: string[]): MigrationOperation => ({
|
||||
async applyAsync(db) {
|
||||
await db.delete(sections).where(inArray(sections.id, sectionIds));
|
||||
},
|
||||
applySync(db) {
|
||||
db.delete(sections).where(inArray(sections.id, sectionIds)).run();
|
||||
},
|
||||
});
|
||||
|
||||
const packPlacements = (placements: readonly MigrationPlacement[], columnCount: number): MigrationPlacement[] => {
|
||||
const placed: MigrationPlacement[] = [];
|
||||
const candidates = placements
|
||||
.map((placement) => ({
|
||||
...placement,
|
||||
x: Math.min(Math.max(0, placement.x), Math.max(0, columnCount - Math.min(columnCount, placement.w))),
|
||||
y: Math.max(0, placement.y),
|
||||
w: Math.min(columnCount, Math.max(1, placement.w)),
|
||||
h: Math.max(1, placement.h),
|
||||
}))
|
||||
.toSorted((first, second) => first.y - second.y || first.x - second.x || first.id.localeCompare(second.id));
|
||||
|
||||
for (const candidate of candidates) {
|
||||
let row = 0;
|
||||
while (placed.some((placement) => overlaps({ ...candidate, y: row }, placement))) row += 1;
|
||||
placed.push({ ...candidate, y: row });
|
||||
}
|
||||
|
||||
return placed;
|
||||
};
|
||||
|
||||
const overlaps = (first: MigrationPlacement, second: MigrationPlacement) =>
|
||||
first.x < second.x + second.w &&
|
||||
first.x + first.w > second.x &&
|
||||
first.y < second.y + second.h &&
|
||||
first.y + first.h > second.y;
|
||||
|
||||
const getPlacementRowCount = (placements: readonly MigrationPlacement[]) =>
|
||||
Math.max(0, ...placements.map((placement) => placement.y + placement.h));
|
||||
@@ -3,10 +3,12 @@ import { migrateReleaseWidgetProviderToOptionsAsync } from "./0000_release_widge
|
||||
import { migrateOpnsenseCredentialsAsync } from "./0001_opnsense_credentials";
|
||||
import { migrateAppWidgetShowDescriptionTooltipToDisplayModeAsync } from "./0002_app_widget_show_description_tooltip_to_display_mode";
|
||||
import { migrateWidgetOnlyIntegrationsToOptionsAsync } from "./0003_remove_widget_only_integrations";
|
||||
import { migrateCategoriesToUnifiedSectionsAsync } from "./0004_unify_sections_and_gutters";
|
||||
|
||||
export const applyCustomMigrationsAsync = async (db: Database) => {
|
||||
await migrateReleaseWidgetProviderToOptionsAsync(db);
|
||||
await migrateOpnsenseCredentialsAsync(db);
|
||||
await migrateAppWidgetShowDescriptionTooltipToDisplayModeAsync(db);
|
||||
await migrateWidgetOnlyIntegrationsToOptionsAsync(db);
|
||||
await migrateCategoriesToUnifiedSectionsAsync(db);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `layout` ADD `left_gutter_column_count` tinyint DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `layout` ADD `right_gutter_column_count` tinyint DEFAULT 0 NOT NULL;
|
||||
@@ -316,6 +316,13 @@
|
||||
"when": 1784305537309,
|
||||
"tag": "0044_custom_widget_v2_tables",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 45,
|
||||
"version": "5",
|
||||
"when": 1786212549933,
|
||||
"tag": "0045_add_layout_gutters",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "layout" ADD COLUMN "left_gutter_column_count" smallint DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "layout" ADD COLUMN "right_gutter_column_count" smallint DEFAULT 0 NOT NULL;
|
||||
@@ -78,6 +78,13 @@
|
||||
"when": 1784305412862,
|
||||
"tag": "0010_custom_widget_v2_tables",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 11,
|
||||
"version": "7",
|
||||
"when": 1786212457549,
|
||||
"tag": "0011_add_layout_gutters",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `layout` ADD `left_gutter_column_count` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `layout` ADD `right_gutter_column_count` integer DEFAULT 0 NOT NULL;
|
||||
@@ -302,6 +302,13 @@
|
||||
"when": 1784305283910,
|
||||
"tag": "0042_custom_widget_v2_tables",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 43,
|
||||
"version": "6",
|
||||
"when": 1786212459480,
|
||||
"tag": "0043_add_layout_gutters",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -340,6 +340,8 @@ export const layouts = mysqlTable("layout", {
|
||||
.notNull()
|
||||
.references(() => boards.id, { onDelete: "cascade" }),
|
||||
columnCount: tinyint().notNull(),
|
||||
leftGutterColumnCount: tinyint().notNull().default(0),
|
||||
rightGutterColumnCount: tinyint().notNull().default(0),
|
||||
breakpoint: smallint().notNull().default(0),
|
||||
});
|
||||
|
||||
|
||||
@@ -339,6 +339,8 @@ export const layouts = pgTable("layout", {
|
||||
.notNull()
|
||||
.references(() => boards.id, { onDelete: "cascade" }),
|
||||
columnCount: smallint().notNull(),
|
||||
leftGutterColumnCount: smallint().notNull().default(0),
|
||||
rightGutterColumnCount: smallint().notNull().default(0),
|
||||
breakpoint: smallint().notNull().default(0),
|
||||
});
|
||||
|
||||
|
||||
@@ -324,6 +324,8 @@ export const layouts = sqliteTable("layout", {
|
||||
.notNull()
|
||||
.references(() => boards.id, { onDelete: "cascade" }),
|
||||
columnCount: int().notNull(),
|
||||
leftGutterColumnCount: int().notNull().default(0),
|
||||
rightGutterColumnCount: int().notNull().default(0),
|
||||
breakpoint: int().notNull().default(0),
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user