diff --git a/package.json b/package.json index 4c62b34..53524fe 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "autoprefixer": "^10.4.16", "postcss": "^8.4.31", "tailwindcss": "^3.4.1", - "unocss": "^0.58.5" + "unocss": "^0.58.5", + "vitest": "^3.2.2" }, "dependencies": { "@formkit/auto-animate": "^0.8.1", diff --git a/src/components/Board.tsx b/src/components/Board.tsx index f73f402..dab7b9d 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -1,5 +1,5 @@ -import { Action, useSubmissions } from "@solidjs/router"; -import { For, batch, createEffect, createMemo, untrack } from "solid-js"; +import { useSubmissions } from "@solidjs/router"; +import { For, batch, createEffect, mapArray, untrack } from "solid-js"; import { createStore, produce, reconcile } from "solid-js/store"; import { AddColumn, @@ -19,6 +19,7 @@ import { editNote, moveNote, } from "./Note"; +import { sortIntoArray } from "~/lib/utils"; export enum DragTypes { Note = "application/note", @@ -211,11 +212,6 @@ export function Board(props: { board: BoardData }) { const { notes, columns } = props.board; applyMutations(mutations, notes, columns); - console.log( - `got server data, reset the board with mutations`, - ...mutations - ); - batch(() => { setBoardStore("notes", reconcile(notes)); setBoardStore("columns", reconcile(columns)); @@ -230,12 +226,7 @@ export function Board(props: { board: BoardData }) { (m) => m.timestamp > prevTimestamp ); - console.log( - `found submission, apply optimistic update with mutations`, - ...latestMutations - ); - - if (!optimisticUpdates) return console.log(`Skipping optimistic update`); + if (!optimisticUpdates) return; setBoardStore( produce((b) => { @@ -245,10 +236,18 @@ export function Board(props: { board: BoardData }) { ); }); - const sortedColumns = createMemo(() => - boardStore.columns.slice().sort((a, b) => a.order - b.order) + const [sortedColumns, setSortedColumns] = createStore([]); + const mapped = mapArray( + () => boardStore.columns, + (column) => { + createEffect(() => { + setSortedColumns(produce((f) => sortIntoArray(f, column))); + }); + } ); + createEffect(() => mapped()); + let scrollContainerRef: HTMLDivElement | undefined; return ( @@ -258,8 +257,8 @@ export function Board(props: { board: BoardData }) { }} class="pb-8 h-[calc(100vh-160px)] min-w-full overflow-x-auto overflow-y-hidden flex flex-start items-start flex-nowrap" > - - + + {(column, i) => ( <> )} diff --git a/src/components/Column.tsx b/src/components/Column.tsx index cca6d73..f2f1e88 100644 --- a/src/components/Column.tsx +++ b/src/components/Column.tsx @@ -5,16 +5,18 @@ import { For, Match, Switch, - createMemo, + createEffect, createSignal, + mapArray, onMount, } from "solid-js"; import { type Board, type BoardId, DragTypes } from "./Board"; -import { getIndexBetween } from "~/lib/utils"; +import { getIndexBetween, sortIntoArray } from "~/lib/utils"; import { AddNote, Note, NoteId, moveNote } from "./Note"; import { getAuthUser } from "~/lib/auth"; import { db } from "~/lib/db"; import { fetchBoard } from "~/lib"; +import { createStore, produce } from "solid-js/store"; export const renameColumn = action( async (id: ColumnId, name: string, timestamp: number) => { @@ -97,12 +99,30 @@ export function Column(props: { column: Column; board: Board; notes: Note[] }) { const [acceptDrop, setAcceptDrop] = createSignal(false); - const filteredNotes = createMemo(() => - props.notes - .filter((n) => n.column === props.column.id) - .sort((a, b) => a.order - b.order) + const [filteredNotes, setFilteredNotes] = createStore([]); + + const mapped = mapArray( + () => props.notes, + (note) => { + createEffect(() => { + setFilteredNotes( + produce((f) => { + if (note.column === props.column.id) { + sortIntoArray(f, note); + } else { + const index = f.findIndex((n) => n.id === note.id); + if (index !== -1) { + f.splice(index, 1); + } + } + }) + ); + }); + } ); + createEffect(() => mapped()); + return (
n.id === noteId)) { + if (noteId && !filteredNotes.find((n) => n.id === noteId)) { moveNoteAction( noteId, props.column.id, getIndexBetween( - filteredNotes()[filteredNotes().length - 1]?.order, + filteredNotes[filteredNotes.length - 1]?.order, undefined ), new Date().getTime() @@ -180,12 +200,12 @@ export function Column(props: { column: Column; board: Board; notes: Note[] }) { class="flex h-full flex-col space-y-2 overflow-y-auto px-1" ref={parent} > - + {(n, i) => ( )} diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts new file mode 100644 index 0000000..b920981 --- /dev/null +++ b/src/lib/utils.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect } from "vitest"; +import { sortIntoArray } from "./utils"; + +describe("sortIntoArray", () => { + type TestItem = { + id: string; + order: number; + data?: string; + }; + + it("should add item to empty array", () => { + const array: TestItem[] = []; + const item: TestItem = { id: "1", order: 1 }; + + sortIntoArray(array, item); + expect(array).toEqual([{ id: "1", order: 1 }]); + }); + + it("should insert item in correct order position", () => { + const array: TestItem[] = [ + { id: "1", order: 1 }, + { id: "3", order: 3 }, + ]; + const item: TestItem = { id: "2", order: 2 }; + + sortIntoArray(array, item); + expect(array).toEqual([ + { id: "1", order: 1 }, + { id: "2", order: 2 }, + { id: "3", order: 3 }, + ]); + }); + + it("should update existing item and maintain order", () => { + const array: TestItem[] = [ + { id: "1", order: 1 }, + { id: "2", order: 2 }, + { id: "3", order: 3 }, + ]; + const item: TestItem = { id: "2", order: 4, data: "updated" }; + + sortIntoArray(array, item); + expect(array).toEqual([ + { id: "1", order: 1 }, + { id: "3", order: 3 }, + { id: "2", order: 4, data: "updated" }, + ]); + }); + + it("should append item with highest order", () => { + const array: TestItem[] = [ + { id: "1", order: 1 }, + { id: "2", order: 2 }, + ]; + const item: TestItem = { id: "3", order: 3 }; + + sortIntoArray(array, item); + expect(array).toEqual([ + { id: "1", order: 1 }, + { id: "2", order: 2 }, + { id: "3", order: 3 }, + ]); + }); + + it("should insert item with lowest order", () => { + const array: TestItem[] = [ + { id: "2", order: 2 }, + { id: "3", order: 3 }, + ]; + const item: TestItem = { id: "1", order: 1 }; + + sortIntoArray(array, item); + expect(array).toEqual([ + { id: "1", order: 1 }, + { id: "2", order: 2 }, + { id: "3", order: 3 }, + ]); + }); + + it("should handle items with same order", () => { + const array: TestItem[] = [ + { id: "1", order: 1 }, + { id: "2", order: 1 }, + ]; + const item: TestItem = { id: "3", order: 1 }; + + sortIntoArray(array, item); + expect(array).toEqual([ + { id: "1", order: 1 }, + { id: "2", order: 1 }, + { id: "3", order: 1 }, + ]); + }); + + it("should preserve additional properties when updating", () => { + const array: TestItem[] = [{ id: "1", order: 1, data: "original" }]; + const item: TestItem = { id: "1", order: 2, data: "updated" }; + + sortIntoArray(array, item); + expect(array).toEqual([{ id: "1", order: 2, data: "updated" }]); + }); +}); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 17efc2b..836dbd3 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -29,3 +29,31 @@ export const getIndexBetween = ( below: number | undefined, above: number | undefined ) => getIndicesBetween(below, above, 1)[0]; + +export function sortIntoArray( + array: T[], + item: T +) { + if (array.length === 0) { + array.push(item); + } else { + const index = array.findIndex((n) => n.id === item.id); + + if (index !== -1) { + array.splice(index, 1); + } + + let inserted = false; + for (let i = 0; i < array.length; i++) { + if (array[i].order > item.order) { + array.splice(i, 0, item); + inserted = true; + break; + } + } + + if (!inserted) { + array.push(item); + } + } +}