From d96ba2a5f1c4628ed8062394320f1f6358792203 Mon Sep 17 00:00:00 2001 From: Andrea Alberti Date: Wed, 14 Aug 2024 19:53:21 +0200 Subject: [PATCH] Backups --- src/main.ts | 85 +++++++++++++++++++++++++++++++--------- src/settings_tab.ts | 95 ++++++++++++++++++++++++++++----------------- src/utils.ts | 8 +++- styles/styles.css | 21 +++++----- 4 files changed, 146 insertions(+), 63 deletions(-) diff --git a/src/main.ts b/src/main.ts index 1f9a6ea..7e897c9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -61,13 +61,13 @@ export default class PluginsAnnotations extends Plugin { } /* Load settings for different versions */ - importSettings(data: unknown): {importedSettings: unknown, wasUpdated: boolean} { + async importSettings(data: unknown): Promise<{importedSettings: unknown, wasUpdated: boolean}> { // Set to true when the settings are updated to the new format let wasUpdated = false; // Nested function to handle different versions of settings - const getSettingsFromData = (data: unknown): unknown => { + const getSettingsFromData = async (data: unknown): Promise => { if(data === null) { // if the file is empty return data; } else if (isPluginsAnnotationsSettings(data)) { @@ -98,8 +98,9 @@ export default class PluginsAnnotations extends Plugin { markdown_file_path: DEFAULT_SETTINGS.markdown_file_path }; wasUpdated = true; - return getSettingsFromData(newSettings); - + console.log("VERSION 1.4"); + await this.backupSettings('Backup before upgrading from 1.4 to 1.5',newSettings); + return await getSettingsFromData(newSettings); } else if (isSettingsFormat_1_3_0(data)) { // previous versions where the name of the plugins was not stored // Upgrade annotations format const upgradedAnnotations: PluginAnnotationDict_1_4_0 = {}; @@ -120,39 +121,75 @@ export default class PluginsAnnotations extends Plugin { plugins_annotations_uuid: DEFAULT_SETTINGS_1_4_0.plugins_annotations_uuid, }; wasUpdated = true; - return getSettingsFromData(newSettings); + console.log("VERSION 1.3"); + + await this.backupSettings('Backup before upgrading from 1.3 to 1.4',newSettings); + return await getSettingsFromData(newSettings); } else { // Very first version of the plugin 1.0 -- no options were stored, only the dictionary of annotations const newSettings: PluginsAnnotationsSettings_1_3_0 = { ...DEFAULT_SETTINGS_1_3_0 }; newSettings.annotations = isPluginAnnotationDictFormat_1_3_0(data) ? data : DEFAULT_SETTINGS_1_3_0.annotations; wasUpdated = true; - return getSettingsFromData(newSettings); + console.log("VERSION 1.0"); + await this.backupSettings('Backup before upgrading from 1.0 to 1.3',newSettings); + return await getSettingsFromData(newSettings); } }; - const importedSettings = getSettingsFromData(data); + const importedSettings = await getSettingsFromData(data); return {importedSettings, wasUpdated}; } - async loadSettings(): Promise { + async backupSettings(backupName: string, settings: unknown) { + // Ensure settings is an object + if (typeof settings !== 'object' || settings === null) return; + + let settingsWithoutBackup; + + // Remove the backups field from the settings to be backed up + if (settings.hasOwnProperty('backups')) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { backups: _, ...rest } = settings as { backups: unknown }; + settingsWithoutBackup = rest; + } else { + settingsWithoutBackup = settings; + } + + // Use JSON.parse(JSON.stringify()) for a deep copy + const deepCopiedSettings = JSON.parse(JSON.stringify(settingsWithoutBackup)); + + // Add the backup with the deep-copied settings + this.settings.backups.push({ + name: backupName, + date: new Date(), + settings: deepCopiedSettings + }); + + await this.saveSettings(); + } + + + async loadSettings(data?: unknown): Promise { // Create a mapping of names to IDs for the installed plugins this.pluginNameToIdMap = this.constructPluginNameToIdMap(); this.pluginIdToNameMap = this.generateInvertedMap(this.pluginNameToIdMap); - const data = await this.loadData(); - - if(data.backups) { - data.backups.forEach((backup: PluginBackup) => { - backup.date = new Date(backup.date); // Convert the date string to a Date object - }); + if(data === undefined) { + data = await this.loadData(); } - const {importedSettings, wasUpdated} = this.importSettings(data); + const {importedSettings, wasUpdated} = await this.importSettings(data); // Merge loaded settings with default settings this.settings = Object.assign({}, DEFAULT_SETTINGS, importedSettings); + if (this.settings.backups) { + this.settings.backups.forEach((backup: PluginBackup) => { + backup.date = new Date(backup.date); // Convert the date string to a Date object + }); + } + if(wasUpdated) { this.debouncedSaveAnnotations(); } @@ -392,7 +429,13 @@ export default class PluginsAnnotations extends Plugin { configureAnnotation(annotation_container:HTMLDivElement,annotation_div:HTMLDivElement,pluginId:string,pluginName:string) { - annotation_div.contentEditable = this.settings.editable ? 'true' : 'false'; + if(this.settings.editable) { + annotation_div.contentEditable = 'true'; + annotation_div.classList.add('plugin-comment-annotation-editable'); + } else { + annotation_div.contentEditable = 'false'; + annotation_div.classList.remove('plugin-comment-annotation-editable'); + } const placeholder = (this.settings.label_placeholder).replace(/\$\{plugin_name\}/g, pluginName); @@ -409,8 +452,12 @@ export default class PluginsAnnotations extends Plugin { annoType = AnnotationType.html; annotation_div.classList.add('plugin-comment-placeholder'); - if (this.settings.hide_placeholders) { - annotation_container.classList.add(this.settings.editable ? 'plugin-comment-placeholder' : 'plugin-comment-hidden'); + if (this.settings.hide_placeholders) { // if it is a placeholder + if(this.settings.editable) { // if fields can be edited, set the placeholder tag + annotation_container.classList.add('plugin-comment-placeholder'); + } else { // if fields cannot be edited, just simply hide placeholders + annotation_container.classList.add('plugin-comment-hidden'); + } } } @@ -568,8 +615,10 @@ export default class PluginsAnnotations extends Plugin { if (div instanceof HTMLDivElement) { if(this.settings.editable) { div.contentEditable = 'true'; + div.classList.add('plugin-comment-annotation-editable'); } else { div.contentEditable = 'false'; + div.classList.remove('plugin-comment-annotation-editable'); } } }); diff --git a/src/settings_tab.ts b/src/settings_tab.ts index 3bd7a89..2a20aac 100644 --- a/src/settings_tab.ts +++ b/src/settings_tab.ts @@ -2,9 +2,9 @@ import PluginsAnnotations from "main"; import { handleMarkdownFilePathChange } from "manageAnnotations"; -import { App, Platform, PluginSettingTab, Setting, TextComponent } from "obsidian"; +import { App, Notice, Platform, PluginSettingTab, Setting, TextComponent, TFile } from "obsidian"; import { PluginAnnotationDict } from "types"; -import { parseFilePath, FileSuggestion, downloadJson } from "utils"; +import { parseFilePath, FileSuggestion, downloadJson, showConfirmationDialog } from "utils"; declare const moment: typeof import('moment'); @@ -153,15 +153,21 @@ export class PluginsAnnotationsSettingTab extends PluginSettingTab { text.setPlaceholder('E.g.: 00 Meta/Plugins annotations.md'); text.setValue(this.plugin.settings.markdown_file_path); - const vault_files = this.app.vault.getFiles().filter((f) => f.extension === "md"); - const inputEl = text.inputEl; - new FileSuggestion(this.app, inputEl, vault_files); + const fileSuggestion = new FileSuggestion(this.app, inputEl); + + const updateVaultFiles = () => { + if(fileSuggestion) { + fileSuggestion.setSuggestions(this.app.vault.getFiles().filter((f) => f.extension === "md")); + } + } + + updateVaultFiles(); inputEl.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); - inputEl.dispatchEvent(new Event('change')); + inputEl.blur(); } }); @@ -188,7 +194,8 @@ export class PluginsAnnotationsSettingTab extends PluginSettingTab { md_filepath_error_div.style.display = 'none'; if(await handleMarkdownFilePathChange(this.plugin, filepath)) { this.plugin.settings.markdown_file_path = filepath; - this.plugin.debouncedSaveAnnotations(); + await this.plugin.saveSettings(); + updateVaultFiles(); } else { text.setValue(this.plugin.settings.markdown_file_path); } @@ -311,18 +318,9 @@ export class PluginsAnnotationsSettingTab extends PluginSettingTab { .setButtonText('Create Backup') .setCta() .onClick(async () => { - const backupName = 'Backup name (click to edit)'; - if (backupName) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { backups: _, ...currentSettings } = this.plugin.settings; - this.plugin.settings.backups.push({ - name: backupName, - date: new Date(), - settings: { ...currentSettings } - }); - await this.plugin.saveSettings(); - this.display(); - } + const backupName = 'Untitled backup'; + await this.plugin.backupSettings(backupName,this.plugin.settings); + this.display(); }) ); @@ -331,7 +329,7 @@ export class PluginsAnnotationsSettingTab extends PluginSettingTab { .setButtonText('Download settings') .setCta() .onClick(async () => { - downloadJson(this.plugin.settings); + downloadJson({...this.plugin.settings, backups: []}); }) ); } @@ -343,18 +341,21 @@ export class PluginsAnnotationsSettingTab extends PluginSettingTab { this.plugin.settings.backups.sort((a, b) => b.date.getTime() - a.date.getTime()); // Create a wrapper div for the table - const tableDiv = containerEl.createDiv({ cls: 'plugin-comment-backup-table' }); + const backupTableContainer = containerEl.createDiv({ cls: 'setting-item' }); + + const tableDiv = backupTableContainer.createDiv({ cls: 'plugin-comment-backup-table' }); // Create the header row - const headerRow = tableDiv.createDiv({ cls: 'backup-table-row header' }); - headerRow.createDiv({ cls: 'backup-table-cell', text: 'Backup name (click to edit)' }); - headerRow.createDiv({ cls: 'backup-table-cell', text: 'Created on' }); + const headerRow = tableDiv.createDiv({ cls: 'plugin-comment-backup-table-row header' }); + headerRow.createDiv({ cls: 'plugin-comment-backup-table-cell', text: 'Backup name (click to edit)' }); + headerRow.createDiv({ cls: 'plugin-comment-backup-table-cell', text: 'Created on' }); + headerRow.createDiv({ cls: 'plugin-comment-backup-table-cell', text: 'Actions' }); this.plugin.settings.backups.forEach((backup, index) => { - const rowDiv = tableDiv.createDiv({ cls: 'backup-table-row' }); + const rowDiv = tableDiv.createDiv({ cls: 'plugin-comment-backup-table-row' }); // Backup name cell - const nameCell = rowDiv.createDiv({ cls: 'backup-table-cell editable-backup-name' }); + const nameCell = rowDiv.createDiv({ cls: 'plugin-comment-backup-table-cell plugin-comment-backup-name' }); const nameDiv = nameCell.createDiv({ text: backup.name, attr: { contenteditable: 'true' } }); // Handle saving the updated name when editing is complete @@ -373,24 +374,48 @@ export class PluginsAnnotationsSettingTab extends PluginSettingTab { }); // Created on cell - const dateCell = rowDiv.createDiv({ cls: 'backup-table-cell' }); + const dateCell = rowDiv.createDiv({ cls: 'plugin-comment-backup-table-cell' }); dateCell.setText(moment(backup.date).format('YYYY-MM-DD HH:mm:ss')); // Add Restore and Delete buttons to the last cell - const actionCell = rowDiv.createDiv({ cls: 'backup-table-cell actions' }); + const actionCell = rowDiv.createDiv({ cls: 'plugin-comment-backup-table-cell plugin-comment-backup-buttons' }); actionCell.createEl('button', { text: 'Restore', cls: 'mod-cta' }) .addEventListener('click', async () => { - this.plugin.settings = { backups: this.plugin.settings.backups, ...backup.settings }; - await this.plugin.saveSettings(); - alert(`Annotations restored from backup: ${backup.name}`); - this.display(); // Refresh the display to reflect the restored annotations + const answer = await showConfirmationDialog(this.plugin.app, 'Delete backup', + createFragment((frag) => { + frag.appendText('You are about to restore the settings from the backup named '); + frag.createEl('strong',{text: this.plugin.settings.backups[index].name}); + frag.appendText(' created on '); + frag.createEl('strong',{text: moment(this.plugin.settings.backups[index].date).format('YYYY-MM-DD HH:mm:ss')}); + frag.appendText('. If you proceed, the current settings will be overwritten with those from the backup. \ + If you want to keep a copy of the current settings, make a backup before proceeding.\ + Do you want to proceed restoring the seettings from the backup?'); + })); + if(answer) { + const backups = [...this.plugin.settings.backups]; // store a copy of the backups before restoring the old settings + this.plugin.loadSettings(this.plugin.settings.backups[index].settings); + this.plugin.settings.backups = backups; // restore the copy of the backups + await this.plugin.saveSettings(); // save the restored settings with the backups + new Notice(`Annotations restored from backup "${backup.name}"`); + this.display(); // Refresh the display to reflect the restored annotations + } }); actionCell.createEl('button', { text: 'Delete', cls: 'mod-cta' }) .addEventListener('click', async () => { - this.plugin.settings.backups.splice(index, 1); - await this.plugin.saveSettings(); - this.display(); // Refresh the display to remove the deleted backup + const answer = await showConfirmationDialog(this.plugin.app, 'Delete backup', + createFragment((frag) => { + frag.appendText('You are about to delete the backup named '); + frag.createEl('strong',{text: this.plugin.settings.backups[index].name}); + frag.appendText(' created on '); + frag.createEl('strong',{text: moment(this.plugin.settings.backups[index].date).format('YYYY-MM-DD HH:mm:ss')}); + frag.appendText('. Do you want to continue?'); + })); + if(answer) { + this.plugin.settings.backups.splice(index, 1); + await this.plugin.saveSettings(); + this.display(); // Refresh the display to remove the deleted backup + } }); }); } diff --git a/src/utils.ts b/src/utils.ts index aeb3c7c..c114622 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -83,7 +83,9 @@ export async function createFolderIfNotExists(vault: Vault, folderPath: string) /* File suggestions */ export class FileSuggestion extends AbstractInputSuggest { - constructor(app: App, inputEl: HTMLInputElement, private files:TFile[], private onSelectCallback: (file: TFile) => void = (v: TFile) => {}) { + private files:TFile[] = []; + + constructor(app: App, inputEl: HTMLInputElement, private onSelectCallback: (file: TFile) => void = (v: TFile) => {}) { super(app, inputEl); } @@ -118,6 +120,10 @@ export class FileSuggestion extends AbstractInputSuggest { this.textInputEl.focus() this.close(); } + + setSuggestions(files:TFile[]) { + this.files = files; + } } diff --git a/styles/styles.css b/styles/styles.css index cb28537..43e7dfe 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -33,6 +33,10 @@ width: 100%; } +.plugin-comment-annotation-editable { + cursor: text; +} + /* Show comment container when hovering over the plugin container */ .setting-item:hover .plugin-comment.plugin-comment-placeholder { display: block; @@ -60,7 +64,7 @@ div.plugin-comment-instructions strong { margin-bottom: 0; } -.plugin-comment-backup, +.plugin-comment-backup-table, .plugin-comment-uninstalled { margin-left:40px !important; } @@ -71,27 +75,26 @@ div.plugin-comment-instructions strong { border-collapse: collapse; } -.backup-table-row { +.plugin-comment-backup-table-row { display: table-row; } -.backup-table-cell { +.plugin-comment-backup-table-cell { display: table-cell; - padding: 8px; + padding: 8px 0 8px 0; border-bottom: 1px solid var(--background-modifier-border); vertical-align: middle; } -.backup-table-row.header .backup-table-cell { - font-weight: bold; - background-color: var(--background-modifier-accent); +.plugin-comment-backup-table-row.header .plugin-comment-backup-table-cell { + font-weight: var(--font-semibold); } -.editable-backup-name { +.plugin-comment-backup-name { cursor: text; } -.actions { +.plugin-comment-backup-buttons { display: flex; gap: 10px; } \ No newline at end of file