This commit is contained in:
@@ -464,6 +464,8 @@ async def create_task(
|
|||||||
@app.get("/api/v1/tasks", response_model=TaskPage)
|
@app.get("/api/v1/tasks", response_model=TaskPage)
|
||||||
async def list_tasks(
|
async def list_tasks(
|
||||||
q: str | None = None,
|
q: str | None = None,
|
||||||
|
list_id: UUID | None = None,
|
||||||
|
completed: bool | None = None,
|
||||||
cursor: str | None = None,
|
cursor: str | None = None,
|
||||||
limit: int = Query(default=50, ge=1, le=100),
|
limit: int = Query(default=50, ge=1, le=100),
|
||||||
user: User = Depends(current_user),
|
user: User = Depends(current_user),
|
||||||
@@ -472,6 +474,11 @@ async def list_tasks(
|
|||||||
query = select(Task).where(
|
query = select(Task).where(
|
||||||
Task.user_id == user.id, Task.deleted_at.is_(None), Task.parent_id.is_(None)
|
Task.user_id == user.id, Task.deleted_at.is_(None), Task.parent_id.is_(None)
|
||||||
)
|
)
|
||||||
|
if list_id is not None:
|
||||||
|
await _owned_list(db, user.id, list_id)
|
||||||
|
query = query.where(Task.list_id == list_id)
|
||||||
|
if completed is not None:
|
||||||
|
query = query.where(Task.completed.is_(completed))
|
||||||
if q:
|
if q:
|
||||||
pattern = f"%{q}%"
|
pattern = f"%{q}%"
|
||||||
tag_match = exists(
|
tag_match = exists(
|
||||||
|
|||||||
+13
-5
@@ -35,7 +35,7 @@ const loading = ref(false)
|
|||||||
const mobileSidebar = ref(false)
|
const mobileSidebar = ref(false)
|
||||||
const mobileDetail = ref(false)
|
const mobileDetail = ref(false)
|
||||||
const markdownPreview = ref(false)
|
const markdownPreview = ref(false)
|
||||||
const showCompleted = ref(true)
|
const showCompleted = ref(false)
|
||||||
const expandedFolders = ref(new Set<string>())
|
const expandedFolders = ref(new Set<string>())
|
||||||
|
|
||||||
const activeName = computed(() => {
|
const activeName = computed(() => {
|
||||||
@@ -70,6 +70,9 @@ watch(query, () => {
|
|||||||
if (activeView.value === 'trash') return
|
if (activeView.value === 'trash') return
|
||||||
searchTimer = window.setTimeout(() => loadAll(), 250)
|
searchTimer = window.setTimeout(() => loadAll(), 250)
|
||||||
})
|
})
|
||||||
|
watch(showCompleted, () => {
|
||||||
|
if (activeView.value !== 'trash') loadAll()
|
||||||
|
})
|
||||||
|
|
||||||
async function api(path: string, options: RequestInit = {}) {
|
async function api(path: string, options: RequestInit = {}) {
|
||||||
const response = await fetch('/api/v1' + path, {
|
const response = await fetch('/api/v1' + path, {
|
||||||
@@ -125,12 +128,16 @@ async function loadTaskPages(path: string) {
|
|||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
loading.value = true; error.value = ''
|
loading.value = true; error.value = ''
|
||||||
try {
|
try {
|
||||||
const taskPath = query.value ? `/tasks?q=${encodeURIComponent(query.value)}` : '/tasks'
|
const [folderData, listData, tagData] = await Promise.all([
|
||||||
const [folderData, listData, tagData, taskData] = await Promise.all([
|
api('/folders'), api('/lists'), api('/tags').catch(() => []),
|
||||||
api('/folders'), api('/lists'), api('/tags').catch(() => []), loadTaskPages(taskPath),
|
|
||||||
])
|
])
|
||||||
folders.value = folderData; lists.value = listData; tags.value = tagData; tasks.value = taskData
|
folders.value = folderData; lists.value = listData; tags.value = tagData
|
||||||
activeList.value ||= lists.value.find((item) => item.is_inbox)?.id || lists.value[0]?.id || ''
|
activeList.value ||= lists.value.find((item) => item.is_inbox)?.id || lists.value[0]?.id || ''
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (query.value) params.set('q', query.value)
|
||||||
|
else if (activeView.value === 'tasks' && activeList.value) params.set('list_id', activeList.value)
|
||||||
|
if (!showCompleted.value && activeView.value !== 'trash') params.set('completed', 'false')
|
||||||
|
tasks.value = await loadTaskPages(`/tasks${params.size ? `?${params}` : ''}`)
|
||||||
expandedFolders.value = new Set(folders.value.map((folder) => folder.id))
|
expandedFolders.value = new Set(folders.value.map((folder) => folder.id))
|
||||||
} catch (reason) { fail(reason) } finally { loading.value = false }
|
} catch (reason) { fail(reason) } finally { loading.value = false }
|
||||||
}
|
}
|
||||||
@@ -142,6 +149,7 @@ async function switchView(view: View, listId?: string) {
|
|||||||
if (listId) activeList.value = listId
|
if (listId) activeList.value = listId
|
||||||
selectedTask.value = null; selectedIds.value = new Set(); mobileSidebar.value = false; mobileDetail.value = false
|
selectedTask.value = null; selectedIds.value = new Set(); mobileSidebar.value = false; mobileDetail.value = false
|
||||||
if (view === 'trash') await loadTrash()
|
if (view === 'trash') await loadTrash()
|
||||||
|
else await loadAll()
|
||||||
}
|
}
|
||||||
async function addTask() {
|
async function addTask() {
|
||||||
if (!title.value.trim() || !activeList.value) return
|
if (!title.value.trim() || !activeList.value) return
|
||||||
|
|||||||
@@ -107,6 +107,20 @@ def test_attachment_security_ownership_and_size(client, tmp_path, monkeypatch):
|
|||||||
assert client.delete(f"/api/v1/attachments/{attachment['id']}").status_code == 204
|
assert client.delete(f"/api/v1/attachments/{attachment['id']}").status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_tasks_can_be_loaded_by_list_and_completion_filter(client):
|
||||||
|
inbox = boot(client)
|
||||||
|
other = client.post("/api/v1/lists", json={"name": "其他清单"}).json()
|
||||||
|
client.post("/api/v1/tasks", json={"title": "收集箱未完成", "list_id": inbox["id"]})
|
||||||
|
done = client.post("/api/v1/tasks", json={"title": "收集箱已完成", "list_id": inbox["id"]}).json()
|
||||||
|
client.patch(f"/api/v1/tasks/{done['id']}", json={"completed": True, "version": done["version"]})
|
||||||
|
client.post("/api/v1/tasks", json={"title": "其他清单未完成", "list_id": other["id"]})
|
||||||
|
|
||||||
|
response = client.get("/api/v1/tasks", params={"list_id": inbox["id"], "completed": False})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert [item["title"] for item in response.json()["items"]] == ["收集箱未完成"]
|
||||||
|
|
||||||
|
|
||||||
def test_ticktick_preview_import_dedupe_and_json_restore(client):
|
def test_ticktick_preview_import_dedupe_and_json_restore(client):
|
||||||
boot(client)
|
boot(client)
|
||||||
csv_data = "Title,List Name,Due Date,Status,ID\nImported,Inbox,2026-10-01,0,ext-1\n"
|
csv_data = "Title,List Name,Due Date,Status,ID\nImported,Inbox,2026-10-01,0,ext-1\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user