1
0
Fork 0
mirror of synced 2024-10-01 09:38:55 +13:00

Working commit

This commit is contained in:
Dean 2023-03-13 12:33:16 +00:00
parent fa6f5caa75
commit 5ce52cad06
21 changed files with 721 additions and 182 deletions

View file

@ -0,0 +1,133 @@
<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 //support multi?
export let title = "Upload file"
export let disabled = false
export let extensions = "*"
export let handleFileTooLarge = null
export let handleTooManyFiles = null
export let fileSizeLimit = BYTES_IN_MB * 20 // Centralise
export let id = null
const dispatch = createEventDispatcher()
//Is this necessary?
const fieldId = id || uuid()
// Centralise all in new file section?
const BYTES_IN_KB = 1000
const BYTES_IN_MB = 1000000
let fileInput
// $: file = value[0] || null
// const imageExtensions = [
// "png",
// "tiff",
// "gif",
// "raw",
// "jpg",
// "jpeg",
// "svg",
// "bmp",
// "jfif",
// ]
// Should support only 1 file for now.
// Files[0]
//What is the limit? 50mb?
async function processFileList(fileList) {
if (
handleFileTooLarge &&
Array.from(fileList).some(file => file.size >= fileSizeLimit)
) {
handleFileTooLarge(fileSizeLimit, value)
return
}
const fileCount = fileList.length + value.length
if (handleTooManyFiles && maximum && fileCount > maximum) {
handleTooManyFiles(maximum)
return
}
if (processFiles) {
const processedFiles = await processFiles(fileList)
const newValue = [...value, ...processedFiles]
dispatch("change", newValue)
selectedImageIdx = newValue.length - 1
} else {
dispatch("change", fileList)
}
}
function handleFile(evt) {
console.log("Hello ", evt.target.files[0])
dispatch("change", evt.target.files[0])
//processFileList(evt.target.files)
}
function clearFile() {
dispatch("change", null)
}
</script>
<input
id={fieldId}
{disabled}
type="file"
accept={extensions}
bind:this={fileInput}
on:change={handleFile}
/>
<div class="field">
{#if value}
<div class="file-view">
<!-- <img alt="" src={value.url} /> -->
<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}
<div class="delete-button" on:click={clearFile}>
<Icon name="Close" size="XS" />
</div>
{/if}
</div>
{/if}
<ActionButton 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 {
cursor: pointer;
}
</style>

View file

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

View file

@ -0,0 +1,23 @@
<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 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} {title} {value} on:change={onChange} />
</Field>

View file

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

View file

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

View file

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

View file

