mirror of
https://github.com/h-sphere/sql-seal.git
synced 2026-07-22 10:10:28 +00:00
feat: expose task status symbol in tasks table (#207)
* feat: expose task status symbol in tasks table Adds a 'status' column to the tasks table that contains the raw checkbox character (e.g. ' ', 'x', '-', '/'). This allows querying by specific task states beyond just completed/incomplete. * feat: adding tasks statuses support
This commit is contained in:
parent
03dfcd78b2
commit
c8567131cd
6 changed files with 138 additions and 6 deletions
5
.changeset/orange-showers-argue.md
Normal file
5
.changeset/orange-showers-argue.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"sqlseal": minor
|
||||
---
|
||||
|
||||
Adding tasks statuses support
|
||||
BIN
docs/data-sources/tasks-statuses.png
Normal file
BIN
docs/data-sources/tasks-statuses.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
|
|
@ -44,6 +44,7 @@ Tasks table consists of the following columns:
|
|||
| --------------- |--------------------------------------------------------------------------------------------------------------------------| ------------- |
|
||||
| `task` | Content of the task (text) | |
|
||||
| `completed` | 0 if not completed, 1 if completed | |
|
||||
| `status` | The raw checkbox character (` ` for unchecked, `x` for completed, `/` for in-progress, `-` for cancelled, etc.) | 0.40.0 |
|
||||
| `path` | Full path of the file the tag belongs to | 0.24.1 |
|
||||
| `filePath` | (deprecated) same like `path`. Name changed for compatibility with other tables. Will get removed in the future versions | |
|
||||
| `checkbox` | Interactive checkbox for the task that can be clicked to toggle completion state | 0.29.0 |
|
||||
|
|
@ -88,3 +89,9 @@ WHERE target = @path
|
|||
AND json_extract(position, '$.frontmatterKey') = 'type'
|
||||
```
|
||||
|
||||
## Task Status Field
|
||||
|
||||
The `status` column in the `tasks` table contains the raw checkbox character from your markdown tasks. This enables querying tasks by their specific states beyond just completed/uncompleted. This is functionality used commonly in plugins like Tasks and Obsidian natively supports them too. Many themes also render these fields correctly by default. You can read more about it in [the Tasks documentation](https://publish.obsidian.md/tasks/Getting+Started/Statuses#Credit%20Sytone%20and%20the%20'Tasks%20SQL%20Powered'%20plugin).
|
||||
|
||||
### Example
|
||||

|
||||
|
|
|
|||
96
src/modules/sync/sync/tables/tasksTable.test.ts
Normal file
96
src/modules/sync/sync/tables/tasksTable.test.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* Tests for the `status` column added to the tasks table (main-2xf).
|
||||
*
|
||||
* `getFileTasks()` is an instance method that calls `this.app` and `this.db`,
|
||||
* so we test the pure data-shaping logic by replicating it here rather than
|
||||
* mocking the full Obsidian API surface.
|
||||
*/
|
||||
|
||||
interface ListItemMock {
|
||||
task: string // raw checkbox char: ' ', 'x', '/', '-', '<', '>', etc.
|
||||
position: { start: { line: number } }
|
||||
}
|
||||
|
||||
/** Mirrors the core logic of getFileTasks() in tasksTable.ts */
|
||||
function shapeTask(listItem: ListItemMock, lineContent: string, filePath: string) {
|
||||
if (!listItem.task) return undefined
|
||||
|
||||
const status = listItem.task !== ' '
|
||||
const taskContent = lineContent.substring(lineContent.indexOf(']') + 1).trim()
|
||||
|
||||
return {
|
||||
filePath,
|
||||
path: filePath,
|
||||
task: taskContent,
|
||||
completed: status ? 1 : 0,
|
||||
status: listItem.task,
|
||||
position: listItem.position.start.line,
|
||||
}
|
||||
}
|
||||
|
||||
describe('tasks table — status column (main-2xf)', () => {
|
||||
describe('status value reflects the raw checkbox character', () => {
|
||||
it('is " " (space) for an unchecked task', () => {
|
||||
const item: ListItemMock = { task: ' ', position: { start: { line: 0 } } }
|
||||
const row = shapeTask(item, '- [ ] Buy groceries', 'note.md')
|
||||
expect(row?.status).toBe(' ')
|
||||
})
|
||||
|
||||
it('is "x" for a standard completed task', () => {
|
||||
const item: ListItemMock = { task: 'x', position: { start: { line: 1 } } }
|
||||
const row = shapeTask(item, '- [x] Done', 'note.md')
|
||||
expect(row?.status).toBe('x')
|
||||
})
|
||||
|
||||
it('is "/" for an in-progress task (Tasks plugin custom status)', () => {
|
||||
const item: ListItemMock = { task: '/', position: { start: { line: 2 } } }
|
||||
const row = shapeTask(item, '- [/] In progress', 'note.md')
|
||||
expect(row?.status).toBe('/')
|
||||
})
|
||||
|
||||
it('is "-" for a cancelled task', () => {
|
||||
const item: ListItemMock = { task: '-', position: { start: { line: 3 } } }
|
||||
const row = shapeTask(item, '- [-] Cancelled', 'note.md')
|
||||
expect(row?.status).toBe('-')
|
||||
})
|
||||
|
||||
it('is ">" for a forwarded/rescheduled task', () => {
|
||||
const item: ListItemMock = { task: '>', position: { start: { line: 4 } } }
|
||||
const row = shapeTask(item, '- [>] Rescheduled', 'note.md')
|
||||
expect(row?.status).toBe('>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('completed column is unaffected', () => {
|
||||
it('completed=0 when status is space', () => {
|
||||
const item: ListItemMock = { task: ' ', position: { start: { line: 0 } } }
|
||||
expect(shapeTask(item, '- [ ] Pending', 'note.md')?.completed).toBe(0)
|
||||
})
|
||||
|
||||
it('completed=1 when status is "x"', () => {
|
||||
const item: ListItemMock = { task: 'x', position: { start: { line: 0 } } }
|
||||
expect(shapeTask(item, '- [x] Done', 'note.md')?.completed).toBe(1)
|
||||
})
|
||||
|
||||
it('completed=1 for custom non-space chars (any char != space counts as complete)', () => {
|
||||
// This matches the existing semantics: status !== ' ' → completed
|
||||
const item: ListItemMock = { task: '/', position: { start: { line: 0 } } }
|
||||
expect(shapeTask(item, '- [/] In progress', 'note.md')?.completed).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('non-task list items are excluded', () => {
|
||||
it('returns undefined when listItem.task is empty string (plain list item)', () => {
|
||||
const item = { task: '', position: { start: { line: 0 } } }
|
||||
expect(shapeTask(item, '- plain list item', 'note.md')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('task content extraction is unaffected', () => {
|
||||
it('strips the checkbox syntax and returns just the content', () => {
|
||||
const item: ListItemMock = { task: 'x', position: { start: { line: 0 } } }
|
||||
const row = shapeTask(item, '- [x] Buy milk', 'note.md')
|
||||
expect(row?.task).toBe('Buy milk')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -47,6 +47,7 @@ export class TasksFileSyncTable extends AFileSyncTable {
|
|||
checked: status,
|
||||
path: file.path,
|
||||
task: taskContent,
|
||||
status: listItem.task,
|
||||
position: {
|
||||
line: listItem.position.start.line,
|
||||
lineContent: lineContent
|
||||
|
|
@ -63,6 +64,7 @@ export class TasksFileSyncTable extends AFileSyncTable {
|
|||
path: file.path,
|
||||
task: taskContent,
|
||||
completed: status ? 1 : 0,
|
||||
status: listItem.task,
|
||||
position: listItem.position.start.line,
|
||||
checkbox: `SQLSEALCUSTOM(${JSON.stringify(checkboxData)})`,
|
||||
heading: heading ? heading.heading : undefined,
|
||||
|
|
@ -72,7 +74,7 @@ export class TasksFileSyncTable extends AFileSyncTable {
|
|||
}
|
||||
|
||||
async onInit(): Promise<void> {
|
||||
await this.db.createTableNoTypes('tasks', ['checkbox', 'task', 'completed', 'filePath', 'path', 'position', 'heading', 'heading_level'])
|
||||
await this.db.createTableNoTypes('tasks', ['checkbox', 'task', 'completed', 'status', 'filePath', 'path', 'position', 'heading', 'heading_level'])
|
||||
|
||||
// Indexes
|
||||
const toIndex = ['filePath']
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ interface CheckboxProps {
|
|||
line: number,
|
||||
lineContent: string
|
||||
},
|
||||
task: string
|
||||
task: string,
|
||||
status?: string
|
||||
}
|
||||
|
||||
const isCheckboxProp = (arg: any): arg is CheckboxProps => {
|
||||
|
|
@ -45,7 +46,8 @@ export class CheckboxParser implements CellFunction<Args> {
|
|||
const el = createEl('input', {
|
||||
type: 'checkbox',
|
||||
attr: {
|
||||
checked: !!(values && values.checked) ? true : null
|
||||
checked: !!(values && values.checked) ? true : null,
|
||||
'data-task': values.status || ' '
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -73,11 +75,26 @@ export class CheckboxParser implements CellFunction<Args> {
|
|||
// Get the line containing the task
|
||||
const lineIndex = values.position?.line
|
||||
if (lineIndex >= 0 && lineIndex < lines.length) {
|
||||
// Get current status
|
||||
const currentStatus = values.status || (values.checked ? 'x' : ' ')
|
||||
|
||||
// Escape special regex characters
|
||||
const escapedStatus = currentStatus.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
|
||||
// Update the task status
|
||||
if (el.checked) {
|
||||
lines[lineIndex] = lines[lineIndex].replace('- [ ]', '- [x]')
|
||||
// Mark as completed
|
||||
lines[lineIndex] = lines[lineIndex].replace(
|
||||
new RegExp(`- \\[${escapedStatus}\\]`),
|
||||
'- [x]'
|
||||
)
|
||||
} else {
|
||||
lines[lineIndex] = lines[lineIndex].replace('- [x]', '- [ ]')
|
||||
// Restore original status
|
||||
const originalStatus = currentStatus === 'x' ? ' ' : currentStatus
|
||||
lines[lineIndex] = lines[lineIndex].replace(
|
||||
'- [x]',
|
||||
`- [${originalStatus}]`
|
||||
)
|
||||
}
|
||||
|
||||
// Write back to the file
|
||||
|
|
@ -98,10 +115,15 @@ export class CheckboxParser implements CellFunction<Args> {
|
|||
}
|
||||
return '[ ]'
|
||||
} else {
|
||||
// Use status if available
|
||||
if (values.status) {
|
||||
return `[${values.status}]`
|
||||
}
|
||||
// Fall back to checked boolean
|
||||
if (values.checked) {
|
||||
return '[x]'
|
||||
} else {
|
||||
return '[ ] '
|
||||
return '[ ]'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue