mirror of
https://github.com/zadam/trilium.git
synced 2025-11-10 07:15:51 +01:00
moved to section widgets
This commit is contained in:
62
src/public/app/widgets/section_widgets/basic_properties.js
Normal file
62
src/public/app/widgets/section_widgets/basic_properties.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import NoteTypeWidget from "../note_type.js";
|
||||
import ProtectedNoteSwitchWidget from "../protected_note_switch.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="basic-properties-widget">
|
||||
<style>
|
||||
.basic-properties-widget {
|
||||
padding: 12px 12px 6px 12px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.note-type-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.basic-properties-widget > * {
|
||||
margin-right: 30px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="note-type-container" style="display: flex">
|
||||
<span>Note type:</span>
|
||||
</div>
|
||||
|
||||
<div class="protected-note-switch-container"></div>
|
||||
</div>`;
|
||||
|
||||
export default class BasicPropertiesWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.noteTypeWidget = new NoteTypeWidget();
|
||||
this.protectedNoteSwitchWidget = new ProtectedNoteSwitchWidget();
|
||||
|
||||
this.child(this.noteTypeWidget, this.protectedNoteSwitchWidget);
|
||||
}
|
||||
|
||||
static getType() { return "basic-properties"; }
|
||||
|
||||
isEnabled() {
|
||||
return this.note;
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return {
|
||||
show: this.isEnabled(),
|
||||
title: 'Basic Properties',
|
||||
icon: 'bx bx-slider'
|
||||
};
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.overflowing();
|
||||
|
||||
this.$widget.find(".note-type-container").append(this.noteTypeWidget.render());
|
||||
this.$widget.find(".protected-note-switch-container").append(this.protectedNoteSwitchWidget.render());
|
||||
}
|
||||
}
|
||||
106
src/public/app/widgets/section_widgets/book_properties.js
Normal file
106
src/public/app/widgets/section_widgets/book_properties.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import server from "../../services/server.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="book-properties-widget">
|
||||
<style>
|
||||
.book-properties-widget {
|
||||
padding: 12px 12px 6px 12px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.book-properties-widget > * {
|
||||
margin-right: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div style="display: flex; align-items: baseline">
|
||||
<span style="white-space: nowrap">View type: </span>
|
||||
|
||||
<select class="view-type-select form-control form-control-sm">
|
||||
<option value="grid">Grid</option>
|
||||
<option value="list">List</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="button"
|
||||
class="collapse-all-button btn btn-sm"
|
||||
title="Collapse all notes">
|
||||
|
||||
<span class="bx bx-layer-minus"></span>
|
||||
|
||||
Collapse
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="expand-children-button btn btn-sm"
|
||||
title="Expand all children">
|
||||
<span class="bx bx-move-vertical"></span>
|
||||
|
||||
Expand
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
static getType() { return "book-properties"; }
|
||||
|
||||
isEnabled() {
|
||||
return this.note && this.note.type === 'book';
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return {
|
||||
show: this.isEnabled(),
|
||||
activate: true,
|
||||
title: 'Book Properties',
|
||||
icon: 'bx bx-book'
|
||||
};
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.overflowing();
|
||||
|
||||
this.$viewTypeSelect = this.$widget.find('.view-type-select');
|
||||
this.$viewTypeSelect.on('change', () => this.toggleViewType(this.$viewTypeSelect.val()));
|
||||
|
||||
this.$expandChildrenButton = this.$widget.find('.expand-children-button');
|
||||
this.$expandChildrenButton.on('click', async () => {
|
||||
if (!this.note.hasLabel('expanded')) {
|
||||
await attributeService.addLabel(this.noteId, 'expanded');
|
||||
}
|
||||
});
|
||||
|
||||
this.$collapseAllButton = this.$widget.find('.collapse-all-button');
|
||||
this.$collapseAllButton.on('click', async () => {
|
||||
// owned is important - we shouldn't remove inherited expanded labels
|
||||
for (const expandedAttr of this.note.getOwnedLabels('expanded')) {
|
||||
await attributeService.removeAttributeById(this.noteId, expandedAttr.attributeId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const viewType = this.note.getLabelValue('viewType') || 'grid';
|
||||
|
||||
this.$viewTypeSelect.val(viewType);
|
||||
|
||||
this.$expandChildrenButton.toggle(viewType === 'list');
|
||||
this.$collapseAllButton.toggle(viewType === 'list');
|
||||
}
|
||||
|
||||
async toggleViewType(type) {
|
||||
if (type !== 'list' && type !== 'grid') {
|
||||
throw new Error(`Invalid view type ${type}`);
|
||||
}
|
||||
|
||||
await attributeService.setLabel(this.noteId, 'viewType', type);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.getAttributes().find(attr => attr.noteId === this.noteId && attr.name === 'viewType')) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
137
src/public/app/widgets/section_widgets/file_properties.js
Normal file
137
src/public/app/widgets/section_widgets/file_properties.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import server from "../../services/server.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import openService from "../../services/open.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="file-properties-widget">
|
||||
<style>
|
||||
.file-table {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.file-table th, .file-table td {
|
||||
padding: 5px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.file-buttons {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
</style>
|
||||
|
||||
<table class="file-table">
|
||||
<tr>
|
||||
<th>Note ID:</th>
|
||||
<td class="file-note-id"></td>
|
||||
<th>Original file name:</th>
|
||||
<td class="file-filename"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>File type:</th>
|
||||
<td class="file-filetype"></td>
|
||||
<th>File size:</th>
|
||||
<td class="file-filesize"></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="file-buttons">
|
||||
<button class="file-download btn btn-sm btn-primary" type="button">Download</button>
|
||||
|
||||
<button class="file-open btn btn-sm btn-primary" type="button">Open</button>
|
||||
|
||||
<button class="file-upload-new-revision btn btn-sm btn-primary">Upload new revision</button>
|
||||
|
||||
<input type="file" class="file-upload-new-revision-input" style="display: none">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>`;
|
||||
|
||||
export default class FilePropertiesWidget extends NoteContextAwareWidget {
|
||||
static getType() { return "file"; }
|
||||
|
||||
isEnabled() {
|
||||
return this.note && this.note.type === 'file';
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return {
|
||||
show: this.isEnabled(),
|
||||
activate: true,
|
||||
title: 'File',
|
||||
icon: 'bx bx-file'
|
||||
};
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
this.$fileNoteId = this.$widget.find(".file-note-id");
|
||||
this.$fileName = this.$widget.find(".file-filename");
|
||||
this.$fileType = this.$widget.find(".file-filetype");
|
||||
this.$fileSize = this.$widget.find(".file-filesize");
|
||||
this.$downloadButton = this.$widget.find(".file-download");
|
||||
this.$openButton = this.$widget.find(".file-open");
|
||||
this.$uploadNewRevisionButton = this.$widget.find(".file-upload-new-revision");
|
||||
this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input");
|
||||
|
||||
this.$downloadButton.on('click', () => openService.downloadFileNote(this.noteId));
|
||||
this.$openButton.on('click', () => openService.openNoteExternally(this.noteId));
|
||||
|
||||
this.$uploadNewRevisionButton.on("click", () => {
|
||||
this.$uploadNewRevisionInput.trigger("click");
|
||||
});
|
||||
|
||||
this.$uploadNewRevisionInput.on('change', async () => {
|
||||
const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
|
||||
this.$uploadNewRevisionInput.val('');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('upload', fileToUpload);
|
||||
|
||||
const result = await $.ajax({
|
||||
url: baseApiUrl + 'notes/' + this.noteId + '/file',
|
||||
headers: await server.getHeaders(),
|
||||
data: formData,
|
||||
type: 'PUT',
|
||||
timeout: 60 * 60 * 1000,
|
||||
contentType: false, // NEEDED, DON'T REMOVE THIS
|
||||
processData: false, // NEEDED, DON'T REMOVE THIS
|
||||
});
|
||||
|
||||
if (result.uploaded) {
|
||||
toastService.showMessage("New file revision has been uploaded.");
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
else {
|
||||
toastService.showError("Upload of a new file revision failed.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const attributes = note.getAttributes();
|
||||
const attributeMap = utils.toObject(attributes, l => [l.name, l.value]);
|
||||
|
||||
this.$widget.show();
|
||||
|
||||
this.$fileNoteId.text(note.noteId);
|
||||
this.$fileName.text(attributeMap.originalFileName || "?");
|
||||
this.$fileType.text(note.mime);
|
||||
|
||||
const noteComplement = await this.noteContext.getNoteComplement();
|
||||
|
||||
this.$fileSize.text(noteComplement.contentLength + " bytes");
|
||||
|
||||
// open doesn't work for protected notes since it works through browser which isn't in protected session
|
||||
this.$openButton.toggle(!note.isProtected);
|
||||
}
|
||||
}
|
||||
121
src/public/app/widgets/section_widgets/image_properties.js
Normal file
121
src/public/app/widgets/section_widgets/image_properties.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import server from "../../services/server.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import openService from "../../services/open.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="image-properties">
|
||||
<div style="display: flex; justify-content: space-evenly; margin: 10px;">
|
||||
<span>
|
||||
<strong>Original file name:</strong>
|
||||
<span class="image-filename"></span>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<strong>File type:</strong>
|
||||
<span class="image-filetype"></span>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<strong>File size:</strong>
|
||||
<span class="image-filesize"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="no-print" style="display: flex; justify-content: space-evenly; margin: 10px;">
|
||||
<button class="image-download btn btn-sm btn-primary" type="button">Download</button>
|
||||
|
||||
<button class="image-open btn btn-sm btn-primary" type="button">Open</button>
|
||||
|
||||
<button class="image-copy-to-clipboard btn btn-sm btn-primary" type="button">Copy to clipboard</button>
|
||||
|
||||
<button class="image-upload-new-revision btn btn-sm btn-primary" type="button">Upload new revision</button>
|
||||
</div>
|
||||
|
||||
<input type="file" class="image-upload-new-revision-input" style="display: none">
|
||||
</div>`;
|
||||
|
||||
export default class ImagePropertiesWidget extends NoteContextAwareWidget {
|
||||
static getType() { return "image"; }
|
||||
|
||||
isEnabled() {
|
||||
return this.note && this.note.type === 'image';
|
||||
}
|
||||
|
||||
getTitle(note) {
|
||||
return {
|
||||
show: this.isEnabled(),
|
||||
activate: true,
|
||||
title: 'Image',
|
||||
icon: 'bx bx-image'
|
||||
};
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
this.$copyToClipboardButton = this.$widget.find(".image-copy-to-clipboard");
|
||||
this.$uploadNewRevisionButton = this.$widget.find(".image-upload-new-revision");
|
||||
this.$uploadNewRevisionInput = this.$widget.find(".image-upload-new-revision-input");
|
||||
this.$fileName = this.$widget.find(".image-filename");
|
||||
this.$fileType = this.$widget.find(".image-filetype");
|
||||
this.$fileSize = this.$widget.find(".image-filesize");
|
||||
|
||||
this.$openButton = this.$widget.find(".image-open");
|
||||
this.$openButton.on('click', () => openService.openNoteExternally(this.noteId));
|
||||
|
||||
this.$imageDownloadButton = this.$widget.find(".image-download");
|
||||
this.$imageDownloadButton.on('click', () => openService.downloadFileNote(this.noteId));
|
||||
|
||||
this.$copyToClipboardButton.on('click', () => this.triggerEvent(`copyImageToClipboard`, {ntxId: this.noteContext.ntxId}));
|
||||
|
||||
this.$uploadNewRevisionButton.on("click", () => {
|
||||
this.$uploadNewRevisionInput.trigger("click");
|
||||
});
|
||||
|
||||
this.$uploadNewRevisionInput.on('change', async () => {
|
||||
const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
|
||||
this.$uploadNewRevisionInput.val('');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('upload', fileToUpload);
|
||||
|
||||
const result = await $.ajax({
|
||||
url: baseApiUrl + 'images/' + this.noteId,
|
||||
headers: await server.getHeaders(),
|
||||
data: formData,
|
||||
type: 'PUT',
|
||||
timeout: 60 * 60 * 1000,
|
||||
contentType: false, // NEEDED, DON'T REMOVE THIS
|
||||
processData: false, // NEEDED, DON'T REMOVE THIS
|
||||
});
|
||||
|
||||
if (result.uploaded) {
|
||||
toastService.showMessage("New image revision has been uploaded.");
|
||||
|
||||
await utils.clearBrowserCache();
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
else {
|
||||
toastService.showError("Upload of a new image revision failed: " + result.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const attributes = note.getAttributes();
|
||||
const attributeMap = utils.toObject(attributes, l => [l.name, l.value]);
|
||||
|
||||
this.$widget.show();
|
||||
|
||||
const noteComplement = await this.noteContext.getNoteComplement();
|
||||
|
||||
this.$fileName.text(attributeMap.originalFileName || "?");
|
||||
this.$fileSize.text(noteComplement.contentLength + " bytes");
|
||||
this.$fileType.text(note.mime);
|
||||
|
||||
const imageHash = utils.randomString(10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import AttributeDetailWidget from "../attribute_widgets/attribute_detail.js";
|
||||
import attributeRenderer from "../../services/attribute_renderer.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="inherited-attributes-widget">
|
||||
<style>
|
||||
.inherited-attributes-widget {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inherited-attributes-container {
|
||||
color: var(--muted-text-color);
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
padding: 12px 12px 11px 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="inherited-attributes-container"></div>
|
||||
</div>`;
|
||||
|
||||
export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.attributeDetailWidget = new AttributeDetailWidget().setParent(this);
|
||||
this.child(this.attributeDetailWidget);
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return {
|
||||
show: true,
|
||||
title: "Inherited attributes",
|
||||
icon: "bx bx-list-plus"
|
||||
};
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.overflowing();
|
||||
|
||||
this.$container = this.$widget.find('.inherited-attributes-container');
|
||||
this.$widget.append(this.attributeDetailWidget.render());
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
this.$container.empty();
|
||||
|
||||
const inheritedAttributes = this.getInheritedAttributes(note);
|
||||
|
||||
if (inheritedAttributes.length === 0) {
|
||||
this.$container.append("No inherited attributes.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const attribute of inheritedAttributes) {
|
||||
const $attr = (await attributeRenderer.renderAttribute(attribute, false))
|
||||
.on('click', e => this.attributeDetailWidget.showAttributeDetail({
|
||||
attribute: {
|
||||
noteId: attribute.noteId,
|
||||
type: attribute.type,
|
||||
name: attribute.name,
|
||||
value: attribute.value,
|
||||
isInheritable: attribute.isInheritable
|
||||
},
|
||||
isOwned: false,
|
||||
x: e.pageX,
|
||||
y: e.pageY
|
||||
}));
|
||||
|
||||
this.$container
|
||||
.append($attr)
|
||||
.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
getInheritedAttributes(note) {
|
||||
return note.getAttributes().filter(attr => attr.noteId !== this.noteId);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.getAttributes(this.componentId).find(attr => attr.isAffecting(this.note))) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/public/app/widgets/section_widgets/link_map.js
Normal file
105
src/public/app/widgets/section_widgets/link_map.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import froca from "../../services/froca.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="link-map-widget">
|
||||
<style>
|
||||
.link-map-widget {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.open-full-dialog-button {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button class="bx bx-expand icon-action open-full-dialog-button" title="Show Link Map dialog"></button>
|
||||
|
||||
<div class="link-map-container" style="height: 300px;"></div>
|
||||
</div>`;
|
||||
|
||||
let linkMapContainerIdCtr = 1;
|
||||
|
||||
export default class LinkMapWidget extends NoteContextAwareWidget {
|
||||
static getType() { return "link-map"; }
|
||||
|
||||
isEnabled() {
|
||||
return this.note;
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return {
|
||||
show: this.isEnabled(),
|
||||
title: 'Link Map',
|
||||
icon: 'bx bx-network-chart'
|
||||
};
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$widget.find('.open-full-dialog-button').on('click', () => this.triggerCommand('showLinkMap'));
|
||||
this.overflowing();
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
let shown = false;
|
||||
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
if (!shown && entries[0].isIntersecting) {
|
||||
shown = true;
|
||||
this.displayLinkMap(note);
|
||||
}
|
||||
}, {
|
||||
rootMargin: '0px',
|
||||
threshold: 0.1
|
||||
});
|
||||
|
||||
observer.observe(this.$widget[0]);
|
||||
}
|
||||
|
||||
async displayLinkMap(note) {
|
||||
this.$widget.find(".link-map-container").empty();
|
||||
|
||||
const $linkMapContainer = this.$widget.find('.link-map-container');
|
||||
$linkMapContainer.attr("id", "link-map-container-" + linkMapContainerIdCtr++);
|
||||
|
||||
const LinkMapServiceClass = (await import('../../services/link_map.js')).default;
|
||||
|
||||
this.linkMapService = new LinkMapServiceClass(note, $linkMapContainer, {
|
||||
maxDepth: 3,
|
||||
zoom: 0.6,
|
||||
stopCheckerCallback: () => this.noteId !== note.noteId // stop when current note is not what was originally requested
|
||||
});
|
||||
|
||||
await this.linkMapService.render();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.linkMapService) {
|
||||
this.linkMapService.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.getAttributes().find(attr => attr.type === 'relation' && (attr.noteId === this.noteId || attr.value === this.noteId))) {
|
||||
this.noteSwitched();
|
||||
}
|
||||
|
||||
const changedNoteIds = loadResults.getNoteIds();
|
||||
|
||||
if (changedNoteIds.length > 0) {
|
||||
const $linkMapContainer = this.$widget.find('.link-map-container');
|
||||
|
||||
for (const noteId of changedNoteIds) {
|
||||
const note = froca.notes[noteId];
|
||||
|
||||
if (note) {
|
||||
$linkMapContainer.find(`a[data-note-path="${noteId}"]`).text(note.title);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
156
src/public/app/widgets/section_widgets/note_info_widget.js
Normal file
156
src/public/app/widgets/section_widgets/note_info_widget.js
Normal file
@@ -0,0 +1,156 @@
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import server from "../../services/server.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-info-widget">
|
||||
<style>
|
||||
.note-info-widget {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.note-info-widget-table {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.note-info-widget-table td, .note-info-widget-table th {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.note-info-mime {
|
||||
max-width: 13em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<table class="note-info-widget-table">
|
||||
<tr>
|
||||
<th>Note ID:</th>
|
||||
<td class="note-info-note-id"></td>
|
||||
<th>Created:</th>
|
||||
<td class="note-info-date-created"></td>
|
||||
<th>Modified:</th>
|
||||
<td class="note-info-date-modified"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Type:</th>
|
||||
<td>
|
||||
<span class="note-info-type"></span>
|
||||
|
||||
<span class="note-info-mime"></span>
|
||||
</td>
|
||||
|
||||
<th title="Note size provides rough estimate of storage requirements for this note. It takes into account note's content and content of its note revisions.">Note size:</th>
|
||||
|
||||
<td colspan="3">
|
||||
<button class="btn btn-sm calculate-button" style="padding: 0px 10px 0px 10px;">
|
||||
<span class="bx bx-calculator"></span> calculate
|
||||
</button>
|
||||
|
||||
<span class="note-sizes-wrapper">
|
||||
<span class="note-size"></span>
|
||||
|
||||
<span class="subtree-size"></span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
export default class NoteInfoWidget extends NoteContextAwareWidget {
|
||||
static getType() { return "note-info"; }
|
||||
|
||||
isEnabled() {
|
||||
return this.note;
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return {
|
||||
show: this.isEnabled(),
|
||||
title: 'Note Info',
|
||||
icon: 'bx bx-info-circle'
|
||||
};
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.overflowing();
|
||||
|
||||
this.$noteId = this.$widget.find(".note-info-note-id");
|
||||
this.$dateCreated = this.$widget.find(".note-info-date-created");
|
||||
this.$dateModified = this.$widget.find(".note-info-date-modified");
|
||||
this.$type = this.$widget.find(".note-info-type");
|
||||
this.$mime = this.$widget.find(".note-info-mime");
|
||||
|
||||
this.$noteSizesWrapper = this.$widget.find('.note-sizes-wrapper');
|
||||
this.$noteSize = this.$widget.find(".note-size");
|
||||
this.$subTreeSize = this.$widget.find(".subtree-size");
|
||||
|
||||
this.$calculateButton = this.$widget.find(".calculate-button");
|
||||
this.$calculateButton.on('click', async () => {
|
||||
this.$noteSizesWrapper.show();
|
||||
this.$calculateButton.hide();
|
||||
|
||||
this.$noteSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
|
||||
this.$subTreeSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
|
||||
|
||||
const noteSizeResp = await server.get(`stats/note-size/${this.noteId}`);
|
||||
this.$noteSize.text(this.formatSize(noteSizeResp.noteSize));
|
||||
|
||||
const subTreeResp = await server.get(`stats/subtree-size/${this.noteId}`);
|
||||
|
||||
if (subTreeResp.subTreeNoteCount > 1) {
|
||||
this.$subTreeSize.text("(subtree size: " + this.formatSize(subTreeResp.subTreeSize) + ` in ${subTreeResp.subTreeNoteCount} notes)`);
|
||||
}
|
||||
else {
|
||||
this.$subTreeSize.text("");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const noteComplement = await this.noteContext.getNoteComplement();
|
||||
|
||||
this.$noteId.text(note.noteId);
|
||||
this.$dateCreated
|
||||
.text(noteComplement.dateCreated.substr(0, 16))
|
||||
.attr("title", noteComplement.dateCreated);
|
||||
|
||||
this.$dateModified
|
||||
.text(noteComplement.combinedDateModified.substr(0, 16))
|
||||
.attr("title", noteComplement.combinedDateModified);
|
||||
|
||||
this.$type.text(note.type);
|
||||
|
||||
if (note.mime) {
|
||||
this.$mime.text('(' + note.mime + ')');
|
||||
}
|
||||
else {
|
||||
this.$mime.empty();
|
||||
}
|
||||
|
||||
this.$calculateButton.show();
|
||||
this.$noteSizesWrapper.hide();
|
||||
}
|
||||
|
||||
formatSize(size) {
|
||||
size = Math.max(Math.round(size / 1024), 1);
|
||||
|
||||
if (size < 1024) {
|
||||
return `${size} KiB`;
|
||||
}
|
||||
else {
|
||||
return `${Math.round(size / 102.4) / 10} MiB`;
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.isNoteReloaded(this.noteId) || loadResults.isNoteContentReloaded(this.noteId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
124
src/public/app/widgets/section_widgets/note_paths.js
Normal file
124
src/public/app/widgets/section_widgets/note_paths.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import linkService from "../../services/link.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-paths-widget">
|
||||
<style>
|
||||
.note-paths-widget {
|
||||
padding: 12px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.note-path-list {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.note-path-list .path-current {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.note-path-list .path-archived {
|
||||
color: var(--muted-text-color) !important;
|
||||
}
|
||||
|
||||
.note-path-list .path-search {
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>This note is placed into the following paths:</div>
|
||||
|
||||
<ul class="note-path-list"></ul>
|
||||
|
||||
<button class="btn btn-sm" data-trigger-command="cloneNoteIdsTo">Clone note to new location...</button>
|
||||
</div>`;
|
||||
|
||||
export default class NotePathsWidget extends NoteContextAwareWidget {
|
||||
static getType() { return "note-paths"; }
|
||||
|
||||
isEnabled() {
|
||||
return this.note;
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return {
|
||||
show: true,
|
||||
title: 'Note Paths',
|
||||
icon: 'bx bx-collection'
|
||||
};
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.overflowing();
|
||||
|
||||
this.$notePathList = this.$widget.find(".note-path-list");
|
||||
this.$widget.on('show.bs.dropdown', () => this.renderDropdown());
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
this.$notePathList.empty();
|
||||
|
||||
if (this.noteId === 'root') {
|
||||
await this.addPath('root');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const notePathRecord of this.note.getSortedNotePaths(this.hoistedNoteId)) {
|
||||
await this.addPath(notePathRecord);
|
||||
}
|
||||
}
|
||||
|
||||
async addPath(notePathRecord) {
|
||||
const notePath = notePathRecord.notePath.join('/');
|
||||
|
||||
const title = await treeService.getNotePathTitle(notePath);
|
||||
|
||||
const $noteLink = await linkService.createNoteLink(notePath, {title});
|
||||
|
||||
$noteLink
|
||||
.find('a')
|
||||
.addClass("no-tooltip-preview");
|
||||
|
||||
const icons = [];
|
||||
|
||||
if (this.notePath === notePath) {
|
||||
$noteLink.addClass("path-current");
|
||||
}
|
||||
|
||||
if (notePathRecord.isInHoistedSubTree) {
|
||||
$noteLink.addClass("path-in-hoisted-subtree");
|
||||
}
|
||||
else {
|
||||
icons.push(`<span class="bx bx-trending-up" title="This path is outside of hoisted note and you would have to unhoist."></span>`);
|
||||
}
|
||||
|
||||
if (notePathRecord.isArchived) {
|
||||
$noteLink.addClass("path-archived");
|
||||
|
||||
icons.push(`<span class="bx bx-archive" title="Archived"></span>`);
|
||||
}
|
||||
|
||||
if (notePathRecord.isSearch) {
|
||||
$noteLink.addClass("path-search");
|
||||
|
||||
icons.push(`<span class="bx bx-search" title="Search"></span>`);
|
||||
}
|
||||
|
||||
if (icons.length > 0) {
|
||||
$noteLink.append(` ${icons.join(' ')}`);
|
||||
}
|
||||
|
||||
this.$notePathList.append($("<li>").append($noteLink));
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.getBranches().find(branch => branch.noteId === this.noteId)
|
||||
|| loadResults.isNoteReloaded(this.noteId)) {
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/public/app/widgets/section_widgets/note_properties.js
Normal file
48
src/public/app/widgets/section_widgets/note_properties.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-properties-widget">
|
||||
<style>
|
||||
.note-properties-widget {
|
||||
padding: 12px;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap">
|
||||
This note was originally taken from: <a class="page-url external"></a>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class NotePropertiesWidget extends NoteContextAwareWidget {
|
||||
static getType() { return "note-properties"; }
|
||||
|
||||
isEnabled() {
|
||||
return this.note && !!this.note.getLabelValue('pageUrl');
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return {
|
||||
show: this.isEnabled(),
|
||||
activate: true,
|
||||
title: 'Info',
|
||||
icon: 'bx bx-info-square'
|
||||
};
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
|
||||
this.$pageUrl = this.$widget.find('.page-url');
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const pageUrl = note.getLabelValue('pageUrl');
|
||||
|
||||
this.$pageUrl
|
||||
.attr('href', pageUrl)
|
||||
.attr('title', pageUrl)
|
||||
.text(pageUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import AttributeDetailWidget from "../attribute_widgets/attribute_detail.js";
|
||||
import AttributeEditorWidget from "../attribute_widgets/attribute_editor.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="attribute-list">
|
||||
<style>
|
||||
.attribute-list {
|
||||
margin-left: 7px;
|
||||
margin-right: 7px;
|
||||
margin-top: 3px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.attribute-list-editor p {
|
||||
margin: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="attr-editor-placeholder"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class OwnedAttributeListWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.attributeDetailWidget = new AttributeDetailWidget().setParent(this);
|
||||
this.attributeEditorWidget = new AttributeEditorWidget(this.attributeDetailWidget).setParent(this);
|
||||
|
||||
this.child(this.attributeEditorWidget, this.attributeDetailWidget);
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return {
|
||||
show: true,
|
||||
title: "Owned attributes",
|
||||
icon: "bx bx-list-check"
|
||||
};
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.overflowing();
|
||||
|
||||
this.$widget.find('.attr-editor-placeholder').replaceWith(this.attributeEditorWidget.render());
|
||||
this.$widget.append(this.attributeDetailWidget.render());
|
||||
|
||||
this.$title = $('<div>');
|
||||
}
|
||||
|
||||
async saveAttributesCommand() {
|
||||
await this.attributeEditorWidget.save();
|
||||
}
|
||||
|
||||
async reloadAttributesCommand() {
|
||||
await this.attributeEditorWidget.refresh();
|
||||
}
|
||||
|
||||
async updateAttributeListCommand({attributes}) {
|
||||
await this.attributeEditorWidget.updateAttributeList(attributes);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.getAttributes(this.componentId).find(attr => attr.isAffecting(this.note))) {
|
||||
this.refreshWithNote(this.note, true);
|
||||
|
||||
this.getTitle(this.note);
|
||||
}
|
||||
}
|
||||
}
|
||||
296
src/public/app/widgets/section_widgets/promoted_attributes.js
Normal file
296
src/public/app/widgets/section_widgets/promoted_attributes.js
Normal file
@@ -0,0 +1,296 @@
|
||||
import server from "../../services/server.js";
|
||||
import ws from "../../services/ws.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<div>
|
||||
<style>
|
||||
.promoted-attributes-container {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
justify-content: space-evenly;
|
||||
overflow: auto;
|
||||
max-height: 400px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.promoted-attribute-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.promoted-attribute-cell div.input-group {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="promoted-attributes-container"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.overflowing();
|
||||
this.$container = this.$widget.find(".promoted-attributes-container");
|
||||
}
|
||||
|
||||
getTitle(note) {
|
||||
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
|
||||
|
||||
if (promotedDefAttrs.length === 0) {
|
||||
return { show: false };
|
||||
}
|
||||
|
||||
return {
|
||||
show: true,
|
||||
activate: true,
|
||||
title: "Promoted attributes",
|
||||
icon: "bx bx-table"
|
||||
};
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
this.$container.empty();
|
||||
|
||||
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
|
||||
const ownedAttributes = note.getOwnedAttributes();
|
||||
|
||||
if (promotedDefAttrs.length === 0) {
|
||||
this.toggleInt(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const $cells = [];
|
||||
|
||||
for (const definitionAttr of promotedDefAttrs) {
|
||||
const valueType = definitionAttr.name.startsWith('label:') ? 'label' : 'relation';
|
||||
const valueName = definitionAttr.name.substr(valueType.length + 1);
|
||||
|
||||
let valueAttrs = ownedAttributes.filter(el => el.name === valueName && el.type === valueType);
|
||||
|
||||
if (valueAttrs.length === 0) {
|
||||
valueAttrs.push({
|
||||
attributeId: "",
|
||||
type: valueType,
|
||||
name: valueName,
|
||||
value: ""
|
||||
});
|
||||
}
|
||||
|
||||
if (definitionAttr.getDefinition().multiplicity === 'single') {
|
||||
valueAttrs = valueAttrs.slice(0, 1);
|
||||
}
|
||||
|
||||
for (const valueAttr of valueAttrs) {
|
||||
const $cell = await this.createPromotedAttributeCell(definitionAttr, valueAttr, valueName);
|
||||
|
||||
$cells.push($cell);
|
||||
}
|
||||
}
|
||||
|
||||
// we replace the whole content in one step so there can't be any race conditions
|
||||
// (previously we saw promoted attributes doubling)
|
||||
this.$container.empty().append(...$cells);
|
||||
this.toggleInt(true);
|
||||
}
|
||||
|
||||
async createPromotedAttributeCell(definitionAttr, valueAttr, valueName) {
|
||||
const definition = definitionAttr.getDefinition();
|
||||
|
||||
const $input = $("<input>")
|
||||
.prop("tabindex", 200 + definitionAttr.position)
|
||||
.prop("attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
|
||||
.prop("attribute-type", valueAttr.type)
|
||||
.prop("attribute-name", valueAttr.name)
|
||||
.prop("value", valueAttr.value)
|
||||
.addClass("form-control")
|
||||
.addClass("promoted-attribute-input")
|
||||
.on('change', event => this.promotedAttributeChanged(event));
|
||||
|
||||
const $actionCell = $("<div>");
|
||||
const $multiplicityCell = $("<td>")
|
||||
.addClass("multiplicity")
|
||||
.attr("nowrap", true);
|
||||
|
||||
const $wrapper = $('<div class="promoted-attribute-cell">')
|
||||
.append($("<strong>").text(valueName))
|
||||
.append($("<div>").addClass("input-group").append($input))
|
||||
.append($actionCell)
|
||||
.append($multiplicityCell);
|
||||
|
||||
if (valueAttr.type === 'label') {
|
||||
if (definition.labelType === 'text') {
|
||||
$input.prop("type", "text");
|
||||
|
||||
// no need to await for this, can be done asynchronously
|
||||
server.get('attributes/values/' + encodeURIComponent(valueAttr.name)).then(attributeValues => {
|
||||
if (attributeValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
attributeValues = attributeValues.map(attribute => ({ value: attribute }));
|
||||
|
||||
$input.autocomplete({
|
||||
appendTo: document.querySelector('body'),
|
||||
hint: false,
|
||||
autoselect: false,
|
||||
openOnFocus: true,
|
||||
minLength: 0,
|
||||
tabAutocomplete: false
|
||||
}, [{
|
||||
displayKey: 'value',
|
||||
source: function (term, cb) {
|
||||
term = term.toLowerCase();
|
||||
|
||||
const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term));
|
||||
|
||||
cb(filtered);
|
||||
}
|
||||
}]);
|
||||
|
||||
$input.on('autocomplete:selected', e => this.promotedAttributeChanged(e))
|
||||
});
|
||||
}
|
||||
else if (definition.labelType === 'number') {
|
||||
$input.prop("type", "number");
|
||||
|
||||
let step = 1;
|
||||
|
||||
for (let i = 0; i < (definition.numberPrecision || 0) && i < 10; i++) {
|
||||
step /= 10;
|
||||
}
|
||||
|
||||
$input.prop("step", step);
|
||||
$input
|
||||
.css("text-align", "right")
|
||||
.css("width", "120");
|
||||
}
|
||||
else if (definition.labelType === 'boolean') {
|
||||
$input.prop("type", "checkbox");
|
||||
// hack, without this the checkbox is invisible
|
||||
// we should be using a different bootstrap structure for checkboxes
|
||||
$input.css('width', '80px');
|
||||
|
||||
if (valueAttr.value === "true") {
|
||||
$input.prop("checked", "checked");
|
||||
}
|
||||
}
|
||||
else if (definition.labelType === 'date') {
|
||||
$input.prop("type", "date");
|
||||
}
|
||||
else if (definition.labelType === 'url') {
|
||||
$input.prop("placeholder", "http://website...");
|
||||
|
||||
const $openButton = $("<span>")
|
||||
.addClass("input-group-text open-external-link-button bx bx-window-open")
|
||||
.prop("title", "Open external link")
|
||||
.on('click', () => window.open($input.val(), '_blank'));
|
||||
|
||||
$input.after($("<div>")
|
||||
.addClass("input-group-append")
|
||||
.append($openButton));
|
||||
}
|
||||
else {
|
||||
ws.logError("Unknown labelType=" + definitionAttr.labelType);
|
||||
}
|
||||
}
|
||||
else if (valueAttr.type === 'relation') {
|
||||
if (valueAttr.value) {
|
||||
$input.val(await treeService.getNoteTitle(valueAttr.value));
|
||||
}
|
||||
|
||||
// no need to wait for this
|
||||
noteAutocompleteService.initNoteAutocomplete($input, {allowCreatingNotes: true});
|
||||
|
||||
$input.on('autocomplete:noteselected', (event, suggestion, dataset) => {
|
||||
this.promotedAttributeChanged(event);
|
||||
});
|
||||
|
||||
$input.setSelectedNotePath(valueAttr.value);
|
||||
}
|
||||
else {
|
||||
ws.logError("Unknown attribute type=" + valueAttr.type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (definition.multiplicity === "multi") {
|
||||
const addButton = $("<span>")
|
||||
.addClass("bx bx-plus pointer")
|
||||
.prop("title", "Add new attribute")
|
||||
.on('click', async () => {
|
||||
const $new = await this.createPromotedAttributeCell(definitionAttr, {
|
||||
attributeId: "",
|
||||
type: valueAttr.type,
|
||||
name: valueName,
|
||||
value: ""
|
||||
}, valueName);
|
||||
|
||||
$wrapper.after($new);
|
||||
|
||||
$new.find('input').trigger('focus');
|
||||
});
|
||||
|
||||
const removeButton = $("<span>")
|
||||
.addClass("bx bx-trash pointer")
|
||||
.prop("title", "Remove this attribute")
|
||||
.on('click', async () => {
|
||||
if (valueAttr.attributeId) {
|
||||
await server.remove("notes/" + this.noteId + "/attributes/" + valueAttr.attributeId, this.componentId);
|
||||
}
|
||||
|
||||
$wrapper.remove();
|
||||
});
|
||||
|
||||
$multiplicityCell
|
||||
.append(" ")
|
||||
.append(addButton)
|
||||
.append(" ")
|
||||
.append(removeButton);
|
||||
}
|
||||
|
||||
return $wrapper;
|
||||
}
|
||||
|
||||
async promotedAttributeChanged(event) {
|
||||
const $attr = $(event.target);
|
||||
|
||||
let value;
|
||||
|
||||
if ($attr.prop("type") === "checkbox") {
|
||||
value = $attr.is(':checked') ? "true" : "false";
|
||||
}
|
||||
else if ($attr.prop("attribute-type") === "relation") {
|
||||
const selectedPath = $attr.getSelectedNotePath();
|
||||
|
||||
value = selectedPath ? treeService.getNoteIdFromNotePath(selectedPath) : "";
|
||||
}
|
||||
else {
|
||||
value = $attr.val();
|
||||
}
|
||||
|
||||
const result = await server.put(`notes/${this.noteId}/attribute`, {
|
||||
attributeId: $attr.prop("attribute-id"),
|
||||
type: $attr.prop("attribute-type"),
|
||||
name: $attr.prop("attribute-name"),
|
||||
value: value
|
||||
}, this.componentId);
|
||||
|
||||
$attr.prop("attribute-id", result.attributeId);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.getAttributes(this.componentId).find(attr => attr.isAffecting(this.note))) {
|
||||
this.refresh();
|
||||
|
||||
this.getTitle(this.note);
|
||||
this.triggerCommand('refreshSectionContainer');
|
||||
}
|
||||
}
|
||||
}
|
||||
344
src/public/app/widgets/section_widgets/search_definition.js
Normal file
344
src/public/app/widgets/section_widgets/search_definition.js
Normal file
@@ -0,0 +1,344 @@
|
||||
import server from "../../services/server.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import ws from "../../services/ws.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
|
||||
import DeleteNoteSearchAction from "../search_actions/delete_note.js";
|
||||
import DeleteLabelSearchAction from "../search_actions/delete_label.js";
|
||||
import DeleteRelationSearchAction from "../search_actions/delete_relation.js";
|
||||
import RenameLabelSearchAction from "../search_actions/rename_label.js";
|
||||
import SetLabelValueSearchAction from "../search_actions/set_label_value.js";
|
||||
import SetRelationTargetSearchAction from "../search_actions/set_relation_target.js";
|
||||
import RenameRelationSearchAction from "../search_actions/rename_relation.js";
|
||||
import ExecuteScriptSearchAction from "../search_actions/execute_script.js"
|
||||
import SearchString from "../search_options/search_string.js";
|
||||
import FastSearch from "../search_options/fast_search.js";
|
||||
import Ancestor from "../search_options/ancestor.js";
|
||||
import IncludeArchivedNotes from "../search_options/include_archived_notes.js";
|
||||
import OrderBy from "../search_options/order_by.js";
|
||||
import SearchScript from "../search_options/search_script.js";
|
||||
import Limit from "../search_options/limit.js";
|
||||
import DeleteNoteRevisionsSearchAction from "../search_actions/delete_note_revisions.js";
|
||||
import Debug from "../search_options/debug.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="search-definition-widget">
|
||||
<style>
|
||||
.search-setting-table {
|
||||
margin-top: 0;
|
||||
margin-bottom: 7px;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 10px;
|
||||
}
|
||||
|
||||
.search-setting-table div {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-setting-table .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;
|
||||
}
|
||||
|
||||
.search-setting-table .title-column {
|
||||
/* minimal width so that table remains static sized and most space remains for middle column with settings */
|
||||
width: 50px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-setting-table .button-column .dropdown-menu {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.attribute-list hr {
|
||||
height: 1px;
|
||||
border-color: var(--main-border-color);
|
||||
position: relative;
|
||||
top: 4px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.search-definition-widget input:invalid {
|
||||
border: 3px solid red;
|
||||
}
|
||||
|
||||
.add-search-option button {
|
||||
margin-top: 5px; /* to give some spacing when buttons overflow on the next line */
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="search-settings">
|
||||
<table class="search-setting-table">
|
||||
<tr>
|
||||
<td class="title-column">Add search option:</td>
|
||||
<td colspan="2" class="add-search-option">
|
||||
<button type="button" class="btn btn-sm" data-search-option-add="searchString">
|
||||
<span class="bx bx-text"></span>
|
||||
search string
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm" data-search-option-add="searchScript">
|
||||
<span class="bx bx-code"></span>
|
||||
search script
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm" data-search-option-add="ancestor">
|
||||
<span class="bx bx-filter-alt"></span>
|
||||
ancestor
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm" data-search-option-add="fastSearch"
|
||||
title="Fast search option disables full text search of note contents which might speed up searching in large databases.">
|
||||
<span class="bx bx-run"></span>
|
||||
fast search
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm" data-search-option-add="includeArchivedNotes"
|
||||
title="Archived notes are by default excluded from search results, with this option they will be included.">
|
||||
<span class="bx bx-archive"></span>
|
||||
include archived
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm" data-search-option-add="orderBy">
|
||||
<span class="bx bx-arrow-from-top"></span>
|
||||
order by
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm" data-search-option-add="limit" title="Limit number of results">
|
||||
<span class="bx bx-stop"></span>
|
||||
limit
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm" data-search-option-add="debug" title="Debug will print extra debugging information into the console to aid in debugging complex queries">
|
||||
<span class="bx bx-bug"></span>
|
||||
debug
|
||||
</button>
|
||||
|
||||
<div class="dropdown" style="display: inline-block;">
|
||||
<button class="btn btn-sm dropdown-toggle action-add-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="bx bxs-zap"></span>
|
||||
action
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteNote">
|
||||
Delete note</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteNoteRevisions">
|
||||
Delete note revisions</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteLabel">
|
||||
Delete label</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteRelation">
|
||||
Delete relation</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="renameLabel">
|
||||
Rename label</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="renameRelation">
|
||||
Rename relation</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="setLabelValue">
|
||||
Set label value</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="setRelationTarget">
|
||||
Set relation target</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="executeScript">
|
||||
Execute script</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tbody class="search-options"></tbody>
|
||||
<tbody class="action-options"></tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<div style="display: flex; justify-content: space-evenly">
|
||||
<button type="button" class="btn btn-sm search-button">
|
||||
<span class="bx bx-search"></span>
|
||||
Search
|
||||
|
||||
<kbd>enter</kbd>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm search-and-execute-button">
|
||||
<span class="bx bxs-zap"></span>
|
||||
Search & Execute actions
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const OPTION_CLASSES = [
|
||||
SearchString,
|
||||
SearchScript,
|
||||
Ancestor,
|
||||
FastSearch,
|
||||
IncludeArchivedNotes,
|
||||
OrderBy,
|
||||
Limit,
|
||||
Debug
|
||||
];
|
||||
|
||||
const ACTION_CLASSES = {};
|
||||
|
||||
for (const clazz of [
|
||||
DeleteNoteSearchAction,
|
||||
DeleteNoteRevisionsSearchAction,
|
||||
DeleteLabelSearchAction,
|
||||
DeleteRelationSearchAction,
|
||||
RenameLabelSearchAction,
|
||||
RenameRelationSearchAction,
|
||||
SetLabelValueSearchAction,
|
||||
SetRelationTargetSearchAction,
|
||||
ExecuteScriptSearchAction
|
||||
]) {
|
||||
ACTION_CLASSES[clazz.actionName] = clazz;
|
||||
}
|
||||
|
||||
export default class SearchDefinitionWidget extends NoteContextAwareWidget {
|
||||
static getType() { return "search"; }
|
||||
|
||||
isEnabled() {
|
||||
return this.note && this.note.type === 'search';
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return {
|
||||
show: this.isEnabled(),
|
||||
activate: true,
|
||||
title: 'Search parameters',
|
||||
icon: 'bx bx-search'
|
||||
};
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$component = this.$widget.find('.search-definition-widget');
|
||||
|
||||
this.contentSized();
|
||||
this.overflowing();
|
||||
|
||||
this.$widget.on('click', '[data-search-option-add]', async event => {
|
||||
const searchOptionName = $(event.target).attr('data-search-option-add');
|
||||
const clazz = OPTION_CLASSES.find(SearchOptionClass => SearchOptionClass.optionName === searchOptionName);
|
||||
|
||||
if (clazz) {
|
||||
await clazz.create(this.noteId);
|
||||
}
|
||||
else {
|
||||
logError(`Unknown search option ${searchOptionName}`);
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
});
|
||||
|
||||
this.$widget.on('click', '[data-action-add]', async event => {
|
||||
const actionName = $(event.target).attr('data-action-add');
|
||||
|
||||
await server.post(`notes/${this.noteId}/attributes`, {
|
||||
type: 'label',
|
||||
name: 'action',
|
||||
value: JSON.stringify({
|
||||
name: actionName
|
||||
})
|
||||
});
|
||||
|
||||
this.$widget.find('.action-add-toggle').dropdown('toggle');
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
this.refresh();
|
||||
});
|
||||
|
||||
this.$searchOptions = this.$widget.find('.search-options');
|
||||
this.$actionOptions = this.$widget.find('.action-options');
|
||||
|
||||
this.$searchButton = this.$widget.find('.search-button');
|
||||
this.$searchButton.on('click', () => this.triggerCommand('refreshResults'));
|
||||
|
||||
this.$searchAndExecuteButton = this.$widget.find('.search-and-execute-button');
|
||||
this.$searchAndExecuteButton.on('click', () => this.searchAndExecute());
|
||||
}
|
||||
|
||||
async refreshResultsCommand() {
|
||||
try {
|
||||
await froca.loadSearchNote(this.noteId);
|
||||
}
|
||||
catch (e) {
|
||||
toastService.showError(e.message);
|
||||
}
|
||||
|
||||
this.triggerEvent('searchRefreshed', {ntxId: this.noteContext.ntxId});
|
||||
}
|
||||
|
||||
async refreshSearchDefinitionCommand() {
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
this.$component.show();
|
||||
|
||||
this.$searchOptions.empty();
|
||||
|
||||
for (const OptionClass of OPTION_CLASSES) {
|
||||
const {attributeType, optionName} = OptionClass;
|
||||
|
||||
const attr = this.note.getAttribute(attributeType, optionName);
|
||||
|
||||
this.$widget.find(`[data-search-option-add='${optionName}'`).toggle(!attr);
|
||||
|
||||
if (attr) {
|
||||
const searchOption = new OptionClass(attr, this.note).setParent(this);
|
||||
this.child(searchOption);
|
||||
|
||||
this.$searchOptions.append(searchOption.render());
|
||||
}
|
||||
}
|
||||
|
||||
this.$actionOptions.empty();
|
||||
|
||||
const actionLabels = this.note.getLabels('action');
|
||||
|
||||
for (const actionAttr of actionLabels) {
|
||||
let actionDef;
|
||||
|
||||
try {
|
||||
actionDef = JSON.parse(actionAttr.value);
|
||||
}
|
||||
catch (e) {
|
||||
logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ActionClass = ACTION_CLASSES[actionDef.name];
|
||||
|
||||
if (!ActionClass) {
|
||||
logError(`No action class for '${actionDef.name}' found.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const action = new ActionClass(actionAttr, actionDef).setParent(this);
|
||||
this.child(action);
|
||||
|
||||
this.$actionOptions.append(action.render());
|
||||
}
|
||||
|
||||
this.$searchAndExecuteButton.css('visibility', actionLabels.length > 0 ? 'visible' : 'hidden');
|
||||
}
|
||||
|
||||
getContent() {
|
||||
return '';
|
||||
}
|
||||
|
||||
async searchAndExecute() {
|
||||
await server.post(`search-and-execute-note/${this.noteId}`);
|
||||
|
||||
this.triggerCommand('refreshResults');
|
||||
|
||||
toastService.showMessage('Actions have been executed.', 3000);
|
||||
}
|
||||
}
|
||||
98
src/public/app/widgets/section_widgets/similar_notes.js
Normal file
98
src/public/app/widgets/section_widgets/similar_notes.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import linkService from "../../services/link.js";
|
||||
import server from "../../services/server.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="similar-notes-widget">
|
||||
<style>
|
||||
.similar-notes-wrapper {
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.similar-notes-wrapper a {
|
||||
display: inline-block;
|
||||
border: 1px dotted var(--main-border-color);
|
||||
border-radius: 20px;
|
||||
background-color: var(--accented-background-color);
|
||||
padding: 0 10px 0 10px;
|
||||
margin: 0 3px 0 3px;
|
||||
max-width: 10em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="similar-notes-wrapper"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class SimilarNotesWidget extends NoteContextAwareWidget {
|
||||
static getType() { return "similar-notes"; }
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled()
|
||||
&& this.note.type !== 'search'
|
||||
&& !this.note.hasLabel('similarNotesWidgetDisabled');
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return {
|
||||
show: this.isEnabled(),
|
||||
title: 'Similar Notes',
|
||||
icon: 'bx bx-bar-chart'
|
||||
};
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.overflowing();
|
||||
|
||||
this.$similarNotesWrapper = this.$widget.find(".similar-notes-wrapper");
|
||||
}
|
||||
|
||||
async refreshWithNote() {
|
||||
// remember which title was when we found the similar notes
|
||||
this.title = this.note.title;
|
||||
|
||||
const similarNotes = await server.get('similar-notes/' + this.noteId);
|
||||
|
||||
if (similarNotes.length === 0) {
|
||||
this.$similarNotesWrapper.empty().append("No similar notes found.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const noteIds = similarNotes.flatMap(note => note.notePath);
|
||||
|
||||
await froca.getNotes(noteIds, true); // preload all at once
|
||||
|
||||
const $list = $('<div>');
|
||||
|
||||
for (const similarNote of similarNotes) {
|
||||
const note = await froca.getNote(similarNote.noteId, true);
|
||||
|
||||
if (!note) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $item = (await linkService.createNoteLink(similarNote.notePath.join("/")))
|
||||
.css("font-size", 24 * (1 - 1 / (1 + similarNote.score)));
|
||||
|
||||
$list.append($item);
|
||||
}
|
||||
|
||||
this.$similarNotesWrapper.empty().append($list);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (this.note && this.title !== this.note.title) {
|
||||
this.rendered = false;
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user