mirror of
https://github.com/zadam/trilium.git
synced 2026-02-17 03:47:00 +01:00
UX improvements for web view & render note (#8725)
This commit is contained in:
@@ -168,6 +168,49 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles whether a dangerous attribute is enabled or not. When an attribute is disabled, its name is prefixed with `disabled:`.
|
||||
*
|
||||
* Note that this work for non-dangerous attributes as well.
|
||||
*
|
||||
* If there are multiple attributes with the same name, all of them will be toggled at the same time.
|
||||
*
|
||||
* @param note the note whose attribute to change.
|
||||
* @param type the type of dangerous attribute (label or relation).
|
||||
* @param name the name of the dangerous attribute.
|
||||
* @param willEnable whether to enable or disable the attribute.
|
||||
* @returns a promise that will resolve when the request to the server completes.
|
||||
*/
|
||||
async function toggleDangerousAttribute(note: FNote, type: "label" | "relation", name: string, willEnable: boolean) {
|
||||
const attrs = [
|
||||
...note.getOwnedAttributes(type, name),
|
||||
...note.getOwnedAttributes(type, `disabled:${name}`)
|
||||
];
|
||||
|
||||
for (const attr of attrs) {
|
||||
const baseName = getNameWithoutDangerousPrefix(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 setLabel(note.noteId, newName, attr.value);
|
||||
} else {
|
||||
await setRelation(note.noteId, newName, attr.value);
|
||||
}
|
||||
await removeAttributeById(note.noteId, attr.attributeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of an attribute without the `disabled:` prefix, or the same name if it's not disabled.
|
||||
* @param name the name of an attribute.
|
||||
* @returns the name without the `disabled:` prefix.
|
||||
*/
|
||||
function getNameWithoutDangerousPrefix(name: string) {
|
||||
return name.startsWith("disabled:") ? name.substring(9) : name;
|
||||
}
|
||||
|
||||
export default {
|
||||
addLabel,
|
||||
setLabel,
|
||||
@@ -177,5 +220,7 @@ export default {
|
||||
removeAttributeById,
|
||||
removeOwnedLabelByName,
|
||||
removeOwnedRelationByName,
|
||||
isAffecting
|
||||
isAffecting,
|
||||
toggleDangerousAttribute,
|
||||
getNameWithoutDangerousPrefix
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import bundleService, { type Bundle } from "./bundle.js";
|
||||
import froca from "./froca.js";
|
||||
import server from "./server.js";
|
||||
|
||||
async function render(note: FNote, $el: JQuery<HTMLElement>) {
|
||||
async function render(note: FNote, $el: JQuery<HTMLElement>, onError?: (e: unknown) => void) {
|
||||
const relations = note.getRelations("renderNote");
|
||||
const renderNoteIds = relations.map((rel) => rel.value).filter((noteId) => noteId);
|
||||
|
||||
@@ -21,12 +21,14 @@ async function render(note: FNote, $el: JQuery<HTMLElement>) {
|
||||
$scriptContainer.append(bundle.html);
|
||||
|
||||
// async so that scripts cannot block trilium execution
|
||||
bundleService.executeBundle(bundle, note, $scriptContainer).then(result => {
|
||||
// Render JSX
|
||||
if (bundle.html === "") {
|
||||
renderIfJsx(bundle, result, $el);
|
||||
}
|
||||
});
|
||||
bundleService.executeBundle(bundle, note, $scriptContainer)
|
||||
.catch(onError)
|
||||
.then(result => {
|
||||
// Render JSX
|
||||
if (bundle.html === "") {
|
||||
renderIfJsx(bundle, result, $el).catch(onError);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return renderNoteIds.length > 0;
|
||||
|
||||
@@ -838,7 +838,7 @@ input[type="range"] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tn-centered-form input,
|
||||
.tn-centered-form .input-group,
|
||||
.tn-centered-form button {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1067,15 +1067,21 @@
|
||||
"click_on_canvas_to_place_new_note": "Click on canvas to place new note"
|
||||
},
|
||||
"render": {
|
||||
"note_detail_render_help_1": "This help note is shown because this note of type Render HTML doesn't have required relation to function properly.",
|
||||
"note_detail_render_help_2": "Render HTML note type is used for <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. In short, you have a HTML code note (optionally with some JavaScript) and this note will render it. To make it work, you need to define a <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relation</a> called \"renderNote\" pointing to the HTML note to render."
|
||||
"setup_title": "Display custom HTML or Preact JSX inside this note",
|
||||
"setup_create_sample_preact": "Create sample note with Preact",
|
||||
"setup_create_sample_html": "Create sample note with HTML",
|
||||
"setup_sample_created": "A sample note was created as a child note.",
|
||||
"disabled_description": "This render notes comes from an external source. To protect you from malicious content, it is not enabled by default. Make sure you trust the source before enabling it.",
|
||||
"disabled_button_enable": "Enable render note"
|
||||
},
|
||||
"web_view_setup": {
|
||||
"title": "Create a live view of a webpage directly into Trilium",
|
||||
"url_placeholder": "Enter or paste the website address, for example https://triliumnotes.org",
|
||||
"create_button": "Create Web View",
|
||||
"invalid_url_title": "Invalid address",
|
||||
"invalid_url_message": "Insert a valid web address, for example https://triliumnotes.org."
|
||||
"invalid_url_message": "Insert a valid web address, for example https://triliumnotes.org.",
|
||||
"disabled_description": "This web view was imported from an external source. To help protect you from phishing or malicious content, it isn’t loading automatically. You can enable it if you trust the source.",
|
||||
"disabled_button_enable": "Enable web view"
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Refresh"
|
||||
@@ -2312,5 +2318,8 @@
|
||||
"menu_change_to_widget": "Change to widget",
|
||||
"menu_change_to_frontend_script": "Change to frontend script",
|
||||
"menu_theme_base": "Theme base"
|
||||
},
|
||||
"setup_form": {
|
||||
"more_info": "Learn more"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,34 +203,14 @@ function ActiveContentToggle({ note, info }: { note: FNote, info: ActiveContentI
|
||||
switchOffTooltip={t("active_content_badges.toggle_tooltip_disable_tooltip", { type: title })}
|
||||
switchOnTooltip={t("active_content_badges.toggle_tooltip_enable_tooltip", { type: title })}
|
||||
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);
|
||||
}
|
||||
await Promise.all(note.getOwnedAttributes()
|
||||
.map(attr => ({ name: attributes.getNameWithoutDangerousPrefix(attr.name), type: attr.type }))
|
||||
.filter(({ name, type }) => DANGEROUS_ATTRIBUTES.some(item => item.name === name && item.type === type))
|
||||
.map(({ name, type }) => attributes.toggleDangerousAttribute(note, type, name, willEnable)));
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -247,11 +227,9 @@ function useActiveContentInfo(note: FNote | null | undefined) {
|
||||
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" ]) {
|
||||
|
||||
@@ -551,7 +551,12 @@ export function useNoteRelation(note: FNote | undefined | null, relationName: Re
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
for (const attr of loadResults.getAttributeRows()) {
|
||||
if (attr.type === "relation" && attr.name === relationName && attributes.isAffecting(attr, note)) {
|
||||
setRelationValue(attr.value ?? null);
|
||||
if (!attr.isDeleted) {
|
||||
setRelationValue(attr.value ?? null);
|
||||
} else {
|
||||
setRelationValue(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -601,6 +606,7 @@ export function useNoteLabel(note: FNote | undefined | null, labelName: FilterLa
|
||||
} else {
|
||||
setLabelValue(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
.note-detail-render {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.note-detail-render .note-detail-render-help {
|
||||
margin: 50px;
|
||||
padding: 20px;
|
||||
}
|
||||
&>.admonition {
|
||||
margin: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,59 @@ import "./Render.css";
|
||||
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import note_create from "../../services/note_create";
|
||||
import render from "../../services/render";
|
||||
import Alert from "../react/Alert";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import toast from "../../services/toast";
|
||||
import { getErrorMessage } from "../../services/utils";
|
||||
import Admonition from "../react/Admonition";
|
||||
import Button, { SplitButton } from "../react/Button";
|
||||
import FormGroup from "../react/FormGroup";
|
||||
import { FormListItem } from "../react/FormList";
|
||||
import { useNoteRelation, useTriliumEvent } from "../react/hooks";
|
||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import SetupForm from "./helpers/SetupForm";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
export default function Render({ note, noteContext, ntxId }: TypeWidgetProps) {
|
||||
const HELP_PAGE = "HcABDtFCkbFN";
|
||||
|
||||
const PREACT_SAMPLE = /*js*/`\
|
||||
export default function() {
|
||||
return <p>Hello world.</p>;
|
||||
}
|
||||
`;
|
||||
|
||||
const HTML_SAMPLE = /*html*/`\
|
||||
<p>Hello world.</p>
|
||||
`;
|
||||
|
||||
export default function Render(props: TypeWidgetProps) {
|
||||
const { note } = props;
|
||||
const [ renderNote ] = useNoteRelation(note, "renderNote");
|
||||
const [ disabledRenderNote ] = useNoteRelation(note, "disabled:renderNote");
|
||||
|
||||
if (disabledRenderNote) {
|
||||
return <DisabledRender {...props} />;
|
||||
}
|
||||
|
||||
if (!renderNote) {
|
||||
return <SetupRenderContent {...props} />;
|
||||
}
|
||||
|
||||
return <RenderContent {...props} />;
|
||||
}
|
||||
|
||||
function RenderContent({ note, noteContext, ntxId }: TypeWidgetProps) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [ renderNotesFound, setRenderNotesFound ] = useState(false);
|
||||
const [ error, setError ] = useState<unknown | null>(null);
|
||||
|
||||
function refresh() {
|
||||
if (!contentRef) return;
|
||||
render.render(note, refToJQuerySelector(contentRef)).then(setRenderNotesFound);
|
||||
setError(null);
|
||||
render.render(note, refToJQuerySelector(contentRef), setError);
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note ]);
|
||||
@@ -49,14 +86,72 @@ export default function Render({ note, noteContext, ntxId }: TypeWidgetProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{!renderNotesFound && (
|
||||
<Alert className="note-detail-render-help" type="warning">
|
||||
<p><strong>{t("render.note_detail_render_help_1")}</strong></p>
|
||||
<p><RawHtml html={t("render.note_detail_render_help_2")} /></p>
|
||||
</Alert>
|
||||
{error && (
|
||||
<Admonition type="caution">
|
||||
{getErrorMessage(error)}
|
||||
</Admonition>
|
||||
)}
|
||||
|
||||
<div ref={contentRef} className="note-detail-render-content" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DisabledRender({ note }: TypeWidgetProps) {
|
||||
return (
|
||||
<SetupForm
|
||||
icon="bx bx-extension"
|
||||
inAppHelpPage={HELP_PAGE}
|
||||
>
|
||||
<p>{t("render.disabled_description")}</p>
|
||||
<Button
|
||||
text={t("render.disabled_button_enable")}
|
||||
icon="bx bx-check-shield"
|
||||
onClick={() => attributes.toggleDangerousAttribute(note, "relation", "renderNote", true)}
|
||||
primary
|
||||
/>
|
||||
</SetupForm>
|
||||
);
|
||||
}
|
||||
|
||||
function SetupRenderContent({ note }: TypeWidgetProps) {
|
||||
return (
|
||||
<SetupForm
|
||||
icon="bx bx-extension"
|
||||
inAppHelpPage={HELP_PAGE}
|
||||
>
|
||||
<FormGroup name="render-target-note" label={t("render.setup_title")}>
|
||||
<NoteAutocomplete noteIdChanged={noteId => {
|
||||
if (!noteId) return;
|
||||
setRenderNote(note, noteId);
|
||||
}} />
|
||||
</FormGroup>
|
||||
|
||||
<SplitButton
|
||||
text={t("render.setup_create_sample_preact")}
|
||||
icon="bx bxl-react"
|
||||
onClick={() => setupSampleNote(note, "text/jsx", PREACT_SAMPLE)}
|
||||
>
|
||||
<FormListItem
|
||||
icon="bx bxl-html5"
|
||||
onClick={() => setupSampleNote(note, "text/html", HTML_SAMPLE)}
|
||||
>{t("render.setup_create_sample_html")}</FormListItem>
|
||||
</SplitButton>
|
||||
</SetupForm>
|
||||
);
|
||||
}
|
||||
|
||||
async function setRenderNote(note: FNote, targetNoteUrl: string) {
|
||||
await attributes.setRelation(note.noteId, "renderNote", targetNoteUrl);
|
||||
}
|
||||
|
||||
async function setupSampleNote(parentNote: FNote, mime: string, content: string) {
|
||||
const { note: codeNote } = await note_create.createNote(parentNote.noteId, {
|
||||
type: "code",
|
||||
mime,
|
||||
content,
|
||||
activate: false
|
||||
});
|
||||
if (!codeNote) return;
|
||||
await setRenderNote(parentNote, codeNote.noteId);
|
||||
toast.showMessage(t("render.setup_sample_created"));
|
||||
}
|
||||
|
||||
@@ -16,20 +16,3 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.web-view-setup-form {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-inline: 40px;
|
||||
|
||||
.form-icon {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,103 @@
|
||||
import { useCallback, useState } from "preact/hooks";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import utils from "../../services/utils";
|
||||
import { useNoteLabel } from "../react/hooks";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
import "./WebView.css";
|
||||
import FormGroup from "../react/FormGroup";
|
||||
|
||||
import { useCallback, useState } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import toast from "../../services/toast";
|
||||
import utils from "../../services/utils";
|
||||
import Button from "../react/Button";
|
||||
import FormGroup from "../react/FormGroup";
|
||||
import FormTextBox from "../react/FormTextBox";
|
||||
import { useNoteLabel } from "../react/hooks";
|
||||
import SetupForm from "./helpers/SetupForm";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
const isElectron = utils.isElectron();
|
||||
const HELP_PAGE = "1vHRoWCEjj0L";
|
||||
|
||||
export default function WebView({ note }: TypeWidgetProps) {
|
||||
const [ webViewSrc ] = useNoteLabel(note, "webViewSrc");
|
||||
const [ disabledWebViewSrc ] = useNoteLabel(note, "disabled:webViewSrc");
|
||||
|
||||
return (webViewSrc
|
||||
? <WebViewContent src={webViewSrc} />
|
||||
: <SetupWebView note={note} />
|
||||
);
|
||||
if (disabledWebViewSrc) {
|
||||
return <DisabledWebView note={note} url={disabledWebViewSrc} />;
|
||||
}
|
||||
|
||||
if (!webViewSrc) {
|
||||
return <SetupWebView note={note} />;
|
||||
}
|
||||
|
||||
return <WebViewContent src={webViewSrc} />;
|
||||
}
|
||||
|
||||
function WebViewContent({ src }: { src: string }) {
|
||||
if (!isElectron) {
|
||||
return <iframe src={src} class="note-detail-web-view-content" sandbox="allow-same-origin allow-scripts allow-popups" />
|
||||
} else {
|
||||
return <webview src={src} class="note-detail-web-view-content" />
|
||||
return <iframe src={src} class="note-detail-web-view-content" sandbox="allow-same-origin allow-scripts allow-popups" />;
|
||||
}
|
||||
return <webview src={src} class="note-detail-web-view-content" />;
|
||||
|
||||
}
|
||||
|
||||
function SetupWebView({note}: {note: FNote}) {
|
||||
const [srcLabel, setSrcLabel] = useNoteLabel(note, "webViewSrc");
|
||||
const [src, setSrc] = useState("");
|
||||
const [ , setSrcLabel] = useNoteLabel(note, "webViewSrc");
|
||||
const [ src, setSrc ] = useState("");
|
||||
|
||||
const submit = useCallback((url: string) => {
|
||||
try {
|
||||
// Validate URL
|
||||
new URL(url);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (ex) {
|
||||
toast.showErrorTitleAndMessage(t("web_view_setup.invalid_url_title"),
|
||||
t("web_view_setup.invalid_url_message"));
|
||||
t("web_view_setup.invalid_url_message"));
|
||||
return;
|
||||
}
|
||||
|
||||
setSrcLabel(url);
|
||||
}, [note]);
|
||||
}, [ setSrcLabel ]);
|
||||
|
||||
return <div class="web-view-setup-form">
|
||||
<form class="tn-centered-form" onSubmit={() => submit(src)}>
|
||||
<span className="bx bx-globe-alt form-icon" />
|
||||
|
||||
<FormGroup name="web-view-src-detail" label={t("web_view_setup.title")}>
|
||||
<input className="form-control"
|
||||
type="text"
|
||||
value={src}
|
||||
placeholder={t("web_view_setup.url_placeholder")}
|
||||
onChange={(e) => {setSrc((e.target as HTMLInputElement)?.value)}}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<Button
|
||||
text={t("web_view_setup.create_button")}
|
||||
primary
|
||||
keyboardShortcut="Enter"
|
||||
return (
|
||||
<SetupForm
|
||||
icon="bx bx-globe-alt" inAppHelpPage={HELP_PAGE}
|
||||
onSubmit={() => submit(src)}
|
||||
>
|
||||
<FormGroup name="web-view-src-detail" label={t("web_view_setup.title")}>
|
||||
<input className="form-control"
|
||||
type="text"
|
||||
value={src}
|
||||
placeholder={t("web_view_setup.url_placeholder")}
|
||||
onChange={(e) => {setSrc((e.target as HTMLInputElement)?.value);}}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</FormGroup>
|
||||
|
||||
<Button
|
||||
text={t("web_view_setup.create_button")}
|
||||
primary
|
||||
keyboardShortcut="Enter"
|
||||
/>
|
||||
</SetupForm>
|
||||
);
|
||||
}
|
||||
|
||||
function DisabledWebView({ note, url }: { note: FNote, url: string }) {
|
||||
return (
|
||||
<SetupForm icon="bx bx-globe-alt" inAppHelpPage={HELP_PAGE}>
|
||||
<FormGroup name="web-view-src-detail" label={t("web_view_setup.disabled_description")}>
|
||||
<FormTextBox
|
||||
type="url"
|
||||
currentValue={url}
|
||||
disabled
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<Button
|
||||
text={t("web_view_setup.disabled_button_enable")}
|
||||
icon="bx bx-check-shield"
|
||||
onClick={() => attributes.toggleDangerousAttribute(note, "label", "webViewSrc", true)}
|
||||
primary
|
||||
/>
|
||||
</SetupForm>
|
||||
);
|
||||
}
|
||||
|
||||
22
apps/client/src/widgets/type_widgets/helpers/SetupForm.css
Normal file
22
apps/client/src/widgets/type_widgets/helpers/SetupForm.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.setup-form {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-inline: 40px;
|
||||
|
||||
.form-icon {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.tn-link {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
34
apps/client/src/widgets/type_widgets/helpers/SetupForm.tsx
Normal file
34
apps/client/src/widgets/type_widgets/helpers/SetupForm.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import "./SetupForm.css";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { ComponentChildren } from "preact";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import { openInAppHelpFromUrl } from "../../../services/utils";
|
||||
import LinkButton from "../../react/LinkButton";
|
||||
|
||||
interface SetupFormProps {
|
||||
icon: string;
|
||||
onSubmit?: () => void;
|
||||
children: ComponentChildren;
|
||||
inAppHelpPage?: string;
|
||||
}
|
||||
|
||||
export default function SetupForm({ icon, children, onSubmit, inAppHelpPage }: SetupFormProps) {
|
||||
return (
|
||||
<div class="setup-form">
|
||||
<form class="tn-centered-form" onSubmit={onSubmit}>
|
||||
<span className={clsx(icon, "form-icon")} />
|
||||
|
||||
{children}
|
||||
|
||||
{inAppHelpPage && (
|
||||
<LinkButton
|
||||
text={t("setup_form.more_info")}
|
||||
onClick={() => openInAppHelpFromUrl(inAppHelpPage)}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -61,6 +61,7 @@ type Labels = {
|
||||
|
||||
// Note-type specific
|
||||
webViewSrc: string;
|
||||
"disabled:webViewSrc": string;
|
||||
readOnly: boolean;
|
||||
mapType: string;
|
||||
mapRootNoteId: string;
|
||||
@@ -77,6 +78,10 @@ type Relations = [
|
||||
"searchScript",
|
||||
"ancestor",
|
||||
|
||||
// Active content
|
||||
"renderNote",
|
||||
"disabled:renderNote",
|
||||
|
||||
// Launcher-specific
|
||||
"target",
|
||||
"widget"
|
||||
|
||||
Reference in New Issue
Block a user