simplified mail settings ui

This commit is contained in:
Gani Georgiev
2022-08-16 07:36:15 +03:00
parent 456ced75ce
commit ccd010c490
24 changed files with 1201 additions and 729 deletions
+1
View File
@@ -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";
+225
View File
@@ -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>
+1
View File
@@ -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}")}>
+12 -17
View File
@@ -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>
+9 -7
View File
@@ -332,10 +332,19 @@ button {
font-family: var(--baseFontFamily);
font-weight: normal;
border-radius: var(--baseRadius);
overflow: auto; /* fallback */
overflow: overlay;
&::placeholder {
color: var(--txtDisabledColor);
}
&:focus,
&:focus-within {
@include scrollbar(
$thumbColor: var(--baseAlt3Color),
$thumbActiveColor: var(--baseAlt4Color)
);
}
&:focus,
&.active {
border-color: var(--primaryColor);
}
@@ -393,13 +402,6 @@ input[type=number]::-webkit-outer-spin-button {
textarea {
min-height: 80px;
resize: vertical;
&:focus,
&:focus-within {
@include scrollbar(
$thumbColor: var(--baseAlt3Color),
$thumbActiveColor: var(--baseAlt4Color)
);
}
}
select {
padding-left: 8px;