From 7095130ea5677761fe20c84a07d0e754e7b3ba90 Mon Sep 17 00:00:00 2001 From: Kacper Kula Date: Sun, 10 Aug 2025 10:49:56 +0200 Subject: [PATCH] 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 --- .changeset/cool-taxis-boil.md | 5 + .../settings/menu/csvColumnContextMenu.ts | 99 ++++++++++++++ src/modules/settings/view/CSVView.ts | 127 +++++++----------- .../sync/syncStrategy/CsvFileSyncStrategy.ts | 22 ++- src/styles/agGrid.scss | 4 + src/utils/csvConfig.ts | 16 ++- styles.css | 2 - 7 files changed, 191 insertions(+), 84 deletions(-) create mode 100644 .changeset/cool-taxis-boil.md create mode 100644 src/modules/settings/menu/csvColumnContextMenu.ts delete mode 100644 styles.css diff --git a/.changeset/cool-taxis-boil.md b/.changeset/cool-taxis-boil.md new file mode 100644 index 0000000..ddfdf84 --- /dev/null +++ b/.changeset/cool-taxis-boil.md @@ -0,0 +1,5 @@ +--- +"sqlseal": minor +--- + +added ability to hide columns from CSV files diff --git a/src/modules/settings/menu/csvColumnContextMenu.ts b/src/modules/settings/menu/csvColumnContextMenu.ts new file mode 100644 index 0000000..1fc0944 --- /dev/null +++ b/src/modules/settings/menu/csvColumnContextMenu.ts @@ -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); + } +} diff --git a/src/modules/settings/view/CSVView.ts b/src/modules/settings/view/CSVView.ts index fc657e5..6b17f5f 100644 --- a/src/modules/settings/view/CSVView.ts +++ b/src/modules/settings/view/CSVView.ts @@ -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) { 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 }) }, }, }, diff --git a/src/modules/sync/syncStrategy/CsvFileSyncStrategy.ts b/src/modules/sync/syncStrategy/CsvFileSyncStrategy.ts index b45b886..21d7842 100644 --- a/src/modules/sync/syncStrategy/CsvFileSyncStrategy.ts +++ b/src/modules/sync/syncStrategy/CsvFileSyncStrategy.ts @@ -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 = {} + for (const c of columns) { + res[c.name] = d[c.name] + } + return res + }) + + return { data: resultedData, columns } } } \ No newline at end of file diff --git a/src/styles/agGrid.scss b/src/styles/agGrid.scss index bb16c27..bb3a330 100644 --- a/src/styles/agGrid.scss +++ b/src/styles/agGrid.scss @@ -1,4 +1,8 @@ /* AG GRID CUSTOMS */ .ag-checkbox-input { opacity: 1 !important; +} + +.sqlseal-hidden-column { + opacity: 0.5; } \ No newline at end of file diff --git a/src/utils/csvConfig.ts b/src/utils/csvConfig.ts index dc5a014..50f9d7b 100644 --- a/src/utils/csvConfig.ts +++ b/src/utils/csvConfig.ts @@ -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 ([key.toLowerCase(), value]))) + return { + ...decoded, + columnDefinitions + } } export const saveConfig = async (file: TFile, content: Object, vault: Vault) => { diff --git a/styles.css b/styles.css deleted file mode 100644 index db7a30b..0000000 --- a/styles.css +++ /dev/null @@ -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 */