diff --git a/app/components/Edit/EditTodoSheet.vue b/app/components/Edit/EditTodoSheet.vue index 53d8ffb..36e2d87 100644 --- a/app/components/Edit/EditTodoSheet.vue +++ b/app/components/Edit/EditTodoSheet.vue @@ -49,6 +49,8 @@ const { mutate: updateTodo } = useUpdateTodo(); async function onSaveTodo() { + if (!isValid.value) return; + updateTodo({ uuid: uuid.value, todoData: todoData.value }); showSaveIcon.value = false; @@ -65,7 +67,9 @@ } const isValid = computed(() => { - return todoData.value.title !== ""; + if (todoData.value.title === "") return false; + + return !isTodoSame(todoData.value, uuid.value); }); const { mutate: deleteTodo } = useDeleteTodo(); diff --git a/app/composables/useTodoUtils.ts b/app/composables/useTodoUtils.ts index bb54d45..1c17076 100644 --- a/app/composables/useTodoUtils.ts +++ b/app/composables/useTodoUtils.ts @@ -1,5 +1,46 @@ +import { dequal } from "dequal"; + +import { toRaw } from "vue"; import { useTodosQuery } from "~/queries/todo"; +export function isTodoSame(todo: CreateTodoData, uuid: UUID): boolean { + const otherTodo = useTodoById(uuid); + + // 1. Guard against missing target + if (!otherTodo) return false; + + // 2. Primitive comparisons + if ((todo.title ?? "") !== (otherTodo.title ?? "")) return false; + if ((todo.note ?? "") !== (otherTodo.note ?? "")) return false; + + // 3. Complex Object comparison (handles key order + reactive proxies) + const rawTimeA = todo.time ? toRaw(todo.time) : undefined; + const rawTimeB = otherTodo.time ? toRaw(otherTodo.time) : undefined; + if (!dequal(rawTimeA, rawTimeB)) return false; + + // 4. Category check + if ((todo.category ?? undefined) !== (otherTodo.category?.uuid ?? undefined)) + return false; + + // 5. Tags comparison + const tagsA = todo.tags ?? []; + const tagsB = otherTodo.tags?.map((t) => t.uuid) ?? []; + + if (tagsA.length !== tagsB.length) return false; + const tagSetB = new Set(tagsB); + if (!tagsA.every((tag) => tagSetB.has(tag))) return false; + + // 6. Reminders comparison check if exists + if ( + (todo.reminders?.length ?? 0) !== 0 || + (otherTodo?.reminders?.length ?? 0) !== 0 + ) { + return false; + } + + return true; +} + export function useTodoById(uuid: UUID) { const queryCache = useQueryCache(); const todos = queryCache.getQueryData(useTodosQuery().key); diff --git a/flake.nix b/flake.nix index 5dbbbaf..b0eca03 100644 --- a/flake.nix +++ b/flake.nix @@ -88,7 +88,7 @@ inherit (finalAttrs) pname version src; inherit pnpm; fetcherVersion = 4; - hash = "sha256-+Ij7VYLQeamCaoQntmfaDLDOJ14+upzUg/CDcLkPImI="; + hash = "sha256-hlTOJkw0FEHnEsc4DaW3CmQHQV82BObQgh3cRD0BI0A="; }; buildPhase = '' diff --git a/package.json b/package.json index e2c88ed..cdaa34f 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@pinia/nuxt": "^1.0.2", "@vueuse/nuxt": "^14.4.0", "better-auth": "^1.7.4", + "dequal": "^2.0.3", "marked": "^18.0.12", "nuxt": "^4.5.2", "pg": "^8.23.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79b229e..176e3ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: better-auth: specifier: ^1.7.4 version: 1.7.4(pg@8.23.0)(vitest@4.1.11(@types/node@22.20.2)(happy-dom@20.14.3)(vite@8.3.0(@types/node@22.20.2)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(yaml@2.9.0)))(vue@3.5.42(typescript@6.0.3)) + dequal: + specifier: ^2.0.3 + version: 2.0.3 marked: specifier: ^18.0.12 version: 18.0.12 @@ -2701,6 +2704,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -7678,6 +7685,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + destr@2.0.5: {} detect-libc@2.1.2: {}