mirror of
https://github.com/zadam/trilium.git
synced 2025-10-26 07:46:30 +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 Icon from "./Icon";
|
||||
import { useEffect, useMemo, useRef, type CSSProperties } from "preact/compat";
|
||||
import "./FormList.css";
|
||||
|
||||
interface FormListOpts {
|
||||
children: ComponentChildren;
|
||||
@@ -76,27 +77,33 @@ interface FormListItemOpts {
|
||||
active?: boolean;
|
||||
badges?: FormListBadge[];
|
||||
disabled?: boolean;
|
||||
checked?: boolean;
|
||||
checked?: boolean | null;
|
||||
selected?: boolean;
|
||||
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) {
|
||||
icon = "bx bx-check";
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
class={`dropdown-item ${active ? "active" : ""} ${disabled ? "disabled" : ""}`}
|
||||
class={`dropdown-item ${active ? "active" : ""} ${disabled ? "disabled" : ""} ${selected ? "selected" : ""}`}
|
||||
data-value={value} title={title}
|
||||
tabIndex={0}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon icon={icon} />
|
||||
{children}
|
||||
{badges && badges.map(({ className, text }) => (
|
||||
<span className={`badge ${className ?? ""}`}>{text}</span>
|
||||
))}
|
||||
<div>
|
||||
{children}
|
||||
{badges && badges.map(({ className, text }) => (
|
||||
<span className={`badge ${className ?? ""}`}>{text}</span>
|
||||
))}
|
||||
{description && <div className="description">{description}</div>}
|
||||
</div>
|
||||
</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] {
|
||||
const [ labelValue, setNoteValue ] = useState<string | null | undefined>(note?.getLabelValue(labelName));
|
||||
const [ labelValue, setLabelValue ] = useState<string | null | undefined>(note?.getLabelValue(labelName));
|
||||
|
||||
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
|
||||
for (const attr of loadResults.getAttributeRows()) {
|
||||
if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) {
|
||||
setNoteValue(attr.value ?? null);
|
||||
setLabelValue(attr.value ?? null);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -334,4 +334,28 @@ export function useNoteLabel(note: FNote | undefined | null, labelName: string):
|
||||
labelValue,
|
||||
setter
|
||||
] 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 { FormDivider, FormListBadge, FormListItem } from "../react/FormList";
|
||||
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 { NoteType } from "@triliumnext/commons";
|
||||
import server from "../../services/server";
|
||||
@@ -11,6 +11,7 @@ import dialog from "../../services/dialog";
|
||||
import FormToggle from "../react/FormToggle";
|
||||
import FNote from "../../entities/fnote";
|
||||
import protected_session from "../../services/protected_session";
|
||||
import FormDropdownList from "../react/FormDropdownList";
|
||||
|
||||
export default function BasicPropertiesTab() {
|
||||
const { note } = useNoteContext();
|
||||
@@ -19,6 +20,7 @@ export default function BasicPropertiesTab() {
|
||||
<div className="basic-properties-widget">
|
||||
<NoteTypeWidget note={note} />
|
||||
<ProtectedNoteSwitch note={note} />
|
||||
<EditabilitySelect note={note} />
|
||||
</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) {
|
||||
if (type === "code") {
|
||||
const mimeTypes = mime_types.getMimeTypes();
|
||||
|
||||
@@ -10,10 +10,6 @@ import type FNote from "../../entities/fnote.js";
|
||||
import NoteLanguageWidget from "../note_language.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="editability-select-container">
|
||||
<span>${t("basic_properties.editable")}:</span>
|
||||
</div>
|
||||
|
||||
<div class="bookmark-switch-container"></div>
|
||||
|
||||
<div class="shared-switch-container"></div>
|
||||
@@ -27,8 +23,6 @@ const TPL = /*html*/`
|
||||
|
||||
export default class BasicPropertiesWidget extends NoteContextAwareWidget {
|
||||
|
||||
private noteTypeWidget: NoteTypeWidget;
|
||||
private protectedNoteSwitchWidget: ProtectedNoteSwitchWidget;
|
||||
private editabilitySelectWidget: EditabilitySelectWidget;
|
||||
private bookmarkSwitchWidget: BookmarkSwitchWidget;
|
||||
private sharedSwitchWidget: SharedSwitchWidget;
|
||||
@@ -45,8 +39,6 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget {
|
||||
this.noteLanguageWidget = new NoteLanguageWidget().contentSized();
|
||||
|
||||
this.child(
|
||||
this.noteTypeWidget,
|
||||
this.protectedNoteSwitchWidget,
|
||||
this.editabilitySelectWidget,
|
||||
this.bookmarkSwitchWidget,
|
||||
this.sharedSwitchWidget,
|
||||
|
||||
Reference in New Issue
Block a user