mirror of
https://github.com/zadam/trilium.git
synced 2026-02-21 13:14:26 +01:00
346 lines
12 KiB
TypeScript
346 lines
12 KiB
TypeScript
import { BUILTIN_ATTRIBUTES } from "@triliumnext/commons";
|
|
import clsx from "clsx";
|
|
import { useEffect, useState } from "preact/hooks";
|
|
|
|
import FNote from "../../entities/fnote";
|
|
import attributes from "../../services/attributes";
|
|
import { t } from "../../services/i18n";
|
|
import { openInAppHelpFromUrl } from "../../services/utils";
|
|
import { BadgeWithDropdown } from "../react/Badge";
|
|
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList";
|
|
import FormToggle from "../react/FormToggle";
|
|
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent } from "../react/hooks";
|
|
import { BookProperty, ViewProperty } from "../react/NotePropertyMenu";
|
|
|
|
const NON_DANGEROUS_ACTIVE_CONTENT = [ "appCss", "appTheme" ];
|
|
const DANGEROUS_ATTRIBUTES = BUILTIN_ATTRIBUTES.filter(a => a.isDangerous || NON_DANGEROUS_ACTIVE_CONTENT.includes(a.name));
|
|
const activeContentLabels = [ "iconPack", "widget", "appCss", "appTheme" ] as const;
|
|
|
|
interface ActiveContentInfo {
|
|
type: "iconPack" | "backendScript" | "frontendScript" | "widget" | "appCss" | "renderNote" | "webView" | "appTheme";
|
|
isEnabled: boolean;
|
|
canToggleEnabled: boolean;
|
|
}
|
|
|
|
const typeMappings: Record<ActiveContentInfo["type"], {
|
|
icon: string;
|
|
helpPage: string;
|
|
apiDocsPage?: string;
|
|
isExecutable?: boolean;
|
|
additionalOptions?: BookProperty[];
|
|
}> = {
|
|
iconPack: {
|
|
icon: "bx bx-package",
|
|
helpPage: "g1mlRoU8CsqC",
|
|
},
|
|
backendScript: {
|
|
icon: "bx bx-server",
|
|
helpPage: "SPirpZypehBG",
|
|
apiDocsPage: "MEtfsqa5VwNi",
|
|
isExecutable: true,
|
|
},
|
|
frontendScript: {
|
|
icon: "bx bx-window",
|
|
helpPage: "yIhgI5H7A2Sm",
|
|
apiDocsPage: "Q2z6av6JZVWm",
|
|
isExecutable: true
|
|
},
|
|
widget: {
|
|
icon: "bx bxs-widget",
|
|
helpPage: "MgibgPcfeuGz"
|
|
},
|
|
appCss: {
|
|
icon: "bx bxs-file-css",
|
|
helpPage: "AlhDUqhENtH7"
|
|
},
|
|
renderNote: {
|
|
icon: "bx bx-extension",
|
|
helpPage: "HcABDtFCkbFN"
|
|
},
|
|
webView: {
|
|
icon: "bx bx-globe",
|
|
helpPage: "1vHRoWCEjj0L"
|
|
},
|
|
appTheme: {
|
|
icon: "bx bx-palette",
|
|
helpPage: "7NfNr5pZpVKV",
|
|
additionalOptions: [
|
|
{
|
|
type: "combobox",
|
|
bindToLabel: "appThemeBase",
|
|
label: t("active_content_badges.menu_theme_base"),
|
|
icon: "bx bx-layer",
|
|
dropStart: true,
|
|
options: [
|
|
{ label: t("theme.auto_theme"), value: "" },
|
|
{ type: "separator" },
|
|
{ label: t("theme.triliumnext"), value: "next" },
|
|
{ label: t("theme.triliumnext-light"), value: "next-light" },
|
|
{ label: t("theme.triliumnext-dark"), value: "next-dark" }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
export function ActiveContentBadges() {
|
|
const { note } = useNoteContext();
|
|
const info = useActiveContentInfo(note);
|
|
|
|
return (note && info &&
|
|
<>
|
|
{info.canToggleEnabled && <ActiveContentToggle info={info} note={note} />}
|
|
<ActiveContentBadge info={info} note={note} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
function ActiveContentBadge({ info, note }: { note: FNote, info: ActiveContentInfo }) {
|
|
const { icon, helpPage, apiDocsPage, isExecutable, additionalOptions } = typeMappings[info.type];
|
|
return (
|
|
<BadgeWithDropdown
|
|
className={clsx("active-content-badge", info.canToggleEnabled && !info.isEnabled && "disabled")}
|
|
icon={icon}
|
|
text={getTranslationForType(info.type)}
|
|
>
|
|
{isExecutable && (
|
|
<>
|
|
<FormListItem
|
|
icon="bx bx-play"
|
|
triggerCommand="runActiveNote"
|
|
>{t("active_content_badges.menu_execute_now")}</FormListItem>
|
|
<ScriptRunOptions note={note} info={info} />
|
|
<FormDropdownDivider />
|
|
</>
|
|
)}
|
|
|
|
{(info.type === "frontendScript" || info.type === "widget") && (
|
|
<>
|
|
<WidgetSwitcher note={note} />
|
|
<FormDropdownDivider />
|
|
</>
|
|
)}
|
|
|
|
{additionalOptions?.length && (
|
|
<>
|
|
{additionalOptions?.map((property, i) => (
|
|
<ViewProperty key={i} note={note} property={property} />
|
|
))}
|
|
<FormDropdownDivider />
|
|
</>
|
|
)}
|
|
|
|
<FormListItem
|
|
icon="bx bx-help-circle"
|
|
onClick={() => openInAppHelpFromUrl(helpPage)}
|
|
>{t("active_content_badges.menu_docs")}</FormListItem>
|
|
|
|
{apiDocsPage && <FormListItem
|
|
icon="bx bx-book-content"
|
|
onClick={() => openInAppHelpFromUrl(apiDocsPage)}
|
|
>{t("code_buttons.trilium_api_docs_button_title")}</FormListItem>}
|
|
</BadgeWithDropdown>
|
|
);
|
|
}
|
|
|
|
function ScriptRunOptions({ info, note }: { note: FNote, info: ActiveContentInfo }) {
|
|
const [ run, setRun ] = useNoteLabel(note, "run");
|
|
|
|
const options: {
|
|
title: string;
|
|
value: string | null;
|
|
type: "both" | "backendScript" | "frontendScript";
|
|
}[] = ([
|
|
{
|
|
title: t("active_content_badges.menu_run_disabled"),
|
|
value: null,
|
|
type: "both"
|
|
},
|
|
{
|
|
title: t("active_content_badges.menu_run_backend_startup"),
|
|
value: "backendStartup",
|
|
type: "backendScript"
|
|
},
|
|
{
|
|
title: t("active_content_badges.menu_run_daily"),
|
|
value: "daily",
|
|
type: "backendScript"
|
|
},
|
|
{
|
|
title: t("active_content_badges.menu_run_hourly"),
|
|
value: "hourly",
|
|
type: "backendScript"
|
|
},
|
|
{
|
|
title: t("active_content_badges.menu_run_frontend_startup"),
|
|
value: "frontendStartup",
|
|
type: "frontendScript"
|
|
},
|
|
{
|
|
title: t("active_content_badges.menu_run_mobile_startup"),
|
|
value: "mobileStartup",
|
|
type: "frontendScript"
|
|
}
|
|
] as const).filter(option => option.type === "both" || option.type === info.type);
|
|
|
|
return (
|
|
<FormDropdownSubmenu title={t("active_content_badges.menu_run")} icon="bx bx-rss" dropStart>
|
|
{options.map(({ title, value }) => (
|
|
<FormListItem
|
|
key={value}
|
|
onClick={() => setRun(value)}
|
|
checked={run ? run === value : value === null }
|
|
>{title}</FormListItem>
|
|
))}
|
|
</FormDropdownSubmenu>
|
|
);
|
|
}
|
|
|
|
function WidgetSwitcher({ note }: { note: FNote }) {
|
|
const [ widget, setWidget ] = useNoteLabelBoolean(note, "widget");
|
|
const [ disabledWidget, setDisabledWidget ] = useNoteLabelBoolean(note, "disabled:widget");
|
|
|
|
return (widget || disabledWidget)
|
|
? <FormListItem
|
|
icon="bx bx-window"
|
|
onClick={() => {
|
|
setWidget(false);
|
|
setDisabledWidget(false);
|
|
}}
|
|
>{t("active_content_badges.menu_change_to_frontend_script")}</FormListItem>
|
|
: <FormListItem
|
|
icon={widget ? "bx bx-window" : "bx bxs-widget"}
|
|
onClick={() => {
|
|
setWidget(true);
|
|
}}
|
|
>{t("active_content_badges.menu_change_to_widget")}</FormListItem>;
|
|
|
|
}
|
|
|
|
function getTranslationForType(type: ActiveContentInfo["type"]) {
|
|
switch (type) {
|
|
case "iconPack":
|
|
return t("active_content_badges.type_icon_pack");
|
|
case "backendScript":
|
|
return t("active_content_badges.type_backend_script");
|
|
case "frontendScript":
|
|
return t("active_content_badges.type_frontend_script");
|
|
case "widget":
|
|
return t("active_content_badges.type_widget");
|
|
case "appCss":
|
|
return t("active_content_badges.type_app_css");
|
|
case "renderNote":
|
|
return t("active_content_badges.type_render_note");
|
|
case "webView":
|
|
return t("active_content_badges.type_web_view");
|
|
case "appTheme":
|
|
return t("active_content_badges.type_app_theme");
|
|
}
|
|
}
|
|
|
|
function ActiveContentToggle({ note, info }: { note: FNote, info: ActiveContentInfo }) {
|
|
const typeTranslation = getTranslationForType(info.type);
|
|
|
|
return info && <FormToggle
|
|
switchOnName="" switchOffName=""
|
|
currentValue={info.isEnabled}
|
|
switchOffTooltip={t("active_content_badges.toggle_tooltip_disable_tooltip", { type: typeTranslation })}
|
|
switchOnTooltip={t("active_content_badges.toggle_tooltip_enable_tooltip", { type: typeTranslation })}
|
|
onChange={async (willEnable) => {
|
|
const attrs = note.getOwnedAttributes()
|
|
.filter(attr => {
|
|
if (attr.isInheritable) return false;
|
|
const baseName = getNameWithoutPrefix(attr.name);
|
|
return DANGEROUS_ATTRIBUTES.some(item => item.name === baseName && item.type === attr.type);
|
|
});
|
|
|
|
for (const attr of attrs) {
|
|
const baseName = getNameWithoutPrefix(attr.name);
|
|
const newName = willEnable ? baseName : `disabled:${baseName}`;
|
|
if (newName === attr.name) continue;
|
|
|
|
// We are adding and removing afterwards to avoid a flicker (because for a moment there would be no active content attribute anymore) because the operations are done in sequence and not atomically.
|
|
if (attr.type === "label") {
|
|
await attributes.setLabel(note.noteId, newName, attr.value);
|
|
} else {
|
|
await attributes.setRelation(note.noteId, newName, attr.value);
|
|
}
|
|
await attributes.removeAttributeById(note.noteId, attr.attributeId);
|
|
}
|
|
}}
|
|
/>;
|
|
}
|
|
|
|
function getNameWithoutPrefix(name: string) {
|
|
return name.startsWith("disabled:") ? name.substring(9) : name;
|
|
}
|
|
|
|
function useActiveContentInfo(note: FNote | null | undefined) {
|
|
const [ info, setInfo ] = useState<ActiveContentInfo | null>(null);
|
|
|
|
function refresh() {
|
|
let type: ActiveContentInfo["type"] | null = null;
|
|
let isEnabled = false;
|
|
let canToggleEnabled = false;
|
|
|
|
if (!note) {
|
|
setInfo(null);
|
|
return;
|
|
}
|
|
|
|
if (note.type === "render") {
|
|
type = "renderNote";
|
|
isEnabled = note.hasRelation("renderNote");
|
|
canToggleEnabled = note.hasRelation("renderNote") || note.hasRelation("disabled:renderNote");
|
|
} else if (note.type === "webView") {
|
|
type = "webView";
|
|
isEnabled = note.hasLabel("webViewSrc");
|
|
canToggleEnabled = note.hasLabelOrDisabled("webViewSrc");
|
|
} else if (note.type === "code" && note.mime === "application/javascript;env=backend") {
|
|
type = "backendScript";
|
|
for (const backendLabel of [ "run", "customRequestHandler", "customResourceProvider" ]) {
|
|
isEnabled ||= note.hasLabel(backendLabel);
|
|
|
|
if (!canToggleEnabled && note.hasLabelOrDisabled(backendLabel)) {
|
|
canToggleEnabled = true;
|
|
}
|
|
}
|
|
} else if (note.type === "code" && note.mime === "application/javascript;env=frontend") {
|
|
type = "frontendScript";
|
|
isEnabled = note.hasLabel("widget") || note.hasLabel("run");
|
|
canToggleEnabled = note.hasLabelOrDisabled("widget") || note.hasLabelOrDisabled("run");
|
|
} else if (note.type === "code" && note.hasLabelOrDisabled("appTheme")) {
|
|
isEnabled = note.hasLabel("appTheme");
|
|
canToggleEnabled = true;
|
|
}
|
|
|
|
for (const labelToCheck of activeContentLabels) {
|
|
if (note.hasLabel(labelToCheck)) {
|
|
type = labelToCheck;
|
|
break;
|
|
} else if (note.hasLabel(`disabled:${labelToCheck}`)) {
|
|
type = labelToCheck;
|
|
isEnabled = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (type) {
|
|
setInfo({ type, isEnabled, canToggleEnabled });
|
|
} else {
|
|
setInfo(null);
|
|
}
|
|
}
|
|
|
|
// Refresh on note change.
|
|
useEffect(refresh, [ note ]);
|
|
|
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
|
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
|
|
refresh();
|
|
}
|
|
});
|
|
|
|
return info;
|
|
}
|