Merge branch 'TriliumNext:main' into main

This commit is contained in:
Lucas 2025-11-23 13:24:24 -08:00 committed by GitHub
commit 7155ab8bdc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 651 additions and 563 deletions

View File

@ -56,7 +56,7 @@
"mark.js": "8.11.1", "mark.js": "8.11.1",
"marked": "17.0.1", "marked": "17.0.1",
"mermaid": "11.12.1", "mermaid": "11.12.1",
"mind-elixir": "5.3.6", "mind-elixir": "5.3.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",

View File

@ -21,7 +21,6 @@ import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js"; import NoteWrapperWidget from "../widgets/note_wrapper.js";
import options from "../services/options.js"; import options from "../services/options.js";
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js"; import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import QuickSearchWidget from "../widgets/quick_search.js"; import QuickSearchWidget from "../widgets/quick_search.js";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx"; import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import Ribbon from "../widgets/ribbon/Ribbon.jsx"; import Ribbon from "../widgets/ribbon/Ribbon.jsx";
@ -45,6 +44,7 @@ import utils from "../services/utils.js";
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js"; import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
import NoteDetail from "../widgets/NoteDetail.jsx"; import NoteDetail from "../widgets/NoteDetail.jsx";
import RightPanelWidget from "../widgets/sidebar/RightPanelWidget.jsx"; import RightPanelWidget from "../widgets/sidebar/RightPanelWidget.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
export default class DesktopLayout { export default class DesktopLayout {
@ -141,7 +141,7 @@ export default class DesktopLayout {
.child(<ReadOnlyNoteInfoBar />) .child(<ReadOnlyNoteInfoBar />)
.child(<SharedInfo />) .child(<SharedInfo />)
) )
.child(new PromotedAttributesWidget()) .child(<PromotedAttributes />)
.child(<SqlTableSchemas />) .child(<SqlTableSchemas />)
.child(<NoteDetail />) .child(<NoteDetail />)
.child(<NoteList media="screen" />) .child(<NoteList media="screen" />)

View File

@ -13,7 +13,6 @@ 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 QuickSearchWidget from "../widgets/quick_search.js"; import QuickSearchWidget from "../widgets/quick_search.js";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx"; import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import RootContainer from "../widgets/containers/root_container.js"; import RootContainer from "../widgets/containers/root_container.js";
@ -29,6 +28,7 @@ import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button
import type AppContext from "../components/app_context.js"; import type AppContext from "../components/app_context.js";
import NoteDetail from "../widgets/NoteDetail.jsx"; import NoteDetail from "../widgets/NoteDetail.jsx";
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx"; import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
const MOBILE_CSS = ` const MOBILE_CSS = `
<style> <style>
@ -152,7 +152,7 @@ export default class MobileLayout {
.child(<MobileDetailMenu />) .child(<MobileDetailMenu />)
) )
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />) .child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
.child(new PromotedAttributesWidget()) .child(<PromotedAttributes />)
.child( .child(
new ScrollingContainer() new ScrollingContainer()
.filling() .filling()

View File

@ -12,7 +12,7 @@
* @param whether to execute at the beginning (`false`) * @param whether to execute at the beginning (`false`)
* @api public * @api public
*/ */
function debounce<T>(func: (...args: unknown[]) => T, waitMs: number, immediate: boolean = false) { function debounce<T>(func: (...args: any[]) => T, waitMs: number, immediate: boolean = false) {
let timeout: any; // TODO: fix once we split client and server. let timeout: any; // TODO: fix once we split client and server.
let args: unknown[] | null; let args: unknown[] | null;
let context: unknown; let context: unknown;

View File

@ -1428,9 +1428,7 @@ div.promoted-attribute-cell .tn-checkbox {
height: 1cap; height: 1cap;
} }
/* Relocate the checkbox before the label */
div.promoted-attribute-cell.promoted-attribute-label-boolean > div:first-of-type { div.promoted-attribute-cell.promoted-attribute-label-boolean > div:first-of-type {
order: -1;
margin-inline-end: 1.5em; margin-inline-end: 1.5em;
} }

View File

@ -0,0 +1,97 @@
body.mobile .promoted-attributes-widget {
/* https://github.com/zadam/trilium/issues/4468 */
flex-shrink: 0.4;
overflow: auto;
}
.component.promoted-attributes-widget {
contain: none;
}
.promoted-attributes-container {
margin: 0 1.5em;
overflow: auto;
max-height: 400px;
flex-wrap: wrap;
display: table;
}
.promoted-attribute-cell {
display: flex;
align-items: center;
margin: 10px;
}
.promoted-attribute-cell > label {
user-select: none;
font-weight: bold;
vertical-align: middle;
}
.promoted-attribute-cell > * {
display: table-cell;
padding: 1px 0;
}
.promoted-attribute-cell div.input-group {
margin-inline-start: 10px;
display: flex;
min-height: 40px;
}
.promoted-attribute-cell strong {
word-break:keep-all;
white-space: nowrap;
}
.promoted-attribute-cell input[type="checkbox"] {
width: 22px !important;
flex-grow: 0;
width: unset;
}
/* Restore default apperance */
.promoted-attribute-cell input[type="number"],
.promoted-attribute-cell input[type="checkbox"] {
appearance: auto;
}
.promoted-attribute-cell input[type="color"] {
width: 24px;
height: 24px;
margin-top: 2px;
appearance: none;
padding: 0;
border: 0;
outline: none;
border-radius: 25% !important;
}
.promoted-attribute-cell input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.promoted-attribute-cell input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 25%;
}
.promoted-attribute-label-number input {
text-align: right;
width: 120px;
}
.promoted-attribute-label-color input[type="hidden"][value=""] + input[type="color"] {
position: relative;
opacity: 0.5;
}
.promoted-attribute-label-color input[type="hidden"][value=""] + input[type="color"]:after {
content: "";
position: absolute;
top: 10px;
inset-inline-start: 0px;
inset-inline-end: 0;
height: 2px;
background: rgba(0, 0, 0, 0.5);
transform: rotate(45deg);
pointer-events: none;
}

View File

@ -0,0 +1,450 @@
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
import "./PromotedAttributes.css";
import { useNoteContext, useNoteLabel, useTriliumEvent, useUniqueName } from "./react/hooks";
import { Attribute } from "../services/attribute_parser";
import FAttribute from "../entities/fattribute";
import clsx from "clsx";
import { t } from "../services/i18n";
import { DefinitionObject, LabelType } from "../services/promoted_attribute_definition_parser";
import server from "../services/server";
import FNote from "../entities/fnote";
import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
import NoteAutocomplete from "./react/NoteAutocomplete";
import ws from "../services/ws";
import { UpdateAttributeResponse } from "@triliumnext/commons";
import attributes from "../services/attributes";
import debounce from "../services/debounce";
interface Cell {
definitionAttr: FAttribute;
definition: DefinitionObject;
valueAttr: Attribute;
valueName: string;
}
interface CellProps {
note: FNote;
componentId: string;
cell: Cell,
cells: Cell[],
shouldFocus: boolean;
setCells(cells: Cell[]): void;
setCellToFocus(cell: Cell): void;
}
type OnChangeEventData = TargetedEvent<HTMLInputElement, Event> | InputEvent | JQuery.TriggeredEvent<HTMLInputElement, undefined, HTMLInputElement, HTMLInputElement>;
type OnChangeListener = (e: OnChangeEventData) => Promise<void>;
export default function PromotedAttributes() {
const { note, componentId } = useNoteContext();
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
const [ cellToFocus, setCellToFocus ] = useState<Cell>();
return (
<div className="promoted-attributes-widget">
{cells && cells.length > 0 && <div className="promoted-attributes-container">
{note && cells?.map(cell => <PromotedAttributeCell
cell={cell}
cells={cells} setCells={setCells}
shouldFocus={cell === cellToFocus} setCellToFocus={setCellToFocus}
componentId={componentId} note={note}
/>)}
</div>}
</div>
);
}
/**
* Handles the individual cells (instances for promoted attributes including empty attributes). Promoted attributes with "multiple" multiplicity will have
* each value represented as a separate cell.
*
* The cells are returned as a state since they can also be altered internally if needed, for example to add a new empty cell.
*/
function usePromotedAttributeData(note: FNote | null | undefined, componentId: string): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
const [ viewType ] = useNoteLabel(note, "viewType");
const [ cells, setCells ] = useState<Cell[]>();
function refresh() {
if (!note || viewType === "table") {
setCells([]);
return;
}
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
const ownedAttributes = note.getOwnedAttributes();
// attrs are not resorted if position changes after the initial load
// promoted attrs are sorted primarily by order of definitions, but with multi-valued promoted attrs
// the order of attributes is important as well
ownedAttributes.sort((a, b) => a.position - b.position);
const cells: Cell[] = [];
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) as Attribute[];
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 definition = definitionAttr.getDefinition();
// if not owned, we'll force creation of a new attribute instead of updating the inherited one
if (valueAttr.noteId !== note.noteId) {
valueAttr.attributeId = "";
}
cells.push({ definitionAttr, definition, valueAttr, valueName });
}
}
setCells(cells);
}
useEffect(refresh, [ note, viewType ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) {
refresh();
}
});
return [ cells, setCells ];
}
function PromotedAttributeCell(props: CellProps) {
const { valueName, valueAttr, definition } = props.cell;
const inputId = useUniqueName(`value-${valueAttr.name}`);
useEffect(() => {
if (!props.shouldFocus) return;
const inputEl = document.getElementById(inputId);
if (inputEl) {
inputEl.focus();
}
}, [ props.shouldFocus ]);
let correspondingInput: ComponentChild;
let className: string | undefined;
switch (valueAttr.type) {
case "label":
correspondingInput = <LabelInput inputId={inputId} {...props} />;
className = `promoted-attribute-label-${definition.labelType}`;
break;
case "relation":
correspondingInput = <RelationInput inputId={inputId} {...props} />;
className = "promoted-attribute-relation";
break;
default:
ws.logError(t(`promoted_attributes.unknown_attribute_type`, { type: valueAttr.type }));
break;
}
return (
<div className={clsx("promoted-attribute-cell", className)}>
{definition.labelType !== "boolean" && <label for={inputId}>{definition.promotedAlias ?? valueName}</label>}
{correspondingInput}
<MultiplicityCell {...props} />
</div>
)
}
const LABEL_MAPPINGS: Record<LabelType, HTMLInputTypeAttribute> = {
text: "text",
number: "number",
boolean: "checkbox",
date: "date",
datetime: "datetime-local",
time: "time",
color: "hidden", // handled separately.
url: "url"
};
function LabelInput({ inputId, ...props }: CellProps & { inputId: string }) {
const { valueName, valueAttr, definition, definitionAttr } = props.cell;
const onChangeListener = buildPromotedAttributeLabelChangedListener({...props});
const extraInputProps: InputHTMLAttributes = {};
useEffect(() => {
if (definition.labelType === "text") {
const el = document.getElementById(inputId);
if (el) {
setupTextLabelAutocomplete(el as HTMLInputElement, valueAttr, onChangeListener);
}
}
}, [ inputId, valueAttr, onChangeListener ]);
switch (definition.labelType) {
case "number": {
let step = 1;
for (let i = 0; i < (definition.numberPrecision || 0) && i < 10; i++) {
step /= 10;
}
extraInputProps.step = step;
break;
}
case "url": {
extraInputProps.placeholder = t("promoted_attributes.url_placeholder");
break;
}
}
const inputNode = <input
className="form-control promoted-attribute-input"
tabIndex={200 + definitionAttr.position}
id={inputId}
type={LABEL_MAPPINGS[definition.labelType ?? "text"]}
value={valueAttr.value}
placeholder={t("promoted_attributes.unset-field-placeholder")}
data-attribute-id={valueAttr.attributeId}
data-attribute-type={valueAttr.type}
data-attribute-name={valueAttr.name}
onChange={onChangeListener}
{...extraInputProps}
/>;
if (definition.labelType === "boolean") {
return <>
<div>
<label className="tn-checkbox">{inputNode}</label>
</div>
<label for={inputId}>{definition.promotedAlias ?? valueName}</label>
</>
} else {
return (
<div className="input-group">
{inputNode}
{ definition.labelType === "color" && <ColorPicker {...props} onChange={onChangeListener} inputId={inputId} />}
{ definition.labelType === "url" && (
<InputButton
className="open-external-link-button"
icon="bx bx-window-open"
title={t("promoted_attributes.open_external_link")}
onClick={(e) => {
const inputEl = document.getElementById(inputId) as HTMLInputElement | null;
const url = inputEl?.value;
if (url) {
window.open(url, "_blank");
}
}}
/>
)}
</div>
);
}
}
// We insert a separate input since the color input does not support empty value.
// This is a workaround to allow clearing the color input.
function ColorPicker({ cell, onChange, inputId }: CellProps & {
onChange: (e: TargetedEvent<HTMLInputElement, Event>) => Promise<void>,
inputId: string;
}) {
const defaultColor = "#ffffff";
const colorInputRef = useRef<HTMLInputElement>(null);
return (
<>
<input
ref={colorInputRef}
className="form-control promoted-attribute-input"
type="color"
value={cell.valueAttr.value || defaultColor}
onChange={onChange}
/>
<InputButton
icon="bx bxs-tag-x"
title={t("promoted_attributes.remove_color")}
onClick={(e) => {
// Indicate to the user the color was reset.
if (colorInputRef.current) {
colorInputRef.current.value = defaultColor;
}
// Trigger the actual attribute change by injecting it into the hidden field.
const inputEl = document.getElementById(inputId) as HTMLInputElement | null;
if (!inputEl) return;
inputEl.value = "";
onChange({
...e,
target: inputEl
} as unknown as TargetedInputEvent<HTMLInputElement>);
}}
/>
</>
)
}
function RelationInput({ inputId, ...props }: CellProps & { inputId: string }) {
return (
<NoteAutocomplete
id={inputId}
noteId={props.cell.valueAttr.value}
noteIdChanged={async (value) => {
const { note, cell, componentId } = props;
cell.valueAttr.attributeId = (await updateAttribute(note, cell, componentId, value)).attributeId;
}}
/>
)
}
function MultiplicityCell({ cell, cells, setCells, setCellToFocus, note, componentId }: CellProps) {
return (cell.definition.multiplicity === "multi" &&
<td className="multiplicity">
<PromotedActionButton
icon="bx bx-plus"
title={t("promoted_attributes.add_new_attribute")}
onClick={() => {
const index = cells.indexOf(cell);
const newCell: Cell = {
...cell,
valueAttr: {
attributeId: "",
type: cell.valueAttr.type,
name: cell.valueName,
value: ""
}
};
setCells([
...cells.slice(0, index + 1),
newCell,
...cells.slice(index + 1)
]);
setCellToFocus(newCell);
}}
/>{' '}
<PromotedActionButton
icon="bx bx-trash"
title={t("promoted_attributes.remove_this_attribute")}
onClick={async () => {
// Remove the attribute from the server if it exists.
const { attributeId, type } = cell.valueAttr;
const valueName = cell.valueName;
if (attributeId) {
await server.remove(`notes/${note.noteId}/attributes/${attributeId}`, componentId);
}
const index = cells.indexOf(cell);
const isLastOneOfType = cells.filter(c => c.valueAttr.type === type && c.valueAttr.name === valueName).length < 2;
const newOnesToInsert: Cell[] = [];
if (isLastOneOfType) {
newOnesToInsert.push({
...cell,
valueAttr: {
attributeId: "",
type: cell.valueAttr.type,
name: cell.valueName,
value: ""
}
})
}
setCells(cells.toSpliced(index, 1, ...newOnesToInsert));
}}
/>
</td>
)
}
function PromotedActionButton({ icon, title, onClick }: {
icon: string,
title: string,
onClick: MouseEventHandler<HTMLSpanElement>
}) {
return (
<span
className={clsx("tn-tool-button pointer", icon)}
title={title}
onClick={onClick}
/>
)
}
function InputButton({ icon, className, title, onClick }: {
icon: string;
className?: string;
title: string;
onClick: MouseEventHandler<HTMLSpanElement>;
}) {
return (
<span
className={clsx("input-group-text", className, icon)}
title={title}
onClick={onClick}
/>
)
}
function setupTextLabelAutocomplete(el: HTMLInputElement, valueAttr: Attribute, onChangeListener: OnChangeListener) {
// no need to await for this, can be done asynchronously
const $input = $(el);
server.get<string[]>(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then((_attributeValues) => {
if (_attributeValues.length === 0) {
return;
}
const 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", onChangeListener);
});
}
function buildPromotedAttributeLabelChangedListener({ note, cell, componentId, ...props }: CellProps): OnChangeListener {
async function onChange(e: OnChangeEventData) {
const inputEl = e.target as HTMLInputElement;
let value: string;
if (inputEl.type === "checkbox") {
value = inputEl.checked ? "true" : "false";
} else {
value = inputEl.value;
}
cell.valueAttr.attributeId = (await updateAttribute(note, cell, componentId, value)).attributeId;
}
return debounce(onChange, 250);
}
function updateAttribute(note: FNote, cell: Cell, componentId: string, value: string | undefined) {
return server.put<UpdateAttributeResponse>(
`notes/${note.noteId}/attribute`,
{
attributeId: cell.valueAttr.attributeId,
type: cell.valueAttr.type,
name: cell.valueName,
value: value || ""
},
componentId
);
}

View File

@ -51,12 +51,12 @@ const ViewComponents: Record<ViewTypeOptions, { normal: LazyLoadedComponent, pri
} }
export default function NoteList(props: Pick<NoteListProps, "displayOnlyCollections" | "media" | "onReady" | "onProgressChanged">) { export default function NoteList(props: Pick<NoteListProps, "displayOnlyCollections" | "media" | "onReady" | "onProgressChanged">) {
const { note, noteContext, notePath, ntxId } = useNoteContext(); const { note, noteContext, notePath, ntxId, viewScope } = useNoteContext();
const viewType = useNoteViewType(note); const viewType = useNoteViewType(note);
const [ enabled, setEnabled ] = useState(noteContext?.hasNoteList()); const [ enabled, setEnabled ] = useState(noteContext?.hasNoteList());
useEffect(() => { useEffect(() => {
setEnabled(noteContext?.hasNoteList()); setEnabled(noteContext?.hasNoteList());
}, [ noteContext, viewType ]) }, [ noteContext, viewType, viewScope?.viewMode ])
return <CustomNoteList viewType={viewType} note={note} isEnabled={!!enabled} notePath={notePath} ntxId={ntxId} {...props} /> return <CustomNoteList viewType={viewType} note={note} isEnabled={!!enabled} notePath={notePath} ntxId={ntxId} {...props} />
} }