@ -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 && $organisation.licenceAgreementEnabled}
/> <Body size="xs" textAlign="center">
<FancyInput By using Budibase Cloud
label="Password" <br />
value={formData.password} you are agreeing to our
type="password" <Link
on:change={e => { href="https://budibase.com/eula"
formData = { target="_blank"
...formData, secondary={true}
password: e.detail, >
} License Agreement
}} </Link>
validate={() => { </Body>
let fieldError = {
password: !formData.password
? "Please enter your password"
: undefined,
}
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.password}
/>
</FancyForm>
{/if} {/if}
</Layout> </Layout>
{#if !$organisation.isSSOEnforced} </TestimonialPage>
<Layout gap="XS" noPadding justifyItems="center"> {/if}
<Button
size="L"
cta
disabled={Object.keys(errors).length > 0}
on:click={login}
>
Log in to {company}
</Button>
</Layout>
<Layout gap="XS" noPadding justifyItems="center">
<div class="user-actions">
<ActionButton size="L" quiet on:click={() => $goto("./forgot")}>
Forgot password?
</ActionButton>
</div>
</Layout>
{/if}
{#if cloud}
<Body size="xs" textAlign="center">
By using Budibase Cloud
<br />
you are agreeing to our
<Link href="https://budibase.com/eula" target="_blank" secondary={true}>
License Agreement
</Link>
</Body>
{/if}
</Layout>
</TestimonialPage>
<style> <style>
.user-actions { .user-actions {

View file

@ -4,53 +4,328 @@
Heading, Heading,
Body, Body,
Divider, Divider,
Label, File,
Dropzone,
notifications, notifications,
Tags,
Tag,
Button,
Toggle,
Input,
Label,
} from "@budibase/bbui" } from "@budibase/bbui"
import { auth, organisation } from "stores/portal" import { auth, organisation, licensing, admin } from "stores/portal"
import { API } from "api" import { API } from "api"
import { onMount } from "svelte"
$: logo: $organisation.logoUrl let loaded = false
? { url: $organisation.logoUrl, type: "image", name: "Logo" } let saving = false
: null, let logoFile = null
async function uploadLogo(file) { let faviconFile = null
try {
let data = new FormData() let config = {}
data.append("file", file) let updated = false
await API.uploadLogo(data) $: onConfigUpdate(config)
} catch (error) {
notifications.error("Error uploading logo") const onConfigUpdate = config => {
if (!loaded || updated) {
return
}
updated = true
console.log("config updated ", config)
}
$: logo = config.logoUrl
? { url: config.logoUrl, type: "image", name: "Logo" }
: null
$: favicon = config.faviconUrl
? { url: config.logoUrl, type: "image", name: "Favicon" }
: null
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
}
// Limit file types
// PNG, GIF, or ICO?
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
console.log("Save Config")
if (logoFile) {
const logoResp = await uploadLogo(logoFile)
if (logoResp.url) {
config = {
...config,
logoUrl: logoResp.url,
}
} else {
//would have to delete
} }
} }
if (faviconFile) {
const faviconResp = await uploadFavicon(faviconFile)
if (faviconResp.url) {
config = {
...config,
faviconUrl: faviconResp.url,
}
}
}
console.log("SAVE CONFIG ", config)
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")
}
saving = false
}
onMount(async () => {
await organisation.init()
const {
faviconUrl,
logoUrl,
platformTitle,
emailBrandingEnabled,
appFooterEnabled,
loginHeading,
loginButton,
licenceAgreementEnabled,
testimonialsEnabled,
} = $organisation
config = {
faviconUrl,
logoUrl,
platformTitle,
emailBrandingEnabled,
appFooterEnabled,
loginHeading,
loginButton,
licenceAgreementEnabled,
testimonialsEnabled,
}
loaded = true
})
</script> </script>
{#if $auth.isAdmin} {#if $auth.isAdmin && loaded}
<Layout noPadding> <Layout noPadding>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Heading size="M">Branding</Heading> <div class="title">
<Body /> <Heading size="M">Branding</Heading>
{#if !$licensing.isBusinessPlan}
<Tags>
<Tag icon="LockClosed">Business</Tag>
</Tags>
{/if}
</div>
<Body>Remove all Budibase branding and use your own.</Body>
</Layout> </Layout>
<Divider /> <Divider />
<div class="fields"> <div class="branding fields">
<div class="field logo"> <div class="field">
<Label size="L">Logo</Label> <Label size="L">Logo</Label>
<div class="file"> <File
<Dropzone title="Upload image"
value={[logo]} on:change={e => {
on:change={e => { console.log("Updated Logo")
if (!e.detail || e.detail.length === 0) { let clone = { ...config }
logo = null if (e.detail) {
} else { logoFile = e.detail
logo = e.detail[0] } else {
} clone.logoUrl = ""
}} }
/> config = clone
}}
value={logo}
/>
</div>
<div class="field">
<Label size="L">Favicon</Label>
<File
title="Upload image"
on:change={e => {
console.log("Updated Favicon")
let clone = { ...config }
if (e.detail) {
faviconFile = e.detail
} else {
clone.faviconUrl = ""
}
config = clone
}}
value={favicon}
/>
</div>
<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 || "Budibase"}
/>
</div>
<div>
<Toggle
text={"Remove Buidbase brand from emails"}
on:change={e => {
let clone = { ...config }
clone.emailBrandingEnabled = !e.detail
config = clone
}}
value={!config.emailBrandingEnabled}
/>
<Toggle
text={"Remove Budibase footer from apps"}
on:change={e => {
let clone = { ...config }
clone.appFooterEnabled = !e.detail
config = clone
}}
value={!config.appFooterEnabled}
/>
</div>
</div>
<!-- Should this be displayed? -->
{#if !$admin.cloud}
<Divider />
<Layout gap="XS" noPadding>
<Heading size="S">Login page (Self host)</Heading>
<Body>You can only customise your login page in self host</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 || "Log in to Budibase"}
/>
</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 || "Log in to Budibase"}
/>
</div>
<div>
<Toggle
text={"Remove customer testimonials"}
on:change={e => {
let clone = { ...config }
clone.testimonialsEnabled = !e.detail
config = clone
}}
value={!config.testimonialsEnabled}
/>
<Toggle
text={"Remove licence agreement"}
on:change={e => {
let clone = { ...config }
clone.licenceAgreementEnabled = !e.detail
config = clone
}}
value={!config.licenceAgreementEnabled}
/>
</div>
</div> </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">Header</Label>
<Input
on:change={e => {
let clone = { ...config }
clone.loginHeading = e.detail ? e.detail : ""
config = clone
}}
value={config.loginHeading || "Log in to Budibase"}
/> -->
</div>
</div>
</div>
<div>
<Button on:click={saveConfig} cta disabled={saving || !updated}>
Save
</Button>
</div> </div>
</Layout> </Layout>
{/if} {/if}
<style> <style>
.title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: var(--spacing-m);
}
.branding,
.login {
width: 60%;
}
.fields {
display: grid;
grid-gap: var(--spacing-m);
}
.field {
display: grid;
grid-template-columns: 80px 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style> </style>

View file

@ -28,21 +28,21 @@
company: $organisation.company, company: $organisation.company,
platformUrl: $organisation.platformUrl, platformUrl: $organisation.platformUrl,
analyticsEnabled: $organisation.analyticsEnabled, analyticsEnabled: $organisation.analyticsEnabled,
logo: $organisation.logoUrl // logo: $organisation.logoUrl
? { url: $organisation.logoUrl, type: "image", name: "Logo" } // ? { url: $organisation.logoUrl, type: "image", name: "Logo" }
: null, // : null,
}) })
let loading = false let loading = false
async function uploadLogo(file) { // async function uploadLogo(file) {
try { // try {
let data = new FormData() // let data = new FormData()
data.append("file", file) // data.append("file", file)
await API.uploadLogo(data) // await API.uploadLogo(data)
} catch (error) { // } catch (error) {
notifications.error("Error uploading logo") // notifications.error("Error uploading logo")
} // }
} // }
async function saveConfig() { async function saveConfig() {
loading = true loading = true
@ -87,21 +87,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 +123,4 @@
grid-gap: var(--spacing-l); grid-gap: var(--spacing-l);
align-items: center; align-items: center;
} }
.file {
max-width: 30ch;
}
.logo {
align-items: start;
}
</style> </style>

