mirror of
https://github.com/stfrigerio/sqliteDB.git
synced 2026-07-22 05:38:02 +00:00
new add text modal
This commit is contained in:
parent
df2f429049
commit
148c874f02
7 changed files with 338 additions and 52 deletions
6
main.ts
6
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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
146
src/components/AddTextEntryModal.ts
Normal file
146
src/components/AddTextEntryModal.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
108
src/webcomponents/AddTextButton/AddTextButton.ts
Normal file
108
src/webcomponents/AddTextButton/AddTextButton.ts
Normal 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();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
|
|
@ -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 };
|
||||
Loading…
Reference in a new issue