View File

@ -11,6 +11,7 @@ import { ComponentChildren } from "preact";
import NoteList from "../collections/NoteList"; import NoteList from "../collections/NoteList";
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter"; import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
import FormattingToolbar from "../ribbon/FormattingToolbar"; import FormattingToolbar from "../ribbon/FormattingToolbar";
import PromotedAttributes from "../PromotedAttributes";
export default function PopupEditor() { export default function PopupEditor() {
const [ shown, setShown ] = useState(false); const [ shown, setShown ] = useState(false);
@ -47,6 +48,7 @@ export default function PopupEditor() {
}} }}
onHidden={() => setShown(false)} onHidden={() => setShown(false)}
> >
<PromotedAttributes />
<StandaloneRibbonAdapter component={FormattingToolbar} /> <StandaloneRibbonAdapter component={FormattingToolbar} />
<NoteDetail /> <NoteDetail />
<NoteList media="screen" displayOnlyCollections /> <NoteList media="screen" displayOnlyCollections />

View File

@ -1,460 +0,0 @@
import { t } from "../services/i18n.js";
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";
import attributeService from "../services/attributes.js";
import options from "../services/options.js";
import utils from "../services/utils.js";
import type FNote from "../entities/fnote.js";
import type { Attribute } from "../services/attribute_parser.js";
import type FAttribute from "../entities/fattribute.js";
import type { EventData } from "../components/app_context.js";
const TPL = /*html*/`
<div class="promoted-attributes-widget">
<style>
body.mobile .promoted-attributes-widget {
/* https://github.com/zadam/trilium/issues/4468 */
flex-shrink: 0.4;
overflow: auto;
}
.promoted-attributes-container {
margin: 0 1.5em;
overflow: auto;
max-height: 400px;
flex-wrap: wrap;
display: table;
}
.promoted-attribute-cell {
display: flex;
align-items: center;
margin: 10px;
display: table-row;
}
.promoted-attribute-cell > label {
user-select: none;
font-weight: bold;
vertical-align: middle;
}
.promoted-attribute-cell > * {
display: table-cell;
padding: 1px 0;
}
.promoted-attribute-cell div.input-group {
margin-inline-start: 10px;
display: flex;
min-height: 40px;
}
.promoted-attribute-cell strong {
word-break:keep-all;
white-space: nowrap;
}
.promoted-attribute-cell input[type="checkbox"] {
width: 22px !important;
flex-grow: 0;
width: unset;
}
/* Restore default apperance */
.promoted-attribute-cell input[type="number"],
.promoted-attribute-cell input[type="checkbox"] {
appearance: auto;
}
.promoted-attribute-cell input[type="color"] {
width: 24px;
height: 24px;
margin-top: 2px;
appearance: none;
padding: 0;
border: 0;
outline: none;
border-radius: 25% !important;
}
.promoted-attribute-cell input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.promoted-attribute-cell input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 25%;
}
.promoted-attribute-label-color input[type="hidden"][value=""] + input[type="color"] {
position: relative;
opacity: 0.5;
}
.promoted-attribute-label-color input[type="hidden"][value=""] + input[type="color"]:after {
content: "";
position: absolute;
top: 10px;
inset-inline-start: 0px;
inset-inline-end: 0;
height: 2px;
background: rgba(0, 0, 0, 0.5);
transform: rotate(45deg);
pointer-events: none;
}
</style>
<div class="promoted-attributes-container"></div>
</div>`;
// TODO: Deduplicate
interface AttributeResult {
attributeId: string;
}
export default class PromotedAttributesWidget extends NoteContextAwareWidget {
private $container!: JQuery<HTMLElement>;
get name() {
return "promotedAttributes";
}
get toggleCommand() {
return "toggleRibbonTabPromotedAttributes";
}
doRender() {
this.$widget = $(TPL);
this.contentSized();
this.$container = this.$widget.find(".promoted-attributes-container");
}
getTitle(note: FNote) {
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
if (promotedDefAttrs.length === 0) {
return { show: false };
}
return {
show: true,
activate: options.is("promotedAttributesOpenInRibbon"),
title: t("promoted_attributes.promoted_attributes"),
icon: "bx bx-table"
};
}
async refreshWithNote(note: FNote) {
this.$container.empty();
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
const ownedAttributes = note.getOwnedAttributes();
// attrs are not resorted if position changes after the initial load
// promoted attrs are sorted primarily by order of definitions, but with multi-valued promoted attrs
// the order of attributes is important as well
ownedAttributes.sort((a, b) => a.position - b.position);
if (promotedDefAttrs.length === 0 || note.getLabelValue("viewType") === "table") {
this.toggleInt(false);
return;
}
const $cells: JQuery<HTMLElement>[] = [];
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) as Attribute[];
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);
if ($cell) {
$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: FAttribute, valueAttr: Attribute, valueName: string) {
const definition = definitionAttr.getDefinition();
const id = `value-${valueAttr.attributeId}`;
const $input = $("<input>")
.prop("tabindex", 200 + definitionAttr.position)
.prop("id", id)
.attr("data-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
.attr("data-attribute-type", valueAttr.type)
.attr("data-attribute-name", valueAttr.name)
.prop("value", valueAttr.value)
.prop("placeholder", t("promoted_attributes.unset-field-placeholder"))
.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(
$("<label>")
.prop("for", id)
.text(definition.promotedAlias ?? valueName)
)
.append($("<div>").addClass("input-group").append($input))
.append($actionCell)
.append($multiplicityCell);
if (valueAttr.type === "label") {
$wrapper.addClass(`promoted-attribute-label-${definition.labelType}`);
if (definition.labelType === "text") {
$input.prop("type", "text");
// autocomplete for label values is just nice to have, mobile can keep labels editable without autocomplete
if (utils.isDesktop()) {
// no need to await for this, can be done asynchronously
server.get<string[]>(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then((_attributeValues) => {
if (_attributeValues.length === 0) {
return;
}
const 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");
$input.wrap($(`<label class="tn-checkbox"></label>`));
$wrapper.find(".input-group").removeClass("input-group");
if (valueAttr.value === "true") {
$input.prop("checked", "checked");
}
} else if (definition.labelType === "date") {
$input.prop("type", "date");
} else if (definition.labelType === "datetime") {
$input.prop("type", "datetime-local");
} else if (definition.labelType === "time") {
$input.prop("type", "time");
} else if (definition.labelType === "url") {
$input.prop("placeholder", t("promoted_attributes.url_placeholder"));
const $openButton = $("<span>")
.addClass("input-group-text open-external-link-button bx bx-window-open")
.prop("title", t("promoted_attributes.open_external_link"))
.on("click", () => window.open($input.val() as string, "_blank"));
$input.after($openButton);
} else if (definition.labelType === "color") {
const defaultColor = "#ffffff";
$input.prop("type", "hidden");
$input.val(valueAttr.value ?? "");
// We insert a separate input since the color input does not support empty value.
// This is a workaround to allow clearing the color input.
const $colorInput = $("<input>")
.prop("type", "color")
.prop("value", valueAttr.value || defaultColor)
.addClass("form-control promoted-attribute-input")
.on("change", e => setValue((e.target as HTMLInputElement).value, e));
$input.after($colorInput);
const $clearButton = $("<span>")
.addClass("input-group-text bx bxs-tag-x")
.prop("title", t("promoted_attributes.remove_color"))
.on("click", e => setValue("", e));
const setValue = (color: string, event: JQuery.TriggeredEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => {
$input.val(color);
if (!color) {
$colorInput.val(defaultColor);
}
event.target = $input[0]; // Set the event target to the main input
this.promotedAttributeChanged(event);
};
$colorInput.after($clearButton);
} else {
ws.logError(t("promoted_attributes.unknown_label_type", { type: definition.labelType }));
}
} else if (valueAttr.type === "relation") {
if (valueAttr.value) {
$input.val(await treeService.getNoteTitle(valueAttr.value));
}
if (utils.isDesktop()) {
// 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 {
// we can't provide user a way to edit the relation so make it read only
$input.attr("readonly", "readonly");
}
} else {
ws.logError(t(`promoted_attributes.unknown_attribute_type`, { type: valueAttr.type }));
return;
}
if (definition.multiplicity === "multi") {
const $addButton = $("<span>")
.addClass("bx bx-plus pointer tn-tool-button")
.prop("title", t("promoted_attributes.add_new_attribute"))
.on("click", async () => {
const $new = await this.createPromotedAttributeCell(
definitionAttr,
{
attributeId: "",
type: valueAttr.type,
name: valueName,
value: ""
},
valueName
);
if ($new) {
$wrapper.after($new);
$new.find("input").trigger("focus");
}
});
const $removeButton = $("<span>")
.addClass("bx bx-trash pointer tn-tool-button")
.prop("title", t("promoted_attributes.remove_this_attribute"))
.on("click", async () => {
const attributeId = $input.attr("data-attribute-id");
if (attributeId) {
await server.remove(`notes/${this.noteId}/attributes/${attributeId}`, this.componentId);
}
// if it's the last one the create new empty form immediately
const sameAttrSelector = `input[data-attribute-type='${valueAttr.type}'][data-attribute-name='${valueName}']`;
if (this.$widget.find(sameAttrSelector).length <= 1) {
const $new = await this.createPromotedAttributeCell(
definitionAttr,
{
attributeId: "",
type: valueAttr.type,
name: valueName,
value: ""
},
valueName
);
if ($new) {
$wrapper.after($new);
}
}
$wrapper.remove();
});
$multiplicityCell.append(" &nbsp;").append($addButton).append(" &nbsp;").append($removeButton);
}
return $wrapper;
}
async promotedAttributeChanged(event: JQuery.TriggeredEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) {
const $attr = $(event.target);
let value;
if ($attr.prop("type") === "checkbox") {
value = $attr.is(":checked") ? "true" : "false";
} else if ($attr.attr("data-attribute-type") === "relation") {
const selectedPath = $attr.getSelectedNotePath();
value = selectedPath ? treeService.getNoteIdFromUrl(selectedPath) : "";
} else {
value = $attr.val();
}
const result = await server.put<AttributeResult>(
`notes/${this.noteId}/attribute`,
{
attributeId: $attr.attr("data-attribute-id"),
type: $attr.attr("data-attribute-type"),
name: $attr.attr("data-attribute-name"),
value: value
},
this.componentId
);
$attr.attr("data-attribute-id", result.attributeId);
}
focus() {
this.$widget.find(".promoted-attribute-input:first").focus();
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
this.refresh();
}
}
}

View File

@ -37,25 +37,6 @@ export default function NoteAutocomplete({ id, inputRef: externalInputRef, text,
...opts, ...opts,
container: container?.current container: container?.current
}); });
if (onChange || noteIdChanged) {
const listener = (_e, suggestion) => {
onChange?.(suggestion);
if (noteIdChanged) {
const noteId = suggestion?.notePath?.split("/")?.at(-1);
noteIdChanged(noteId);
}
};
$autoComplete
.on("autocomplete:noteselected", listener)
.on("autocomplete:externallinkselected", listener)
.on("autocomplete:commandselected", listener)
.on("change", (e) => {
if (!ref.current?.value) {
listener(e, null);
}
});
}
if (onTextChange) { if (onTextChange) {
$autoComplete.on("input", () => onTextChange($autoComplete[0].value)); $autoComplete.on("input", () => onTextChange($autoComplete[0].value));
} }
@ -67,6 +48,40 @@ export default function NoteAutocomplete({ id, inputRef: externalInputRef, text,
} }
}, [opts, container?.current]); }, [opts, container?.current]);
// On change event handlers.
useEffect(() => {
if (!ref.current) return;
const $autoComplete = $(ref.current);
if (onChange || noteIdChanged) {
const autoCompleteListener = (_e, suggestion) => {
onChange?.(suggestion);
if (noteIdChanged) {
const noteId = suggestion?.notePath?.split("/")?.at(-1);
noteIdChanged(noteId);
}
};
const changeListener = (e) => {
if (!ref.current?.value) {
autoCompleteListener(e, null);
}
};
$autoComplete
.on("autocomplete:noteselected", autoCompleteListener)
.on("autocomplete:externallinkselected", autoCompleteListener)
.on("autocomplete:commandselected", autoCompleteListener)
.on("change", changeListener);
return () => {
$autoComplete
.off("autocomplete:noteselected", autoCompleteListener)
.off("autocomplete:externallinkselected", autoCompleteListener)
.off("autocomplete:commandselected", autoCompleteListener)
.off("change", changeListener);
};
}
}, [opts, container?.current, onChange, noteIdChanged])
useEffect(() => { useEffect(() => {
if (!ref.current) return; if (!ref.current) return;
const $autoComplete = $(ref.current); const $autoComplete = $(ref.current);
@ -76,6 +91,8 @@ export default function NoteAutocomplete({ id, inputRef: externalInputRef, text,
} else if (text) { } else if (text) {
note_autocomplete.setText($autoComplete, text); note_autocomplete.setText($autoComplete, text);
} else { } else {
$autoComplete.setSelectedNotePath("");
$autoComplete.autocomplete("val", "").trigger("change");
ref.current.value = ""; ref.current.value = "";
} }
}, [text, noteId]); }, [text, noteId]);

View File

@ -2,10 +2,10 @@ import { useEffect, useRef, useState } from "preact/hooks";
import { useNoteContext } from "./react/hooks"; import { useNoteContext } from "./react/hooks";
export default function ScrollPadding() { export default function ScrollPadding() {
const { note, parentComponent, ntxId } = useNoteContext(); const { note, parentComponent, ntxId, viewScope } = useNoteContext();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState<number>(10); const [height, setHeight] = useState<number>(10);
const isEnabled = ["text", "code"].includes(note?.type ?? ""); const isEnabled = ["text", "code"].includes(note?.type ?? "") && viewScope?.viewMode === "default";
const refreshHeight = () => { const refreshHeight = () => {
if (!ref.current) return; if (!ref.current) return;
@ -28,7 +28,7 @@ export default function ScrollPadding() {
refreshHeight(); refreshHeight();
return () => observer.disconnect(); return () => observer.disconnect();
}, [note]); // re-run when note changes }, [ note, isEnabled ]); // re-run when note changes
return (isEnabled ? return (isEnabled ?
<div <div

View File

@ -34,7 +34,7 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Options["locale"] | null>
ja: "ja", ja: "ja",
pt: "pt", pt: "pt",
pt_br: "pt", pt_br: "pt",
ro: null, ro: "ro",
ru: "ru", ru: "ru",
tw: "zh_TW", tw: "zh_TW",
uk: null uk: null

View File

@ -7,6 +7,7 @@ import BAttribute from "../../becca/entities/battribute.js";
import becca from "../../becca/becca.js"; import becca from "../../becca/becca.js";
import ValidationError from "../../errors/validation_error.js"; import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express"; import type { Request } from "express";
import { UpdateAttributeResponse } from "@triliumnext/commons";
function getEffectiveNoteAttributes(req: Request) { function getEffectiveNoteAttributes(req: Request) {
const note = becca.getNote(req.params.noteId); const note = becca.getNote(req.params.noteId);
@ -18,7 +19,7 @@ function updateNoteAttribute(req: Request) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const body = req.body; const body = req.body;
let attribute; let attribute: BAttribute;
if (body.attributeId) { if (body.attributeId) {
attribute = becca.getAttributeOrThrow(body.attributeId); attribute = becca.getAttributeOrThrow(body.attributeId);
@ -64,7 +65,7 @@ function updateNoteAttribute(req: Request) {
return { return {
attributeId: attribute.attributeId attributeId: attribute.attributeId
}; } satisfies UpdateAttributeResponse;
} }
function setNoteAttribute(req: Request) { function setNoteAttribute(req: Request) {

65
docs/README-ko.md vendored
View File

@ -41,7 +41,7 @@ Trilium Notes는 대규모 개인 지식 기반 구축에 중점을 둔 무료
**[docs.triliumnotes.org](https://docs.triliumnotes.org/)에서 전체 문서를 확인하세요** **[docs.triliumnotes.org](https://docs.triliumnotes.org/)에서 전체 문서를 확인하세요**
저희 문서는 다양한 형식으로 제공됩니다: 문서는 다양한 형식으로 제공됩니다:
- **온라인 문서**: [docs.triliumnotes.org](https://docs.triliumnotes.org/)에서 모든 문서를 - **온라인 문서**: [docs.triliumnotes.org](https://docs.triliumnotes.org/)에서 모든 문서를
보여줍니다 보여줍니다
- **도움말**: 트릴리움 어플리케이션에서 `F1` 버튼을 눌러 같은 문서를 직접 볼 수 있습니다 - **도움말**: 트릴리움 어플리케이션에서 `F1` 버튼을 눌러 같은 문서를 직접 볼 수 있습니다
@ -60,45 +60,34 @@ Trilium Notes는 대규모 개인 지식 기반 구축에 중점을 둔 무료
- [개인 지식 베이스의 - [개인 지식 베이스의
패턴들](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge) 패턴들](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge)
## 🎁 기능 ## 🎁 주요 기능
* 노트는 다양한 깊이의 트리로 배열될 수 있습니다. 하나의 노트는 트리의 여러 위치에 둘 수 있습니다 * 노트는 다양한 깊이의 트리로 배열될 수 있으며, 하나의 노트는 트리의 여러 위치에 둘 수 있음
([cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes) 참고) ([cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes) 참고)
* Rich WYSIWYG note editor including e.g. tables, images and * 마크다운 [자동서식](https://triliumnext.github.io/Docs/Wiki/text-notes#autoformat)과 함께
[math](https://triliumnext.github.io/Docs/Wiki/text-notes) with markdown 테이블, 이미지, 그리고 [수학](https://triliumnext.github.io/Docs/Wiki/text-notes) 등의 기능을
[autoformat](https://triliumnext.github.io/Docs/Wiki/text-notes#autoformat) 포함한 다양한 기능의 WYSIWYG 노트 편집기 제공
* Support for editing [notes with source * 구문 강조를 포함한 [소스코드](https://triliumnext.github.io/Docs/Wiki/code-notes) 편집 기능
code](https://triliumnext.github.io/Docs/Wiki/code-notes), including syntax * 쉽고 빠르게 노트를 찾을 수 있는
highlighting [내비게이션](https://triliumnext.github.io/Docs/Wiki/note-navigation), 전체 텍스트 검색 및
* Fast and easy [navigation between [노트 호이스팅](https://triliumnext.github.io/Docs/Wiki/note-hoisting)
notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text * 원활한 [노트 버전 관리](https://triliumnext.github.io/Docs/Wiki/note-revisions)
search and [note * 노트의 [속성](https://triliumnext.github.io/Docs/Wiki/attributes)은 노트 조직화, 쿼리, 그리고
hoisting](https://triliumnext.github.io/Docs/Wiki/note-hoisting) 고급 기능인 [스크립팅](https://triliumnext.github.io/Docs/Wiki/scripts)에 사용
* Seamless [note * 영어, 독일어, 스페인어, 프랑스어, 루마니아어, 중국어 (간체, 번체) UI 제공
versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions) * 더욱 안전한 로그인을 위해 직접 [OpenID 및 TOTP
* Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be 통합](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md)
used for note organization, querying and advanced * self-hosted 동기화 서버를 통한
[scripting](https://triliumnext.github.io/Docs/Wiki/scripts) [동기화](https://triliumnext.github.io/Docs/Wiki/synchronization)
* UI available in English, German, Spanish, French, Romanian, and Chinese * [동기화 서버 호스팅을 위한 제3자 서비스](https://trilium.cc/paid-hosting) 제공
(simplified and traditional) * 노트의 인터넷 [공유](https://triliumnext.github.io/Docs/Wiki/sharing) (퍼블리싱) 기능
* Direct [OpenID and TOTP * 노트마다 세분화된 강력한 [노트
integration](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md) 암호화](https://triliumnext.github.io/Docs/Wiki/protected-notes)
for more secure login * [Excalidraw](https://excalidraw.com/) 기반 스케치 다이어그램 (노트 타입 "캔버스")
* [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization) * 노트 사이의 관계 시각화를 위한 [Relation
with self-hosted sync server 지도](https://triliumnext.github.io/Docs/Wiki/relation-map)과 [link
* there's a [3rd party service for hosting synchronisation 지도](https://triliumnext.github.io/Docs/Wiki/link-map)
server](https://trilium.cc/paid-hosting) * [Mind Elixir](https://docs.mind-elixir.com/) 기반 마인드맵
* [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes
to public internet
* Strong [note
encryption](https://triliumnext.github.io/Docs/Wiki/protected-notes) with
per-note granularity
* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type
"canvas")
* [Relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map) and
[link maps](https://triliumnext.github.io/Docs/Wiki/link-map) for visualizing
notes and their relations
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
* [Geo maps](./docs/User%20Guide/User%20Guide/Note%20Types/Geo%20Map.md) with * [Geo maps](./docs/User%20Guide/User%20Guide/Note%20Types/Geo%20Map.md) with
location pins and GPX tracks location pins and GPX tracks
* [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - see [Advanced * [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - see [Advanced

View File

@ -60,7 +60,7 @@
"jiti": "2.6.1", "jiti": "2.6.1",
"jsonc-eslint-parser": "2.4.1", "jsonc-eslint-parser": "2.4.1",
"react-refresh": "0.18.0", "react-refresh": "0.18.0",
"rollup-plugin-webpack-stats": "2.1.7", "rollup-plugin-webpack-stats": "2.1.8",
"tslib": "2.8.1", "tslib": "2.8.1",
"tsx": "4.20.6", "tsx": "4.20.6",
"typescript": "~5.9.0", "typescript": "~5.9.0",

View File

@ -273,3 +273,7 @@ export interface NoteMapPostResponse {
links: NoteMapLink[]; links: NoteMapLink[];
noteIdToDescendantCountMap: Record<string, number>; noteIdToDescendantCountMap: Record<string, number>;
} }
export interface UpdateAttributeResponse {
attributeId: string;
}

48
pnpm-lock.yaml generated
View File

@ -104,8 +104,8 @@ importers:
specifier: 0.18.0 specifier: 0.18.0
version: 0.18.0 version: 0.18.0
rollup-plugin-webpack-stats: rollup-plugin-webpack-stats:
specifier: 2.1.7 specifier: 2.1.8
version: 2.1.7(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) version: 2.1.8(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))
tslib: tslib:
specifier: 2.8.1 specifier: 2.8.1
version: 2.8.1 version: 2.8.1
@ -189,7 +189,7 @@ importers:
version: 0.2.0(mermaid@11.12.1) version: 0.2.0(mermaid@11.12.1)
'@mind-elixir/node-menu': '@mind-elixir/node-menu':
specifier: 5.0.1 specifier: 5.0.1
version: 5.0.1(mind-elixir@5.3.6) version: 5.0.1(mind-elixir@5.3.7)
'@popperjs/core': '@popperjs/core':
specifier: 2.11.8 specifier: 2.11.8
version: 2.11.8 version: 2.11.8
@ -281,8 +281,8 @@ importers:
specifier: 11.12.1 specifier: 11.12.1
version: 11.12.1 version: 11.12.1
mind-elixir: mind-elixir:
specifier: 5.3.6 specifier: 5.3.7
version: 5.3.6 version: 5.3.7
normalize.css: normalize.css:
specifier: 8.0.1 specifier: 8.0.1
version: 8.0.1 version: 8.0.1
@ -10420,8 +10420,8 @@ packages:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
mind-elixir@5.3.6: mind-elixir@5.3.7:
resolution: {integrity: sha512-LU5HuRrtq/Fq/YkgZHUu4gb1Vg6tNQq0Ob7bQKNDTP3A8prcohHF5D7ca5blvqVkyf9+xUdBWsdFMNffMNPnkA==} resolution: {integrity: sha512-lwsyzkgOTOj/8B/aB9jd3stTFfvInKkhM/8lNbhhfGfZ/qFiaaTN2r9de/IFWlzGDjY3eAJdyhe2RMXowHw0hw==}
mini-css-extract-plugin@2.4.7: mini-css-extract-plugin@2.4.7:
resolution: {integrity: sha512-euWmddf0sk9Nv1O0gfeeUAvAkoSlWncNLF77C0TP2+WoPvy8mAHKOzMajcCz2dzvyt3CNgxb1obIEVFIRxaipg==} resolution: {integrity: sha512-euWmddf0sk9Nv1O0gfeeUAvAkoSlWncNLF77C0TP2+WoPvy8mAHKOzMajcCz2dzvyt3CNgxb1obIEVFIRxaipg==}
@ -12690,8 +12690,8 @@ packages:
resolution: {integrity: sha512-EsoOi8moHN6CAYyTZipxDDVTJn0j2nBCWor4wRU45RQ8ER2qREDykXLr3Ulz6hBh6oBKCFTQIjo21i0FXNo/IA==} resolution: {integrity: sha512-EsoOi8moHN6CAYyTZipxDDVTJn0j2nBCWor4wRU45RQ8ER2qREDykXLr3Ulz6hBh6oBKCFTQIjo21i0FXNo/IA==}
hasBin: true hasBin: true
rollup-plugin-stats@1.5.2: rollup-plugin-stats@1.5.3:
resolution: {integrity: sha512-3PtTLkgJ9zDaBITh92sysBxpaIJHSokODV4eo6ivnxfzDZxFPpTPooWHPse/X/Qi9A186Opu+hPycZNPxSgtnA==} resolution: {integrity: sha512-0IYVGhsFTjcddpqcElzU7Mi4vmDLihCCTH5QgCCgWpNY1VKMXVoEpxmCmGjivtJKLzI6t5QIicsPBC93UWWN2g==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
rolldown: ^1.0.0-beta.0 rolldown: ^1.0.0-beta.0
@ -12717,8 +12717,8 @@ packages:
peerDependencies: peerDependencies:
rollup: ^3.0.0||^4.0.0 rollup: ^3.0.0||^4.0.0
rollup-plugin-webpack-stats@2.1.7: rollup-plugin-webpack-stats@2.1.8:
resolution: {integrity: sha512-hg+lhXu/lOp+k9SR7Xi47+s/YJyYnSSfE7h3iuqwDthaFwxYRw9cztd7HLXrwDgW18CC1L7n9ueZBr/Te2BWUw==} resolution: {integrity: sha512-agc1OE+QwG3sGeTSdruh16DkxPb6QkgR7I3gntPDFHMXsK1bR2ADHUVod1eoE+epAOqiv3idx/hcSqZAI3a1yg==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
rolldown: ^1.0.0-beta.0 rolldown: ^1.0.0-beta.0
@ -15754,8 +15754,6 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.2.0 '@ckeditor/ckeditor5-core': 47.2.0
'@ckeditor/ckeditor5-utils': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0
ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-code-block@47.2.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': '@ckeditor/ckeditor5-code-block@47.2.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
dependencies: dependencies:
@ -15989,8 +15987,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0
ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
es-toolkit: 1.39.5 es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-classic@47.2.0': '@ckeditor/ckeditor5-editor-classic@47.2.0':
dependencies: dependencies:
@ -16000,8 +15996,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0
ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
es-toolkit: 1.39.5 es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-decoupled@47.2.0': '@ckeditor/ckeditor5-editor-decoupled@47.2.0':
dependencies: dependencies:
@ -16071,8 +16065,6 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.2.0 '@ckeditor/ckeditor5-core': 47.2.0
'@ckeditor/ckeditor5-engine': 47.2.0 '@ckeditor/ckeditor5-engine': 47.2.0
'@ckeditor/ckeditor5-utils': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-essentials@47.2.0': '@ckeditor/ckeditor5-essentials@47.2.0':
dependencies: dependencies:
@ -16376,8 +16368,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0
'@ckeditor/ckeditor5-widget': 47.2.0 '@ckeditor/ckeditor5-widget': 47.2.0
ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-mention@47.2.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)': '@ckeditor/ckeditor5-mention@47.2.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)':
dependencies: dependencies:
@ -16532,8 +16522,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.2.0 '@ckeditor/ckeditor5-ui': 47.2.0
'@ckeditor/ckeditor5-utils': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0
ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-restricted-editing@47.2.0': '@ckeditor/ckeditor5-restricted-editing@47.2.0':
dependencies: dependencies:
@ -16731,6 +16719,8 @@ snapshots:
'@ckeditor/ckeditor5-icons': 47.2.0 '@ckeditor/ckeditor5-icons': 47.2.0
'@ckeditor/ckeditor5-ui': 47.2.0 '@ckeditor/ckeditor5-ui': 47.2.0
'@ckeditor/ckeditor5-utils': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-upload@47.2.0': '@ckeditor/ckeditor5-upload@47.2.0':
dependencies: dependencies:
@ -18679,9 +18669,9 @@ snapshots:
'@microsoft/tsdoc@0.15.1': {} '@microsoft/tsdoc@0.15.1': {}
'@mind-elixir/node-menu@5.0.1(mind-elixir@5.3.6)': '@mind-elixir/node-menu@5.0.1(mind-elixir@5.3.7)':
dependencies: dependencies:
mind-elixir: 5.3.6 mind-elixir: 5.3.7
'@mixmark-io/domino@2.2.0': {} '@mixmark-io/domino@2.2.0': {}
@ -27202,7 +27192,7 @@ snapshots:
mimic-response@3.1.0: {} mimic-response@3.1.0: {}
mind-elixir@5.3.6: {} mind-elixir@5.3.7: {}
mini-css-extract-plugin@2.4.7(webpack@5.101.3(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.27.0)): mini-css-extract-plugin@2.4.7(webpack@5.101.3(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.27.0)):
dependencies: dependencies:
@ -29666,7 +29656,7 @@ snapshots:
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.29 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.29
optional: true optional: true
rollup-plugin-stats@1.5.2(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): rollup-plugin-stats@1.5.3(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)):
optionalDependencies: optionalDependencies:
rolldown: 1.0.0-beta.29 rolldown: 1.0.0-beta.29
rollup: 4.52.0 rollup: 4.52.0
@ -29699,9 +29689,9 @@ snapshots:
'@rollup/pluginutils': 5.1.4(rollup@4.40.0) '@rollup/pluginutils': 5.1.4(rollup@4.40.0)
rollup: 4.40.0 rollup: 4.40.0
rollup-plugin-webpack-stats@2.1.7(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): rollup-plugin-webpack-stats@2.1.8(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)):
dependencies: dependencies:
rollup-plugin-stats: 1.5.2(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) rollup-plugin-stats: 1.5.3(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))
optionalDependencies: optionalDependencies:
rolldown: 1.0.0-beta.29 rolldown: 1.0.0-beta.29
rollup: 4.52.0 rollup: 4.52.0