diff --git a/src/bulk-edit-modal.ts b/src/bulk-edit-modal.ts index 55fd839..47b7149 100644 --- a/src/bulk-edit-modal.ts +++ b/src/bulk-edit-modal.ts @@ -1,7 +1,7 @@ import {AbstractInputSuggest, App, Modal, Notice, setIcon, Setting, TFile} from "obsidian"; import type BulkPropertiesPlugin from "./main"; import {getPropertyValues, getSelectedFiles} from "./files"; -import {confirmEmptyValue, confirmReplace} from "./confirm-modal"; +import {confirmDeleteFiles, confirmEmptyValue, confirmReplace} from "./confirm-modal"; import {withProgress} from "./progress"; import {makeToggleAccessible, updateToggleAriaChecked} from "./accessible-toggle"; @@ -119,7 +119,8 @@ export class BulkEditModal extends Modal { private countEl!: HTMLElement; private pendingSaves: Map> = new Map(); private fileCheckboxes: Map = new Map(); - private updateBtn!: HTMLButtonElement; + private updateBtn?: HTMLButtonElement; + private deleteBtn!: HTMLButtonElement; private selectAllBtn!: HTMLButtonElement; private deselectAllBtn!: HTMLButtonElement; private uiLocked = false; @@ -140,7 +141,7 @@ export class BulkEditModal extends Modal { const {settings} = this.plugin; contentEl.addClass("bulk-properties-modal"); - new Setting(contentEl).setName("Bulk edit properties").setHeading(); + new Setting(contentEl).setName("Bulk edit selected files").setHeading(); if (this.fileSelection.size === 0) { contentEl.createEl("p", { @@ -180,8 +181,9 @@ export class BulkEditModal extends Modal { }); const editableProperties = settings.properties.filter(p => p.name !== settings.selectionProperty); + const hasEditableProperties = editableProperties.length > 0; - if (editableProperties.length === 0) { + if (!hasEditableProperties) { const p = contentEl.createEl("p"); p.appendText("No properties configured. Add properties in the "); // eslint-disable-next-line obsidianmd/ui/sentence-case -- mid-sentence text @@ -201,51 +203,63 @@ export class BulkEditModal extends Modal { /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */ }); p.appendText("."); - return; } - const lastSelected = settings.lastSelectedProperty; - const rememberedExists = lastSelected && editableProperties.some(p => p.name === lastSelected); - this.selectedProperty = rememberedExists ? lastSelected : editableProperties[0]?.name ?? ""; + if (hasEditableProperties) { + const lastSelected = settings.lastSelectedProperty; + const rememberedExists = lastSelected && editableProperties.some(p => p.name === lastSelected); + this.selectedProperty = rememberedExists ? lastSelected : editableProperties[0]?.name ?? ""; - new Setting(contentEl) - .setName("Property") - .addDropdown(dropdown => { - for (const prop of editableProperties) { - dropdown.addOption(prop.name, prop.name); - } - dropdown.setValue(this.selectedProperty); - dropdown.onChange(value => { - this.selectedProperty = value; - this.renderValueInput(); - }); - }); - - this.valueContainerEl = contentEl.createDiv(); - this.renderValueInput(); - - new Setting(contentEl) - .setName("Deselect when finished") - .addToggle(toggle => { - makeToggleAccessible(toggle, "Deselect when finished", this.deselectWhenFinished); - toggle - .setValue(this.deselectWhenFinished) - .onChange(value => { - this.deselectWhenFinished = value; - updateToggleAriaChecked(toggle, value); + new Setting(contentEl) + .setName("Property") + .addDropdown(dropdown => { + for (const prop of editableProperties) { + dropdown.addOption(prop.name, prop.name); + } + dropdown.setValue(this.selectedProperty); + dropdown.onChange(value => { + this.selectedProperty = value; + this.renderValueInput(); }); - }); + }); - new Setting(contentEl) + this.valueContainerEl = contentEl.createDiv(); + this.renderValueInput(); + + new Setting(contentEl) + .setName("Deselect when finished") + .addToggle(toggle => { + makeToggleAccessible(toggle, "Deselect when finished", this.deselectWhenFinished); + toggle + .setValue(this.deselectWhenFinished) + .onChange(value => { + this.deselectWhenFinished = value; + updateToggleAriaChecked(toggle, value); + }); + }); + } + + const footer = new Setting(contentEl) .addButton(btn => { btn - .setButtonText("Update") + .setButtonText("Delete selected files") + .setWarning() + .onClick(() => { + void this.doDelete(); + }); + this.deleteBtn = btn.buttonEl; + }); + if (hasEditableProperties) { + footer.addButton(btn => { + btn + .setButtonText("Update properties") .setCta() .onClick(() => { void this.doUpdate(); }); this.updateBtn = btn.buttonEl; }); + } } override onClose() { @@ -262,10 +276,15 @@ export class BulkEditModal extends Modal { const checked = this.getCheckedFiles().length; const total = this.fileSelection.size; this.countEl.setText(`${checked} of ${total} file${total === 1 ? "" : "s"} selected`); - if (this.updateBtn && !this.uiLocked) { - const hasUncommitted = this.activePillInput !== null - && this.activePillInput.value.trim() !== ""; - this.updateBtn.disabled = checked === 0 || hasUncommitted; + if (!this.uiLocked) { + if (this.updateBtn) { + const hasUncommitted = this.activePillInput !== null + && this.activePillInput.value.trim() !== ""; + this.updateBtn.disabled = checked === 0 || hasUncommitted; + } + if (this.deleteBtn) { + this.deleteBtn.disabled = checked === 0; + } } } @@ -664,10 +683,14 @@ export class BulkEditModal extends Modal { } if (this.selectAllBtn) this.selectAllBtn.disabled = !enabled; if (this.deselectAllBtn) this.deselectAllBtn.disabled = !enabled; + const checkedCount = this.getCheckedFiles().length; if (this.updateBtn) { const hasUncommitted = this.activePillInput !== null && this.activePillInput.value.trim() !== ""; - this.updateBtn.disabled = !enabled || this.getCheckedFiles().length === 0 || hasUncommitted; + this.updateBtn.disabled = !enabled || checkedCount === 0 || hasUncommitted; + } + if (this.deleteBtn) { + this.deleteBtn.disabled = !enabled || checkedCount === 0; } } @@ -802,4 +825,43 @@ export class BulkEditModal extends Modal { } new Notice(msg); } + + private async doDelete() { + this.uiLocked = true; + this.setUIEnabled(false); + await Promise.all(this.pendingSaves.values()); + + const filesToDelete = this.getCheckedFiles(); + if (filesToDelete.length === 0) { + this.uiLocked = false; + this.setUIEnabled(true); + new Notice("No files selected"); + return; + } + + const confirmed = await confirmDeleteFiles(this.app, filesToDelete.length); + if (!confirmed) { + this.uiLocked = false; + this.setUIEnabled(true); + return; + } + + this.close(); + + const result = await withProgress( + filesToDelete, + "Deleting", + (file) => this.app.fileManager.trashFile(file), + ); + + const {succeeded, failed, cancelled, total} = result; + let msg = `Deleted ${succeeded} file${succeeded === 1 ? "" : "s"}`; + if (cancelled) { + msg = `Deleted ${succeeded} of ${total} file${total === 1 ? "" : "s"} (cancelled)`; + } + if (failed.length > 0) { + msg += `, failed on ${failed.length}: ${failed.join(", ")}`; + } + new Notice(msg); + } } diff --git a/src/confirm-modal.ts b/src/confirm-modal.ts index 848db76..bfe91be 100644 --- a/src/confirm-modal.ts +++ b/src/confirm-modal.ts @@ -1,28 +1,47 @@ import {App, Modal, Setting} from "obsidian"; +interface ConfirmOptions { + confirmText?: string; + confirmStyle?: "cta" | "warning"; +} + class ConfirmModal extends Modal { private resolved = false; private resolve: (value: boolean) => void; private readonly message: string; + private readonly confirmText: string; + private readonly confirmStyle: "cta" | "warning"; - constructor(app: App, message: string, resolve: (value: boolean) => void) { + constructor( + app: App, + message: string, + resolve: (value: boolean) => void, + options?: ConfirmOptions, + ) { super(app); this.message = message; this.resolve = resolve; + this.confirmText = options?.confirmText ?? "Continue"; + this.confirmStyle = options?.confirmStyle ?? "cta"; } override onOpen() { const {contentEl} = this; contentEl.createEl("p", {text: this.message}); new Setting(contentEl) - .addButton(btn => btn - .setButtonText("Continue") - .setCta() - .onClick(() => { + .addButton(btn => { + btn.setButtonText(this.confirmText); + if (this.confirmStyle === "warning") { + btn.setWarning(); + } else { + btn.setCta(); + } + btn.onClick(() => { this.resolved = true; this.resolve(true); this.close(); - })) + }); + }) .addButton(btn => btn .setButtonText("Cancel") .onClick(() => { @@ -74,3 +93,17 @@ export function confirmReplace( new ConfirmModal(app, message, resolve).open(); }); } + +export function confirmDeleteFiles( + app: App, + fileCount: number, +): Promise { + const noun = fileCount === 1 ? "file" : "files"; + const message = `This will delete ${fileCount} ${noun}. Are you sure?`; + return new Promise(resolve => { + new ConfirmModal(app, message, resolve, { + confirmText: "Delete", + confirmStyle: "warning", + }).open(); + }); +}