Merge pull request #10093 from Budibase/feature/whitelabelling
White labelling updates
This commit is contained in:
commit
fcc623dc67
|
@ -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>
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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}}
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue