Merge pull request #10093 from Budibase/feature/whitelabelling

White labelling updates
This commit is contained in:
deanhannigan 2023-03-28 14:26:53 +01:00 committed by GitHub
commit fcc623dc67
33 changed files with 1089 additions and 207 deletions

View File

@ -0,0 +1,115 @@
<script>
import ActionButton from "../../ActionButton/ActionButton.svelte"
import { uuid } from "../../helpers"
import Icon from "../../Icon/Icon.svelte"
import { createEventDispatcher } from "svelte"
export let value = null
export let title = "Upload file"
export let disabled = false
export let allowClear = null
export let extensions = null
export let handleFileTooLarge = null
export let fileSizeLimit = BYTES_IN_MB * 20
export let id = null
export let previewUrl = null
const fieldId = id || uuid()
const BYTES_IN_KB = 1000
const BYTES_IN_MB = 1000000
const dispatch = createEventDispatcher()
let fileInput
$: inputAccept = Array.isArray(extensions) ? extensions.join(",") : "*"
async function processFile(targetFile) {
if (handleFileTooLarge && targetFile?.size >= fileSizeLimit) {
handleFileTooLarge(targetFile)
return
}
dispatch("change", targetFile)
}
function handleFile(evt) {
processFile(evt.target.files[0])
}
function clearFile() {
dispatch("change", null)
}
</script>
<input
id={fieldId}
{disabled}
type="file"
accept={inputAccept}
bind:this={fileInput}
on:change={handleFile}
/>
<div class="field">
{#if value}
<div class="file-view">
{#if previewUrl}
<img class="preview" alt="" src={previewUrl} />
{/if}
<div class="filename">{value.name}</div>
{#if value.size}
<div class="filesize">
{#if value.size <= BYTES_IN_MB}
{`${value.size / BYTES_IN_KB} KB`}
{:else}
{`${value.size / BYTES_IN_MB} MB`}
{/if}
</div>
{/if}
{#if !disabled || (allowClear === true && disabled)}
<div class="delete-button" on:click={clearFile}>
<Icon name="Close" size="XS" />
</div>
{/if}
</div>
{/if}
<ActionButton {disabled} on:click={fileInput.click()}>{title}</ActionButton>
</div>
<style>
.field {
display: flex;
gap: var(--spacing-m);
}
.file-view {
display: flex;
gap: var(--spacing-l);
align-items: center;
border: 1px solid var(--spectrum-alias-border-color);
border-radius: var(--spectrum-global-dimension-size-50);
padding: 0px var(--spectrum-alias-item-padding-m);
}
input[type="file"] {
display: none;
}
.delete-button {
transition: all 0.3s;
margin-left: 10px;
display: flex;
}
.delete-button:hover {
cursor: pointer;
color: var(--red);
}
.filesize {
white-space: nowrap;
}
.filename {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.preview {
height: 1.5em;
}
</style>

View File

@ -13,3 +13,4 @@ export { default as CoreDropzone } from "./Dropzone.svelte"
export { default as CoreStepper } from "./Stepper.svelte" export { default as CoreStepper } from "./Stepper.svelte"
export { default as CoreRichTextField } from "./RichTextField.svelte" export { default as CoreRichTextField } from "./RichTextField.svelte"
export { default as CoreSlider } from "./Slider.svelte" export { default as CoreSlider } from "./Slider.svelte"
export { default as CoreFile } from "./File.svelte"

View File

@ -0,0 +1,37 @@
<script>
import Field from "./Field.svelte"
import { CoreFile } from "./Core"
import { createEventDispatcher } from "svelte"
export let label = null
export let labelPosition = "above"
export let disabled = false
export let allowClear = null
export let handleFileTooLarge = () => {}
export let previewUrl = null
export let extensions = null
export let error = null
export let title = null
export let value = null
export let tooltip = null
const dispatch = createEventDispatcher()
const onChange = e => {
value = e.detail
dispatch("change", e.detail)
}
</script>
<Field {label} {labelPosition} {error} {tooltip}>
<CoreFile
{error}
{disabled}
{allowClear}
{title}
{value}
{previewUrl}
{handleFileTooLarge}
{extensions}
on:change={onChange}
/>
</Field>

View File

@ -77,6 +77,7 @@ export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte" export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
export { default as Slider } from "./Form/Slider.svelte" export { default as Slider } from "./Form/Slider.svelte"
export { default as Accordion } from "./Accordion/Accordion.svelte" export { default as Accordion } from "./Accordion/Accordion.svelte"
export { default as File } from "./Form/File.svelte"
// Renderers // Renderers
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte" export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"

View File

@ -1,17 +1,17 @@
<!doctype html> <!doctype html>
<html class="spectrum spectrum--medium spectrum--darkest" lang="en" dir="ltr"> <html class="spectrum spectrum--medium spectrum--darkest" lang="en" dir="ltr">
<head> <head>
<meta charset='utf8'> <meta charset='utf8'>
<meta name='viewport' content='width=device-width'> <meta name='viewport' content='width=device-width'>
<title>Budibase</title> <title>Budibase</title>
<link rel='icon' href='/src/favicon.png'>
<link rel="preconnect" href="https://fonts.gstatic.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" />
<link <link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap" rel="stylesheet" />
rel="stylesheet"
/>
</head> </head>
<body id="app"> <body id="app">
<script type="module" src='/src/main.js'></script> <script type="module" src='/src/main.js'></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,32 @@
<script>
import { organisation, auth } from "stores/portal"
import { onMount } from "svelte"
let loaded = false
$: platformTitleText = $organisation.platformTitle
$: platformTitle =
!$auth.user && platformTitleText ? platformTitleText : "Budibase"
$: faviconUrl = $organisation.faviconUrl || "https://i.imgur.com/Xhdt1YP.png"
onMount(async () => {
await organisation.init()
loaded = true
})
</script>
<!--
In order to update the org elements, an update will have to be made to clear them.
-->
<svelte:head>
<title>{platformTitle}</title>
{#if loaded && !$auth.user && faviconUrl}
<link rel="icon" href={faviconUrl} />
{:else}
<!-- A default must be set or the browser defaults to favicon.ico behaviour -->
<link rel="icon" href={"https://i.imgur.com/Xhdt1YP.png"} />
{/if}
</svelte:head>

View File

@ -4,6 +4,7 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import { CookieUtils, Constants } from "@budibase/frontend-core" import { CookieUtils, Constants } from "@budibase/frontend-core"
import { API } from "api" import { API } from "api"
import Branding from "./Branding.svelte"
let loaded = false let loaded = false
@ -146,6 +147,9 @@
} }
</script> </script>
<!--Portal branding overrides -->
<Branding />
{#if loaded} {#if loaded}
<slot /> <slot />
{/if} {/if}

View File

@ -30,7 +30,7 @@
async function login() { async function login() {
form.validate() form.validate()
if (Object.keys(errors).length > 0) { if (Object.keys(errors).length > 0) {
console.log("errors") console.log("errors", errors)
return return
} }
try { try {
@ -64,99 +64,106 @@
</script> </script>
<svelte:window on:keydown={handleKeydown} /> <svelte:window on:keydown={handleKeydown} />
{#if loaded}
<TestimonialPage> <TestimonialPage enabled={$organisation.testimonialsEnabled}>
<Layout gap="L" noPadding> <Layout gap="L" noPadding>
<Layout justifyItems="center" noPadding> <Layout justifyItems="center" noPadding>
{#if loaded} {#if loaded}
<img alt="logo" src={$organisation.logoUrl || Logo} /> <img alt="logo" src={$organisation.logoUrl || Logo} />
{/if} {/if}
<Heading size="M">Log in to Budibase</Heading> <Heading size="M">
</Layout> {$organisation.loginHeading || "Log in to Budibase"}
<Layout gap="S" noPadding> </Heading>
{#if loaded && ($organisation.google || $organisation.oidc)} </Layout>
<FancyForm> <Layout gap="S" noPadding>
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} /> {#if loaded && ($organisation.google || $organisation.oidc)}
<GoogleButton /> <FancyForm>
</FancyForm> <OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
{/if} <GoogleButton />
</FancyForm>
{/if}
{#if !$organisation.isSSOEnforced}
<Divider />
<FancyForm bind:this={form}>
<FancyInput
label="Your work email"
value={formData.username}
on:change={e => {
formData = {
...formData,
username: e.detail,
}
}}
validate={() => {
let fieldError = {
username: !formData.username
? "Please enter a valid email"
: undefined,
}
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.username}
/>
<FancyInput
label="Password"
value={formData.password}
type="password"
on:change={e => {
formData = {
...formData,
password: e.detail,
}
}}
validate={() => {
let fieldError = {
password: !formData.password
? "Please enter your password"
: undefined,
}
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.password}
/>
</FancyForm>
{/if}
</Layout>
{#if !$organisation.isSSOEnforced} {#if !$organisation.isSSOEnforced}
<Divider /> <Layout gap="XS" noPadding justifyItems="center">
<FancyForm bind:this={form}> <Button
<FancyInput size="L"
label="Your work email" cta
value={formData.username} disabled={Object.keys(errors).length > 0}
on:change={e => { on:click={login}
formData = { >
...formData, {$organisation.loginButton || `Log in to ${company}`}
username: e.detail, </Button>
} </Layout>
}} <Layout gap="XS" noPadding justifyItems="center">
validate={() => { <div class="user-actions">
let fieldError = { <ActionButton size="L" quiet on:click={() => $goto("./forgot")}>
username: !formData.username Forgot password?
? "Please enter a valid email" </ActionButton>
: undefined, </div>
} </Layout>
errors = handleError({ ...errors, ...fieldError }) {/if}
}}
error={errors.username} {#if cloud}
/> <Body size="xs" textAlign="center">
<FancyInput By using Budibase Cloud
label="Password" <br />
value={formData.password} you are agreeing to our
type="password" <Link
on:change={e => { href="https://budibase.com/eula"
formData = { target="_blank"
...formData, secondary={true}
password: e.detail, >
} License Agreement
}} </Link>
validate={() => { </Body>
let fieldError = {
password: !formData.password
? "Please enter your password"
: undefined,
}
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.password}
/>
</FancyForm>
{/if} {/if}
</Layout> </Layout>
{#if !$organisation.isSSOEnforced} </TestimonialPage>
<Layout gap="XS" noPadding justifyItems="center"> {/if}
<Button
size="L"
cta
disabled={Object.keys(errors).length > 0}
on:click={login}
>
Log in to {company}
</Button>
</Layout>
<Layout gap="XS" noPadding justifyItems="center">
<div class="user-actions">
<ActionButton size="L" quiet on:click={() => $goto("./forgot")}>
Forgot password?
</ActionButton>
</div>
</Layout>
{/if}
{#if cloud}
<Body size="xs" textAlign="center">
By using Budibase Cloud
<br />
you are agreeing to our
<Link href="https://budibase.com/eula" target="_blank" secondary={true}>
License Agreement
</Link>
</Body>
{/if}
</Layout>
</TestimonialPage>
<style> <style>
.user-actions { .user-actions {

View File

@ -0,0 +1,446 @@
<script>
import {
Layout,
Heading,
Body,
Divider,
File,
notifications,
Tags,
Tag,
Button,
Toggle,
Input,
Label,
TextArea,
} from "@budibase/bbui"
import { auth, organisation, licensing, admin } from "stores/portal"
import { API } from "api"
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
const imageExtensions = [
".png",
".tiff",
".gif",
".raw",
".jpg",
".jpeg",
".svg",
".bmp",
".jfif",
]
const faviconExtensions = [".png", ".ico", ".gif"]
let mounted = false
let saving = false
let logoFile = null
let logoPreview = null
let faviconFile = null
let faviconPreview = null
let config = {}
let updated = false
$: onConfigUpdate(config, mounted)
$: init = Object.keys(config).length > 0
$: isCloud = $admin.cloud
$: brandingEnabled = $licensing.brandingEnabled
const onConfigUpdate = () => {
if (!mounted || updated || !init) {
return
}
updated = true
}
$: logo = config.logoUrl
? { url: config.logoUrl, type: "image", name: "Logo" }
: null
$: favicon = config.faviconUrl
? { url: config.faviconUrl, type: "image", name: "Favicon" }
: null
const previewUrl = async localFile => {
if (!localFile) {
return Promise.resolve(null)
}
return new Promise(resolve => {
let reader = new FileReader()
try {
reader.onload = e => {
resolve({
result: e.target.result,
})
}
reader.readAsDataURL(localFile)
} catch (error) {
console.error(error)
resolve(null)
}
})
}
$: previewUrl(logoFile).then(response => {
if (response) {
logoPreview = response.result
}
})
$: previewUrl(faviconFile).then(response => {
if (response) {
faviconPreview = response.result
}
})
async function uploadLogo(file) {
let response = {}
try {
let data = new FormData()
data.append("file", file)
response = await API.uploadLogo(data)
} catch (error) {
notifications.error("Error uploading logo")
}
return response
}
async function uploadFavicon(file) {
let response = {}
try {
let data = new FormData()
data.append("file", file)
response = await API.uploadFavicon(data)
} catch (error) {
notifications.error("Error uploading favicon")
}
return response
}
async function saveConfig() {
saving = true
if (logoFile) {
const logoResp = await uploadLogo(logoFile)
if (logoResp.url) {
config = {
...config,
logoUrl: logoResp.url,
}
logoFile = null
logoPreview = null
}
}
if (faviconFile) {
const faviconResp = await uploadFavicon(faviconFile)
if (faviconResp.url) {
config = {
...config,
faviconUrl: faviconResp.url,
}
faviconFile = null
faviconPreview = null
}
}
// Trim
const userStrings = [
"metaTitle",
"platformTitle",
"loginButton",
"loginHeading",
"metaDescription",
"metaImageUrl",
]
const trimmed = userStrings.reduce((acc, fieldName) => {
acc[fieldName] = config[fieldName] ? config[fieldName].trim() : undefined
return acc
}, {})
config = {
...config,
...trimmed,
}
try {
// Update settings
await organisation.save(config)
await organisation.init()
notifications.success("Branding settings updated")
} catch (e) {
console.error("Branding updated failed", e)
notifications.error("Branding updated failed")
}
updated = false
saving = false
}
onMount(async () => {
await organisation.init()
config = {
faviconUrl: $organisation.faviconUrl,
logoUrl: $organisation.logoUrl,
platformTitle: $organisation.platformTitle,
emailBrandingEnabled: $organisation.emailBrandingEnabled,
loginHeading: $organisation.loginHeading,
loginButton: $organisation.loginButton,
testimonialsEnabled: $organisation.testimonialsEnabled,
metaDescription: $organisation.metaDescription,
metaImageUrl: $organisation.metaImageUrl,
metaTitle: $organisation.metaTitle,
}
mounted = true
})
</script>
{#if $auth.isAdmin && mounted}
<Layout noPadding>
<Layout gap="XS" noPadding>
<div class="title">
<Heading size="M">Branding</Heading>
{#if !isCloud && !brandingEnabled}
<Tags>
<Tag icon="LockClosed">Business</Tag>
</Tags>
{/if}
{#if isCloud && !brandingEnabled}
<Tags>
<Tag icon="LockClosed">Pro</Tag>
</Tags>
{/if}
</div>
<Body>Remove all Budibase branding and use your own.</Body>
</Layout>
<Divider />
<div class="branding fields">
<div class="field">
<Label size="L">Logo</Label>
<File
title="Upload image"
handleFileTooLarge={() => {
notifications.warn("File too large. 20mb limit")
}}
extensions={imageExtensions}
previewUrl={logoPreview || logo?.url}
on:change={e => {
let clone = { ...config }
if (e.detail) {
logoFile = e.detail
logoPreview = null
} else {
logoFile = null
clone.logoUrl = ""
}
config = clone
}}
value={logoFile || logo}
disabled={!brandingEnabled || saving}
allowClear={true}
/>
</div>
<div class="field">
<Label size="L">Favicon</Label>
<File
title="Upload image"
handleFileTooLarge={() => {
notifications.warn("File too large. 20mb limit")
}}
extensions={faviconExtensions}
previewUrl={faviconPreview || favicon?.url}
on:change={e => {
let clone = { ...config }
if (e.detail) {
faviconFile = e.detail
faviconPreview = null
} else {
clone.faviconUrl = ""
}
config = clone
}}
value={faviconFile || favicon}
disabled={!brandingEnabled || saving}
allowClear={true}
/>
</div>
{#if !isCloud}
<div class="field">
<Label size="L">Title</Label>
<Input
on:change={e => {
let clone = { ...config }
clone.platformTitle = e.detail ? e.detail : ""
config = clone
}}
value={config.platformTitle || ""}
disabled={!brandingEnabled || saving}
/>
</div>
{/if}
<div>
<Toggle
text={"Remove Budibase brand from emails"}
on:change={e => {
let clone = { ...config }
clone.emailBrandingEnabled = !e.detail
config = clone
}}
value={!config.emailBrandingEnabled}
disabled={!brandingEnabled || saving}
/>
</div>
</div>
{#if !isCloud}
<Divider />
<Layout gap="XS" noPadding>
<Heading size="S">Login page</Heading>
<Body />
</Layout>
<div class="login">
<div class="fields">
<div class="field">
<Label size="L">Header</Label>
<Input
on:change={e => {
let clone = { ...config }
clone.loginHeading = e.detail ? e.detail : ""
config = clone
}}
value={config.loginHeading || ""}
disabled={!brandingEnabled || saving}
/>
</div>
<div class="field">
<Label size="L">Button</Label>
<Input
on:change={e => {
let clone = { ...config }
clone.loginButton = e.detail ? e.detail : ""
config = clone
}}
value={config.loginButton || ""}
disabled={!brandingEnabled || saving}
/>
</div>
<div>
<Toggle
text={"Remove customer testimonials"}
on:change={e => {
let clone = { ...config }
clone.testimonialsEnabled = !e.detail
config = clone
}}
value={!config.testimonialsEnabled}
disabled={!brandingEnabled || saving}
/>
</div>
</div>
</div>
{/if}
<Divider />
<Layout gap="XS" noPadding>
<Heading size="S">Application previews</Heading>
<Body>Customise the meta tags on your app preview</Body>
</Layout>
<div class="app-previews">
<div class="fields">
<div class="field">
<Label size="L">Image URL</Label>
<Input
on:change={e => {
let clone = { ...config }
clone.metaImageUrl = e.detail ? e.detail : ""
config = clone
}}
value={config.metaImageUrl}
disabled={!brandingEnabled || saving}
/>
</div>
<div class="field">
<Label size="L">Title</Label>
<Input
on:change={e => {
let clone = { ...config }
clone.metaTitle = e.detail ? e.detail : ""
config = clone
}}
value={config.metaTitle}
disabled={!brandingEnabled || saving}
/>
</div>
<div class="field">
<Label size="L">Description</Label>
<TextArea
on:change={e => {
let clone = { ...config }
clone.metaDescription = e.detail ? e.detail : ""
config = clone
}}
value={config.metaDescription}
disabled={!brandingEnabled || saving}
/>
</div>
</div>
</div>
<div class="buttons">
{#if !brandingEnabled}
<Button
on:click={() => {
if (isCloud && $auth?.user?.accountPortalAccess) {
window.open($admin.accountPortalUrl + "/portal/upgrade", "_blank")
} else if ($auth.isAdmin) {
$goto("/builder/portal/account/upgrade")
}
}}
secondary
disabled={saving}
>
Upgrade
</Button>
{/if}
<Button on:click={saveConfig} cta disabled={saving || !updated || !init}>
Save
</Button>
</div>
</Layout>
{/if}
<style>
.buttons {
display: flex;
gap: var(--spacing-m);
}
.title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: var(--spacing-m);
}
.branding,
.login {
width: 70%;
max-width: 70%;
}
.fields {
display: grid;
grid-gap: var(--spacing-m);
}
.field {
display: grid;
grid-template-columns: 80px auto;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View File

@ -7,12 +7,10 @@
Divider, Divider,
Label, Label,
Input, Input,
Dropzone,
notifications, notifications,
Toggle, Toggle,
} from "@budibase/bbui" } from "@budibase/bbui"
import { auth, organisation, admin } from "stores/portal" import { auth, organisation, admin } from "stores/portal"
import { API } from "api"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
@ -28,32 +26,14 @@
company: $organisation.company, company: $organisation.company,
platformUrl: $organisation.platformUrl, platformUrl: $organisation.platformUrl,
analyticsEnabled: $organisation.analyticsEnabled, analyticsEnabled: $organisation.analyticsEnabled,
logo: $organisation.logoUrl
? { url: $organisation.logoUrl, type: "image", name: "Logo" }
: null,
}) })
let loading = false
async function uploadLogo(file) { let loading = false
try {
let data = new FormData()
data.append("file", file)
await API.uploadLogo(data)
} catch (error) {
notifications.error("Error uploading logo")
}
}
async function saveConfig() { async function saveConfig() {
loading = true loading = true
try { try {
// Upload logo if required
if ($values.logo && !$values.logo.url) {
await uploadLogo($values.logo)
await organisation.init()
}
const config = { const config = {
isSSOEnforced: $values.isSSOEnforced, isSSOEnforced: $values.isSSOEnforced,
company: $values.company ?? "", company: $values.company ?? "",
@ -61,11 +41,6 @@
analyticsEnabled: $values.analyticsEnabled, analyticsEnabled: $values.analyticsEnabled,
} }
// Remove logo if required
if (!$values.logo) {
config.logoUrl = ""
}
// Update settings // Update settings
await organisation.save(config) await organisation.save(config)
} catch (error) { } catch (error) {
@ -87,21 +62,7 @@
<Label size="L">Org. name</Label> <Label size="L">Org. name</Label>
<Input thin bind:value={$values.company} /> <Input thin bind:value={$values.company} />
</div> </div>
<div class="field logo">
<Label size="L">Logo</Label>
<div class="file">
<Dropzone
value={[$values.logo]}
on:change={e => {
if (!e.detail || e.detail.length === 0) {
$values.logo = null
} else {
$values.logo = e.detail[0]
}
}}
/>
</div>
</div>
{#if !$admin.cloud} {#if !$admin.cloud}
<div class="field"> <div class="field">
<Label <Label
@ -137,10 +98,4 @@
grid-gap: var(--spacing-l); grid-gap: var(--spacing-l);
align-items: center; align-items: center;
} }
.file {
max-width: 30ch;
}
.logo {
align-items: start;
}
</style> </style>

View File

@ -13,9 +13,11 @@ export const createLicensingStore = () => {
license: undefined, license: undefined,
isFreePlan: true, isFreePlan: true,
isEnterprisePlan: true, isEnterprisePlan: true,
isBusinessPlan: true,
// features // features
groupsEnabled: false, groupsEnabled: false,
backupsEnabled: false, backupsEnabled: false,
brandingEnabled: false,
// the currently used quotas from the db // the currently used quotas from the db
quotaUsage: undefined, quotaUsage: undefined,
// derived quota metrics for percentages used // derived quota metrics for percentages used
@ -57,6 +59,7 @@ export const createLicensingStore = () => {
const planType = license?.plan.type const planType = license?.plan.type
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
const isFreePlan = planType === Constants.PlanType.FREE const isFreePlan = planType === Constants.PlanType.FREE
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
const groupsEnabled = license.features.includes( const groupsEnabled = license.features.includes(
Constants.Features.USER_GROUPS Constants.Features.USER_GROUPS
) )
@ -69,7 +72,9 @@ export const createLicensingStore = () => {
const enforceableSSO = license.features.includes( const enforceableSSO = license.features.includes(
Constants.Features.ENFORCEABLE_SSO Constants.Features.ENFORCEABLE_SSO
) )
const brandingEnabled = license.features.includes(
Constants.Features.BRANDING
)
const auditLogsEnabled = license.features.includes( const auditLogsEnabled = license.features.includes(
Constants.Features.AUDIT_LOGS Constants.Features.AUDIT_LOGS
) )
@ -79,8 +84,10 @@ export const createLicensingStore = () => {
license, license,
isEnterprisePlan, isEnterprisePlan,
isFreePlan, isFreePlan,
isBusinessPlan,
groupsEnabled, groupsEnabled,
backupsEnabled, backupsEnabled,
brandingEnabled,
environmentVariablesEnabled, environmentVariablesEnabled,
auditLogsEnabled, auditLogsEnabled,
enforceableSSO, enforceableSSO,

View File

@ -50,6 +50,10 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
title: "Organisation", title: "Organisation",
href: "/builder/portal/settings/organisation", href: "/builder/portal/settings/organisation",
}, },
{
title: "Branding",
href: "/builder/portal/settings/branding",
},
{ {
title: "Environment", title: "Environment",
href: "/builder/portal/settings/environment", href: "/builder/portal/settings/environment",

View File

@ -6,6 +6,15 @@ import _ from "lodash"
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
platformUrl: "", platformUrl: "",
logoUrl: undefined, logoUrl: undefined,
faviconUrl: undefined,
emailBrandingEnabled: true,
testimonialsEnabled: true,
platformTitle: "Budibase",
loginHeading: undefined,
loginButton: undefined,
metaDescription: undefined,
metaImageUrl: undefined,
metaTitle: undefined,
docsUrl: undefined, docsUrl: undefined,
company: "Budibase", company: "Budibase",
oidc: undefined, oidc: undefined,

View File

@ -16,6 +16,7 @@ export { rowSelectionStore } from "./rowSelection.js"
export { blockStore } from "./blocks.js" export { blockStore } from "./blocks.js"
export { environmentStore } from "./environment" export { environmentStore } from "./environment"
export { eventStore } from "./events.js" export { eventStore } from "./events.js"
export { orgStore } from "./org.js"
export { export {
dndStore, dndStore,
dndIndex, dndIndex,

View File

@ -1,7 +1,9 @@
import { routeStore } from "./routes" import { routeStore } from "./routes"
import { appStore } from "./app" import { appStore } from "./app"
import { orgStore } from "./org"
export async function initialise() { export async function initialise() {
await routeStore.actions.fetchRoutes() await routeStore.actions.fetchRoutes()
await appStore.actions.fetchAppDefinition() await appStore.actions.fetchAppDefinition()
await orgStore.actions.init()
} }

View File

@ -0,0 +1,29 @@
import { API } from "api"
import { writable, get } from "svelte/store"
import { appStore } from "./app"
const createOrgStore = () => {
const store = writable(null)
const { subscribe, set } = store
async function init() {
const tenantId = get(appStore).application?.tenantId
if (!tenantId) return
try {
const settingsConfigDoc = await API.getTenantConfig(tenantId)
set({ logoUrl: settingsConfigDoc.config.logoUrl })
} catch (e) {
console.log("Could not init org ", e)
}
}
return {
subscribe,
actions: {
init,
},
}
}
export const orgStore = createOrgStore()

View File

@ -2,6 +2,7 @@ import { derived } from "svelte/store"
import { routeStore } from "./routes" import { routeStore } from "./routes"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import { appStore } from "./app" import { appStore } from "./app"
import { orgStore } from "./org"
import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js" import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import { findComponentById, findComponentParent } from "../utils/components.js" import { findComponentById, findComponentParent } from "../utils/components.js"
@ -14,6 +15,7 @@ const createScreenStore = () => {
appStore, appStore,
routeStore, routeStore,
builderStore, builderStore,
orgStore,
dndParent, dndParent,
dndIndex, dndIndex,
dndIsNewComponent, dndIsNewComponent,
@ -23,6 +25,7 @@ const createScreenStore = () => {
$appStore, $appStore,
$routeStore, $routeStore,
$builderStore, $builderStore,
$orgStore,
$dndParent, $dndParent,
$dndIndex, $dndIndex,
$dndIsNewComponent, $dndIsNewComponent,
@ -146,6 +149,11 @@ const createScreenStore = () => {
if (!navigationSettings.title && !navigationSettings.hideTitle) { if (!navigationSettings.title && !navigationSettings.hideTitle) {
navigationSettings.title = $appStore.application?.name navigationSettings.title = $appStore.application?.name
} }
// Default to the org logo
if (!navigationSettings.logoUrl) {
navigationSettings.logoUrl = $orgStore?.logoUrl
}
} }
activeLayout = { activeLayout = {
_id: "layout", _id: "layout",

View File

@ -23,11 +23,6 @@
chalk "^2.0.0" chalk "^2.0.0"
js-tokens "^4.0.0" js-tokens "^4.0.0"
"@budibase/types@2.4.8-alpha.4":
version "2.4.8-alpha.4"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.4.8-alpha.4.tgz#4e6dec50eef381994432ef4d08587a9a7156dd84"
integrity sha512-aiHHOvsDLHQ2OFmLgaSUttQwSuaPBqF1lbyyCkEJIbbl/qo9EPNZGl+AkB7wo12U5HdqWhr9OpFL12EqkcD4GA==
"@jridgewell/gen-mapping@^0.3.0": "@jridgewell/gen-mapping@^0.3.0":
version "0.3.2" version "0.3.2"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"

View File

@ -73,6 +73,18 @@ export const buildConfigEndpoints = API => ({
}) })
}, },
/**
* Updates the company favicon for the environment.
* @param data the favicon form data
*/
uploadFavicon: async data => {
return await API.post({
url: "/api/global/configs/upload/settings/faviconUrl",
body: data,
json: false,
})
},
/** /**
* Uploads a logo for an OIDC provider. * Uploads a logo for an OIDC provider.
* @param name the name of the OIDC provider * @param name the name of the OIDC provider

View File

@ -5,6 +5,8 @@
import Covanta from "../../assets/covanta.png" import Covanta from "../../assets/covanta.png"
import Schnellecke from "../../assets/schnellecke.png" import Schnellecke from "../../assets/schnellecke.png"
export let enabled = true
const testimonials = [ const testimonials = [
{ {
text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.", text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.",
@ -33,23 +35,25 @@
<SplitPage> <SplitPage>
<slot /> <slot />
<div class="wrapper" slot="right"> <div class:wrapper={enabled} slot="right">
<div class="testimonial"> {#if enabled}
<Layout noPadding gap="S"> <div class="testimonial">
<img <Layout noPadding gap="S">
width={testimonial.imageSize} <img
alt="a-happy-budibase-user" width={testimonial.imageSize}
src={testimonial.image} alt="a-happy-budibase-user"
/> src={testimonial.image}
<div class="text"> />
"{testimonial.text}" <div class="text">
</div> "{testimonial.text}"
<div class="author"> </div>
<div class="name">{testimonial.name}</div> <div class="author">
<div class="company">{testimonial.role}</div> <div class="name">{testimonial.name}</div>
</div> <div class="company">{testimonial.role}</div>
</Layout> </div>
</div> </Layout>
</div>
{/if}
</div> </div>
</SplitPage> </SplitPage>

View File

@ -68,6 +68,7 @@ export const Features = {
ENVIRONMENT_VARIABLES: "environmentVariables", ENVIRONMENT_VARIABLES: "environmentVariables",
AUDIT_LOGS: "auditLogs", AUDIT_LOGS: "auditLogs",
ENFORCEABLE_SSO: "enforceableSSO", ENFORCEABLE_SSO: "enforceableSSO",
BRANDING: "branding",
} }
// Role IDs // Role IDs

View File

@ -11,10 +11,12 @@ import {
} from "../../../utilities/fileSystem" } from "../../../utilities/fileSystem"
import env from "../../../environment" import env from "../../../environment"
import { DocumentType } from "../../../db/utils" import { DocumentType } from "../../../db/utils"
import { context, objectStore, utils } from "@budibase/backend-core" import { context, objectStore, utils, configs } from "@budibase/backend-core"
import AWS from "aws-sdk" import AWS from "aws-sdk"
import fs from "fs" import fs from "fs"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import * as pro from "@budibase/pro"
const send = require("koa-send") const send = require("koa-send")
async function prepareUpload({ s3Key, bucket, metadata, file }: any) { async function prepareUpload({ s3Key, bucket, metadata, file }: any) {
@ -98,33 +100,74 @@ export const deleteObjects = async function (ctx: any) {
} }
export const serveApp = async function (ctx: any) { export const serveApp = async function (ctx: any) {
const db = context.getAppDB({ skip_setup: true }) //Public Settings
const appInfo = await db.get(DocumentType.APP_METADATA) const { config } = await configs.getSettingsConfigDoc()
let appId = context.getAppId() const branding = await pro.branding.getBrandingConfig(config)
if (!env.isJest()) { let db
const App = require("./templates/BudibaseApp.svelte").default try {
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins) db = context.getAppDB({ skip_setup: true })
const { head, html, css } = App.render({ const appInfo = await db.get(DocumentType.APP_METADATA)
metaImage: let appId = context.getAppId()
"https://res.cloudinary.com/daog6scxm/image/upload/v1666109324/meta-images/budibase-meta-image_uukc1m.png",
title: appInfo.name,
production: env.isProd(),
appId,
clientLibPath: objectStore.clientLibraryUrl(appId!, appInfo.version),
usedPlugins: plugins,
})
const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`) if (!env.isJest()) {
ctx.body = await processString(appHbs, { const App = require("./templates/BudibaseApp.svelte").default
head, const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
body: html, const { head, html, css } = App.render({
style: css.code, metaImage:
appId, branding?.metaImageUrl ||
}) "https://res.cloudinary.com/daog6scxm/image/upload/v1666109324/meta-images/budibase-meta-image_uukc1m.png",
} else { metaDescription: branding?.metaDescription || "",
// just return the app info for jest to assert on metaTitle:
ctx.body = appInfo branding?.metaTitle || `${appInfo.name} - built with Budibase`,
title: appInfo.name,
production: env.isProd(),
appId,
clientLibPath: objectStore.clientLibraryUrl(appId!, appInfo.version),
usedPlugins: plugins,
favicon:
branding.faviconUrl !== ""
? objectStore.getGlobalFileUrl("settings", "faviconUrl")
: "",
logo:
config?.logoUrl !== ""
? objectStore.getGlobalFileUrl("settings", "logoUrl")
: "",
})
const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`)
ctx.body = await processString(appHbs, {
head,
body: html,
style: css.code,
appId,
})
} else {
// just return the app info for jest to assert on
ctx.body = appInfo
}
} catch (error) {
if (!env.isJest()) {
const App = require("./templates/BudibaseApp.svelte").default
const { head, html, css } = App.render({
title: branding?.metaTitle,
metaTitle: branding?.metaTitle,
metaImage:
branding?.metaImageUrl ||
"https://res.cloudinary.com/daog6scxm/image/upload/v1666109324/meta-images/budibase-meta-image_uukc1m.png",
metaDescription: branding?.metaDescription || "",
favicon:
branding.faviconUrl !== ""
? objectStore.getGlobalFileUrl("settings", "faviconUrl")
: "",
})
const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`)
ctx.body = await processString(appHbs, {
head,
body: html,
style: css.code,
})
}
} }
} }

View File

@ -1,7 +1,10 @@
<script> <script>
export let title = "" export let title = ""
export let favicon = "" export let favicon = ""
export let metaImage = "" export let metaImage = ""
export let metaTitle = ""
export let metaDescription = ""
export let clientLibPath export let clientLibPath
export let usedPlugins export let usedPlugins
@ -13,18 +16,33 @@
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover" content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/> />
<!-- Primary Meta Tags -->
<meta name="title" content={metaTitle} />
<meta name="description" content={metaDescription} />
<!-- Opengraph Meta Tags --> <!-- Opengraph Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@budibase" />
<meta name="twitter:image" content={metaImage} />
<meta name="twitter:title" content="{title} - built with Budibase" />
<meta property="og:site_name" content="Budibase" /> <meta property="og:site_name" content="Budibase" />
<meta property="og:title" content="{title} - built with Budibase" /> <meta property="og:title" content="{title} - built with Budibase" />
<meta property="og:description" content={metaDescription} />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:image" content={metaImage} /> <meta property="og:image" content={metaImage} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:site" content="@budibase" />
<meta property="twitter:image" content={metaImage} />
<meta property="twitter:image:alt" content="{title} - built with Budibase" />
<meta property="twitter:title" content="{title} - built with Budibase" />
<meta property="twitter:description" content={metaDescription} />
<title>{title}</title> <title>{title}</title>
<link rel="icon" type="image/png" href={favicon} /> {#if favicon !== ""}
<link rel="icon" type="image/png" href={favicon} />
{:else}
<link rel="icon" type="image/png" href="https://i.imgur.com/Xhdt1YP.png" />
{/if}
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" /> <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<link rel="preconnect" href="https://fonts.gstatic.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" />
<link <link
@ -83,11 +101,16 @@
<body id="app"> <body id="app">
<div id="error"> <div id="error">
<h1>There was an error loading your app</h1> {#if clientLibPath}
<h2> <h1>There was an error loading your app</h1>
The Budibase client library could not be loaded. Try republishing your <h2>
app. The Budibase client library could not be loaded. Try republishing your
</h2> app.
</h2>
{:else}
<h2>We couldn't find that application</h2>
<p />
{/if}
</div> </div>
<script type="application/javascript"> <script type="application/javascript">
window.INIT_TIME = Date.now() window.INIT_TIME = Date.now()

View File

@ -22,6 +22,24 @@ export interface SMTPConfig extends Config {
config: SMTPInnerConfig config: SMTPInnerConfig
} }
/**
* Accessible only via pro.
*/
export interface SettingsBrandingConfig {
faviconUrl?: string
faviconUrlEtag?: string
emailBrandingEnabled?: boolean
testimonialsEnabled?: boolean
platformTitle?: string
loginHeading?: string
loginButton?: string
metaDescription?: string
metaImageUrl?: string
metaTitle?: string
}
export interface SettingsInnerConfig { export interface SettingsInnerConfig {
platformUrl?: string platformUrl?: string
company?: string company?: string

View File

@ -4,4 +4,5 @@ export enum Feature {
ENVIRONMENT_VARIABLES = "environmentVariables", ENVIRONMENT_VARIABLES = "environmentVariables",
AUDIT_LOGS = "auditLogs", AUDIT_LOGS = "auditLogs",
ENFORCEABLE_SSO = "enforceableSSO", ENFORCEABLE_SSO = "enforceableSSO",
BRANDING = "branding",
} }

View File

@ -11,6 +11,7 @@ import {
tenancy, tenancy,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { checkAnyUserExists } from "../../../utilities/users" import { checkAnyUserExists } from "../../../utilities/users"
import { getLicensedConfig } from "../../../utilities/configs"
import { import {
Config, Config,
ConfigType, ConfigType,
@ -211,6 +212,38 @@ export async function save(ctx: UserCtx<Config>) {
ctx.throw(400, err) ctx.throw(400, err)
} }
// Ignore branding changes if the license does not permit it
// Favicon and Logo Url are excluded.
try {
const brandingEnabled = await pro.features.isBrandingEnabled()
if (existingConfig?.config && !brandingEnabled) {
const {
emailBrandingEnabled,
testimonialsEnabled,
platformTitle,
metaDescription,
loginHeading,
loginButton,
metaImageUrl,
metaTitle,
} = existingConfig.config
body.config = {
...body.config,
emailBrandingEnabled,
testimonialsEnabled,
platformTitle,
metaDescription,
loginHeading,
loginButton,
metaImageUrl,
metaTitle,
}
}
} catch (e) {
console.error("There was an issue retrieving the license", e)
}
try { try {
body._id = configs.generateConfigID(type) body._id = configs.generateConfigID(type)
const response = await configs.save(body) const response = await configs.save(body)
@ -276,6 +309,9 @@ export async function publicSettings(
// settings // settings
const configDoc = await configs.getSettingsConfigDoc() const configDoc = await configs.getSettingsConfigDoc()
const config = configDoc.config const config = configDoc.config
const branding = await pro.branding.getBrandingConfig(config)
// enrich the logo url - empty url means deleted // enrich the logo url - empty url means deleted
if (config.logoUrl && config.logoUrl !== "") { if (config.logoUrl && config.logoUrl !== "") {
config.logoUrl = objectStore.getGlobalFileUrl( config.logoUrl = objectStore.getGlobalFileUrl(
@ -285,6 +321,15 @@ export async function publicSettings(
) )
} }
if (branding.faviconUrl && branding.faviconUrl !== "") {
// @ts-ignore
config.faviconUrl = objectStore.getGlobalFileUrl(
"settings",
"faviconUrl",
branding.faviconUrl
)
}
// google // google
const googleConfig = await configs.getGoogleConfig() const googleConfig = await configs.getGoogleConfig()
const preActivated = googleConfig && googleConfig.activated == null const preActivated = googleConfig && googleConfig.activated == null
@ -305,6 +350,7 @@ export async function publicSettings(
_rev: configDoc._rev, _rev: configDoc._rev,
config: { config: {
...config, ...config,
...branding,
google, google,
oidc, oidc,
isSSOEnforced, isSSOEnforced,

View File

@ -286,6 +286,7 @@ describe("configs", () => {
type: "settings", type: "settings",
config: { config: {
company: "Budibase", company: "Budibase",
emailBrandingEnabled: true,
logoUrl: "", logoUrl: "",
analyticsEnabled: false, analyticsEnabled: false,
google: false, google: false,
@ -294,6 +295,7 @@ describe("configs", () => {
oidc: false, oidc: false,
oidcCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/oidc/callback`, oidcCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/oidc/callback`,
platformUrl: "http://localhost:10000", platformUrl: "http://localhost:10000",
testimonialsEnabled: true,
}, },
} }
delete body._rev delete body._rev

View File

@ -20,6 +20,7 @@ export enum TemplateType {
} }
export enum EmailTemplatePurpose { export enum EmailTemplatePurpose {
CORE = "core",
BASE = "base", BASE = "base",
PASSWORD_RECOVERY = "password_recovery", PASSWORD_RECOVERY = "password_recovery",
INVITATION = "invitation", INVITATION = "invitation",

View File

@ -0,0 +1,33 @@
{{#if enableEmailBranding}}
<tr>
<td cellpadding="0" cellspacing="0">
<table
align="center"
width="570"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<tbody>
<tr>
<td
style="padding:0 35px; padding-top: 15px;"
cellpadding="0"
cellspacing="0"
>
<img
width="32px"
style="margin-right:16px; vertical-align: middle;"
alt="Budibase Logo"
src="https://i.imgur.com/Xhdt1YP.png"
/>
<strong style="vertical-align: middle; font-size: 1.1em">
Budibase
</strong>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
{{/if}}

View File

@ -21,6 +21,7 @@ export const EmailTemplates = {
join(__dirname, "welcome.hbs") join(__dirname, "welcome.hbs")
), ),
[EmailTemplatePurpose.CUSTOM]: readStaticFile(join(__dirname, "custom.hbs")), [EmailTemplatePurpose.CUSTOM]: readStaticFile(join(__dirname, "custom.hbs")),
[EmailTemplatePurpose.CORE]: readStaticFile(join(__dirname, "core.hbs")),
} }
export function addBaseTemplates(templates: Template[], type?: string) { export function addBaseTemplates(templates: Template[], type?: string) {

View File

@ -0,0 +1,27 @@
import * as pro from "@budibase/pro"
export async function getLicensedConfig() {
let licensedConfig: object = {}
const defaults = {
emailBrandingEnabled: true,
testimonialsEnabled: true,
platformTitle: undefined,
metaDescription: undefined,
loginHeading: undefined,
loginButton: undefined,
metaImageUrl: undefined,
metaTitle: undefined,
}
try {
// License/Feature Checks
const enabled = await pro.features.isBrandingEnabled()
if (!enabled) {
licensedConfig = { ...defaults }
}
} catch (e) {
licensedConfig = { ...defaults }
console.info("Could not retrieve license", e)
}
return licensedConfig
}

View File

@ -1,6 +1,6 @@
import env from "../environment" import env from "../environment"
import { EmailTemplatePurpose, TemplateType } from "../constants" import { EmailTemplatePurpose, TemplateType } from "../constants"
import { getTemplateByPurpose } from "../constants/templates" import { getTemplateByPurpose, EmailTemplates } from "../constants/templates"
import { getSettingsTemplateContext } from "./templates" import { getSettingsTemplateContext } from "./templates"
import { processString } from "@budibase/string-templates" import { processString } from "@budibase/string-templates"
import { getResetPasswordCode, getInviteCode } from "./redis" import { getResetPasswordCode, getInviteCode } from "./redis"
@ -109,11 +109,16 @@ async function buildEmail(
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE), getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE),
getTemplateByPurpose(TYPE, purpose), getTemplateByPurpose(TYPE, purpose),
]) ])
if (!base || !body) {
// Change from branding to core
let core = EmailTemplates[EmailTemplatePurpose.CORE]
if (!base || !body || !core) {
throw "Unable to build email, missing base components" throw "Unable to build email, missing base components"
} }
base = base.contents base = base.contents
body = body.contents body = body.contents
let name = user ? user.name : undefined let name = user ? user.name : undefined
if (user && !name && user.firstName) { if (user && !name && user.firstName) {
name = user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName name = user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName
@ -126,8 +131,12 @@ async function buildEmail(
user: user || {}, user: user || {},
} }
body = await processString(body, context) // Prepend the core template
// this should now be the complete email HTML const fullBody = core + body
body = await processString(fullBody, context)
// this should now be the core email HTML
return processString(base, { return processString(base, {
...context, ...context,
body, body,

View File

@ -1,17 +1,22 @@
import { tenancy, configs } from "@budibase/backend-core" import { tenancy, configs } from "@budibase/backend-core"
import { SettingsInnerConfig } from "@budibase/types"
import { import {
InternalTemplateBinding, InternalTemplateBinding,
LOGO_URL, LOGO_URL,
EmailTemplatePurpose, EmailTemplatePurpose,
} from "../constants" } from "../constants"
import { checkSlashesInUrl } from "./index" import { checkSlashesInUrl } from "./index"
import { getLicensedConfig } from "./configs"
const BASE_COMPANY = "Budibase" const BASE_COMPANY = "Budibase"
import * as pro from "@budibase/pro"
export async function getSettingsTemplateContext( export async function getSettingsTemplateContext(
purpose: EmailTemplatePurpose, purpose: EmailTemplatePurpose,
code?: string | null code?: string | null
) { ) {
let settings = await configs.getSettingsConfig() const settings = await configs.getSettingsConfig()
const branding = await pro.branding.getBrandingConfig(settings)
const URL = settings.platformUrl const URL = settings.platformUrl
const context: any = { const context: any = {
[InternalTemplateBinding.LOGO_URL]: [InternalTemplateBinding.LOGO_URL]:
@ -25,6 +30,9 @@ export async function getSettingsTemplateContext(
[InternalTemplateBinding.CURRENT_DATE]: new Date().toISOString(), [InternalTemplateBinding.CURRENT_DATE]: new Date().toISOString(),
[InternalTemplateBinding.CURRENT_YEAR]: new Date().getFullYear(), [InternalTemplateBinding.CURRENT_YEAR]: new Date().getFullYear(),
} }
context["enableEmailBranding"] = branding.emailBrandingEnabled === true
// attach purpose specific context // attach purpose specific context
switch (purpose) { switch (purpose) {
case EmailTemplatePurpose.PASSWORD_RECOVERY: case EmailTemplatePurpose.PASSWORD_RECOVERY: