mirror of
https://github.com/h-sphere/sql-seal.git
synced 2026-07-22 05:12:18 +00:00
feat: added option to display interactive checkboxes in tasks table
This commit is contained in:
parent
fba988511c
commit
66b1172342
8 changed files with 217 additions and 101 deletions
|
|
@ -43,6 +43,8 @@ Tasks table consists of the following columns:
|
|||
| `completed` | 0 if not completed, 1 if completed | |
|
||||
| `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 |
|
||||
| `position` | Line number where the task appears in the original file | 0.29.0 |
|
||||
|
||||
### `links` table
|
||||
Table containing all the links between files.
|
||||
|
|
|
|||
|
|
@ -58,4 +58,32 @@ LIMIT 10
|
|||
You can display boolean data as checkbox in the interface by calling `checkbox` function:
|
||||
```sqlseal
|
||||
SELECT date, checkbox(excercised) FROM files WHERE date is not null
|
||||
```
|
||||
```
|
||||
|
||||
### Interactive Task Checkboxes
|
||||
|
||||
SQLSeal provides two ways to create interactive checkboxes for tasks:
|
||||
|
||||
#### Method 1: Direct checkbox column (Recommended)
|
||||
The simplest and most reliable approach is to directly use the `checkbox` column:
|
||||
|
||||
```sqlseal
|
||||
SELECT checkbox, task FROM tasks
|
||||
```
|
||||
|
||||
This approach automatically renders interactive checkboxes without requiring any function wrappers, making your queries cleaner and more maintainable.
|
||||
|
||||
#### Method 2: Using the `checkbox` function
|
||||
For backwards compatibility, you can also use the checkbox function with a single parameter:
|
||||
|
||||
```sqlseal
|
||||
-- Using the checkbox column
|
||||
SELECT task, checkbox(completed) FROM tasks
|
||||
|
||||
The checkbox function accepts a single parameter which can be either:
|
||||
- A numeric value (0 = unchecked, 1 = checked) - renders as a disabled checkbox
|
||||
- Any boolean/truthy value - renders as a disabled checkbox
|
||||
|
||||
> [!NOTE] Interactive vs Disabled Checkboxes
|
||||
> Only checkboxes created using the `checkbox` column are interactive. When using the `completed` column or other values, the checkbox will be rendered in a disabled state to indicate it cannot be modified.
|
||||
|
||||
|
|
|
|||
101
src/utils/ui.ts
101
src/utils/ui.ts
|
|
@ -1,6 +1,9 @@
|
|||
import { identity } from "lodash"
|
||||
import { App } from "obsidian"
|
||||
import { CellParserRegistar } from "../cellParser"
|
||||
import { renderCheckbox } from "./ui/checkbox"
|
||||
import { renderLink } from "./ui/link"
|
||||
import { renderImage } from "./ui/image"
|
||||
|
||||
export const displayNotice = (el: HTMLElement, text: string) => {
|
||||
el.empty()
|
||||
|
|
@ -12,34 +15,7 @@ export const displayError = (el: HTMLElement, text: string) => {
|
|||
el.createDiv({ text: text, cls: 'sqlseal-error' })
|
||||
}
|
||||
|
||||
const isLinkLocal = (link: string) => !link?.trim().startsWith('http')
|
||||
|
||||
const isLinktext = (link: string) => link.startsWith('[[') && link.endsWith(']]')
|
||||
|
||||
|
||||
const renderCheckbox = (app: App) => ([value]: [string]) => {
|
||||
const el = createEl('input', {
|
||||
type: 'checkbox',
|
||||
attr: {
|
||||
disabled: true
|
||||
},
|
||||
})
|
||||
if (!!value) {
|
||||
el.checked = true
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
const removeExtension = (filename: string) => {
|
||||
const parts = filename.split('.')
|
||||
if (parts.length > 1) {
|
||||
return parts.slice(0, -1).join('.')
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
|
||||
type LinkType = [string] | [string, string]
|
||||
|
||||
export const isStringifiedArray = (str: string) => {
|
||||
return str.length > 1 && str.startsWith('[') && str.endsWith(']') && str[1] !== '[' && str[str.length - 2] !== ']'
|
||||
|
|
@ -56,77 +32,6 @@ export const renderStringifiedArray = (input: string, transformer: (f: string) =
|
|||
return el
|
||||
}
|
||||
|
||||
export const renderLink = (app: App, create = createEl) => ([href, name]: LinkType): string | Node => {
|
||||
if (!href) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isStringifiedArray(href)) {
|
||||
try {
|
||||
return renderStringifiedArray(href, href => renderLink(app, create)([href]))
|
||||
} catch (e) {
|
||||
return href
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
name = href
|
||||
}
|
||||
href = href.trim()
|
||||
let cls = ''
|
||||
if (isLinktext(href)) {
|
||||
const [path, linkName] = href.slice(2, -2).split('|')
|
||||
const link = app.metadataCache.getFirstLinkpathDest(path, '')
|
||||
|
||||
if (name.trim() === href) {
|
||||
name = linkName ?? path
|
||||
}
|
||||
|
||||
if (!link) {
|
||||
href = path
|
||||
} else {
|
||||
href = link.path
|
||||
}
|
||||
cls = 'internal-link'
|
||||
|
||||
} else if (isLinkLocal(href)) {
|
||||
let fileHref = href
|
||||
if (fileHref.endsWith('.md')) {
|
||||
fileHref = fileHref.slice(0, -3)
|
||||
}
|
||||
if (name === href) {
|
||||
const components = href.split('/')
|
||||
name = removeExtension(components[components.length - 1])
|
||||
}
|
||||
cls = 'internal-link'
|
||||
}
|
||||
const el = create('a', { text: name, href, cls })
|
||||
return el
|
||||
}
|
||||
|
||||
const renderImage = (app: App) => ([href, path]: [string, string]) => {
|
||||
if (!isLinkLocal(href)) {
|
||||
return createEl('img', { attr: { src: href } });
|
||||
}
|
||||
href = (href ?? '').trim()
|
||||
if (href.startsWith('![[')) {
|
||||
href = href.slice(3, -2)
|
||||
}
|
||||
let parentPath = ''
|
||||
if (path) {
|
||||
const parent = app.vault.getFileByPath(path)
|
||||
if (parent) {
|
||||
parentPath = parent.parent?.path ?? ''
|
||||
}
|
||||
}
|
||||
path = (path ? parentPath + '/' : '') + href
|
||||
const file = app.vault.getFileByPath(path);
|
||||
if (!file) {
|
||||
return 'File does not exist'
|
||||
}
|
||||
return createEl('img', { attr: { src: app.vault.getResourcePath(file) } });
|
||||
}
|
||||
|
||||
export const registerDefaultFunctions = (registar: CellParserRegistar, app: App) => {
|
||||
registar.register('checkbox', renderCheckbox(app), 1)
|
||||
registar.register('a', renderLink(app), 2)
|
||||
|
|
|
|||
75
src/utils/ui/checkbox.ts
Normal file
75
src/utils/ui/checkbox.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { App } from "obsidian";
|
||||
|
||||
interface CheckboxProps {
|
||||
checked: boolean,
|
||||
path: string,
|
||||
position: {
|
||||
line: number,
|
||||
lineContent: string
|
||||
},
|
||||
task: string
|
||||
}
|
||||
|
||||
const isCheckboxProp = (arg: any): arg is CheckboxProps => {
|
||||
return arg && arg['position'] && arg['path']
|
||||
}
|
||||
|
||||
export const renderCheckbox = (app: App) => (values: number[] | CheckboxProps) => {
|
||||
|
||||
if (!isCheckboxProp(values)) {
|
||||
const el = createEl('input', {
|
||||
type: 'checkbox',
|
||||
attr: {
|
||||
disabled: true
|
||||
}
|
||||
});
|
||||
el.checked = !!values[0]
|
||||
return el
|
||||
}
|
||||
|
||||
const el = createEl('input', {
|
||||
type: 'checkbox',
|
||||
})
|
||||
|
||||
if (values && values.checked) {
|
||||
el.checked = true
|
||||
}
|
||||
|
||||
el.addEventListener('change', async () => {
|
||||
try {
|
||||
// Values should contain the data we need
|
||||
if (!values || !values.path) {
|
||||
console.error('Missing required checkbox data')
|
||||
return
|
||||
}
|
||||
|
||||
const file = app.vault.getFileByPath(values.path)
|
||||
if (!file) {
|
||||
console.error('File not found:', values.path)
|
||||
return
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const content = await app.vault.read(file)
|
||||
const lines = content.split('\n')
|
||||
|
||||
// Get the line containing the task
|
||||
const lineIndex = values.position?.line
|
||||
if (lineIndex >= 0 && lineIndex < lines.length) {
|
||||
// Update the task status
|
||||
if (el.checked) {
|
||||
lines[lineIndex] = lines[lineIndex].replace('- [ ]', '- [x]')
|
||||
} else {
|
||||
lines[lineIndex] = lines[lineIndex].replace('- [x]', '- [ ]')
|
||||
}
|
||||
|
||||
// Write back to the file
|
||||
await app.vault.modify(file, lines.join('\n'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating task:', error)
|
||||
}
|
||||
})
|
||||
|
||||
return el
|
||||
}
|
||||
12
src/utils/ui/helperFunctions.ts
Normal file
12
src/utils/ui/helperFunctions.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
export const isLinkLocal = (link: string) => !link?.trim().startsWith('http')
|
||||
|
||||
export const isLinktext = (link: string) => link.startsWith('[[') && link.endsWith(']]')
|
||||
|
||||
export const removeExtension = (filename: string) => {
|
||||
const parts = filename.split('.')
|
||||
if (parts.length > 1) {
|
||||
return parts.slice(0, -1).join('.')
|
||||
}
|
||||
return filename
|
||||
}
|
||||
25
src/utils/ui/image.ts
Normal file
25
src/utils/ui/image.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { App } from "obsidian";
|
||||
import { isLinkLocal } from "./helperFunctions";
|
||||
|
||||
export const renderImage = (app: App) => ([href, path]: [string, string]) => {
|
||||
if (!isLinkLocal(href)) {
|
||||
return createEl('img', { attr: { src: href } });
|
||||
}
|
||||
href = (href ?? '').trim()
|
||||
if (href.startsWith('![[')) {
|
||||
href = href.slice(3, -2)
|
||||
}
|
||||
let parentPath = ''
|
||||
if (path) {
|
||||
const parent = app.vault.getFileByPath(path)
|
||||
if (parent) {
|
||||
parentPath = parent.parent?.path ?? ''
|
||||
}
|
||||
}
|
||||
path = (path ? parentPath + '/' : '') + href
|
||||
const file = app.vault.getFileByPath(path);
|
||||
if (!file) {
|
||||
return 'File does not exist'
|
||||
}
|
||||
return createEl('img', { attr: { src: app.vault.getResourcePath(file) } });
|
||||
}
|
||||
53
src/utils/ui/link.ts
Normal file
53
src/utils/ui/link.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { App } from "obsidian"
|
||||
import { isLinkLocal, isLinktext, removeExtension } from "./helperFunctions"
|
||||
import { isStringifiedArray, renderStringifiedArray } from "../ui"
|
||||
|
||||
type LinkType = [string] | [string, string]
|
||||
|
||||
export const renderLink = (app: App, create = createEl) => ([href, name]: LinkType): string | Node => {
|
||||
if (!href) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isStringifiedArray(href)) {
|
||||
try {
|
||||
return renderStringifiedArray(href, href => renderLink(app, create)([href]))
|
||||
} catch (e) {
|
||||
return href
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
name = href
|
||||
}
|
||||
href = href.trim()
|
||||
let cls = ''
|
||||
if (isLinktext(href)) {
|
||||
const [path, linkName] = href.slice(2, -2).split('|')
|
||||
const link = app.metadataCache.getFirstLinkpathDest(path, '')
|
||||
|
||||
if (name.trim() === href) {
|
||||
name = linkName ?? path
|
||||
}
|
||||
|
||||
if (!link) {
|
||||
href = path
|
||||
} else {
|
||||
href = link.path
|
||||
}
|
||||
cls = 'internal-link'
|
||||
|
||||
} else if (isLinkLocal(href)) {
|
||||
let fileHref = href
|
||||
if (fileHref.endsWith('.md')) {
|
||||
fileHref = fileHref.slice(0, -3)
|
||||
}
|
||||
if (name === href) {
|
||||
const components = href.split('/')
|
||||
name = removeExtension(components[components.length - 1])
|
||||
}
|
||||
cls = 'internal-link'
|
||||
}
|
||||
const el = create('a', { text: name, href, cls })
|
||||
return el
|
||||
}
|
||||
|
|
@ -39,17 +39,33 @@ export class TasksFileSyncTable extends AFileSyncTable {
|
|||
const taskContent = lineContent.substring(
|
||||
lineContent.indexOf(']') + 1
|
||||
).trim();
|
||||
|
||||
const checkboxData = {
|
||||
type: "checkbox",
|
||||
values: {
|
||||
checked: status,
|
||||
path: file.path,
|
||||
task: taskContent,
|
||||
position: {
|
||||
line: listItem.position.start.line,
|
||||
lineContent: lineContent
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
filePath: file.path,
|
||||
path: file.path,
|
||||
task: taskContent,
|
||||
completed: status ? 1 : 0
|
||||
completed: status ? 1 : 0,
|
||||
position: listItem.position.start.line,
|
||||
checkbox: `SQLSEALCUSTOM(${JSON.stringify(checkboxData)})`
|
||||
}
|
||||
}).filter(t => !!t)
|
||||
}
|
||||
|
||||
async onInit(): Promise<void> {
|
||||
await this.db.createTableNoTypes('tasks', ['task', 'completed', 'filePath', 'path'])
|
||||
await this.db.createTableNoTypes('tasks', ['checkbox', 'task', 'completed', 'filePath', 'path', 'position'])
|
||||
|
||||
// Indexes
|
||||
const toIndex = ['filePath']
|
||||
|
|
|
|||
Loading…
Reference in a new issue