View file

@ -13,6 +13,7 @@ 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,
@ -57,6 +58,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
) )
@ -79,6 +81,7 @@ export const createLicensingStore = () => {
license, license,
isEnterprisePlan, isEnterprisePlan,
isFreePlan, isFreePlan,
isBusinessPlan,
groupsEnabled, groupsEnabled,
backupsEnabled, backupsEnabled,
environmentVariablesEnabled, environmentVariablesEnabled,

View file

@ -5,7 +5,19 @@ import _ from "lodash"
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
platformUrl: "", platformUrl: "",
logoUrl: undefined, logoUrl: undefined,
faviconUrl: undefined,
emailBrandingEnabled: true,
appFooterEnabled: true,
// Self host only
testimonialsEnabled: true,
licenceAgreementEnabled: true,
platformTitle: "Budibase",
loginHeading: undefined,
loginButton: undefined,
docsUrl: undefined, docsUrl: undefined,
company: "Budibase", company: "Budibase",
oidc: undefined, oidc: undefined,

View file

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

View file

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

View file

@ -105,14 +105,16 @@ export const serveApp = async function (ctx: any) {
if (!env.isJest()) { if (!env.isJest()) {
const App = require("./templates/BudibaseApp.svelte").default const App = require("./templates/BudibaseApp.svelte").default
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins) const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
console.log(appInfo)
const { head, html, css } = App.render({ const { head, html, css } = App.render({
metaImage: metaImage:
"https://res.cloudinary.com/daog6scxm/image/upload/v1666109324/meta-images/budibase-meta-image_uukc1m.png", "https://res.cloudinary.com/daog6scxm/image/upload/v1666109324/meta-images/budibase-meta-image_uukc1m.png",
title: appInfo.name, title: appInfo.name, //Replace Title here?
production: env.isProd(), production: env.isProd(),
appId, appId,
clientLibPath: objectStore.clientLibraryUrl(appId!, appInfo.version), clientLibPath: objectStore.clientLibraryUrl(appId!, appInfo.version),
usedPlugins: plugins, usedPlugins: plugins,
favicon: objectStore.getGlobalFileUrl("settings", "faviconUrl"),
}) })
const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`) const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`)

View file

@ -22,7 +22,6 @@
<meta property="og:title" content="{title} - built with Budibase" /> <meta property="og:title" content="{title} - built with Budibase" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:image" content={metaImage} /> <meta property="og:image" content={metaImage} />
<title>{title}</title> <title>{title}</title>
<link rel="icon" type="image/png" href={favicon} /> <link rel="icon" type="image/png" href={favicon} />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" /> <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />

View file

@ -27,6 +27,20 @@ export interface SettingsInnerConfig {
company?: string company?: string
logoUrl?: string // Populated on read logoUrl?: string // Populated on read
logoUrlEtag?: string logoUrlEtag?: string
faviconUrl?: string
faviconUrlEtag?: string
emailBrandingEnabled?: boolean
// Self host only
appFooterEnabled?: boolean
testimonialsEnabled?: boolean
licenceAgreementEnabled?: boolean
platformTitle?: string
loginHeading?: string
loginButton?: string
uniqueTenantId?: string uniqueTenantId?: string
analyticsEnabled?: boolean analyticsEnabled?: boolean
isSSOEnforced?: boolean isSSOEnforced?: boolean

View file

@ -285,6 +285,14 @@ export async function publicSettings(
) )
} }
if (config.faviconUrl && config.faviconUrl !== "") {
config.faviconUrl = objectStore.getGlobalFileUrl(
"settings",
"faviconUrl",
config.faviconUrl
)
}
// google // google
const googleConfig = await configs.getGoogleConfig() const googleConfig = await configs.getGoogleConfig()
const preActivated = googleConfig?.activated == null const preActivated = googleConfig?.activated == null

