Tasks: import TickTick backup CSV (csv: keys, matched against API rows, rekeyed when the API returns them)
All checks were successful
Deploy production / deploy (push) Successful in 2m29s

This commit is contained in:
2026-10-24 03:28:14 +00:00
parent 1754ea9316
commit b77c3f8cf5
3 changed files with 312 additions and 11 deletions

View File

@@ -1,6 +1,8 @@
import { describe, expect, it } from 'vitest';
import {
mapTask,
mergeCsv,
parseTickTickCsv,
moreComplete,
normTitle,
slugify,
@@ -81,3 +83,58 @@ describe('ticktick mapping', () => {
expect(normTitle('Book haircut!')).toBe('book haircut');
});
});
describe('ticktick backup csv', () => {
const csv = [
'"Date: 2026-10-24+0000"',
'"Status: ',
'0 Normal',
'2 Completed"',
'"Folder Name","List Name","Title","Kind","Tags","Content","Is Check list","Start Date","Due Date","Reminder","Repeat","Priority","Status","Created Time","Completed Time","Order","Timezone","Is All Day","Is Floating","Column Name","Column Order","View Mode","taskId","parentId","projectKind"',
'"","Inbox","Known","TEXT","","","N","","","","","0","2","2025-01-01T10:00:00+0000","2025-01-02T10:00:00+0000","0","Europe/Amsterdam","false","false",,,"list","1","","TASK"',
'"","Inbox","Old won\'t do","TEXT","a, b","line ""one""\nline two","N","","","","","5","-1","2024-01-01T10:00:00+0000","2024-02-01T10:00:00+0000","0","Europe/Amsterdam","false","false",,,"list","2","1","TASK"',
'"Old","Trips","Pack","CHECKLIST","","▫Tea\n▪Jerky\n","Y","","","","","0","0","2023-01-01T10:00:00+0000","","0","Europe/Amsterdam","false","false",,,"list","3","","TASK"',
''
].join('\n');
const api = {
projects: [],
tasks: [
{
id: 'api1',
projectId: 'inbox9',
title: 'Known',
status: 2,
createdTime: '2025-01-01T10:00:00.000+0000'
}
]
};
it('parses the backup, including multi-line quoted fields and checklists', () => {
const t = parseTickTickCsv(csv);
expect(t).toHaveLength(3);
expect(t[1]).toMatchObject({ content: 'line "one"\nline two', tags: ['a', 'b'], status: -1 });
expect(t[2].items).toEqual([
{ id: 'item1', title: 'Tea', status: 0 },
{ id: 'item2', title: 'Jerky', status: 1 }
]);
});
it('skips rows the API already has and keys the rest stably', () => {
const a = mergeCsv(api, parseTickTickCsv(csv));
const b = mergeCsv(api, parseTickTickCsv(csv));
expect(a.report).toMatchObject({
csvRows: 3,
matchedListTitleCreated: 1,
added: 2,
newLists: ['Trips']
});
const added = a.data.tasks.slice(1);
expect(added.map((t) => t.id)).toEqual(b.data.tasks.slice(1).map((t) => t.id));
expect(added[0].id).toMatch(/^csv:/);
expect(added[0]).toMatchObject({ projectId: 'inbox9', parentId: 'api1' });
expect(added[1].projectId).toBe('csvlist:Trips');
expect(a.data.projects).toContainEqual({
id: 'csvlist:Trips',
name: 'Old / Trips',
closed: true
});
});
});