mirror of
https://github.com/zadam/trilium.git
synced 2025-11-01 02:45:54 +01:00
feat(react/ribbon): port editability select
This commit is contained in:
@@ -1,120 +0,0 @@
|
|||||||
import attributeService from "../services/attributes.js";
|
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
|
||||||
import { t } from "../services/i18n.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
import type { EventData } from "../components/app_context.js";
|
|
||||||
import { Dropdown } from "bootstrap";
|
|
||||||
|
|
||||||
type Editability = "auto" | "readOnly" | "autoReadOnlyDisabled";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="dropdown editability-select-widget">
|
|
||||||
<style>
|
|
||||||
.editability-dropdown {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editability-dropdown .dropdown-item {
|
|
||||||
display: flex !importamt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editability-dropdown .dropdown-item > div {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editability-dropdown .description {
|
|
||||||
font-size: small;
|
|
||||||
color: var(--muted-text-color);
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm select-button dropdown-toggle editability-button">
|
|
||||||
<span class="editability-active-desc">${t("editability_select.auto")}</span>
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<div class="editability-dropdown dropdown-menu dropdown-menu-right tn-dropdown-list">
|
|
||||||
<a class="dropdown-item" href="#" data-editability="auto">
|
|
||||||
<span class="check">✓</span>
|
|
||||||
<div>
|
|
||||||
${t("editability_select.auto")}
|
|
||||||
<div class="description">${t("editability_select.note_is_editable")}</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="dropdown-item" href="#" data-editability="readOnly">
|
|
||||||
<span class="check">✓</span>
|
|
||||||
<div>
|
|
||||||
${t("editability_select.read_only")}
|
|
||||||
<div class="description">${t("editability_select.note_is_read_only")}</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="dropdown-item" href="#" data-editability="autoReadOnlyDisabled">
|
|
||||||
<span class="check">✓</span>
|
|
||||||
<div>
|
|
||||||
${t("editability_select.always_editable")}
|
|
||||||
<div class="description">${t("editability_select.note_is_always_editable")}</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default class EditabilitySelectWidget extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private dropdown!: Dropdown;
|
|
||||||
private $editabilityActiveDesc!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
|
|
||||||
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]);
|
|
||||||
|
|
||||||
this.$editabilityActiveDesc = this.$widget.find(".editability-active-desc");
|
|
||||||
|
|
||||||
this.$widget.on("click", ".dropdown-item", async (e) => {
|
|
||||||
this.dropdown.toggle();
|
|
||||||
|
|
||||||
const editability = $(e.target).closest("[data-editability]").attr("data-editability");
|
|
||||||
|
|
||||||
if (!this.note || !this.noteId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const ownedAttr of this.note.getOwnedLabels()) {
|
|
||||||
if (["readOnly", "autoReadOnlyDisabled"].includes(ownedAttr.name)) {
|
|
||||||
await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editability && editability !== "auto") {
|
|
||||||
await attributeService.addLabel(this.noteId, editability);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
|
||||||
let editability: Editability = "auto";
|
|
||||||
|
|
||||||
if (this.note?.isLabelTruthy("readOnly")) {
|
|
||||||
editability = "readOnly";
|
|
||||||
} else if (this.note?.isLabelTruthy("autoReadOnlyDisabled")) {
|
|
||||||
editability = "autoReadOnlyDisabled";
|
|
||||||
}
|
|
||||||
|
|
||||||
const labels = {
|
|
||||||
auto: t("editability_select.auto"),
|
|
||||||
readOnly: t("editability_select.read_only"),
|
|
||||||
autoReadOnlyDisabled: t("editability_select.always_editable")
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$widget.find(".dropdown-item").removeClass("selected");
|
|
||||||
this.$widget.find(`.dropdown-item[data-editability='${editability}']`).addClass("selected");
|
|
||||||
|
|
||||||
this.$editabilityActiveDesc.text(labels[editability]);
|
|
||||||
}
|
|
||||||
|
|
||||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
|
||||||
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
30
apps/client/src/widgets/react/FormDropdownList.tsx
Normal file
30
apps/client/src/widgets/react/FormDropdownList.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import Dropdown from "./Dropdown";
|
||||||
|
import { FormListItem } from "./FormList";
|
||||||
|
|
||||||
|
interface FormDropdownList<T> {
|
||||||
|
values: T[];
|
||||||
|
keyProperty: keyof T;
|
||||||
|
titleProperty: keyof T;
|
||||||
|
descriptionProperty?: keyof T;
|
||||||
|
currentValue: string;
|
||||||
|
onChange(newValue: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FormDropdownList<T>({ values, keyProperty, titleProperty, descriptionProperty, currentValue, onChange }: FormDropdownList<T>) {
|
||||||
|
const currentValueData = values.find(value => value[keyProperty] === currentValue);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown text={currentValueData?.[titleProperty] ?? ""}>
|
||||||
|
{values.map(item => (
|
||||||
|
<FormListItem
|
||||||
|
onClick={() => onChange(item[keyProperty] as string)}
|
||||||
|
checked={currentValue === item[keyProperty]}
|
||||||
|
description={descriptionProperty && item[descriptionProperty] as string}
|
||||||
|
selected={currentValue === item[keyProperty]}
|
||||||
|
>
|
||||||
|
{item[titleProperty] as string}
|
||||||
|
</FormListItem>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
5
apps/client/src/widgets/react/FormList.css
Normal file
5
apps/client/src/widgets/react/FormList.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.dropdown-item .description {
|
||||||
|
font-size: small;
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Dropdown as BootstrapDropdown } from "bootstrap";
|
|||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { useEffect, useMemo, useRef, type CSSProperties } from "preact/compat";
|
import { useEffect, useMemo, useRef, type CSSProperties } from "preact/compat";
|
||||||
|
import "./FormList.css";
|
||||||
|
|
||||||
interface FormListOpts {
|
interface FormListOpts {
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
@@ -76,27 +77,33 @@ interface FormListItemOpts {
|
|||||||
active?: boolean;
|
active?: boolean;
|
||||||
badges?: FormListBadge[];
|
badges?: FormListBadge[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
checked?: boolean;
|
checked?: boolean | null;
|
||||||
|
selected?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
description?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormListItem({ children, icon, value, title, active, badges, disabled, checked, onClick }: FormListItemOpts) {
|
export function FormListItem({ children, icon, value, title, active, badges, disabled, checked, onClick, description, selected }: FormListItemOpts) {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
icon = "bx bx-check";
|
icon = "bx bx-check";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
class={`dropdown-item ${active ? "active" : ""} ${disabled ? "disabled" : ""}`}
|
class={`dropdown-item ${active ? "active" : ""} ${disabled ? "disabled" : ""} ${selected ? "selected" : ""}`}
|
||||||
data-value={value} title={title}
|
data-value={value} title={title}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<Icon icon={icon} />
|
<Icon icon={icon} />
|
||||||
|
<div>
|
||||||
{children}
|
{children}
|
||||||
{badges && badges.map(({ className, text }) => (
|
{badges && badges.map(({ className, text }) => (
|
||||||
<span className={`badge ${className ?? ""}`}>{text}</span>
|
<span className={`badge ${className ?? ""}`}>{text}</span>
|
||||||
))}
|
))}
|
||||||
|
{description && <div className="description">{description}</div>}
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -314,12 +314,12 @@ export function useNoteProperty<T extends keyof FNote>(note: FNote | null | unde
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useNoteLabel(note: FNote | undefined | null, labelName: string): [string | null | undefined, (newValue: string) => void] {
|
export function useNoteLabel(note: FNote | undefined | null, labelName: string): [string | null | undefined, (newValue: string) => void] {
|
||||||
const [ labelValue, setNoteValue ] = useState<string | null | undefined>(note?.getLabelValue(labelName));
|
const [ labelValue, setLabelValue ] = useState<string | null | undefined>(note?.getLabelValue(labelName));
|
||||||
|
|
||||||
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
|
useTriliumEventBeta("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)) {
|
||||||
setNoteValue(attr.value ?? null);
|
setLabelValue(attr.value ?? null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -335,3 +335,27 @@ export function useNoteLabel(note: FNote | undefined | null, labelName: string):
|
|||||||
setter
|
setter
|
||||||
] as const;
|
] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: string): [ boolean | null | undefined, (newValue: boolean) => void] {
|
||||||
|
const [ labelValue, setLabelValue ] = useState<boolean | null | undefined>(note?.hasLabel(labelName));
|
||||||
|
|
||||||
|
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
for (const attr of loadResults.getAttributeRows()) {
|
||||||
|
if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) {
|
||||||
|
setLabelValue(!attr.isDeleted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setter = useCallback((value: boolean) => {
|
||||||
|
if (note) {
|
||||||
|
if (value) {
|
||||||
|
attributes.setLabel(note.noteId, labelName, "");
|
||||||
|
} else {
|
||||||
|
attributes.removeOwnedLabelByName(note, labelName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [note]);
|
||||||
|
|
||||||
|
return [ labelValue, setter ] as const;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import Dropdown from "../react/Dropdown";
|
|||||||
import { NOTE_TYPES } from "../../services/note_types";
|
import { NOTE_TYPES } from "../../services/note_types";
|
||||||
import { FormDivider, FormListBadge, FormListItem } from "../react/FormList";
|
import { FormDivider, FormListBadge, FormListItem } from "../react/FormList";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { useNoteContext, useNoteProperty, useTriliumOption } from "../react/hooks";
|
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumOption } from "../react/hooks";
|
||||||
import mime_types from "../../services/mime_types";
|
import mime_types from "../../services/mime_types";
|
||||||
import { NoteType } from "@triliumnext/commons";
|
import { NoteType } from "@triliumnext/commons";
|
||||||
import server from "../../services/server";
|
import server from "../../services/server";
|
||||||
@@ -11,6 +11,7 @@ import dialog from "../../services/dialog";
|
|||||||
import FormToggle from "../react/FormToggle";
|
import FormToggle from "../react/FormToggle";
|
||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
import protected_session from "../../services/protected_session";
|
import protected_session from "../../services/protected_session";
|
||||||
|
import FormDropdownList from "../react/FormDropdownList";
|
||||||
|
|
||||||
export default function BasicPropertiesTab() {
|
export default function BasicPropertiesTab() {
|
||||||
const { note } = useNoteContext();
|
const { note } = useNoteContext();
|
||||||
@@ -19,6 +20,7 @@ export default function BasicPropertiesTab() {
|
|||||||
<div className="basic-properties-widget">
|
<div className="basic-properties-widget">
|
||||||
<NoteTypeWidget note={note} />
|
<NoteTypeWidget note={note} />
|
||||||
<ProtectedNoteSwitch note={note} />
|
<ProtectedNoteSwitch note={note} />
|
||||||
|
<EditabilitySelect note={note} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -121,6 +123,45 @@ function ProtectedNoteSwitch({ note }: { note?: FNote | null }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EditabilitySelect({ note }: { note?: FNote | null }) {
|
||||||
|
const [ readOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||||
|
const [ autoReadOnlyDisabled, setAutoReadOnlyDisabled ] = useNoteLabelBoolean(note, "autoReadOnlyDisabled");
|
||||||
|
|
||||||
|
const options = useMemo(() => ([
|
||||||
|
{
|
||||||
|
value: "auto",
|
||||||
|
label: t("editability_select.auto"),
|
||||||
|
description: t("editability_select.note_is_editable"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "readOnly",
|
||||||
|
label: t("editability_select.read_only"),
|
||||||
|
description: t("editability_select.note_is_read_only")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "autoReadOnlyDisabled",
|
||||||
|
label: t("editability_select.always_editable"),
|
||||||
|
description: t("editability_select.note_is_always_editable")
|
||||||
|
}
|
||||||
|
]), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="editability-select-container">
|
||||||
|
<span>{t("basic_properties.editable")}:</span>
|
||||||
|
|
||||||
|
<FormDropdownList
|
||||||
|
values={options}
|
||||||
|
currentValue={ readOnly ? "readOnly" : autoReadOnlyDisabled ? "autoReadOnlyDisabled" : "auto" }
|
||||||
|
keyProperty="value" titleProperty="label" descriptionProperty="description"
|
||||||
|
onChange={(editability: string) => {
|
||||||
|
setReadOnly(editability === "readOnly");
|
||||||
|
setAutoReadOnlyDisabled(editability === "autoReadOnlyDisabled");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function findTypeTitle(type?: NoteType, mime?: string | null) {
|
function findTypeTitle(type?: NoteType, mime?: string | null) {
|
||||||
if (type === "code") {
|
if (type === "code") {
|
||||||
const mimeTypes = mime_types.getMimeTypes();
|
const mimeTypes = mime_types.getMimeTypes();
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ import type FNote from "../../entities/fnote.js";
|
|||||||
import NoteLanguageWidget from "../note_language.js";
|
import NoteLanguageWidget from "../note_language.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="editability-select-container">
|
|
||||||
<span>${t("basic_properties.editable")}:</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bookmark-switch-container"></div>
|
<div class="bookmark-switch-container"></div>
|
||||||
|
|
||||||
<div class="shared-switch-container"></div>
|
<div class="shared-switch-container"></div>
|
||||||
@@ -27,8 +23,6 @@ const TPL = /*html*/`
|
|||||||
|
|
||||||
export default class BasicPropertiesWidget extends NoteContextAwareWidget {
|
export default class BasicPropertiesWidget extends NoteContextAwareWidget {
|
||||||
|
|
||||||
private noteTypeWidget: NoteTypeWidget;
|
|
||||||
private protectedNoteSwitchWidget: ProtectedNoteSwitchWidget;
|
|
||||||
private editabilitySelectWidget: EditabilitySelectWidget;
|
private editabilitySelectWidget: EditabilitySelectWidget;
|
||||||
private bookmarkSwitchWidget: BookmarkSwitchWidget;
|
private bookmarkSwitchWidget: BookmarkSwitchWidget;
|
||||||
private sharedSwitchWidget: SharedSwitchWidget;
|
private sharedSwitchWidget: SharedSwitchWidget;
|
||||||
@@ -45,8 +39,6 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget {
|
|||||||
this.noteLanguageWidget = new NoteLanguageWidget().contentSized();
|
this.noteLanguageWidget = new NoteLanguageWidget().contentSized();
|
||||||
|
|
||||||
this.child(
|
this.child(
|
||||||
this.noteTypeWidget,
|
|
||||||
this.protectedNoteSwitchWidget,
|
|
||||||
this.editabilitySelectWidget,
|
this.editabilitySelectWidget,
|
||||||
this.bookmarkSwitchWidget,
|
this.bookmarkSwitchWidget,
|
||||||
this.sharedSwitchWidget,
|
this.sharedSwitchWidget,
|
||||||
|
|||||||
Reference in New Issue
Block a user