mirror of
https://github.com/zadam/trilium.git
synced 2025-11-17 18:50:41 +01:00
chore(monorepo): relocate client files
This commit is contained in:
116
apps/client/src/widgets/dialogs/about.ts
Normal file
116
apps/client/src/widgets/dialogs/about.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { formatDateTime } from "../../utils/formatters.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import openService from "../../services/open.js";
|
||||
import server from "../../services/server.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
interface AppInfo {
|
||||
appVersion: string;
|
||||
dbVersion: number;
|
||||
syncVersion: number;
|
||||
buildDate: string;
|
||||
buildRevision: string;
|
||||
dataDirectory: string;
|
||||
}
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="about-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${t("about.title")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("about.close")}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<th>${t("about.homepage")}</th>
|
||||
<td><a class="tn-link" href="https://github.com/TriliumNext/Notes" class="external">https://github.com/TriliumNext/Notes</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>${t("about.app_version")}</th>
|
||||
<td class="app-version"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>${t("about.db_version")}</th>
|
||||
<td class="db-version"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>${t("about.sync_version")}</th>
|
||||
<td class="sync-version"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>${t("about.build_date")}</th>
|
||||
<td class="build-date"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>${t("about.build_revision")}</th>
|
||||
<td><a class="tn-link build-revision external" href="" target="_blank"></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>${t("about.data_directory")}</th>
|
||||
<td class="data-directory"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.about-dialog a {
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
export default class AboutDialog extends BasicWidget {
|
||||
private $appVersion!: JQuery<HTMLElement>;
|
||||
private $dbVersion!: JQuery<HTMLElement>;
|
||||
private $syncVersion!: JQuery<HTMLElement>;
|
||||
private $buildDate!: JQuery<HTMLElement>;
|
||||
private $buildRevision!: JQuery<HTMLElement>;
|
||||
private $dataDirectory!: JQuery<HTMLElement>;
|
||||
|
||||
doRender(): void {
|
||||
this.$widget = $(TPL);
|
||||
this.$appVersion = this.$widget.find(".app-version");
|
||||
this.$dbVersion = this.$widget.find(".db-version");
|
||||
this.$syncVersion = this.$widget.find(".sync-version");
|
||||
this.$buildDate = this.$widget.find(".build-date");
|
||||
this.$buildRevision = this.$widget.find(".build-revision");
|
||||
this.$dataDirectory = this.$widget.find(".data-directory");
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
const appInfo = await server.get<AppInfo>("app-info");
|
||||
|
||||
this.$appVersion.text(appInfo.appVersion);
|
||||
this.$dbVersion.text(appInfo.dbVersion.toString());
|
||||
this.$syncVersion.text(appInfo.syncVersion.toString());
|
||||
this.$buildDate.text(formatDateTime(appInfo.buildDate));
|
||||
this.$buildRevision.text(appInfo.buildRevision);
|
||||
this.$buildRevision.attr("href", `https://github.com/TriliumNext/Notes/commit/${appInfo.buildRevision}`);
|
||||
if (utils.isElectron()) {
|
||||
this.$dataDirectory.html(
|
||||
$("<a></a>", {
|
||||
href: "#",
|
||||
class: "tn-link",
|
||||
text: appInfo.dataDirectory
|
||||
}).prop("outerHTML")
|
||||
);
|
||||
this.$dataDirectory.find("a").on("click", (event: JQuery.ClickEvent) => {
|
||||
event.preventDefault();
|
||||
openService.openDirectory(appInfo.dataDirectory);
|
||||
});
|
||||
} else {
|
||||
this.$dataDirectory.text(appInfo.dataDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
async openAboutDialogEvent() {
|
||||
await this.refresh();
|
||||
utils.openDialog(this.$widget);
|
||||
}
|
||||
}
|
||||
188
apps/client/src/widgets/dialogs/add_link.ts
Normal file
188
apps/client/src/widgets/dialogs/add_link.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import type { Suggestion } from "../../services/note_autocomplete.js";
|
||||
import type { default as TextTypeWidget } from "../type_widgets/editable_text.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="add-link-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title flex-grow-1">${t("add_link.add_link")}</h5>
|
||||
<button type="button" class="help-button" title="${t("add_link.help_on_links")}" data-help-page="links.html">?</button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("add_link.close")}"></button>
|
||||
</div>
|
||||
<form class="add-link-form">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="add-link-note-autocomplete">${t("add_link.note")}</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input class="add-link-note-autocomplete form-control" placeholder="${t("add_link.search_note")}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="add-link-title-settings">
|
||||
<div class="add-link-title-radios form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="radio" name="link-type" value="reference-link" checked>
|
||||
${t("add_link.link_title_mirrors")}
|
||||
</label>
|
||||
</div>
|
||||
<div class="add-link-title-radios form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="radio" name="link-type" value="hyper-link">
|
||||
${t("add_link.link_title_arbitrary")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="add-link-title-form-group form-group">
|
||||
<br/>
|
||||
<label>
|
||||
${t("add_link.link_title")}
|
||||
|
||||
<input class="link-title form-control" style="width: 100%;">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">${t("add_link.button_add_link")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class AddLinkDialog extends BasicWidget {
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
private $autoComplete!: JQuery<HTMLElement>;
|
||||
private $linkTitle!: JQuery<HTMLElement>;
|
||||
private $addLinkTitleSettings!: JQuery<HTMLElement>;
|
||||
private $addLinkTitleRadios!: JQuery<HTMLElement>;
|
||||
private $addLinkTitleFormGroup!: JQuery<HTMLElement>;
|
||||
private textTypeWidget: TextTypeWidget | null = null;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$form = this.$widget.find(".add-link-form");
|
||||
this.$autoComplete = this.$widget.find(".add-link-note-autocomplete");
|
||||
this.$linkTitle = this.$widget.find(".link-title");
|
||||
this.$addLinkTitleSettings = this.$widget.find(".add-link-title-settings");
|
||||
this.$addLinkTitleRadios = this.$widget.find(".add-link-title-radios");
|
||||
this.$addLinkTitleFormGroup = this.$widget.find(".add-link-title-form-group");
|
||||
|
||||
this.$form.on("submit", () => {
|
||||
if (this.$autoComplete.getSelectedNotePath()) {
|
||||
this.$widget.modal("hide");
|
||||
|
||||
const linkTitle = this.getLinkType() === "reference-link" ? null : this.$linkTitle.val() as string;
|
||||
|
||||
this.textTypeWidget?.addLink(this.$autoComplete.getSelectedNotePath()!, linkTitle);
|
||||
} else if (this.$autoComplete.getSelectedExternalLink()) {
|
||||
this.$widget.modal("hide");
|
||||
|
||||
this.textTypeWidget?.addLink(this.$autoComplete.getSelectedExternalLink()!, this.$linkTitle.val() as string, true);
|
||||
} else {
|
||||
logError("No link to add.");
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
async showAddLinkDialogEvent({ textTypeWidget, text = "" }: EventData<"showAddLinkDialog">) {
|
||||
this.textTypeWidget = textTypeWidget;
|
||||
|
||||
this.$addLinkTitleSettings.toggle(!this.textTypeWidget.hasSelection());
|
||||
|
||||
this.$addLinkTitleSettings.find("input[type=radio]").on("change", () => this.updateTitleSettingsVisibility());
|
||||
|
||||
// with selection hyperlink is implied
|
||||
if (this.textTypeWidget.hasSelection()) {
|
||||
this.$addLinkTitleSettings.find("input[value='hyper-link']").prop("checked", true);
|
||||
} else {
|
||||
this.$addLinkTitleSettings.find("input[value='reference-link']").prop("checked", true);
|
||||
}
|
||||
|
||||
this.updateTitleSettingsVisibility();
|
||||
|
||||
utils.openDialog(this.$widget);
|
||||
|
||||
this.$autoComplete.val("");
|
||||
this.$linkTitle.val("");
|
||||
|
||||
const setDefaultLinkTitle = async (noteId: string) => {
|
||||
const noteTitle = await treeService.getNoteTitle(noteId);
|
||||
this.$linkTitle.val(noteTitle);
|
||||
};
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
|
||||
allowExternalLinks: true,
|
||||
allowCreatingNotes: true
|
||||
});
|
||||
|
||||
this.$autoComplete.on("autocomplete:noteselected", (event: JQuery.Event, suggestion: Suggestion) => {
|
||||
if (!suggestion.notePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.updateTitleSettingsVisibility();
|
||||
|
||||
const noteId = treeService.getNoteIdFromUrl(suggestion.notePath);
|
||||
|
||||
if (noteId) {
|
||||
setDefaultLinkTitle(noteId);
|
||||
}
|
||||
});
|
||||
|
||||
this.$autoComplete.on("autocomplete:externallinkselected", (event: JQuery.Event, suggestion: Suggestion) => {
|
||||
if (!suggestion.externalLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.updateTitleSettingsVisibility();
|
||||
|
||||
this.$linkTitle.val(suggestion.externalLink);
|
||||
});
|
||||
|
||||
this.$autoComplete.on("autocomplete:cursorchanged", (event: JQuery.Event, suggestion: Suggestion) => {
|
||||
if (suggestion.externalLink) {
|
||||
this.$linkTitle.val(suggestion.externalLink);
|
||||
} else {
|
||||
const noteId = treeService.getNoteIdFromUrl(suggestion.notePath!);
|
||||
|
||||
if (noteId) {
|
||||
setDefaultLinkTitle(noteId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (text && text.trim()) {
|
||||
noteAutocompleteService.setText(this.$autoComplete, text);
|
||||
} else {
|
||||
noteAutocompleteService.showRecentNotes(this.$autoComplete);
|
||||
}
|
||||
|
||||
this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text
|
||||
}
|
||||
|
||||
private getLinkType() {
|
||||
if (this.$autoComplete.getSelectedExternalLink()) {
|
||||
return "external-link";
|
||||
}
|
||||
|
||||
return this.$addLinkTitleSettings.find("input[type=radio]:checked").val();
|
||||
}
|
||||
|
||||
private updateTitleSettingsVisibility() {
|
||||
const linkType = this.getLinkType();
|
||||
|
||||
this.$addLinkTitleFormGroup.toggle(linkType !== "reference-link");
|
||||
this.$addLinkTitleRadios.toggle(linkType !== "external-link");
|
||||
}
|
||||
}
|
||||
108
apps/client/src/widgets/dialogs/branch_prefix.ts
Normal file
108
apps/client/src/widgets/dialogs/branch_prefix.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import treeService from "../../services/tree.js";
|
||||
import server from "../../services/server.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const TPL = /*html*/`<div class="branch-prefix-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<form class="branch-prefix-form">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title flex-grow-1">${t("branch_prefix.edit_branch_prefix")}</h5>
|
||||
<button class="help-button" type="button" data-help-page="tree-concepts.html#prefix" title="${t("branch_prefix.help_on_tree_prefix")}">?</button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("branch_prefix.close")}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="branch-prefix-input">${t("branch_prefix.prefix")}</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input class="branch-prefix-input form-control">
|
||||
<div class="branch-prefix-note-title input-group-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary btn-sm">${t("branch_prefix.save")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class BranchPrefixDialog extends BasicWidget {
|
||||
private modal!: Modal;
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
private $treePrefixInput!: JQuery<HTMLElement>;
|
||||
private $noteTitle!: JQuery<HTMLElement>;
|
||||
private branchId: string | null = null;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||
this.$form = this.$widget.find(".branch-prefix-form");
|
||||
this.$treePrefixInput = this.$widget.find(".branch-prefix-input");
|
||||
this.$noteTitle = this.$widget.find(".branch-prefix-note-title");
|
||||
|
||||
this.$form.on("submit", () => {
|
||||
this.savePrefix();
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$widget.on("shown.bs.modal", () => this.$treePrefixInput.trigger("focus"));
|
||||
}
|
||||
|
||||
async refresh(notePath: string) {
|
||||
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
|
||||
|
||||
if (!noteId || !parentNoteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newBranchId = await froca.getBranchId(parentNoteId, noteId);
|
||||
if (!newBranchId) {
|
||||
return;
|
||||
}
|
||||
this.branchId = newBranchId;
|
||||
|
||||
const branch = froca.getBranch(this.branchId);
|
||||
if (!branch || branch.noteId === "root") {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentNote = await froca.getNote(branch.parentNoteId);
|
||||
if (!parentNote || parentNote.type === "search") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$treePrefixInput.val(branch.prefix || "");
|
||||
|
||||
const noteTitle = await treeService.getNoteTitle(noteId);
|
||||
this.$noteTitle.text(` - ${noteTitle}`);
|
||||
}
|
||||
|
||||
async editBranchPrefixEvent() {
|
||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (!notePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.refresh(notePath);
|
||||
utils.openDialog(this.$widget);
|
||||
}
|
||||
|
||||
async savePrefix() {
|
||||
const prefix = this.$treePrefixInput.val();
|
||||
|
||||
await server.put(`branches/${this.branchId}/set-prefix`, { prefix: prefix });
|
||||
|
||||
this.modal.hide();
|
||||
|
||||
toastService.showMessage(t("branch_prefix.branch_prefix_saved"));
|
||||
}
|
||||
}
|
||||
175
apps/client/src/widgets/dialogs/bulk_actions.ts
Normal file
175
apps/client/src/widgets/dialogs/bulk_actions.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import bulkActionService from "../../services/bulk_action.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import server from "../../services/server.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="bulk-actions-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||
<style>
|
||||
.bulk-actions-dialog .modal-body h4:not(:first-child) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.bulk-actions-dialog .bulk-available-action-list button {
|
||||
padding: 2px 7px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.bulk-actions-dialog .bulk-existing-action-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bulk-actions-dialog .bulk-existing-action-list td {
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.bulk-actions-dialog .bulk-existing-action-list .button-column {
|
||||
/* minimal width so that table remains static sized and most space remains for middle column with settings */
|
||||
width: 50px;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${t("bulk_actions.bulk_actions")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("bulk_actions.close")}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h4>${t("bulk_actions.affected_notes")}: <span class="affected-note-count">0</span></h4>
|
||||
|
||||
<div class="form-check">
|
||||
<label for="include-descendants" class="form-check-label tn-checkbox">
|
||||
<input id="include-descendants" class="include-descendants form-check-input" type="checkbox" value="">
|
||||
${t("bulk_actions.include_descendants")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h4>${t("bulk_actions.available_actions")}</h4>
|
||||
|
||||
<table class="bulk-available-action-list"></table>
|
||||
|
||||
<h4>${t("bulk_actions.chosen_actions")}</h4>
|
||||
|
||||
<table class="bulk-existing-action-list"></table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="execute-bulk-actions btn btn-primary">${t("bulk_actions.execute_bulk_actions")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class BulkActionsDialog extends BasicWidget {
|
||||
private $includeDescendants!: JQuery<HTMLElement>;
|
||||
private $affectedNoteCount!: JQuery<HTMLElement>;
|
||||
private $availableActionList!: JQuery<HTMLElement>;
|
||||
private $existingActionList!: JQuery<HTMLElement>;
|
||||
private $executeButton!: JQuery<HTMLElement>;
|
||||
private selectedOrActiveNoteIds: string[] | null = null;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$includeDescendants = this.$widget.find(".include-descendants");
|
||||
this.$includeDescendants.on("change", () => this.refresh());
|
||||
|
||||
this.$affectedNoteCount = this.$widget.find(".affected-note-count");
|
||||
|
||||
this.$availableActionList = this.$widget.find(".bulk-available-action-list");
|
||||
this.$existingActionList = this.$widget.find(".bulk-existing-action-list");
|
||||
|
||||
this.$widget.on("click", "[data-action-add]", async (event) => {
|
||||
const actionName = $(event.target).attr("data-action-add");
|
||||
if (!actionName) {
|
||||
return;
|
||||
}
|
||||
|
||||
await bulkActionService.addAction("_bulkAction", actionName);
|
||||
await this.refresh();
|
||||
});
|
||||
|
||||
this.$executeButton = this.$widget.find(".execute-bulk-actions");
|
||||
this.$executeButton.on("click", async () => {
|
||||
await server.post("bulk-action/execute", {
|
||||
noteIds: this.selectedOrActiveNoteIds,
|
||||
includeDescendants: this.$includeDescendants.is(":checked")
|
||||
});
|
||||
|
||||
toastService.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
|
||||
utils.closeActiveDialog();
|
||||
});
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.renderAvailableActions();
|
||||
|
||||
if (!this.selectedOrActiveNoteIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { affectedNoteCount } = await server.post("bulk-action/affected-notes", {
|
||||
noteIds: this.selectedOrActiveNoteIds,
|
||||
includeDescendants: this.$includeDescendants.is(":checked")
|
||||
}) as { affectedNoteCount: number };
|
||||
|
||||
this.$affectedNoteCount.text(affectedNoteCount);
|
||||
|
||||
const bulkActionNote = await froca.getNote("_bulkAction");
|
||||
if (!bulkActionNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actions = bulkActionService.parseActions(bulkActionNote);
|
||||
|
||||
this.$existingActionList.empty();
|
||||
|
||||
if (actions.length > 0) {
|
||||
this.$existingActionList.append(...actions.map((action) => action.render()).filter((action) => action !== null));
|
||||
} else {
|
||||
this.$existingActionList.append($("<p>").text(t("bulk_actions.none_yet")));
|
||||
}
|
||||
}
|
||||
|
||||
renderAvailableActions() {
|
||||
this.$availableActionList.empty();
|
||||
|
||||
for (const actionGroup of bulkActionService.ACTION_GROUPS) {
|
||||
const $actionGroupList = $("<td>");
|
||||
const $actionGroup = $("<tr>")
|
||||
.append($("<td>").text(`${actionGroup.title}: `))
|
||||
.append($actionGroupList);
|
||||
|
||||
for (const action of actionGroup.actions) {
|
||||
$actionGroupList.append($('<button class="btn btn-sm">').attr("data-action-add", action.actionName).text(action.actionTitle));
|
||||
}
|
||||
|
||||
this.$availableActionList.append($actionGroup);
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
// only refreshing deleted attrs, otherwise components update themselves
|
||||
if (loadResults.getAttributeRows().find((row) => row.type === "label" && row.name === "action" && row.noteId === "_bulkAction" && row.isDeleted)) {
|
||||
// this may be triggered from e.g., sync without open widget, then no need to refresh the widget
|
||||
if (this.selectedOrActiveNoteIds && this.$widget.is(":visible")) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async openBulkActionsDialogEvent({ selectedOrActiveNoteIds }: EventData<"openBulkActionsDialog">) {
|
||||
this.selectedOrActiveNoteIds = selectedOrActiveNoteIds;
|
||||
this.$includeDescendants.prop("checked", false);
|
||||
|
||||
await this.refresh();
|
||||
utils.openDialog(this.$widget);
|
||||
}
|
||||
}
|
||||
140
apps/client/src/widgets/dialogs/clone_to.ts
Normal file
140
apps/client/src/widgets/dialogs/clone_to.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import branchService from "../../services/branches.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="clone-to-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title flex-grow-1">${t("clone_to.clone_notes_to")}</h5>
|
||||
<button type="button" class="help-button" title="${t("clone_to.help_on_links")}" data-help-page="cloning-notes.html">?</button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("clone_to.close")}"></button>
|
||||
</div>
|
||||
<form class="clone-to-form">
|
||||
<div class="modal-body">
|
||||
<h5>${t("clone_to.notes_to_clone")}</h5>
|
||||
|
||||
<ul class="clone-to-note-list" style="max-height: 200px; overflow: auto;"></ul>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="width: 100%">
|
||||
${t("clone_to.target_parent_note")}
|
||||
<div class="input-group">
|
||||
<input class="clone-to-note-autocomplete form-control" placeholder="${t("clone_to.search_for_note_by_its_name")}">
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" title="${t("clone_to.cloned_note_prefix_title")}">
|
||||
<label style="width: 100%">
|
||||
${t("clone_to.prefix_optional")}
|
||||
<input class="clone-prefix form-control" style="width: 100%;">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">${t("clone_to.clone_to_selected_note")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class CloneToDialog extends BasicWidget {
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
private $noteAutoComplete!: JQuery<HTMLElement>;
|
||||
private $clonePrefix!: JQuery<HTMLElement>;
|
||||
private $noteList!: JQuery<HTMLElement>;
|
||||
private clonedNoteIds: string[] | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$form = this.$widget.find(".clone-to-form");
|
||||
this.$noteAutoComplete = this.$widget.find(".clone-to-note-autocomplete");
|
||||
this.$clonePrefix = this.$widget.find(".clone-prefix");
|
||||
this.$noteList = this.$widget.find(".clone-to-note-list");
|
||||
|
||||
this.$form.on("submit", () => {
|
||||
const notePath = this.$noteAutoComplete.getSelectedNotePath();
|
||||
|
||||
if (notePath) {
|
||||
this.$widget.modal("hide");
|
||||
this.cloneNotesTo(notePath);
|
||||
} else {
|
||||
logError(t("clone_to.no_path_to_clone_to"));
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
async cloneNoteIdsToEvent({ noteIds }: EventData<"cloneNoteIdsTo">) {
|
||||
if (!noteIds || noteIds.length === 0) {
|
||||
noteIds = [appContext.tabManager.getActiveContextNoteId() ?? ""];
|
||||
}
|
||||
|
||||
this.clonedNoteIds = [];
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
if (!this.clonedNoteIds.includes(noteId)) {
|
||||
this.clonedNoteIds.push(noteId);
|
||||
}
|
||||
}
|
||||
|
||||
utils.openDialog(this.$widget);
|
||||
this.$noteAutoComplete.val("").trigger("focus");
|
||||
this.$noteList.empty();
|
||||
|
||||
for (const noteId of this.clonedNoteIds) {
|
||||
const note = await froca.getNote(noteId);
|
||||
if (!note) {
|
||||
continue;
|
||||
}
|
||||
this.$noteList.append($("<li>").text(note.title));
|
||||
}
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete(this.$noteAutoComplete);
|
||||
noteAutocompleteService.showRecentNotes(this.$noteAutoComplete);
|
||||
}
|
||||
|
||||
async cloneNotesTo(notePath: string) {
|
||||
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
|
||||
if (!noteId || !parentNoteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetBranchId = await froca.getBranchId(parentNoteId, noteId);
|
||||
if (!targetBranchId || !this.clonedNoteIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const cloneNoteId of this.clonedNoteIds) {
|
||||
await branchService.cloneNoteToBranch(cloneNoteId, targetBranchId, this.$clonePrefix.val() as string);
|
||||
|
||||
const clonedNote = await froca.getNote(cloneNoteId);
|
||||
const targetBranch = froca.getBranch(targetBranchId);
|
||||
if (!clonedNote || !targetBranch) {
|
||||
continue;
|
||||
}
|
||||
const targetNote = await targetBranch.getNote();
|
||||
if (!targetNote) {
|
||||
continue;
|
||||
}
|
||||
|
||||
toastService.showMessage(t("clone_to.note_cloned", { clonedTitle: clonedNote.title, targetTitle: targetNote.title }));
|
||||
}
|
||||
}
|
||||
}
|
||||
151
apps/client/src/widgets/dialogs/confirm.ts
Normal file
151
apps/client/src/widgets/dialogs/confirm.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const DELETE_NOTE_BUTTON_CLASS = "confirm-dialog-delete-note";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="confirm-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${t("confirm.confirmation")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("confirm.close")}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="confirm-dialog-content"></div>
|
||||
|
||||
<div class="confirm-dialog-custom"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="confirm-dialog-cancel-button btn btn-sm">${t("confirm.cancel")}</button>
|
||||
|
||||
|
||||
|
||||
<button class="confirm-dialog-ok-button btn btn-primary btn-sm">${t("confirm.ok")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export type ConfirmDialogResult = false | ConfirmDialogOptions;
|
||||
export type ConfirmDialogCallback = (val?: ConfirmDialogResult) => void;
|
||||
|
||||
export interface ConfirmDialogOptions {
|
||||
confirmed: boolean;
|
||||
isDeleteNoteChecked: boolean;
|
||||
}
|
||||
|
||||
// For "showConfirmDialog"
|
||||
|
||||
export interface ConfirmWithMessageOptions {
|
||||
message: string | HTMLElement | JQuery<HTMLElement>;
|
||||
callback: ConfirmDialogCallback;
|
||||
}
|
||||
|
||||
export interface ConfirmWithTitleOptions {
|
||||
title: string;
|
||||
callback: ConfirmDialogCallback;
|
||||
}
|
||||
|
||||
export default class ConfirmDialog extends BasicWidget {
|
||||
private resolve: ConfirmDialogCallback | null;
|
||||
|
||||
private modal!: Modal;
|
||||
private $originallyFocused!: JQuery<HTMLElement> | null;
|
||||
private $confirmContent!: JQuery<HTMLElement>;
|
||||
private $okButton!: JQuery<HTMLElement>;
|
||||
private $cancelButton!: JQuery<HTMLElement>;
|
||||
private $custom!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.resolve = null;
|
||||
this.$originallyFocused = null; // element focused before the dialog was opened, so we can return to it afterward
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||
this.$confirmContent = this.$widget.find(".confirm-dialog-content");
|
||||
this.$okButton = this.$widget.find(".confirm-dialog-ok-button");
|
||||
this.$cancelButton = this.$widget.find(".confirm-dialog-cancel-button");
|
||||
this.$custom = this.$widget.find(".confirm-dialog-custom");
|
||||
|
||||
this.$widget.on("shown.bs.modal", () => this.$okButton.trigger("focus"));
|
||||
|
||||
this.$widget.on("hidden.bs.modal", () => {
|
||||
if (this.resolve) {
|
||||
this.resolve(false);
|
||||
}
|
||||
|
||||
if (this.$originallyFocused) {
|
||||
this.$originallyFocused.trigger("focus");
|
||||
this.$originallyFocused = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.$cancelButton.on("click", () => this.doResolve(false));
|
||||
this.$okButton.on("click", () => this.doResolve(true));
|
||||
}
|
||||
|
||||
showConfirmDialogEvent({ message, callback }: ConfirmWithMessageOptions) {
|
||||
this.$originallyFocused = $(":focus");
|
||||
|
||||
this.$custom.hide();
|
||||
|
||||
glob.activeDialog = this.$widget;
|
||||
|
||||
if (typeof message === "string") {
|
||||
message = $("<div>").text(message);
|
||||
}
|
||||
|
||||
this.$confirmContent.empty().append(message);
|
||||
|
||||
this.modal.show();
|
||||
|
||||
this.resolve = callback;
|
||||
}
|
||||
|
||||
showConfirmDeleteNoteBoxWithNoteDialogEvent({ title, callback }: ConfirmWithTitleOptions) {
|
||||
glob.activeDialog = this.$widget;
|
||||
|
||||
this.$confirmContent.text(`${t("confirm.are_you_sure_remove_note", { title: title })}`);
|
||||
|
||||
this.$custom
|
||||
.empty()
|
||||
.append("<br/>")
|
||||
.append(
|
||||
$("<div>")
|
||||
.addClass("form-check")
|
||||
.append(
|
||||
$("<label>")
|
||||
.addClass("form-check-label")
|
||||
.attr("style", "text-decoration: underline dotted var(--main-text-color)")
|
||||
.attr("title", `${t("confirm.if_you_dont_check")}`)
|
||||
.append($("<input>").attr("type", "checkbox").addClass(`form-check-input ${DELETE_NOTE_BUTTON_CLASS}`))
|
||||
.append(`${t("confirm.also_delete_note")}`)
|
||||
)
|
||||
);
|
||||
|
||||
this.$custom.show();
|
||||
|
||||
this.modal.show();
|
||||
|
||||
this.resolve = callback;
|
||||
}
|
||||
|
||||
doResolve(ret: boolean) {
|
||||
if (this.resolve) {
|
||||
this.resolve({
|
||||
confirmed: ret,
|
||||
isDeleteNoteChecked: this.$widget.find(`.${DELETE_NOTE_BUTTON_CLASS}:checked`).length > 0
|
||||
});
|
||||
}
|
||||
|
||||
this.resolve = null;
|
||||
|
||||
this.modal.hide();
|
||||
}
|
||||
}
|
||||
198
apps/client/src/widgets/dialogs/delete_notes.ts
Normal file
198
apps/client/src/widgets/dialogs/delete_notes.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import server from "../../services/server.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import linkService from "../../services/link.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type { FAttributeRow } from "../../entities/fattribute.js";
|
||||
|
||||
// TODO: Use common with server.
|
||||
interface Response {
|
||||
noteIdsToBeDeleted: string[];
|
||||
brokenRelations: FAttributeRow[];
|
||||
}
|
||||
|
||||
export interface ResolveOptions {
|
||||
proceed: boolean;
|
||||
deleteAllClones?: boolean;
|
||||
eraseNotes?: boolean;
|
||||
}
|
||||
|
||||
interface ShowDeleteNotesDialogOpts {
|
||||
branchIdsToDelete: string[];
|
||||
callback: (opts: ResolveOptions) => void;
|
||||
forceDeleteAllClones: boolean;
|
||||
}
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="delete-notes-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">${t("delete_notes.delete_notes_preview")}</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("delete_notes.close")}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-checkbox">
|
||||
<label for="delete-all-clones" class="form-check-label tn-checkbox">
|
||||
<input id="delete-all-clones" class="delete-all-clones form-check-input" value="1" type="checkbox">
|
||||
${t("delete_notes.delete_all_clones_description")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-checkbox" style="margin-bottom: 1rem">
|
||||
<label for="erase-notes" class="form-check-label tn-checkbox">
|
||||
<input id="erase-notes" class="erase-notes form-check-input" value="1" type="checkbox">
|
||||
${t("delete_notes.erase_notes_warning")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="delete-notes-list-wrapper">
|
||||
<h4>${t("delete_notes.notes_to_be_deleted", { noteCount: '<span class="deleted-notes-count"></span>' })}</h4>
|
||||
|
||||
<ul class="delete-notes-list" style="max-height: 200px; overflow: auto;"></ul>
|
||||
</div>
|
||||
|
||||
<div class="no-note-to-delete-wrapper alert alert-info">
|
||||
${t("delete_notes.no_note_to_delete")}
|
||||
</div>
|
||||
|
||||
<div class="broken-relations-wrapper">
|
||||
<div class="alert alert-danger">
|
||||
<h4>${t("delete_notes.broken_relations_to_be_deleted", { relationCount: '<span class="broke-relations-count"></span>' })}</h4>
|
||||
|
||||
<ul class="broken-relations-list" style="max-height: 200px; overflow: auto;"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="delete-notes-dialog-cancel-button btn btn-sm">${t("delete_notes.cancel")}</button>
|
||||
|
||||
|
||||
|
||||
<button class="delete-notes-dialog-ok-button btn btn-primary btn-sm">${t("delete_notes.ok")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class DeleteNotesDialog extends BasicWidget {
|
||||
private branchIds: string[] | null;
|
||||
private resolve!: (options: ResolveOptions) => void;
|
||||
|
||||
private $content!: JQuery<HTMLElement>;
|
||||
private $okButton!: JQuery<HTMLElement>;
|
||||
private $cancelButton!: JQuery<HTMLElement>;
|
||||
private $deleteNotesList!: JQuery<HTMLElement>;
|
||||
private $brokenRelationsList!: JQuery<HTMLElement>;
|
||||
private $deletedNotesCount!: JQuery<HTMLElement>;
|
||||
private $noNoteToDeleteWrapper!: JQuery<HTMLElement>;
|
||||
private $deleteNotesListWrapper!: JQuery<HTMLElement>;
|
||||
private $brokenRelationsListWrapper!: JQuery<HTMLElement>;
|
||||
private $brokenRelationsCount!: JQuery<HTMLElement>;
|
||||
private $deleteAllClones!: JQuery<HTMLElement>;
|
||||
private $eraseNotes!: JQuery<HTMLElement>;
|
||||
|
||||
private forceDeleteAllClones?: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.branchIds = null;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$content = this.$widget.find(".recent-changes-content");
|
||||
this.$okButton = this.$widget.find(".delete-notes-dialog-ok-button");
|
||||
this.$cancelButton = this.$widget.find(".delete-notes-dialog-cancel-button");
|
||||
this.$deleteNotesList = this.$widget.find(".delete-notes-list");
|
||||
this.$brokenRelationsList = this.$widget.find(".broken-relations-list");
|
||||
this.$deletedNotesCount = this.$widget.find(".deleted-notes-count");
|
||||
this.$noNoteToDeleteWrapper = this.$widget.find(".no-note-to-delete-wrapper");
|
||||
this.$deleteNotesListWrapper = this.$widget.find(".delete-notes-list-wrapper");
|
||||
this.$brokenRelationsListWrapper = this.$widget.find(".broken-relations-wrapper");
|
||||
this.$brokenRelationsCount = this.$widget.find(".broke-relations-count");
|
||||
this.$deleteAllClones = this.$widget.find(".delete-all-clones");
|
||||
this.$eraseNotes = this.$widget.find(".erase-notes");
|
||||
|
||||
this.$widget.on("shown.bs.modal", () => this.$okButton.trigger("focus"));
|
||||
|
||||
this.$cancelButton.on("click", () => {
|
||||
utils.closeActiveDialog();
|
||||
|
||||
this.resolve({ proceed: false });
|
||||
});
|
||||
|
||||
this.$okButton.on("click", () => {
|
||||
utils.closeActiveDialog();
|
||||
|
||||
this.resolve({
|
||||
proceed: true,
|
||||
deleteAllClones: this.forceDeleteAllClones || this.isDeleteAllClonesChecked(),
|
||||
eraseNotes: this.isEraseNotesChecked()
|
||||
});
|
||||
});
|
||||
|
||||
this.$deleteAllClones.on("click", () => this.renderDeletePreview());
|
||||
}
|
||||
|
||||
async renderDeletePreview() {
|
||||
const response = await server.post<Response>("delete-notes-preview", {
|
||||
branchIdsToDelete: this.branchIds,
|
||||
deleteAllClones: this.forceDeleteAllClones || this.isDeleteAllClonesChecked()
|
||||
});
|
||||
|
||||
this.$deleteNotesList.empty();
|
||||
this.$brokenRelationsList.empty();
|
||||
|
||||
this.$deleteNotesListWrapper.toggle(response.noteIdsToBeDeleted.length > 0);
|
||||
this.$noNoteToDeleteWrapper.toggle(response.noteIdsToBeDeleted.length === 0);
|
||||
|
||||
for (const note of await froca.getNotes(response.noteIdsToBeDeleted)) {
|
||||
this.$deleteNotesList.append($("<li>").append(await linkService.createLink(note.noteId, { showNotePath: true })));
|
||||
}
|
||||
|
||||
this.$deletedNotesCount.text(response.noteIdsToBeDeleted.length);
|
||||
|
||||
this.$brokenRelationsListWrapper.toggle(response.brokenRelations.length > 0);
|
||||
this.$brokenRelationsCount.text(response.brokenRelations.length);
|
||||
|
||||
await froca.getNotes(response.brokenRelations.map((br) => br.noteId));
|
||||
|
||||
for (const attr of response.brokenRelations) {
|
||||
this.$brokenRelationsList.append(
|
||||
$("<li>").html(
|
||||
t("delete_notes.deleted_relation_text", {
|
||||
note: (await linkService.createLink(attr.value)).html(),
|
||||
relation: `<code>${attr.name}</code>`,
|
||||
source: (await linkService.createLink(attr.noteId)).html()
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async showDeleteNotesDialogEvent({ branchIdsToDelete, callback, forceDeleteAllClones }: ShowDeleteNotesDialogOpts) {
|
||||
this.branchIds = branchIdsToDelete;
|
||||
this.forceDeleteAllClones = forceDeleteAllClones;
|
||||
|
||||
await this.renderDeletePreview();
|
||||
|
||||
utils.openDialog(this.$widget);
|
||||
|
||||
this.$deleteAllClones.prop("checked", !!forceDeleteAllClones).prop("disabled", !!forceDeleteAllClones);
|
||||
|
||||
this.$eraseNotes.prop("checked", false);
|
||||
|
||||
this.resolve = callback;
|
||||
}
|
||||
|
||||
isDeleteAllClonesChecked() {
|
||||
return this.$deleteAllClones.is(":checked");
|
||||
}
|
||||
|
||||
isEraseNotesChecked() {
|
||||
return this.$eraseNotes.is(":checked");
|
||||
}
|
||||
}
|
||||
263
apps/client/src/widgets/dialogs/export.ts
Normal file
263
apps/client/src/widgets/dialogs/export.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import treeService from "../../services/tree.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import ws from "../../services/ws.js";
|
||||
import toastService, { type ToastOptions } from "../../services/toast.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import openService from "../../services/open.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="export-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||
<style>
|
||||
.export-dialog .export-form .form-check {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.export-dialog .export-form .format-choice {
|
||||
padding-left: 40px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.export-dialog .export-form .opml-versions {
|
||||
padding-left: 60px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.export-dialog .export-form .form-check-label {
|
||||
padding: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${t("export.export_note_title")} <span class="export-note-title"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("export.close")}"></button>
|
||||
</div>
|
||||
<form class="export-form">
|
||||
<div class="modal-body">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label tn-radio">
|
||||
<input class="export-type-subtree form-check-input" type="radio" name="export-type" value="subtree">
|
||||
${t("export.export_type_subtree")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="export-subtree-formats format-choice">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label tn-radio">
|
||||
<input class="form-check-input" type="radio" name="export-subtree-format" value="html">
|
||||
${t("export.format_html_zip")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<label class="form-check-label tn-radio">
|
||||
<input class="form-check-input" type="radio" name="export-subtree-format" value="markdown">
|
||||
${t("export.format_markdown")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<label class="form-check-label tn-radio">
|
||||
<input class="form-check-input" type="radio" name="export-subtree-format" value="opml">
|
||||
${t("export.format_opml")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="opml-versions">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label tn-radio">
|
||||
<input class="form-check-input" type="radio" name="opml-version" value="1.0">
|
||||
${t("export.opml_version_1")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<label class="form-check-label tn-radio">
|
||||
<input class="form-check-input" type="radio" name="opml-version" value="2.0">
|
||||
${t("export.opml_version_2")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<label class="form-check-label tn-radio">
|
||||
<input class="form-check-input" type="radio" name="export-type" value="single">
|
||||
${t("export.export_type_single")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="export-single-formats format-choice">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label tn-radio">
|
||||
<input class="form-check-input" type="radio" name="export-single-format" value="html">
|
||||
${t("export.format_html")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<label class="form-check-label tn-radio">
|
||||
<input class="form-check-input" type="radio" name="export-single-format" value="markdown">
|
||||
${t("export.format_markdown")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="export-button btn btn-primary">${t("export.export")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class ExportDialog extends BasicWidget {
|
||||
|
||||
private taskId: string;
|
||||
private branchId: string | null;
|
||||
private modal?: Modal;
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
private $noteTitle!: JQuery<HTMLElement>;
|
||||
private $subtreeFormats!: JQuery<HTMLElement>;
|
||||
private $singleFormats!: JQuery<HTMLElement>;
|
||||
private $subtreeType!: JQuery<HTMLElement>;
|
||||
private $singleType!: JQuery<HTMLElement>;
|
||||
private $exportButton!: JQuery<HTMLElement>;
|
||||
private $opmlVersions!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.taskId = "";
|
||||
this.branchId = null;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||
this.$form = this.$widget.find(".export-form");
|
||||
this.$noteTitle = this.$widget.find(".export-note-title");
|
||||
this.$subtreeFormats = this.$widget.find(".export-subtree-formats");
|
||||
this.$singleFormats = this.$widget.find(".export-single-formats");
|
||||
this.$subtreeType = this.$widget.find(".export-type-subtree");
|
||||
this.$singleType = this.$widget.find(".export-type-single");
|
||||
this.$exportButton = this.$widget.find(".export-button");
|
||||
this.$opmlVersions = this.$widget.find(".opml-versions");
|
||||
|
||||
this.$form.on("submit", () => {
|
||||
this.modal?.hide();
|
||||
|
||||
const exportType = this.$widget.find("input[name='export-type']:checked").val();
|
||||
|
||||
if (!exportType) {
|
||||
toastService.showError(t("export.choose_export_type"));
|
||||
return;
|
||||
}
|
||||
|
||||
const exportFormat = exportType === "subtree" ? this.$widget.find("input[name=export-subtree-format]:checked").val() : this.$widget.find("input[name=export-single-format]:checked").val();
|
||||
|
||||
const exportVersion = exportFormat === "opml" ? this.$widget.find("input[name='opml-version']:checked").val() : "1.0";
|
||||
|
||||
if (this.branchId) {
|
||||
this.exportBranch(this.branchId, String(exportType), String(exportFormat), String(exportVersion));
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$widget.find("input[name=export-type]").on("change", (e) => {
|
||||
if ((e.currentTarget as HTMLInputElement).value === "subtree") {
|
||||
if (this.$widget.find("input[name=export-subtree-format]:checked").length === 0) {
|
||||
this.$widget.find("input[name=export-subtree-format]:first").prop("checked", true);
|
||||
}
|
||||
|
||||
this.$subtreeFormats.slideDown();
|
||||
this.$singleFormats.slideUp();
|
||||
} else {
|
||||
if (this.$widget.find("input[name=export-single-format]:checked").length === 0) {
|
||||
this.$widget.find("input[name=export-single-format]:first").prop("checked", true);
|
||||
}
|
||||
|
||||
this.$subtreeFormats.slideUp();
|
||||
this.$singleFormats.slideDown();
|
||||
}
|
||||
});
|
||||
|
||||
this.$widget.find("input[name=export-subtree-format]").on("change", (e) => {
|
||||
if ((e.currentTarget as HTMLInputElement).value === "opml") {
|
||||
this.$opmlVersions.slideDown();
|
||||
} else {
|
||||
this.$opmlVersions.slideUp();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async showExportDialogEvent({ notePath, defaultType }: EventData<"showExportDialog">) {
|
||||
this.taskId = "";
|
||||
this.$exportButton.removeAttr("disabled");
|
||||
|
||||
if (defaultType === "subtree") {
|
||||
this.$subtreeType.prop("checked", true).trigger("change");
|
||||
|
||||
this.$widget.find("input[name=export-subtree-format]:checked").trigger("change");
|
||||
} else if (defaultType === "single") {
|
||||
this.$singleType.prop("checked", true).trigger("change");
|
||||
} else {
|
||||
throw new Error(`Unrecognized type '${defaultType}'`);
|
||||
}
|
||||
|
||||
this.$widget.find(".opml-v2").prop("checked", true); // setting default
|
||||
|
||||
utils.openDialog(this.$widget);
|
||||
|
||||
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
|
||||
|
||||
if (parentNoteId) {
|
||||
this.branchId = await froca.getBranchId(parentNoteId, noteId);
|
||||
}
|
||||
if (noteId) {
|
||||
this.$noteTitle.text(await treeService.getNoteTitle(noteId));
|
||||
}
|
||||
}
|
||||
|
||||
exportBranch(branchId: string, type: string, format: string, version: string) {
|
||||
this.taskId = utils.randomString(10);
|
||||
|
||||
const url = openService.getUrlForDownload(`api/branches/${branchId}/export/${type}/${format}/${version}/${this.taskId}`);
|
||||
|
||||
openService.download(url);
|
||||
}
|
||||
}
|
||||
|
||||
ws.subscribeToMessages(async (message) => {
|
||||
function makeToast(id: string, message: string): ToastOptions {
|
||||
return {
|
||||
id: id,
|
||||
title: t("export.export_status"),
|
||||
message: message,
|
||||
icon: "arrow-square-up-right"
|
||||
};
|
||||
}
|
||||
|
||||
if (message.taskType !== "export") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === "taskError") {
|
||||
toastService.closePersistent(message.taskId);
|
||||
toastService.showError(message.message);
|
||||
} else if (message.type === "taskProgressCount") {
|
||||
toastService.showPersistent(makeToast(message.taskId, t("export.export_in_progress", { progressCount: message.progressCount })));
|
||||
} else if (message.type === "taskSucceeded") {
|
||||
const toast = makeToast(message.taskId, t("export.export_finished_successfully"));
|
||||
toast.closeAfter = 5000;
|
||||
|
||||
toastService.showPersistent(toast);
|
||||
}
|
||||
});
|
||||
159
apps/client/src/widgets/dialogs/help.ts
Normal file
159
apps/client/src/widgets/dialogs/help.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import utils from "../../services/utils.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="help-dialog modal use-tn-links" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document" style="min-width: 90%;">
|
||||
<div class="modal-content" style="height: auto;">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${t("help.fullDocumentation")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("help.close")}"></button>
|
||||
</div>
|
||||
<div class="modal-body" style="overflow: auto;">
|
||||
<div class="help-cards row row-cols-md-3 g-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${t("help.noteNavigation")}</h5>
|
||||
|
||||
<p class="card-text">
|
||||
<ul>
|
||||
<li>${t("help.goUpDown")}</li>
|
||||
<li>${t("help.collapseExpand")}</li>
|
||||
<li><kbd data-command="backInNoteHistory">${t("help.notSet")}</kbd>, <kbd data-command="forwardInNoteHistory">${t("help.notSet")}</kbd> - ${t("help.goBackForwards")}</li>
|
||||
<li><kbd data-command="jumpToNote">${t("help.notSet")}</kbd> - ${t("help.showJumpToNoteDialog")}</li>
|
||||
<li><kbd data-command="scrollToActiveNote">${t("help.notSet")}</kbd> - ${t("help.scrollToActiveNote")}</li>
|
||||
<li>${t("help.jumpToParentNote")}</li>
|
||||
<li><kbd data-command="collapseTree">${t("help.notSet")}</kbd> - ${t("help.collapseWholeTree")}</li>
|
||||
<li><kbd data-command="collapseSubtree">${t("help.notSet")}</kbd> - ${t("help.collapseSubTree")}</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${t("help.tabShortcuts")}</h5>
|
||||
|
||||
<p class="card-text">
|
||||
<ul>
|
||||
<li>${t("help.newTabNoteLink")}</li>
|
||||
</ul>
|
||||
<h6>${t("help.onlyInDesktop")}:</h6>
|
||||
<ul>
|
||||
<li><kbd data-command="openNewTab">${t("help.notSet")}</kbd> ${t("help.openEmptyTab")}</li>
|
||||
<li><kbd data-command="closeActiveTab">${t("help.notSet")}</kbd> ${t("help.closeActiveTab")}</li>
|
||||
<li><kbd data-command="activateNextTab">${t("help.notSet")}</kbd> ${t("help.activateNextTab")}</li>
|
||||
<li><kbd data-command="activatePreviousTab">${t("help.notSet")}</kbd> ${t("help.activatePreviousTab")}</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${t("help.creatingNotes")}</h5>
|
||||
|
||||
<p class="card-text">
|
||||
<ul>
|
||||
<li><kbd data-command="createNoteAfter">${t("help.notSet")}</kbd> - ${t("help.createNoteAfter")}</li>
|
||||
<li><kbd data-command="createNoteInto">${t("help.notSet")}</kbd> - ${t("help.createNoteInto")}</li>
|
||||
<li><kbd data-command="editBranchPrefix">${t("help.notSet")}</kbd> - ${t("help.editBranchPrefix")}</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${t("help.movingCloningNotes")}</h5>
|
||||
|
||||
<p class="card-text">
|
||||
<ul>
|
||||
<li><kbd data-command="moveNoteUp">${t("help.notSet")}</kbd>, <kbd data-command="moveNoteDown">${t("help.notSet")}</kbd> - ${t("help.moveNoteUpDown")}</li>
|
||||
<li><kbd data-command="moveNoteUpInHierarchy">${t("help.notSet")}</kbd>, <kbd data-command="moveNoteDownInHierarchy">${t("help.notSet")}</kbd> - ${t("help.moveNoteUpHierarchy")}</li>
|
||||
<li><kbd data-command="addNoteAboveToSelection">${t("help.notSet")}</kbd>, <kbd data-command="addNoteBelowToSelection">${t("help.notSet")}</kbd> - ${t("help.multiSelectNote")}</li>
|
||||
<li><kbd data-command="selectAllNotesInParent">${t("help.notSet")}</kbd> - ${t("help.selectAllNotes")}</li>
|
||||
<li>${t("help.selectNote")}</li>
|
||||
<li><kbd data-command="copyNotesToClipboard">${t("help.notSet")}</kbd> - ${t("help.copyNotes")}</li>
|
||||
<li><kbd data-command="cutNotesToClipboard">${t("help.notSet")}</kbd> - ${t("help.cutNotes")}</li>
|
||||
<li><kbd data-command="pasteNotesFromClipboard">${t("help.notSet")}</kbd> - ${t("help.pasteNotes")}</li>
|
||||
<li><kbd data-command="deleteNotes">${t("help.notSet")}</kbd> - ${t("help.deleteNotes")}</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${t("help.editingNotes")}</h5>
|
||||
|
||||
<p class="card-text">
|
||||
<ul>
|
||||
<li><kbd data-command="editNoteTitle">${t("help.notSet")}</kbd> ${t("help.editNoteTitle")}</li>
|
||||
<li>${t("help.createEditLink")}</li>
|
||||
<li><kbd data-command="addLinkToText">${t("help.notSet")}</kbd> - ${t("help.createInternalLink")}</li>
|
||||
<li><kbd data-command="followLinkUnderCursor">${t("help.notSet")}</kbd> - ${t("help.followLink")}</li>
|
||||
<li><kbd data-command="insertDateTimeToText">${t("help.notSet")}</kbd> - ${t("help.insertDateTime")}</li>
|
||||
<li><kbd data-command="scrollToActiveNote">${t("help.notSet")}</kbd> - ${t("help.jumpToTreePane")}</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><a class="external" href="https://triliumnext.github.io/Docs/Wiki/text-notes.html#markdown--autoformat">${t("help.markdownAutoformat")}</a></h5>
|
||||
|
||||
<p class="card-text">
|
||||
<ul>
|
||||
<li>${t("help.headings")}</li>
|
||||
<li>${t("help.bulletList")}</li>
|
||||
<li>${t("help.numberedList")}</li>
|
||||
<li>${t("help.blockQuote")}</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${t("help.troubleshooting")}</h5>
|
||||
|
||||
<p class="card-text">
|
||||
<ul>
|
||||
<li><kbd data-command="reloadFrontendApp">${t("help.notSet")}</kbd> - ${t("help.reloadFrontend")}</li>
|
||||
<li><kbd data-command="openDevTools">${t("help.notSet")}</kbd> - ${t("help.showDevTools")}</li>
|
||||
<li><kbd data-command="showSQLConsole">${t("help.notSet")}</kbd> - ${t("help.showSQLConsole")}</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${t("help.other")}</h5>
|
||||
|
||||
<p class="card-text">
|
||||
<ul>
|
||||
<li><kbd data-command="quickSearch">${t("help.notSet")}</kbd> - ${t("help.quickSearch")}</li>
|
||||
<li><kbd data-command="findInText">${t("help.notSet")}</kbd> - ${t("help.inPageSearch")}</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class HelpDialog extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
}
|
||||
|
||||
showCheatsheetEvent() {
|
||||
utils.openDialog(this.$widget);
|
||||
}
|
||||
}
|
||||
179
apps/client/src/widgets/dialogs/import.ts
Normal file
179
apps/client/src/widgets/dialogs/import.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import utils, { escapeQuotes } from "../../services/utils.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import importService, { type UploadFilesOptions } from "../../services/import.js";
|
||||
import options from "../../services/options.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import { Modal, Tooltip } from "bootstrap";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${t("import.importIntoNote")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("import.close")}"></button>
|
||||
</div>
|
||||
<form class="import-form">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="import-file-upload-input"><strong>${t("import.chooseImportFile")}</strong></label>
|
||||
|
||||
<label class="tn-file-input tn-input-field">
|
||||
<input type="file" class="import-file-upload-input form-control-file" multiple />
|
||||
</label>
|
||||
|
||||
<p>${t("import.importDescription")} <strong class="import-note-title"></strong>.
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<strong>${t("import.options")}:</strong>
|
||||
|
||||
<div class="checkbox">
|
||||
<label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("import.safeImportTooltip"))}">
|
||||
<input class="safe-import-checkbox" value="1" type="checkbox" checked>
|
||||
<span>${t("import.safeImport")}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("import.explodeArchivesTooltip"))}">
|
||||
<input class="explode-archives-checkbox" value="1" type="checkbox" checked>
|
||||
<span>${t("import.explodeArchives")}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("import.shrinkImagesTooltip"))}">
|
||||
<input class="shrink-images-checkbox" value="1" type="checkbox" checked> <span>${t("import.shrinkImages")}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label class="tn-checkbox">
|
||||
<input class="text-imported-as-text-checkbox" value="1" type="checkbox" checked>
|
||||
${t("import.textImportedAsText")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label class="tn-checkbox">
|
||||
<input class="code-imported-as-code-checkbox" value="1" type="checkbox" checked> ${t("import.codeImportedAsCode")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label class="tn-checkbox">
|
||||
<input class="replace-underscores-with-spaces-checkbox" value="1" type="checkbox" checked>
|
||||
${t("import.replaceUnderscoresWithSpaces")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="import-button btn btn-primary">${t("import.import")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class ImportDialog extends BasicWidget {
|
||||
|
||||
private parentNoteId: string | null;
|
||||
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
private $noteTitle!: JQuery<HTMLElement>;
|
||||
private $fileUploadInput!: JQuery<HTMLInputElement>;
|
||||
private $importButton!: JQuery<HTMLElement>;
|
||||
private $safeImportCheckbox!: JQuery<HTMLElement>;
|
||||
private $shrinkImagesCheckbox!: JQuery<HTMLElement>;
|
||||
private $textImportedAsTextCheckbox!: JQuery<HTMLElement>;
|
||||
private $codeImportedAsCodeCheckbox!: JQuery<HTMLElement>;
|
||||
private $explodeArchivesCheckbox!: JQuery<HTMLElement>;
|
||||
private $replaceUnderscoresWithSpacesCheckbox!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.parentNoteId = null;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
Modal.getOrCreateInstance(this.$widget[0]);
|
||||
|
||||
this.$form = this.$widget.find(".import-form");
|
||||
this.$noteTitle = this.$widget.find(".import-note-title");
|
||||
this.$fileUploadInput = this.$widget.find(".import-file-upload-input");
|
||||
this.$importButton = this.$widget.find(".import-button");
|
||||
this.$safeImportCheckbox = this.$widget.find(".safe-import-checkbox");
|
||||
this.$shrinkImagesCheckbox = this.$widget.find(".shrink-images-checkbox");
|
||||
this.$textImportedAsTextCheckbox = this.$widget.find(".text-imported-as-text-checkbox");
|
||||
this.$codeImportedAsCodeCheckbox = this.$widget.find(".code-imported-as-code-checkbox");
|
||||
this.$explodeArchivesCheckbox = this.$widget.find(".explode-archives-checkbox");
|
||||
this.$replaceUnderscoresWithSpacesCheckbox = this.$widget.find(".replace-underscores-with-spaces-checkbox");
|
||||
|
||||
this.$form.on("submit", () => {
|
||||
// disabling so that import is not triggered again.
|
||||
this.$importButton.attr("disabled", "disabled");
|
||||
|
||||
if (this.parentNoteId) {
|
||||
this.importIntoNote(this.parentNoteId);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$fileUploadInput.on("change", () => {
|
||||
if (this.$fileUploadInput.val()) {
|
||||
this.$importButton.removeAttr("disabled");
|
||||
} else {
|
||||
this.$importButton.attr("disabled", "disabled");
|
||||
}
|
||||
});
|
||||
|
||||
let _ = [...this.$widget.find('[data-bs-toggle="tooltip"]')].forEach((element) => {
|
||||
Tooltip.getOrCreateInstance(element, {
|
||||
html: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async showImportDialogEvent({ noteId }: EventData<"showImportDialog">) {
|
||||
this.parentNoteId = noteId;
|
||||
|
||||
this.$fileUploadInput.val("").trigger("change"); // to trigger Import button disabling listener below
|
||||
|
||||
this.$safeImportCheckbox.prop("checked", true);
|
||||
this.$shrinkImagesCheckbox.prop("checked", options.is("compressImages"));
|
||||
this.$textImportedAsTextCheckbox.prop("checked", true);
|
||||
this.$codeImportedAsCodeCheckbox.prop("checked", true);
|
||||
this.$explodeArchivesCheckbox.prop("checked", true);
|
||||
this.$replaceUnderscoresWithSpacesCheckbox.prop("checked", true);
|
||||
|
||||
this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId));
|
||||
|
||||
utils.openDialog(this.$widget);
|
||||
}
|
||||
|
||||
async importIntoNote(parentNoteId: string) {
|
||||
const files = Array.from(this.$fileUploadInput[0].files ?? []); // shallow copy since we're resetting the upload button below
|
||||
|
||||
const boolToString = ($el: JQuery<HTMLElement>) => ($el.is(":checked") ? "true" : "false");
|
||||
|
||||
const options: UploadFilesOptions = {
|
||||
safeImport: boolToString(this.$safeImportCheckbox),
|
||||
shrinkImages: boolToString(this.$shrinkImagesCheckbox),
|
||||
textImportedAsText: boolToString(this.$textImportedAsTextCheckbox),
|
||||
codeImportedAsCode: boolToString(this.$codeImportedAsCodeCheckbox),
|
||||
explodeArchives: boolToString(this.$explodeArchivesCheckbox),
|
||||
replaceUnderscoresWithSpaces: boolToString(this.$replaceUnderscoresWithSpacesCheckbox)
|
||||
};
|
||||
|
||||
this.$widget.modal("hide");
|
||||
|
||||
await importService.uploadFiles("notes", parentNoteId, files, options);
|
||||
}
|
||||
}
|
||||
116
apps/client/src/widgets/dialogs/include_note.ts
Normal file
116
apps/client/src/widgets/dialogs/include_note.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { Modal } from "bootstrap";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import type EditableTextTypeWidget from "../type_widgets/editable_text.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="include-note-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${t("include_note.dialog_title")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("include_note.close")}"></button>
|
||||
</div>
|
||||
<form class="include-note-form">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="include-note-autocomplete">${t("include_note.label_note")}</label>
|
||||
<div class="input-group">
|
||||
<input class="include-note-autocomplete form-control" placeholder="${t("include_note.placeholder_search")}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${t("include_note.box_size_prompt")}
|
||||
|
||||
<div class="form-check">
|
||||
<label class="form-check-label tn-radio">
|
||||
<input class="form-check-input" type="radio" name="include-note-box-size" value="small">
|
||||
${t("include_note.box_size_small")}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label class="form-check-label tn-radio">
|
||||
<input class="form-check-input" type="radio" name="include-note-box-size" value="medium" checked>
|
||||
${t("include_note.box_size_medium")}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label class="form-check-label tn-radio">
|
||||
<input class="form-check-input" type="radio" name="include-note-box-size" value="full">
|
||||
${t("include_note.box_size_full")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">${t("include_note.button_include")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class IncludeNoteDialog extends BasicWidget {
|
||||
|
||||
private modal!: bootstrap.Modal;
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
private $autoComplete!: JQuery<HTMLElement>;
|
||||
private textTypeWidget?: EditableTextTypeWidget;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||
this.$form = this.$widget.find(".include-note-form");
|
||||
this.$autoComplete = this.$widget.find(".include-note-autocomplete");
|
||||
this.$form.on("submit", () => {
|
||||
const notePath = this.$autoComplete.getSelectedNotePath();
|
||||
|
||||
if (notePath) {
|
||||
this.modal.hide();
|
||||
this.includeNote(notePath);
|
||||
} else {
|
||||
logError("No noteId to include.");
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
async showIncludeNoteDialogEvent({ textTypeWidget }: EventData<"showIncludeDialog">) {
|
||||
this.textTypeWidget = textTypeWidget;
|
||||
await this.refresh();
|
||||
utils.openDialog(this.$widget);
|
||||
|
||||
this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.$autoComplete.val("");
|
||||
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
|
||||
hideGoToSelectedNoteButton: true,
|
||||
allowCreatingNotes: true
|
||||
});
|
||||
noteAutocompleteService.showRecentNotes(this.$autoComplete);
|
||||
}
|
||||
|
||||
async includeNote(notePath: string) {
|
||||
const noteId = treeService.getNoteIdFromUrl(notePath);
|
||||
if (!noteId) {
|
||||
return;
|
||||
}
|
||||
const note = await froca.getNote(noteId);
|
||||
const boxSize = $("input[name='include-note-box-size']:checked").val() as string;
|
||||
|
||||
if (["image", "canvas", "mermaid"].includes(note?.type ?? "")) {
|
||||
// there's no benefit to use insert note functionlity for images,
|
||||
// so we'll just add an IMG tag
|
||||
this.textTypeWidget?.addImage(noteId);
|
||||
} else {
|
||||
this.textTypeWidget?.addIncludeNote(noteId, boxSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
79
apps/client/src/widgets/dialogs/info.ts
Normal file
79
apps/client/src/widgets/dialogs/info.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { Modal } from "bootstrap";
|
||||
import type { ConfirmDialogCallback } from "./confirm.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="info-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${t("info.modalTitle")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("info.closeButton")}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="info-dialog-content"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="info-dialog-ok-button btn btn-primary btn-sm">${t("info.okButton")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class InfoDialog extends BasicWidget {
|
||||
|
||||
private resolve: ConfirmDialogCallback | null;
|
||||
private modal!: bootstrap.Modal;
|
||||
private $originallyFocused!: JQuery<HTMLElement> | null;
|
||||
private $infoContent!: JQuery<HTMLElement>;
|
||||
private $okButton!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.resolve = null;
|
||||
this.$originallyFocused = null; // element focused before the dialog was opened, so we can return to it afterward
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||
this.$infoContent = this.$widget.find(".info-dialog-content");
|
||||
this.$okButton = this.$widget.find(".info-dialog-ok-button");
|
||||
|
||||
this.$widget.on("shown.bs.modal", () => this.$okButton.trigger("focus"));
|
||||
|
||||
this.$widget.on("hidden.bs.modal", () => {
|
||||
if (this.resolve) {
|
||||
this.resolve();
|
||||
}
|
||||
|
||||
if (this.$originallyFocused) {
|
||||
this.$originallyFocused.trigger("focus");
|
||||
this.$originallyFocused = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.$okButton.on("click", () => this.modal.hide());
|
||||
}
|
||||
|
||||
showInfoDialogEvent({ message, callback }: EventData<"showInfoDialog">) {
|
||||
this.$originallyFocused = $(":focus");
|
||||
|
||||
if (typeof message === "string") {
|
||||
this.$infoContent.text(message);
|
||||
} else if (Array.isArray(message)) {
|
||||
this.$infoContent.html(message[0]);
|
||||
} else {
|
||||
this.$infoContent.html(message as HTMLElement);
|
||||
}
|
||||
|
||||
|
||||
utils.openDialog(this.$widget);
|
||||
|
||||
this.resolve = callback;
|
||||
}
|
||||
}
|
||||
132
apps/client/src/widgets/dialogs/jump_to_note.ts
Normal file
132
apps/client/src/widgets/dialogs/jump_to_note.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import shortcutService from "../../services/shortcuts.js";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const TPL = /*html*/`<div class="jump-to-note-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="input-group">
|
||||
<input class="jump-to-note-autocomplete form-control" placeholder="${t("jump_to_note.search_placeholder")}">
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("jump_to_note.close")}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="algolia-autocomplete-container jump-to-note-results"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="show-in-full-text-button btn btn-sm">${t("jump_to_note.search_button")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
||||
|
||||
export default class JumpToNoteDialog extends BasicWidget {
|
||||
|
||||
private lastOpenedTs: number;
|
||||
private modal!: bootstrap.Modal;
|
||||
private $autoComplete!: JQuery<HTMLElement>;
|
||||
private $results!: JQuery<HTMLElement>;
|
||||
private $showInFullTextButton!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.lastOpenedTs = 0;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||
|
||||
this.$autoComplete = this.$widget.find(".jump-to-note-autocomplete");
|
||||
this.$results = this.$widget.find(".jump-to-note-results");
|
||||
this.$showInFullTextButton = this.$widget.find(".show-in-full-text-button");
|
||||
this.$showInFullTextButton.on("click", (e) => this.showInFullText(e));
|
||||
|
||||
shortcutService.bindElShortcut(this.$widget, "ctrl+return", (e) => this.showInFullText(e));
|
||||
}
|
||||
|
||||
async jumpToNoteEvent() {
|
||||
const dialogPromise = utils.openDialog(this.$widget);
|
||||
if (utils.isMobile()) {
|
||||
dialogPromise.then(($dialog) => {
|
||||
const el = $dialog.find(">.modal-dialog")[0];
|
||||
|
||||
function reposition() {
|
||||
const offset = 100;
|
||||
const modalHeight = (window.visualViewport?.height ?? 0) - offset;
|
||||
const safeAreaInsetBottom = (window.visualViewport?.height ?? 0) - window.innerHeight;
|
||||
el.style.height = `${modalHeight}px`;
|
||||
el.style.bottom = `${(window.visualViewport?.height ?? 0) - modalHeight - safeAreaInsetBottom - offset}px`;
|
||||
}
|
||||
|
||||
this.$autoComplete.on("focus", () => {
|
||||
reposition();
|
||||
});
|
||||
|
||||
window.visualViewport?.addEventListener("resize", () => {
|
||||
reposition();
|
||||
});
|
||||
|
||||
reposition();
|
||||
});
|
||||
}
|
||||
|
||||
// first open dialog, then refresh since refresh is doing focus which should be visible
|
||||
this.refresh();
|
||||
|
||||
this.lastOpenedTs = Date.now();
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
noteAutocompleteService
|
||||
.initNoteAutocomplete(this.$autoComplete, {
|
||||
allowCreatingNotes: true,
|
||||
hideGoToSelectedNoteButton: true,
|
||||
allowJumpToSearchNotes: true,
|
||||
container: this.$results[0]
|
||||
})
|
||||
// clear any event listener added in previous invocation of this function
|
||||
.off("autocomplete:noteselected")
|
||||
.on("autocomplete:noteselected", function (event, suggestion, dataset) {
|
||||
if (!suggestion.notePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
appContext.tabManager.getActiveContext()?.setNote(suggestion.notePath);
|
||||
});
|
||||
|
||||
// if you open the Jump To dialog soon after using it previously, it can often mean that you
|
||||
// actually want to search for the same thing (e.g., you opened the wrong note at first try)
|
||||
// so we'll keep the content.
|
||||
// if it's outside of this time limit, then we assume it's a completely new search and show recent notes instead.
|
||||
if (Date.now() - this.lastOpenedTs > KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000) {
|
||||
noteAutocompleteService.showRecentNotes(this.$autoComplete);
|
||||
} else {
|
||||
this.$autoComplete
|
||||
// hack, the actual search value is stored in <pre> element next to the search input
|
||||
// this is important because the search input value is replaced with the suggestion note's title
|
||||
.autocomplete("val", this.$autoComplete.next().text())
|
||||
.trigger("focus")
|
||||
.trigger("select");
|
||||
}
|
||||
}
|
||||
|
||||
showInFullText(e: JQuery.TriggeredEvent) {
|
||||
// stop from propagating upwards (dangerous, especially with ctrl+enter executable javascript notes)
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const searchString = String(this.$autoComplete.val());
|
||||
|
||||
this.triggerCommand("searchNotes", { searchString });
|
||||
this.modal.hide();
|
||||
}
|
||||
}
|
||||
104
apps/client/src/widgets/dialogs/markdown_import.ts
Normal file
104
apps/client/src/widgets/dialogs/markdown_import.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import shortcutService from "../../services/shortcuts.js";
|
||||
import server from "../../services/server.js";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="markdown-import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${t("markdown_import.dialog_title")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("markdown_import.close")}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>${t("markdown_import.modal_body_text")}</p>
|
||||
|
||||
<textarea class="markdown-import-textarea" style="height: 340px; width: 100%"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="markdown-import-button btn btn-primary">${t("markdown_import.import_button")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
interface RenderMarkdownResponse {
|
||||
htmlContent: string;
|
||||
}
|
||||
|
||||
export default class MarkdownImportDialog extends BasicWidget {
|
||||
|
||||
private lastOpenedTs: number;
|
||||
private modal!: bootstrap.Modal;
|
||||
private $importTextarea!: JQuery<HTMLElement>;
|
||||
private $importButton!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.lastOpenedTs = 0;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||
this.$importTextarea = this.$widget.find(".markdown-import-textarea");
|
||||
this.$importButton = this.$widget.find(".markdown-import-button");
|
||||
|
||||
this.$importButton.on("click", () => this.sendForm());
|
||||
|
||||
this.$widget.on("shown.bs.modal", () => this.$importTextarea.trigger("focus"));
|
||||
|
||||
shortcutService.bindElShortcut(this.$widget, "ctrl+return", () => this.sendForm());
|
||||
}
|
||||
|
||||
async convertMarkdownToHtml(markdownContent: string) {
|
||||
const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent });
|
||||
|
||||
const textEditor = await appContext.tabManager.getActiveContext()?.getTextEditor();
|
||||
if (!textEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewFragment = textEditor.data.processor.toView(htmlContent);
|
||||
const modelFragment = textEditor.data.toModel(viewFragment);
|
||||
|
||||
textEditor.model.insertContent(modelFragment, textEditor.model.document.selection);
|
||||
|
||||
toastService.showMessage(t("markdown_import.import_success"));
|
||||
}
|
||||
|
||||
async pasteMarkdownIntoTextEvent() {
|
||||
await this.importMarkdownInlineEvent(); // BC with keyboard shortcuts command
|
||||
}
|
||||
|
||||
async importMarkdownInlineEvent() {
|
||||
if (appContext.tabManager.getActiveContextNoteType() !== "text") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const { clipboard } = utils.dynamicRequire("electron");
|
||||
const text = clipboard.readText();
|
||||
|
||||
this.convertMarkdownToHtml(text);
|
||||
} else {
|
||||
utils.openDialog(this.$widget);
|
||||
}
|
||||
}
|
||||
|
||||
async sendForm() {
|
||||
const text = String(this.$importTextarea.val());
|
||||
|
||||
this.modal.hide();
|
||||
|
||||
await this.convertMarkdownToHtml(text);
|
||||
|
||||
this.$importTextarea.val("");
|
||||
}
|
||||
}
|
||||
120
apps/client/src/widgets/dialogs/move_to.ts
Normal file
120
apps/client/src/widgets/dialogs/move_to.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import branchService from "../../services/branches.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="move-to-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title me-auto">${t("move_to.dialog_title")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("move_to.close")}"></button>
|
||||
</div>
|
||||
<form class="move-to-form">
|
||||
<div class="modal-body">
|
||||
<h5>${t("move_to.notes_to_move")}</h5>
|
||||
|
||||
<ul class="move-to-note-list" style="max-height: 200px; overflow: auto;"></ul>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="width: 100%">
|
||||
${t("move_to.target_parent_note")}
|
||||
<div class="input-group">
|
||||
<input class="move-to-note-autocomplete form-control" placeholder="${t("move_to.search_placeholder")}">
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">${t("move_to.move_button")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class MoveToDialog extends BasicWidget {
|
||||
|
||||
private movedBranchIds: string[] | null;
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
private $noteAutoComplete!: JQuery<HTMLElement>;
|
||||
private $noteList!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.movedBranchIds = null;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$form = this.$widget.find(".move-to-form");
|
||||
this.$noteAutoComplete = this.$widget.find(".move-to-note-autocomplete");
|
||||
this.$noteList = this.$widget.find(".move-to-note-list");
|
||||
|
||||
this.$form.on("submit", () => {
|
||||
const notePath = this.$noteAutoComplete.getSelectedNotePath();
|
||||
|
||||
if (notePath) {
|
||||
this.$widget.modal("hide");
|
||||
|
||||
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
|
||||
if (parentNoteId) {
|
||||
froca.getBranchId(parentNoteId, noteId).then((branchId) => {
|
||||
if (branchId) {
|
||||
this.moveNotesTo(branchId);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
logError(t("move_to.error_no_path"));
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
async moveBranchIdsToEvent({ branchIds }: EventData<"moveBranchIdsTo">) {
|
||||
this.movedBranchIds = branchIds;
|
||||
|
||||
utils.openDialog(this.$widget);
|
||||
|
||||
this.$noteAutoComplete.val("").trigger("focus");
|
||||
|
||||
this.$noteList.empty();
|
||||
|
||||
for (const branchId of this.movedBranchIds) {
|
||||
const branch = froca.getBranch(branchId);
|
||||
if (!branch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const note = await froca.getNote(branch.noteId);
|
||||
if (!note) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.$noteList.append($("<li>").text(note.title));
|
||||
}
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete(this.$noteAutoComplete);
|
||||
noteAutocompleteService.showRecentNotes(this.$noteAutoComplete);
|
||||
}
|
||||
|
||||
async moveNotesTo(parentBranchId: string) {
|
||||
if (this.movedBranchIds) {
|
||||
await branchService.moveToParentNote(this.movedBranchIds, parentBranchId);
|
||||
}
|
||||
|
||||
const parentBranch = froca.getBranch(parentBranchId);
|
||||
const parentNote = await parentBranch?.getNote();
|
||||
|
||||
toastService.showMessage(`${t("move_to.move_success_message")} ${parentNote?.title}`);
|
||||
}
|
||||
}
|
||||
166
apps/client/src/widgets/dialogs/note_type_chooser.ts
Normal file
166
apps/client/src/widgets/dialogs/note_type_chooser.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import type { CommandNames } from "../../components/app_context.js";
|
||||
import type { MenuCommandItem } from "../../menus/context_menu.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import noteTypesService from "../../services/note_types.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { Dropdown, Modal } from "bootstrap";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="note-type-chooser-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||
<style>
|
||||
.note-type-chooser-dialog {
|
||||
/* note type chooser needs to be higher than other dialogs from which it is triggered, e.g. "add link"*/
|
||||
z-index: 1100 !important;
|
||||
}
|
||||
|
||||
.note-type-chooser-dialog .note-type-dropdown {
|
||||
position: relative;
|
||||
font-size: large;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
<div class="modal-dialog" style="max-width: 500px;" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${t("note_type_chooser.modal_title")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("note_type_chooser.close")}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${t("note_type_chooser.modal_body")}
|
||||
|
||||
<div class="dropdown" style="display: flex;">
|
||||
<button class="note-type-dropdown-trigger" type="button" style="display: none;"
|
||||
data-bs-toggle="dropdown" data-bs-display="static">
|
||||
</button>
|
||||
|
||||
<div class="note-type-dropdown dropdown-menu"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export interface ChooseNoteTypeResponse {
|
||||
success: boolean;
|
||||
noteType?: string;
|
||||
templateNoteId?: string;
|
||||
}
|
||||
|
||||
type Callback = (data: ChooseNoteTypeResponse) => void;
|
||||
|
||||
export default class NoteTypeChooserDialog extends BasicWidget {
|
||||
private resolve: Callback | null;
|
||||
private dropdown!: Dropdown;
|
||||
private modal!: Modal;
|
||||
private $noteTypeDropdown!: JQuery<HTMLElement>;
|
||||
private $originalFocused: JQuery<HTMLElement> | null;
|
||||
private $originalDialog: JQuery<HTMLElement> | null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.resolve = null;
|
||||
this.$originalFocused = null; // element focused before the dialog was opened, so we can return to it afterward
|
||||
this.$originalDialog = null;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||
|
||||
this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown");
|
||||
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find(".note-type-dropdown-trigger")[0]);
|
||||
|
||||
this.$widget.on("hidden.bs.modal", () => {
|
||||
if (this.resolve) {
|
||||
this.resolve({ success: false });
|
||||
}
|
||||
|
||||
if (this.$originalFocused) {
|
||||
this.$originalFocused.trigger("focus");
|
||||
this.$originalFocused = null;
|
||||
}
|
||||
|
||||
glob.activeDialog = this.$originalDialog;
|
||||
});
|
||||
|
||||
this.$noteTypeDropdown.on("click", ".dropdown-item", (e) => this.doResolve(e));
|
||||
|
||||
this.$noteTypeDropdown.on("focus", ".dropdown-item", (e) => {
|
||||
this.$noteTypeDropdown.find(".dropdown-item").each((i, el) => {
|
||||
$(el).toggleClass("active", el === e.target);
|
||||
});
|
||||
});
|
||||
|
||||
this.$noteTypeDropdown.on("keydown", ".dropdown-item", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
this.doResolve(e);
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
this.$noteTypeDropdown.parent().on("hide.bs.dropdown", (e) => {
|
||||
// prevent closing dropdown by clicking outside
|
||||
// TODO: Check if this actually works.
|
||||
//@ts-ignore
|
||||
if (e.clickEvent) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async chooseNoteTypeEvent({ callback }: { callback: Callback }) {
|
||||
this.$originalFocused = $(":focus");
|
||||
|
||||
const noteTypes = await noteTypesService.getNoteTypeItems();
|
||||
|
||||
this.$noteTypeDropdown.empty();
|
||||
|
||||
for (const noteType of noteTypes) {
|
||||
if (noteType.title === "----") {
|
||||
this.$noteTypeDropdown.append($('<h6 class="dropdown-header">').append(t("note_type_chooser.templates")));
|
||||
} else {
|
||||
const commandItem = noteType as MenuCommandItem<CommandNames>;
|
||||
this.$noteTypeDropdown.append(
|
||||
$('<a class="dropdown-item" tabindex="0">')
|
||||
.attr("data-note-type", commandItem.type || "")
|
||||
.attr("data-template-note-id", commandItem.templateNoteId || "")
|
||||
.append($("<span>").addClass(commandItem.uiIcon || ""))
|
||||
.append(` ${noteType.title}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.dropdown.show();
|
||||
|
||||
this.$originalDialog = glob.activeDialog;
|
||||
glob.activeDialog = this.$widget;
|
||||
this.modal.show();
|
||||
|
||||
this.$noteTypeDropdown.find(".dropdown-item:first").focus();
|
||||
|
||||
this.resolve = callback;
|
||||
}
|
||||
|
||||
doResolve(e: JQuery.KeyDownEvent | JQuery.ClickEvent) {
|
||||
const $item = $(e.target).closest(".dropdown-item");
|
||||
const noteType = $item.attr("data-note-type");
|
||||
const templateNoteId = $item.attr("data-template-note-id");
|
||||
|
||||
if (this.resolve) {
|
||||
this.resolve({
|
||||
success: true,
|
||||
noteType,
|
||||
templateNoteId
|
||||
});
|
||||
}
|
||||
this.resolve = null;
|
||||
|
||||
this.modal.hide();
|
||||
}
|
||||
}
|
||||
42
apps/client/src/widgets/dialogs/password_not_set.ts
Normal file
42
apps/client/src/widgets/dialogs/password_not_set.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="password-not-set-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-md" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${t("password_not_set.title")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("password_not_set.close")}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${t("password_not_set.body1")}
|
||||
|
||||
${t("password_not_set.body2")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class PasswordNoteSetDialog extends BasicWidget {
|
||||
|
||||
private modal!: Modal;
|
||||
private $openPasswordOptionsButton!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||
this.$openPasswordOptionsButton = this.$widget.find(".open-password-options-button");
|
||||
this.$openPasswordOptionsButton.on("click", () => {
|
||||
this.modal.hide();
|
||||
this.triggerCommand("showOptions", { section: "_optionsPassword" });
|
||||
});
|
||||
}
|
||||
|
||||
showPasswordNotSetEvent() {
|
||||
utils.openDialog(this.$widget);
|
||||
}
|
||||
}
|
||||
115
apps/client/src/widgets/dialogs/prompt.ts
Normal file
115
apps/client/src/widgets/dialogs/prompt.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="prompt-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<form class="prompt-dialog-form">
|
||||
<div class="modal-header">
|
||||
<h5 class="prompt-title modal-title">${t("prompt.title")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("prompt.close")}"></button>
|
||||
</div>
|
||||
<div class="modal-body"></div>
|
||||
<div class="modal-footer">
|
||||
<button class="prompt-dialog-ok-button btn btn-primary btn-sm">${t("prompt.ok")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
interface ShownCallbackData {
|
||||
$dialog: JQuery<HTMLElement>;
|
||||
$question: JQuery<HTMLElement> | null;
|
||||
$answer: JQuery<HTMLElement> | null;
|
||||
$form: JQuery<HTMLElement>;
|
||||
}
|
||||
|
||||
export interface PromptDialogOptions {
|
||||
title?: string;
|
||||
message?: string;
|
||||
defaultValue?: string;
|
||||
shown?: PromptShownDialogCallback;
|
||||
callback?: (value: string | null) => void;
|
||||
}
|
||||
|
||||
export type PromptShownDialogCallback = ((callback: ShownCallbackData) => void) | null;
|
||||
|
||||
export default class PromptDialog extends BasicWidget {
|
||||
private resolve?: ((value: string | null) => void) | undefined | null;
|
||||
private shownCb?: PromptShownDialogCallback | null;
|
||||
|
||||
private modal!: Modal;
|
||||
private $dialogBody!: JQuery<HTMLElement>;
|
||||
private $question!: JQuery<HTMLElement> | null;
|
||||
private $answer!: JQuery<HTMLElement> | null;
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.resolve = null;
|
||||
this.shownCb = null;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||
this.$dialogBody = this.$widget.find(".modal-body");
|
||||
this.$form = this.$widget.find(".prompt-dialog-form");
|
||||
this.$question = null;
|
||||
this.$answer = null;
|
||||
|
||||
this.$widget.on("shown.bs.modal", () => {
|
||||
if (this.shownCb) {
|
||||
this.shownCb({
|
||||
$dialog: this.$widget,
|
||||
$question: this.$question,
|
||||
$answer: this.$answer,
|
||||
$form: this.$form
|
||||
});
|
||||
}
|
||||
|
||||
this.$answer?.trigger("focus").select();
|
||||
});
|
||||
|
||||
this.$widget.on("hidden.bs.modal", () => {
|
||||
if (this.resolve) {
|
||||
this.resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
this.$form.on("submit", (e) => {
|
||||
e.preventDefault();
|
||||
if (this.resolve) {
|
||||
this.resolve(this.$answer?.val() as string);
|
||||
}
|
||||
|
||||
this.modal.hide();
|
||||
});
|
||||
}
|
||||
|
||||
showPromptDialogEvent({ title, message, defaultValue, shown, callback }: PromptDialogOptions) {
|
||||
this.shownCb = shown;
|
||||
this.resolve = callback;
|
||||
|
||||
this.$widget.find(".prompt-title").text(title || t("prompt.defaultTitle"));
|
||||
|
||||
this.$question = $("<label>")
|
||||
.prop("for", "prompt-dialog-answer")
|
||||
.text(message || "");
|
||||
|
||||
this.$answer = $("<input>")
|
||||
.prop("type", "text")
|
||||
.prop("id", "prompt-dialog-answer")
|
||||
.addClass("form-control")
|
||||
.val(defaultValue || "");
|
||||
|
||||
this.$dialogBody.empty().append($("<div>").addClass("form-group").append(this.$question).append(this.$answer));
|
||||
|
||||
utils.openDialog(this.$widget, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import protectedSessionService from "../../services/protected_session.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="protected-session-password-dialog modal mx-auto" data-backdrop="false" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-md" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title flex-grow-1">${t("protected_session_password.modal_title")}</h5>
|
||||
<button class="help-button" type="button" data-help-page="protected-notes.html" title="${t("protected_session_password.help_title")}">?</button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("protected_session_password.close_label")}"></button>
|
||||
</div>
|
||||
<form class="protected-session-password-form">
|
||||
<div class="modal-body">
|
||||
<label for="protected-session-password" class="col-form-label">${t("protected_session_password.form_label")}</label>
|
||||
<input id="protected-session-password" class="form-control protected-session-password" type="password" autocomplete="current-password">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary">${t("protected_session_password.start_button")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class ProtectedSessionPasswordDialog extends BasicWidget {
|
||||
|
||||
private modal!: bootstrap.Modal;
|
||||
private $passwordForm!: JQuery<HTMLElement>;
|
||||
private $passwordInput!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||
|
||||
this.$passwordForm = this.$widget.find(".protected-session-password-form");
|
||||
this.$passwordInput = this.$widget.find(".protected-session-password");
|
||||
this.$passwordForm.on("submit", () => {
|
||||
const password = String(this.$passwordInput.val());
|
||||
this.$passwordInput.val("");
|
||||
|
||||
protectedSessionService.setupProtectedSession(password);
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
showProtectedSessionPasswordDialogEvent() {
|
||||
utils.openDialog(this.$widget);
|
||||
|
||||
this.$passwordInput.trigger("focus");
|
||||
}
|
||||
|
||||
closeProtectedSessionPasswordDialogEvent() {
|
||||
this.modal.hide();
|
||||
}
|
||||
}
|
||||
178
apps/client/src/widgets/dialogs/recent_changes.ts
Normal file
178
apps/client/src/widgets/dialogs/recent_changes.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { formatDateTime } from "../../utils/formatters.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import appContext, { type EventData } from "../../components/app_context.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import dialogService from "../../services/dialog.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import hoistedNoteService from "../../services/hoisted_note.js";
|
||||
import linkService from "../../services/link.js";
|
||||
import server from "../../services/server.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import ws from "../../services/ws.js";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="recent-changes-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title flex-grow-1">${t("recent_changes.title")}</h5>
|
||||
<button class="erase-deleted-notes-now-button btn btn-sm" style="padding: 0 10px">${t("recent_changes.erase_notes_button")}</button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("recent_changes.close")}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="recent-changes-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// TODO: Deduplicate with server.
|
||||
interface RecentChangesRow {
|
||||
noteId: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export default class RecentChangesDialog extends BasicWidget {
|
||||
|
||||
private ancestorNoteId?: string;
|
||||
|
||||
private modal!: bootstrap.Modal;
|
||||
private $content!: JQuery<HTMLElement>;
|
||||
private $eraseDeletedNotesNow!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||
|
||||
this.$content = this.$widget.find(".recent-changes-content");
|
||||
this.$eraseDeletedNotesNow = this.$widget.find(".erase-deleted-notes-now-button");
|
||||
this.$eraseDeletedNotesNow.on("click", () => {
|
||||
server.post("notes/erase-deleted-notes-now").then(() => {
|
||||
this.refresh();
|
||||
|
||||
toastService.showMessage(t("recent_changes.deleted_notes_message"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async showRecentChangesEvent({ ancestorNoteId }: EventData<"showRecentChanges">) {
|
||||
this.ancestorNoteId = ancestorNoteId;
|
||||
|
||||
await this.refresh();
|
||||
|
||||
utils.openDialog(this.$widget);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
if (!this.ancestorNoteId) {
|
||||
this.ancestorNoteId = hoistedNoteService.getHoistedNoteId();
|
||||
}
|
||||
|
||||
const recentChangesRows = await server.get<RecentChangesRow[]>(`recent-changes/${this.ancestorNoteId}`);
|
||||
|
||||
// preload all notes into cache
|
||||
await froca.getNotes(
|
||||
recentChangesRows.map((r) => r.noteId),
|
||||
true
|
||||
);
|
||||
|
||||
this.$content.empty();
|
||||
|
||||
if (recentChangesRows.length === 0) {
|
||||
this.$content.append(t("recent_changes.no_changes_message"));
|
||||
}
|
||||
|
||||
const groupedByDate = this.groupByDate(recentChangesRows);
|
||||
|
||||
for (const [dateDay, dayChanges] of groupedByDate) {
|
||||
const $changesList = $("<ul>");
|
||||
|
||||
const formattedDate = formatDateTime(dateDay, "full", "none");
|
||||
const dayEl = $("<div>").append($("<b>").text(formattedDate)).append($changesList);
|
||||
|
||||
for (const change of dayChanges) {
|
||||
const formattedTime = formatDateTime(change.date, "none", "short");
|
||||
|
||||
let $noteLink;
|
||||
|
||||
if (change.current_isDeleted) {
|
||||
$noteLink = $("<span>");
|
||||
|
||||
$noteLink.append($("<span>").addClass("note-title").text(change.current_title));
|
||||
|
||||
if (change.canBeUndeleted) {
|
||||
const $undeleteLink = $(`<a href="javascript:">`)
|
||||
.text(t("recent_changes.undelete_link"))
|
||||
.on("click", async () => {
|
||||
const text = t("recent_changes.confirm_undelete");
|
||||
|
||||
if (await dialogService.confirm(text)) {
|
||||
await server.put(`notes/${change.noteId}/undelete`);
|
||||
|
||||
this.modal.hide();
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
if (activeContext) {
|
||||
activeContext.setNote(change.noteId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$noteLink.append(" (").append($undeleteLink).append(")");
|
||||
}
|
||||
} else {
|
||||
const note = await froca.getNote(change.noteId);
|
||||
const notePath = note?.getBestNotePathString();
|
||||
|
||||
if (notePath) {
|
||||
$noteLink = await linkService.createLink(notePath, {
|
||||
title: change.title,
|
||||
showNotePath: true
|
||||
});
|
||||
} else {
|
||||
$noteLink = $("<span>").text(note?.title ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
$changesList.append(
|
||||
$("<li>")
|
||||
.on("click", (e) => {
|
||||
// Skip clicks on the link or deleted notes
|
||||
if (e.target?.nodeName !== "A" && !change.current_isDeleted) {
|
||||
// Open the current note
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
if (activeContext) {
|
||||
activeContext.setNote(change.noteId);
|
||||
}
|
||||
}
|
||||
})
|
||||
.toggleClass("deleted-note", !!change.current_isDeleted)
|
||||
.append($("<span>").text(formattedTime).attr("title", change.date))
|
||||
.append($noteLink.addClass("note-title"))
|
||||
);
|
||||
}
|
||||
|
||||
this.$content.append(dayEl);
|
||||
}
|
||||
}
|
||||
|
||||
groupByDate(rows: RecentChangesRow[]) {
|
||||
const groupedByDate = new Map();
|
||||
|
||||
for (const row of rows) {
|
||||
const dateDay = row.date.substr(0, 10);
|
||||
|
||||
if (!groupedByDate.has(dateDay)) {
|
||||
groupedByDate.set(dateDay, []);
|
||||
}
|
||||
|
||||
groupedByDate.get(dateDay).push(row);
|
||||
}
|
||||
|
||||
return groupedByDate;
|
||||
}
|
||||
}
|
||||
382
apps/client/src/widgets/dialogs/revisions.ts
Normal file
382
apps/client/src/widgets/dialogs/revisions.ts
Normal file
@@ -0,0 +1,382 @@
|
||||
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 libraryLoader from "../../services/library_loader.js";
|
||||
import openService from "../../services/open.js";
|
||||
import protectedSessionHolder from "../../services/protected_session_holder.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import dialogService 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";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
utils.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) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.KATEX);
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
111
apps/client/src/widgets/dialogs/sort_child_notes.ts
Normal file
111
apps/client/src/widgets/dialogs/sort_child_notes.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import server from "../../services/server.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
|
||||
const TPL = /*html*/`<div class="sort-child-notes-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" style="max-width: 500px" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${t("sort_child_notes.sort_children_by")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("sort_child_notes.close")}"></button>
|
||||
</div>
|
||||
<form class="sort-child-notes-form">
|
||||
<div class="modal-body">
|
||||
<h5>${t("sort_child_notes.sorting_criteria")}</h5>
|
||||
<div class="form-check">
|
||||
<label for="sort-by-title" class="form-check-label tn-radio">
|
||||
<input id="sort-by-title" class="form-check-input" type="radio" name="sort-by" value="title" checked>
|
||||
${t("sort_child_notes.title")}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label for="sort-by-dateCreated" class="form-check-label tn-radio">
|
||||
<input id="sort-by-dateCreated" class="form-check-input" type="radio" name="sort-by" value="dateCreated">
|
||||
${t("sort_child_notes.date_created")}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label for="sort-by-dateModified" class="form-check-label tn-radio">
|
||||
<input id="sort-by-dateModified" class="form-check-input" type="radio" name="sort-by" value="dateModified">
|
||||
${t("sort_child_notes.date_modified")}
|
||||
</label>
|
||||
</div>
|
||||
<br/>
|
||||
<h5>${t("sort_child_notes.sorting_direction")}</h5>
|
||||
<div class="form-check">
|
||||
<label for="sort-direction-asc" class="form-check-label tn-radio">
|
||||
<input id="sort-direction-asc" class="form-check-input" type="radio" name="sort-direction" value="asc" checked>
|
||||
${t("sort_child_notes.ascending")}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label for="sort-direction-desc" class="form-check-label tn-radio">
|
||||
<input id="sort-direction-desc" class="form-check-input" type="radio" name="sort-direction" value="desc">
|
||||
${t("sort_child_notes.descending")}
|
||||
</label>
|
||||
</div>
|
||||
<br />
|
||||
<h5>${t("sort_child_notes.folders")}</h5>
|
||||
<div class="form-check">
|
||||
<label for="sort-folders-first" class="form-check-label tn-checkbox">
|
||||
<input id="sort-folders-first" class="form-check-input" type="checkbox" name="sort-folders-first" value="1">
|
||||
${t("sort_child_notes.sort_folders_at_top")}
|
||||
</label>
|
||||
</div>
|
||||
<br />
|
||||
<h5>${t("sort_child_notes.natural_sort")}</h5>
|
||||
<div class="form-check">
|
||||
<label for="sort-natural" class="form-check-label tn-checkbox">
|
||||
<input id="sort-natural" class="form-check-input" type="checkbox" name="sort-natural" value="1">
|
||||
${t("sort_child_notes.sort_with_respect_to_different_character_sorting")}
|
||||
</label>
|
||||
</div>
|
||||
<br />
|
||||
<div class="form-check">
|
||||
<label>
|
||||
${t("sort_child_notes.natural_sort_language")}
|
||||
<input class="form-control" name="sort-locale">
|
||||
${t("sort_child_notes.the_language_code_for_natural_sort")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">${t("sort_child_notes.sort")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class SortChildNotesDialog extends BasicWidget {
|
||||
|
||||
private parentNoteId?: string;
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$form = this.$widget.find(".sort-child-notes-form");
|
||||
|
||||
this.$form.on("submit", async () => {
|
||||
const sortBy = this.$form.find("input[name='sort-by']:checked").val();
|
||||
const sortDirection = this.$form.find("input[name='sort-direction']:checked").val();
|
||||
const foldersFirst = this.$form.find("input[name='sort-folders-first']").is(":checked");
|
||||
const sortNatural = this.$form.find("input[name='sort-natural']").is(":checked");
|
||||
const sortLocale = this.$form.find("input[name='sort-locale']").val();
|
||||
|
||||
await server.put(`notes/${this.parentNoteId}/sort-children`, { sortBy, sortDirection, foldersFirst, sortNatural, sortLocale });
|
||||
|
||||
utils.closeActiveDialog();
|
||||
});
|
||||
}
|
||||
|
||||
async sortChildNotesEvent({ node }: EventData<"sortChildNotes">) {
|
||||
this.parentNoteId = node.data.noteId;
|
||||
|
||||
utils.openDialog(this.$widget);
|
||||
|
||||
this.$form.find("input:first").focus();
|
||||
}
|
||||
}
|
||||
119
apps/client/src/widgets/dialogs/upload_attachments.ts
Normal file
119
apps/client/src/widgets/dialogs/upload_attachments.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import utils, { escapeQuotes } from "../../services/utils.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import importService from "../../services/import.js";
|
||||
import options from "../../services/options.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { Modal, Tooltip } from "bootstrap";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="upload-attachments-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${t("upload_attachments.upload_attachments_to_note")}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("upload_attachments.close")}"></button>
|
||||
</div>
|
||||
<form class="upload-attachment-form">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="upload-attachment-file-upload-input"><strong>${t("upload_attachments.choose_files")}</strong></label>
|
||||
<label class="tn-file-input tn-input-field">
|
||||
<input type="file" class="upload-attachment-file-upload-input form-control-file" multiple />
|
||||
</label>
|
||||
<p>${t("upload_attachments.files_will_be_uploaded")} <strong class="upload-attachment-note-title"></strong>.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<strong>${t("upload_attachments.options")}:</strong>
|
||||
<div class="checkbox">
|
||||
<label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("upload_attachments.tooltip"))}">
|
||||
<input class="shrink-images-checkbox form-check-input" value="1" type="checkbox" checked> <span>${t("upload_attachments.shrink_images")}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="upload-attachment-button btn btn-primary">${t("upload_attachments.upload")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class UploadAttachmentsDialog extends BasicWidget {
|
||||
|
||||
private parentNoteId: string | null;
|
||||
private modal!: bootstrap.Modal;
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
private $noteTitle!: JQuery<HTMLElement>;
|
||||
private $fileUploadInput!: JQuery<HTMLInputElement>;
|
||||
private $uploadButton!: JQuery<HTMLElement>;
|
||||
private $shrinkImagesCheckbox!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.parentNoteId = null;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||
|
||||
this.$form = this.$widget.find(".upload-attachment-form");
|
||||
this.$noteTitle = this.$widget.find(".upload-attachment-note-title");
|
||||
this.$fileUploadInput = this.$widget.find(".upload-attachment-file-upload-input");
|
||||
this.$uploadButton = this.$widget.find(".upload-attachment-button");
|
||||
this.$shrinkImagesCheckbox = this.$widget.find(".shrink-images-checkbox");
|
||||
|
||||
this.$form.on("submit", () => {
|
||||
// disabling so that import is not triggered again.
|
||||
this.$uploadButton.attr("disabled", "disabled");
|
||||
if (this.parentNoteId) {
|
||||
this.uploadAttachments(this.parentNoteId);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$fileUploadInput.on("change", () => {
|
||||
if (this.$fileUploadInput.val()) {
|
||||
this.$uploadButton.removeAttr("disabled");
|
||||
} else {
|
||||
this.$uploadButton.attr("disabled", "disabled");
|
||||
}
|
||||
});
|
||||
|
||||
Tooltip.getOrCreateInstance(this.$widget.find('[data-bs-toggle="tooltip"]')[0], {
|
||||
html: true
|
||||
});
|
||||
}
|
||||
|
||||
async showUploadAttachmentsDialogEvent({ noteId }: EventData<"showUploadAttachmentsDialog">) {
|
||||
this.parentNoteId = noteId;
|
||||
|
||||
this.$fileUploadInput.val("").trigger("change"); // to trigger upload button disabling listener below
|
||||
this.$shrinkImagesCheckbox.prop("checked", options.is("compressImages"));
|
||||
|
||||
this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId));
|
||||
|
||||
utils.openDialog(this.$widget);
|
||||
}
|
||||
|
||||
async uploadAttachments(parentNoteId: string) {
|
||||
const files = Array.from(this.$fileUploadInput[0].files ?? []); // shallow copy since we're resetting the upload button below
|
||||
|
||||
function boolToString($el: JQuery<HTMLElement>): "true" | "false" {
|
||||
return ($el.is(":checked") ? "true" : "false");
|
||||
}
|
||||
|
||||
const options = {
|
||||
shrinkImages: boolToString(this.$shrinkImagesCheckbox)
|
||||
};
|
||||
|
||||
this.modal.hide();
|
||||
|
||||
await importService.uploadFiles("attachments", parentNoteId, files, options);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user