mirror of
https://github.com/zadam/trilium.git
synced 2025-11-13 16:55:50 +01:00
Compare commits
42 Commits
react/prom
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20b301ac0e | ||
|
|
bacbe9f47c | ||
|
|
4ecb693be5 | ||
|
|
454310c3e4 | ||
|
|
e51daad5da | ||
|
|
b13c0fe7a2 | ||
|
|
3036d18df5 | ||
|
|
5dbe9e7da6 | ||
|
|
fd9b6e9e67 | ||
|
|
4f580a37a3 | ||
|
|
f64d11b7c8 | ||
|
|
2bf9e0edd9 | ||
|
|
b807079c55 | ||
|
|
2cc5b896e0 | ||
|
|
7c79caba78 | ||
|
|
fbf4a910fa | ||
|
|
95947a9f8c | ||
|
|
1c05acf5ed | ||
|
|
5cc5f3ffae | ||
|
|
54556c73e2 | ||
|
|
5aa63ac50c | ||
|
|
38eaa94a53 | ||
|
|
7810f6c8da | ||
|
|
6d94efb6c8 | ||
|
|
e89646ee7c | ||
|
|
dee8c115ab | ||
|
|
54d3936c7b | ||
|
|
02452a0513 | ||
|
|
e9f40c48e3 | ||
|
|
6b74b227cb | ||
|
|
00874840b7 | ||
|
|
d79a23bc9e | ||
|
|
3015576d7e | ||
|
|
46c2e162f0 | ||
|
|
3c42577da4 | ||
|
|
76f791da93 | ||
|
|
0c5adcee2d | ||
|
|
b11b3ff67f | ||
|
|
e006afc5a2 | ||
|
|
40dbb818c5 | ||
|
|
62dc570d38 | ||
|
|
b759c5e7d2 |
@@ -38,7 +38,7 @@
|
|||||||
"@playwright/test": "1.56.1",
|
"@playwright/test": "1.56.1",
|
||||||
"@stylistic/eslint-plugin": "5.5.0",
|
"@stylistic/eslint-plugin": "5.5.0",
|
||||||
"@types/express": "5.0.5",
|
"@types/express": "5.0.5",
|
||||||
"@types/node": "24.10.0",
|
"@types/node": "24.10.1",
|
||||||
"@types/yargs": "17.0.34",
|
"@types/yargs": "17.0.34",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Elian Doran <contact@eliandoran.me>",
|
"author": "Elian Doran <contact@eliandoran.me>",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"packageManager": "pnpm@10.21.0",
|
"packageManager": "pnpm@10.22.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redocly/cli": "2.11.1",
|
"@redocly/cli": "2.11.1",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
"preact": "10.27.2",
|
"preact": "10.27.2",
|
||||||
"react-i18next": "16.2.4",
|
"react-i18next": "16.3.1",
|
||||||
"reveal.js": "5.2.1",
|
"reveal.js": "5.2.1",
|
||||||
"svg-pan-zoom": "3.6.2",
|
"svg-pan-zoom": "3.6.2",
|
||||||
"tabulator-tables": "6.3.1",
|
"tabulator-tables": "6.3.1",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { Froca } from "../services/froca-interface.js";
|
|||||||
import type FAttachment from "./fattachment.js";
|
import type FAttachment from "./fattachment.js";
|
||||||
import type { default as FAttribute, AttributeType } from "./fattribute.js";
|
import type { default as FAttribute, AttributeType } from "./fattribute.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
|
import search from "../services/search.js";
|
||||||
|
|
||||||
const LABEL = "label";
|
const LABEL = "label";
|
||||||
const RELATION = "relation";
|
const RELATION = "relation";
|
||||||
@@ -255,6 +256,21 @@ export default class FNote {
|
|||||||
return this.children;
|
return this.children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getChildNoteIdsWithArchiveFiltering(includeArchived = false) {
|
||||||
|
if (!includeArchived) {
|
||||||
|
const unorderedIds = new Set(await search.searchForNoteIds(`note.parents.noteId="${this.noteId}" #!archived`));
|
||||||
|
const results: string[] = [];
|
||||||
|
for (const id of this.children) {
|
||||||
|
if (unorderedIds.has(id)) {
|
||||||
|
results.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
} else {
|
||||||
|
return this.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getSubtreeNoteIds(includeArchived = false) {
|
async getSubtreeNoteIds(includeArchived = false) {
|
||||||
let noteIds: (string | string[])[] = [];
|
let noteIds: (string | string[])[] = [];
|
||||||
for (const child of await this.getChildNotes()) {
|
for (const child of await this.getChildNotes()) {
|
||||||
@@ -839,8 +855,7 @@ export default class FNote {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const promotedAttrs = this.getAttributes()
|
const promotedAttrs = this.getAttributeDefinitions()
|
||||||
.filter((attr) => attr.isDefinition())
|
|
||||||
.filter((attr) => {
|
.filter((attr) => {
|
||||||
const def = attr.getDefinition();
|
const def = attr.getDefinition();
|
||||||
|
|
||||||
@@ -860,6 +875,11 @@ export default class FNote {
|
|||||||
return promotedAttrs;
|
return promotedAttrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAttributeDefinitions() {
|
||||||
|
return this.getAttributes()
|
||||||
|
.filter((attr) => attr.isDefinition());
|
||||||
|
}
|
||||||
|
|
||||||
hasAncestor(ancestorNoteId: string, followTemplates = false, visitedNoteIds: Set<string> | null = null) {
|
hasAncestor(ancestorNoteId: string, followTemplates = false, visitedNoteIds: Set<string> | null = null) {
|
||||||
if (this.noteId === ancestorNoteId) {
|
if (this.noteId === ancestorNoteId) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.
|
|||||||
import ApiLog from "../widgets/api_log.jsx";
|
import ApiLog from "../widgets/api_log.jsx";
|
||||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
||||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
||||||
import ContentHeader from "../widgets/containers/content-header.js";
|
import ContentHeader from "../widgets/containers/content_header.js";
|
||||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||||
import FindWidget from "../widgets/find.js";
|
import FindWidget from "../widgets/find.js";
|
||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import LauncherContainer from "../widgets/containers/launcher_container.js";
|
|||||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||||
import NoteTitleWidget from "../widgets/note_title.js";
|
import NoteTitleWidget from "../widgets/note_title.js";
|
||||||
import ContentHeader from "../widgets/containers/content-header.js";
|
import ContentHeader from "../widgets/containers/content_header.js";
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ const HIDDEN_ATTRIBUTES = [
|
|||||||
"viewType",
|
"viewType",
|
||||||
"geolocation",
|
"geolocation",
|
||||||
"docName",
|
"docName",
|
||||||
"webViewSrc"
|
"webViewSrc",
|
||||||
|
"archived"
|
||||||
];
|
];
|
||||||
|
|
||||||
async function renderNormalAttributes(note: FNote) {
|
async function renderNormalAttributes(note: FNote) {
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ function getHue(color: ColorInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getReadableTextColor(bgColor: string) {
|
||||||
|
const colorInstance = Color(bgColor);
|
||||||
|
return colorInstance.isLight() ? "#000" : "#fff";
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createClassForColor
|
createClassForColor
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
.promoted-attributes {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promoted-attributes .promoted-attribute {
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: var(--chip-bg, rgba(0, 0, 0, 0.08));
|
||||||
|
color: var(--chip-fg, inherit);
|
||||||
|
border: 1px solid var(--chip-border, rgba(0, 0, 0, 0.15));
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promoted-attributes .promoted-attribute:hover {
|
||||||
|
background-color: var(--chip-bg-hover, rgba(0, 0, 0, 0.12));
|
||||||
|
border-color: var(--chip-border-hover, rgba(0, 0, 0, 0.22));
|
||||||
|
}
|
||||||
|
|
||||||
|
.promoted-attributes .promoted-attribute .name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promoted-attributes .promoted-attribute .value {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import FNote from "../../entities/fnote";
|
||||||
|
import "./PromotedAttributesDisplay.css";
|
||||||
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
import attributes from "../../services/attributes";
|
||||||
|
import { DefinitionObject } from "../../services/promoted_attribute_definition_parser";
|
||||||
|
import { formatDateTime } from "../../utils/formatters";
|
||||||
|
import { ComponentChild, ComponentChildren, CSSProperties } from "preact";
|
||||||
|
import Icon from "../react/Icon";
|
||||||
|
import NoteLink from "../react/NoteLink";
|
||||||
|
import { getReadableTextColor } from "../../services/css_class_manager";
|
||||||
|
|
||||||
|
interface PromotedAttributesDisplayProps {
|
||||||
|
note: FNote;
|
||||||
|
ignoredAttributes?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttributeWithDefinitions {
|
||||||
|
friendlyName: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
def: DefinitionObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PromotedAttributesDisplay({ note, ignoredAttributes }: PromotedAttributesDisplayProps) {
|
||||||
|
const promotedDefinitionAttributes = useNoteAttributesWithDefinitions(note, ignoredAttributes);
|
||||||
|
return promotedDefinitionAttributes?.length > 0 && (
|
||||||
|
<div className="promoted-attributes">
|
||||||
|
{promotedDefinitionAttributes?.map(attr => buildPromotedAttribute(attr))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function useNoteAttributesWithDefinitions(note: FNote, attributesToIgnore: string[] = []): AttributeWithDefinitions[] {
|
||||||
|
const [ promotedDefinitionAttributes, setPromotedDefinitionAttributes ] = useState<AttributeWithDefinitions[]>(getAttributesWithDefinitions(note, attributesToIgnore));
|
||||||
|
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
|
||||||
|
setPromotedDefinitionAttributes(getAttributesWithDefinitions(note, attributesToIgnore));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return promotedDefinitionAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PromotedAttribute({ attr, children, style }: { attr: AttributeWithDefinitions, children: ComponentChildren, style?: CSSProperties }) {
|
||||||
|
const className = `${attr.type === "label" ? "label" + " " + attr.def.labelType : "relation"}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={attr.friendlyName} className={`promoted-attribute type-${className}`} style={style}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPromotedAttribute(attr: AttributeWithDefinitions): ComponentChildren {
|
||||||
|
const defaultLabel = <><strong>{attr.friendlyName}:</strong>{" "}</>;
|
||||||
|
let content: ComponentChildren;
|
||||||
|
let style: CSSProperties | undefined;
|
||||||
|
|
||||||
|
if (attr.type === "label") {
|
||||||
|
let value = attr.value;
|
||||||
|
switch (attr.def.labelType) {
|
||||||
|
case "number":
|
||||||
|
let formattedValue = value;
|
||||||
|
const numberValue = Number(value);
|
||||||
|
if (!Number.isNaN(numberValue) && attr.def.numberPrecision) formattedValue = numberValue.toFixed(attr.def.numberPrecision);
|
||||||
|
content = <>{defaultLabel}{formattedValue}</>;
|
||||||
|
break;
|
||||||
|
case "date":
|
||||||
|
case "datetime": {
|
||||||
|
const date = new Date(value);
|
||||||
|
const timeFormat = attr.def.labelType !== "date" ? "short" : "none";
|
||||||
|
const formattedValue = formatDateTime(date, "short", timeFormat);
|
||||||
|
content = <>{defaultLabel}{formattedValue}</>;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "time": {
|
||||||
|
const date = new Date(`1970-01-01T${value}Z`);
|
||||||
|
const formattedValue = formatDateTime(date, "none", "short");
|
||||||
|
content = <>{defaultLabel}{formattedValue}</>;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "boolean":
|
||||||
|
content = <><Icon icon={value === "true" ? "bx bx-check-square" : "bx bx-square"} />{" "}<strong>{attr.friendlyName}</strong></>;
|
||||||
|
break;
|
||||||
|
case "url":
|
||||||
|
content = <a href={value} target="_blank" rel="noopener noreferrer">{attr.friendlyName}</a>;
|
||||||
|
break;
|
||||||
|
case "color":
|
||||||
|
style = { backgroundColor: value, color: getReadableTextColor(value) };
|
||||||
|
content = <>{attr.friendlyName}</>;
|
||||||
|
break;
|
||||||
|
case "text":
|
||||||
|
default:
|
||||||
|
content = <>{defaultLabel}{value}</>;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (attr.type === "relation") {
|
||||||
|
content = <>{defaultLabel}<NoteLink notePath={attr.value} showNoteIcon /></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PromotedAttribute attr={attr} style={style}>{content}</PromotedAttribute>
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAttributesWithDefinitions(note: FNote, attributesToIgnore: string[] = []): AttributeWithDefinitions[] {
|
||||||
|
const promotedDefinitionAttributes = note.getAttributeDefinitions();
|
||||||
|
const result: AttributeWithDefinitions[] = [];
|
||||||
|
for (const attr of promotedDefinitionAttributes) {
|
||||||
|
const def = attr.getDefinition();
|
||||||
|
const [ type, name ] = attr.name.split(":", 2);
|
||||||
|
const friendlyName = def?.promotedAlias || name;
|
||||||
|
const props: Omit<AttributeWithDefinitions, "value"> = { def, name, type, friendlyName };
|
||||||
|
|
||||||
|
if (attributesToIgnore.includes(name)) continue;
|
||||||
|
|
||||||
|
if (type === "label") {
|
||||||
|
const labels = note.getLabels(name);
|
||||||
|
for (const label of labels) {
|
||||||
|
if (!label.value) continue;
|
||||||
|
result.push({ ...props, value: label.value } );
|
||||||
|
}
|
||||||
|
} else if (type === "relation") {
|
||||||
|
const relations = note.getRelations(name);
|
||||||
|
for (const relation of relations) {
|
||||||
|
if (!relation.value) continue;
|
||||||
|
result.push({ ...props, value: relation.value } );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -141,7 +141,7 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt
|
|||||||
|
|
||||||
async function getNoteIds(note: FNote) {
|
async function getNoteIds(note: FNote) {
|
||||||
if (viewType === "list" || viewType === "grid" || viewType === "table" || note.type === "search") {
|
if (viewType === "list" || viewType === "grid" || viewType === "table" || note.type === "search") {
|
||||||
return note.getChildNoteIds();
|
return await note.getChildNoteIdsWithArchiveFiltering(includeArchived);
|
||||||
} else {
|
} else {
|
||||||
return await note.getSubtreeNoteIds(includeArchived);
|
return await note.getSubtreeNoteIds(includeArchived);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default class BoardApi {
|
|||||||
private byColumn: ColumnMap | undefined,
|
private byColumn: ColumnMap | undefined,
|
||||||
public columns: string[],
|
public columns: string[],
|
||||||
private parentNote: FNote,
|
private parentNote: FNote,
|
||||||
private statusAttribute: string,
|
readonly statusAttribute: string,
|
||||||
private viewConfig: BoardViewData,
|
private viewConfig: BoardViewData,
|
||||||
private saveConfig: (newConfig: BoardViewData) => void,
|
private saveConfig: (newConfig: BoardViewData) => void,
|
||||||
private setBranchIdToEdit: (branchId: string | undefined) => void
|
private setBranchIdToEdit: (branchId: string | undefined) => void
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { BoardViewContext, TitleEditor } from ".";
|
|||||||
import { ContextMenuEvent } from "../../../menus/context_menu";
|
import { ContextMenuEvent } from "../../../menus/context_menu";
|
||||||
import { openNoteContextMenu } from "./context_menu";
|
import { openNoteContextMenu } from "./context_menu";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
|
import PromotedAttributesDisplay from "../../attribute_widgets/PromotedAttributesDisplay";
|
||||||
|
|
||||||
export const CARD_CLIPBOARD_TYPE = "trilium/board-card";
|
export const CARD_CLIPBOARD_TYPE = "trilium/board-card";
|
||||||
|
|
||||||
@@ -108,6 +109,7 @@ export default function Card({
|
|||||||
title={t("board_view.edit-note-title")}
|
title={t("board_view.edit-note-title")}
|
||||||
onClick={handleEdit}
|
onClick={handleEdit}
|
||||||
/>
|
/>
|
||||||
|
<PromotedAttributesDisplay note={note} ignoredAttributes={[api.statusAttribute]} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<TitleEditor
|
<TitleEditor
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ export default function Column({
|
|||||||
{!isEditing ? (
|
{!isEditing ? (
|
||||||
<>
|
<>
|
||||||
<span className="title">{column}</span>
|
<span className="title">{column}</span>
|
||||||
|
<span className="counter-badge">{columnItems?.length ?? 0}</span>
|
||||||
|
<div className="spacer" />
|
||||||
<span
|
<span
|
||||||
className="edit-icon icon bx bx-edit-alt"
|
className="edit-icon icon bx bx-edit-alt"
|
||||||
title={t("board_view.edit-column-title")}
|
title={t("board_view.edit-column-title")}
|
||||||
|
|||||||
@@ -53,7 +53,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-view-container .board-column h3 > .title {
|
.board-view-container .board-column h3 .counter-badge {
|
||||||
|
background-color: var(--muted-text-color);
|
||||||
|
color: var(--main-background-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0.1em 0.6em;
|
||||||
|
font-size: 0.75em;
|
||||||
|
margin-inline-start: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-view-container .board-column h3 > .spacer {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-book-card.archived {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.note-book-card:not(.expanded) .note-book-content {
|
.note-book-card:not(.expanded) .note-book-content {
|
||||||
padding: 10px
|
padding: 10px
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function ListNoteCard({ note, parentNote, expand, highlightedTokens }: { note: F
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`note-book-card no-tooltip-preview ${isExpanded ? "expanded" : ""}`}
|
className={`note-book-card no-tooltip-preview ${isExpanded ? "expanded" : ""} ${note.isArchived ? "archived" : ""}`}
|
||||||
data-note-id={note.noteId}
|
data-note-id={note.noteId}
|
||||||
>
|
>
|
||||||
<h5 className="note-book-header">
|
<h5 className="note-book-header">
|
||||||
@@ -100,7 +100,7 @@ function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, pa
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`note-book-card no-tooltip-preview block-link`}
|
className={`note-book-card no-tooltip-preview block-link ${note.isArchived ? "archived" : ""}`}
|
||||||
data-href={`#${notePath}`}
|
data-href={`#${notePath}`}
|
||||||
data-note-id={note.noteId}
|
data-note-id={note.noteId}
|
||||||
onClick={(e) => link.goToLink(e)}
|
onClick={(e) => link.goToLink(e)}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default class ContentHeader extends Container<BasicWidget> {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.class("content-header-widget");
|
||||||
this.css("contain", "unset");
|
this.css("contain", "unset");
|
||||||
this.resizeObserver = new ResizeObserver(this.onResize.bind(this));
|
this.resizeObserver = new ResizeObserver(this.onResize.bind(this));
|
||||||
}
|
}
|
||||||
@@ -406,14 +406,17 @@ export function useNoteLabelWithDefault(note: FNote | undefined | null, labelNam
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: FilterLabelsByType<boolean>): [ boolean, (newValue: boolean) => void] {
|
export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: FilterLabelsByType<boolean>): [ boolean, (newValue: boolean) => void] {
|
||||||
const [ labelValue, setLabelValue ] = useState<boolean>(!!note?.hasLabel(labelName));
|
const [, forceRender] = useState({});
|
||||||
|
|
||||||
useEffect(() => setLabelValue(!!note?.hasLabel(labelName)), [ note ]);
|
useEffect(() => {
|
||||||
|
forceRender({});
|
||||||
|
}, [ note ]);
|
||||||
|
|
||||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
for (const attr of loadResults.getAttributeRows()) {
|
for (const attr of loadResults.getAttributeRows()) {
|
||||||
if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) {
|
if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) {
|
||||||
setLabelValue(!attr.isDeleted);
|
forceRender({});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -430,6 +433,7 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: F
|
|||||||
|
|
||||||
useDebugValue(labelName);
|
useDebugValue(labelName);
|
||||||
|
|
||||||
|
const labelValue = !!note?.hasLabel(labelName);
|
||||||
return [ labelValue, setter ] as const;
|
return [ labelValue, setter ] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,13 +59,12 @@ function CollectionTypeSwitcher({ viewType, setViewType }: { viewType: string, s
|
|||||||
function BookProperties({ viewType, note, properties }: { viewType: ViewTypeOptions, note: FNote, properties: BookProperty[] }) {
|
function BookProperties({ viewType, note, properties }: { viewType: ViewTypeOptions, note: FNote, properties: BookProperty[] }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{properties.map(property => (
|
{properties.map(property => (
|
||||||
<div className={`type-${property}`}>
|
<div className={`type-${property}`}>
|
||||||
{mapPropertyView({ note, property })}
|
{mapPropertyView({ note, property })}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{viewType !== "list" && viewType !== "grid" && (
|
|
||||||
<CheckboxPropertyView
|
<CheckboxPropertyView
|
||||||
note={note} property={{
|
note={note} property={{
|
||||||
bindToLabel: "includeArchived",
|
bindToLabel: "includeArchived",
|
||||||
@@ -73,7 +72,6 @@ function BookProperties({ viewType, note, properties }: { viewType: ViewTypeOpti
|
|||||||
type: "checkbox"
|
type: "checkbox"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
|||||||
await attributes.removeAttributeById(noteId, expandedAttr.attributeId);
|
await attributes.removeAttributeById(noteId, expandedAttr.attributeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerCommand("refreshNoteList", { noteId: noteId });
|
triggerCommand("refreshNoteList", { noteId });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.11.0-bullseye-slim AS builder
|
FROM node:24.11.1-bullseye-slim AS builder
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Install native dependencies since we might be building cross-platform.
|
# Install native dependencies since we might be building cross-platform.
|
||||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
|||||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||||
|
|
||||||
FROM node:24.11.0-bullseye-slim
|
FROM node:24.11.1-bullseye-slim
|
||||||
# Install only runtime dependencies
|
# Install only runtime dependencies
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.11.0-alpine AS builder
|
FROM node:24.11.1-alpine AS builder
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Install native dependencies since we might be building cross-platform.
|
# Install native dependencies since we might be building cross-platform.
|
||||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
|||||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||||
|
|
||||||
FROM node:24.11.0-alpine
|
FROM node:24.11.1-alpine
|
||||||
# Install runtime dependencies
|
# Install runtime dependencies
|
||||||
RUN apk add --no-cache su-exec shadow
|
RUN apk add --no-cache su-exec shadow
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.11.0-alpine AS builder
|
FROM node:24.11.1-alpine AS builder
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Install native dependencies since we might be building cross-platform.
|
# Install native dependencies since we might be building cross-platform.
|
||||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
|||||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||||
|
|
||||||
FROM node:24.11.0-alpine
|
FROM node:24.11.1-alpine
|
||||||
# Create a non-root user with configurable UID/GID
|
# Create a non-root user with configurable UID/GID
|
||||||
ARG USER=trilium
|
ARG USER=trilium
|
||||||
ARG UID=1001
|
ARG UID=1001
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.11.0-bullseye-slim AS builder
|
FROM node:24.11.1-bullseye-slim AS builder
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Install native dependencies since we might be building cross-platform.
|
# Install native dependencies since we might be building cross-platform.
|
||||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
|||||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||||
|
|
||||||
FROM node:24.11.0-bullseye-slim
|
FROM node:24.11.1-bullseye-slim
|
||||||
# Create a non-root user with configurable UID/GID
|
# Create a non-root user with configurable UID/GID
|
||||||
ARG USER=trilium
|
ARG USER=trilium
|
||||||
ARG UID=1001
|
ARG UID=1001
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
"is-animated": "2.0.2",
|
"is-animated": "2.0.2",
|
||||||
"is-svg": "6.1.0",
|
"is-svg": "6.1.0",
|
||||||
"jimp": "1.6.0",
|
"jimp": "1.6.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.1",
|
||||||
"marked": "16.4.2",
|
"marked": "16.4.2",
|
||||||
"mime-types": "3.0.1",
|
"mime-types": "3.0.1",
|
||||||
"multer": "2.0.2",
|
"multer": "2.0.2",
|
||||||
|
|||||||
@@ -113,7 +113,16 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
const normalizedFlatText = normalizeSearchText(flatText);
|
const normalizedFlatText = normalizeSearchText(flatText);
|
||||||
|
|
||||||
// Check if =phrase appears in flatText (indicates attribute value match)
|
// Check if =phrase appears in flatText (indicates attribute value match)
|
||||||
matches = normalizedFlatText.includes(`=${normalizedPhrase}`);
|
// For single words, use word-boundary matching to avoid substring matches
|
||||||
|
if (!normalizedPhrase.includes(' ')) {
|
||||||
|
// Single word: look for =word with word boundaries
|
||||||
|
// Split by = to get attribute values, then check each value for exact word match
|
||||||
|
const parts = normalizedFlatText.split('=');
|
||||||
|
matches = parts.slice(1).some(part => this.exactWordMatch(normalizedPhrase, part));
|
||||||
|
} else {
|
||||||
|
// Multi-word phrase: check for substring match
|
||||||
|
matches = normalizedFlatText.includes(`=${normalizedPhrase}`);
|
||||||
|
}
|
||||||
|
|
||||||
if ((this.operator === "=" && matches) || (this.operator === "!=" && !matches)) {
|
if ((this.operator === "=" && matches) || (this.operator === "!=" && !matches)) {
|
||||||
resultNoteSet.add(noteFromBecca);
|
resultNoteSet.add(noteFromBecca);
|
||||||
@@ -124,6 +133,17 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
return resultNoteSet;
|
return resultNoteSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to check if a single word appears as an exact match in text
|
||||||
|
* @param wordToFind - The word to search for (should be normalized)
|
||||||
|
* @param text - The text to search in (should be normalized)
|
||||||
|
* @returns true if the word is found as an exact match (not substring)
|
||||||
|
*/
|
||||||
|
private exactWordMatch(wordToFind: string, text: string): boolean {
|
||||||
|
const words = text.split(/\s+/);
|
||||||
|
return words.some(word => word === wordToFind);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if content contains the exact word (with word boundaries) or exact phrase
|
* Checks if content contains the exact word (with word boundaries) or exact phrase
|
||||||
* This is case-insensitive since content and token are already normalized
|
* This is case-insensitive since content and token are already normalized
|
||||||
@@ -139,9 +159,8 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
return normalizedContent.includes(normalizedToken);
|
return normalizedContent.includes(normalizedToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For single words, split content into words and check for exact match
|
// For single words, use exact word matching to avoid substring matches
|
||||||
const words = normalizedContent.split(/\s+/);
|
return this.exactWordMatch(normalizedToken, normalizedContent);
|
||||||
return words.some(word => word === normalizedToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,7 +174,14 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
// Join tokens with single space to form the phrase
|
// Join tokens with single space to form the phrase
|
||||||
const phrase = normalizedTokens.join(" ");
|
const phrase = normalizedTokens.join(" ");
|
||||||
|
|
||||||
// Check if the phrase appears as a substring (consecutive words)
|
// For single-word phrases, use word-boundary matching to avoid substring matches
|
||||||
|
// e.g., "asd" should not match "asdfasdf"
|
||||||
|
if (!phrase.includes(' ')) {
|
||||||
|
// Single word: use exact word matching to avoid substring matches
|
||||||
|
return this.exactWordMatch(phrase, normalizedContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For multi-word phrases, check if the phrase appears as consecutive words
|
||||||
if (normalizedContent.includes(phrase)) {
|
if (normalizedContent.includes(phrase)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"preact": "10.27.2",
|
"preact": "10.27.2",
|
||||||
"preact-iso": "2.11.0",
|
"preact-iso": "2.11.0",
|
||||||
"preact-render-to-string": "6.6.3",
|
"preact-render-to-string": "6.6.3",
|
||||||
"react-i18next": "16.2.4"
|
"react-i18next": "16.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@preact/preset-vite": "2.10.2",
|
"@preact/preset-vite": "2.10.2",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
"@playwright/test": "1.56.1",
|
"@playwright/test": "1.56.1",
|
||||||
"@triliumnext/server": "workspace:*",
|
"@triliumnext/server": "workspace:*",
|
||||||
"@types/express": "5.0.5",
|
"@types/express": "5.0.5",
|
||||||
"@types/node": "24.10.0",
|
"@types/node": "24.10.1",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"@vitest/ui": "3.2.4",
|
"@vitest/ui": "3.2.4",
|
||||||
"chalk": "5.6.2",
|
"chalk": "5.6.2",
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"url": "https://github.com/TriliumNext/Trilium/issues"
|
"url": "https://github.com/TriliumNext/Trilium/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://triliumnotes.org",
|
"homepage": "https://triliumnotes.org",
|
||||||
"packageManager": "pnpm@10.21.0",
|
"packageManager": "pnpm@10.22.0",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@ckeditor/ckeditor5-mention": "patches/@ckeditor__ckeditor5-mention.patch",
|
"@ckeditor/ckeditor5-mention": "patches/@ckeditor__ckeditor5-mention.patch",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"ckeditor5-premium-features": "47.2.0"
|
"ckeditor5-premium-features": "47.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@smithy/middleware-retry": "4.4.7",
|
"@smithy/middleware-retry": "4.4.10",
|
||||||
"@types/jquery": "3.5.33"
|
"@types/jquery": "3.5.33"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
773
pnpm-lock.yaml
generated
773
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user