feat: add dynamic section (#842)
* chore: add parent_section_id and change position to x and y_offset for sqlite section table * chore: rename existing positions to x_offset and y_offset * chore: add related mysql migration * chore: add missing height and width to section table * fix: missing width and height in migration copy script * fix: typecheck issues * fix: test not working caused by unsimilar schemas * wip: add dynamic section * refactor: improve structure of gridstack sections * feat: add rendering of dynamic sections * feat: add saving of moved sections * wip: add static row count, restrict min-width and height * chore: address pull request feedback * fix: format issues * fix: size calculation within dynamic sections * fix: on resize not called when min width or height is reached * fix: size of items while dragging is to big * chore: temporarly remove migration files * chore: readd migrations * fix: format and deepsource issues * chore: remove db_dev.sqlite file * chore: add *.sqlite to .gitignore * chore: address pull request feedback * feat: add dynamic section actions for adding and removing them
This commit is contained in:
@@ -109,7 +109,8 @@ export const boardRouter = createTRPCRouter({
|
||||
await transaction.insert(sections).values({
|
||||
id: createId(),
|
||||
kind: "empty",
|
||||
position: 0,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
boardId,
|
||||
});
|
||||
});
|
||||
@@ -206,7 +207,11 @@ export const boardRouter = createTRPCRouter({
|
||||
addedSections.map((section) => ({
|
||||
id: section.id,
|
||||
kind: section.kind,
|
||||
position: section.position,
|
||||
yOffset: section.yOffset,
|
||||
xOffset: section.kind === "dynamic" ? section.xOffset : 0,
|
||||
height: "height" in section ? section.height : null,
|
||||
width: "width" in section ? section.width : null,
|
||||
parentSectionId: "parentSectionId" in section ? section.parentSectionId : null,
|
||||
name: "name" in section ? section.name : null,
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
@@ -292,7 +297,11 @@ export const boardRouter = createTRPCRouter({
|
||||
await transaction
|
||||
.update(sections)
|
||||
.set({
|
||||
position: section.position,
|
||||
yOffset: section.yOffset,
|
||||
xOffset: section.xOffset,
|
||||
height: prev?.kind === "dynamic" && "height" in section ? section.height : null,
|
||||
width: prev?.kind === "dynamic" && "width" in section ? section.width : null,
|
||||
parentSectionId: prev?.kind === "dynamic" && "parentSectionId" in section ? section.parentSectionId : null,
|
||||
name: prev?.kind === "category" && "name" in section ? section.name : null,
|
||||
})
|
||||
.where(eq(sections.id, section.id));
|
||||
@@ -538,6 +547,7 @@ const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kin
|
||||
|
||||
const parseSection = (section: unknown) => {
|
||||
const result = createSectionSchema(outputItemSchema).safeParse(section);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
|
||||
@@ -619,7 +619,8 @@ describe("saveBoard should save full board", () => {
|
||||
{
|
||||
id: createId(),
|
||||
kind: "empty",
|
||||
position: 0,
|
||||
yOffset: 0,
|
||||
xOffset: 0,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
@@ -655,7 +656,8 @@ describe("saveBoard should save full board", () => {
|
||||
{
|
||||
id: sectionId,
|
||||
kind: "empty",
|
||||
position: 0,
|
||||
yOffset: 0,
|
||||
xOffset: 0,
|
||||
items: [
|
||||
{
|
||||
id: createId(),
|
||||
@@ -716,7 +718,8 @@ describe("saveBoard should save full board", () => {
|
||||
{
|
||||
id: sectionId,
|
||||
kind: "empty",
|
||||
position: 0,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
items: [
|
||||
{
|
||||
id: itemId,
|
||||
@@ -778,14 +781,16 @@ describe("saveBoard should save full board", () => {
|
||||
sections: [
|
||||
{
|
||||
id: newSectionId,
|
||||
position: 1,
|
||||
xOffset: 0,
|
||||
yOffset: 1,
|
||||
items: [],
|
||||
...partialSection,
|
||||
},
|
||||
{
|
||||
id: sectionId,
|
||||
kind: "empty",
|
||||
position: 0,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
@@ -808,7 +813,7 @@ describe("saveBoard should save full board", () => {
|
||||
expect(addedSection).toBeDefined();
|
||||
expect(addedSection.id).toBe(newSectionId);
|
||||
expect(addedSection.kind).toBe(partialSection.kind);
|
||||
expect(addedSection.position).toBe(1);
|
||||
expect(addedSection.yOffset).toBe(1);
|
||||
if ("name" in partialSection) {
|
||||
expect(addedSection.name).toBe(partialSection.name);
|
||||
}
|
||||
@@ -830,7 +835,8 @@ describe("saveBoard should save full board", () => {
|
||||
{
|
||||
id: sectionId,
|
||||
kind: "empty",
|
||||
position: 0,
|
||||
yOffset: 0,
|
||||
xOffset: 0,
|
||||
items: [
|
||||
{
|
||||
id: newItemId,
|
||||
@@ -899,7 +905,8 @@ describe("saveBoard should save full board", () => {
|
||||
{
|
||||
id: sectionId,
|
||||
kind: "empty",
|
||||
position: 0,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
items: [
|
||||
{
|
||||
id: itemId,
|
||||
@@ -956,7 +963,8 @@ describe("saveBoard should save full board", () => {
|
||||
id: newSectionId,
|
||||
kind: "category",
|
||||
name: "Before",
|
||||
position: 1,
|
||||
yOffset: 1,
|
||||
xOffset: 0,
|
||||
boardId,
|
||||
});
|
||||
|
||||
@@ -966,7 +974,8 @@ describe("saveBoard should save full board", () => {
|
||||
{
|
||||
id: sectionId,
|
||||
kind: "category",
|
||||
position: 1,
|
||||
yOffset: 1,
|
||||
xOffset: 0,
|
||||
name: "Test",
|
||||
items: [],
|
||||
},
|
||||
@@ -974,7 +983,8 @@ describe("saveBoard should save full board", () => {
|
||||
id: newSectionId,
|
||||
kind: "category",
|
||||
name: "After",
|
||||
position: 0,
|
||||
yOffset: 0,
|
||||
xOffset: 0,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
@@ -992,12 +1002,12 @@ describe("saveBoard should save full board", () => {
|
||||
const firstSection = expectToBeDefined(definedBoard.sections.find((section) => section.id === sectionId));
|
||||
expect(firstSection.id).toBe(sectionId);
|
||||
expect(firstSection.kind).toBe("empty");
|
||||
expect(firstSection.position).toBe(1);
|
||||
expect(firstSection.yOffset).toBe(1);
|
||||
expect(firstSection.name).toBe(null);
|
||||
const secondSection = expectToBeDefined(definedBoard.sections.find((section) => section.id === newSectionId));
|
||||
expect(secondSection.id).toBe(newSectionId);
|
||||
expect(secondSection.kind).toBe("category");
|
||||
expect(secondSection.position).toBe(0);
|
||||
expect(secondSection.yOffset).toBe(0);
|
||||
expect(secondSection.name).toBe("After");
|
||||
});
|
||||
it("should update item when present in input", async () => {
|
||||
@@ -1013,7 +1023,8 @@ describe("saveBoard should save full board", () => {
|
||||
{
|
||||
id: sectionId,
|
||||
kind: "empty",
|
||||
position: 0,
|
||||
yOffset: 0,
|
||||
xOffset: 0,
|
||||
items: [
|
||||
{
|
||||
id: itemId,
|
||||
@@ -1268,7 +1279,8 @@ const createFullBoardAsync = async (db: Database, name: string) => {
|
||||
await db.insert(sections).values({
|
||||
id: sectionId,
|
||||
kind: "empty",
|
||||
position: 0,
|
||||
yOffset: 0,
|
||||
xOffset: 0,
|
||||
boardId,
|
||||
});
|
||||
|
||||
|
||||
6
packages/db/migrations/mysql/0006_young_micromax.sql
Normal file
6
packages/db/migrations/mysql/0006_young_micromax.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE `section` RENAME COLUMN `position` TO `y_offset`;--> statement-breakpoint
|
||||
ALTER TABLE `section` ADD `x_offset` int NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `section` ADD `width` int;--> statement-breakpoint
|
||||
ALTER TABLE `section` ADD `height` int;--> statement-breakpoint
|
||||
ALTER TABLE `section` ADD `parent_section_id` text;--> statement-breakpoint
|
||||
ALTER TABLE `section` ADD CONSTRAINT `section_parent_section_id_section_id_fk` FOREIGN KEY (`parent_section_id`) REFERENCES `section`(`id`) ON DELETE cascade ON UPDATE no action;
|
||||
1367
packages/db/migrations/mysql/meta/0006_snapshot.json
Normal file
1367
packages/db/migrations/mysql/meta/0006_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,13 @@
|
||||
"when": 1722068832607,
|
||||
"tag": "0005_soft_microbe",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "5",
|
||||
"when": 1722517058725,
|
||||
"tag": "0006_young_micromax",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
35
packages/db/migrations/sqlite/0006_windy_doctor_faustus.sql
Normal file
35
packages/db/migrations/sqlite/0006_windy_doctor_faustus.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
COMMIT TRANSACTION;
|
||||
--> statement-breakpoint
|
||||
PRAGMA foreign_keys = OFF;
|
||||
--> statement-breakpoint
|
||||
BEGIN TRANSACTION;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `section` RENAME TO `__section_old`;
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `section` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`board_id` text NOT NULL,
|
||||
`kind` text NOT NULL,
|
||||
`x_offset` integer NOT NULL,
|
||||
`y_offset` integer NOT NULL,
|
||||
`width` integer,
|
||||
`height` integer,
|
||||
`name` text,
|
||||
`parent_section_id` text,
|
||||
FOREIGN KEY (`board_id`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
FOREIGN KEY (`parent_section_id`) REFERENCES `section`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `section` SELECT `id`, `board_id`, `kind`, 0, `position`, null, null, `name`, null FROM `__section_old`;
|
||||
--> statement-breakpoint
|
||||
DROP TABLE `__section_old`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `section` RENAME TO `__section_old`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `__section_old` RENAME TO `section`;
|
||||
--> statement-breakpoint
|
||||
COMMIT TRANSACTION;
|
||||
--> statement-breakpoint
|
||||
PRAGMA foreign_keys = ON;
|
||||
--> statement-breakpoint
|
||||
BEGIN TRANSACTION;
|
||||
1310
packages/db/migrations/sqlite/meta/0006_snapshot.json
Normal file
1310
packages/db/migrations/sqlite/meta/0006_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,13 @@
|
||||
"when": 1722014142492,
|
||||
"tag": "0005_lean_random",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "6",
|
||||
"when": 1722517033483,
|
||||
"tag": "0006_windy_doctor_faustus",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -269,8 +269,14 @@ export const sections = mysqlTable("section", {
|
||||
.notNull()
|
||||
.references(() => boards.id, { onDelete: "cascade" }),
|
||||
kind: text("kind").$type<SectionKind>().notNull(),
|
||||
position: int("position").notNull(),
|
||||
xOffset: int("x_offset").notNull(),
|
||||
yOffset: int("y_offset").notNull(),
|
||||
width: int("width"),
|
||||
height: int("height"),
|
||||
name: text("name"),
|
||||
parentSectionId: text("parent_section_id").references((): AnyMySqlColumn => sections.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const items = mysqlTable("item", {
|
||||
|
||||
@@ -272,8 +272,14 @@ export const sections = sqliteTable("section", {
|
||||
.notNull()
|
||||
.references(() => boards.id, { onDelete: "cascade" }),
|
||||
kind: text("kind").$type<SectionKind>().notNull(),
|
||||
position: int("position").notNull(),
|
||||
xOffset: int("x_offset").notNull(),
|
||||
yOffset: int("y_offset").notNull(),
|
||||
width: int("width"),
|
||||
height: int("height"),
|
||||
name: text("name"),
|
||||
parentSectionId: text("parent_section_id").references((): AnySQLiteColumn => sections.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const items = sqliteTable("item", {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const sectionKinds = ["category", "empty"] as const;
|
||||
export const sectionKinds = ["category", "empty", "dynamic"] as const;
|
||||
export type SectionKind = (typeof sectionKinds)[number];
|
||||
|
||||
@@ -634,6 +634,17 @@ export default {
|
||||
mantineReactTable: MRT_Localization_EN,
|
||||
},
|
||||
section: {
|
||||
dynamic: {
|
||||
action: {
|
||||
create: "New dynamic section",
|
||||
remove: "Remove dynamic section",
|
||||
},
|
||||
remove: {
|
||||
title: "Remove dynamic section",
|
||||
message:
|
||||
"Are you sure you want to remove this dynamic section? Items will be moved at the same location in the parent section.",
|
||||
},
|
||||
},
|
||||
category: {
|
||||
field: {
|
||||
name: {
|
||||
|
||||
@@ -41,7 +41,8 @@ const createCategorySchema = <TItemSchema extends z.ZodTypeAny>(itemSchema: TIte
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
kind: z.literal("category"),
|
||||
position: z.number(),
|
||||
yOffset: z.number(),
|
||||
xOffset: z.number(),
|
||||
items: z.array(itemSchema),
|
||||
});
|
||||
|
||||
@@ -49,9 +50,22 @@ const createEmptySchema = <TItemSchema extends z.ZodTypeAny>(itemSchema: TItemSc
|
||||
z.object({
|
||||
id: z.string(),
|
||||
kind: z.literal("empty"),
|
||||
position: z.number(),
|
||||
yOffset: z.number(),
|
||||
xOffset: z.number(),
|
||||
items: z.array(itemSchema),
|
||||
});
|
||||
|
||||
const createDynamicSchema = <TItemSchema extends z.ZodTypeAny>(itemSchema: TItemSchema) =>
|
||||
z.object({
|
||||
id: z.string(),
|
||||
kind: z.literal("dynamic"),
|
||||
yOffset: z.number(),
|
||||
xOffset: z.number(),
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
items: z.array(itemSchema),
|
||||
parentSectionId: z.string(),
|
||||
});
|
||||
|
||||
export const createSectionSchema = <TItemSchema extends z.ZodTypeAny>(itemSchema: TItemSchema) =>
|
||||
z.union([createCategorySchema(itemSchema), createEmptySchema(itemSchema)]);
|
||||
z.union([createCategorySchema(itemSchema), createEmptySchema(itemSchema), createDynamicSchema(itemSchema)]);
|
||||
|
||||
Reference in New Issue
Block a user