[#275] added support to customize the default user email templates from the Admin UI

This commit is contained in:
Gani Georgiev
2022-08-14 19:30:45 +03:00
parent 1de56d3d9e
commit 7d10d20de1
47 changed files with 1648 additions and 1188 deletions
@@ -28,6 +28,10 @@
export function collapse() {
accordion?.collapse();
}
export function collapseSiblings() {
accordion?.collapseSiblings();
}
</script>
<Accordion bind:this={accordion} on:expand on:collapse on:toggle {...$$restProps}>
@@ -11,7 +11,7 @@
let accordion;
$: hasErrors = !CommonHelper.isEmpty($errors?.emailPassword);
$: hasErrors = !CommonHelper.isEmpty($errors?.emailAuth);
export function expand() {
accordion?.expand();
@@ -50,7 +50,7 @@
{/if}
</svelte:fragment>
<Field class="form-field form-field-toggle m-b-0" name="emailPassword.enabled" let:uniqueId>
<Field class="form-field form-field-toggle m-b-0" name="emailAuth.enabled" let:uniqueId>
<input type="checkbox" id={uniqueId} bind:checked={config.enabled} />
<label for={uniqueId}>Enable</label>
</Field>
@@ -58,7 +58,7 @@
{#if config.enabled}
<div class="grid" transition:slide|local={{ duration: 150 }}>
<div class="col-sm-12 m-t-sm">
<Field class="form-field required" name="emailPassword.minPasswordLength" let:uniqueId>
<Field class="form-field required" name="emailAuth.minPasswordLength" let:uniqueId>
<label for={uniqueId}>Minimum password length</label>
<input
type="number"
@@ -73,7 +73,7 @@
<div class="col-lg-6">
<Field
class="form-field {!CommonHelper.isEmpty(config.onlyDomains) ? 'disabled' : ''}"
name="emailPassword.exceptDomains"
name="emailAuth.exceptDomains"
let:uniqueId
>
<label for={uniqueId}>
@@ -97,7 +97,7 @@
<div class="col-lg-6">
<Field
class="form-field {!CommonHelper.isEmpty(config.exceptDomains) ? 'disabled' : ''}"
name="emailPassword.onlyDomains"
name="emailAuth.onlyDomains"
let:uniqueId
>
<label for="{uniqueId}.config.onlyDomains">
@@ -0,0 +1,121 @@
<script>
import { scale } from "svelte/transition";
import tooltip from "@/actions/tooltip";
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";
export let key;
export let title;
export let config = {};
let accordion;
$: hasErrors = !CommonHelper.isEmpty(CommonHelper.getNestedVal($errors, key));
$: if (!config.enabled) {
removeError(key);
}
export function expand() {
accordion?.expand();
}
export function collapse() {
accordion?.collapse();
}
export function collapseSiblings() {
accordion?.collapseSiblings();
}
function copy(param) {
CommonHelper.copyToClipboard(param);
addInfoToast(`Copied ${param} to clipboard`, 2000);
}
</script>
<Accordion bind:this={accordion} on:expand on:collapse on:toggle {...$$restProps}>
<svelte:fragment slot="header">
<div class="inline-flex">
<i class="ri-draft-line" />
<span class="txt">{title}</span>
</div>
<div class="flex-fill" />
{#if hasErrors}
<i
class="ri-error-warning-fill txt-danger"
transition:scale={{ duration: 150, start: 0.7 }}
use:tooltip={{ text: "Has errors", position: "left" }}
/>
{/if}
</svelte:fragment>
<Field class="form-field required" name="{key}.subject" let:uniqueId>
<label for={uniqueId}>Subject</label>
<input type="text" id={uniqueId} bind:value={config.subject} spellcheck="false" required />
<div class="help-block">
Available placeholder parameters:
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{APP_NAME}")}>
{"{APP_NAME}"}
</span>,
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{APP_URL}")}>
{"{APP_URL}"}
</span>.
</div>
</Field>
<Field class="form-field required" name="{key}.actionUrl" let:uniqueId>
<label for={uniqueId}>Action URL</label>
<input type="text" id={uniqueId} bind:value={config.actionUrl} spellcheck="false" required />
<div class="help-block">
Available placeholder parameters:
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{APP_NAME}")}>
{"{APP_NAME}"}
</span>,
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{APP_URL}")}>
{"{APP_URL}"}
</span>,
<span
class="label label-sm link-primary txt-mono"
title="Required parameter"
on:click={() => copy("{TOKEN}")}>{"{TOKEN}"}</span
>.
</div>
</Field>
<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
/>
<div class="help-block">
Available placeholder parameters:
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{APP_NAME}")}>
{"{APP_NAME}"}
</span>,
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{APP_URL}")}>
{"{APP_URL}"}
</span>,
<span class="label label-sm link-primary txt-mono" on:click={() => copy("{TOKEN}")}>
{"{TOKEN}"}
</span>,
<span
class="label label-sm link-primary txt-mono"
title="Required parameter"
on:click={() => copy("{ACTION_URL}")}
>
{"{ACTION_URL}"}
</span>.
</div>
</Field>
</Accordion>
@@ -9,11 +9,14 @@
$pageTitle = "Application settings";
let originalFormSettings = {};
let formSettings = {};
let isLoading = false;
let isSaving = false;
let initialHash = "";
$: initialHash = JSON.stringify(originalFormSettings);
$: hasChanges = initialHash != JSON.stringify(formSettings);
loadSettings();
@@ -57,7 +60,11 @@
logs: settings?.logs || {},
};
initialHash = JSON.stringify(formSettings);
originalFormSettings = JSON.parse(JSON.stringify(formSettings));
}
function reset() {
formSettings = JSON.parse(JSON.stringify(originalFormSettings || {}));
}
</script>
@@ -103,6 +110,16 @@
<div class="col-lg-12 flex">
<div class="flex-fill" />
{#if hasChanges}
<button
type="button"
class="btn btn-secondary btn-hint"
disabled={isSaving}
on:click={() => reset()}
>
<span class="txt">Cancel</span>
</button>
{/if}
<button
type="submit"
class="btn btn-expanded"
@@ -12,12 +12,14 @@
$pageTitle = "Auth providers";
let emailAuthAccordion;
let authSettings = {};
let originalFormSettings = {};
let formSettings = {};
let isLoading = false;
let isSaving = false;
let initialHash = "";
$: hasChanges = initialHash != JSON.stringify(authSettings);
$: initialHash = JSON.stringify(originalFormSettings);
$: hasChanges = initialHash != JSON.stringify(formSettings);
loadSettings();
@@ -42,7 +44,7 @@
isSaving = true;
try {
const result = await ApiClient.settings.update(CommonHelper.filterRedactedProps(authSettings));
const result = await ApiClient.settings.update(CommonHelper.filterRedactedProps(formSettings));
initSettings(result);
setErrors({});
emailAuthAccordion?.collapseSiblings();
@@ -57,18 +59,23 @@
function initSettings(data) {
data = data || {};
authSettings = {};
authSettings.emailAuth = Object.assign({ enabled: true }, data.emailAuth);
formSettings = {
emailAuth: Object.assign({ enabled: true }, data.emailAuth),
};
const providers = ["googleAuth", "facebookAuth", "githubAuth", "gitlabAuth"];
for (const provider of providers) {
authSettings[provider] = Object.assign(
formSettings[provider] = Object.assign(
{ enabled: false, allowRegistrations: true },
data[provider]
);
}
initialHash = JSON.stringify(authSettings);
originalFormSettings = JSON.parse(JSON.stringify(formSettings));
}
function reset() {
formSettings = JSON.parse(JSON.stringify(originalFormSettings || {}));
}
</script>
@@ -93,28 +100,28 @@
<EmailAuthAccordion
bind:this={emailAuthAccordion}
single
bind:config={authSettings.emailAuth}
bind:config={formSettings.emailAuth}
/>
<AuthProviderAccordion
single
key="googleAuth"
title="Google"
icon="ri-google-line"
bind:config={authSettings.googleAuth}
bind:config={formSettings.googleAuth}
/>
<AuthProviderAccordion
single
key="facebookAuth"
title="Facebook"
icon="ri-facebook-line"
bind:config={authSettings.facebookAuth}
bind:config={formSettings.facebookAuth}
/>
<AuthProviderAccordion
single
key="githubAuth"
title="GitHub"
icon="ri-github-line"
bind:config={authSettings.githubAuth}
bind:config={formSettings.githubAuth}
/>
<AuthProviderAccordion
single
@@ -122,12 +129,22 @@
title="GitLab"
icon="ri-gitlab-line"
showSelfHostedFields
bind:config={authSettings.gitlabAuth}
bind:config={formSettings.gitlabAuth}
/>
</div>
<div class="flex m-t-base">
<div class="flex-fill" />
{#if hasChanges}
<button
type="button"
class="btn btn-secondary btn-hint"
disabled={isSaving}
on:click={() => reset()}
>
<span class="txt">Cancel</span>
</button>
{/if}
<button
type="submit"
class="btn btn-expanded"
+47 -44
View File
@@ -3,24 +3,29 @@
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import { pageTitle } from "@/stores/app";
import { setErrors } from "@/stores/errors";
import { addSuccessToast } from "@/stores/toasts";
import PageWrapper from "@/components/base/PageWrapper.svelte";
import Field from "@/components/base/Field.svelte";
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
import RedactedPasswordInput from "@/components/base/RedactedPasswordInput.svelte";
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
import EmailTemplateAccordion from "@/components/settings/EmailTemplateAccordion.svelte";
const tlsOptions = [
{ label: "Optional (StartTLS)", value: false },
{ label: "Auto (StartTLS)", value: false },
{ label: "Always", value: true },
];
$pageTitle = "Mail settings";
let firstAccordion;
let originalFormSettings = {};
let formSettings = {};
let isLoading = false;
let isSaving = false;
let initialHash = "";
$: initialHash = JSON.stringify(originalFormSettings);
$: hasChanges = initialHash != JSON.stringify(formSettings);
@@ -49,6 +54,8 @@
try {
const settings = await ApiClient.settings.update(CommonHelper.filterRedactedProps(formSettings));
init(settings);
setErrors({});
firstAccordion?.collapseSiblings();
addSuccessToast("Successfully saved mail settings.");
} catch (err) {
ApiClient.errorResponseHandler(err);
@@ -62,7 +69,12 @@
meta: settings?.meta || {},
smtp: settings?.smtp || {},
};
initialHash = JSON.stringify(formSettings);
originalFormSettings = JSON.parse(JSON.stringify(formSettings));
}
function reset() {
formSettings = JSON.parse(JSON.stringify(originalFormSettings || {}));
}
</script>
@@ -85,7 +97,7 @@
{#if isLoading}
<div class="loader" />
{:else}
<div class="grid">
<div class="grid m-b-base">
<div class="col-lg-6">
<Field class="form-field required" name="meta.senderName" let:uniqueId>
<label for={uniqueId}>Sender name</label>
@@ -109,49 +121,30 @@
/>
</Field>
</div>
</div>
<Field class="form-field required" name="meta.userVerificationUrl" let:uniqueId>
<label for={uniqueId}>User verification page url</label>
<input
type="text"
id={uniqueId}
required
bind:value={formSettings.meta.userVerificationUrl}
/>
<div class="help-block">
Used in the user verification email. Available placeholder parameters:
<code>%APP_URL%</code>, <code>%TOKEN%</code>.
</div>
</Field>
<div class="accordions">
<EmailTemplateAccordion
bind:this={firstAccordion}
single
key="meta.verificationTemplate"
title={'Default "Verification" email template'}
bind:config={formSettings.meta.verificationTemplate}
/>
<Field class="form-field required" name="meta.userResetPasswordUrl" let:uniqueId>
<label for={uniqueId}>User reset password page url</label>
<input
type="text"
id={uniqueId}
required
bind:value={formSettings.meta.userResetPasswordUrl}
/>
<div class="help-block">
Used in the user password reset email. Available placeholder parameters:
<code>%APP_URL%</code>, <code>%TOKEN%</code>.
</div>
</Field>
<EmailTemplateAccordion
single
key="meta.resetPasswordTemplate"
title={'Default "Password reset" email template'}
bind:config={formSettings.meta.resetPasswordTemplate}
/>
<Field class="form-field required" name="meta.userConfirmEmailChangeUrl" let:uniqueId>
<label for={uniqueId}>User confirm email change page url</label>
<input
type="text"
id={uniqueId}
required
bind:value={formSettings.meta.userConfirmEmailChangeUrl}
/>
<div class="help-block">
Used in the user email change confirmation email. Available placeholder
parameters:
<code>%APP_URL%</code>, <code>%TOKEN%</code>.
</div>
</Field>
<EmailTemplateAccordion
single
key="meta.confirmEmailChangeTemplate"
title={'Default "Confirm email change" email template'}
bind:config={formSettings.meta.confirmEmailChangeTemplate}
/>
</div>
<hr />
@@ -228,6 +221,16 @@
<div class="flex">
<div class="flex-fill" />
{#if hasChanges}
<button
type="button"
class="btn btn-secondary btn-hint"
disabled={isSaving}
on:click={() => reset()}
>
<span class="txt">Cancel</span>
</button>
{/if}
<button
type="submit"
class="btn btn-expanded"
+67 -20
View File
@@ -13,13 +13,14 @@
$pageTitle = "Files storage";
let s3 = {};
let originalFormSettings = {};
let formSettings = {};
let isLoading = false;
let isSaving = false;
let initialHash = "";
let initialEnabled = false;
$: hasChanges = initialHash != JSON.stringify(s3);
$: initialHash = JSON.stringify(originalFormSettings);
$: hasChanges = initialHash != JSON.stringify(formSettings);
loadSettings();
@@ -44,10 +45,10 @@
isSaving = true;
try {
const settings = await ApiClient.settings.update(CommonHelper.filterRedactedProps({ s3 }));
const settings = await ApiClient.settings.update(CommonHelper.filterRedactedProps(formSettings));
init(settings);
setErrors({});
addSuccessToast("Successfully saved Files storage settings.");
addSuccessToast("Successfully saved files storage settings.");
} catch (err) {
ApiClient.errorResponseHandler(err);
}
@@ -56,9 +57,14 @@
}
function init(settings = {}) {
s3 = settings?.s3 || {};
initialEnabled = s3.enabled;
initialHash = JSON.stringify(s3);
formSettings = {
s3: settings?.s3 || {},
};
originalFormSettings = JSON.parse(JSON.stringify(formSettings));
}
function reset() {
formSettings = JSON.parse(JSON.stringify(originalFormSettings || {}));
}
</script>
@@ -85,11 +91,11 @@
<div class="loader" />
{:else}
<Field class="form-field form-field-toggle" let:uniqueId>
<input type="checkbox" id={uniqueId} required bind:checked={s3.enabled} />
<input type="checkbox" id={uniqueId} required bind:checked={formSettings.s3.enabled} />
<label for={uniqueId}>Use S3 storage</label>
</Field>
{#if initialEnabled != s3.enabled}
{#if originalFormSettings.s3?.enabled != formSettings.s3.enabled}
<div transition:slide|local={{ duration: 150 }}>
<div class="alert alert-warning m-0">
<div class="icon">
@@ -98,9 +104,12 @@
<div class="content">
If you have existing uploaded files, you'll have to migrate them manually from
the
<strong>{initialEnabled ? "S3 storage" : "local file system"}</strong>
<strong>
{originalFormSettings.s3?.enabled ? "S3 storage" : "local file system"}
</strong>
to the
<strong>{s3.enabled ? "S3 storage" : "local file system"}</strong>.
<strong>{formSettings.s3.enabled ? "S3 storage" : "local file system"}</strong
>.
<br />
There are numerous command line tools that can help you, such as:
<a
@@ -125,41 +134,69 @@
</div>
{/if}
{#if s3.enabled}
{#if formSettings.s3.enabled}
<div class="grid" transition:slide|local={{ duration: 150 }}>
<div class="col-lg-12">
<Field class="form-field required" name="s3.endpoint" let:uniqueId>
<label for={uniqueId}>Endpoint</label>
<input type="text" id={uniqueId} required bind:value={s3.endpoint} />
<input
type="text"
id={uniqueId}
required
bind:value={formSettings.s3.endpoint}
/>
</Field>
</div>
<div class="col-lg-6">
<Field class="form-field required" name="s3.bucket" let:uniqueId>
<label for={uniqueId}>Bucket</label>
<input type="text" id={uniqueId} required bind:value={s3.bucket} />
<input
type="text"
id={uniqueId}
required
bind:value={formSettings.s3.bucket}
/>
</Field>
</div>
<div class="col-lg-6">
<Field class="form-field required" name="s3.region" let:uniqueId>
<label for={uniqueId}>Region</label>
<input type="text" id={uniqueId} required bind:value={s3.region} />
<input
type="text"
id={uniqueId}
required
bind:value={formSettings.s3.region}
/>
</Field>
</div>
<div class="col-lg-6">
<Field class="form-field required" name="s3.accessKey" let:uniqueId>
<label for={uniqueId}>Access key</label>
<input type="text" id={uniqueId} required bind:value={s3.accessKey} />
<input
type="text"
id={uniqueId}
required
bind:value={formSettings.s3.accessKey}
/>
</Field>
</div>
<div class="col-lg-6">
<Field class="form-field required" name="s3.secret" let:uniqueId>
<label for={uniqueId}>Secret</label>
<RedactedPasswordInput id={uniqueId} required bind:value={s3.secret} />
<RedactedPasswordInput
id={uniqueId}
required
bind:value={formSettings.s3.secret}
/>
</Field>
</div>
<div class="col-lg-12">
<Field class="form-field" name="s3.forcePathStyle" let:uniqueId>
<input type="checkbox" id={uniqueId} bind:checked={s3.forcePathStyle} />
<input
type="checkbox"
id={uniqueId}
bind:checked={formSettings.s3.forcePathStyle}
/>
<label for={uniqueId}>
<span class="txt">Force path-style addressing</span>
<i
@@ -179,6 +216,16 @@
<div class="flex">
<div class="flex-fill" />
{#if hasChanges}
<button
type="button"
class="btn btn-secondary btn-hint"
disabled={isSaving}
on:click={() => reset()}
>
<span class="txt">Cancel</span>
</button>
{/if}
<button
type="submit"
class="btn btn-expanded"
@@ -18,12 +18,14 @@
$pageTitle = "Token options";
let tokenSettings = {};
let originalFormSettings = {};
let formSettings = {};
let isLoading = false;
let isSaving = false;
let initialHash = "";
$: hasChanges = initialHash != JSON.stringify(tokenSettings);
$: initialHash = JSON.stringify(originalFormSettings);
$: hasChanges = initialHash != JSON.stringify(formSettings);
loadSettings();
@@ -48,7 +50,7 @@
isSaving = true;
try {
const result = await ApiClient.settings.update(CommonHelper.filterRedactedProps(tokenSettings));
const result = await ApiClient.settings.update(CommonHelper.filterRedactedProps(formSettings));
initSettings(result);
addSuccessToast("Successfully saved tokens options.");
} catch (err) {
@@ -60,15 +62,19 @@
function initSettings(data) {
data = data || {};
tokenSettings = {};
formSettings = {};
for (const listItem of tokensList) {
tokenSettings[listItem.key] = {
formSettings[listItem.key] = {
duration: data[listItem.key]?.duration || 0,
};
}
initialHash = JSON.stringify(tokenSettings);
originalFormSettings = JSON.parse(JSON.stringify(formSettings));
}
function reset() {
formSettings = JSON.parse(JSON.stringify(originalFormSettings || {}));
}
</script>
@@ -98,19 +104,19 @@
type="number"
id={uniqueId}
required
bind:value={tokenSettings[token.key].duration}
bind:value={formSettings[token.key].duration}
/>
<div class="help-block">
<span
class="link-primary"
class:txt-success={tokenSettings[token.key].secret}
class:txt-success={formSettings[token.key].secret}
on:click={() => {
// toggle
if (tokenSettings[token.key].secret) {
delete tokenSettings[token.key].secret;
tokenSettings[token.key] = tokenSettings[token.key];
if (formSettings[token.key].secret) {
delete formSettings[token.key].secret;
formSettings[token.key] = formSettings[token.key];
} else {
tokenSettings[token.key].secret = CommonHelper.randomString(50);
formSettings[token.key].secret = CommonHelper.randomString(50);
}
}}
>
@@ -122,6 +128,16 @@
<div class="flex">
<div class="flex-fill" />
{#if hasChanges}
<button
type="button"
class="btn btn-secondary btn-hint"
disabled={isSaving}
on:click={() => reset()}
>
<span class="txt">Cancel</span>
</button>
{/if}
<button
type="submit"
class="btn btn-expanded"
@@ -31,7 +31,7 @@
<div class="sidebar-title">
<span class="txt">Sync</span>
<small class="label label-danger label-compact">Experimental</small>
<small class="label label-danger label-sm">Experimental</small>
</div>
<a
href="/settings/export-collections"
@@ -35,7 +35,7 @@
<div class="alert alert-success">
<div class="icon"><i class="ri-checkbox-circle-line" /></div>
<div class="content txt-bold">
<p>Email address changed</p>
<p>Successfully changed the user email address.</p>
<p>You can now sign in with your new email address.</p>
</div>
</div>
@@ -36,7 +36,7 @@
<div class="alert alert-success">
<div class="icon"><i class="ri-checkbox-circle-line" /></div>
<div class="content txt-bold">
<p>Password changed</p>
<p>Successfully changed the user password.</p>
<p>You can now sign in with your new password.</p>
</div>
</div>
+1 -1
View File
@@ -443,7 +443,7 @@ a,
background: var(--baseAlt2Color);
color: var(--txtPrimaryColor);
white-space: nowrap;
&.label-compact {
&.label-sm {
font-size: var(--xsFontSize);
padding: 3px 5px;
min-height: 18px;
+1 -1
View File
@@ -26,7 +26,7 @@
--warningColor: #ff8e3c;
--warningAltColor: #ffe7d6;
--overlayColor: rgba(70, 85, 100, 0.3);
--overlayColor: rgba(70, 82, 110, 0.3);
--tooltipColor: rgba(0, 0, 0, 0.85);
--shadowColor: rgba(0, 0, 0, 0.05);
+2 -2
View File
@@ -284,7 +284,7 @@ export default class CommonHelper {
*/
static getNestedVal(data, path, defaultVal = null, delimiter = ".") {
let result = data || {};
let parts = path.split(delimiter);
let parts = (path || '').split(delimiter);
for (const part of parts) {
if (
@@ -353,7 +353,7 @@ export default class CommonHelper {
*/
static deleteByPath(data, path, delimiter = ".") {
let result = data || {};
let parts = path.split(delimiter);
let parts = (path || '').split(delimiter);
let lastPart = parts.pop();
for (const part of parts) {