simplified mail settings ui
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
// @todo consider replacing with readonly CodeEditor
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/plugins/normalize-whitespace/prism-normalize-whitespace.js";
|
||||
import "prismjs/components/prism-dart.js";
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
<script>
|
||||
/**
|
||||
* This component uses Codemirror editor under the hood and its a "little heavy".
|
||||
* To allow manuall chunking it is recommended to load the component lazily!
|
||||
*
|
||||
* Example usage:
|
||||
* ```
|
||||
* <script>
|
||||
* import { onMount } from "svelte";
|
||||
*
|
||||
* let editorComponent;
|
||||
*
|
||||
* onMount(async () => {
|
||||
* try {
|
||||
* editorComponent = (await import("@/components/base/CodeEditor.svelte")).default;
|
||||
* } catch (err) {
|
||||
* console.warn(err);
|
||||
* }
|
||||
* });
|
||||
* <//script>
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* <svelte:component
|
||||
* this={editorComponent}
|
||||
* bind:value={value}
|
||||
* disabled={disabled}
|
||||
* language="html"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
// code mirror imports
|
||||
// ---
|
||||
import {
|
||||
keymap,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
rectangularSelection,
|
||||
highlightActiveLineGutter,
|
||||
EditorView,
|
||||
placeholder as placeholderExt,
|
||||
} from "@codemirror/view";
|
||||
import { EditorState, Compartment } from "@codemirror/state";
|
||||
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from "@codemirror/language";
|
||||
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
|
||||
import {
|
||||
autocompletion,
|
||||
completionKeymap,
|
||||
closeBrackets,
|
||||
closeBracketsKeymap,
|
||||
} from "@codemirror/autocomplete";
|
||||
import { html as htmlLang } from "@codemirror/lang-html";
|
||||
import { javascript as javascriptLang } from "@codemirror/lang-javascript";
|
||||
// ---
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let id = "";
|
||||
export let value = "";
|
||||
export let maxHeight = null;
|
||||
export let disabled = false;
|
||||
export let placeholder = "";
|
||||
export let language = "javascript";
|
||||
export let singleLine = false;
|
||||
|
||||
let editor;
|
||||
let container;
|
||||
let langCompartment = new Compartment();
|
||||
let editableCompartment = new Compartment();
|
||||
let readOnlyCompartment = new Compartment();
|
||||
let placeholderCompartment = new Compartment();
|
||||
|
||||
$: if (id) {
|
||||
addLabelListeners();
|
||||
}
|
||||
|
||||
$: if (editor && language) {
|
||||
editor.dispatch({
|
||||
effects: [langCompartment.reconfigure(getEditorLang())],
|
||||
});
|
||||
}
|
||||
|
||||
$: if (editor && typeof disabled !== "undefined") {
|
||||
editor.dispatch({
|
||||
effects: [
|
||||
editableCompartment.reconfigure(EditorView.editable.of(!disabled)),
|
||||
readOnlyCompartment.reconfigure(EditorState.readOnly.of(disabled)),
|
||||
],
|
||||
});
|
||||
|
||||
triggerNativeChange();
|
||||
}
|
||||
|
||||
$: if (editor && value != editor.state.doc.toString()) {
|
||||
editor.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: editor.state.doc.length,
|
||||
insert: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$: if (editor && typeof placeholder !== "undefined") {
|
||||
editor.dispatch({
|
||||
effects: [placeholderCompartment.reconfigure(placeholderExt(placeholder))],
|
||||
});
|
||||
}
|
||||
|
||||
// Focus the editor (if inited).
|
||||
export function focus() {
|
||||
editor?.focus();
|
||||
}
|
||||
|
||||
// Emulate native change event for the editor container element.
|
||||
function triggerNativeChange() {
|
||||
container?.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: { value },
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Remove any attached label listeners.
|
||||
function removeLabelListeners() {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = document.querySelectorAll('[for="' + id + '"]');
|
||||
for (let label of labels) {
|
||||
label.removeEventListener("click", focus);
|
||||
}
|
||||
}
|
||||
|
||||
// Add `<label for="ID">...</label>` focus support.
|
||||
function addLabelListeners() {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeLabelListeners();
|
||||
|
||||
const labels = document.querySelectorAll('[for="' + id + '"]');
|
||||
for (let label of labels) {
|
||||
label.addEventListener("click", focus);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the current active editor language.
|
||||
function getEditorLang() {
|
||||
return language === "html" ? htmlLang() : javascriptLang();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const submitShortcut = {
|
||||
key: "Enter",
|
||||
run: (_) => {
|
||||
// trigger submit on enter for singleline input
|
||||
if (singleLine) {
|
||||
dispatch("submit", value);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
addLabelListeners();
|
||||
|
||||
editor = new EditorView({
|
||||
parent: container,
|
||||
state: EditorState.create({
|
||||
doc: value,
|
||||
extensions: [
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
rectangularSelection(),
|
||||
highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
submitShortcut,
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...completionKeymap,
|
||||
]),
|
||||
EditorView.lineWrapping,
|
||||
autocompletion({
|
||||
icons: false,
|
||||
}),
|
||||
langCompartment.of(getEditorLang()),
|
||||
placeholderCompartment.of(placeholderExt(placeholder)),
|
||||
editableCompartment.of(EditorView.editable.of(true)),
|
||||
readOnlyCompartment.of(EditorState.readOnly.of(false)),
|
||||
EditorState.transactionFilter.of((tr) => {
|
||||
return singleLine && tr.newDoc.lines > 1 ? [] : tr;
|
||||
}),
|
||||
EditorView.updateListener.of((v) => {
|
||||
if (!v.docChanged || disabled) {
|
||||
return;
|
||||
}
|
||||
value = v.state.doc.toString();
|
||||
triggerNativeChange();
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeLabelListeners();
|
||||
editor?.destroy();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class="code-editor" style:max-height={maxHeight ? maxHeight + "px" : "auto"} />
|
||||
@@ -1,5 +1,7 @@
|
||||
<script>
|
||||
/**
|
||||
* @todo consider combining with the CodeEditor component.
|
||||
*
|
||||
* This component uses Codemirror editor under the hood and its a "little heavy".
|
||||
* To allow manuall chunking it is recommended to load the component lazily!
|
||||
*
|
||||
@@ -65,6 +67,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let id = "";
|
||||
export let value = "";
|
||||
export let disabled = false;
|
||||
export let placeholder = "";
|
||||
@@ -83,6 +86,10 @@
|
||||
|
||||
$: mergedCollections = mergeWithBaseCollection($collections);
|
||||
|
||||
$: if (id) {
|
||||
addLabelListeners();
|
||||
}
|
||||
|
||||
$: if (editor && baseCollection?.schema) {
|
||||
editor.dispatch({
|
||||
effects: [langCompartment.reconfigure(ruleLang())],
|
||||
@@ -138,7 +145,33 @@
|
||||
);
|
||||
}
|
||||
|
||||
// Returns list with all collection field keys recursively.
|
||||
// Remove any attached label listeners.
|
||||
function removeLabelListeners() {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = document.querySelectorAll('[for="' + id + '"]');
|
||||
for (let label of labels) {
|
||||
label.removeEventListener("click", focus);
|
||||
}
|
||||
}
|
||||
|
||||
// Add `<label for="ID">...</label>` focus support.
|
||||
function addLabelListeners() {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeLabelListeners();
|
||||
|
||||
const labels = document.querySelectorAll('[for="' + id + '"]');
|
||||
for (let label of labels) {
|
||||
label.addEventListener("click", focus);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a list with all collection field keys recursively.
|
||||
function getCollectionFieldKeys(nameOrId, prefix = "", level = 0) {
|
||||
let collection = mergedCollections.find((item) => item.name == nameOrId || item.id == nameOrId);
|
||||
if (!collection || level >= 4) {
|
||||
@@ -324,6 +357,8 @@
|
||||
},
|
||||
};
|
||||
|
||||
addLabelListeners();
|
||||
|
||||
editor = new EditorView({
|
||||
parent: container,
|
||||
state: EditorState.create({
|
||||
@@ -371,7 +406,10 @@
|
||||
}),
|
||||
});
|
||||
|
||||
return () => editor?.destroy();
|
||||
return () => {
|
||||
removeLabelListeners();
|
||||
editor?.destroy();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
{#if filterComponent && !isFilterComponentLoading}
|
||||
<svelte:component
|
||||
this={filterComponent}
|
||||
id={uniqueId}
|
||||
singleLine
|
||||
disableRequestKeys
|
||||
disableIndirectCollectionsKeys
|
||||
|
||||
@@ -152,12 +152,13 @@
|
||||
name={prop}
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId} on:click={() => editorRefs[prop]?.focus()}>
|
||||
<label for={uniqueId}>
|
||||
{label} - {isAdminOnly(collection[prop]) ? "Admins only" : "Custom rule"}
|
||||
</label>
|
||||
|
||||
<svelte:component
|
||||
this={ruleInputComponent}
|
||||
id={uniqueId}
|
||||
bind:this={editorRefs[prop]}
|
||||
bind:value={collection[prop]}
|
||||
baseCollection={collection}
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
import { errors, removeError } from "@/stores/errors";
|
||||
import { addInfoToast } from "@/stores/toasts";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Accordion from "@/components/base/Accordion.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import Accordion from "@/components/base/Accordion.svelte";
|
||||
|
||||
export let key;
|
||||
export let title;
|
||||
export let config = {};
|
||||
|
||||
let accordion;
|
||||
let editorComponent;
|
||||
let isEditorComponentLoading = false;
|
||||
|
||||
$: hasErrors = !CommonHelper.isEmpty(CommonHelper.getNestedVal($errors, key));
|
||||
|
||||
@@ -31,6 +33,20 @@
|
||||
accordion?.collapseSiblings();
|
||||
}
|
||||
|
||||
async function loadEditorComponent() {
|
||||
if (editorComponent || isEditorComponentLoading) {
|
||||
return; // already loaded or in the process
|
||||
}
|
||||
|
||||
isEditorComponentLoading = true;
|
||||
|
||||
editorComponent = (await import("@/components/base/CodeEditor.svelte")).default;
|
||||
|
||||
isEditorComponentLoading = false;
|
||||
}
|
||||
|
||||
loadEditorComponent();
|
||||
|
||||
function copy(param) {
|
||||
CommonHelper.copyToClipboard(param);
|
||||
addInfoToast(`Copied ${param} to clipboard`, 2000);
|
||||
@@ -90,14 +106,25 @@
|
||||
|
||||
<Field class="form-field m-0 required" name="{key}.body" let:uniqueId>
|
||||
<label for={uniqueId}>Body (HTML)</label>
|
||||
<textarea
|
||||
id={uniqueId}
|
||||
bind:value={config.body}
|
||||
class="txt-mono"
|
||||
spellcheck="false"
|
||||
rows="12"
|
||||
required
|
||||
/>
|
||||
|
||||
{#if editorComponent && !isEditorComponentLoading}
|
||||
<svelte:component
|
||||
this={editorComponent}
|
||||
id={uniqueId}
|
||||
language="html"
|
||||
bind:value={config.body}
|
||||
/>
|
||||
{:else}
|
||||
<textarea
|
||||
id={uniqueId}
|
||||
class="txt-mono"
|
||||
spellcheck="false"
|
||||
rows="12"
|
||||
required
|
||||
bind:value={config.body}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="help-block">
|
||||
Available placeholder parameters:
|
||||
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{APP_NAME}")}>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { pageTitle } from "@/stores/app";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import PageWrapper from "@/components/base/PageWrapper.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
@@ -19,7 +20,6 @@
|
||||
|
||||
$pageTitle = "Mail settings";
|
||||
|
||||
let firstAccordion;
|
||||
let originalFormSettings = {};
|
||||
let formSettings = {};
|
||||
let isLoading = false;
|
||||
@@ -55,7 +55,6 @@
|
||||
const settings = await ApiClient.settings.update(CommonHelper.filterRedactedProps(formSettings));
|
||||
init(settings);
|
||||
setErrors({});
|
||||
firstAccordion?.collapseSiblings();
|
||||
addSuccessToast("Successfully saved mail settings.");
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
@@ -125,7 +124,6 @@
|
||||
|
||||
<div class="accordions">
|
||||
<EmailTemplateAccordion
|
||||
bind:this={firstAccordion}
|
||||
single
|
||||
key="meta.verificationTemplate"
|
||||
title={'Default "Verification" email template'}
|
||||
@@ -151,22 +149,19 @@
|
||||
|
||||
<Field class="form-field form-field-toggle m-b-sm" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} required bind:checked={formSettings.smtp.enabled} />
|
||||
<label for={uniqueId}>Use SMTP mail server</label>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Use SMTP mail server <strong>(recommended)</strong></span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: 'By default PocketBase uses the unix "sendmail" command for sending emails. For better emails deliverability it is recommended to use a SMTP mail server.',
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</Field>
|
||||
|
||||
{#if !formSettings.smtp.enabled}
|
||||
<div class="content" transition:slide|local={{ duration: 150 }}>
|
||||
<p>
|
||||
By default PocketBase uses the OS <code>sendmail</code> command for sending
|
||||
emails.
|
||||
<br />
|
||||
<strong class="txt-bold">
|
||||
For better emails deliverability it is recommended to use a SMTP mail server.
|
||||
</strong>
|
||||
</p>
|
||||
<div class="clearfix m-t-lg" />
|
||||
</div>
|
||||
{:else}
|
||||
{#if formSettings.smtp.enabled}
|
||||
<div class="grid" transition:slide|local={{ duration: 150 }}>
|
||||
<div class="col-lg-6">
|
||||
<Field class="form-field required" name="smtp.host" let:uniqueId>
|
||||
|
||||
Reference in New Issue
Block a user