Compare commits

...

2 Commits

Author SHA1 Message Date
Elian Doran
54c8322960 chore(react/promoted_attributes): multiplicity 2025-11-13 08:17:32 +02:00
Elian Doran
3d0d1fa36e chore(react/promoted_attributes): basic structures 2025-11-12 10:21:28 +02:00
4 changed files with 215 additions and 200 deletions

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";
@@ -44,6 +43,7 @@ import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import utils from "../services/utils.js"; 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 PromotedAttributes from "../widgets/PromotedAttributes.jsx";
export default class DesktopLayout { export default class DesktopLayout {
@@ -140,7 +140,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

@@ -0,0 +1,91 @@
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;
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;
}

View File

@@ -0,0 +1,120 @@
import { useEffect, useState } from "preact/hooks";
import "./PromotedAttributes.css";
import { useNoteContext, useNoteLabel } from "./react/hooks";
import { Attribute } from "../services/attribute_parser";
import { ComponentChild } from "preact";
import FAttribute from "../entities/fattribute";
import { t } from "../services/i18n";
import ActionButton from "./react/ActionButton";
export default function PromotedAttributes() {
const { note } = useNoteContext();
const [ promotedAttributes, setPromotedAttributes ] = useState<ComponentChild[]>();
const [ viewType ] = useNoteLabel(note, "viewType");
useEffect(() => {
if (!note) {
setPromotedAttributes([]);
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);
let promotedAttributes: ComponentChild[] = [];
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) {
promotedAttributes.push(<PromotedAttributeCell
noteId={note.noteId}
definitionAttr={definitionAttr}
valueAttr={valueAttr} valueName={valueName} />)
}
}
setPromotedAttributes(promotedAttributes);
console.log("Got ", promotedAttributes);
}, [ note ]);
return (
<div className="promoted-attributes-widget">
{viewType !== "table" && (
<div className="promoted-attributes-container">
{promotedAttributes}
</div>
)}
</div>
);
}
function PromotedAttributeCell({ noteId, definitionAttr, valueAttr, valueName }: {
noteId: string;
definitionAttr: FAttribute;
valueAttr: Attribute;
valueName: string;
}) {
const definition = definitionAttr.getDefinition();
const id = `value-${valueAttr.attributeId}`;
return (
<div className="promoted-attribute-cell">
<label
for={id}
>{definition.promotedAlias ?? valueName}</label>
<div className="input-group">
<input
className="form-control promoted-attribute-input"
tabindex={200 + definitionAttr.position}
id={id}
// if not owned, we'll force creation of a new attribute instead of updating the inherited one
data-attribute-id={valueAttr.noteId === noteId ? valueAttr.attributeId ?? "" : ""}
data-attribute-type={valueAttr.type}
data-attribute-name={valueAttr.name}
value={valueAttr.value}
placeholder={t("promoted_attributes.unset-field-placeholder")}
/>
</div>
<div />
{definition.multiplicity === "multi" && (
<td className="multiplicity">
<ActionButton
icon="bx bx-plus"
className="pointer tn-tool-button"
text={t("promoted_attributes.add_new_attribute")}
noIconActionClass
/>
<ActionButton
icon="bx bx-trash"
className="pointer tn-tool-button"
text={t("promoted_attributes.remove_this_attribute")}
noIconActionClass
/>
</td>
)}
</div>
)
}

View File

@@ -12,102 +12,6 @@ import type { Attribute } from "../services/attribute_parser.js";
import type FAttribute from "../entities/fattribute.js"; import type FAttribute from "../entities/fattribute.js";
import type { EventData } from "../components/app_context.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 // TODO: Deduplicate
interface AttributeResult { interface AttributeResult {
attributeId: string; attributeId: string;
@@ -117,115 +21,17 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
private $container!: JQuery<HTMLElement>; private $container!: JQuery<HTMLElement>;
get name() {
return "promotedAttributes";
}
get toggleCommand() {
return "toggleRibbonTabPromotedAttributes";
}
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $("");
this.contentSized(); this.contentSized();
this.$container = this.$widget.find(".promoted-attributes-container"); 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) { async createPromotedAttributeCell(definitionAttr: FAttribute, valueAttr: Attribute, valueName: string) {
const definition = definitionAttr.getDefinition(); // .on("change", (event) => this.promotedAttributeChanged(event));
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 $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") { if (valueAttr.type === "label") {
$wrapper.addClass(`promoted-attribute-label-${definition.labelType}`); $wrapper.addClass(`promoted-attribute-label-${definition.labelType}`);
if (definition.labelType === "text") { if (definition.labelType === "text") {
@@ -359,8 +165,6 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
if (definition.multiplicity === "multi") { if (definition.multiplicity === "multi") {
const $addButton = $("<span>") const $addButton = $("<span>")
.addClass("bx bx-plus pointer tn-tool-button")
.prop("title", t("promoted_attributes.add_new_attribute"))
.on("click", async () => { .on("click", async () => {
const $new = await this.createPromotedAttributeCell( const $new = await this.createPromotedAttributeCell(
definitionAttr, definitionAttr,