mirror of
https://github.com/h-sphere/sql-seal.git
synced 2026-07-22 10:10:28 +00:00
Merge pull request #121 from h-sphere/feat/interactive-task-checkboxes
Feat/interactive task checkboxes
This commit is contained in:
commit
aa3dc772b0
11 changed files with 448 additions and 102 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { renderLink } from "./ui"
|
||||
import { renderLink } from "./ui/link"
|
||||
|
||||
const makeApp = () => ({ metadataCache: { getFirstLinkpathDest: jest.fn(p => ({ path: p })) } })
|
||||
|
||||
|
|
|
|||
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
|
||||
}
|
||||
67
src/utils/ui/helperFunctions.test.ts
Normal file
67
src/utils/ui/helperFunctions.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { isLinkLocal, isLinktext, removeExtension } from './helperFunctions';
|
||||
|
||||
describe('helperFunctions', () => {
|
||||
describe('isLinkLocal', () => {
|
||||
it('should return true for local links', () => {
|
||||
expect(isLinkLocal('local/file.md')).toBe(true);
|
||||
expect(isLinkLocal('folder/file')).toBe(true);
|
||||
expect(isLinkLocal('file')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for http links', () => {
|
||||
expect(isLinkLocal('http://example.com')).toBe(false);
|
||||
expect(isLinkLocal('https://example.com')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle empty or undefined input', () => {
|
||||
expect(isLinkLocal('')).toBe(true);
|
||||
expect(isLinkLocal(undefined as any)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLinktext', () => {
|
||||
it('should return true for valid linktext format', () => {
|
||||
expect(isLinktext('[[file]]')).toBe(true);
|
||||
expect(isLinktext('[[folder/file]]')).toBe(true);
|
||||
expect(isLinktext('[[file with spaces]]')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid linktext format', () => {
|
||||
expect(isLinktext('file')).toBe(false);
|
||||
expect(isLinktext('[file]')).toBe(false);
|
||||
expect(isLinktext('[[file')).toBe(false);
|
||||
expect(isLinktext('file]]')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeExtension', () => {
|
||||
it('should remove recognized file extensions', () => {
|
||||
expect(removeExtension('file.md')).toBe('file');
|
||||
expect(removeExtension('document.csv')).toBe('document');
|
||||
expect(removeExtension('data.json')).toBe('data');
|
||||
expect(removeExtension('config.json5')).toBe('config');
|
||||
});
|
||||
|
||||
it('should handle files with multiple dots', () => {
|
||||
expect(removeExtension('my.file.md')).toBe('my.file');
|
||||
expect(removeExtension('config.min.js')).toBe('config.min.js'); // .js is not recognized
|
||||
});
|
||||
|
||||
it('should return original filename if no extension', () => {
|
||||
expect(removeExtension('file')).toBe('file');
|
||||
expect(removeExtension('document')).toBe('document');
|
||||
});
|
||||
|
||||
it('should handle filenames starting with dot', () => {
|
||||
expect(removeExtension('.gitignore')).toBe('.gitignore');
|
||||
});
|
||||
|
||||
it('should not remove unrecognized extensions', () => {
|
||||
expect(removeExtension('this.com')).toBe('this.com');
|
||||
expect(removeExtension('this.com.md')).toBe('this.com');
|
||||
expect(removeExtension('file.txt')).toBe('file.txt');
|
||||
expect(removeExtension('image.jpg')).toBe('image.jpg');
|
||||
expect(removeExtension('script.js')).toBe('script.js');
|
||||
});
|
||||
});
|
||||
});
|
||||
16
src/utils/ui/helperFunctions.ts
Normal file
16
src/utils/ui/helperFunctions.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export const isLinkLocal = (link: string) => !link?.trim().startsWith('http')
|
||||
|
||||
export const isLinktext = (link: string) => link.startsWith('[[') && link.endsWith(']]')
|
||||
|
||||
const RECOGNIZED_EXTENSIONS = ['md', 'csv', 'json', 'json5']
|
||||
|
||||
export const removeExtension = (filename: string) => {
|
||||
const parts = filename.split('.')
|
||||
if (parts.length > 1) {
|
||||
const lastPart = parts[parts.length - 1].toLowerCase()
|
||||
if (RECOGNIZED_EXTENSIONS.includes(lastPart)) {
|
||||
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']
|
||||
|
|
|
|||
159
src/view/SidebarView.ts
Normal file
159
src/view/SidebarView.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import { ItemView, TFile, WorkspaceLeaf } from 'obsidian';
|
||||
import SqlSealPlugin from '../main';
|
||||
|
||||
export const SIDEBAR_VIEW_TYPE = 'sql-seal-sidebar-view';
|
||||
|
||||
export class SidebarView extends ItemView {
|
||||
private currentFile: TFile | null = null;
|
||||
public contentEl: HTMLElement;
|
||||
private plugin: SqlSealPlugin;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: SqlSealPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return SIDEBAR_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return 'SQL Seal Sidebar';
|
||||
}
|
||||
|
||||
async onload(): Promise<void> {
|
||||
super.onload();
|
||||
|
||||
// Create container for the sidebar content
|
||||
this.contentEl = this.containerEl.createDiv('sql-seal-sidebar-container');
|
||||
|
||||
// Register event handler for file changes
|
||||
this.registerEvent(
|
||||
this.app.workspace.on('file-open', (file: TFile | null) => {
|
||||
this.currentFile = file;
|
||||
this.updateView();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async updateView(): Promise<void> {
|
||||
this.contentEl.empty();
|
||||
|
||||
if (!this.currentFile) {
|
||||
this.contentEl.createEl('div', {
|
||||
text: 'No file is currently open',
|
||||
cls: 'sql-seal-sidebar-empty'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get file metadata or frontmatter
|
||||
const fileCache = this.app.metadataCache.getFileCache(this.currentFile);
|
||||
const frontmatter = fileCache?.frontmatter || {};
|
||||
|
||||
// Add file-specific properties to frontmatter
|
||||
const enhancedFrontmatter = {
|
||||
...frontmatter,
|
||||
'@file': {
|
||||
name: this.currentFile.name,
|
||||
path: this.currentFile.path,
|
||||
basename: this.currentFile.basename,
|
||||
extension: this.currentFile.extension,
|
||||
stat: {
|
||||
ctime: this.currentFile.stat.ctime,
|
||||
mtime: this.currentFile.stat.mtime,
|
||||
size: this.currentFile.stat.size
|
||||
}
|
||||
},
|
||||
'@tags': fileCache?.tags?.map(tag => tag.tag) || [],
|
||||
'@links': fileCache?.links?.map(link => link.link) || [],
|
||||
'@headings': fileCache?.headings?.map(h => ({
|
||||
text: h.heading,
|
||||
level: h.level
|
||||
})) || []
|
||||
};
|
||||
|
||||
// Check if there's a specific SQL query defined in frontmatter
|
||||
const query = frontmatter?.['sql-seal-query'];
|
||||
const view = frontmatter?.['sql-seal-view'] || 'table'; // Default to table view
|
||||
|
||||
if (query) {
|
||||
const result = await this.plugin.sqlSeal.db.select(query, enhancedFrontmatter);
|
||||
this.renderQueryResults(result.data, view);
|
||||
} else {
|
||||
this.contentEl.createEl('div', {
|
||||
text: 'No SQL query defined for this file',
|
||||
cls: 'sql-seal-sidebar-empty'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.contentEl.createEl('div', {
|
||||
text: `Error: ${error.message}`,
|
||||
cls: 'sql-seal-sidebar-error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private renderQueryResults(results: any[], view: string): void {
|
||||
if (!results || results.length === 0) {
|
||||
this.contentEl.createEl('div', {
|
||||
text: 'No results found',
|
||||
cls: 'sql-seal-sidebar-empty'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (view === 'list') {
|
||||
this.renderListView(results);
|
||||
} else {
|
||||
this.renderTableView(results);
|
||||
}
|
||||
}
|
||||
|
||||
private renderTableView(results: any[]): void {
|
||||
const table = this.contentEl.createEl('table', {
|
||||
cls: 'sql-seal-sidebar-table'
|
||||
});
|
||||
|
||||
// Create header row
|
||||
const headerRow = table.createEl('tr');
|
||||
Object.keys(results[0]).forEach(key => {
|
||||
headerRow.createEl('th', { text: key });
|
||||
});
|
||||
|
||||
// Create data rows
|
||||
results.forEach(row => {
|
||||
const tr = table.createEl('tr');
|
||||
Object.values(row).forEach(value => {
|
||||
tr.createEl('td', { text: String(value) });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private renderListView(results: any[]): void {
|
||||
const list = this.contentEl.createEl('ul', {
|
||||
cls: 'sql-seal-sidebar-list'
|
||||
});
|
||||
|
||||
results.forEach(row => {
|
||||
const li = list.createEl('li');
|
||||
Object.entries(row).forEach(([key, value]) => {
|
||||
const span = li.createEl('span', {
|
||||
cls: 'sql-seal-sidebar-list-item'
|
||||
});
|
||||
span.createEl('span', {
|
||||
text: key,
|
||||
cls: 'sql-seal-sidebar-list-key'
|
||||
});
|
||||
span.createEl('span', {
|
||||
text: ': '
|
||||
});
|
||||
span.createEl('span', {
|
||||
text: String(value),
|
||||
cls: 'sql-seal-sidebar-list-value'
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue