h-sphere_sql-seal/src/view/CSVView.ts

284 lines
No EOL
8.7 KiB
TypeScript

import { WorkspaceLeaf, TextFileView, Menu, Notice } from 'obsidian';
import { parse, unparse } from 'papaparse';
import { DeleteConfirmationModal } from '../modal/deleteConfirmationModal';
import { RenameColumnModal } from '../modal/renameColumnModal';
import { CodeSampleModal } from '../modal/showCodeSample';
import { GridRenderer } from '../renderer/GridRenderer';
import { errorNotice } from '../utils/notice';
import { ModernCellParser } from 'src/cellParser/ModernCellParser';
export const CSV_VIEW_TYPE = "csv-viewer" as const;
export const CSV_VIEW_EXTENSIONS = ['csv'];
export class CSVView extends TextFileView {
private content: string;
private table: HTMLTableElement;
constructor(
leaf: WorkspaceLeaf,
private enableEditing: boolean,
private cellParser: ModernCellParser
) {
super(leaf);
}
getViewType(): string {
return CSV_VIEW_TYPE;
}
getDisplayText(): string {
return this.file?.basename || 'CSV Viewer';
}
async onload(): Promise<void> {
super.onload();
this.contentEl.addClass('csv-viewer-container');
}
async onOpen() {
this.renderCSV();
}
async onClose() {
// Cleanup if needed
}
async setViewData(data: string, clear: boolean): Promise<void> {
this.content = data;
await this.renderCSV();
}
getViewData(): string {
return this.content;
}
clear(): void {
this.content = '';
this.table.empty();
}
private result: any;
updateRow(newRowData: Record<string, any>) {
this.result.data[parseInt(newRowData.__index, 10)] = newRowData;
this.saveData()
}
deleteRow(idx: number) {
this.result.data.splice(idx, 1)
this.saveData()
}
deleteColumn(column: string) {
this.result.fields = this.result.fields.filter((c: string) => c !== column)
this.saveData()
}
addNewColumn(columnName: string) {
if (this.result.fields.indexOf(columnName) > -1) {
throw new Error('Column already exists')
}
this.result.fields.push(columnName)
this.result.data = this.result.data.map((d: any) => ({ [columnName]: '', ...d }))
this.saveData()
}
renameColumn(oldName: string, newName: string) {
if (!newName) {
return
}
if (this.result.fields.contains(newName)) {
errorNotice('Column already exists')
return
}
const oldNameIdx = this.result.fields.indexOf(oldName)
if (oldNameIdx < 0) {
errorNotice('Old column does not exist')
return
}
this.result.fields[oldNameIdx] = newName
this.result.data = this.result.data.map((d: Record<string, unknown>) => {
d[newName] = d[oldName]
delete d[oldName]
return d
})
this.saveData()
}
setIsEditable(newValue: boolean) {
this.enableEditing = newValue
// FIXME: if there already rendered view, use this to rerender it?
}
saveData() {
const output = unparse(this.result)
this.app.vault.modify(this.file!, output)
}
createRow() {
this.result.data.push(this.result.fields.reduce((acc: Record<string, any>, f: string) => ({ ...acc, [f]: '' }), {}))
this.saveData()
}
moveColumn(name: string, toIndex: number) {
let fields = this.result.fields as Array<string>
fields = fields.filter(f => f !== name)
fields = [
...fields.slice(0, toIndex),
name,
...fields.slice(toIndex)
]
this.result.fields = fields
this.saveData()
}
api: any = null;
loadDataIntoGrid() {
setTimeout(() => {
const result = parse(this.content, {
header: true,
skipEmptyLines: true,
});
const data = result.data.map((d: any, i) => ({
...d,
__index: i.toString()
}))
this.result = {
data: data,
fields: result.meta.fields
}
this.api!.render({
data: data,
columns: result.meta.fields
})
}, 100)
}
private async renderCSV() {
if (this.api) {
this.loadDataIntoGrid()
return
}
this.contentEl.empty()
const csvEditorDiv = this.contentEl.createDiv({ cls: 'sql-seal-csv-editor' })
const buttonsRow = csvEditorDiv.createDiv({ cls: 'sql-seal-csv-viewer-buttons' })
if (this.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 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.app, null)
const csvView = this;
const api = grid.render({
defaultColDef: {
editable: this.enableEditing,
headerComponentParams: {
enableMenu: this.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.colId, res)
})
modal.open()
})
})
menu.addItem(item => {
item.setTitle('Delete Column')
item.onClick(() => {
this.column.colId
const modal = new DeleteConfirmationModal(csvView.app, `column ${this.column.colId}`, () => {
csvView.deleteColumn(this.column.colId)
})
modal.open()
})
})
const pos = e.getBoundingClientRect();
menu.showAtPosition({ x: pos.x, y: pos.y + 20 })
}
}
},
enableCellTextSelection: false,
ensureDomOrder: false,
paginationAutoPageSize: true,
suppressMovableColumns: false,
onColumnMoved: (e) => {
if (!e.finished) {
return
}
const columnName = e.column?.getUserProvidedColDef()
if (!columnName) {
return
}
csvView.moveColumn(columnName?.field!, e.toIndex!)
},
domLayout: 'normal',
getRowId: (p) => p.data.__index,
onCellValueChanged: (event) => {
if (event.rowIndex === null) {
return;
}
this.updateRow(event.data)
},
onCellContextMenu: (e) => {
if (!this.enableEditing) {
return
}
const menu = new Menu()
menu.addItem(item => {
item.setTitle('Delete Row')
item.onClick(() => {
const modal = new DeleteConfirmationModal(csvView.app, `row`, () => {
csvView.deleteRow(e.data.__index)
})
modal.open()
})
})
menu.showAtMouseEvent(e.event as any)
}
}, gridEl, { cellParser: this.cellParser })
this.api = api;
this.loadDataIntoGrid()
}
}