new add text modal

This commit is contained in:
stfrigerio 2025-04-16 19:26:47 +02:00
parent df2f429049
commit 148c874f02
7 changed files with 338 additions and 52 deletions

View file

@ -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,6 +51,7 @@ export default class SQLiteDBPlugin extends Plugin {
registerSqlChartRenderer(this, this.dbService);
registerTimestampUpdaterButton(this);
registerMoodNoteButtonProcessor(this, this.dbService);
registerAddTextSupport(this, this.dbService);
//? Codeblocks
this.registerMarkdownCodeBlockProcessor(

View file

@ -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
}

View file

@ -10,6 +10,8 @@ export async function inspectTableStructure(
editor: Editor,
app: any
) {
// 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.");
@ -46,4 +48,46 @@ export async function inspectTableStructure(
});
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}`);
}
}
}

View file

@ -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();
}
}

View file

@ -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<string, any>;
}
/**
* 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<HTMLDivElement>('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<string, string | undefined> = {};
// 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<string, any> = {};
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();
}
);
}

View file

@ -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<HTMLElement>('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.");
}

View file

@ -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 };
export { registerHabitCounter, registerBooleanSwitch, registerTextInput, registerTimestampUpdaterButton, registerSqlChartRenderer, registerMoodNoteButtonProcessor, registerAddTextSupport };