View file

@ -0,0 +1,31 @@
{{#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;vertical-align: middle;"
cellpadding="0"
cellspacing="0"
>
<img
width="32px"
style="margin-right: 16px;"
alt="Budibase Logo"
src="https://i.imgur.com/Xhdt1YP.png"
/>
<h2 style="margin: 0px">Budibase</h2>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
{{/if}}

View file

@ -21,6 +21,8 @@ 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")),
//Core wrapper
["branding"]: readStaticFile(join(__dirname, "core.hbs")),
} }
export function addBaseTemplates(templates: Template[], type?: string) { export function addBaseTemplates(templates: Template[], type?: string) {

View file

@ -1,6 +1,6 @@
import env from "../environment" import env from "../environment"
import { EmailTemplatePurpose, TemplateType } from "../constants" import { EmailTemplatePurpose, TemplateType } from "../constants"
import { getTemplateByPurpose } from "../constants/templates" import { getTemplateByPurpose, EmailTemplates } from "../constants/templates"
import { getSettingsTemplateContext } from "./templates" import { getSettingsTemplateContext } from "./templates"
import { processString } from "@budibase/string-templates" import { processString } from "@budibase/string-templates"
import { getResetPasswordCode, getInviteCode } from "./redis" import { getResetPasswordCode, getInviteCode } from "./redis"
@ -108,30 +108,53 @@ async function buildEmail(
let [base, body] = await Promise.all([ let [base, body] = await Promise.all([
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE), getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE),
getTemplateByPurpose(TYPE, purpose), getTemplateByPurpose(TYPE, purpose),
//getTemplateByPurpose(TYPE, "branding"), //should generalise to 'branding'
]) ])
if (!base || !body) {
let branding = EmailTemplates["branding"]
if (!base || !body || !branding) {
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
//branding = branding.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
} }
context = { context = {
...context, ...context, //enableEmailBranding
contents, contents,
email, email,
name, name,
user: user || {}, user: user || {},
} }
body = await processString(body, context) const core = branding + body
// this should now be the complete email HTML
body = await processString(core, context)
// Conditional elements
// branding = await processString(branding, {
// ...context,
// body,
// })
// this should now be the core email HTML
return processString(base, { return processString(base, {
...context, ...context,
body, body, //: branding, // pass as body as usual
}) })
// body = await processString(body, context)
// // this should now be the coree email HTML
// return processString(base, {
// ...context,
// body,
// })
} }
/** /**

View file

@ -25,6 +25,11 @@ 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(),
} }
// Need to be careful with the binding as it shouldn't be surfacable
// Also default to false if not explicit
context["enableEmailBranding"] = settings.emailBrandingEnabled
// attach purpose specific context // attach purpose specific context
switch (purpose) { switch (purpose) {
case EmailTemplatePurpose.PASSWORD_RECOVERY: case EmailTemplatePurpose.PASSWORD_RECOVERY: