mirror of
https://github.com/zadam/trilium.git
synced 2025-11-13 16:55:50 +01:00
Compare commits
2 Commits
main
...
react/prom
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54c8322960 | ||
|
|
3d0d1fa36e |
@@ -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" />)
|
||||||
|
|||||||
91
apps/client/src/widgets/PromotedAttributes.css
Normal file
91
apps/client/src/widgets/PromotedAttributes.css
Normal 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;
|
||||||
|
}
|
||||||
120
apps/client/src/widgets/PromotedAttributes.tsx
Normal file
120
apps/client/src/widgets/PromotedAttributes.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user