Working commit
This commit is contained in:
parent
fa6f5caa75
commit
5ce52cad06
|
@ -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>
|
|
@ -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,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>
|
|
@ -78,6 +78,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>
|
|
@ -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}
|
||||
|
|
|
@ -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 && $organisation.licenceAgreementEnabled}
|
||||
<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 {
|
||||
|
|
|
@ -4,53 +4,328 @@
|
|||
Heading,
|
||||
Body,
|
||||
Divider,
|
||||
Label,
|
||||
Dropzone,
|
||||
File,
|
||||
notifications,
|
||||
Tags,
|
||||
Tag,
|
||||
Button,
|
||||
Toggle,
|
||||
Input,
|
||||
Label,
|
||||
} from "@budibase/bbui"
|
||||
import { auth, organisation } from "stores/portal"
|
||||
import { auth, organisation, licensing, admin } from "stores/portal"
|
||||
import { API } from "api"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
$: logo: $organisation.logoUrl
|
||||
? { url: $organisation.logoUrl, type: "image", name: "Logo" }
|
||||
: null,
|
||||
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 loaded = false
|
||||
let saving = false
|
||||
let logoFile = null
|
||||
let faviconFile = null
|
||||
|
||||
let config = {}
|
||||
let updated = false
|
||||
$: onConfigUpdate(config)
|
||||
|
||||
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>
|
||||
|
||||
{#if $auth.isAdmin}
|
||||
{#if $auth.isAdmin && loaded}
|
||||
<Layout noPadding>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="M">Branding</Heading>
|
||||
<Body />
|
||||
<div class="title">
|
||||
<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>
|
||||
<Divider />
|
||||
<div class="fields">
|
||||
<div class="field logo">
|
||||
<div class="branding fields">
|
||||
<div class="field">
|
||||
<Label size="L">Logo</Label>
|
||||
<div class="file">
|
||||
<Dropzone
|
||||
value={[logo]}
|
||||
on:change={e => {
|
||||
if (!e.detail || e.detail.length === 0) {
|
||||
logo = null
|
||||
} else {
|
||||
logo = e.detail[0]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<File
|
||||
title="Upload image"
|
||||
on:change={e => {
|
||||
console.log("Updated Logo")
|
||||
let clone = { ...config }
|
||||
if (e.detail) {
|
||||
logoFile = e.detail
|
||||
} 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>
|
||||
{/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>
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
<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>
|
||||
|
|
|
@ -28,21 +28,21 @@
|
|||
company: $organisation.company,
|
||||
platformUrl: $organisation.platformUrl,
|
||||
analyticsEnabled: $organisation.analyticsEnabled,
|
||||
logo: $organisation.logoUrl
|
||||
? { url: $organisation.logoUrl, type: "image", name: "Logo" }
|
||||
: null,
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
// async function uploadLogo(file) {
|
||||
// try {
|
||||
// let data = new FormData()
|
||||
// data.append("file", file)
|
||||
// await API.uploadLogo(data)
|
||||
// } catch (error) {
|
||||
// notifications.error("Error uploading logo")
|
||||
// }
|
||||
// }
|
||||
|
||||
async function saveConfig() {
|
||||
loading = true
|
||||
|
@ -87,21 +87,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 +123,4 @@
|
|||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
.file {
|
||||
max-width: 30ch;
|
||||
}
|
||||
.logo {
|
||||
align-items: start;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -13,6 +13,7 @@ export const createLicensingStore = () => {
|
|||
license: undefined,
|
||||
isFreePlan: true,
|
||||
isEnterprisePlan: true,
|
||||
isBusinessPlan: true,
|
||||
// features
|
||||
groupsEnabled: false,
|
||||
backupsEnabled: false,
|
||||
|
@ -57,6 +58,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
|
||||
)
|
||||
|
@ -79,6 +81,7 @@ export const createLicensingStore = () => {
|
|||
license,
|
||||
isEnterprisePlan,
|
||||
isFreePlan,
|
||||
isBusinessPlan,
|
||||
groupsEnabled,
|
||||
backupsEnabled,
|
||||
environmentVariablesEnabled,
|
||||
|
|
|
@ -5,7 +5,19 @@ import _ from "lodash"
|
|||
|
||||
const DEFAULT_CONFIG = {
|
||||
platformUrl: "",
|
||||
|
||||
logoUrl: undefined,
|
||||
faviconUrl: undefined,
|
||||
|
||||
emailBrandingEnabled: true,
|
||||
appFooterEnabled: true,
|
||||
// Self host only
|
||||
testimonialsEnabled: true,
|
||||
licenceAgreementEnabled: true,
|
||||
platformTitle: "Budibase",
|
||||
loginHeading: undefined,
|
||||
loginButton: undefined,
|
||||
|
||||
docsUrl: undefined,
|
||||
company: "Budibase",
|
||||
oidc: undefined,
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -105,14 +105,16 @@ export const serveApp = async function (ctx: any) {
|
|||
if (!env.isJest()) {
|
||||
const App = require("./templates/BudibaseApp.svelte").default
|
||||
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
|
||||
console.log(appInfo)
|
||||
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,
|
||||
title: appInfo.name, //Replace Title here?
|
||||
production: env.isProd(),
|
||||
appId,
|
||||
clientLibPath: objectStore.clientLibraryUrl(appId!, appInfo.version),
|
||||
usedPlugins: plugins,
|
||||
favicon: objectStore.getGlobalFileUrl("settings", "faviconUrl"),
|
||||
})
|
||||
|
||||
const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`)
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
<meta property="og:title" content="{title} - built with Budibase" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content={metaImage} />
|
||||
|
||||
<title>{title}</title>
|
||||
<link rel="icon" type="image/png" href={favicon} />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
|
|
|
@ -27,6 +27,20 @@ export interface SettingsInnerConfig {
|
|||
company?: string
|
||||
logoUrl?: string // Populated on read
|
||||
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
|
||||
analyticsEnabled?: boolean
|
||||
isSSOEnforced?: boolean
|
||||
|
|
|
@ -285,6 +285,14 @@ export async function publicSettings(
|
|||
)
|
||||
}
|
||||
|
||||
if (config.faviconUrl && config.faviconUrl !== "") {
|
||||
config.faviconUrl = objectStore.getGlobalFileUrl(
|
||||
"settings",
|
||||
"faviconUrl",
|
||||
config.faviconUrl
|
||||
)
|
||||
}
|
||||
|
||||
// google
|
||||
const googleConfig = await configs.getGoogleConfig()
|
||||
const preActivated = googleConfig?.activated == null
|
||||
|
|
|
@ -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}}
|
|
@ -21,6 +21,8 @@ export const EmailTemplates = {
|
|||
join(__dirname, "welcome.hbs")
|
||||
),
|
||||
[EmailTemplatePurpose.CUSTOM]: readStaticFile(join(__dirname, "custom.hbs")),
|
||||
//Core wrapper
|
||||
["branding"]: readStaticFile(join(__dirname, "core.hbs")),
|
||||
}
|
||||
|
||||
export function addBaseTemplates(templates: Template[], type?: string) {
|
||||
|
|
|
@ -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"
|
||||
|
@ -108,30 +108,53 @@ async function buildEmail(
|
|||
let [base, body] = await Promise.all([
|
||||
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE),
|
||||
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"
|
||||
}
|
||||
base = base.contents
|
||||
body = body.contents
|
||||
|
||||
//branding = branding.contents
|
||||
|
||||
let name = user ? user.name : undefined
|
||||
if (user && !name && user.firstName) {
|
||||
name = user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName
|
||||
}
|
||||
context = {
|
||||
...context,
|
||||
...context, //enableEmailBranding
|
||||
contents,
|
||||
email,
|
||||
name,
|
||||
user: user || {},
|
||||
}
|
||||
|
||||
body = await processString(body, context)
|
||||
// this should now be the complete email HTML
|
||||
const core = branding + body
|
||||
|
||||
body = await processString(core, context)
|
||||
|
||||
// Conditional elements
|
||||
// branding = await processString(branding, {
|
||||
// ...context,
|
||||
// body,
|
||||
// })
|
||||
|
||||
// this should now be the core email HTML
|
||||
return processString(base, {
|
||||
...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,
|
||||
// })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,6 +25,11 @@ export async function getSettingsTemplateContext(
|
|||
[InternalTemplateBinding.CURRENT_DATE]: new Date().toISOString(),
|
||||
[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
|
||||
switch (purpose) {
|
||||
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
||||
|
|
Loading…
Reference in New Issue