mirror of
https://github.com/TriliumNext/Notes.git
synced 2026-02-07 06:08:31 -06:00
Merge branch 'develop' into feat_add-link-to-swagger-ui
This commit is contained in:
@@ -203,7 +203,7 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
|
||||
// open doesn't work for protected notes since it works through a browser which isn't in protected session
|
||||
$openButton.toggle(!entity.isProtected);
|
||||
|
||||
$content.append($('<div style="display: flex; justify-content: space-evenly; margin-top: 5px;">').append($downloadButton).append($openButton));
|
||||
$content.append($('<footer class="file-footer">').append($downloadButton).append($openButton));
|
||||
}
|
||||
|
||||
$renderedContent.append($content);
|
||||
@@ -239,7 +239,7 @@ async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery
|
||||
* @param {FNote} note
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
|
||||
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
|
||||
let childNoteIds = note.getChildNoteIds();
|
||||
|
||||
if (!childNoteIds.length) {
|
||||
|
||||
@@ -9,7 +9,7 @@ interface CreateNewTasksOpts {
|
||||
export async function createNewTask({ parentNoteId, title }: CreateNewTasksOpts) {
|
||||
await server.post(`tasks`, {
|
||||
parentNoteId,
|
||||
title
|
||||
title: title.trim()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "happy-dom"
|
||||
environment: "happy-dom",
|
||||
coverage: {
|
||||
reporter: [ "text", "html" ]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,6 +20,10 @@ const TPL = `
|
||||
.note-list-widget.full-height .note-list-widget-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-list-widget video {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="note-list-widget-content">
|
||||
|
||||
@@ -63,7 +63,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (note.type === "file" && note.mime === "application/pdf") {
|
||||
if (note.type === "file" && (note.mime === "application/pdf" || note.mime.startsWith("video/"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ const TPL = `
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.note-split.full-content-width .note-detail-file[data-preview-type="video"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-preview-content {
|
||||
background-color: var(--accented-background-color);
|
||||
padding: 15px;
|
||||
@@ -29,6 +33,11 @@ const TPL = `
|
||||
overflow: auto;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.note-detail-file > .video-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="file-preview-too-big alert alert-info hidden-ext">
|
||||
@@ -85,6 +94,8 @@ export default class FileTypeWidget extends TypeWidget {
|
||||
this.$videoPreview.hide();
|
||||
this.$audioPreview.hide();
|
||||
|
||||
let previewType: string;
|
||||
|
||||
if (blob?.content) {
|
||||
this.$previewContent.show().scrollTop(0);
|
||||
const trimmedContent = blob.content.substring(0, TEXT_MAX_NUM_CHARS);
|
||||
@@ -92,23 +103,30 @@ export default class FileTypeWidget extends TypeWidget {
|
||||
this.$previewTooBig.removeClass("hidden-ext");
|
||||
}
|
||||
this.$previewContent.text(trimmedContent);
|
||||
previewType = "text";
|
||||
} else if (note.mime === "application/pdf") {
|
||||
this.$pdfPreview.show().attr("src", openService.getUrlForDownload(`api/notes/${this.noteId}/open`));
|
||||
previewType = "pdf";
|
||||
} else if (note.mime.startsWith("video/")) {
|
||||
this.$videoPreview
|
||||
.show()
|
||||
.attr("src", openService.getUrlForDownload(`api/notes/${this.noteId}/open-partial`))
|
||||
.attr("type", this.note?.mime ?? "")
|
||||
.css("width", this.$widget.width() ?? 0);
|
||||
previewType = "video";
|
||||
} else if (note.mime.startsWith("audio/")) {
|
||||
this.$audioPreview
|
||||
.show()
|
||||
.attr("src", openService.getUrlForDownload(`api/notes/${this.noteId}/open-partial`))
|
||||
.attr("type", this.note?.mime ?? "")
|
||||
.css("width", this.$widget.width() ?? 0);
|
||||
previewType = "audio";
|
||||
} else {
|
||||
this.$previewNotAvailable.show();
|
||||
previewType = "not-available";
|
||||
}
|
||||
|
||||
this.$widget.attr("data-preview-type", previewType ?? "");
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as taskService from "../../services/tasks.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import dayjs from "dayjs";
|
||||
import calendarTime from "dayjs/plugin/calendar.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
dayjs.extend(calendarTime);
|
||||
|
||||
const TPL = `
|
||||
@@ -25,7 +26,7 @@ const TPL = `
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.note-detail-task-list header {
|
||||
.note-detail-task-list > header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
@@ -59,15 +60,23 @@ const TPL = `
|
||||
transition: background 250ms ease-in-out;
|
||||
}
|
||||
|
||||
.note-detail-task-list .task-container li > header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.note-detail-task-list .task-container li .check {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.note-detail-task-list .task-container li .title {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.note-detail-task-list .task-container li .due-date {
|
||||
float: right;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.note-detail-task-list .task-container li.overdue .due-date {
|
||||
@@ -81,6 +90,7 @@ function buildTasks(tasks: FTask[]) {
|
||||
let html = '';
|
||||
|
||||
const now = dayjs();
|
||||
const dateFormat = "DD-MM-YYYY";
|
||||
for (const task of tasks) {
|
||||
const classes = ["task"];
|
||||
|
||||
@@ -89,14 +99,25 @@ function buildTasks(tasks: FTask[]) {
|
||||
}
|
||||
|
||||
html += `<li class="${classes.join(" ")}" data-task-id="${task.taskId}">`;
|
||||
html += "<header>";
|
||||
html += '<span class="title">';
|
||||
html += `<input type="checkbox" class="check" ${task.isDone ? "checked" : ""} />`;
|
||||
html += task.title;
|
||||
html += `${task.title}</span>`;
|
||||
html += '</span>';
|
||||
if (task.dueDate) {
|
||||
html += `<span class="due-date">`;
|
||||
html += `<span class="bx bx-calendar"></span> `;
|
||||
html += dayjs(task.dueDate).calendar();
|
||||
html += dayjs(task.dueDate).calendar(null, {
|
||||
sameDay: `[${t("tasks.due.today")}]`,
|
||||
nextDay: `[${t("tasks.due.tomorrow")}]`,
|
||||
nextWeek: "dddd",
|
||||
lastDay: `[${t("tasks.due.yesterday")}]`,
|
||||
lastWeek: dateFormat,
|
||||
sameElse: dateFormat
|
||||
});
|
||||
html += "</span>";
|
||||
}
|
||||
html += "</header>";
|
||||
html += `<div class="edit-container"></div>`;
|
||||
html += `</li>`;
|
||||
}
|
||||
@@ -142,20 +163,19 @@ export default class TaskListWidget extends TypeWidget {
|
||||
});
|
||||
|
||||
this.$taskContainer.on("click", "li", (e) => {
|
||||
const $target = $(e.target);
|
||||
|
||||
// Don't collapse when clicking on an inside element such as the due date dropdown.
|
||||
if (e.currentTarget !== e.target) {
|
||||
if ((e.target as HTMLElement).tagName === "INPUT") {
|
||||
return;
|
||||
}
|
||||
|
||||
const $target = $(e.target);
|
||||
|
||||
// Clear existing edit containers.
|
||||
const $existingContainers = this.$taskContainer.find(".edit-container");
|
||||
|
||||
$existingContainers.html("");
|
||||
|
||||
// Add the new edit container.
|
||||
const $editContainer = $target.find(".edit-container");
|
||||
const $editContainer = $target.closest("li").find(".edit-container");
|
||||
const task = this.#getCorrespondingTask($target);
|
||||
if (task) {
|
||||
$editContainer.html(buildEditContainer(task));
|
||||
@@ -183,7 +203,11 @@ export default class TaskListWidget extends TypeWidget {
|
||||
}
|
||||
|
||||
#getCorrespondingTask($target: JQuery<HTMLElement>) {
|
||||
const taskId = $target.closest("li")[0].dataset.taskId;
|
||||
const $parentEl = $target.closest("li");
|
||||
if (!$parentEl.length) {
|
||||
return;
|
||||
}
|
||||
const taskId = $parentEl[0].dataset.taskId;
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1684,4 +1684,15 @@ body.zen .note-title-widget,
|
||||
body.zen .note-title-widget input {
|
||||
font-size: 1rem !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Content renderer */
|
||||
|
||||
footer.file-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
footer.file-footer button {
|
||||
margin: 5px;
|
||||
}
|
||||
@@ -1560,6 +1560,10 @@ div.bookmark-folder-widget .note-link .bx {
|
||||
border-bottom-color: var(--card-border-color);
|
||||
}
|
||||
|
||||
.note-list-wrapper .note-book-card footer.file-footer {
|
||||
border: 1px solid var(--card-border-color);
|
||||
}
|
||||
|
||||
.note-list-wrapper .note-book-card .note-book-header .note-icon {
|
||||
font-size: 17px;
|
||||
vertical-align: text-bottom;
|
||||
@@ -1610,7 +1614,8 @@ div.bookmark-folder-widget .note-link .bx {
|
||||
|
||||
.note-list-wrapper .note-book-card .note-book-content.type-canvas .rendered-content,
|
||||
.note-list-wrapper .note-book-card .note-book-content.type-mindMap .rendered-content,
|
||||
.note-list-wrapper .note-book-card .note-book-content.type-code .rendered-content {
|
||||
.note-list-wrapper .note-book-card .note-book-content.type-code .rendered-content,
|
||||
.note-list-wrapper .note-book-card .note-book-content.type-video .rendered-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1675,5 +1675,12 @@
|
||||
"time_selector": {
|
||||
"invalid_input": "The entered time value is not a valid number.",
|
||||
"minimum_input": "The entered time value needs to be at least {{minimumSeconds}} seconds."
|
||||
},
|
||||
"tasks": {
|
||||
"due": {
|
||||
"today": "Today",
|
||||
"tomorrow": "Tomorrow",
|
||||
"yesterday": "Yesterday"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1678,5 +1678,12 @@
|
||||
"redirect_bare_domain_description": "Redirecționează utilizatorii anonimi către pagina de partajare în locul paginii de autentificare",
|
||||
"redirect_bare_domain": "Redirecționează domeniul principal la pagina de partajare",
|
||||
"check_share_root": "Verificare stare pagină partajată principală"
|
||||
},
|
||||
"tasks": {
|
||||
"due": {
|
||||
"today": "Azi",
|
||||
"tomorrow": "Mâine",
|
||||
"yesterday": "Ieri"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
src/share/routes.spec.ts
Normal file
34
src/share/routes.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import supertest from "supertest";
|
||||
import { initializeTranslations } from "../services/i18n.js";
|
||||
import type { Application, Request, Response, NextFunction } from "express";
|
||||
|
||||
let app: Application;
|
||||
|
||||
describe("Share API test", () => {
|
||||
let cannotSetHeadersCount = 0;
|
||||
|
||||
beforeAll(async () => {
|
||||
initializeTranslations();
|
||||
app = (await import("../app.js")).default;
|
||||
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
||||
if (err.message.includes("Cannot set headers after they are sent to the client")) {
|
||||
cannotSetHeadersCount++;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cannotSetHeadersCount = 0;
|
||||
});
|
||||
|
||||
it("requests password for password-protected share", async () => {
|
||||
await supertest(app)
|
||||
.get("/share/YjlPRj2E9fOV")
|
||||
.expect("WWW-Authenticate", 'Basic realm="User Visible Realm", charset="UTF-8"');
|
||||
expect(cannotSetHeadersCount).toBe(0);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -87,7 +87,6 @@ function checkNoteAccess(noteId: string, req: Request, res: Response) {
|
||||
const header = req.header("Authorization");
|
||||
|
||||
if (!header?.startsWith("Basic ")) {
|
||||
requestCredentials(res);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user