Compare commits
No commits in common. "137dc83bf845aa765cf8c2611cde4d642c5871f2" and "71ee7027399a88079de710c86f3b729039d216d3" have entirely different histories.
137dc83bf8
...
71ee702739
10 changed files with 97 additions and 206 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { View, Text, Pressable, TextInput, useColorScheme, Alert, RefreshControl } from 'react-native';
|
||||
import { View, Text, Pressable, TextInput, useColorScheme, Alert } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Plus, ArrowUpDown, Filter, Download, Search, X } from 'lucide-react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -44,7 +44,6 @@ export default function InboxScreen() {
|
|||
const theme = useSettingsStore((s) => s.theme);
|
||||
const isDark = (theme === 'system' ? systemScheme : theme) === 'dark';
|
||||
const isDraggingRef = useRef(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const { sortBy, sortOrder, filterPriority, filterTag, filterCompleted, filterDueDate, hasActiveFilters } = useTaskStore();
|
||||
|
||||
|
|
@ -71,12 +70,6 @@ export default function InboxScreen() {
|
|||
return () => clearInterval(interval);
|
||||
}, [loadTasks]);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
await loadTasks();
|
||||
setRefreshing(false);
|
||||
}, [loadTasks]);
|
||||
|
||||
const handleToggle = async (id: string) => {
|
||||
await toggleComplete(id);
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
|
|
@ -208,14 +201,6 @@ export default function InboxScreen() {
|
|||
onDragBegin={() => { isDraggingRef.current = true; }}
|
||||
onDragEnd={handleDragEnd}
|
||||
activationDistance={canDrag ? 0 : 10000}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={colors.bleu.DEFAULT}
|
||||
colors={[colors.bleu.DEFAULT]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</GestureHandlerRootView>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { View, Text, Pressable, TextInput, useColorScheme, Alert, RefreshControl } from 'react-native';
|
||||
import { View, Text, Pressable, TextInput, useColorScheme, Alert } from 'react-native';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import {
|
||||
ArrowLeft, Plus, ArrowUpDown, Filter, Download, Search, X,
|
||||
|
|
@ -61,7 +61,6 @@ export default function ListDetailScreen() {
|
|||
const theme = useSettingsStore((s) => s.theme);
|
||||
const isDark = (theme === 'system' ? systemScheme : theme) === 'dark';
|
||||
const isDraggingRef = useRef(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const { sortBy, sortOrder, filterPriority, filterTag, filterCompleted, filterDueDate, hasActiveFilters } = useTaskStore();
|
||||
|
||||
|
|
@ -96,12 +95,6 @@ export default function ListDetailScreen() {
|
|||
return () => clearInterval(interval);
|
||||
}, [loadData]);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
await loadData();
|
||||
setRefreshing(false);
|
||||
}, [loadData]);
|
||||
|
||||
const handleToggle = async (taskId: string) => {
|
||||
await toggleComplete(taskId);
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
|
|
@ -256,14 +249,6 @@ export default function ListDetailScreen() {
|
|||
onDragBegin={() => { isDraggingRef.current = true; }}
|
||||
onDragEnd={handleDragEnd}
|
||||
activationDistance={canDrag ? 0 : 10000}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={colors.bleu.DEFAULT}
|
||||
colors={[colors.bleu.DEFAULT]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</GestureHandlerRootView>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ type TaskData = {
|
|||
priority: number;
|
||||
dueDate: Date | null;
|
||||
listId: string;
|
||||
parentId: string | null;
|
||||
recurrence: string | null;
|
||||
};
|
||||
|
||||
|
|
@ -401,71 +400,67 @@ export default function TaskDetailScreen() {
|
|||
</>
|
||||
)}
|
||||
|
||||
{/* Subtasks — only for root tasks (not subtasks themselves) */}
|
||||
{!task?.parentId && (
|
||||
<>
|
||||
<Text className={`mb-2 mt-6 text-xs uppercase tracking-wide ${isDark ? 'text-[#A0A0A0]' : 'text-[#6B6B6B]'}`} style={{ fontFamily: 'Inter_600SemiBold' }}>
|
||||
{t('task.subtasks')}
|
||||
</Text>
|
||||
{subtasks.map((sub) => (
|
||||
<Pressable
|
||||
key={sub.id}
|
||||
onPress={() => editingSubtaskId === sub.id ? undefined : handleToggleSubtask(sub.id)}
|
||||
onLongPress={() => handleEditSubtask(sub)}
|
||||
className={`flex-row items-center border-b py-2.5 ${isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'}`}
|
||||
>
|
||||
<View
|
||||
className="mr-3 h-5 w-5 items-center justify-center rounded-full border-2"
|
||||
style={{
|
||||
borderColor: sub.completed ? colors.bleu.DEFAULT : colors.priority.none,
|
||||
backgroundColor: sub.completed ? colors.bleu.DEFAULT : 'transparent',
|
||||
}}
|
||||
>
|
||||
{sub.completed && <Text className="text-xs text-white" style={{ fontFamily: 'Inter_700Bold' }}>✓</Text>}
|
||||
</View>
|
||||
{editingSubtaskId === sub.id ? (
|
||||
<TextInput
|
||||
value={editingTitle}
|
||||
onChangeText={setEditingTitle}
|
||||
onSubmitEditing={handleSaveSubtaskEdit}
|
||||
onBlur={handleSaveSubtaskEdit}
|
||||
autoFocus
|
||||
className={`flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
className={`flex-1 text-base ${sub.completed ? 'line-through ' + (isDark ? 'text-[#A0A0A0]' : 'text-[#9CA3AF]') : isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
>
|
||||
{sub.title}
|
||||
</Text>
|
||||
)}
|
||||
<Pressable
|
||||
onPress={() => handleDeleteSubtask(sub.id)}
|
||||
className="ml-2 p-1.5"
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
>
|
||||
<X size={16} color={isDark ? '#A0A0A0' : '#9CA3AF'} />
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
))}
|
||||
|
||||
{/* Add subtask */}
|
||||
<View className="mt-2 flex-row items-center">
|
||||
<Plus size={18} color={colors.bleu.DEFAULT} />
|
||||
{/* Subtasks */}
|
||||
<Text className={`mb-2 mt-6 text-xs uppercase tracking-wide ${isDark ? 'text-[#A0A0A0]' : 'text-[#6B6B6B]'}`} style={{ fontFamily: 'Inter_600SemiBold' }}>
|
||||
{t('task.subtasks')}
|
||||
</Text>
|
||||
{subtasks.map((sub) => (
|
||||
<Pressable
|
||||
key={sub.id}
|
||||
onPress={() => editingSubtaskId === sub.id ? undefined : handleToggleSubtask(sub.id)}
|
||||
onLongPress={() => handleEditSubtask(sub)}
|
||||
className={`flex-row items-center border-b py-2.5 ${isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'}`}
|
||||
>
|
||||
<View
|
||||
className="mr-3 h-5 w-5 items-center justify-center rounded-full border-2"
|
||||
style={{
|
||||
borderColor: sub.completed ? colors.bleu.DEFAULT : colors.priority.none,
|
||||
backgroundColor: sub.completed ? colors.bleu.DEFAULT : 'transparent',
|
||||
}}
|
||||
>
|
||||
{sub.completed && <Text className="text-xs text-white" style={{ fontFamily: 'Inter_700Bold' }}>✓</Text>}
|
||||
</View>
|
||||
{editingSubtaskId === sub.id ? (
|
||||
<TextInput
|
||||
value={newSubtask}
|
||||
onChangeText={setNewSubtask}
|
||||
onSubmitEditing={handleAddSubtask}
|
||||
placeholder={t('task.addSubtask')}
|
||||
placeholderTextColor={isDark ? '#A0A0A0' : '#6B6B6B'}
|
||||
className={`ml-2 flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
value={editingTitle}
|
||||
onChangeText={setEditingTitle}
|
||||
onSubmitEditing={handleSaveSubtaskEdit}
|
||||
onBlur={handleSaveSubtaskEdit}
|
||||
autoFocus
|
||||
className={`flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
) : (
|
||||
<Text
|
||||
className={`flex-1 text-base ${sub.completed ? 'line-through ' + (isDark ? 'text-[#A0A0A0]' : 'text-[#9CA3AF]') : isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
>
|
||||
{sub.title}
|
||||
</Text>
|
||||
)}
|
||||
<Pressable
|
||||
onPress={() => handleDeleteSubtask(sub.id)}
|
||||
className="ml-2 p-1.5"
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
>
|
||||
<X size={16} color={isDark ? '#A0A0A0' : '#9CA3AF'} />
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
))}
|
||||
|
||||
{/* Add subtask */}
|
||||
<View className="mt-2 flex-row items-center">
|
||||
<Plus size={18} color={colors.bleu.DEFAULT} />
|
||||
<TextInput
|
||||
value={newSubtask}
|
||||
onChangeText={setNewSubtask}
|
||||
onSubmitEditing={handleAddSubtask}
|
||||
placeholder={t('task.addSubtask')}
|
||||
placeholderTextColor={isDark ? '#A0A0A0' : '#6B6B6B'}
|
||||
className={`ml-2 flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={{ height: 32 }} />
|
||||
</KeyboardAwareScrollView>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/db/client';
|
||||
import { slLists, slTasks, slTags, slTaskTags } from '@/db/schema';
|
||||
import { eq, and, gte, isNull } from 'drizzle-orm';
|
||||
import { eq, and, gte } from 'drizzle-orm';
|
||||
import { requireAuth, parseBody } from '@/lib/api';
|
||||
import { rateLimit } from '@/lib/rateLimit';
|
||||
import { syncPushSchema, type SyncOperation } from '@/lib/validators';
|
||||
|
|
@ -197,43 +197,15 @@ async function processOperation(op: SyncOperation, userId: string) {
|
|||
switch (entityType) {
|
||||
case 'list': {
|
||||
if (action === 'create') {
|
||||
const d = (data as Record<string, unknown>) || {};
|
||||
const incomingIsInbox = d.isInbox as boolean | undefined;
|
||||
|
||||
const listValues = {
|
||||
await db.insert(slLists).values({
|
||||
id: entityId,
|
||||
userId,
|
||||
name: d.name as string || 'Untitled',
|
||||
color: d.color as string | undefined,
|
||||
icon: d.icon as string | undefined,
|
||||
position: d.position as number | undefined,
|
||||
isInbox: incomingIsInbox,
|
||||
};
|
||||
|
||||
// If the incoming list is an inbox, check for an existing inbox and merge
|
||||
if (incomingIsInbox) {
|
||||
await db.transaction(async (tx) => {
|
||||
const [existingInbox] = await tx
|
||||
.select()
|
||||
.from(slLists)
|
||||
.where(and(eq(slLists.userId, userId), eq(slLists.isInbox, true), isNull(slLists.deletedAt)));
|
||||
|
||||
if (existingInbox && existingInbox.id !== entityId) {
|
||||
// Reassign all tasks (including subtasks) from the old inbox to the new one
|
||||
await tx.update(slTasks)
|
||||
.set({ listId: entityId, updatedAt: now })
|
||||
.where(and(eq(slTasks.listId, existingInbox.id), eq(slTasks.userId, userId)));
|
||||
// Soft-delete the old inbox
|
||||
await tx.update(slLists)
|
||||
.set({ deletedAt: now, updatedAt: now })
|
||||
.where(eq(slLists.id, existingInbox.id));
|
||||
}
|
||||
|
||||
await tx.insert(slLists).values(listValues).onConflictDoNothing();
|
||||
});
|
||||
} else {
|
||||
await db.insert(slLists).values(listValues).onConflictDoNothing();
|
||||
}
|
||||
name: (data as Record<string, unknown>)?.name as string || 'Untitled',
|
||||
color: (data as Record<string, unknown>)?.color as string | undefined,
|
||||
icon: (data as Record<string, unknown>)?.icon as string | undefined,
|
||||
position: (data as Record<string, unknown>)?.position as number | undefined,
|
||||
isInbox: (data as Record<string, unknown>)?.isInbox as boolean | undefined,
|
||||
}).onConflictDoNothing();
|
||||
} else if (action === 'update') {
|
||||
await verifyOwnership(slLists, entityId, userId);
|
||||
await db.update(slLists)
|
||||
|
|
|
|||
|
|
@ -25,20 +25,16 @@ export async function POST(request: Request) {
|
|||
return NextResponse.json({ error: 'List not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// If parentId, verify parent task belongs to user and is not itself a subtask
|
||||
// If parentId, verify parent task belongs to user
|
||||
if (body.data.parentId) {
|
||||
const [parent] = await db
|
||||
.select({ id: slTasks.id, parentId: slTasks.parentId })
|
||||
.select({ id: slTasks.id })
|
||||
.from(slTasks)
|
||||
.where(and(eq(slTasks.id, body.data.parentId), eq(slTasks.userId, auth.userId)));
|
||||
|
||||
if (!parent) {
|
||||
return NextResponse.json({ error: 'Parent task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (parent.parentId) {
|
||||
return NextResponse.json({ error: 'Cannot create sub-subtasks (max 2 levels)' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const [task] = await db
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
Calendar,
|
||||
Repeat,
|
||||
Check,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Task } from "@/lib/types";
|
||||
|
|
@ -41,7 +40,6 @@ export function TaskItem({ task, subtasks = [], depth = 0 }: TaskItemProps) {
|
|||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [title, setTitle] = useState(task.title);
|
||||
const [notes, setNotes] = useState(task.notes || "");
|
||||
|
|
@ -125,21 +123,17 @@ export function TaskItem({ task, subtasks = [], depth = 0 }: TaskItemProps) {
|
|||
>
|
||||
{/* Main row */}
|
||||
<div className="flex items-center gap-2 px-3 py-2">
|
||||
{/* Expand subtasks toggle — only shown when subtasks exist */}
|
||||
{subtasks.length > 0 ? (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="p-0.5 text-foreground/40 hover:text-foreground shrink-0"
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
<ChevronRight size={14} />
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<span className="w-[18px] shrink-0" />
|
||||
)}
|
||||
{/* Expand toggle */}
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="p-0.5 text-foreground/40 hover:text-foreground shrink-0"
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
<ChevronRight size={14} />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Checkbox */}
|
||||
<button
|
||||
|
|
@ -153,12 +147,12 @@ export function TaskItem({ task, subtasks = [], depth = 0 }: TaskItemProps) {
|
|||
{task.completed && <Check size={12} />}
|
||||
</button>
|
||||
|
||||
{/* Title — click opens detail */}
|
||||
{/* Title */}
|
||||
<span
|
||||
className={`flex-1 text-sm cursor-pointer ${
|
||||
task.completed ? "line-through text-foreground/50" : ""
|
||||
}`}
|
||||
onClick={() => setDetailOpen(!detailOpen)}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{task.title}
|
||||
</span>
|
||||
|
|
@ -180,20 +174,10 @@ export function TaskItem({ task, subtasks = [], depth = 0 }: TaskItemProps) {
|
|||
{subtasks.filter((s) => s.completed).length}/{subtasks.length}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Detail view toggle */}
|
||||
<button
|
||||
onClick={() => setDetailOpen(!detailOpen)}
|
||||
className={`p-0.5 shrink-0 transition-colors ${
|
||||
detailOpen ? "text-bleu" : "text-foreground/30 hover:text-foreground/60"
|
||||
}`}
|
||||
>
|
||||
<Search size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Detail view */}
|
||||
{detailOpen && (
|
||||
{/* Expanded view */}
|
||||
{expanded && (
|
||||
<div className="px-3 pb-3 pt-1 border-t border-border-light dark:border-border-dark">
|
||||
{editing ? (
|
||||
<div className="space-y-2">
|
||||
|
|
@ -277,14 +261,12 @@ export function TaskItem({ task, subtasks = [], depth = 0 }: TaskItemProps) {
|
|||
>
|
||||
{t("task.edit")}
|
||||
</button>
|
||||
{depth < 1 && (
|
||||
<button
|
||||
onClick={() => setShowSubtaskForm(!showSubtaskForm)}
|
||||
className="text-xs text-bleu hover:underline"
|
||||
>
|
||||
{t("task.addSubtask")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowSubtaskForm(!showSubtaskForm)}
|
||||
className="text-xs text-bleu hover:underline"
|
||||
>
|
||||
{t("task.addSubtask")}
|
||||
</button>
|
||||
<button
|
||||
onClick={deleteTask}
|
||||
className="text-xs text-rouge hover:underline flex items-center gap-1"
|
||||
|
|
@ -300,7 +282,7 @@ export function TaskItem({ task, subtasks = [], depth = 0 }: TaskItemProps) {
|
|||
</div>
|
||||
|
||||
{/* Subtask form */}
|
||||
{showSubtaskForm && detailOpen && (
|
||||
{showSubtaskForm && expanded && (
|
||||
<div style={{ marginLeft: 24 }} className="mb-1.5">
|
||||
<TaskForm
|
||||
listId={task.listId}
|
||||
|
|
@ -310,8 +292,8 @@ export function TaskItem({ task, subtasks = [], depth = 0 }: TaskItemProps) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Subtasks — toggled by chevron */}
|
||||
{expanded && subtasks.map((sub) => (
|
||||
{/* Subtasks */}
|
||||
{subtasks.map((sub) => (
|
||||
<TaskItem key={sub.id} task={sub} depth={depth + 1} />
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import type { Task } from "@/lib/types";
|
|||
import { TaskItem } from "./TaskItem";
|
||||
import { TaskForm } from "./TaskForm";
|
||||
import { FilterBar } from "./FilterBar";
|
||||
import { ClipboardList, RefreshCw } from "lucide-react";
|
||||
import { Suspense, useState, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ClipboardList } from "lucide-react";
|
||||
import { Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface TaskListProps {
|
||||
|
|
@ -18,31 +17,12 @@ interface TaskListProps {
|
|||
|
||||
export function TaskList({ tasks, subtasksMap, listId, listName }: TaskListProps) {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
router.refresh();
|
||||
// Brief visual feedback
|
||||
setTimeout(() => setRefreshing(false), 500);
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto w-full">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-xl font-semibold">{listName}</h2>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className="p-1.5 text-foreground/40 hover:text-foreground transition-colors disabled:opacity-50"
|
||||
title={t("task.refresh")}
|
||||
>
|
||||
<RefreshCw size={18} className={refreshing ? "animate-spin" : ""} />
|
||||
</button>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-3">{listName}</h2>
|
||||
<Suspense fallback={null}>
|
||||
<FilterBar />
|
||||
</Suspense>
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ async function seed() {
|
|||
.where(and(eq(slLists.userId, userId), eq(slLists.isInbox, true)));
|
||||
|
||||
if (existing.length === 0) {
|
||||
// Let the DB generate a random UUID — the sync endpoint handles
|
||||
// inbox deduplication when mobile pushes its fixed-ID inbox.
|
||||
await db.insert(slLists).values({
|
||||
userId,
|
||||
name: 'Inbox',
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
"subtaskPlaceholder": "New subtask...",
|
||||
"notesPlaceholder": "Notes...",
|
||||
"empty": "No tasks",
|
||||
"refresh": "Refresh",
|
||||
"edit": "Edit",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
"subtaskPlaceholder": "Nouvelle sous-tâche...",
|
||||
"notesPlaceholder": "Notes...",
|
||||
"empty": "Aucune tâche",
|
||||
"refresh": "Rafraîchir",
|
||||
"edit": "Modifier",
|
||||
"save": "Enregistrer",
|
||||
"cancel": "Annuler",
|
||||
|
|
|
|||
Loading…
Reference in a new issue