From 148c874f02efc0ea8b288d803cf7f7f0495ee7ef Mon Sep 17 00:00:00 2001 From: stfrigerio Date: Wed, 16 Apr 2025 19:26:47 +0200 Subject: [PATCH] new add text modal --- main.ts | 6 +- src/codeblocks/processSqlBlock/index.ts | 2 - src/commands/inspectTableStructure.ts | 106 +++++++++---- src/components/AddTextEntryModal.ts | 146 ++++++++++++++++++ .../AddTextButton/AddTextButton.ts | 108 +++++++++++++ .../SqlChart/registerSqlChart.ts | 19 +-- src/webcomponents/index.ts | 3 +- 7 files changed, 338 insertions(+), 52 deletions(-) create mode 100644 src/components/AddTextEntryModal.ts create mode 100644 src/webcomponents/AddTextButton/AddTextButton.ts diff --git a/main.ts b/main.ts index 4d0fd52..0bd1980 100644 --- a/main.ts +++ b/main.ts @@ -19,7 +19,8 @@ import { registerTextInput, registerTimestampUpdaterButton, registerSqlChartRenderer, - registerMoodNoteButtonProcessor + registerMoodNoteButtonProcessor, + registerAddTextSupport } from "./src/webcomponents"; import { SQLiteDBSettings, DEFAULT_SETTINGS } from "./src/types"; import { pluginState } from "src/pluginState"; @@ -50,7 +51,8 @@ export default class SQLiteDBPlugin extends Plugin { registerSqlChartRenderer(this, this.dbService); registerTimestampUpdaterButton(this); registerMoodNoteButtonProcessor(this, this.dbService); - + registerAddTextSupport(this, this.dbService); + //? Codeblocks this.registerMarkdownCodeBlockProcessor( "sql", diff --git a/src/codeblocks/processSqlBlock/index.ts b/src/codeblocks/processSqlBlock/index.ts index 8ad851f..b5634e1 100644 --- a/src/codeblocks/processSqlBlock/index.ts +++ b/src/codeblocks/processSqlBlock/index.ts @@ -79,10 +79,8 @@ orderDirection: asc` // --- Choose Renderer based on displayFormat --- el.empty(); // Clear loading message or previous content if (params.displayFormat === 'table') { - console.log("Rendering SQL block as table"); renderResultsAsTable(resultsToRender, el); } else { // Default to 'list' - console.log("Rendering SQL block as list"); renderResults(resultsToRender, el); // Assuming renderResults is the list renderer } diff --git a/src/commands/inspectTableStructure.ts b/src/commands/inspectTableStructure.ts index 7b326eb..1b6ead3 100644 --- a/src/commands/inspectTableStructure.ts +++ b/src/commands/inspectTableStructure.ts @@ -10,40 +10,84 @@ export async function inspectTableStructure( editor: Editor, app: any ) { - const db = dbService.getDB(); - if (!db) { - new Notice("No DB loaded. Please open the DB first."); - return; - } - - // 1) get all table names - const result = db.exec("SELECT name FROM sqlite_master WHERE type='table';"); - if (!result || result.length === 0) { - new Notice("No tables found in the DB."); - return; - } - const tableNames = result[0].values.map((row) => row[0] as string); - - // 2) show a modal to pick one - const modal = new TablePickerModal(app, tableNames, (selectedTable) => { - // 3) query first 10 rows from that table - const rowResult = db.exec(`SELECT * FROM "${selectedTable}" LIMIT 10;`); - if (!rowResult || rowResult.length === 0) { - editor.replaceSelection(`\nNo rows found in table ${selectedTable}.\n`); + // Check if we're in local or remote mode + if (dbService.mode === "local") { + const db = dbService.getDB(); + if (!db) { + new Notice("No DB loaded. Please open the DB first."); return; } - // rowResult[0].columns -> array of column names - // rowResult[0].values -> array of row arrays - const columns = rowResult[0].columns; - const rows = rowResult[0].values; + // 1) get all table names + const result = db.exec("SELECT name FROM sqlite_master WHERE type='table';"); + if (!result || result.length === 0) { + new Notice("No tables found in the DB."); + return; + } + const tableNames = result[0].values.map((row) => row[0] as string); - let output = `\n**Columns in "${selectedTable}"**\n`; - output += columns.join(", "); - output += "\n\n**First row**\n"; - output += rows[0].join(", "); - editor.replaceSelection(output + "\n"); - }); + // 2) show a modal to pick one + const modal = new TablePickerModal(app, tableNames, (selectedTable) => { + // 3) query first 10 rows from that table + const rowResult = db.exec(`SELECT * FROM "${selectedTable}" LIMIT 10;`); + if (!rowResult || rowResult.length === 0) { + editor.replaceSelection(`\nNo rows found in table ${selectedTable}.\n`); + return; + } - modal.open(); + // rowResult[0].columns -> array of column names + // rowResult[0].values -> array of row arrays + const columns = rowResult[0].columns; + const rows = rowResult[0].values; + + let output = `\n**Columns in "${selectedTable}"**\n`; + output += columns.join(", "); + output += "\n\n**First row**\n"; + output += rows[0].join(", "); + editor.replaceSelection(output + "\n"); + }); + + modal.open(); + } else { + // Remote mode - Use the DBService methods which handle API calls + try { + // 1) get all table names using the getQuery method + const tables = await dbService.getQuery<{name: string}>("SELECT name FROM sqlite_master WHERE type='table';"); + if (!tables || tables.length === 0) { + new Notice("No tables found in the DB."); + return; + } + const tableNames = tables.map(table => table.name); + + // 2) show a modal to pick one + const modal = new TablePickerModal(app, tableNames, async (selectedTable) => { + try { + // 3) query first 10 rows from that table + const rows = await dbService.getQuery(`SELECT * FROM "${selectedTable}" LIMIT 10;`); + if (!rows || rows.length === 0) { + editor.replaceSelection(`\nNo rows found in table ${selectedTable}.\n`); + return; + } + + // For remote results, we get objects directly + const columns = Object.keys(rows[0]); + + let output = `\n**Columns in "${selectedTable}"**\n`; + output += columns.join(", "); + output += "\n\n**First row**\n"; + output += columns.map(col => rows[0][col]).join(", "); + editor.replaceSelection(output + "\n"); + } catch (error) { + console.error("Error querying table rows:", error); + new Notice(`Error querying table: ${(error as Error).message}`); + editor.replaceSelection(`\nError querying table ${selectedTable}.\n`); + } + }); + + modal.open(); + } catch (error) { + console.error("Error fetching tables:", error); + new Notice(`Error fetching tables: ${(error as Error).message}`); + } + } } diff --git a/src/components/AddTextEntryModal.ts b/src/components/AddTextEntryModal.ts new file mode 100644 index 0000000..baf21b6 --- /dev/null +++ b/src/components/AddTextEntryModal.ts @@ -0,0 +1,146 @@ +import { App, Modal, Notice, TextAreaComponent, Setting } from 'obsidian'; +import { DBService } from '../DBService'; // Adjust path +import { AddTextButtonConfig } from '../webcomponents/AddTextButton/AddTextButton'; // Import config type + +export class AddTextEntryModal extends Modal { + private dbService: DBService; + private config: AddTextButtonConfig; + private inputText: string = ''; + private textArea: TextAreaComponent | null = null; + + constructor(app: App, dbService: DBService, config: AddTextButtonConfig) { + super(app); + this.dbService = dbService; + this.config = config; // Store the received configuration + this.modalEl.addClass('add-text-entry-modal'); + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('add-text-modal-content'); + + // Use button text or a generic title + contentEl.createEl('h2', { text: this.config.buttonText || 'Add Text Entry' }); + + // Display the target table/column for clarity (optional) + contentEl.createEl('p', { + text: `Adding to table "${this.config.dbTable}", column "${this.config.textColumn}".`, + cls: 'add-text-modal-info' + }); + // Display extra data being added (optional) + if (Object.keys(this.config.extraData).length > 0) { + const extraDataStr = Object.entries(this.config.extraData) + .map(([key, value]) => `${key}: ${value}`) + .join(', '); + contentEl.createEl('p', { + text: `With fixed data: ${extraDataStr}`, + cls: 'add-text-modal-info' + }); + } + + // --- Text Input Area --- + // Create a container div for the text area to center it + const textAreaContainer = contentEl.createDiv({ cls: 'text-area-container' }); + textAreaContainer.style.display = 'flex'; + textAreaContainer.style.justifyContent = 'center'; + textAreaContainer.style.width = '100%'; + textAreaContainer.style.margin = '1rem 0'; + + // Create the text area directly instead of using a Setting + this.textArea = new TextAreaComponent(textAreaContainer); + this.textArea.setPlaceholder("Type your text here...") + .setValue(this.inputText) + .onChange((value) => { + this.inputText = value; + }); + + // Style the text area + this.textArea.inputEl.style.width = '90%'; + this.textArea.inputEl.style.height = '150px'; + this.textArea.inputEl.style.margin = '0 auto'; + this.textArea.inputEl.style.display = 'block'; + this.textArea.inputEl.addClass('add-text-entry-input'); + + // --- Action Buttons --- + new Setting(contentEl) + .addButton((btn) => + btn + .setButtonText('Save Entry') + .setCta() + .onClick(() => { + this.handleSave(); + }) + ) + .addButton((btn) => + btn + .setButtonText('Cancel') + .onClick(() => { + this.close(); + }) + ); + + // Focus the textarea when the modal opens + this.textArea?.inputEl.focus(); + } + + private async handleSave() { + const textToSave = this.inputText.trim(); + + if (!textToSave) { + new Notice("Please enter some text."); + this.textArea?.inputEl.focus(); // Focus back + return; + } + + // --- Prepare SQL --- + const columnsToInsert = [this.config.textColumn]; + const valuesToInsert = [textToSave]; + const placeholders = ['?']; + + // Add extra data columns and values + for (const [key, value] of Object.entries(this.config.extraData)) { + columnsToInsert.push(key); // Use key directly (e.g., 'period', 'key') + valuesToInsert.push(value); + placeholders.push('?'); + } + + // generate a random uuid for the entry + const uuid = this.dbService.generateUuid(); + const createdAt = new Date().toISOString(); + // Add the uuid to the columns and values + columnsToInsert.push("uuid"); + valuesToInsert.push(uuid); + placeholders.push('?'); + columnsToInsert.push("createdAt"); + columnsToInsert.push("updatedAt"); + valuesToInsert.push(createdAt); + valuesToInsert.push(createdAt); + placeholders.push('?'); + placeholders.push('?'); + + // Quote table and column names + const quotedTable = `"${this.config.dbTable}"`; + const quotedColumns = columnsToInsert.map(col => `"${col}"`).join(', '); + const placeholderString = placeholders.join(', '); + + const sql = `INSERT INTO ${quotedTable} (${quotedColumns}) VALUES (${placeholderString});`; + const params = valuesToInsert; + + // --- Execute Query --- + try { + await this.dbService.runQuery(sql, params); // Assumes runQuery for INSERT + new Notice(`Entry added to "${this.config.dbTable}"!`); + this.close(); + // Optionally trigger a refresh or callback here if needed + } catch (error) { + console.error(`Error saving text entry to ${this.config.dbTable}:`, error); + new Notice(`Failed to save entry. Check console. Error: ${error.message}`); + } + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} \ No newline at end of file diff --git a/src/webcomponents/AddTextButton/AddTextButton.ts b/src/webcomponents/AddTextButton/AddTextButton.ts new file mode 100644 index 0000000..5dde322 --- /dev/null +++ b/src/webcomponents/AddTextButton/AddTextButton.ts @@ -0,0 +1,108 @@ +import { Plugin, Notice, App, ButtonComponent, MarkdownPostProcessorContext } from 'obsidian'; +import { DBService } from '../../DBService'; // Adjust path +import { AddTextEntryModal } from '../../components/AddTextEntryModal'; // Adjust path +import { replacePlaceholders } from '../../helpers/replacePlaceholders'; // Import your placeholder function - ADJUST PATH + +// Re-define or import the config interface if not imported from component +export interface AddTextButtonConfig { + buttonText: string; + dbTable: string; + textColumn: string; + extraData: Record; +} + +/** + * Registers the post-processor for add-text buttons and the global event listener. + */ +export function registerAddTextSupport(plugin: Plugin, dbService: DBService) { + + // --- 1. Register the Post Processor --- + plugin.registerMarkdownPostProcessor((el: HTMLElement, ctx: MarkdownPostProcessorContext) => { + const placeholders = el.querySelectorAll('div.add-text-button-placeholder'); + + placeholders.forEach(placeholder => { + if (placeholder.dataset.processed) return; + placeholder.dataset.processed = 'true'; + + // --- Read Raw Configuration from Attributes --- + const rawDataTable = placeholder.dataset.table; + const rawTextColumn = placeholder.dataset.column; + const rawButtonText = placeholder.dataset.buttonText || `Add Entry to ${rawDataTable || 'Table'}`; + + // --- Basic Validation (on raw attributes) --- + if (!rawDataTable || !rawTextColumn) { + placeholder.textContent = '[Error: add-text-button-placeholder requires data-table and data-column attributes]'; + placeholder.style.color = 'var(--text-error)'; + placeholder.style.fontFamily = 'monospace'; + console.error('AddTextProcessor: Missing required attributes data-table or data-column', placeholder); + return; + } + + // --- Collect Raw Extra Data --- + const rawExtraData: Record = {}; + // Iterate over dataset keys + for (const key in placeholder.dataset) { + // Include keys other than the standard ones and 'processed' + if (key !== 'table' && key !== 'column' && key !== 'buttonText' && key !== 'processed') { + rawExtraData[key] = placeholder.dataset[key]; + } + } + + // --- Render the Button --- + placeholder.innerHTML = ''; // Clear placeholder content + placeholder.addClass('add-text-button-container'); + + // Use Obsidian's ButtonComponent for styling + const button = new ButtonComponent(placeholder) + // Set initial text (placeholders will be resolved onClick) + .setButtonText(replacePlaceholders(rawButtonText)) // Resolve button text once for display + .setClass('add-text-trigger-button'); + button.buttonEl.style.margin = '0.5em 0'; + + // --- Attach Click Handler --- + button.onClick(() => { + // --- Apply Placeholders NOW (inside onClick) --- + const processedExtraData: Record = {}; + for (const [key, rawValue] of Object.entries(rawExtraData)) { + // Only process if the raw value exists + processedExtraData[key] = rawValue !== undefined ? replacePlaceholders(rawValue) : undefined; + } + const processedButtonText = replacePlaceholders(rawButtonText); + + // --- Construct Final Config --- + const finalConfig: AddTextButtonConfig = { + buttonText: processedButtonText, + dbTable: rawDataTable, // Table/Column names usually don't need placeholder replacement + textColumn: rawTextColumn, + extraData: processedExtraData + }; + + // --- Dispatch Event with *processed* config --- + placeholder.dispatchEvent(new CustomEvent('request-add-text-modal', { + bubbles: true, + composed: true, + detail: finalConfig // Pass the processed config + })); + }); + }); + }); + + // --- 2. Register the Global Event Listener (Remains the Same) --- + plugin.registerDomEvent( + document, + 'request-add-text-modal' as keyof DocumentEventMap, + (evt: Event) => { + if (!(evt instanceof CustomEvent) || !evt.detail) return; + + const config = evt.detail as AddTextButtonConfig; + + if (!dbService) { + new Notice("Database service is not ready."); + return; + } + + const appInstance = plugin.app; + new AddTextEntryModal(appInstance, dbService, config).open(); + } + ); +} \ No newline at end of file diff --git a/src/webcomponents/SqlChart/registerSqlChart.ts b/src/webcomponents/SqlChart/registerSqlChart.ts index 8435e7f..18e43f4 100644 --- a/src/webcomponents/SqlChart/registerSqlChart.ts +++ b/src/webcomponents/SqlChart/registerSqlChart.ts @@ -1,9 +1,8 @@ import { MarkdownPostProcessorContext, Plugin } from 'obsidian'; -import { DBService } from '../../DBService'; // Adjust path as needed -import { processSqlChartBlock } from '../../codeblocks/processSqlChartBlock'; // Adjust path as needed +import { DBService } from '../../DBService'; +import { processSqlChartBlock } from '../../codeblocks/processSqlChartBlock'; import { replacePlaceholders } from 'src/helpers/replacePlaceholders'; -// This function sets up the post-processor export function registerSqlChartRenderer(plugin: Plugin, dbService: DBService) { plugin.registerMarkdownPostProcessor( @@ -13,13 +12,10 @@ export function registerSqlChartRenderer(plugin: Plugin, dbService: DBService) { const chartContainers = el.querySelectorAll('div.sql-chart-render'); if (chartContainers.length === 0) { - return; // No charts found in this section + return; } - // Use Promise.all to process charts potentially in parallel (optional) - // Or a simple loop if preferred: for (const container of chartContainers) { ... } await Promise.all(Array.from(chartContainers).map(async (container) => { - // Check if already processed (important if post-processor runs multiple times) if (container.dataset.sqlChartProcessed) { return; } @@ -28,8 +24,6 @@ export function registerSqlChartRenderer(plugin: Plugin, dbService: DBService) { const source = container.innerHTML.trim(); const parsedSource = replacePlaceholders(source); - - // Clear the original config text before rendering container.innerHTML = ''; // Clear the container if (!source) { @@ -38,25 +32,18 @@ export function registerSqlChartRenderer(plugin: Plugin, dbService: DBService) { return; } - // Add a loading indicator (optional) const loadingEl = container.createEl("p", { text: "Loading chart..." }); try { - // Call your existing core logic directly on the container await processSqlChartBlock(dbService, parsedSource, container); - // Remove loading indicator if processSqlChartBlock doesn't clear the container itself loadingEl.remove(); } catch (error: any) { console.error("Error rendering SQL Chart from div:", error); - // Clear potential loading message or partial render container.innerHTML = ''; container.createEl("p", { text: "Error rendering SQL Chart:" }); - // Display the error message safely container.createEl("pre", { text: String(error.message || error) }); } })); } ); - - console.log("SQL Chart Renderer registered."); } \ No newline at end of file diff --git a/src/webcomponents/index.ts b/src/webcomponents/index.ts index 5dcd2d5..ec6e955 100644 --- a/src/webcomponents/index.ts +++ b/src/webcomponents/index.ts @@ -4,5 +4,6 @@ import { registerTextInput } from "./TextInput/registerTextInput"; import { registerTimestampUpdaterButton } from "./TimestampUpdaterButton/registerTimestampUpdaterButton"; import { registerSqlChartRenderer } from "./SqlChart/registerSqlChart"; import { registerMoodNoteButtonProcessor } from "./MoodNote/moodButtonProcessor"; +import { registerAddTextSupport } from "./AddTextButton/AddTextButton"; -export { registerHabitCounter, registerBooleanSwitch, registerTextInput, registerTimestampUpdaterButton, registerSqlChartRenderer, registerMoodNoteButtonProcessor }; \ No newline at end of file +export { registerHabitCounter, registerBooleanSwitch, registerTextInput, registerTimestampUpdaterButton, registerSqlChartRenderer, registerMoodNoteButtonProcessor, registerAddTextSupport }; \ No newline at end of file