fix: add item does not respect dynamic sections (#2010)
* bug: add item does not respect dynamic sections * fix: deepsource issue
This commit is contained in:
@@ -0,0 +1,62 @@
|
|||||||
|
import type { Modify } from "@homarr/common/types";
|
||||||
|
import { createId } from "@homarr/db/client";
|
||||||
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
|
|
||||||
|
import type { Board, DynamicSection, EmptySection, Item } from "~/app/[locale]/boards/_types";
|
||||||
|
import { getFirstEmptyPosition } from "./empty-position";
|
||||||
|
|
||||||
|
export interface CreateItemInput {
|
||||||
|
kind: WidgetKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createItemCallback =
|
||||||
|
({ kind }: CreateItemInput) =>
|
||||||
|
(previous: Board): Board => {
|
||||||
|
const firstSection = previous.sections
|
||||||
|
.filter((section): section is EmptySection => section.kind === "empty")
|
||||||
|
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset)
|
||||||
|
.at(0);
|
||||||
|
|
||||||
|
if (!firstSection) return previous;
|
||||||
|
|
||||||
|
const dynamicSectionsOfFirstSection = previous.sections.filter(
|
||||||
|
(section): section is DynamicSection => section.kind === "dynamic" && section.parentSectionId === firstSection.id,
|
||||||
|
);
|
||||||
|
const elements = [...firstSection.items, ...dynamicSectionsOfFirstSection];
|
||||||
|
const emptyPosition = getFirstEmptyPosition(elements, previous.columnCount);
|
||||||
|
|
||||||
|
if (!emptyPosition) {
|
||||||
|
console.error("Your board is full");
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
id: createId(),
|
||||||
|
kind,
|
||||||
|
options: {},
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
...emptyPosition,
|
||||||
|
integrationIds: [],
|
||||||
|
advancedOptions: {
|
||||||
|
customCssClasses: [],
|
||||||
|
},
|
||||||
|
} satisfies Modify<
|
||||||
|
Item,
|
||||||
|
{
|
||||||
|
kind: WidgetKind;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...previous,
|
||||||
|
sections: previous.sections.map((section) => {
|
||||||
|
// Return same section if item is not in it
|
||||||
|
if (section.id !== firstSection.id) return section;
|
||||||
|
return {
|
||||||
|
...section,
|
||||||
|
items: section.items.concat(widget),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { createId } from "@homarr/db/client";
|
||||||
|
|
||||||
|
import type { Board, DynamicSection, EmptySection } from "~/app/[locale]/boards/_types";
|
||||||
|
import { getFirstEmptyPosition } from "./empty-position";
|
||||||
|
|
||||||
|
export interface DuplicateItemInput {
|
||||||
|
itemId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const duplicateItemCallback =
|
||||||
|
({ itemId }: DuplicateItemInput) =>
|
||||||
|
(previous: Board): Board => {
|
||||||
|
const itemToDuplicate = previous.sections
|
||||||
|
.flatMap((section) => section.items.map((item) => ({ ...item, sectionId: section.id })))
|
||||||
|
.find((item) => item.id === itemId);
|
||||||
|
if (!itemToDuplicate) return previous;
|
||||||
|
|
||||||
|
const currentSection = previous.sections.find((section) => section.id === itemToDuplicate.sectionId);
|
||||||
|
if (!currentSection) return previous;
|
||||||
|
|
||||||
|
const dynamicSectionsOfCurrentSection = previous.sections.filter(
|
||||||
|
(section): section is DynamicSection =>
|
||||||
|
section.kind === "dynamic" && section.parentSectionId === currentSection.id,
|
||||||
|
);
|
||||||
|
const elements = [...currentSection.items, ...dynamicSectionsOfCurrentSection];
|
||||||
|
let sectionId = currentSection.id;
|
||||||
|
let emptyPosition = getFirstEmptyPosition(
|
||||||
|
elements,
|
||||||
|
currentSection.kind === "dynamic" ? currentSection.width : previous.columnCount,
|
||||||
|
currentSection.kind === "dynamic" ? currentSection.height : undefined,
|
||||||
|
{
|
||||||
|
width: itemToDuplicate.width,
|
||||||
|
height: itemToDuplicate.height,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!emptyPosition) {
|
||||||
|
const firstSection = previous.sections
|
||||||
|
.filter((section): section is EmptySection => section.kind === "empty")
|
||||||
|
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset)
|
||||||
|
.at(0);
|
||||||
|
|
||||||
|
if (!firstSection) return previous;
|
||||||
|
|
||||||
|
const dynamicSectionsOfFirstSection = previous.sections.filter(
|
||||||
|
(section): section is DynamicSection =>
|
||||||
|
section.kind === "dynamic" && section.parentSectionId === firstSection.id,
|
||||||
|
);
|
||||||
|
const elements = [...firstSection.items, ...dynamicSectionsOfFirstSection];
|
||||||
|
emptyPosition = getFirstEmptyPosition(elements, previous.columnCount, undefined, {
|
||||||
|
width: itemToDuplicate.width,
|
||||||
|
height: itemToDuplicate.height,
|
||||||
|
});
|
||||||
|
if (!emptyPosition) {
|
||||||
|
console.error("Your board is full");
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionId = firstSection.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = structuredClone(itemToDuplicate);
|
||||||
|
widget.id = createId();
|
||||||
|
widget.xOffset = emptyPosition.xOffset;
|
||||||
|
widget.yOffset = emptyPosition.yOffset;
|
||||||
|
widget.sectionId = sectionId;
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
...previous,
|
||||||
|
sections: previous.sections.map((section) => {
|
||||||
|
// Return same section if item is not in it
|
||||||
|
if (section.id !== sectionId) return section;
|
||||||
|
return {
|
||||||
|
...section,
|
||||||
|
items: section.items.concat(widget),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Item } from "~/app/[locale]/boards/_types";
|
||||||
|
|
||||||
|
export const getFirstEmptyPosition = (
|
||||||
|
elements: Pick<Item, "yOffset" | "xOffset" | "width" | "height">[],
|
||||||
|
columnCount: number,
|
||||||
|
rowCount = 9999,
|
||||||
|
size: { width: number; height: number } = { width: 1, height: 1 },
|
||||||
|
) => {
|
||||||
|
for (let yOffset = 0; yOffset < rowCount + 1 - size.height; yOffset++) {
|
||||||
|
for (let xOffset = 0; xOffset < columnCount; xOffset++) {
|
||||||
|
const isOccupied = elements.some(
|
||||||
|
(element) =>
|
||||||
|
element.yOffset < yOffset + size.height &&
|
||||||
|
element.yOffset + element.height > yOffset &&
|
||||||
|
element.xOffset < xOffset + size.width &&
|
||||||
|
element.xOffset + element.width > xOffset,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOccupied) {
|
||||||
|
return { xOffset, yOffset };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { Board } from "~/app/[locale]/boards/_types";
|
||||||
|
import { createItemCallback } from "../create-item";
|
||||||
|
import * as emptyPosition from "../empty-position";
|
||||||
|
import { createDynamicSection, createEmptySection, createItem } from "./shared";
|
||||||
|
|
||||||
|
describe("item actions create-item", () => {
|
||||||
|
test("should add it to first section", () => {
|
||||||
|
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
|
||||||
|
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
|
||||||
|
const input = {
|
||||||
|
sections: [createEmptySection("1", 2), createEmptySection("2", 0), createEmptySection("3", 1)],
|
||||||
|
columnCount: 4,
|
||||||
|
} satisfies Pick<Board, "sections" | "columnCount">;
|
||||||
|
|
||||||
|
const result = createItemCallback({
|
||||||
|
kind: "clock",
|
||||||
|
})(input as unknown as Board);
|
||||||
|
|
||||||
|
const firstSection = result.sections.find((section) => section.id === "2");
|
||||||
|
const item = firstSection?.items.at(0);
|
||||||
|
expect(item).toEqual(expect.objectContaining({ kind: "clock", xOffset: 5, yOffset: 5 }));
|
||||||
|
expect(spy).toHaveBeenCalledWith([], input.columnCount);
|
||||||
|
});
|
||||||
|
test("should correctly pass dynamic section and items to getFirstEmptyPosition", () => {
|
||||||
|
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
|
||||||
|
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
|
||||||
|
const firstSection = createEmptySection("2", 0);
|
||||||
|
const expectedItem = createItem({ id: "12", xOffset: 1, yOffset: 2, width: 3, height: 2 });
|
||||||
|
firstSection.items.push(expectedItem);
|
||||||
|
const dynamicSectionInFirst = createDynamicSection({
|
||||||
|
id: "4",
|
||||||
|
parentSectionId: "2",
|
||||||
|
yOffset: 0,
|
||||||
|
xOffset: 0,
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
sections: [
|
||||||
|
createEmptySection("1", 2),
|
||||||
|
firstSection,
|
||||||
|
createEmptySection("3", 1),
|
||||||
|
dynamicSectionInFirst,
|
||||||
|
createDynamicSection({ id: "5", parentSectionId: "3", yOffset: 1 }),
|
||||||
|
],
|
||||||
|
columnCount: 4,
|
||||||
|
} satisfies Pick<Board, "sections" | "columnCount">;
|
||||||
|
|
||||||
|
const result = createItemCallback({
|
||||||
|
kind: "clock",
|
||||||
|
})(input as unknown as Board);
|
||||||
|
|
||||||
|
const firstSectionResult = result.sections.find((section) => section.id === "2");
|
||||||
|
const item = firstSectionResult?.items.find((item) => item.id !== "12");
|
||||||
|
expect(item).toEqual(expect.objectContaining({ kind: "clock", xOffset: 5, yOffset: 5 }));
|
||||||
|
expect(spy).toHaveBeenCalledWith([expectedItem, dynamicSectionInFirst], input.columnCount);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { Board } from "~/app/[locale]/boards/_types";
|
||||||
|
import { duplicateItemCallback } from "../duplicate-item";
|
||||||
|
import * as emptyPosition from "../empty-position";
|
||||||
|
import { createEmptySection, createItem } from "./shared";
|
||||||
|
|
||||||
|
describe("item actions duplicate-item", () => {
|
||||||
|
test("should copy it in the same section", () => {
|
||||||
|
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
|
||||||
|
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
|
||||||
|
const currentSection = createEmptySection("2", 1);
|
||||||
|
const currentItem = createItem({
|
||||||
|
id: "1",
|
||||||
|
xOffset: 1,
|
||||||
|
yOffset: 3,
|
||||||
|
width: 3,
|
||||||
|
height: 2,
|
||||||
|
kind: "minecraftServerStatus",
|
||||||
|
integrationIds: ["1"],
|
||||||
|
options: { address: "localhost" },
|
||||||
|
advancedOptions: { customCssClasses: ["test"] },
|
||||||
|
});
|
||||||
|
const otherItem = createItem({
|
||||||
|
id: "2",
|
||||||
|
});
|
||||||
|
currentSection.items.push(currentItem, otherItem);
|
||||||
|
const input = {
|
||||||
|
columnCount: 10,
|
||||||
|
sections: [createEmptySection("1", 0), currentSection, createEmptySection("3", 2)],
|
||||||
|
} satisfies Pick<Board, "sections" | "columnCount">;
|
||||||
|
|
||||||
|
const result = duplicateItemCallback({ itemId: currentItem.id })(input as unknown as Board);
|
||||||
|
|
||||||
|
const section = result.sections.find((section) => section.id === "2");
|
||||||
|
expect(section?.items.length).toBe(3);
|
||||||
|
const duplicatedItem = section?.items.find((item) => item.id !== currentItem.id && item.id !== otherItem.id);
|
||||||
|
|
||||||
|
expect(duplicatedItem).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
kind: "minecraftServerStatus",
|
||||||
|
xOffset: 5,
|
||||||
|
yOffset: 5,
|
||||||
|
width: 3,
|
||||||
|
height: 2,
|
||||||
|
integrationIds: ["1"],
|
||||||
|
options: { address: "localhost" },
|
||||||
|
advancedOptions: { customCssClasses: ["test"] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import type { Item } from "~/app/[locale]/boards/_types";
|
||||||
|
import { getFirstEmptyPosition } from "../empty-position";
|
||||||
|
|
||||||
|
describe("get first empty position", () => {
|
||||||
|
test.each([
|
||||||
|
[[[" ", " ", " ", " "]], [1, 1], 0, 0],
|
||||||
|
[[["a", " ", " ", " "]], [1, 1], 1, 0],
|
||||||
|
[[[" ", "a", " ", " "]], [1, 1], 0, 0],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "a", " ", " "],
|
||||||
|
["a", "a", " ", " "],
|
||||||
|
],
|
||||||
|
[1, 1],
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
[[["a", "a", "a", "a"]], [1, 1], 0, 1],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "a", "a", "a"],
|
||||||
|
["a", "a", "a", "a"],
|
||||||
|
],
|
||||||
|
[1, 1],
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "a", " ", "b", "b"],
|
||||||
|
["a", "a", " ", "b", "b"],
|
||||||
|
],
|
||||||
|
[1, 2],
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "a", " ", " ", "b", "b"],
|
||||||
|
["a", "a", " ", " ", "b", "b"],
|
||||||
|
],
|
||||||
|
[2, 2],
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "a", " ", "d", "b", "b"],
|
||||||
|
["a", "a", "c", "e", "b", "b"],
|
||||||
|
],
|
||||||
|
[1, 1],
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "a", " ", " ", "b", "b"],
|
||||||
|
["a", "a", " ", "e", "b", "b"],
|
||||||
|
],
|
||||||
|
[2, 2],
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
],
|
||||||
|
])("should return the first empty position", (layout, size, expectedX, expectedY) => {
|
||||||
|
const elements = createElementsFromLayout(layout);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const result = getFirstEmptyPosition(elements, layout[0]!.length, undefined, { width: size[0]!, height: size[1]! });
|
||||||
|
|
||||||
|
expect(result).toEqual({ xOffset: expectedX, yOffset: expectedY });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[[[" ", " "]], [1, 1], 0, 0, 1],
|
||||||
|
[[["a", " "]], [1, 1], 1, 0, 1],
|
||||||
|
[[["a", "a"]], [1, 1], undefined, undefined, 1],
|
||||||
|
[[["a", "a"]], [1, 1], 0, 1, 2],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "b", " ", " "],
|
||||||
|
["a", "c", " ", "d"],
|
||||||
|
],
|
||||||
|
[2, 2],
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
3,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["a", "b", " ", " "],
|
||||||
|
["a", "c", " ", "d"],
|
||||||
|
],
|
||||||
|
[2, 2],
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
4,
|
||||||
|
],
|
||||||
|
[[["a", "b"]], [2, 1], 0, 1, 2],
|
||||||
|
])("should return the first empty position with limited rows", (layout, size, expectedX, expectedY, rowCount) => {
|
||||||
|
const elements = createElementsFromLayout(layout);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const result = getFirstEmptyPosition(elements, layout[0]!.length, rowCount, { width: size[0]!, height: size[1]! });
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedX !== undefined ? { xOffset: expectedX, yOffset: expectedY } : undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const createElementsFromLayout = (layout: string[][]) => {
|
||||||
|
const elements: (Pick<Item, "xOffset" | "yOffset" | "width" | "height"> & { char: string })[] = [];
|
||||||
|
for (let yOffset = 0; yOffset < layout.length; yOffset++) {
|
||||||
|
const row = layout[yOffset];
|
||||||
|
if (!row) continue;
|
||||||
|
for (let xOffset = 0; xOffset < row.length; xOffset++) {
|
||||||
|
const item = row[xOffset];
|
||||||
|
if (item === " " || !item) continue;
|
||||||
|
|
||||||
|
const existing = elements.find((element) => element.char === item);
|
||||||
|
if (existing) {
|
||||||
|
existing.height = yOffset - existing.yOffset + 1;
|
||||||
|
existing.width = xOffset - existing.xOffset + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.push({
|
||||||
|
yOffset,
|
||||||
|
xOffset,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
char: item,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { DynamicSection, EmptySection, Item } from "~/app/[locale]/boards/_types";
|
||||||
|
|
||||||
|
export const createEmptySection = (id: string, yOffset: number): EmptySection => ({
|
||||||
|
id,
|
||||||
|
kind: "empty",
|
||||||
|
yOffset,
|
||||||
|
xOffset: 0,
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createDynamicSection = (section: Omit<Partial<DynamicSection>, "kind">): DynamicSection => ({
|
||||||
|
id: section.id ?? "0",
|
||||||
|
kind: "dynamic",
|
||||||
|
parentSectionId: section.parentSectionId ?? "0",
|
||||||
|
height: section.height ?? 1,
|
||||||
|
width: section.width ?? 1,
|
||||||
|
yOffset: section.yOffset ?? 0,
|
||||||
|
xOffset: section.xOffset ?? 0,
|
||||||
|
items: section.items ?? [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createItem = (item: Partial<Item>): Item => ({
|
||||||
|
id: item.id ?? "0",
|
||||||
|
width: item.width ?? 1,
|
||||||
|
height: item.height ?? 1,
|
||||||
|
yOffset: item.yOffset ?? 0,
|
||||||
|
xOffset: item.xOffset ?? 0,
|
||||||
|
kind: item.kind ?? "clock",
|
||||||
|
integrationIds: item.integrationIds ?? [],
|
||||||
|
options: item.options ?? {},
|
||||||
|
advancedOptions: item.advancedOptions ?? { customCssClasses: [] },
|
||||||
|
});
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import type { Modify } from "@homarr/common/types";
|
|
||||||
import { createId } from "@homarr/db/client";
|
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
|
||||||
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
||||||
|
|
||||||
import type { EmptySection, Item } from "~/app/[locale]/boards/_types";
|
import type { Item } from "~/app/[locale]/boards/_types";
|
||||||
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
|
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
|
||||||
|
import type { CreateItemInput } from "./actions/create-item";
|
||||||
|
import { createItemCallback } from "./actions/create-item";
|
||||||
|
import type { DuplicateItemInput } from "./actions/duplicate-item";
|
||||||
|
import { duplicateItemCallback } from "./actions/duplicate-item";
|
||||||
|
|
||||||
interface MoveAndResizeItem {
|
interface MoveAndResizeItem {
|
||||||
itemId: string;
|
itemId: string;
|
||||||
@@ -42,87 +43,19 @@ interface UpdateItemIntegrations {
|
|||||||
newIntegrations: string[];
|
newIntegrations: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateItem {
|
|
||||||
kind: WidgetKind;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DuplicateItem {
|
|
||||||
itemId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useItemActions = () => {
|
export const useItemActions = () => {
|
||||||
const { updateBoard } = useUpdateBoard();
|
const { updateBoard } = useUpdateBoard();
|
||||||
|
|
||||||
const createItem = useCallback(
|
const createItem = useCallback(
|
||||||
({ kind }: CreateItem) => {
|
(input: CreateItemInput) => {
|
||||||
updateBoard((previous) => {
|
updateBoard(createItemCallback(input));
|
||||||
const lastSection = previous.sections
|
|
||||||
.filter((section): section is EmptySection => section.kind === "empty")
|
|
||||||
.sort((sectionA, sectionB) => sectionB.yOffset - sectionA.yOffset)[0];
|
|
||||||
|
|
||||||
if (!lastSection) return previous;
|
|
||||||
|
|
||||||
const widget = {
|
|
||||||
id: createId(),
|
|
||||||
kind,
|
|
||||||
options: {},
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
integrationIds: [],
|
|
||||||
advancedOptions: {
|
|
||||||
customCssClasses: [],
|
|
||||||
},
|
|
||||||
} satisfies Modify<
|
|
||||||
Omit<Item, "yOffset" | "xOffset">,
|
|
||||||
{
|
|
||||||
kind: WidgetKind;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...previous,
|
|
||||||
sections: previous.sections.map((section) => {
|
|
||||||
// Return same section if item is not in it
|
|
||||||
if (section.id !== lastSection.id) return section;
|
|
||||||
return {
|
|
||||||
...section,
|
|
||||||
items: section.items.concat(widget as unknown as Item),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[updateBoard],
|
[updateBoard],
|
||||||
);
|
);
|
||||||
|
|
||||||
const duplicateItem = useCallback(
|
const duplicateItem = useCallback(
|
||||||
({ itemId }: DuplicateItem) => {
|
({ itemId }: DuplicateItemInput) => {
|
||||||
updateBoard((previous) => {
|
updateBoard(duplicateItemCallback({ itemId }));
|
||||||
const itemToDuplicate = previous.sections
|
|
||||||
.flatMap((section) => section.items)
|
|
||||||
.find((item) => item.id === itemId);
|
|
||||||
|
|
||||||
if (!itemToDuplicate) return previous;
|
|
||||||
|
|
||||||
const newItem = {
|
|
||||||
...itemToDuplicate,
|
|
||||||
id: createId(),
|
|
||||||
yOffset: undefined,
|
|
||||||
xOffset: undefined,
|
|
||||||
} satisfies Modify<Item, { yOffset?: number; xOffset?: number }>;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...previous,
|
|
||||||
sections: previous.sections.map((section) => {
|
|
||||||
// Return same section if item is not in it
|
|
||||||
if (!section.items.some((item) => item.id === itemId)) return section;
|
|
||||||
return {
|
|
||||||
...section,
|
|
||||||
items: section.items.concat(newItem as unknown as Item),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[updateBoard],
|
[updateBoard],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user