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 CoreRichTextField } from "./RichTextField.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 Slider } from "./Form/Slider.svelte"
|
||||
export { default as Accordion } from "./Accordion/Accordion.svelte"
|
||||
export { default as File } from "./Form/File.svelte"
|
||||
|
||||
// Renderers
|
||||
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<!doctype html>
|
||||
<html class="spectrum spectrum--medium spectrum--darkest" lang="en" dir="ltr">
|
||||
|
||||
<head>
|
||||
<meta charset='utf8'>
|
||||
<meta name='viewport' content='width=device-width'>
|
||||
<title>Budibase</title>
|
||||
<link rel='icon' href='/src/favicon.png'>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
||||
rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body id="app">
|
||||
<script type="module" src='/src/main.js'></script>
|
||||
</body>
|
||||
|
||||
</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 { CookieUtils, Constants } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import Branding from "./Branding.svelte"
|
||||
|
||||
let loaded = false
|
||||
|
||||
|
@ -146,6 +147,9 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<!--Portal branding overrides -->
|
||||
<Branding />
|
||||
|
||||
{#if loaded}
|
||||
<slot />
|
||||
{/if}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
async function login() {
|
||||
form.validate()
|
||||
if (Object.keys(errors).length > 0) {
|
||||
console.log("errors")
|
||||
console.log("errors", errors)
|
||||
return
|
||||
}
|
||||
try {
|
||||
|
@ -64,99 +64,106 @@
|
|||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<TestimonialPage>
|
||||
<Layout gap="L" noPadding>
|
||||
<Layout justifyItems="center" noPadding>
|
||||
{#if loaded}
|
||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
{/if}
|
||||
<Heading size="M">Log in to Budibase</Heading>
|
||||
</Layout>
|
||||
<Layout gap="S" noPadding>
|
||||
{#if loaded && ($organisation.google || $organisation.oidc)}
|
||||
<FancyForm>
|
||||
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
||||
<GoogleButton />
|
||||
</FancyForm>
|
||||
{/if}
|
||||
{#if loaded}
|
||||
<TestimonialPage enabled={$organisation.testimonialsEnabled}>
|
||||
<Layout gap="L" noPadding>
|
||||
<Layout justifyItems="center" noPadding>
|
||||
{#if loaded}
|
||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
{/if}
|
||||
<Heading size="M">
|
||||
{$organisation.loginHeading || "Log in to Budibase"}
|
||||
</Heading>
|
||||
</Layout>
|
||||
<Layout gap="S" noPadding>
|
||||
{#if loaded && ($organisation.google || $organisation.oidc)}
|
||||
<FancyForm>
|
||||
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
||||
<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}
|
||||
<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>
|
||||
<Layout gap="XS" noPadding justifyItems="center">
|
||||
<Button
|
||||
size="L"
|
||||
cta
|
||||
disabled={Object.keys(errors).length > 0}
|
||||
on:click={login}
|
||||
>
|
||||
{$organisation.loginButton || `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>
|
||||
{#if !$organisation.isSSOEnforced}
|
||||
<Layout gap="XS" noPadding justifyItems="center">
|
||||
<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>
|
||||
</TestimonialPage>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.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,
|
||||
Label,
|
||||
Input,
|
||||
Dropzone,
|
||||
notifications,
|
||||
Toggle,
|
||||
} from "@budibase/bbui"
|
||||
import { auth, organisation, admin } from "stores/portal"
|
||||
import { API } from "api"
|
||||
import { writable } from "svelte/store"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
|
@ -28,32 +26,14 @@
|
|||
company: $organisation.company,
|
||||
platformUrl: $organisation.platformUrl,
|
||||
analyticsEnabled: $organisation.analyticsEnabled,
|
||||
logo: $organisation.logoUrl
|
||||
? { url: $organisation.logoUrl, type: "image", name: "Logo" }
|
||||
: null,
|
||||
})
|
||||
let loading = false
|
||||
|
||||
async function uploadLogo(file) {
|
||||
try {
|
||||
let data = new FormData()
|
||||
data.append("file", file)
|
||||
await API.uploadLogo(data)
|
||||
} catch (error) {
|
||||
notifications.error("Error uploading logo")
|
||||
}
|
||||
}
|
||||
let loading = false
|
||||
|
||||
async function saveConfig() {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
// Upload logo if required
|
||||
if ($values.logo && !$values.logo.url) {
|
||||
await uploadLogo($values.logo)
|
||||
await organisation.init()
|
||||
}
|
||||
|
||||
const config = {
|
||||
isSSOEnforced: $values.isSSOEnforced,
|
||||
company: $values.company ?? "",
|
||||
|
@ -61,11 +41,6 @@
|
|||
analyticsEnabled: $values.analyticsEnabled,
|
||||
}
|
||||
|
||||
// Remove logo if required
|
||||
if (!$values.logo) {
|
||||
config.logoUrl = ""
|
||||
}
|
||||
|
||||
// Update settings
|
||||
await organisation.save(config)
|
||||
} catch (error) {
|
||||
|
@ -87,21 +62,7 @@
|
|||
<Label size="L">Org. name</Label>
|
||||
<Input thin bind:value={$values.company} />
|
||||
</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}
|
||||
<div class="field">
|
||||
<Label
|
||||
|
@ -137,10 +98,4 @@
|
|||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
.file {
|
||||
max-width: 30ch;
|
||||
}
|
||||
.logo {
|
||||
align-items: start;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -13,9 +13,11 @@ export const createLicensingStore = () => {
|
|||
license: undefined,
|
||||
isFreePlan: true,
|
||||
isEnterprisePlan: true,
|
||||
isBusinessPlan: true,
|
||||
// features
|
||||
groupsEnabled: false,
|
||||
backupsEnabled: false,
|
||||
brandingEnabled: false,
|
||||
// the currently used quotas from the db
|
||||
quotaUsage: undefined,
|
||||
// derived quota metrics for percentages used
|
||||
|
@ -57,6 +59,7 @@ export const createLicensingStore = () => {
|
|||
const planType = license?.plan.type
|
||||
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
|
||||
const isFreePlan = planType === Constants.PlanType.FREE
|
||||
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
|
||||
const groupsEnabled = license.features.includes(
|
||||
Constants.Features.USER_GROUPS
|
||||
)
|
||||
|
@ -69,7 +72,9 @@ export const createLicensingStore = () => {
|
|||
const enforceableSSO = license.features.includes(
|
||||
Constants.Features.ENFORCEABLE_SSO
|
||||
)
|
||||
|
||||
const brandingEnabled = license.features.includes(
|
||||
Constants.Features.BRANDING
|
||||
)
|
||||
const auditLogsEnabled = license.features.includes(
|
||||
Constants.Features.AUDIT_LOGS
|
||||
)
|
||||
|
@ -79,8 +84,10 @@ export const createLicensingStore = () => {
|
|||
license,
|
||||
isEnterprisePlan,
|
||||
isFreePlan,
|
||||
isBusinessPlan,
|
||||
groupsEnabled,
|
||||
backupsEnabled,
|
||||
brandingEnabled,
|
||||
environmentVariablesEnabled,
|
||||
auditLogsEnabled,
|
||||
enforceableSSO,
|
||||
|
|
|
@ -50,6 +50,10 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
|||
title: "Organisation",
|
||||
href: "/builder/portal/settings/organisation",
|
||||
},
|
||||
{
|
||||
title: "Branding",
|
||||
href: "/builder/portal/settings/branding",
|
||||
},
|
||||
{
|
||||
title: "Environment",
|
||||
href: "/builder/portal/settings/environment",
|
||||
|
|
|
@ -6,6 +6,15 @@ import _ from "lodash"
|
|||
const DEFAULT_CONFIG = {
|
||||
platformUrl: "",
|
||||
logoUrl: undefined,
|
||||
faviconUrl: undefined,
|
||||
emailBrandingEnabled: true,
|
||||
testimonialsEnabled: true,
|
||||
platformTitle: "Budibase",
|
||||
loginHeading: undefined,
|
||||
loginButton: undefined,
|
||||
metaDescription: undefined,
|
||||
metaImageUrl: undefined,
|
||||
metaTitle: undefined,
|
||||
docsUrl: undefined,
|
||||
company: "Budibase",
|
||||
oidc: undefined,
|
||||
|
|
|
@ -16,6 +16,7 @@ export { rowSelectionStore } from "./rowSelection.js"
|
|||
export { blockStore } from "./blocks.js"
|
||||
export { environmentStore } from "./environment"
|
||||
export { eventStore } from "./events.js"
|
||||
export { orgStore } from "./org.js"
|
||||
export {
|
||||
dndStore,
|
||||
dndIndex,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { routeStore } from "./routes"
|
||||
import { appStore } from "./app"
|
||||
import { orgStore } from "./org"
|
||||
|
||||
export async function initialise() {
|
||||
await routeStore.actions.fetchRoutes()
|
||||
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 { builderStore } from "./builder"
|
||||
import { appStore } from "./app"
|
||||
import { orgStore } from "./org"
|
||||
import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { findComponentById, findComponentParent } from "../utils/components.js"
|
||||
|
@ -14,6 +15,7 @@ const createScreenStore = () => {
|
|||
appStore,
|
||||
routeStore,
|
||||
builderStore,
|
||||
orgStore,
|
||||
dndParent,
|
||||
dndIndex,
|
||||
dndIsNewComponent,
|
||||
|
@ -23,6 +25,7 @@ const createScreenStore = () => {
|
|||
$appStore,
|
||||
$routeStore,
|
||||
$builderStore,
|
||||
$orgStore,
|
||||
$dndParent,
|
||||
$dndIndex,
|
||||
$dndIsNewComponent,
|
||||
|
@ -146,6 +149,11 @@ const createScreenStore = () => {
|
|||
if (!navigationSettings.title && !navigationSettings.hideTitle) {
|
||||
navigationSettings.title = $appStore.application?.name
|
||||
}
|
||||
|
||||
// Default to the org logo
|
||||
if (!navigationSettings.logoUrl) {
|
||||
navigationSettings.logoUrl = $orgStore?.logoUrl
|
||||
}
|
||||
}
|
||||
activeLayout = {
|
||||
_id: "layout",
|
||||
|
|
|
@ -23,11 +23,6 @@
|
|||
chalk "^2.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":
|
||||
version "0.3.2"
|
||||
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.
|
||||
* @param name the name of the OIDC provider
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
import Covanta from "../../assets/covanta.png"
|
||||
import Schnellecke from "../../assets/schnellecke.png"
|
||||
|
||||
export let enabled = true
|
||||
|
||||
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.",
|
||||
|
@ -33,23 +35,25 @@
|
|||
|
||||
<SplitPage>
|
||||
<slot />
|
||||
<div class="wrapper" slot="right">
|
||||
<div class="testimonial">
|
||||
<Layout noPadding gap="S">
|
||||
<img
|
||||
width={testimonial.imageSize}
|
||||
alt="a-happy-budibase-user"
|
||||
src={testimonial.image}
|
||||
/>
|
||||
<div class="text">
|
||||
"{testimonial.text}"
|
||||
</div>
|
||||
<div class="author">
|
||||
<div class="name">{testimonial.name}</div>
|
||||
<div class="company">{testimonial.role}</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
<div class:wrapper={enabled} slot="right">
|
||||
{#if enabled}
|
||||
<div class="testimonial">
|
||||
<Layout noPadding gap="S">
|
||||
<img
|
||||
width={testimonial.imageSize}
|
||||
alt="a-happy-budibase-user"
|
||||
src={testimonial.image}
|
||||
/>
|
||||
<div class="text">
|
||||
"{testimonial.text}"
|
||||
</div>
|
||||
<div class="author">
|
||||
<div class="name">{testimonial.name}</div>
|
||||
<div class="company">{testimonial.role}</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</SplitPage>
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ export const Features = {
|
|||
ENVIRONMENT_VARIABLES: "environmentVariables",
|
||||
AUDIT_LOGS: "auditLogs",
|
||||
ENFORCEABLE_SSO: "enforceableSSO",
|
||||
BRANDING: "branding",
|
||||
}
|
||||
|
||||
// Role IDs
|
||||
|
|
|
@ -11,10 +11,12 @@ import {
|
|||
} from "../../../utilities/fileSystem"
|
||||
import env from "../../../environment"
|
||||
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 fs from "fs"
|
||||
import sdk from "../../../sdk"
|
||||
import * as pro from "@budibase/pro"
|
||||
|
||||
const send = require("koa-send")
|
||||
|
||||
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) {
|
||||
const db = context.getAppDB({ skip_setup: true })
|
||||
const appInfo = await db.get(DocumentType.APP_METADATA)
|
||||
let appId = context.getAppId()
|
||||
//Public Settings
|
||||
const { config } = await configs.getSettingsConfigDoc()
|
||||
const branding = await pro.branding.getBrandingConfig(config)
|
||||
|
||||
if (!env.isJest()) {
|
||||
const App = require("./templates/BudibaseApp.svelte").default
|
||||
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
|
||||
const { head, html, css } = App.render({
|
||||
metaImage:
|
||||
"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,
|
||||
})
|
||||
let db
|
||||
try {
|
||||
db = context.getAppDB({ skip_setup: true })
|
||||
const appInfo = await db.get(DocumentType.APP_METADATA)
|
||||
let appId = context.getAppId()
|
||||
|
||||
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
|
||||
if (!env.isJest()) {
|
||||
const App = require("./templates/BudibaseApp.svelte").default
|
||||
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
|
||||
const { head, html, css } = App.render({
|
||||
metaImage:
|
||||
branding?.metaImageUrl ||
|
||||
"https://res.cloudinary.com/daog6scxm/image/upload/v1666109324/meta-images/budibase-meta-image_uukc1m.png",
|
||||
metaDescription: branding?.metaDescription || "",
|
||||
metaTitle:
|
||||
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>
|
||||
export let title = ""
|
||||
export let favicon = ""
|
||||
|
||||
export let metaImage = ""
|
||||
export let metaTitle = ""
|
||||
export let metaDescription = ""
|
||||
|
||||
export let clientLibPath
|
||||
export let usedPlugins
|
||||
|
@ -13,18 +16,33 @@
|
|||
name="viewport"
|
||||
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 -->
|
||||
<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:title" content="{title} - built with Budibase" />
|
||||
<meta property="og:description" content={metaDescription} />
|
||||
<meta property="og:type" content="website" />
|
||||
<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>
|
||||
<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="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
|
@ -83,11 +101,16 @@
|
|||
|
||||
<body id="app">
|
||||
<div id="error">
|
||||
<h1>There was an error loading your app</h1>
|
||||
<h2>
|
||||
The Budibase client library could not be loaded. Try republishing your
|
||||
app.
|
||||
</h2>
|
||||
{#if clientLibPath}
|
||||
<h1>There was an error loading your app</h1>
|
||||
<h2>
|
||||
The Budibase client library could not be loaded. Try republishing your
|
||||
app.
|
||||
</h2>
|
||||
{:else}
|
||||
<h2>We couldn't find that application</h2>
|
||||
<p />
|
||||
{/if}
|
||||
</div>
|
||||
<script type="application/javascript">
|
||||
window.INIT_TIME = Date.now()
|
||||
|
|
|
@ -22,6 +22,24 @@ export interface SMTPConfig extends Config {
|
|||
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 {
|
||||
platformUrl?: string
|
||||
company?: string
|
||||
|
|
|
@ -4,4 +4,5 @@ export enum Feature {
|
|||
ENVIRONMENT_VARIABLES = "environmentVariables",
|
||||
AUDIT_LOGS = "auditLogs",
|
||||
ENFORCEABLE_SSO = "enforceableSSO",
|
||||
BRANDING = "branding",
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
tenancy,
|
||||
} from "@budibase/backend-core"
|
||||
import { checkAnyUserExists } from "../../../utilities/users"
|
||||
import { getLicensedConfig } from "../../../utilities/configs"
|
||||
import {
|
||||
Config,
|
||||
ConfigType,
|
||||
|
@ -211,6 +212,38 @@ export async function save(ctx: UserCtx<Config>) {
|
|||
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 {
|
||||
body._id = configs.generateConfigID(type)
|
||||
const response = await configs.save(body)
|
||||
|
@ -276,6 +309,9 @@ export async function publicSettings(
|
|||
// settings
|
||||
const configDoc = await configs.getSettingsConfigDoc()
|
||||
const config = configDoc.config
|
||||
|
||||
const branding = await pro.branding.getBrandingConfig(config)
|
||||
|
||||
// enrich the logo url - empty url means deleted
|
||||
if (config.logoUrl && config.logoUrl !== "") {
|
||||
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
|
||||
const googleConfig = await configs.getGoogleConfig()
|
||||
const preActivated = googleConfig && googleConfig.activated == null
|
||||
|
@ -305,6 +350,7 @@ export async function publicSettings(
|
|||
_rev: configDoc._rev,
|
||||
config: {
|
||||
...config,
|
||||
...branding,
|
||||
google,
|
||||
oidc,
|
||||
isSSOEnforced,
|
||||
|
|
|
@ -286,6 +286,7 @@ describe("configs", () => {
|
|||
type: "settings",
|
||||
config: {
|
||||
company: "Budibase",
|
||||
emailBrandingEnabled: true,
|
||||
logoUrl: "",
|
||||
analyticsEnabled: false,
|
||||
google: false,
|
||||
|
@ -294,6 +295,7 @@ describe("configs", () => {
|
|||
oidc: false,
|
||||
oidcCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/oidc/callback`,
|
||||
platformUrl: "http://localhost:10000",
|
||||
testimonialsEnabled: true,
|
||||
},
|
||||
}
|
||||
delete body._rev
|
||||
|
|
|
@ -20,6 +20,7 @@ export enum TemplateType {
|
|||
}
|
||||
|
||||
export enum EmailTemplatePurpose {
|
||||
CORE = "core",
|
||||
BASE = "base",
|
||||
PASSWORD_RECOVERY = "password_recovery",
|
||||
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")
|
||||
),
|
||||
[EmailTemplatePurpose.CUSTOM]: readStaticFile(join(__dirname, "custom.hbs")),
|
||||
[EmailTemplatePurpose.CORE]: readStaticFile(join(__dirname, "core.hbs")),
|
||||
}
|
||||
|
||||
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 { EmailTemplatePurpose, TemplateType } from "../constants"
|
||||
import { getTemplateByPurpose } from "../constants/templates"
|
||||
import { getTemplateByPurpose, EmailTemplates } from "../constants/templates"
|
||||
import { getSettingsTemplateContext } from "./templates"
|
||||
import { processString } from "@budibase/string-templates"
|
||||
import { getResetPasswordCode, getInviteCode } from "./redis"
|
||||
|
@ -109,11 +109,16 @@ async function buildEmail(
|
|||
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE),
|
||||
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"
|
||||
}
|
||||
base = base.contents
|
||||
body = body.contents
|
||||
|
||||
let name = user ? user.name : undefined
|
||||
if (user && !name && user.firstName) {
|
||||
name = user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName
|
||||
|
@ -126,8 +131,12 @@ async function buildEmail(
|
|||
user: user || {},
|
||||
}
|
||||
|
||||
body = await processString(body, context)
|
||||
// this should now be the complete email HTML
|
||||
// Prepend the core template
|
||||
const fullBody = core + body
|
||||
|
||||
body = await processString(fullBody, context)
|
||||
|
||||
// this should now be the core email HTML
|
||||
return processString(base, {
|
||||
...context,
|
||||
body,
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
import { tenancy, configs } from "@budibase/backend-core"
|
||||
import { SettingsInnerConfig } from "@budibase/types"
|
||||
import {
|
||||
InternalTemplateBinding,
|
||||
LOGO_URL,
|
||||
EmailTemplatePurpose,
|
||||
} from "../constants"
|
||||
import { checkSlashesInUrl } from "./index"
|
||||
import { getLicensedConfig } from "./configs"
|
||||
|
||||
const BASE_COMPANY = "Budibase"
|
||||
import * as pro from "@budibase/pro"
|
||||
|
||||
export async function getSettingsTemplateContext(
|
||||
purpose: EmailTemplatePurpose,
|
||||
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 context: any = {
|
||||
[InternalTemplateBinding.LOGO_URL]:
|
||||
|
@ -25,6 +30,9 @@ export async function getSettingsTemplateContext(
|
|||
[InternalTemplateBinding.CURRENT_DATE]: new Date().toISOString(),
|
||||
[InternalTemplateBinding.CURRENT_YEAR]: new Date().getFullYear(),
|
||||
}
|
||||
|
||||
context["enableEmailBranding"] = branding.emailBrandingEnabled === true
|
||||
|
||||
// attach purpose specific context
|
||||
switch (purpose) {
|
||||
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
||||
|
|
Loading…
Reference in New Issue