Feat/hide csv columns (#176)

* 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
This commit is contained in:
Kacper Kula 2025-08-10 10:49:56 +02:00 committed by GitHub
parent 285684dfc4
commit 7095130ea5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 191 additions and 84 deletions

View file

@ -0,0 +1,5 @@
---
"sqlseal": minor
---
added ability to hide columns from CSV files

View file

@ -0,0 +1,99 @@
import { Menu, MenuItem } from "obsidian";
import { RenameColumnModal } from "../modal/renameColumnModal";
import { CSVView } from "../view/CSVView";
import { AgColumn } from "ag-grid-community";
import { DeleteConfirmationModal } from "../modal/deleteConfirmationModal";
import { ColumnType } from "../../../utils/types";
export class CSVColumnContextMenu {
constructor(
private csvView: CSVView,
private column: AgColumn,
) { }
onRename() {
const modal = new RenameColumnModal(this.csvView.app, (res) => {
this.csvView.renameColumn(this.columnField, res);
});
modal.open();
}
onDelete() {
const colName = this.columnField;
const modal = new DeleteConfirmationModal(
this.csvView.app,
`column ${colName}`,
() => {
this.csvView.deleteColumn(colName);
},
);
modal.open();
}
get columnField() {
return this.column.getUserProvidedColDef()?.field!;
}
get config() {
return this.csvView.getColumnConfig(this.columnField)
}
show(pos: { x: number; y: number }) {
const menu = new Menu();
menu.addItem((item) => {
item.setTitle("Rename Column");
item.onClick(this.onRename.bind(this));
});
menu.addItem((item) => {
item.setTitle("Delete Column");
item.onClick(this.onDelete.bind(this));
});
menu.addSeparator();
menu.addItem(item => {
const isHidden = !!this.config.isHidden
item.setTitle(isHidden ? 'Show in queries' : 'Hide from queries')
item.onClick(async () => {
await this.csvView.setConfig(this.columnField, { isHidden: !isHidden })
this.csvView.refreshTypes()
this.csvView.saveData()
})
})
// FIXME: rework it to submenus.
menu.addSeparator();
menu.addItem((item) => {
// item.setDisabled(true)
item.setTitle("Data Type");
// item.setIsLabel(true)
const ipfSubmenu = (item as any).setSubmenu();
const types = [
"auto",
"text",
"number",
"boolean",
"date",
] as ColumnType[];
const colName = this.columnField;
const current = this.csvView.getColumnType(colName);
types.forEach((type) => {
ipfSubmenu.addItem((subItem: MenuItem) => {
const checkbox = type === current ? "✓ " : "";
subItem.setTitle(checkbox + type);
subItem.onClick(() => {
this.csvView.changeColumnType(colName, type);
this.csvView.refreshTypes();
this.csvView.loadDataIntoGrid();
});
});
});
});
menu.showAtPosition(pos);
}
}

View file

@ -5,12 +5,14 @@ import {
GridRendererCommunicator,
} from "../../editor/renderer/GridRenderer";
import { errorNotice } from "../../../utils/notice";
import { ConfigObject, loadConfig, saveConfig } from "src/utils/csvConfig";
import { ColumnDefinition, ConfigObject, DEFAULT_CONFIG, loadConfig, saveConfig } from "src/utils/csvConfig";
import { ColumnType } from "../../../utils/types";
import { Settings } from "../Settings";
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";
const delay = (n: number) => new Promise((resolve) => setTimeout(resolve, n));
@ -158,14 +160,25 @@ export class CSVView extends TextFileView {
return "auto";
}
getColumnConfig(columnName: string) {
if (this.config.columnDefinitions[columnName]) {
return this.config.columnDefinitions[columnName]
}
return { ...DEFAULT_CONFIG }
}
async changeColumnType(columnName: string, type: ColumnType) {
return await this.setConfig(columnName, { type })
}
async setConfig(columnName: string, config: Partial<ColumnDefinition>) {
const prev = this.config.columnDefinitions[columnName] ?? {};
this.config.columnDefinitions[columnName] = {
this.config.columnDefinitions[columnName] = {
...prev,
type: type,
...config,
};
await this.saveConfig();
}
}
async loadConfig() {
while (!this.file) {
@ -179,25 +192,34 @@ export class CSVView extends TextFileView {
}
private getColumnConfigurations(columns: string[]) {
return columns.map((f) => {
return columns
.filter(f => this.showHidden || !this.getColumnConfig(f).isHidden)
.map((f) => {
if (!this.config) {
return {
field: f,
};
}
const def = this.config.columnDefinitions[f]?.type;
if (!def || def === "auto") {
return { field: f };
const config = this.getColumnConfig(f)
const type = config.type
const cellStyle = { opacity: config.isHidden ? 0.5 : 1 }
const result = {
field: f,
cellStyle,
headerClass: config.isHidden ? 'sqlseal-hidden-column' : ''
}
if (!type || type === "auto") {
return result;
}
if (def === "date") {
if (type === "date") {
return {
field: f,
...result,
cellDataType: "dateString",
};
}
return {
field: f,
cellDataType: def,
...result,
cellDataType: type,
};
});
}
@ -283,6 +305,8 @@ export class CSVView extends TextFileView {
isLoading: boolean = false;
showHidden: boolean = false;
private async renderCSV() {
if (this.isLoading) {
if (this.api) {
@ -323,6 +347,19 @@ export class CSVView extends TextFileView {
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()
})
const gridEl = csvEditorDiv.createDiv({ cls: "sql-seal-csv-viewer" });
generateSqlCode.addEventListener("click", (e) => {
@ -346,69 +383,9 @@ export class CSVView extends TextFileView {
headerComponentParams: {
enableMenu: this.settings.get("enableEditing"),
showColumnMenu: function (e: any) {
const menu = new Menu();
menu.addItem((item) => {
item.setTitle("Rename Column");
item.onClick(() => {
const modal = new RenameColumnModal(csvView.app, (res) => {
csvView.renameColumn(
this.column.userProvidedColDef.field,
res,
);
});
modal.open();
});
});
menu.addItem((item) => {
item.setTitle("Delete Column");
item.onClick(() => {
const colName = this.column.userProvidedColDef.field;
const modal = new DeleteConfirmationModal(
csvView.app,
`column ${colName}`,
() => {
csvView.deleteColumn(colName);
},
);
modal.open();
});
});
// FIXME: rework it to submenus.
menu.addSeparator();
menu.addItem((item) => {
// item.setDisabled(true)
item.setTitle("Data Type");
// item.setIsLabel(true)
const ipfSubmenu = (item as any).setSubmenu();
const types = [
"auto",
"text",
"number",
"boolean",
"date",
] as ColumnType[];
const colName = this.column.userProvidedColDef.field;
const current = csvView.getColumnType(colName);
types.forEach((type) => {
ipfSubmenu.addItem((subItem: MenuItem) => {
const checkbox = type === current ? "✓ " : "";
subItem.setTitle(checkbox + type);
subItem.onClick(() => {
csvView.changeColumnType(colName, type);
csvView.refreshTypes();
csvView.loadDataIntoGrid();
});
});
});
});
const pos = e.getBoundingClientRect();
menu.showAtPosition({ x: pos.x, y: pos.y + 20 });
const menu = new CSVColumnContextMenu(csvView, this.column as AgColumn)
const pos = e.getBoundingClientRect();
menu.show({ x: pos.x, y: pos.y + 20 })
},
},
},

View file

@ -59,14 +59,26 @@ export class CsvFileSyncStrategy extends ISyncStrategy {
header: true,
dynamicTyping: false,
skipEmptyLines: true,
transformHeader: sanitise
transformHeader: sanitise,
})
const config = await loadConfig(file, this.app.vault)
return { data: parsed.data, columns: parsed.meta.fields?.map(f => ({
name: f,
type: config.columnDefinitions[f]?.type ?? 'auto' as const
})) ?? [] }
const columns = parsed.meta.fields?.
filter(f => !config.columnDefinitions[f]?.isHidden)
.map(f => ({
name: f,
type: config.columnDefinitions[f]?.type ?? 'auto' as const
})) ?? []
const resultedData = parsed.data.map(d => {
const res: Record<string, any> = {}
for (const c of columns) {
res[c.name] = d[c.name]
}
return res
})
return { data: resultedData, columns }
}
}

View file

@ -1,4 +1,8 @@
/* AG GRID CUSTOMS */
.ag-checkbox-input {
opacity: 1 !important;
}
.sqlseal-hidden-column {
opacity: 0.5;
}

View file

@ -5,10 +5,16 @@ import { parse as jsonParse, stringify as jsonStringify } from 'json5'
// export type ColumnType = 'auto' | 'number' | 'text'
export type ColumnType = string
interface ColumnDefinition {
export interface ColumnDefinition {
type: ColumnType
isHidden: boolean
}
export const DEFAULT_CONFIG = {
type: 'auto',
isHidden: false
} satisfies ColumnDefinition
export interface ConfigObject {
columnDefinitions: {[key: string]: ColumnDefinition }
}
@ -27,7 +33,13 @@ export const loadConfig = async (file: TFile, vault: Vault): Promise<ConfigObjec
const loadedConfig = await vault.read(configFile)
// decode
const decoded = jsonParse(loadedConfig)
return decoded
// MAKE SURE ALL KEYS ARE LOWERCASE
const columnDefinitions = Object.fromEntries(Object.entries(decoded?.columnDefinitions ?? {}).map(([key, value]) => ([key.toLowerCase(), value])))
return {
...decoded,
columnDefinitions
}
}
export const saveConfig = async (file: TFile, content: Object, vault: Vault) => {

View file

@ -1,2 +0,0 @@
.sqlseal-table-container{overflow-y:scroll}.sqlseal-table-container table{width:100%;border-collapse:collapse}.sqlseal-grid-wrapper{height:auto;position:relative}.block-language-sqlseal{overflow-y:auto}.sqlseal-error{padding:1em;background:#b80f0f;color:#fff}.sqlseal-notice{padding:1em;background:#1f3f98;color:#fff}.sqlseal-grid-error-message{box-sizing:border-box;width:80%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;background:#b80f0f;font-size:.8em;color:#fff;padding:.5em 2em}.sqlseal-grid-error-message-overlay{content:"";position:absolute;inset:0;background:#fffc;z-index:5}.sqlseal-grid-error-message-overlay.hidden{display:none}.edit-block-button{z-index:1000}.sqlseal-markdown-table{font-family:monospace;white-space:pre-wrap}.sqlseal-markdown-table.no-wrap{display:inline-block;overflow-x:scroll;width:fit-content}.sqlseal-parse-error{background:#b80f0f;color:#fff;padding:.2em;border-radius:2px}.sqlseal-notice-error{color:#f26464}.sqlseal-notice-error:before{content:"";width:16px;height:16px;display:inline-block;margin-right:8px;transform:translateY(3px);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ff0000' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/%3E%3C/svg%3E")}.sql-seal-csv-editor{display:flex;flex-direction:column;height:100%;gap:var(--size-4-4)}.sql-seal-csv-viewer{flex:1;display:flex;flex-direction:column}.sqlseal-grid-wrapper{height:100%;flex:1;display:flex;flex-direction:column}.ag-theme-quartz{flex:1}.sql-seal-csv-viewer-buttons{display:flex;gap:var(--size-4-4)}.sql-seal-modal-code{width:100%;font-family:monospace;padding:8px}.sqlseal-inline-result{display:inline-block;padding:0 4px;margin:0 2px;background-color:var(--background-secondary);border-radius:4px;font-size:.9em;cursor:pointer}.sqlseal-inline-result .error{color:var(--text-error);font-style:italic}.sqlseal-list-element-single .sqlseal-column-name{display:none}.sqlseal-list-element-single .sqlseal-column-name:after{content:": "}.sqlseal-list.show-column-names .sqlseal-list-element-single .sqlseal-column-name{display:inline}.sqlseal-element-inline-list{display:inline-block}.cm-sqlseal-block-query{--color: #297f30}.cm-sqlseal-block-view{--color: #c834dd}.cm-sqlseal-block-flag{--color: #d08306;font-weight:700;color:var(--color)}.cm-sqlseal-block-table{--color: #4985af}.cm-sqlseal-identifier{color:#2d7b33}.cm-sqlseal-function,.cm-sqlseal-parameter{color:#2c0e9b}.cm-sqlseal-comment{color:#7a7a7a;font-style:italic}.cm-sqlseal-keyword{color:var(--color);font-weight:700}.cm-sqlseal-literal{color:#03f}.cm-sqlseal-error{color:#eb2f2f;font-style:italic}.cm-sqlseal-template-keyword{color:#e3ab53}.theme-dark .cm-sqlseal-template-keyword{color:#dd8e10}.theme-dark .cm-sqlseal-block-error{color:red}.theme-dark .cm-sqlseal-block-query{--color: #5eff6a}.theme-dark .cm-sqlseal-comment{color:#898787}.theme-dark .cm-sqlseal-parameter{color:#95ed22}.theme-dark .cm-sqlseal-literal{color:#ff0}.theme-dark .cm-sqlseal-block-view{--color: #e945ff}.theme-dark .cm-sqlseal-block-flag{--color: #e3ab53}.theme-dark .cm-sqlseal-block-table{--color: #56b5fa}.theme-dark .cm-sqlseal-function{color:#c4b8ef}.theme-dark .cm-sqlseal-identifier{color:#6ff77a}:is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table):before{background-color:var(--color);width:5px;top:-3px;bottom:-3px;left:0;content:"";display:block;position:absolute}.cm-indent+:is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table):before{display:none}.markdown-source-view.mod-cm6.is-readable-line-width .cm-contentContainer.cm-contentContainer>.cm-content>:has(>.block-language-sqlseal table.dataview){width:var(--container-dataview-table-width);max-width:var(--container-table-max-width)}.ag-checkbox-input{opacity:1!important}.sqlseal-settings-warn{background:#ffa600;padding:16px;border:2px solid rgb(134,87,0);border-radius:5px;margin:.5em 2em}
/*# sourceMappingURL=styles.css.map */