mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 05:28:59 +01:00 
			
		
		
		
	feat(react/dialogs): port note revisions
This commit is contained in:
		
							parent
							
								
									f7e7b38551
								
							
						
					
					
						commit
						7ac0828ae7
					
				@ -92,7 +92,9 @@ export type CommandMappings = {
 | 
				
			|||||||
    closeTocCommand: CommandData;
 | 
					    closeTocCommand: CommandData;
 | 
				
			||||||
    closeHlt: CommandData;
 | 
					    closeHlt: CommandData;
 | 
				
			||||||
    showLaunchBarSubtree: CommandData;
 | 
					    showLaunchBarSubtree: CommandData;
 | 
				
			||||||
    showRevisions: CommandData;
 | 
					    showRevisions: CommandData & {
 | 
				
			||||||
 | 
					        noteId?: string | null;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
    showLlmChat: CommandData;
 | 
					    showLlmChat: CommandData;
 | 
				
			||||||
    createAiChat: CommandData;
 | 
					    createAiChat: CommandData;
 | 
				
			||||||
    showOptions: CommandData & {
 | 
					    showOptions: CommandData & {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,380 +0,0 @@
 | 
				
			|||||||
import { t } from "../../services/i18n.js";
 | 
					 | 
				
			||||||
import utils from "../../services/utils.js";
 | 
					 | 
				
			||||||
import server from "../../services/server.js";
 | 
					 | 
				
			||||||
import toastService from "../../services/toast.js";
 | 
					 | 
				
			||||||
import appContext from "../../components/app_context.js";
 | 
					 | 
				
			||||||
import openService from "../../services/open.js";
 | 
					 | 
				
			||||||
import protectedSessionHolder from "../../services/protected_session_holder.js";
 | 
					 | 
				
			||||||
import BasicWidget from "../basic_widget.js";
 | 
					 | 
				
			||||||
import dialogService, { openDialog } from "../../services/dialog.js";
 | 
					 | 
				
			||||||
import options from "../../services/options.js";
 | 
					 | 
				
			||||||
import type FNote from "../../entities/fnote.js";
 | 
					 | 
				
			||||||
import type { NoteType } from "../../entities/fnote.js";
 | 
					 | 
				
			||||||
import { Dropdown, Modal } from "bootstrap";
 | 
					 | 
				
			||||||
import { renderMathInElement } from "../../services/math.js";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const TPL = /*html*/`
 | 
					 | 
				
			||||||
<div class="revisions-dialog modal fade mx-auto" tabindex="-1" role="dialog">
 | 
					 | 
				
			||||||
    <style>
 | 
					 | 
				
			||||||
        .revisions-dialog .revision-content-wrapper {
 | 
					 | 
				
			||||||
            flex-grow: 1;
 | 
					 | 
				
			||||||
            margin-left: 20px;
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            flex-direction: column;
 | 
					 | 
				
			||||||
            min-width: 0;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .revisions-dialog .revision-content {
 | 
					 | 
				
			||||||
            overflow: auto;
 | 
					 | 
				
			||||||
            word-break: break-word;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .revisions-dialog .revision-content img {
 | 
					 | 
				
			||||||
            max-width: 100%;
 | 
					 | 
				
			||||||
            object-fit: contain;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .revisions-dialog .revision-content pre {
 | 
					 | 
				
			||||||
            max-width: 100%;
 | 
					 | 
				
			||||||
            word-break: break-all;
 | 
					 | 
				
			||||||
            white-space: pre-wrap;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    </style>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="modal-dialog modal-xl" role="document">
 | 
					 | 
				
			||||||
        <div class="modal-content">
 | 
					 | 
				
			||||||
            <div class="modal-header">
 | 
					 | 
				
			||||||
                <h5 class="modal-title flex-grow-1">${t("revisions.note_revisions")}</h5>
 | 
					 | 
				
			||||||
                <button class="revisions-erase-all-revisions-button btn btn-sm"
 | 
					 | 
				
			||||||
                        title="${t("revisions.delete_all_revisions")}"
 | 
					 | 
				
			||||||
                        style="padding: 0 10px 0 10px;" type="button">${t("revisions.delete_all_button")}</button>
 | 
					 | 
				
			||||||
                <button class="help-button" type="button" data-help-page="note-revisions.html" title="${t("revisions.help_title")}">?</button>
 | 
					 | 
				
			||||||
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("revisions.close")}"></button>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div class="modal-body" style="display: flex; height: 80vh;">
 | 
					 | 
				
			||||||
                <div class="dropdown">
 | 
					 | 
				
			||||||
                    <button class="revision-list-dropdown" type="button" style="display: none;"
 | 
					 | 
				
			||||||
                            data-bs-toggle="dropdown" data-bs-display="static">
 | 
					 | 
				
			||||||
                    </button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <div class="revision-list dropdown-menu static" style="position: static; height: 100%; overflow: auto;"></div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <div class="revision-content-wrapper">
 | 
					 | 
				
			||||||
                    <div style="flex-grow: 0; display: flex; justify-content: space-between;">
 | 
					 | 
				
			||||||
                        <h3 class="revision-title" style="margin: 3px; flex-grow: 100;"></h3>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        <div class="revision-title-buttons"></div>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <div class="revision-content use-tn-links"></div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div class="modal-footer py-0">
 | 
					 | 
				
			||||||
                <span class="revisions-snapshot-interval flex-grow-1 my-0 py-0"></span>
 | 
					 | 
				
			||||||
                <span class="maximum-revisions-for-current-note flex-grow-1 my-0 py-0"></span>
 | 
					 | 
				
			||||||
                <button class="revision-settings-button icon-action bx bx-cog my-0 py-0" title="${t("revisions.settings")}"></button>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
</div>`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface RevisionItem {
 | 
					 | 
				
			||||||
    noteId: string;
 | 
					 | 
				
			||||||
    revisionId: string;
 | 
					 | 
				
			||||||
    dateLastEdited: string;
 | 
					 | 
				
			||||||
    contentLength: number;
 | 
					 | 
				
			||||||
    type: NoteType;
 | 
					 | 
				
			||||||
    title: string;
 | 
					 | 
				
			||||||
    isProtected: boolean;
 | 
					 | 
				
			||||||
    mime: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface FullRevision {
 | 
					 | 
				
			||||||
    content: string;
 | 
					 | 
				
			||||||
    mime: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default class RevisionsDialog extends BasicWidget {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private revisionItems: RevisionItem[];
 | 
					 | 
				
			||||||
    private note: FNote | null;
 | 
					 | 
				
			||||||
    private revisionId: string | null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private modal!: Modal;
 | 
					 | 
				
			||||||
    private listDropdown!: Dropdown;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private $list!: JQuery<HTMLElement>;
 | 
					 | 
				
			||||||
    private $listDropdown!: JQuery<HTMLElement>;
 | 
					 | 
				
			||||||
    private $content!: JQuery<HTMLElement>;
 | 
					 | 
				
			||||||
    private $title!: JQuery<HTMLElement>;
 | 
					 | 
				
			||||||
    private $titleButtons!: JQuery<HTMLElement>;
 | 
					 | 
				
			||||||
    private $eraseAllRevisionsButton!: JQuery<HTMLElement>;
 | 
					 | 
				
			||||||
    private $maximumRevisions!: JQuery<HTMLElement>;
 | 
					 | 
				
			||||||
    private $snapshotInterval!: JQuery<HTMLElement>;
 | 
					 | 
				
			||||||
    private $revisionSettingsButton!: JQuery<HTMLElement>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    constructor() {
 | 
					 | 
				
			||||||
        super();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.revisionItems = [];
 | 
					 | 
				
			||||||
        this.note = null;
 | 
					 | 
				
			||||||
        this.revisionId = null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    doRender() {
 | 
					 | 
				
			||||||
        this.$widget = $(TPL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.modal = Modal.getOrCreateInstance(this.$widget[0]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$list = this.$widget.find(".revision-list");
 | 
					 | 
				
			||||||
        this.$listDropdown = this.$widget.find(".revision-list-dropdown");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.listDropdown = Dropdown.getOrCreateInstance(this.$listDropdown[0], { autoClose: false });
 | 
					 | 
				
			||||||
        this.$content = this.$widget.find(".revision-content");
 | 
					 | 
				
			||||||
        this.$title = this.$widget.find(".revision-title");
 | 
					 | 
				
			||||||
        this.$titleButtons = this.$widget.find(".revision-title-buttons");
 | 
					 | 
				
			||||||
        this.$eraseAllRevisionsButton = this.$widget.find(".revisions-erase-all-revisions-button");
 | 
					 | 
				
			||||||
        this.$snapshotInterval = this.$widget.find(".revisions-snapshot-interval");
 | 
					 | 
				
			||||||
        this.$maximumRevisions = this.$widget.find(".maximum-revisions-for-current-note");
 | 
					 | 
				
			||||||
        this.$revisionSettingsButton = this.$widget.find(".revision-settings-button");
 | 
					 | 
				
			||||||
        this.listDropdown.show();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$listDropdown.parent().on("hide.bs.dropdown", (e) => {
 | 
					 | 
				
			||||||
            this.modal.hide();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$widget.on("shown.bs.modal", () => {
 | 
					 | 
				
			||||||
            this.$list.find(`[data-revision-id="${this.revisionId}"]`).trigger("focus");
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$eraseAllRevisionsButton.on("click", async () => {
 | 
					 | 
				
			||||||
            if (!this.note) {
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const text = t("revisions.confirm_delete_all");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (await dialogService.confirm(text)) {
 | 
					 | 
				
			||||||
                await server.remove(`notes/${this.note.noteId}/revisions`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                this.modal.hide();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                toastService.showMessage(t("revisions.revisions_deleted"));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$list.on("focus", ".dropdown-item", (e) => {
 | 
					 | 
				
			||||||
            this.$list.find(".dropdown-item").each((i, el) => {
 | 
					 | 
				
			||||||
                $(el).toggleClass("active", el === e.target);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            this.setContentPane();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$revisionSettingsButton.on("click", async () => {
 | 
					 | 
				
			||||||
            appContext.tabManager.openContextWithNote("_optionsOther", { activate: true });
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async showRevisionsEvent({ noteId = appContext.tabManager.getActiveContextNoteId() }) {
 | 
					 | 
				
			||||||
        if (!noteId) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        openDialog(this.$widget);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await this.loadRevisions(noteId);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async loadRevisions(noteId: string) {
 | 
					 | 
				
			||||||
        this.$title.empty();
 | 
					 | 
				
			||||||
        this.$list.empty();
 | 
					 | 
				
			||||||
        this.$content.empty();
 | 
					 | 
				
			||||||
        this.$titleButtons.empty();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.note = appContext.tabManager.getActiveContextNote();
 | 
					 | 
				
			||||||
        this.revisionItems = await server.get<RevisionItem[]>(`notes/${noteId}/revisions`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (const item of this.revisionItems) {
 | 
					 | 
				
			||||||
            this.$list.append(
 | 
					 | 
				
			||||||
                $('<a class="dropdown-item" tabindex="0">')
 | 
					 | 
				
			||||||
                    .text(`${item.dateLastEdited.substr(0, 16)} (${utils.formatSize(item.contentLength)})`)
 | 
					 | 
				
			||||||
                    .attr("data-revision-id", item.revisionId)
 | 
					 | 
				
			||||||
                    .attr("title", t("revisions.revision_last_edited", { date: item.dateLastEdited }))
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.listDropdown.show();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (this.revisionItems.length > 0) {
 | 
					 | 
				
			||||||
            if (!this.revisionId) {
 | 
					 | 
				
			||||||
                this.revisionId = this.revisionItems[0].revisionId;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            this.$title.text(t("revisions.no_revisions"));
 | 
					 | 
				
			||||||
            this.revisionId = null;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$eraseAllRevisionsButton.toggle(this.revisionItems.length > 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Show the footer of the revisions dialog
 | 
					 | 
				
			||||||
        this.$snapshotInterval.text(t("revisions.snapshot_interval", { seconds: options.getInt("revisionSnapshotTimeInterval") }));
 | 
					 | 
				
			||||||
        let revisionsNumberLimit: number | string = parseInt(this.note?.getLabelValue("versioningLimit") ?? "");
 | 
					 | 
				
			||||||
        if (!Number.isInteger(revisionsNumberLimit)) {
 | 
					 | 
				
			||||||
            revisionsNumberLimit = options.getInt("revisionSnapshotNumberLimit") ?? 0;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (revisionsNumberLimit === -1) {
 | 
					 | 
				
			||||||
            revisionsNumberLimit = "∞";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this.$maximumRevisions.text(t("revisions.maximum_revisions", { number: revisionsNumberLimit }));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async setContentPane() {
 | 
					 | 
				
			||||||
        const revisionId = this.$list.find(".active").attr("data-revision-id");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const revisionItem = this.revisionItems.find((r) => r.revisionId === revisionId);
 | 
					 | 
				
			||||||
        if (!revisionItem) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$title.html(revisionItem.title);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.renderContentButtons(revisionItem);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await this.renderContent(revisionItem);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    renderContentButtons(revisionItem: RevisionItem) {
 | 
					 | 
				
			||||||
        this.$titleButtons.empty();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const $restoreRevisionButton = $(`
 | 
					 | 
				
			||||||
            <button class="btn btn-sm" type="button">
 | 
					 | 
				
			||||||
                <span class="bx bx-history"></span>
 | 
					 | 
				
			||||||
                ${t("revisions.restore_button")}
 | 
					 | 
				
			||||||
            </button>
 | 
					 | 
				
			||||||
        `);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $restoreRevisionButton.on("click", async () => {
 | 
					 | 
				
			||||||
            const text = t("revisions.confirm_restore");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (await dialogService.confirm(text)) {
 | 
					 | 
				
			||||||
                await server.post(`revisions/${revisionItem.revisionId}/restore`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                this.modal.hide();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                toastService.showMessage(t("revisions.revision_restored"));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const $eraseRevisionButton = $(`
 | 
					 | 
				
			||||||
            <button class="btn btn-sm" type="button">
 | 
					 | 
				
			||||||
                <span class="bx bx-trash"></span>
 | 
					 | 
				
			||||||
                ${t("revisions.delete_button")}
 | 
					 | 
				
			||||||
            </button>
 | 
					 | 
				
			||||||
        `);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $eraseRevisionButton.on("click", async () => {
 | 
					 | 
				
			||||||
            const text = t("revisions.confirm_delete");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (await dialogService.confirm(text)) {
 | 
					 | 
				
			||||||
                await server.remove(`revisions/${revisionItem.revisionId}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                this.loadRevisions(revisionItem.noteId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                toastService.showMessage(t("revisions.revision_deleted"));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!revisionItem.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
					 | 
				
			||||||
            this.$titleButtons.append($restoreRevisionButton).append("   ");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$titleButtons.append($eraseRevisionButton).append("   ");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const $downloadButton = $(`
 | 
					 | 
				
			||||||
            <button class="btn btn-sm btn-primary" type="button">
 | 
					 | 
				
			||||||
                <span class="bx bx-download"></span>
 | 
					 | 
				
			||||||
                ${t("revisions.download_button")}
 | 
					 | 
				
			||||||
            </button>
 | 
					 | 
				
			||||||
        `);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $downloadButton.on("click", () => openService.downloadRevision(revisionItem.noteId, revisionItem.revisionId));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!revisionItem.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
					 | 
				
			||||||
            this.$titleButtons.append($downloadButton);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async renderContent(revisionItem: RevisionItem) {
 | 
					 | 
				
			||||||
        this.$content.empty();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const fullRevision = await server.get<FullRevision>(`revisions/${revisionItem.revisionId}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (revisionItem.type === "text") {
 | 
					 | 
				
			||||||
            this.$content.html(`<div class="ck-content">${fullRevision.content}</div>`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (this.$content.find("span.math-tex").length > 0) {
 | 
					 | 
				
			||||||
                renderMathInElement(this.$content[0], { trust: true });
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else if (revisionItem.type === "code") {
 | 
					 | 
				
			||||||
            this.$content.html($("<pre>")
 | 
					 | 
				
			||||||
                .text(fullRevision.content).prop("outerHTML"));
 | 
					 | 
				
			||||||
        } else if (revisionItem.type === "image") {
 | 
					 | 
				
			||||||
            if (fullRevision.mime === "image/svg+xml") {
 | 
					 | 
				
			||||||
                let encodedSVG = encodeURIComponent(fullRevision.content); //Base64 of other format images may be embedded in svg
 | 
					 | 
				
			||||||
                this.$content.html($("<img>")
 | 
					 | 
				
			||||||
                    .attr("src", `data:${fullRevision.mime};utf8,${encodedSVG}`)
 | 
					 | 
				
			||||||
                    .css("max-width", "100%")
 | 
					 | 
				
			||||||
                    .css("max-height", "100%").prop("outerHTML"));
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                this.$content.html(
 | 
					 | 
				
			||||||
                    $("<img>")
 | 
					 | 
				
			||||||
                        // the reason why we put this inline as base64 is that we do not want to let user copy this
 | 
					 | 
				
			||||||
                        // as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be uploaded as a new note
 | 
					 | 
				
			||||||
                        .attr("src", `data:${fullRevision.mime};base64,${fullRevision.content}`)
 | 
					 | 
				
			||||||
                        .css("max-width", "100%")
 | 
					 | 
				
			||||||
                        .css("max-height", "100%")
 | 
					 | 
				
			||||||
                        .prop("outerHTML")
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else if (revisionItem.type === "file") {
 | 
					 | 
				
			||||||
            const $table = $("<table cellpadding='10'>")
 | 
					 | 
				
			||||||
                .append($("<tr>")
 | 
					 | 
				
			||||||
                    .append(
 | 
					 | 
				
			||||||
                        $("<th>").text(t("revisions.mime")),
 | 
					 | 
				
			||||||
                        $("<td>").text(revisionItem.mime)))
 | 
					 | 
				
			||||||
                    .append($("<tr>").append($("<th>").text(t("revisions.file_size")), $("<td>").text(utils.formatSize(revisionItem.contentLength))));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (fullRevision.content) {
 | 
					 | 
				
			||||||
                $table.append(
 | 
					 | 
				
			||||||
                    $("<tr>").append(
 | 
					 | 
				
			||||||
                        $('<td colspan="2">').append($('<div style="font-weight: bold;">').text(t("revisions.preview")), $('<pre class="file-preview-content"></pre>').text(fullRevision.content))
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            this.$content.html($table.prop("outerHTML"));
 | 
					 | 
				
			||||||
        } else if (["canvas", "mindMap"].includes(revisionItem.type)) {
 | 
					 | 
				
			||||||
            const encodedTitle = encodeURIComponent(revisionItem.title);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            this.$content.html(
 | 
					 | 
				
			||||||
                $("<img>")
 | 
					 | 
				
			||||||
                    .attr("src", `api/revisions/${revisionItem.revisionId}/image/${encodedTitle}?${Math.random()}`)
 | 
					 | 
				
			||||||
                    .css("max-width", "100%")
 | 
					 | 
				
			||||||
                    .prop("outerHTML"));
 | 
					 | 
				
			||||||
        } else if (revisionItem.type === "mermaid") {
 | 
					 | 
				
			||||||
            const encodedTitle = encodeURIComponent(revisionItem.title);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            this.$content.html(
 | 
					 | 
				
			||||||
                $("<img>")
 | 
					 | 
				
			||||||
                    .attr("src", `api/revisions/${revisionItem.revisionId}/image/${encodedTitle}?${Math.random()}`)
 | 
					 | 
				
			||||||
                    .css("max-width", "100%")
 | 
					 | 
				
			||||||
                    .prop("outerHTML"));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            this.$content.append($("<pre>").text(fullRevision.content));
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            this.$content.text(t("revisions.preview_not_available"));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										170
									
								
								apps/client/src/widgets/dialogs/revisions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								apps/client/src/widgets/dialogs/revisions.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,170 @@
 | 
				
			|||||||
 | 
					import { NoteType } from "@triliumnext/commons";
 | 
				
			||||||
 | 
					import appContext, { EventData } from "../../components/app_context";
 | 
				
			||||||
 | 
					import FNote from "../../entities/fnote";
 | 
				
			||||||
 | 
					import dialog, { closeActiveDialog, openDialog } from "../../services/dialog";
 | 
				
			||||||
 | 
					import froca from "../../services/froca";
 | 
				
			||||||
 | 
					import { t } from "../../services/i18n";
 | 
				
			||||||
 | 
					import server from "../../services/server";
 | 
				
			||||||
 | 
					import toast from "../../services/toast";
 | 
				
			||||||
 | 
					import Button from "../react/Button";
 | 
				
			||||||
 | 
					import Modal from "../react/Modal";
 | 
				
			||||||
 | 
					import ReactBasicWidget from "../react/ReactBasicWidget";
 | 
				
			||||||
 | 
					import FormList, { FormListItem } from "../react/FormList";
 | 
				
			||||||
 | 
					import utils from "../../services/utils";
 | 
				
			||||||
 | 
					import { useEffect, useState } from "preact/hooks";
 | 
				
			||||||
 | 
					import protected_session_holder from "../../services/protected_session_holder";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RevisionsDialogProps {
 | 
				
			||||||
 | 
					    note?: FNote;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RevisionItem {
 | 
				
			||||||
 | 
					    noteId: string;
 | 
				
			||||||
 | 
					    revisionId: string;
 | 
				
			||||||
 | 
					    dateLastEdited: string;
 | 
				
			||||||
 | 
					    contentLength: number;
 | 
				
			||||||
 | 
					    type: NoteType;
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					    isProtected: boolean;
 | 
				
			||||||
 | 
					    mime: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FullRevision {
 | 
				
			||||||
 | 
					    content: string;
 | 
				
			||||||
 | 
					    mime: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function RevisionsDialogComponent({ note }: RevisionsDialogProps) {
 | 
				
			||||||
 | 
					    const [ revisions, setRevisions ] = useState<RevisionItem[]>([]);
 | 
				
			||||||
 | 
					    const [ currentRevision, setCurrentRevision ] = useState<RevisionItem>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (note) {
 | 
				
			||||||
 | 
					        useEffect(() => {
 | 
				
			||||||
 | 
					            server.get<RevisionItem[]>(`notes/${note.noteId}/revisions`).then(setRevisions);
 | 
				
			||||||
 | 
					        }, [ note.noteId ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (revisions?.length && !currentRevision) {
 | 
				
			||||||
 | 
					        setCurrentRevision(revisions[0]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (note &&
 | 
				
			||||||
 | 
					        <Modal
 | 
				
			||||||
 | 
					            className="revisions-dialog"
 | 
				
			||||||
 | 
					            size="xl"
 | 
				
			||||||
 | 
					            title={t("revisions.note_revisions")}
 | 
				
			||||||
 | 
					            helpPageId="vZWERwf8U3nx"
 | 
				
			||||||
 | 
					            bodyStyle={{ display: "flex", height: "80vh" }}
 | 
				
			||||||
 | 
					            header={<>
 | 
				
			||||||
 | 
					                <Button text={t("revisions.delete_all_revisions")} small style={{ padding: "0 10px" }}
 | 
				
			||||||
 | 
					                    onClick={async () => {
 | 
				
			||||||
 | 
					                        const text = t("revisions.confirm_delete_all");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (await dialog.confirm(text)) {
 | 
				
			||||||
 | 
					                            await server.remove(`notes/${note.noteId}/revisions`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            closeActiveDialog();
 | 
				
			||||||
 | 
					                            toast.showMessage(t("revisions.revisions_deleted"));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }}/>
 | 
				
			||||||
 | 
					            </>}                
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					                <RevisionsList
 | 
				
			||||||
 | 
					                    revisions={revisions}
 | 
				
			||||||
 | 
					                    onSelect={(revisionId) => {
 | 
				
			||||||
 | 
					                        const correspondingRevision = revisions.find((r) => r.revisionId === revisionId);
 | 
				
			||||||
 | 
					                        if (correspondingRevision) {
 | 
				
			||||||
 | 
					                            setCurrentRevision(correspondingRevision);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="revision-content-wrapper" style={{
 | 
				
			||||||
 | 
					                    "flex-grow": "1",
 | 
				
			||||||
 | 
					                    "margin-left": "20px",
 | 
				
			||||||
 | 
					                    "display": "flex",
 | 
				
			||||||
 | 
					                    "flex-direction": "column",
 | 
				
			||||||
 | 
					                    "min-width": 0
 | 
				
			||||||
 | 
					                }}>
 | 
				
			||||||
 | 
					                    <RevisionPreview revisionItem={currentRevision} />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					        </Modal>
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function RevisionsList({ revisions, onSelect }: { revisions: RevisionItem[], onSelect: (val: string) => void }) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <FormList style={{ height: "100%", flexShrink: 0 }} onSelect={onSelect}>
 | 
				
			||||||
 | 
					            {revisions.map((item) => 
 | 
				
			||||||
 | 
					                <FormListItem
 | 
				
			||||||
 | 
					                    title={t("revisions.revision_last_edited", { date: item.dateLastEdited })}
 | 
				
			||||||
 | 
					                    value={item.revisionId}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                    {item.dateLastEdited.substr(0, 16)} ({utils.formatSize(item.contentLength)})
 | 
				
			||||||
 | 
					                </FormListItem>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					        </FormList>);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function RevisionPreview({ revisionItem }: { revisionItem?: RevisionItem}) {
 | 
				
			||||||
 | 
					    const [ fullRevision, setFullRevision ] = useState<FullRevision>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        if (revisionItem) {
 | 
				
			||||||
 | 
					            server.get<FullRevision>(`revisions/${revisionItem.revisionId}`).then(setFullRevision);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            setFullRevision(undefined);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }, [revisionItem]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return revisionItem && (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					            <div style="flex-grow: 0; display: flex; justify-content: space-between;">
 | 
				
			||||||
 | 
					                <h3 class="revision-title" style="margin: 3px; flex-grow: 100;">{revisionItem.title}</h3>
 | 
				
			||||||
 | 
					                <div class="revision-title-buttons">
 | 
				
			||||||
 | 
					                    {(!revisionItem.isProtected || protected_session_holder.isProtectedSessionAvailable()) &&
 | 
				
			||||||
 | 
					                        <Button icon="bx bx-history" text={t("revisions.restore_button")} />
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <RevisionContent revisionItem={revisionItem} fullRevision={fullRevision} />
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: RevisionItem, fullRevision?: FullRevision }) {
 | 
				
			||||||
 | 
					    if (!revisionItem || !fullRevision) {
 | 
				
			||||||
 | 
					        return <></>;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (revisionItem.type) {
 | 
				
			||||||
 | 
					        case "text":
 | 
				
			||||||
 | 
					            return <div class="ck-content" dangerouslySetInnerHTML={{ __html: fullRevision.content }}></div>
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class RevisionsDialog extends ReactBasicWidget  {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private props: RevisionsDialogProps = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    get component() {
 | 
				
			||||||
 | 
					        return <RevisionsDialogComponent {...this.props} />
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async showRevisionsEvent({ noteId }: EventData<"showRevisions">) {
 | 
				
			||||||
 | 
					        this.props = {
 | 
				
			||||||
 | 
					            note: await getNote(noteId) ?? undefined
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        this.doRender();
 | 
				
			||||||
 | 
					        openDialog(this.$widget);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getNote(noteId?: string | null) {
 | 
				
			||||||
 | 
					    if (noteId) {
 | 
				
			||||||
 | 
					        return await froca.getNote(noteId);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        return appContext.tabManager.getActiveContextNote();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import { RefObject } from "preact";
 | 
					import { RefObject } from "preact";
 | 
				
			||||||
 | 
					import { CSSProperties } from "preact/compat";
 | 
				
			||||||
import { useRef } from "preact/hooks";
 | 
					import { useRef } from "preact/hooks";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ButtonProps {
 | 
					interface ButtonProps {
 | 
				
			||||||
@ -12,9 +13,11 @@ interface ButtonProps {
 | 
				
			|||||||
    onClick?: () => void;
 | 
					    onClick?: () => void;
 | 
				
			||||||
    primary?: boolean;
 | 
					    primary?: boolean;
 | 
				
			||||||
    disabled?: boolean;
 | 
					    disabled?: boolean;
 | 
				
			||||||
 | 
					    small?: boolean;
 | 
				
			||||||
 | 
					    style?: CSSProperties;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Button({ buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled }: ButtonProps) {
 | 
					export default function Button({ buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, small, style }: ButtonProps) {
 | 
				
			||||||
    const classes: string[] = ["btn"];
 | 
					    const classes: string[] = ["btn"];
 | 
				
			||||||
    if (primary) {
 | 
					    if (primary) {
 | 
				
			||||||
        classes.push("btn-primary");
 | 
					        classes.push("btn-primary");
 | 
				
			||||||
@ -24,6 +27,9 @@ export default function Button({ buttonRef: _buttonRef, className, text, onClick
 | 
				
			|||||||
    if (className) {
 | 
					    if (className) {
 | 
				
			||||||
        classes.push(className);
 | 
					        classes.push(className);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (small) {
 | 
				
			||||||
 | 
					        classes.push("btn-sm");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const buttonRef = _buttonRef ?? useRef<HTMLButtonElement>(null);
 | 
					    const buttonRef = _buttonRef ?? useRef<HTMLButtonElement>(null);
 | 
				
			||||||
    const splitShortcut = (keyboardShortcut ?? "").split("+");
 | 
					    const splitShortcut = (keyboardShortcut ?? "").split("+");
 | 
				
			||||||
@ -35,6 +41,7 @@ export default function Button({ buttonRef: _buttonRef, className, text, onClick
 | 
				
			|||||||
            onClick={onClick}
 | 
					            onClick={onClick}
 | 
				
			||||||
            ref={buttonRef}
 | 
					            ref={buttonRef}
 | 
				
			||||||
            disabled={disabled}
 | 
					            disabled={disabled}
 | 
				
			||||||
 | 
					            style={style}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
            {icon && <span className={`bx ${icon}`}></span>}
 | 
					            {icon && <span className={`bx ${icon}`}></span>}
 | 
				
			||||||
            {text} {keyboardShortcut && (
 | 
					            {text} {keyboardShortcut && (
 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +1,18 @@
 | 
				
			|||||||
import { ComponentChildren } from "preact";
 | 
					import { ComponentChildren } from "preact";
 | 
				
			||||||
import Icon from "./Icon";
 | 
					import Icon from "./Icon";
 | 
				
			||||||
 | 
					import { CSSProperties } from "preact/compat";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface FormListOpts {
 | 
					interface FormListOpts {
 | 
				
			||||||
    children: ComponentChildren;
 | 
					    children: ComponentChildren;
 | 
				
			||||||
    onSelect?: (value: string) => void;
 | 
					    onSelect?: (value: string) => void;
 | 
				
			||||||
 | 
					    style?: CSSProperties;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function FormList({ children, onSelect }: FormListOpts) {
 | 
					export default function FormList({ children, onSelect, style }: FormListOpts) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <div class="dropdown-menu static show" style={{
 | 
					        <div class="dropdown-menu static show" style={{
 | 
				
			||||||
            position: "relative"
 | 
					            ...style ?? {},
 | 
				
			||||||
 | 
					            position: "relative",
 | 
				
			||||||
        }} onClick={(e) => {
 | 
					        }} onClick={(e) => {
 | 
				
			||||||
            const value = (e.target as HTMLElement)?.dataset?.value;
 | 
					            const value = (e.target as HTMLElement)?.dataset?.value;
 | 
				
			||||||
            if (value && onSelect) {
 | 
					            if (value && onSelect) {
 | 
				
			||||||
@ -25,11 +28,12 @@ interface FormListItemOpts {
 | 
				
			|||||||
    children: ComponentChildren;
 | 
					    children: ComponentChildren;
 | 
				
			||||||
    icon?: string;
 | 
					    icon?: string;
 | 
				
			||||||
    value?: string;
 | 
					    value?: string;
 | 
				
			||||||
 | 
					    title?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function FormListItem({ children, icon, value }: FormListItemOpts) {
 | 
					export function FormListItem({ children, icon, value, title }: FormListItemOpts) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <a class="dropdown-item" data-value={value}>
 | 
					        <a class="dropdown-item" data-value={value} title={title}>
 | 
				
			||||||
            <Icon icon={icon} /> 
 | 
					            <Icon icon={icon} /> 
 | 
				
			||||||
            {children}
 | 
					            {children}
 | 
				
			||||||
        </a>
 | 
					        </a>
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,10 @@ interface ModalProps {
 | 
				
			|||||||
    title: string | ComponentChildren;
 | 
					    title: string | ComponentChildren;
 | 
				
			||||||
    size: "xl" | "lg" | "md" | "sm";
 | 
					    size: "xl" | "lg" | "md" | "sm";
 | 
				
			||||||
    children: ComponentChildren;
 | 
					    children: ComponentChildren;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Items to display in the modal header, apart from the title itself which is handled separately.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    header?: ComponentChildren;
 | 
				
			||||||
    footer?: ComponentChildren;
 | 
					    footer?: ComponentChildren;
 | 
				
			||||||
    footerAlignment?: "right" | "between";
 | 
					    footerAlignment?: "right" | "between";
 | 
				
			||||||
    minWidth?: string;
 | 
					    minWidth?: string;
 | 
				
			||||||
@ -39,9 +43,10 @@ interface ModalProps {
 | 
				
			|||||||
     * Gives access to the underlying form element of the modal. This is only set if `onSubmit` is provided.
 | 
					     * Gives access to the underlying form element of the modal. This is only set if `onSubmit` is provided.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    formRef?: RefObject<HTMLFormElement>;
 | 
					    formRef?: RefObject<HTMLFormElement>;
 | 
				
			||||||
 | 
					    bodyStyle?: CSSProperties;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Modal({ children, className, size, title, footer, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: _modalRef, formRef: _formRef }: ModalProps) {
 | 
					export default function Modal({ children, className, size, title, header, footer, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: _modalRef, formRef: _formRef, bodyStyle }: ModalProps) {
 | 
				
			||||||
    const modalRef = _modalRef ?? useRef<HTMLDivElement>(null);
 | 
					    const modalRef = _modalRef ?? useRef<HTMLDivElement>(null);
 | 
				
			||||||
    const formRef = _formRef ?? useRef<HTMLFormElement>(null);
 | 
					    const formRef = _formRef ?? useRef<HTMLFormElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -91,6 +96,7 @@ export default function Modal({ children, className, size, title, footer, footer
 | 
				
			|||||||
                        ) : (
 | 
					                        ) : (
 | 
				
			||||||
                            title
 | 
					                            title
 | 
				
			||||||
                        )}
 | 
					                        )}
 | 
				
			||||||
 | 
					                        {header}
 | 
				
			||||||
                        {helpPageId && (
 | 
					                        {helpPageId && (
 | 
				
			||||||
                            <button className="help-button" type="button" data-in-app-help={helpPageId} title={t("modal.help_title")}>?</button>
 | 
					                            <button className="help-button" type="button" data-in-app-help={helpPageId} title={t("modal.help_title")}>?</button>
 | 
				
			||||||
                        )}
 | 
					                        )}
 | 
				
			||||||
@ -102,10 +108,10 @@ export default function Modal({ children, className, size, title, footer, footer
 | 
				
			|||||||
                            e.preventDefault();
 | 
					                            e.preventDefault();
 | 
				
			||||||
                            onSubmit();
 | 
					                            onSubmit();
 | 
				
			||||||
                        }}>
 | 
					                        }}>
 | 
				
			||||||
                            <ModalInner footer={footer}>{children}</ModalInner>
 | 
					                            <ModalInner footer={footer} bodyStyle={bodyStyle}>{children}</ModalInner>
 | 
				
			||||||
                        </form>
 | 
					                        </form>
 | 
				
			||||||
                    ) : (
 | 
					                    ) : (
 | 
				
			||||||
                        <ModalInner footer={footer}>
 | 
					                        <ModalInner footer={footer} bodyStyle={bodyStyle}>
 | 
				
			||||||
                            {children}
 | 
					                            {children}
 | 
				
			||||||
                        </ModalInner>
 | 
					                        </ModalInner>
 | 
				
			||||||
                    )}
 | 
					                    )}
 | 
				
			||||||
@ -115,7 +121,7 @@ export default function Modal({ children, className, size, title, footer, footer
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function ModalInner({ children, footer, footerAlignment }: Pick<ModalProps, "children" | "footer" | "footerAlignment">) {
 | 
					function ModalInner({ children, footer, footerAlignment, bodyStyle }: Pick<ModalProps, "children" | "footer" | "footerAlignment" | "bodyStyle">) {
 | 
				
			||||||
    const footerStyle: CSSProperties = {};
 | 
					    const footerStyle: CSSProperties = {};
 | 
				
			||||||
    if (footerAlignment === "between") {
 | 
					    if (footerAlignment === "between") {
 | 
				
			||||||
        footerStyle.justifyContent = "space-between";
 | 
					        footerStyle.justifyContent = "space-between";
 | 
				
			||||||
@ -123,7 +129,7 @@ function ModalInner({ children, footer, footerAlignment }: Pick<ModalProps, "chi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
            <div className="modal-body">
 | 
					            <div className="modal-body" style={bodyStyle}>
 | 
				
			||||||
                {children}
 | 
					                {children}
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user