Feat/global tables new (#173)

* chore: reworking plugin internals into modules

* feat: settings module is now working, added debug module, enabled new api module

* chore: restoring external API

* feat: refactoring project to use proper inversion of control

* feat: plugins can now be loaded in any order

* chore: final file migration, all typescript is in modules now

* chore: fixing dependencies of cellParser

* feat: csv and json views are only registered when they are not colliding with other plugins

* feat: fixed library behaviour on canvas

* feat: added ability to hide columns from csv files

* feat: adding global tables support (wip)

* feat: global tables full implementation

* feat: added changeset

* chore: fixing typechecks
This commit is contained in:
Kacper Kula 2025-08-10 11:01:08 +02:00 committed by GitHub
parent 7095130ea5
commit 2bfd2060d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 975 additions and 60 deletions

View file

@ -0,0 +1,5 @@
---
"sqlseal": minor
---
adding global tables support - you can now define table that will be available in all your files

View file

@ -37,7 +37,7 @@ const wasmPlugin = {
loader: 'js',
};
});
},
}
};
// Plugin to inject worker code
@ -108,6 +108,9 @@ const context = await esbuild.context({
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
loader: {
'.svg': 'text'
},
plugins: [
wasmPlugin,
workerPlugin

View file

@ -96,6 +96,11 @@ export class SqlSealDatabase {
return this.db?.getColumns(tableName)
}
async count(tableName: string) {
const data = await this.db?.select(`SELECT COUNT(*) as count FROM ${tableName}`, {})
return data?.data[0]['count']
}
async addColumns(tableName: string, newColumns: string[]) {
return this.db?.addColumns(tableName, newColumns)
}

View file

@ -182,10 +182,11 @@ export class CodeblockProcessor extends MarkdownRenderChild {
const canvasViews = this.app.workspace.getLeavesOfType("canvas");
for (const leaf of canvasViews) {
const nodes = JSON.parse(leaf.view.data).nodes
const node = nodes.filter(n => n.text).find(n => n.text.contains(this.source))
const canvasView = leaf.view as any; // Canvas view has data and file properties not exposed in base View type
const nodes = JSON.parse(canvasView.data).nodes
const node = nodes.filter((n: any) => n.text).find((n: any) => n.text.contains(this.source))
if (node) {
this.cachedName = leaf.view.file.path
this.cachedName = canvasView.file.path
return this.cachedName as string
}
}

View file

@ -0,0 +1,49 @@
import { TFile, Vault } from "obsidian";
export class FileConfig<T> {
constructor(private path: string, private vault: Vault) {
}
private data: T[] = []
private isLoaded: boolean = false
private fileHandler: TFile | null = null
async load() {
this.fileHandler = this.vault.getFileByPath(this.path)
if (!this.fileHandler) {
return
}
// FIXME: add proper ZOD parsing here
this.data = JSON.parse(await this.vault.cachedRead(this.fileHandler))
this.isLoaded = true
}
async insert(v: T) {
if (!this.isLoaded) {
await this.load()
}
this.data.push(v)
await this.save()
}
get items() {
return this.data
}
async save() {
const data = JSON.stringify(this.data)
if (!this.fileHandler) {
// Creating new file
this.fileHandler = await this.vault.create(this.path, data)
return
}
await this.vault.modify(this.fileHandler, data)
}
async remove(v: T) {
this.data = this.data.filter(d => d !== v)
await this.save()
}
}

View file

@ -0,0 +1,206 @@
import { IconName, ItemView, Vault, WorkspaceLeaf } from "obsidian";
import { MenuBar } from "./GlobalTablesView/MenuBar";
import { FileConfig } from "./FileConfig";
import { TableConfiguration } from "./modal/NewGlobalTableModal";
import { GridRenderer } from "../editor/renderer/GridRenderer";
import { createGrid, GridApi, themeQuartz } from "ag-grid-community";
import { ConfigCellRenderer } from "./cells/ConfigCell";
import { StatsRenderer } from "./cells/StatsRenderer";
import { ActionCellRenderer } from "./cells/ActionCell";
import { Sync } from "../sync/sync/sync";
import { ParserTableDefinition } from "../sync/syncStrategy/types";
import { throttle } from "lodash";
export const GLOBAL_TABLES_VIEW_TYPE = "sqlseal-global-tables";
const GLOBAL_SOURCE_FILE = "/";
export class GlobalTablesView extends ItemView {
config: FileConfig<TableConfiguration>;
constructor(
leaf: WorkspaceLeaf,
vault: Vault,
public sync: Sync,
) {
super(leaf);
this.config = new FileConfig("__globalviews.sqlseal", vault);
}
getViewType() {
return GLOBAL_TABLES_VIEW_TYPE;
}
getDisplayText() {
return "SQLSeal Global Tables";
}
getIcon(): IconName {
return "logo-sqlseal";
}
api: GridApi | null = null;
async onOpen() {
const container = this.containerEl.children[1];
container.empty();
const c = container.createDiv({ cls: "sqlseal-global-tables-container" });
const el = c.createEl("div");
const menuBar = new MenuBar(el, this.app);
await this.config.load();
for (const conf of this.gridData) {
this.sync.registerTable(this.getTableDefinition(conf));
}
menuBar.events.on("new-table", (data) => {
this.addElement(data);
});
// el.createDiv({ text: 'HELLO FROM SQLSEAL GLOBAL TABLES VIEW' })
const gridEl = c.createDiv({ cls: "sql-seal-csv-viewer" });
const myTheme = themeQuartz.withParams({
borderRadius: 0,
browserColorScheme: "light",
headerFontSize: 14,
headerRowBorder: false,
headerVerticalPaddingScale: 1,
rowBorder: false,
spacing: 16,
wrapperBorder: false,
wrapperBorderRadius: 0,
});
this.api = createGrid(gridEl, {
theme: myTheme,
detailRowAutoHeight: true,
paginationAutoPageSize: true,
rowData: this.gridData,
suppressMoveWhenColumnDragging: true,
suppressRowDrag: true,
suppressCellFocus: true,
suppressMaintainUnsortedOrder: true,
suppressDragLeaveHidesColumns: true,
suppressMoveWhenRowDragging: true,
defaultColDef: {
resizable: false,
sortable: false,
rowDrag: false,
suppressMovable: true,
flex: 1,
},
autoSizeStrategy: {
type: "fitGridWidth",
},
columnDefs: [
{ field: "name" },
{ field: "config.type", minWidth: 50 },
{ field: "config", cellRenderer: ConfigCellRenderer },
{
field: "name",
cellRenderer: StatsRenderer,
headerName: "Stats",
minWidth: 200,
},
{ cellRenderer: ActionCellRenderer, headerName: "Actions" },
],
context: this,
});
this.setupResizeObserver(gridEl);
}
// Vibe Coded.
private smartResize() {
if (!this.api) return;
const api = this.api
const columnApi = this.api
const containerWidth = this.containerEl.clientWidth;
// First, auto-size all columns based on content
const allColumnIds = columnApi.getColumns()?.map(col => col.getColId()) || [];
api.autoSizeColumns(allColumnIds);
// Calculate total width needed
const totalContentWidth = columnApi.getColumns()
?.reduce((sum, col) => sum + col.getActualWidth(), 0) || 0;
// If content fits in container, optionally expand to fill
if (totalContentWidth < containerWidth * 0.8) {
// Content fits comfortably, you can choose to:
// Option A: Leave as-is (content-based sizing)
// Option B: Expand to fill container
api.sizeColumnsToFit();
}
// If content is wider than container, keep auto-sized widths
// This will enable horizontal scrolling automatically
}
get gridData() {
return this.config.items;
}
async addElement(data: TableConfiguration) {
await this.config.insert(data);
switch (data.config.type) {
case "csv":
case "json":
await this.sync.registerTable(this.getTableDefinition(data));
break;
case "md-table":
console.log("NOT IMPLEMENTED YET", data);
}
this.refresh();
}
getTableDefinition(data: TableConfiguration): ParserTableDefinition {
switch (data.config.type) {
case "csv":
return {
tableAlias: data.name,
sourceFile: GLOBAL_SOURCE_FILE,
type: "file",
arguments: [data.config.filename],
};
case 'json':
return {
tableAlias: data.name,
sourceFile: GLOBAL_SOURCE_FILE,
type: "file",
arguments: [data.config.filename, data.config.xpath ? data.config.xpath : '$'],
}
case "md-table":
throw new Error("Not implemented");
}
}
async deleteElement(e: TableConfiguration) {
await this.config.remove(e);
const def = this.getTableDefinition(e);
await this.sync.unregisterTable(def);
this.refresh();
}
refresh() {
this.api?.setGridOption("rowData", this.gridData);
}
async onClose() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.api) {
this.api.destroy();
}
}
private resizeObserver: ResizeObserver;
private setupResizeObserver(gridContainer: HTMLElement) {
const fn = throttle(() => this.smartResize(), 100)
this.resizeObserver = new ResizeObserver(fn);
this.resizeObserver.observe(gridContainer);
}
}

View file

@ -0,0 +1,39 @@
import { args, BusBuilder } from "@hypersphere/omnibus"
import { App, ButtonComponent } from "obsidian"
import { NewGlobalTableModal, TableConfiguration } from "../modal/NewGlobalTableModal"
export class MenuBar {
private bus =(new BusBuilder())
.register('new-table', args<TableConfiguration>())
.build()
constructor(private el: HTMLDivElement, private app: App) {
this.show()
}
get events() {
return this.bus.getRegistrator()
}
openNewTableModal() {
const modal = new NewGlobalTableModal(this.app)
modal.open()
modal.bus.on('data', d => this.bus.trigger('new-table', d))
}
private show() {
this.el.empty()
this.addButton('Create New Table', () => this.openNewTableModal(), true)
}
private addButton(text: string, onClick: () => void, cta: boolean = false) {
const btn = new ButtonComponent(this.el)
.setButtonText(text)
.onClick(onClick)
if (cta) {
btn.setCta()
}
}
}

View file

@ -0,0 +1,46 @@
import { makeInjector } from "@hypersphere/dity";
import { GlobalTablesModule } from "./module";
import { addIcon, App, Plugin, WorkspaceLeaf } from "obsidian";
import { GLOBAL_TABLES_VIEW_TYPE, GlobalTablesView } from "./GlobalTablesView";
// @ts-ignore: Handled by esbuild
import SQLSealIcon from './sqlseal-bw.svg'
import { Sync } from "../sync/sync/sync";
@(makeInjector<GlobalTablesModule, 'factory'>()(
['plugin', 'app', 'sync']
))
export class GlobalTablesViewRegister {
make(plugin: Plugin, app: App, sync: Sync) {
const activateView = async () => {
const { workspace } = plugin.app;
let leaf: WorkspaceLeaf | null = null;
const leaves = workspace.getLeavesOfType(GLOBAL_TABLES_VIEW_TYPE);
if (leaves.length > 0) {
// A leaf with our view already exists, use that
leaf = leaves[0];
} else {
leaf = workspace.getLeaf('tab')
if (!leaf) { return }
await leaf.setViewState({ type: GLOBAL_TABLES_VIEW_TYPE, active: true });
}
// "Reveal" the leaf in case it is in a collapsed sidebar
workspace.revealLeaf(leaf);
}
return () => {
plugin.registerView(GLOBAL_TABLES_VIEW_TYPE, leaf => new GlobalTablesView(leaf, app.vault, sync))
// SQLSeal Icon
addIcon('logo-sqlseal', SQLSealIcon)
plugin.addRibbonIcon('logo-sqlseal', 'SQLSeal Global Tables', activateView)
}
}
}

View file

@ -0,0 +1,11 @@
import { makeInjector } from "@hypersphere/dity"
import { GlobalTablesModule } from "./module"
@(makeInjector<GlobalTablesModule, 'factory'>()(['globalTablesViewRegister']))
export class InitFactory {
make(register: () => void) {
return () => {
register()
}
}
}

View file

@ -0,0 +1,82 @@
import { args, BusBuilder } from "@hypersphere/omnibus"
import { TFile, Vault } from "obsidian"
interface DropdownValue {
name: string
path: string
originalFile: TFile
}
export class AutocompleteInput {
constructor(parent: HTMLElement, private vault: Vault, private extensions: string[]) {
const c = parent.createDiv({ cls: 'sqlseal-autocomplete-container'})
this.selectedEl = c.createEl('div', { cls: 'selected-el'})
this.selectedElText = this.selectedEl.createSpan()
this.selectedEl.createEl('button', { text: 'X'}).addEventListener('click', () => {
this.input.show()
this.input.value = ''
this.selectedEl.hide()
this.bus.trigger('change', '')
})
this.selectedEl.hide()
this.input = c.createEl('input', { type: 'text' })
this.dropdownContainer = c.createDiv({ cls: 'sqlseal-autocomplete-dropdown'})
this.render()
}
bus = new BusBuilder()
.register('item-selected', args<DropdownValue>())
.register('change', args<string>())
.build()
dropdownContainer: HTMLDivElement
input: HTMLInputElement
dropdownValues: DropdownValue[] = []
selectedEl: HTMLDivElement
selectedElText: HTMLSpanElement
render() {
this.input.addEventListener('keydown', e => {
requestAnimationFrame(() => {
this.dropdownValues = this.getFilesForInput((e.target! as any).value)
this.setDropdownValues()
})
})
this.bus.on('item-selected', e => {
this.input.value = e.originalFile.path
this.bus.trigger('change', e.originalFile.path)
this.dropdownValues = []
this.setDropdownValues()
this.selectedEl.show()
this.selectedElText.textContent = e.originalFile.path
this.input.hide()
})
}
setDropdownValues() {
this.dropdownContainer.empty()
for (const val of this.dropdownValues) {
this.dropdownContainer.appendChild(this.createDropdownOption(val))
}
}
private createDropdownOption(opt: DropdownValue) {
const el = createEl('div', { cls: 'sqlseal-dropdown-el' })
el.createDiv({ text: opt.name, cls: 'sqlseal-dropdown-el-name' })
el.createDiv({ text: opt.path, cls: 'sqlseal-dropdown-el-path' })
el.addEventListener('click', e => {
this.bus.trigger('item-selected', opt)
})
return el
}
getFilesForInput(inp: string): DropdownValue[] {
return this.vault.getFiles()
.filter(f => this.extensions.includes(f.extension))
.filter(f => f.path.includes(inp))
.map(f => ({ name: f.name, path: f.parent?.path ?? '', originalFile: f }))
}
}

View file

@ -0,0 +1,28 @@
import type { ICellRendererComp, ICellRendererParams } from "ag-grid-community";
import { TableConfiguration } from "../modal/NewGlobalTableModal";
import { ButtonComponent } from "obsidian";
import { GlobalTablesView } from "../GlobalTablesView";
export class ActionCellRenderer implements ICellRendererComp {
private eGui!: HTMLDivElement;
public init(params: ICellRendererParams<any, string, GlobalTablesView>): void {
const { value, data, context } = params;
this.eGui = document.createElement("div");
new ButtonComponent(this.eGui)
.setIcon('trash-2')
.onClick(() => context.deleteElement(data))
}
public getGui(): HTMLElement {
return this.eGui;
}
public refresh(params: ICellRendererParams): boolean {
return true;
}
}

View file

@ -0,0 +1,47 @@
import type { ICellRendererComp, ICellRendererParams } from "ag-grid-community";
import { TableConfiguration } from "../modal/NewGlobalTableModal";
import { Component, MarkdownRenderer, TFile } from "obsidian";
import { GlobalTablesView } from "../GlobalTablesView";
export class ConfigCellRenderer implements ICellRendererComp {
private eGui!: HTMLDivElement;
public init(
params: ICellRendererParams<
TableConfiguration,
TableConfiguration["config"],
GlobalTablesView
>,
): void {
const { value, data, context } = params;
this.eGui = document.createElement("div");
this.eGui.className = "config";
const container = this.eGui.createDiv();
if (value?.type === "csv") {
const link = container.createEl("a");
link.href = value.filename;
link.textContent = value.filename;
link.addEventListener("click", (e) => {
e.preventDefault();
const file = context.app.vault.getAbstractFileByPath(value.filename);
if (file instanceof TFile) {
context.app.workspace.openLinkText(value.filename, "", true);
}
});
}
if (value?.type === 'json') {
container.textContent = value.filename + '#' + (value.xpath ? value.xpath : '$')
}
}
public getGui(): HTMLElement {
return this.eGui;
}
public refresh(params: ICellRendererParams): boolean {
return true;
}
}

View file

@ -0,0 +1,59 @@
import type { ICellRendererComp, ICellRendererParams } from "ag-grid-community";
import { TableConfiguration } from "../modal/NewGlobalTableModal";
import { GlobalTablesView } from "../GlobalTablesView";
import { OmnibusRegistrator } from "@hypersphere/omnibus";
export class StatsRenderer implements ICellRendererComp {
private eGui!: HTMLDivElement;
context: GlobalTablesView
data: TableConfiguration
eventName: string = ''
reg: OmnibusRegistrator
syncFn = () => {
this.sync()
}
public init(params: ICellRendererParams<TableConfiguration, string, GlobalTablesView>): void {
const { value, data, context } = params;
this.context = context
this.data = data!
this.eGui = document.createElement("div");
this.eGui.className = "stats";
const stats = this.eGui.createSpan()
stats.textContent = 'Loading'
requestAnimationFrame(async () => { this.sync() })
// Watching for the changes
this.setupWatchersAsync(context, data!)
}
async setupWatchersAsync(context: GlobalTablesView, data: TableConfiguration) {
this.reg = context.sync.getRegistrator()
this.eventName = await context.sync.getEventNameForAlias('/', data!.name)
this.reg.on(this.eventName, this.syncFn)
}
async sync() {
const result = await this.context.sync.getStats('/', this.data!.name)
this.eGui.textContent = `Rows: ${result.rows} / Columns: ${result.columns}`
}
public getGui(): HTMLElement {
return this.eGui;
}
public refresh(params: ICellRendererParams): boolean {
return false;
}
destroy(): void {
this.reg.off(this.eventName, this.syncFn)
}
}

View file

@ -0,0 +1,133 @@
import { MenuSeparator, Modal, Setting } from "obsidian";
import { AutocompleteInput } from "../autocompleteElement/AutocompleteInput";
import { args, BusBuilder } from "@hypersphere/omnibus";
export interface CSVConfig {
type: 'csv'
filename: string
}
export interface JsonConfig {
type: 'json'
filename: string
xpath: string
}
export interface MDTable {
type: 'md-table'
filename: string
selector: string
}
export interface TableConfiguration {
name: string
config: CSVConfig | JsonConfig | MDTable
}
export type Type = TableConfiguration['config']['type']
export class NewGlobalTableModal extends Modal {
bus = new BusBuilder()
.register('data', args<TableConfiguration>())
.build()
data: TableConfiguration = {
name: "",
config: {
type: 'csv',
filename: ''
}
};
typeSectionEl: HTMLElement | null = null
onOpen(): void {
const { contentEl } = this;
contentEl.closest('.modal')?.classList.add('modal-overflow')
contentEl.createEl("h2", { text: "New Table Modal" });
const settingsEl = contentEl.createDiv();
const name = new Setting(settingsEl).setName("Table Name").addText((txt) =>
txt.setPlaceholder("eg. Data").onChange((val) => {
this.data.name = val;
}),
);
const type = new Setting(settingsEl)
.setName("Source Type")
.addDropdown((drop) =>
drop.addOptions({
csv: "CSV",
json: "JSON",
// "md-table": "Markdown Table",
})
.setValue(this.data.config.type)
.onChange(v => {
this.data.config.type = v as Type
this.renderTypeSection()
})
);
this.typeSectionEl = contentEl.createDiv()
this.renderTypeSection()
new Setting(contentEl)
.addButton(b => b
.setButtonText('Add')
.setCta()
.onClick(() => {
this.bus.trigger('data', this.data)
this.close()
})
)
}
renderTypeSection() {
if (!this.typeSectionEl) {
return
}
const el = this.typeSectionEl
el.empty()
switch (this.data.config.type) {
case 'csv':
this.renderCsvSection()
break
case 'json':
this.renderJsonSection()
break
case 'md-table':
break
default:
break
}
}
renderCsvSection() {
const filename = new Setting(this.typeSectionEl!)
.setName('Filename')
const autocomplete = new AutocompleteInput(filename.controlEl, this.app.vault, ['csv'])
autocomplete.bus.on('change', (path) => {
this.data.config.filename = path
})
}
renderJsonSection() {
const filename = new Setting(this.typeSectionEl!)
.setName('Filename')
const autocomplete = new AutocompleteInput(filename.controlEl, this.app.vault, ['json', 'json5'])
autocomplete.bus.on('change', (path) => {
this.data.config.filename = path
})
const xpath = new Setting(this.typeSectionEl!)
.setName('Selector')
.addText(c => c.setPlaceholder('$.[0]')
.onChange(e => {
(this.data.config as JsonConfig).xpath = e
}))
}
}

View file

@ -0,0 +1,20 @@
import { asFactory, buildContainer } from "@hypersphere/dity";
import { InitFactory } from "./InitFactory";
import { App, Plugin } from "obsidian";
import { GlobalTablesViewRegister } from "./GlobalTablesViewRegister";
import { Sync } from "../sync/sync/sync";
export const globalTables = buildContainer(c =>
c.register({
init: asFactory(InitFactory),
globalTablesViewRegister: asFactory(GlobalTablesViewRegister)
})
.externals<{
plugin: Plugin,
app: App,
sync: Sync
}>()
.exports('init')
)
export type GlobalTablesModule = typeof globalTables

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 91 91" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;"><g><g><g><path d="M76.595,73.106c-1.409,-1.318 -2.846,-2.451 -3.7,-3.097c0.15,-2.473 -0.239,-4.9 -1.171,-7.268c-1.962,-4.986 -5.948,-8.893 -9.201,-11.739c-4.354,-3.81 -3.922,-9.686 -3.276,-12.851c0.361,-1.785 -0.072,-3.6 -1.135,-4.999c-0.904,-1.189 -5.401,-2.873 -8.602,-1.632c-5.254,2.037 -8.97,4.302 -10.802,7.457c-1.335,2.299 -1.688,4.865 -1.05,7.628c0.195,0.848 0.762,1.934 1.545,3.438c1.646,3.157 4.133,7.928 3.103,10.994c-0.354,1.055 -1.136,1.857 -2.391,2.451c-4.534,2.147 -6.793,-0.153 -9.977,-4.196c-1.169,-1.486 -2.275,-2.889 -3.594,-3.743c-1.806,-1.168 -4.117,-1.465 -6.683,-0.861c-1.208,0.284 -2.147,1.209 -2.45,2.413c-0.298,1.179 0.08,2.409 0.986,3.212c0.04,0.036 0.076,0.071 0.103,0.096c0.559,0.548 0.44,1.205 0.363,1.462c-0.172,0.576 -0.623,0.988 -1.205,1.102c-1.537,0.302 -3.123,0.813 -4.715,1.521c-1.778,0.794 -2.898,2.57 -2.852,4.523c0.045,1.958 1.2,3.616 3.012,4.328c2.315,0.908 5.277,1.412 8.804,1.496c1.952,0.047 3.731,0.966 4.882,2.521c0.407,0.55 0.842,1.085 1.296,1.604c-0.364,0.432 -0.745,0.869 -1.145,1.312c-1.141,1.263 -1.381,3.706 -0.875,4.627c2.044,3.717 13.868,2.571 16.224,0.599c0.113,-0.096 0.216,1.548 3.184,2.28c6.373,1.573 19.837,0.086 22.118,-5.228c1.11,0.221 3.056,0.571 5.076,0.749c0.749,0.067 1.563,0.119 2.38,0.119c2.589,-0 5.202,-0.522 5.841,-2.762c0.574,-2.005 -0.727,-4.407 -4.093,-7.556Zm-9.694,4.685c-2.685,4.59 -8.859,7.267 -15.731,7.457l-1.498,0c-1.877,-0.046 -3.789,-0.272 -5.684,-0.698c4.033,-2.439 6.299,-5.342 6.47,-5.565c0.562,-0.736 0.421,-1.788 -0.315,-2.35c-0.736,-0.562 -1.788,-0.421 -2.35,0.315c-0.067,0.087 -6.82,8.751 -17.855,7.5c-0.624,-0.071 -0.863,-0.534 -0.937,-0.73c-0.076,-0.2 -0.205,-0.716 0.226,-1.194c2.965,-3.281 4.995,-6.26 6.033,-8.854c0.344,-0.859 -0.074,-1.835 -0.934,-2.179c-0.86,-0.344 -1.835,0.074 -2.179,0.934c-0.459,1.147 -1.192,2.439 -2.166,3.829c-0.239,-0.291 -0.474,-0.586 -0.697,-0.888c-1.771,-2.393 -4.504,-3.807 -7.497,-3.879c-3.09,-0.073 -5.738,-0.511 -7.659,-1.265c-0.78,-0.306 -0.879,-1 -0.886,-1.284c-0.006,-0.232 0.041,-1.016 0.865,-1.384c1.36,-0.604 2.705,-1.039 3.995,-1.292c1.799,-0.353 3.245,-1.667 3.773,-3.432c0.515,-1.724 0.044,-3.57 -1.23,-4.818c-0.005,-0.005 -0.013,-0.012 -0.017,-0.017c-0.026,-0.024 -0.052,-0.049 -0.078,-0.074c1.626,-0.361 2.962,-0.213 3.972,0.441c0.851,0.55 1.789,1.741 2.782,3.002c2.695,3.424 6.77,8.598 14.045,5.152c2.093,-0.992 3.484,-2.477 4.135,-4.414c1.484,-4.418 -1.4,-9.951 -3.308,-13.611c-0.54,-1.036 -1.153,-2.211 -1.252,-2.642c-0.446,-1.934 -0.223,-3.631 0.682,-5.191c1.6,-2.754 5.234,-4.952 10.803,-6.53c0.882,-0.249 1.837,-0.042 2.548,0.551l0.049,0.041c0.799,0.665 1.165,1.723 0.954,2.76c-0.79,3.877 -1.276,11.116 4.354,16.042c2.968,2.596 6.592,6.13 8.289,10.444c1.728,4.389 1.171,8.911 -1.702,13.823Zm5.243,2.115c-0.85,-0.091 -1.677,-0.211 -2.404,-0.331c0.018,-0.03 0.037,-0.06 0.055,-0.091c1.124,-1.921 1.944,-3.827 2.463,-5.71c0.598,0.488 1.266,1.057 1.926,1.669c2.889,2.675 3.235,3.925 3.276,4.226c-0.275,0.16 -1.464,0.65 -5.316,0.237Z" style="fill-rule:nonzero;"/></g></g><g><g><circle cx="47.242" cy="39.859" r="1.676"/></g></g><g><g><path d="M76.42,14.232c-1.116,3.3 -7.37,4.166 -13.969,1.935c-6.598,-2.231 -11.044,-6.715 -9.928,-10.014c1.115,-3.3 7.369,-4.166 13.968,-1.935c6.599,2.231 11.044,6.715 9.929,10.014Z" style="fill:none;fill-rule:nonzero;stroke:#000;stroke-width:3.15px;"/><path d="M73.895,21.7c-1.116,3.299 -7.369,4.165 -13.968,1.934c-6.599,-2.231 -11.044,-6.714 -9.929,-10.014" style="fill:none;fill-rule:nonzero;stroke:#000;stroke-width:3.15px;"/><path d="M52.523,6.153l-5.05,14.935c-1.115,3.3 3.33,7.783 9.929,10.014c6.599,2.231 12.853,1.365 13.968,-1.934l5.05,-14.936" style="fill:none;fill-rule:nonzero;stroke:#000;stroke-width:3.15px;"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -11,7 +11,8 @@ type InitFn = () => void
'contextMenu.init',
'sync.init',
'debug.init',
'api.init'
'api.init',
'globalTables.init'
]))
export class Init {
async make(
@ -21,7 +22,8 @@ export class Init {
contextMenu: InitFn,
syncInit: InitFn,
debugInit: InitFn,
apiInit: InitFn
apiInit: InitFn,
globalTablesInit: InitFn
) {
return () => {
settingsInit()
@ -29,8 +31,9 @@ export class Init {
highlighInit()
contextMenu()
syncInit()
debugInit()
// debugInit()
apiInit()
globalTablesInit()
}
}
}

View file

@ -10,6 +10,7 @@ import { syntaxHighlight } from '../syntaxHighlight/module'
import { contextMenu } from '../contextMenu/module'
import { debugModule } from '../debug/module'
import { apiModule } from '../api/module'
import { globalTables } from '../globalTables/module'
const obsidian = buildContainer(c => c
.externals<{
@ -30,7 +31,8 @@ export const mainModule = buildContainer(c => c
syntaxHighlight,
contextMenu,
debug: debugModule,
api: apiModule
api: apiModule,
globalTables
})
.register({
init: asFactory(Init)
@ -77,6 +79,11 @@ export const mainModule = buildContainer(c => c
'api.db': 'db.db',
'api.rendererRegistry': 'editor.rendererRegistry'
})
.resolve({
'globalTables.plugin': 'obsidian.plugin',
'globalTables.app': 'obsidian.app',
'globalTables.sync': 'sync.syncBus'
})
)
export type MainModule = typeof mainModule

View file

@ -1,4 +1,4 @@
import { WorkspaceLeaf, TextFileView, Menu, MenuItem } from "obsidian";
import { WorkspaceLeaf, TextFileView, Menu, MenuItem, IconName } from "obsidian";
import { parse, unparse } from "papaparse";
import {
GridRenderer,
@ -12,7 +12,8 @@ import { RenameColumnModal } from "../modal/renameColumnModal";
import { CodeSampleModal } from "../modal/showCodeSample";
import { DeleteConfirmationModal } from "../modal/deleteConfirmationModal";
import { CSVColumnContextMenu } from "../menu/csvColumnContextMenu";
import { AgColumn } from "ag-grid-community";
import { AgColumn, Column } from "ag-grid-community";
import { CSVViewMenuBar } from "./CSVViewMenuBar";
const delay = (n: number) => new Promise((resolve) => setTimeout(resolve, n));
@ -236,6 +237,10 @@ export class CSVView extends TextFileView {
}
}
getIcon(): IconName {
return 'table'
}
moveColumn(name: string, toIndex: number) {
let fields = this.result.fields as Array<string>;
fields = fields.filter((f) => f !== name);
@ -307,6 +312,38 @@ export class CSVView extends TextFileView {
showHidden: boolean = false;
private menu(container: HTMLElement) {
const buttonsRow = container.createDiv({
cls: "sql-seal-csv-viewer-buttons",
});
const menuBar = new CSVViewMenuBar(buttonsRow, this.settings, this.app)
const events = menuBar.events
events.on('add-column', (columnName: string) => {
this.addNewColumn(columnName)
})
events.on('add-row', () => {
this.createRow();
})
events.on('generate-code', () => {
if (!this.file) {
return;
}
const modal = new CodeSampleModal(this.app, this.file);
modal.open();
})
events.on('toggle-hidden', (v) => {
this.showHidden = v
this.refreshTypes();
})
}
private async renderCSV() {
if (this.isLoading) {
if (this.api) {
@ -320,57 +357,12 @@ export class CSVView extends TextFileView {
const csvEditorDiv = this.contentEl.createDiv({
cls: "sql-seal-csv-editor",
});
const buttonsRow = csvEditorDiv.createDiv({
cls: "sql-seal-csv-viewer-buttons",
});
await this.loadConfig();
if (this.settings.get("enableEditing")) {
const createColumn = buttonsRow.createEl("button", {
text: "Add Column",
});
const createRow = buttonsRow.createEl("button", { text: "Add Row" });
createColumn.addEventListener("click", (e) => {
e.preventDefault();
const modal = new RenameColumnModal(this.app, (res) => {
this.addNewColumn(res);
});
modal.open();
});
createRow.addEventListener("click", (e) => {
e.preventDefault();
this.createRow();
});
}
const generateSqlCode = buttonsRow.createEl("button", {
text: "Generate SQLSeal code",
});
const showHidden = buttonsRow.createEl('button', {
text: 'Show Hidden Columns'
})
showHidden.addEventListener('click', () => {
this.showHidden = !this.showHidden
showHidden.textContent = this.showHidden ? 'Hide Hidden Columns' : 'Show Hidden Columns'
this.refreshTypes()
})
this.menu(csvEditorDiv)
const gridEl = csvEditorDiv.createDiv({ cls: "sql-seal-csv-viewer" });
generateSqlCode.addEventListener("click", (e) => {
e.preventDefault();
if (!this.file) {
return;
}
const modal = new CodeSampleModal(this.app, this.file);
modal.open();
});
const grid = new GridRenderer(this.settings, null, this.app);
const csvView = this;
const data = this.prepareData();

View file

@ -0,0 +1,64 @@
import { App, ButtonComponent } from "obsidian";
import { RenameColumnModal } from "../modal/renameColumnModal";
import { Settings } from "../Settings";
import { args, BusBuilder } from "@hypersphere/omnibus";
const SHOW_HIDDEN_TEXT = "Show Hidden Columns";
const HIDE_HIDDEN_TEXT = "Hide Hidden Columns";
export class CSVViewMenuBar {
private bus = new BusBuilder()
.register("add-row", args<[]>())
.register("add-column", args<string>())
.register("generate-code", args<[]>())
.register("toggle-hidden", args<boolean>())
.build();
constructor(
private el: HTMLElement,
private settings: Settings,
private app: App,
) {
this.show();
}
private showHidden: boolean = false;
get events() {
return this.bus.getRegistrator();
}
private show() {
const el = this.el;
el.empty();
if (this.settings.get("enableEditing")) {
new ButtonComponent(el)
.setButtonText("Add Row")
.setCta()
.onClick(() => this.bus.trigger("add-row"));
new ButtonComponent(el).setButtonText("Add Column").onClick(() => {
const modal = new RenameColumnModal(this.app, (res) => {
this.bus.trigger("add-column", res);
});
modal.open();
});
new ButtonComponent(el)
.setButtonText("Generate SQLSeal Code")
.onClick(() => this.bus.trigger("generate-code"));
const showHiddenButton = new ButtonComponent(el)
.setButtonText(SHOW_HIDDEN_TEXT)
.onClick(() => {
this.showHidden = !this.showHidden;
showHiddenButton.setButtonText(
this.showHidden ? HIDE_HIDDEN_TEXT : SHOW_HIDDEN_TEXT,
);
this.bus.trigger("toggle-hidden", this.showHidden);
});
}
}
}

View file

@ -1,4 +1,4 @@
import { WorkspaceLeaf, TextFileView, Menu } from 'obsidian';
import { WorkspaceLeaf, TextFileView, Menu, IconName } from 'obsidian';
import { parse, stringify } from 'json5'
export const JSON_VIEW_TYPE = "sqlseal-json-viewer";
@ -33,6 +33,10 @@ export class JsonView extends TextFileView {
// Cleanup if needed
}
getIcon(): IconName {
return 'file-json'
}
async setViewData(data: string, clear: boolean): Promise<void> {
this.content = data;
await this.renderJson();

View file

@ -19,7 +19,14 @@ export class TableAliasesRepository extends Repository {
}
async deleteMapping(id: string) {
this.db.deleteData(this.TABLE_NAME, [{ id: id }], 'id')
await this.db.deleteData(this.TABLE_NAME, [{ id: id }], 'id')
}
async deleteMappingByNames(aliasName: string, sourceFileName: string) {
const data = await this.getByAlias(sourceFileName, aliasName)
if (data) {
await this.db.deleteData(this.TABLE_NAME, [{ id: data.id }], 'id')
}
}
private async createTable() {

View file

@ -63,6 +63,8 @@ export class Sync {
// START SYNCING
this.startSync()
await this.refreshGlobalMappings()
}
async syncFileByName(fileName: string) {
@ -113,6 +115,17 @@ export class Sync {
await this.tableDefinitionsRepo.insert(log)
}
private globalTables: Record<string, string> = {}
async refreshGlobalMappings() {
const globalMappings = await this.tableMapLog.getByContext('/') as { alias_name: string, table_name: string }[]
this.globalTables = Object.fromEntries(globalMappings.map(g => [g.alias_name, g.table_name]))
}
get globalTablesMapping() {
return this.globalTables
}
async getTablesMappingForContext(sourceFileName: string) {
const tables = await this.tableMapLog.getByContext(sourceFileName) as { alias_name: string, table_name: string }[]
const map = tables.reduce((acc, t) => ({
@ -120,11 +133,14 @@ export class Sync {
[t.alias_name as string]: t.table_name
}), {})
// FIXME: adding globals here.
return {
...map,
...this.globalTablesMapping,
files: 'files',
tasks: 'tasks',
tags: 'tags'
tags: 'tags',
}
}
@ -171,6 +187,27 @@ export class Sync {
})
}
}
if (reg.sourceFile === '/') {
await this.refreshGlobalMappings()
}
}
unregisterTable(reg: ParserTableDefinition) {
return this.tableMapLog.deleteMappingByNames(reg.tableAlias, reg.sourceFile)
}
async getStats(sourceFileName: string, table: string) {
const tab = await this.tableMapLog.getByAlias(sourceFileName, table)
if (!tab) {
return { rows: 0, columns: 0 }
}
const columns = await this.db.getColumns(tab.table_name)
const rows = await this.db.count(tab.table_name)
return {
rows,
columns: columns ? columns.length : 0
}
}
getRegistrator() {

View file

@ -0,0 +1,58 @@
.sqlseal-autocomplete-container {
position: relative;
width: 100%;
& input[type="text"] {
width: 100%;
}
}
.sqlseal-autocomplete-dropdown {
position: absolute;
top: 100%; left: 0;
margin-top: 0.5em;
right: 0;
text-align: left;
background: white;
max-height: 200px;
overflow-y: scroll;
border-radius: 5px;
box-shadow: 0 3px 5px #CCC;
}
.modal-overflow {
overflow: visible;
// --dialog-width: 1000px;
}
.sqlseal-dropdown-el {
padding: 4px 16px 4px 8px;
border-top: 1px solid #EEE;
overflow: hidden;
&:first-child {
border-top: 0;
}
&:hover {
background: #a8c4ef;
}
}
.sqlseal-dropdown-el-name {
font-weight: bold;
font-size: 0.9em;
text-overflow: ellipsis;
max-width: 100%;
overflow: hidden;
text-wrap: nowrap;
}
.sqlseal-dropdown-el-path {
opacity: 0.9;
font-size: 0.7em;
text-overflow: ellipsis;
max-width: 100%;
overflow: hidden;
text-wrap: nowrap;
}

View file

@ -0,0 +1,6 @@
.sqlseal-global-tables-container {
height: 100%;
display: flex;
flex-direction: column;
gap: 1em;
}

View file

@ -6,4 +6,6 @@
@use 'obsidianMinimal';
@use 'agGrid';
@use 'settings';
@use 'canvas';
@use 'canvas';
@use 'autocomplete';
@use 'global-tables';