Browse Source

email template E2E, adding preview

pull/1479/head
Martin McKeaveney 5 years ago
parent
commit
3bf3fc5e02
  1. 19
      .github/CONTRIBUTING.md
  2. 61
      hosting/scripts/setup.js
  3. 3
      package.json
  4. 2
      packages/bbui/package.json
  5. 97
      packages/bbui/src/Form/Core/Dropzone.svelte
  6. 1
      packages/bbui/src/Form/Core/TextField.svelte
  7. 2
      packages/bbui/src/Form/Dropzone.svelte
  8. 1
      packages/bbui/src/Form/Input.svelte
  9. 2
      packages/bbui/src/Layout/Layout.svelte
  10. 9
      packages/bbui/src/Layout/Page.svelte
  11. 21
      packages/bbui/src/Modal/Modal.svelte
  12. 2
      packages/bbui/src/Table/CellRenderer.svelte
  13. 76
      packages/bbui/src/Table/Table.svelte
  14. 8
      packages/bbui/yarn.lock
  15. 4
      packages/builder/package.json
  16. 66
      packages/builder/src/actions.js
  17. 12
      packages/builder/src/components/common/ConfigChecklist.svelte
  18. 9
      packages/builder/src/components/common/ConfirmDialog.svelte
  19. 88
      packages/builder/src/components/start/AppCard.svelte
  20. 25
      packages/builder/src/components/start/AppList.svelte
  21. 80
      packages/builder/src/components/start/AppRow.svelte
  22. 239
      packages/builder/src/components/start/CreateAppModal.svelte
  23. 83
      packages/builder/src/components/start/Indicator.svelte
  24. 121
      packages/builder/src/components/start/Steps/Info.svelte
  25. 29
      packages/builder/src/components/start/Steps/User.svelte
  26. 2
      packages/builder/src/components/start/Steps/index.js
  27. 0
      packages/builder/src/pages/builder/app/[application]/_layout.svelte
  28. 8
      packages/builder/src/pages/builder/portal/_layout.svelte
  29. 7
      packages/builder/src/pages/builder/portal/apps/_layout.svelte
  30. 221
      packages/builder/src/pages/builder/portal/apps/index.svelte
  31. 15
      packages/builder/src/pages/builder/portal/email/TemplateBindings.svelte
  32. 17
      packages/builder/src/pages/builder/portal/email/TemplateLink.svelte
  33. 140
      packages/builder/src/pages/builder/portal/email/[template].svelte
  34. 99
      packages/builder/src/pages/builder/portal/email/index.svelte
  35. 2
      packages/builder/src/pages/builder/portal/oauth/index.svelte
  36. 44
      packages/builder/src/stores/portal/email.js
  37. 1
      packages/builder/src/stores/portal/index.js
  38. 10
      packages/builder/yarn.lock
  39. 2
      packages/client/package.json
  40. 2
      packages/server/package.json
  41. 3
      packages/server/src/api/index.js
  42. 4
      packages/server/src/utilities/workerRequests.js
  43. 1327
      packages/server/yarn.lock
  44. 2
      packages/standard-components/package.json
  45. 12
      packages/worker/src/api/controllers/admin/templates.js
  46. 93
      packages/worker/src/api/controllers/auth.js
  47. 7
      packages/worker/src/api/routes/tests/email.spec.js
  48. 92
      packages/worker/src/constants/index.js

19
.github/CONTRIBUTING.md

@ -92,6 +92,16 @@ then `cd ` into your local copy.
### 3. Install and Build ### 3. Install and Build
To develop the Budibase platform you'll need [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed.
#### Quick method
`yarn setup` will check that all necessary components are installed and setup the repo for usage.
#### Manual method
The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed).
`yarn` to install project dependencies `yarn` to install project dependencies
`yarn bootstrap` will install all budibase modules and symlink them together using lerna. `yarn bootstrap` will install all budibase modules and symlink them together using lerna.
@ -112,10 +122,17 @@ To run the budibase server and builder in dev mode (i.e. with live reloading):
1. Open a new console 1. Open a new console
2. `yarn dev` (from root) 2. `yarn dev` (from root)
3. Access the builder on http://localhost:4001/_builder/ 3. Access the builder on http://localhost:10000/builder
This will enable watch mode for both the builder app, server, client library and any component libraries. This will enable watch mode for both the builder app, server, client library and any component libraries.
### 5. Cleanup
If you wish to delete all the apps created in development and reset the environment then run the following:
1. `yarn nuke:docker` will wipe all the Budibase services
2. `yarn dev` will restart all the services
## Data Storage ## Data Storage
When you are running locally, budibase stores data on disk using [PouchDB](https://pouchdb.com/), as well as some JSON on local files. After setting up budibase, you can find all of this data in the `~/.budibase` directory. When you are running locally, budibase stores data on disk using [PouchDB](https://pouchdb.com/), as well as some JSON on local files. After setting up budibase, you can find all of this data in the `~/.budibase` directory.

61
hosting/scripts/setup.js

@ -0,0 +1,61 @@
#!/usr/bin/env node
const os = require("os")
const exec = require("child_process").exec
const fs = require("fs")
const platform = os.platform()
const windows = platform === "win32"
const mac = platform === "darwin"
const linux = platform === "linux"
function execute(command) {
return new Promise(resolve => {
exec(command, (err, stdout) => resolve(linux ? !!stdout : true))
})
}
async function commandExistsUnix(command) {
const unixCmd = `command -v ${command} 2>/dev/null && { echo >&1 ${command}; exit 0; }`
return execute(command)
}
async function commandExistsWindows(command) {
if (/[\x00-\x1f<>:"|?*]/.test(command)) {
return false
}
return execute(`where ${command}`)
}
function commandExists(command) {
return windows ? commandExistsWindows(command) : commandExistsUnix(command)
}
async function init() {
const docker = commandExists("docker")
const dockerCompose = commandExists("docker-compose")
if (docker && dockerCompose) {
console.log("Docker installed - continuing.")
return
}
if (mac) {
console.log(
"Please install docker by visiting: https://docs.docker.com/docker-for-mac/install/"
)
} else if (windows) {
console.log(
"Please install docker by visiting: https://docs.docker.com/docker-for-windows/install/"
)
} else if (linux) {
console.log("Beginning automated linux installation.")
await execute(`./hosting/scripts/linux/get-docker.sh`)
await execute(`./hosting/scripts/linux/get-docker-compose.sh`)
} else {
console.error(
"Platform unknown - please look online for information about installing docker for our OS."
)
}
console.log("Once installation complete please re-run the setup script.")
process.exit(-1)
}
init()

3
package.json

@ -14,9 +14,10 @@
"prettier-plugin-svelte": "^2.2.0", "prettier-plugin-svelte": "^2.2.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0", "rollup-plugin-replace": "^2.2.0",
"svelte": "^3.37.0" "svelte": "^3.38.2"
}, },
"scripts": { "scripts": {
"setup": "./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
"bootstrap": "lerna link && lerna bootstrap", "bootstrap": "lerna link && lerna bootstrap",
"build": "lerna run build", "build": "lerna run build",
"initialise": "lerna run initialise", "initialise": "lerna run initialise",

2
packages/bbui/package.json

@ -27,7 +27,7 @@
"rollup-plugin-postcss": "^4.0.0", "rollup-plugin-postcss": "^4.0.0",
"rollup-plugin-svelte": "^7.1.0", "rollup-plugin-svelte": "^7.1.0",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"svelte": "^3.37.0" "svelte": "^3.38.2"
}, },
"keywords": [ "keywords": [
"svelte" "svelte"

97
packages/bbui/src/Form/Core/Dropzone.svelte

@ -15,6 +15,8 @@
export let fileSizeLimit = BYTES_IN_MB * 20 export let fileSizeLimit = BYTES_IN_MB * 20
export let processFiles = null export let processFiles = null
export let handleFileTooLarge = null export let handleFileTooLarge = null
export let gallery = true
export let error = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const imageExtensions = [ const imageExtensions = [
@ -52,6 +54,8 @@
const newValue = [...value, ...processedFiles] const newValue = [...value, ...processedFiles]
dispatch("change", newValue) dispatch("change", newValue)
selectedImageIdx = newValue.length - 1 selectedImageIdx = newValue.length - 1
} else {
dispatch("change", fileList)
} }
} }
@ -94,47 +98,68 @@
<div class="container"> <div class="container">
{#if selectedImage} {#if selectedImage}
<div class="gallery"> {#if gallery}
<div class="title"> <div class="gallery">
<div class="filename">{selectedImage.name}</div> <div class="title">
<div class="filesize"> <div class="filename">{selectedImage.name}</div>
{#if selectedImage.size <= BYTES_IN_MB} <div class="filesize">
{`${selectedImage.size / BYTES_IN_KB} KB`} {#if selectedImage.size <= BYTES_IN_MB}
{:else}{`${selectedImage.size / BYTES_IN_MB} MB`}{/if} {`${selectedImage.size / BYTES_IN_KB} KB`}
{:else}{`${selectedImage.size / BYTES_IN_MB} MB`}{/if}
</div>
{#if !disabled}
<div class="delete-button" on:click={removeFile}>
<Icon name="Close" />
</div>
{/if}
</div> </div>
{#if !disabled} {#if isImage}
<div class="delete-button" on:click={removeFile}> <img alt="preview" src={selectedImage.url} />
<Icon name="Close" /> {:else}
<div class="placeholder">
<div class="extension">{selectedImage.extension}</div>
<div>Preview not supported</div>
</div> </div>
{/if} {/if}
</div> <div
{#if isImage} class="nav left"
<img alt="preview" src={selectedImage.url} /> class:visible={selectedImageIdx > 0}
{:else} on:click={navigateLeft}
<div class="placeholder"> >
<div class="extension">{selectedImage.extension}</div> <Icon name="ChevronLeft" />
<div>Preview not supported</div>
</div> </div>
{/if} <div
<div class="nav right"
class="nav left" class:visible={selectedImageIdx < fileCount - 1}
class:visible={selectedImageIdx > 0} on:click={navigateRight}
on:click={navigateLeft} >
> <Icon name="ChevronRight" />
<Icon name="ChevronLeft" /> </div>
</div> <div class="footer">File {selectedImageIdx + 1} of {fileCount}</div>
<div
class="nav right"
class:visible={selectedImageIdx < fileCount - 1}
on:click={navigateRight}
>
<Icon name="ChevronRight" />
</div> </div>
<div class="footer">File {selectedImageIdx + 1} of {fileCount}</div> {:else if value?.length}
</div> {#each value as file}
<div class="gallery">
<div class="title">
<div class="filename">{file.name}</div>
<div class="filesize">
{#if file.size <= BYTES_IN_MB}
{`${file.size / BYTES_IN_KB} KB`}
{:else}{`${file.size / BYTES_IN_MB} MB`}{/if}
</div>
{#if !disabled}
<div class="delete-button" on:click={removeFile}>
<Icon name="Close" />
</div>
{/if}
</div>
</div>
{/each}
{/if}
{/if} {/if}
<div <div
class="spectrum-Dropzone" class="spectrum-Dropzone"
class:is-invalid={!!error}
class:disabled class:disabled
role="region" role="region"
tabindex="0" tabindex="0"
@ -245,6 +270,9 @@
.spectrum-Dropzone { .spectrum-Dropzone {
user-select: none; user-select: none;
} }
.spectrum-Dropzone.is-invalid {
border-color: var(--spectrum-global-color-red-400);
}
input[type="file"] { input[type="file"] {
display: none; display: none;
} }
@ -276,7 +304,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: center;
} }
.filename { .filename {
flex: 1 1 auto; flex: 1 1 auto;
@ -331,6 +359,7 @@
.delete-button { .delete-button {
transition: all 0.3s; transition: all 0.3s;
margin-left: 10px; margin-left: 10px;
display: flex;
} }
.delete-button i { .delete-button i {
font-size: 2em; font-size: 2em;

1
packages/bbui/src/Form/Core/TextField.svelte

@ -37,6 +37,7 @@
} }
focus = false focus = false
updateValue(event.target.value) updateValue(event.target.value)
dispatch("blur")
} }
const updateValueOnEnter = event => { const updateValueOnEnter = event => {

2
packages/bbui/src/Form/Dropzone.svelte

@ -11,6 +11,7 @@
export let fileSizeLimit = undefined export let fileSizeLimit = undefined
export let processFiles = undefined export let processFiles = undefined
export let handleFileTooLarge = undefined export let handleFileTooLarge = undefined
export let gallery = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -27,6 +28,7 @@
{fileSizeLimit} {fileSizeLimit}
{processFiles} {processFiles}
{handleFileTooLarge} {handleFileTooLarge}
{gallery}
on:change={onChange} on:change={onChange}
/> />
</Field> </Field>

1
packages/bbui/src/Form/Input.svelte

@ -30,5 +30,6 @@
on:change={onChange} on:change={onChange}
on:click on:click
on:input on:input
on:blur
/> />
</Field> </Field>

2
packages/bbui/src/Layout/Layout.svelte

@ -5,9 +5,11 @@
export let noPadding = false export let noPadding = false
export let gap = "M" export let gap = "M"
export let noGap = false export let noGap = false
export let alignContent = "normal"
</script> </script>
<div <div
style="align-content:{alignContent};"
class:horizontal class:horizontal
class="container paddingX-{!noPadding && paddingX} paddingY-{!noPadding && class="container paddingX-{!noPadding && paddingX} paddingY-{!noPadding &&
paddingY} gap-{!noGap && gap}" paddingY} gap-{!noGap && gap}"

9
packages/bbui/src/Layout/Page.svelte

@ -8,8 +8,10 @@
<style> <style>
div { div {
display: grid; display: flex;
grid-template-columns: 1fr; flex-direction: column;
justify-content: flex-start;
align-items: stretch;
max-width: 80ch; max-width: 80ch;
margin: 0 auto; margin: 0 auto;
padding: calc(var(--spacing-xl) * 2); padding: calc(var(--spacing-xl) * 2);
@ -18,6 +20,7 @@
.wide { .wide {
max-width: none; max-width: none;
margin: 0; margin: 0;
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2); padding: var(--spacing-xl) calc(var(--spacing-xl) * 2)
calc(var(--spacing-xl) * 2) calc(var(--spacing-xl) * 2);
} }
</style> </style>

21
packages/bbui/src/Modal/Modal.svelte

@ -7,9 +7,10 @@
import Context from "../context" import Context from "../context"
export let fixed = false export let fixed = false
export let inline = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let visible = !!fixed let visible = fixed || inline
$: dispatch(visible ? "show" : "hide") $: dispatch(visible ? "show" : "hide")
export function show() { export function show() {
@ -20,7 +21,7 @@
} }
export function hide() { export function hide() {
if (!visible || fixed) { if (!visible || fixed || inline) {
return return
} }
visible = false visible = false
@ -45,11 +46,17 @@
<svelte:window on:keydown={handleKey} /> <svelte:window on:keydown={handleKey} />
{#if visible} <!-- These svelte if statements need to be defined like this. -->
<!-- The modal transitions do not work if nested inside more than one "if" -->
{#if visible && inline}
<div use:focusFirstInput class="spectrum-Modal inline is-open">
<slot />
</div>
{:else if visible}
<Portal target=".modal-container"> <Portal target=".modal-container">
<div <div
class="spectrum-Underlay is-open" class="spectrum-Underlay is-open"
transition:fade={{ duration: 200 }} transition:fade|local={{ duration: 200 }}
on:mousedown|self={hide} on:mousedown|self={hide}
> >
<div class="modal-wrapper" on:mousedown|self={hide}> <div class="modal-wrapper" on:mousedown|self={hide}>
@ -57,7 +64,7 @@
<div <div
use:focusFirstInput use:focusFirstInput
class="spectrum-Modal is-open" class="spectrum-Modal is-open"
transition:fly={{ y: 30, duration: 200 }} transition:fly|local={{ y: 30, duration: 200 }}
> >
<slot /> <slot />
</div> </div>
@ -98,6 +105,7 @@
} }
.spectrum-Modal { .spectrum-Modal {
background: var(--background);
overflow: visible; overflow: visible;
max-height: none; max-height: none;
margin: 40px 0; margin: 40px 0;
@ -106,4 +114,7 @@
--spectrum-global-dimension-size-100 --spectrum-global-dimension-size-100
); );
} }
:global(.spectrum--lightest .spectrum-Modal.inline) {
border: var(--border-light);
}
</style> </style>

2
packages/bbui/src/Table/CellRenderer.svelte

@ -23,7 +23,7 @@
} }
$: type = schema?.type ?? "string" $: type = schema?.type ?? "string"
$: customRenderer = customRenderers?.find(x => x.column === schema?.name) $: customRenderer = customRenderers?.find(x => x.column === schema?.name)
$: renderer = customRenderer?.component ?? typeMap[type] $: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
</script> </script>
{#if renderer && (customRenderer || (value != null && value !== ""))} {#if renderer && (customRenderer || (value != null && value !== ""))}

76
packages/bbui/src/Table/Table.svelte

@ -214,7 +214,7 @@
> >
<div style={contentStyle}> <div style={contentStyle}>
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}> <table class="spectrum-Table" class:spectrum-Table--quiet={quiet}>
{#if sortedRows?.length} {#if fields.length}
<thead class="spectrum-Table-head"> <thead class="spectrum-Table-head">
<tr> <tr>
{#if showEditColumn} {#if showEditColumn}
@ -269,7 +269,7 @@
</thead> </thead>
{/if} {/if}
<tbody class="spectrum-Table-body"> <tbody class="spectrum-Table-body">
{#if sortedRows?.length} {#if sortedRows?.length && fields.length}
{#each sortedRows as row, idx} {#each sortedRows as row, idx}
<tr <tr
on:click={() => toggleSelectRow(row)} on:click={() => toggleSelectRow(row)}
@ -316,15 +316,25 @@
</tr> </tr>
{/each} {/each}
{:else} {:else}
<div class="placeholder"> <tr class="placeholder-row">
<svg {#if showEditColumn}
class="spectrum-Icon spectrum-Icon--sizeXXL" <td class="placeholder-offset" />
focusable="false" {/if}
> {#each fields as field}
<use xlink:href="#spectrum-icon-18-Table" /> <td />
</svg> {/each}
<div>No rows found</div> <div class="placeholder" class:has-fields={fields.length > 0}>
</div> <div class="placeholder-content">
<svg
class="spectrum-Icon spectrum-Icon--sizeXXL"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Table" />
</svg>
<div>No rows found</div>
</div>
</div>
</tr>
{/if} {/if}
</tbody> </tbody>
</table> </table>
@ -347,7 +357,7 @@
overflow: auto; overflow: auto;
} }
.container.quiet { .container.quiet {
border: none !important; border: none;
} }
table { table {
width: 100%; width: 100%;
@ -381,7 +391,7 @@
z-index: 2; z-index: 2;
background-color: var(--spectrum-alias-background-color-secondary); background-color: var(--spectrum-alias-background-color-secondary);
border-bottom: 1px solid border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important; var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
} }
.spectrum-Table-headCell-content { .spectrum-Table-headCell-content {
white-space: nowrap; white-space: nowrap;
@ -396,7 +406,34 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.placeholder-row {
position: relative;
height: 150px;
}
.placeholder-row td {
border-top: none !important;
border-bottom: none !important;
}
.placeholder-offset {
width: 1px;
}
.placeholder { .placeholder {
top: 0;
height: 100%;
left: 0;
width: 100%;
position: absolute;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.placeholder.has-fields {
top: var(--header-height);
height: calc(100% - var(--header-height));
}
.placeholder-content {
padding: 20px; padding: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -407,12 +444,13 @@
var(--spectrum-alias-text-color) var(--spectrum-alias-text-color)
); );
} }
.placeholder div { .placeholder-content div {
margin-top: 10px; margin-top: 10px;
font-size: var( font-size: var(
--spectrum-table-cell-text-size, --spectrum-table-cell-text-size,
var(--spectrum-alias-font-size-default) var(--spectrum-alias-font-size-default)
); );
text-align: center;
} }
tbody { tbody {
@ -431,17 +469,17 @@
td { td {
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
border-bottom: none !important; border-bottom: none;
border-top: 1px solid border-top: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important; var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
border-radius: 0 !important; border-radius: 0;
} }
tr:first-child td { tr:first-child td {
border-top: none !important; border-top: none;
} }
tr:last-child td { tr:last-child td {
border-bottom: 1px solid border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important; var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
} }
td.spectrum-Table-cell--divider { td.spectrum-Table-cell--divider {
width: 1px; width: 1px;

8
packages/bbui/yarn.lock

@ -2407,10 +2407,10 @@ svelte-portal@^1.0.0:
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3" resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3"
integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q== integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q==
svelte@^3.37.0: svelte@^3.38.2:
version "3.37.0" version "3.38.2"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.37.0.tgz#dc7cd24bcc275cdb3f8c684ada89e50489144ccd" resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5"
integrity sha512-TRF30F4W4+d+Jr2KzUUL1j8Mrpns/WM/WacxYlo5MMb2E5Qy2Pk1Guj6GylxsW9OnKQl1tnF8q3hG/hQ3h6VUA== integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg==
svgo@^1.0.0: svgo@^1.0.0:
version "1.3.2" version "1.3.2"

4
packages/builder/package.json

@ -90,7 +90,7 @@
"@babel/preset-env": "^7.13.12", "@babel/preset-env": "^7.13.12",
"@babel/runtime": "^7.13.10", "@babel/runtime": "^7.13.10",
"@rollup/plugin-replace": "^2.4.2", "@rollup/plugin-replace": "^2.4.2",
"@roxi/routify": "2.15.1", "@roxi/routify": "2.18.0",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.5", "@sveltejs/vite-plugin-svelte": "^1.0.0-next.5",
"@testing-library/jest-dom": "^5.11.10", "@testing-library/jest-dom": "^5.11.10",
"@testing-library/svelte": "^3.0.0", "@testing-library/svelte": "^3.0.0",
@ -106,7 +106,7 @@
"rollup": "^2.44.0", "rollup": "^2.44.0",
"rollup-plugin-copy": "^3.4.0", "rollup-plugin-copy": "^3.4.0",
"start-server-and-test": "^1.12.1", "start-server-and-test": "^1.12.1",
"svelte": "^3.37.0", "svelte": "^3.38.2",
"svelte-jester": "^1.3.2", "svelte-jester": "^1.3.2",
"vite": "^2.1.5" "vite": "^2.1.5"
}, },

66
packages/builder/src/actions.js

@ -1,9 +1,10 @@
export const gradient = (node, config = {}) => { export const gradient = (node, config = {}) => {
const defaultConfig = { const defaultConfig = {
points: 10, points: 12,
saturation: 0.8, saturation: 0.85,
lightness: 0.75, lightness: 0.7,
softness: 0.8, softness: 0.9,
seed: null,
} }
// Applies a gradient background // Applies a gradient background
@ -13,42 +14,67 @@ export const gradient = (node, config = {}) => {
...config, ...config,
} }
const { saturation, lightness, softness, points } = config const { saturation, lightness, softness, points } = config
const seed = config.seed || Math.random().toString(32).substring(2)
// Generates a random number between min and max // Hash function which returns a fixed hash between specified limits
const rand = (min, max) => { // for a given seed and a given version
return Math.round(min + Math.random() * (max - min)) const rangeHash = (seed, min = 0, max = 100, version = 0) => {
const range = max - min
let hash = range + version
for (let i = 0; i < seed.length * 2 + version; i++) {
hash = (hash << 5) - hash + seed.charCodeAt(i % seed.length)
hash = ((hash & hash) % range) + version
}
return min + (hash % range)
} }
// Generates a random HSL colour using the options specified // Generates a random HSL colour using the options specified
const randomHSL = () => { const randomHSL = (seed, version, alpha = 1) => {
const lowerSaturation = Math.min(100, saturation * 100) const lowerSaturation = Math.min(100, saturation * 100)
const upperSaturation = Math.min(100, (saturation + 0.2) * 100) const upperSaturation = Math.min(100, (saturation + 0.2) * 100)
const lowerLightness = Math.min(100, lightness * 100) const lowerLightness = Math.min(100, lightness * 100)
const upperLightness = Math.min(100, (lightness + 0.2) * 100) const upperLightness = Math.min(100, (lightness + 0.2) * 100)
const hue = rand(0, 360) const hue = rangeHash(seed, 0, 360, version)
const sat = `${rand(lowerSaturation, upperSaturation)}%` const sat = `${rangeHash(
const light = `${rand(lowerLightness, upperLightness)}%` seed,
return `hsl(${hue},${sat},${light})` lowerSaturation,
upperSaturation,
version
)}%`
const light = `${rangeHash(
seed,
lowerLightness,
upperLightness,
version
)}%`
return `hsla(${hue},${sat},${light},${alpha})`
} }
// Generates a radial gradient stop point // Generates a radial gradient stop point
const randomGradientPoint = () => { const randomGradientPoint = (seed, version) => {
const lowerTransparency = Math.min(100, softness * 100) const lowerTransparency = Math.min(100, softness * 100)
const upperTransparency = Math.min(100, (softness + 0.2) * 100) const upperTransparency = Math.min(100, (softness + 0.2) * 100)
const transparency = rand(lowerTransparency, upperTransparency) const transparency = rangeHash(
seed,
lowerTransparency,
upperTransparency,
version
)
return ( return (
`radial-gradient(` + `radial-gradient(at ` +
`at ${rand(10, 90)}% ${rand(10, 90)}%,` + `${rangeHash(seed, 0, 100, version)}% ` +
`${randomHSL()} 0,` + `${rangeHash(seed, 0, 100, version + 1)}%,` +
`${randomHSL(seed, version, saturation)} 0,` +
`transparent ${transparency}%)` `transparent ${transparency}%)`
) )
} }
let css = `opacity:0.9;background-color:${randomHSL()};background-image:` let css = `opacity:0.9;background:${randomHSL(seed, 0, 0.7)};`
css += "background-image:"
for (let i = 0; i < points - 1; i++) { for (let i = 0; i < points - 1; i++) {
css += `${randomGradientPoint()},` css += `${randomGradientPoint(seed, i)},`
} }
css += `${randomGradientPoint()};` css += `${randomGradientPoint(seed, points)};`
node.style = css node.style = css
} }

12
packages/builder/src/components/common/ConfigChecklist.svelte

@ -1,22 +1,12 @@
<script> <script>
import { isActive, url, goto } from "@roxi/routify"
import { onMount } from "svelte"
import { import {
ActionMenu, ActionMenu,
Checkbox, Checkbox,
Body,
MenuItem, MenuItem,
Icon,
Heading, Heading,
Avatar,
Search,
Layout,
ProgressCircle, ProgressCircle,
SideNavigation as Navigation,
SideNavigationItem as Item,
} from "@budibase/bbui" } from "@budibase/bbui"
import api from "builderStore/api" import { admin } from "stores/portal"
import { organisation, admin } from "stores/portal"
const MESSAGES = { const MESSAGES = {
apps: "Create your first app", apps: "Create your first app",

9
packages/builder/src/components/common/ConfirmDialog.svelte

@ -7,6 +7,7 @@
export let cancelText = "Cancel" export let cancelText = "Cancel"
export let onOk = undefined export let onOk = undefined
export let onCancel = undefined export let onCancel = undefined
export let warning = true
let modal let modal
@ -19,7 +20,13 @@
</script> </script>
<Modal bind:this={modal} on:hide={onCancel}> <Modal bind:this={modal} on:hide={onCancel}>
<ModalContent onConfirm={onOk} {title} confirmText={okText} {cancelText} red> <ModalContent
onConfirm={onOk}
{title}
confirmText={okText}
{cancelText}
{warning}
>
<Body size="S"> <Body size="S">
{body} {body}
<slot /> <slot />

88
packages/builder/src/components/start/AppCard.svelte

@ -7,56 +7,49 @@
ActionMenu, ActionMenu,
MenuItem, MenuItem,
Link, Link,
notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import download from "downloadjs"
import { gradient } from "actions" import { gradient } from "actions"
import { url } from "@roxi/routify"
export let name export let app
export let _id export let exportApp
export let deleteApp
let appExportLoading = false
async function exportApp() {
appExportLoading = true
try {
download(
`/api/backups/export?appId=${_id}&appname=${encodeURIComponent(name)}`
)
notifications.success("App export complete")
} catch (err) {
console.error(err)
notifications.error("App export failed")
} finally {
appExportLoading = false
}
}
</script> </script>
<Layout noPadding gap="XS"> <div class="wrapper">
<div class="preview" use:gradient /> <Layout noPadding gap="XS" alignContent="start">
<div class="title"> <div class="preview" use:gradient={{ seed: app.name }} />
<Link href={`/builder/app/${_id}`}> <div class="title">
<Heading size="XS"> <Link href={$url(`../../app/${app._id}`)}>
{name} <Heading size="XS">
</Heading> {app.name}
</Link> </Heading>
<ActionMenu> </Link>
<Icon slot="control" name="More" hoverable /> <ActionMenu align="right">
<MenuItem on:click={exportApp} icon="Download">Export</MenuItem> <Icon slot="control" name="More" hoverable />
</ActionMenu> <MenuItem on:click={() => exportApp(app)} icon="Download">
</div> Export
<div class="status"> </MenuItem>
<Body noPadding size="S"> <MenuItem on:click={() => deleteApp(app)} icon="Delete">
Edited {Math.floor(1 + Math.random() * 10)} months ago Delete
</Body> </MenuItem>
{#if Math.random() > 0.5} </ActionMenu>
<Icon name="LockClosed" /> </div>
{/if} <div class="status">
</div> <Body noPadding size="S">
</Layout> Edited {Math.floor(1 + Math.random() * 10)} months ago
</Body>
{#if Math.random() > 0.5}
<Icon name="LockClosed" />
{/if}
</div>
</Layout>
</div>
<style> <style>
.wrapper {
overflow: hidden;
}
.preview { .preview {
height: 135px; height: 135px;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
@ -73,6 +66,15 @@
.title :global(a) { .title :global(a) {
text-decoration: none; text-decoration: none;
flex: 1 1 auto;
width: 0;
overflow: hidden;
text-overflow: ellipsis;
margin-right: var(--spacing-m);
}
.title :global(h1) {
overflow: hidden;
text-overflow: ellipsis;
} }
.title :global(h1:hover) { .title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600); color: var(--spectrum-global-color-blue-600);

25
packages/builder/src/components/start/AppList.svelte

@ -1,25 +0,0 @@
<script>
import { onMount } from "svelte"
import AppCard from "./AppCard.svelte"
import { apps } from "stores/portal"
onMount(apps.load)
</script>
{#if $apps.length}
<div class="appList">
{#each $apps as app}
<AppCard {...app} />
{/each}
</div>
{:else}
<div>No apps found.</div>
{/if}
<style>
.appList {
display: grid;
grid-gap: 50px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
</style>

80
packages/builder/src/components/start/AppRow.svelte

@ -0,0 +1,80 @@
<script>
import { gradient } from "actions"
import {
Heading,
Button,
Icon,
ActionMenu,
MenuItem,
Link,
} from "@budibase/bbui"
import { url } from "@roxi/routify"
export let app
export let openApp
export let exportApp
export let deleteApp
export let last
</script>
<div class="title" class:last>
<div class="preview" use:gradient={{ seed: app.name }} />
<Link href={$url(`../../app/${app._id}`)}>
<Heading size="XS">
{app.name}
</Heading>
</Link>
</div>
<div class:last>
Edited {Math.round(Math.random() * 10 + 1)} months ago
</div>
<div class:last>
{#if Math.random() < 0.33}
<div class="status status--open" />
Open
{:else if Math.random() < 0.33}
<div class="status status--locked-other" />
Locked by Will Wheaton
{:else}
<div class="status status--locked-you" />
Locked by you
{/if}
</div>
<div class:last>
<Button on:click={() => openApp(app)} size="S" secondary>Open</Button>
<ActionMenu align="right">
<Icon hoverable slot="control" name="More" />
<MenuItem on:click={() => exportApp(app)} icon="Download">Export</MenuItem>
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
</ActionMenu>
</div>
<style>
.preview {
height: 40px;
width: 40px;
border-radius: var(--border-radius-s);
}
.title :global(a) {
text-decoration: none;
}
.title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600);
cursor: pointer;
transition: color 130ms ease;
}
.status {
height: 10px;
width: 10px;
border-radius: 50%;
}
.status--locked-you {
background-color: var(--spectrum-global-color-orange-600);
}
.status--locked-other {
background-color: var(--spectrum-global-color-red-600);
}
.status--open {
background-color: var(--spectrum-global-color-green-600);
}
</style>

239
packages/builder/src/components/start/CreateAppModal.svelte

@ -1,58 +1,50 @@
<script> <script>
import { writable, get as svelteGet } from "svelte/store" import { writable, get as svelteGet } from "svelte/store"
import { notifications, Heading, Button } from "@budibase/bbui" import {
notifications,
Input,
ModalContent,
Dropzone,
Body,
Checkbox,
} from "@budibase/bbui"
import { store, automationStore, hostingStore } from "builderStore" import { store, automationStore, hostingStore } from "builderStore"
import { string, object } from "yup" import { string, mixed, object } from "yup"
import api, { get } from "builderStore/api" import api, { get } from "builderStore/api"
import Spinner from "components/common/Spinner.svelte"
import { Info, User } from "./Steps"
import Indicator from "./Indicator.svelte"
import { goto } from "@roxi/routify"
import { fade } from "svelte/transition"
import { post } from "builderStore/api" import { post } from "builderStore/api"
import analytics from "analytics" import analytics from "analytics"
import { onMount } from "svelte" import { onMount } from "svelte"
import Logo from "/assets/bb-logo.svg" import { capitalise } from "helpers"
import { capitalise } from "../../helpers" import { goto } from "@roxi/routify"
export let template export let template
const currentStep = writable(0) const values = writable({ name: null })
const values = writable({ roleId: "ADMIN" })
const errors = writable({}) const errors = writable({})
const touched = writable({}) const touched = writable({})
const steps = [Info, User] const validator = {
let validators = [ name: string().required("Your application must have a name"),
{ file: template ? mixed().required("Please choose a file to import") : null,
applicationName: string().required("Your application must have a name"), }
},
{
roleId: string()
.nullable()
.required("You need to select a role for this app"),
},
]
let submitting = false let submitting = false
let valid = false let valid = false
$: checkValidity($values, validators[$currentStep]) $: checkValidity($values, validator)
onMount(async () => { onMount(async () => {
const hostingInfo = await hostingStore.actions.fetch() await hostingStore.actions.fetchDeployedApps()
if (hostingInfo.type === "self") { const existingAppNames = svelteGet(hostingStore).deployedAppNames
await hostingStore.actions.fetchDeployedApps() validator.name = string()
const existingAppNames = svelteGet(hostingStore).deployedAppNames .required("Your application must have a name")
validators[0].applicationName = string() .test(
.required("Your application must have a name.") "non-existing-app-name",
.test( "Another app with the same name already exists",
"non-existing-app-name", value => {
"App with same name already exists. Please try another app name.", return !existingAppNames.some(
value => appName => appName.toLowerCase() === value.toLowerCase()
!existingAppNames.some( )
appName => appName.toLowerCase() === value.toLowerCase() }
) )
)
}
}) })
const checkValidity = async (values, validator) => { const checkValidity = async (values, validator) => {
@ -70,15 +62,24 @@
async function createNewApp() { async function createNewApp() {
submitting = true submitting = true
// Check a template exists if we are important
if (template && !$values.file) {
$errors.file = "Please choose a file to import"
valid = false
submitting = false
return false
}
try { try {
// Create form data to create app // Create form data to create app
let data = new FormData() let data = new FormData()
data.append("name", $values.applicationName) data.append("name", $values.name)
data.append("useTemplate", template != null) data.append("useTemplate", template != null)
if (template) { if (template) {
data.append("templateName", template.name) data.append("templateName", template.name)
data.append("templateKey", template.key) data.append("templateKey", template.key)
data.append("templateFile", template.file) data.append("templateFile", $values.file)
} }
// Create App // Create App
@ -89,7 +90,7 @@
} }
analytics.captureEvent("App Created", { analytics.captureEvent("App Created", {
name: $values.applicationName, name: $values.name,
appId: appJson._id, appId: appJson._id,
template, template,
}) })
@ -112,7 +113,7 @@
} }
const userResp = await api.post(`/api/users/metadata/self`, user) const userResp = await api.post(`/api/users/metadata/self`, user)
await userResp.json() await userResp.json()
window.location = `/builder/app/${appJson._id}` $goto(`/builder/app/${appJson._id}`)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
notifications.error(error) notifications.error(error)
@ -121,129 +122,33 @@
} }
</script> </script>
<div class="container"> <ModalContent
<div class="sidebar"> title={template ? "Import app" : "Create new app"}
<img src={Logo} alt="budibase icon" /> confirmText={template ? "Import app" : "Create app"}
<div class="steps"> onConfirm={createNewApp}
{#each steps as component, i} disabled={!valid}
<Indicator >
active={$currentStep === i} {#if template}
done={i < $currentStep} <Dropzone
step={i + 1} error={$touched.file && $errors.file}
/> gallery={false}
{/each} label="File to import"
</div> value={[$values.file]}
</div> on:change={e => {
<div class="body"> $values.file = e.detail?.[0]
<div class="heading"> $touched.file = true
<Heading size="L">Get started with Budibase</Heading> }}
</div> />
<div class="step">
{#each steps as component, i (i)}
<div class:hidden={$currentStep !== i}>
<svelte:component
this={component}
{template}
{values}
{errors}
{touched}
/>
</div>
{/each}
</div>
<div class="footer">
{#if $currentStep > 0}
<Button medium secondary on:click={() => $currentStep--}>Back</Button>
{/if}
{#if $currentStep < steps.length - 1}
<Button medium cta on:click={() => $currentStep++} disabled={!valid}>
Next
</Button>
{/if}
{#if $currentStep === steps.length - 1}
<Button
medium
cta
on:click={createNewApp}
disabled={!valid || submitting}
>
{submitting ? "Loading..." : "Submit"}
</Button>
{/if}
</div>
</div>
{#if submitting}
<div in:fade class="spinner-container">
<Spinner />
<span class="spinner-text">Creating your app...</span>
</div>
{/if} {/if}
</div> <Body size="S">
Give your new app a name, and choose which groups have access (paid plans
<style> only).
.container { </Body>
min-height: 600px; <Input
display: grid; bind:value={$values.name}
grid-template-columns: 80px 1fr; error={$touched.name && $errors.name}
position: relative; on:blur={() => ($touched.name = true)}
} label="Name"
.sidebar { />
display: flex; <Checkbox label="Group access" disabled value={true} text="All users" />
flex-direction: column; </ModalContent>
justify-content: flex-start;
align-items: stretch;
padding: 40px 0;
background: var(--grey-1);
}
.steps {
flex: 1 1 auto;
display: grid;
border-bottom-left-radius: 0.5rem;
border-top-left-radius: 0.5rem;
grid-gap: 30px;
align-content: center;
}
.heading {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
}
.body {
padding: 40px 60px 40px 60px;
display: grid;
align-items: center;
grid-template-rows: auto 1fr auto;
}
.footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 15px;
}
.spinner-container {
background: var(--background);
position: absolute;
border-radius: 5px;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: grid;
justify-items: center;
align-content: center;
grid-gap: 50px;
}
.spinner-text {
font-size: 2em;
}
.hidden {
display: none;
}
img {
height: 40px;
margin-bottom: 20px;
}
</style>

83
packages/builder/src/components/start/Indicator.svelte

@ -1,83 +0,0 @@
<script>
export let step, done, active
</script>
<div class="container" class:active class:done>
<div class="circle" class:active class:done>
{#if done}
<svg
width="12"
height="10"
viewBox="0 0 12 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.1212 0.319527C10.327 0.115582 10.6047 0.000803464 10.8944
4.20219e-06C11.1841 -0.00079506 11.4624 0.11245 11.6693
0.315256C11.8762 0.518062 11.9949 0.794134 11.9998 1.08379C12.0048
1.37344 11.8955 1.65339 11.6957 1.86313L5.82705 9.19893C5.72619
9.30757 5.60445 9.39475 5.46913 9.45527C5.3338 9.51578 5.18766 9.54839
5.03944 9.55113C4.89123 9.55388 4.74398 9.52671 4.60651
9.47124C4.46903 9.41578 4.34416 9.33316 4.23934 9.22833L0.350925
5.33845C0.242598 5.23751 0.155712 5.11578 0.0954499 4.98054C0.0351876
4.84529 0.00278364 4.69929 0.00017159 4.55124C-0.00244046 4.4032
0.024793 4.25615 0.0802466 4.11886C0.1357 3.98157 0.218238 3.85685
0.322937 3.75215C0.427636 3.64746 0.55235 3.56492 0.68964
3.50946C0.82693 3.45401 0.973983 3.42678 1.12203 3.42939C1.27007 3.432
1.41607 3.46441 1.55132 3.52467C1.68657 3.58493 1.80829 3.67182
1.90923 3.78014L4.98762 6.85706L10.0933 0.35187C10.1024 0.340482
10.1122 0.329679 10.1227 0.319527H10.1212Z"
fill="var(--background)"
/>
</svg>
{:else}{step}{/if}
</div>
</div>
<style>
.container::before {
content: "";
position: absolute;
top: -30px;
width: 1px;
height: 30px;
background: var(--grey-5);
}
.container:first-child::before {
display: none;
}
.container {
position: relative;
height: 45px;
display: grid;
place-items: center;
}
.container.active {
box-shadow: inset 3px 0 0 0 var(--blue);
}
.circle.active {
background: var(--blue);
color: white;
border: none;
}
.circle.done {
background: var(--grey-5);
color: white;
border: none;
}
.circle {
color: var(--grey-5);
font-size: 14px;
font-weight: 500;
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid var(--grey-5);
box-sizing: border-box;
}
</style>

121
packages/builder/src/components/start/Steps/Info.svelte

@ -1,121 +0,0 @@
<script>
import { Label, Heading, Input, notifications } from "@budibase/bbui"
const BYTES_IN_MB = 1000000
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
export let template
export let values
export let errors
export let touched
let blurred = { appName: false }
let file
function handleFile(evt) {
const fileArray = Array.from(evt.target.files)
if (fileArray.some(file => file.size >= FILE_SIZE_LIMIT)) {
notifications.error(
`Files cannot exceed ${
FILE_SIZE_LIMIT / BYTES_IN_MB
}MB. Please try again with smaller files.`
)
return
}
file = evt.target.files[0]
template.file = file
}
</script>
<div class="container">
{#if template?.fromFile}
<Heading size="L">Import your Web App</Heading>
{:else}
<Heading size="L">Create your Web App</Heading>
{/if}
{#if template?.fromFile}
<div class="template">
<Label extraSmall grey>Import File</Label>
<div class="dropzone">
<input
id="file-upload"
accept=".txt"
type="file"
on:change={handleFile}
/>
<label for="file-upload" class:uploaded={file}>
{#if file}{file.name}{:else}Import{/if}
</label>
</div>
</div>
{:else if template}
<div class="template">
<Label extraSmall grey>Selected Template</Label>
<Heading size="S">{template.name}</Heading>
</div>
{/if}
<Input
on:change={() => ($touched.applicationName = true)}
bind:value={$values.applicationName}
label="Web App Name"
placeholder="Enter name of your web application"
error={$touched.applicationName && $errors.applicationName}
/>
</div>
<style>
.container {
display: grid;
grid-gap: var(--spacing-xl);
margin-top: var(--spacing-xl);
}
.template :global(label) {
/* Fix layout due to LH 0 on heading */
margin-bottom: 16px;
}
.dropzone {
text-align: center;
display: flex;
align-items: center;
flex-direction: column;
border-radius: 10px;
transition: all 0.3s;
}
.uploaded {
color: var(--blue);
}
input[type="file"] {
display: none;
}
label {
font-family: var(--font-sans);
cursor: pointer;
font-weight: 500;
box-sizing: border-box;
overflow: hidden;
border-radius: var(--border-radius-s);
color: var(--ink);
padding: var(--spacing-m) var(--spacing-l);
transition: all 0.2s ease 0s;
display: inline-flex;
text-rendering: optimizeLegibility;
min-width: auto;
outline: none;
font-feature-settings: "case" 1, "rlig" 1, "calt" 0;
-webkit-box-align: center;
user-select: none;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 100%;
background-color: var(--grey-2);
font-size: var(--font-size-xs);
line-height: normal;
border: var(--border-transparent);
}
</style>

29
packages/builder/src/components/start/Steps/User.svelte

@ -1,29 +0,0 @@
<script>
import { Select, Heading } from "@budibase/bbui"
export let values
export let errors
export let touched
</script>
<div class="container">
<Heading size="L">What's your role for this app?</Heading>
<Select
bind:value={$values.roleId}
label="Role"
options={[
{ label: "Admin", value: "ADMIN" },
{ label: "Power User", value: "POWER_USER" },
]}
getOptionLabel={option => option.label}
getOptionValue={option => option.value}
error={$errors.roleId}
/>
</div>
<style>
.container {
display: grid;
grid-gap: var(--spacing-xl);
}
</style>

2
packages/builder/src/components/start/Steps/index.js

@ -1,2 +0,0 @@
export { default as Info } from "./Info.svelte"
export { default as User } from "./User.svelte"

0
packages/builder/src/pages/builder/app/[application]/_reset.svelte → packages/builder/src/pages/builder/app/[application]/_layout.svelte

8
packages/builder/src/pages/builder/portal/_layout.svelte

@ -17,9 +17,6 @@
import { auth } from "stores/backend" import { auth } from "stores/backend"
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte" import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
organisation.init()
apps.load()
let orgName let orgName
let orgLogo let orgLogo
let user let user
@ -32,7 +29,10 @@
user = { name: "John Doe" } user = { name: "John Doe" }
} }
onMount(getInfo) onMount(() => {
organisation.init()
getInfo()
})
let menu = [ let menu = [
{ title: "Apps", href: "/builder/portal/apps" }, { title: "Apps", href: "/builder/portal/apps" },

7
packages/builder/src/pages/builder/portal/apps/_layout.svelte

@ -1,7 +0,0 @@
<script>
import { Page } from "@budibase/bbui"
</script>
<Page wide>
<slot />
</Page>

221
packages/builder/src/pages/builder/portal/apps/index.svelte

@ -8,18 +8,31 @@
ButtonGroup, ButtonGroup,
Select, Select,
Modal, Modal,
ModalContent,
Page,
notifications,
Body,
} from "@budibase/bbui" } from "@budibase/bbui"
import AppList from "components/start/AppList.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
import api from "builderStore/api" import api, { del } from "builderStore/api"
import analytics from "analytics" import analytics from "analytics"
import { onMount } from "svelte" import { onMount } from "svelte"
import { apps } from "stores/portal"
import download from "downloadjs"
import { goto } from "@roxi/routify"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import AppCard from "components/start/AppCard.svelte"
import AppRow from "components/start/AppRow.svelte"
let layout = "grid" let layout = "grid"
let modal
let template let template
let appToDelete
let creationModal
let deletionModal
let creatingApp = false
let loaded = false
async function checkKeys() { const checkKeys = async () => {
const response = await api.get(`/api/keys/`) const response = await api.get(`/api/keys/`)
const keys = await response.json() const keys = await response.json()
if (keys.userId) { if (keys.userId) {
@ -27,55 +40,146 @@
} }
} }
function initiateAppImport() { const initiateAppCreation = () => {
creationModal.show()
creatingApp = true
}
const initiateAppImport = () => {
template = { fromFile: true } template = { fromFile: true }
modal.show() creationModal.show()
creatingApp = true
}
const stopAppCreation = () => {
template = null
creatingApp = false
}
const openApp = app => {
$goto(`../../app/${app._id}`)
}
const exportApp = app => {
try {
download(
`/api/backups/export?appId=${app._id}&appname=${encodeURIComponent(
app.name
)}`
)
notifications.success("App export complete")
} catch (err) {
console.error(err)
notifications.error("App export failed")
}
}
const deleteApp = app => {
appToDelete = app
deletionModal.show()
}
const confirmDeleteApp = async () => {
if (!appToDelete) {
return
}
await del(`/api/applications/${appToDelete?._id}`)
await apps.load()
appToDelete = null
} }
onMount(checkKeys) onMount(async () => {
checkKeys()
await apps.load()
loaded = true
})
</script> </script>
<Layout noPadding> <Page wide>
<div class="title"> {#if $apps.length}
<Heading>Apps</Heading> <Layout noPadding>
<ButtonGroup> <div class="title">
<Button secondary on:click={initiateAppImport}>Import app</Button> <Heading>Apps</Heading>
<Button cta on:click={modal.show}>Create new app</Button> <ButtonGroup>
</ButtonGroup> <Button secondary on:click={initiateAppImport}>Import app</Button>
</div> <Button cta on:click={initiateAppCreation}>Create new app</Button>
<div class="filter"> </ButtonGroup>
<div class="select"> </div>
<Select quiet placeholder="Filter by groups" /> <div class="filter">
<div class="select">
<Select quiet placeholder="Filter by groups" />
</div>
<ActionGroup>
<ActionButton
on:click={() => (layout = "grid")}
selected={layout === "grid"}
quiet
icon="ClassicGridView"
/>
<ActionButton
on:click={() => (layout = "table")}
selected={layout === "table"}
quiet
icon="ViewRow"
/>
</ActionGroup>
</div>
<div
class:appGrid={layout === "grid"}
class:appTable={layout === "table"}
>
{#each $apps as app, idx (app._id)}
<svelte:component
this={layout === "grid" ? AppCard : AppRow}
{app}
{openApp}
{exportApp}
{deleteApp}
last={idx === $apps.length - 1}
/>
{/each}
</div>
</Layout>
{/if}
{#if !$apps.length && !creatingApp && loaded}
<div class="empty-wrapper">
<Modal inline>
<ModalContent
title="Create your first app"
confirmText="Create app"
showCancelButton={false}
showCloseIcon={false}
onConfirm={initiateAppCreation}
size="M"
>
<div slot="footer">
<Button on:click={initiateAppImport} secondary>Import app</Button>
</div>
<Body size="S">
The purpose of the Budibase builder is to help you build beautiful,
powerful applications quickly and easily.
</Body>
</ModalContent>
</Modal>
</div> </div>
<ActionGroup>
<ActionButton
on:click={() => (layout = "grid")}
selected={layout === "grid"}
quiet
icon="ClassicGridView"
/>
<ActionButton
on:click={() => (layout = "table")}
selected={layout === "table"}
quiet
icon="ViewRow"
/>
</ActionGroup>
</div>
{#if layout === "grid"}
<AppList />
{:else}
Table view.
{/if} {/if}
</Layout> </Page>
<Modal <Modal
bind:this={modal} bind:this={creationModal}
padding={false} padding={false}
width="600px" width="600px"
on:hide={() => (template = null)} on:hide={stopAppCreation}
> >
<CreateAppModal {template} /> <CreateAppModal {template} />
</Modal> </Modal>
<ConfirmDialog
bind:this={deletionModal}
title="Confirm deletion"
okText="Delete app"
onOk={confirmDeleteApp}
>
Are you sure you want to delete the app <b>{appToDelete?.name}</b>?
</ConfirmDialog>
<style> <style>
.title, .title,
@ -87,6 +191,41 @@
} }
.select { .select {
width: 110px; width: 120px;
}
.appGrid {
display: grid;
grid-gap: 50px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.appTable {
display: grid;
grid-template-rows: auto;
grid-template-columns: 1fr 1fr 1fr auto;
align-items: center;
}
.appTable :global(> div) {
height: 70px;
display: grid;
align-items: center;
gap: var(--spacing-xl);
grid-template-columns: auto 1fr;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 var(--spacing-s);
}
.appTable :global(> div:not(.last)) {
border-bottom: var(--border-light);
}
.empty-wrapper {
flex: 1 1 auto;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
} }
</style> </style>

15
packages/builder/src/pages/builder/portal/email/TemplateBindings.svelte

@ -0,0 +1,15 @@
<script>
import { Body, Menu, MenuItem, Detail, MenuSection, DetailSummary } from "@budibase/bbui"
export let bindings
export let onBindingClick = () => {}
</script>
<Menu>
{#each bindings as binding}
<MenuItem on:click={() => onBindingClick(binding)}>
<Detail size="M">{binding.name}</Detail>
<Body size="XS" noPadding>{binding.description}</Body>
</MenuItem>
{/each}
</Menu>

17
packages/builder/src/pages/builder/portal/email/TemplateLink.svelte

@ -0,0 +1,17 @@
<script>
import { url } from "@roxi/routify"
import {
Link,
} from "@budibase/bbui"
import { roles } from "stores/backend"
export let value
</script>
<Link quiet href={$url(`./${value}`)}><span>{value}</span></Link>
<style>
span {
text-transform: capitalize;
}
</style>

140
packages/builder/src/pages/builder/portal/email/[template].svelte

@ -0,0 +1,140 @@
<script>
import {
Menu,
MenuItem,
Button,
Detail,
Heading,
Divider,
Label,
Modal,
ModalContent,
notifications,
Layout,
Icon,
Body,
Page,
Select,
Tabs,
Tab,
MenuSection,
MenuSeparator,
} from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { onMount } from "svelte"
import { fade } from "svelte/transition"
import { email } from "stores/portal"
import Editor from "components/integration/QueryEditor.svelte"
import TemplateBindings from "./TemplateBindings.svelte"
import api from "builderStore/api"
const ConfigTypes = {
SMTP: "smtp",
}
export let template
let selected = "Edit"
let selectedBindingTab = "Template"
let htmlEditor
$: selectedTemplate = $email.templates.find(
({ purpose }) => purpose === template
)
$: templateBindings =
$email.definitions?.bindings[selectedTemplate.purpose] || []
async function saveTemplate() {
try {
// Save your template config
await email.templates.save(selectedTemplate)
notifications.success(`Template saved.`)
} catch (err) {
notifications.error(`Failed to update template settings. ${err}`)
}
}
function setTemplateBinding(binding) {
htmlEditor.update((selectedTemplate.contents += `{{ ${binding.name} }}`))
}
</script>
<Page wide gap="L">
<div class="backbutton" on:click={() => $goto("./")}>
<Icon name="BackAndroid" />
<span>Back</span>
</div>
<header>
<Heading>
Email Template: {template}
</Heading>
<Button cta on:click={saveTemplate}>Save</Button>
</header>
<Tabs {selected}>
<Tab title="Edit">
<div class="template-editor">
<Editor
editorHeight={800}
bind:this={htmlEditor}
mode="handlebars"
on:change={e => {
selectedTemplate.contents = e.detail.value
}}
value={selectedTemplate.contents}
/>
<div class="bindings-editor">
<Detail size="L">Bindings</Detail>
<Tabs selected={selectedBindingTab}>
<Tab title="Template">
<TemplateBindings
title="Template Bindings"
bindings={templateBindings}
onBindingClick={setTemplateBinding}
/>
</Tab>
<Tab title="Common">
<TemplateBindings
title="Common Bindings"
bindings={$email.definitions.bindings.common}
onBindingClick={setTemplateBinding}
/>
</Tab>
</Tabs>
</div>
</Tab>
<Tab title="Preview">
<div class="preview" transition:fade>
{@html selectedTemplate.contents}
</div>
</Tab>
</Tabs>
</Page>
<style>
.template-editor {
display: grid;
grid-template-columns: 1fr 20%;
grid-gap: var(--spacing-xl);
margin-top: var(--spacing-xl);
}
header {
display: flex;
width: 100%;
justify-content: space-between;
}
.preview {
background: white;
height: 800px;
padding: var(--spacing-xl);
}
.backbutton {
display: flex;
gap: var(--spacing-m);
margin-bottom: var(--spacing-xl);
cursor: pointer;
}
</style>

99
packages/builder/src/pages/builder/portal/email/index.svelte

@ -1,5 +1,8 @@
<script> <script>
import { goto } from "@roxi/routify"
import { import {
Menu,
MenuItem,
Button, Button,
Heading, Heading,
Divider, Divider,
@ -13,27 +16,40 @@
Body, Body,
Page, Page,
Select, Select,
MenuSection,
MenuSeparator,
Table,
} from "@budibase/bbui" } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import { email } from "stores/portal"
import Editor from "components/integration/QueryEditor.svelte" import Editor from "components/integration/QueryEditor.svelte"
import TemplateBindings from "./TemplateBindings.svelte"
import TemplateLink from "./TemplateLink.svelte"
import api from "builderStore/api" import api from "builderStore/api"
const ConfigTypes = { const ConfigTypes = {
SMTP: "smtp", SMTP: "smtp",
} }
let smtpConfig const templateSchema = {
let templateIdx = 0 purpose: {
let templateDefinition displayName: "Email",
let templates = [] editable: false
let htmlModal },
}
$: templateTypes = templates.map((template, idx) => ({ const customRenderers = [
label: template.purpose, {
value: idx, column: "purpose",
})) component: TemplateLink,
},
]
$: selectedTemplate = templates[templateIdx] let smtpConfig
let bindingsOpen = false
let htmlModal
let htmlEditor
let loading
async function saveSmtp() { async function saveSmtp() {
try { try {
@ -52,13 +68,7 @@
async function saveTemplate() { async function saveTemplate() {
try { try {
// Save your SMTP config await email.templates.save(selectedTemplate)
const response = await api.post(`/api/admin/template`, selectedTemplate)
const json = await response.json()
if (response.status !== 200) throw new Error(json.message)
selectedTemplate._rev = json._rev
selectedTemplate._id = json._id
notifications.success(`Template saved.`) notifications.success(`Template saved.`)
} catch (err) { } catch (err) {
notifications.error(`Failed to update template settings. ${err}`) notifications.error(`Failed to update template settings. ${err}`)
@ -84,22 +94,11 @@
} }
} }
async function fetchTemplates() {
// fetch the email template definitions
const templatesResponse = await api.get(`/api/admin/template/definitions`)
const templateDefDoc = await templatesResponse.json()
// fetch the email templates themselves
const emailTemplatesResponse = await api.get(`/api/admin/template/email`)
const emailTemplates = await emailTemplatesResponse.json()
templateDefinition = templateDefDoc
templates = emailTemplates
}
onMount(async () => { onMount(async () => {
loading = true
await fetchSmtp() await fetchSmtp()
await fetchTemplates() await email.templates.fetch()
loading = false
}) })
</script> </script>
@ -144,8 +143,8 @@
<Label>From email address</Label> <Label>From email address</Label>
<Input type="email" bind:value={smtpConfig.config.from} /> <Input type="email" bind:value={smtpConfig.config.from} />
</div> </div>
<Button cta on:click={saveSmtp}>Save</Button>
</Layout> </Layout>
<Button cta on:click={saveSmtp}>Save</Button>
</div> </div>
<Divider /> <Divider />
@ -155,26 +154,17 @@
Budibase comes out of the box with ready-made email templates to help Budibase comes out of the box with ready-made email templates to help
with user onboarding. Please refrain from changing the links. with user onboarding. Please refrain from changing the links.
</Body> </Body>
<div class="template-controls">
<Select bind:value={templateIdx} options={templateTypes} />
<Button primary on:click={htmlModal.show}>Edit HTML</Button>
</div>
</div> </div>
<Modal bind:this={htmlModal}> <Table
<ModalContent {customRenderers}
title="Edit Template HTML" on:editrow={evt => $goto(`./${evt.detail.purpose}`)}
onConfirm={saveTemplate} data={$email.templates}
size="XL" schema={templateSchema}
> {loading}
<Editor allowEditRows={false}
mode="handlebars" allowSelectRows={false}
on:change={e => { allowEditColumns={false}
selectedTemplate.contents = e.detail.value />
}}
value={selectedTemplate.contents}
/>
</ModalContent>
</Modal>
{/if} {/if}
</Page> </Page>
@ -199,11 +189,4 @@
header { header {
margin-bottom: 42px; margin-bottom: 42px;
} }
.template-controls {
display: grid;
grid-template-columns: 80% 1fr;
grid-gap: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
}
</style> </style>

2
packages/builder/src/pages/builder/portal/oauth/index.svelte

@ -91,7 +91,7 @@
</div> </div>
{/each} {/each}
<div> <div>
<Button primary on:click={() => save(google)}>Save</Button> <Button cta on:click={() => save(google)}>Save</Button>
</div> </div>
<Divider /> <Divider />
{/if} {/if}

44
packages/builder/src/stores/portal/email.js

@ -0,0 +1,44 @@
import { writable } from "svelte/store"
import api from "builderStore/api"
export function createEmailStore() {
const store = writable([])
return {
subscribe: store.subscribe,
templates: {
fetch: async () => {
// fetch the email template definitions
const response = await api.get(`/api/admin/template/definitions`)
const definitions = await response.json()
// fetch the email templates themselves
const templatesResponse = await api.get(`/api/admin/template/email`)
const templates = await templatesResponse.json()
store.set({
definitions,
templates,
})
},
save: async template => {
// Save your template config
const response = await api.post(`/api/admin/template`, template)
const json = await response.json()
if (response.status !== 200) throw new Error(json.message)
template._rev = json._rev
template._id = json._id
store.update(state => {
const currentIdx = state.templates.findIndex(
template => template.purpose === json.purpose
)
state.templates.splice(currentIdx, 1, template)
return state
})
},
},
}
}
export const email = createEmailStore()

1
packages/builder/src/stores/portal/index.js

@ -1,3 +1,4 @@
export { organisation } from "./organisation" export { organisation } from "./organisation"
export { admin } from "./admin" export { admin } from "./admin"
export { apps } from "./apps" export { apps } from "./apps"
export { email } from "./email"

10
packages/builder/yarn.lock

@ -1194,10 +1194,10 @@
estree-walker "^2.0.1" estree-walker "^2.0.1"
picomatch "^2.2.2" picomatch "^2.2.2"
"@roxi/routify@2.15.1": "@roxi/routify@2.18.0":
version "2.15.1" version "2.18.0"
resolved "https://registry.yarnpkg.com/@roxi/routify/-/routify-2.15.1.tgz#cbd5eafedfee7f04b154173dccd7474c177acb4f" resolved "https://registry.yarnpkg.com/@roxi/routify/-/routify-2.18.0.tgz#8f88bedd936312d0dbe44cbc11ab179b1f938ec2"
integrity sha512-IRdoaPSfP09EwWtB+tpbHgH6ejYtowale24rgfpxRQhFNyTUK4jYXclvx3XkUD1NSupSgl1kDAsWSiRSG0WvkQ== integrity sha512-MVB50HN+VQWLzfjLplcBjsSBvwOiExKOmht2DuWR3WQ60JxQi9pSejkB06tFVkFKNXz2X5iYtKDqKBTdae/gRg==
dependencies: dependencies:
"@roxi/ssr" "^0.2.1" "@roxi/ssr" "^0.2.1"
"@types/node" ">=4.2.0 < 13" "@types/node" ">=4.2.0 < 13"
@ -5821,7 +5821,7 @@ svelte-portal@0.1.0:
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-0.1.0.tgz#cc2821cc84b05ed5814e0218dcdfcbebc53c1742" resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-0.1.0.tgz#cc2821cc84b05ed5814e0218dcdfcbebc53c1742"
integrity sha512-kef+ksXVKun224mRxat+DdO4C+cGHla+fEcZfnBAvoZocwiaceOfhf5azHYOPXSSB1igWVFTEOF3CDENPnuWxg== integrity sha512-kef+ksXVKun224mRxat+DdO4C+cGHla+fEcZfnBAvoZocwiaceOfhf5azHYOPXSSB1igWVFTEOF3CDENPnuWxg==
svelte@^3.37.0: svelte@^3.38.2:
version "3.38.2" version "3.38.2"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5" resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5"
integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg== integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg==

2
packages/client/package.json

@ -38,7 +38,7 @@
"rollup-plugin-svelte": "^7.1.0", "rollup-plugin-svelte": "^7.1.0",
"rollup-plugin-svg": "^2.0.0", "rollup-plugin-svg": "^2.0.0",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"svelte": "^3.37.0" "svelte": "^3.38.2"
}, },
"gitHead": "4b6efc42ed3273595c7a129411f4d883733d3321" "gitHead": "4b6efc42ed3273595c7a129411f4d883733d3321"
} }

2
packages/server/package.json

@ -122,7 +122,7 @@
"pouchdb-find": "^7.2.2", "pouchdb-find": "^7.2.2",
"pouchdb-replication-stream": "1.2.9", "pouchdb-replication-stream": "1.2.9",
"server-destroy": "1.0.1", "server-destroy": "1.0.1",
"svelte": "3.30.0", "svelte": "^3.38.2",
"to-json-schema": "0.2.5", "to-json-schema": "0.2.5",
"uuid": "3.3.2", "uuid": "3.3.2",
"validate.js": "0.13.1", "validate.js": "0.13.1",

3
packages/server/src/api/index.js

@ -67,4 +67,7 @@ for (let route of mainRoutes) {
router.use(staticRoutes.routes()) router.use(staticRoutes.routes())
router.use(staticRoutes.allowedMethods()) router.use(staticRoutes.allowedMethods())
// add a redirect for when hitting server directly
router.redirect("/", "/builder")
module.exports = router module.exports = router

4
packages/server/src/utilities/workerRequests.js

@ -19,12 +19,14 @@ function request(ctx, request) {
if (!request.headers) { if (!request.headers) {
request.headers = {} request.headers = {}
} }
if (request.body) { if (request.body && Object.keys(request.body).length > 0) {
request.headers["Content-Type"] = "application/json" request.headers["Content-Type"] = "application/json"
request.body = request.body =
typeof request.body === "object" typeof request.body === "object"
? JSON.stringify(request.body) ? JSON.stringify(request.body)
: request.body : request.body
} else {
delete request.body
} }
if (ctx.headers) { if (ctx.headers) {
request.headers.cookie = ctx.headers.cookie request.headers.cookie = ctx.headers.cookie

1327
packages/server/yarn.lock

File diff suppressed because it is too large

2
packages/standard-components/package.json

@ -23,7 +23,7 @@
], ],
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.5", "@sveltejs/vite-plugin-svelte": "^1.0.0-next.5",
"svelte": "^3.37.0", "svelte": "^3.38.2",
"vite": "^2.1.5" "vite": "^2.1.5"
}, },
"keywords": [ "keywords": [

12
packages/worker/src/api/controllers/admin/templates.js

@ -27,9 +27,17 @@ exports.save = async ctx => {
} }
exports.definitions = async ctx => { exports.definitions = async ctx => {
const bindings = {}
for (let template of TemplateMetadata.email) {
bindings[template.purpose] = template.bindings
}
ctx.body = { ctx.body = {
purpose: TemplateMetadata, bindings: {
bindings: Object.values(TemplateBindings), ...bindings,
common: Object.values(TemplateBindings),
},
} }
} }

93
packages/worker/src/api/controllers/auth.js

@ -0,0 +1,93 @@
const authPkg = require("@budibase/auth")
const { google } = require("@budibase/auth/src/middleware")
const { Configs } = require("../../constants")
const CouchDB = require("../../db")
const { clearCookie } = authPkg.utils
const { Cookies } = authPkg.constants
const { passport } = authPkg.auth
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
exports.authenticate = async (ctx, next) => {
return passport.authenticate("local", async (err, user) => {
if (err) {
return ctx.throw(403, "Unauthorized")
}
const expires = new Date()
expires.setDate(expires.getDate() + 1)
if (!user) {
return ctx.throw(403, "Unauthorized")
}
ctx.cookies.set(Cookies.Auth, user.token, {
expires,
path: "/",
httpOnly: false,
overwrite: true,
})
delete user.token
ctx.body = { user }
})(ctx, next)
}
exports.logout = async ctx => {
clearCookie(ctx, Cookies.Auth)
ctx.body = { message: "User logged out" }
}
/**
* The initial call that google authentication makes to take you to the google login screen.
* On a successful login, you will be redirected to the googleAuth callback route.
*/
exports.googlePreAuth = async (ctx, next) => {
const db = new CouchDB(GLOBAL_DB)
const config = await authPkg.db.getScopedFullConfig(db, {
type: Configs.GOOGLE,
group: ctx.query.group,
})
const strategy = await google.strategyFactory(config)
return passport.authenticate(strategy, {
scope: ["profile", "email"],
})(ctx, next)
}
exports.googleAuth = async (ctx, next) => {
const db = new CouchDB(GLOBAL_DB)
const config = await authPkg.db.getScopedFullConfig(db, {
type: Configs.GOOGLE,
group: ctx.query.group,
})
const strategy = await google.strategyFactory(config)
return passport.authenticate(
strategy,
{ successRedirect: "/", failureRedirect: "/error" },
async (err, user) => {
if (err) {
return ctx.throw(403, "Unauthorized")
}
const expires = new Date()
expires.setDate(expires.getDate() + 1)
if (!user) {
return ctx.throw(403, "Unauthorized")
}
ctx.cookies.set(Cookies.Auth, user.token, {
expires,
path: "/",
httpOnly: false,
overwrite: true,
})
ctx.redirect("/")
}
)(ctx, next)
}

7
packages/worker/src/api/routes/tests/email.spec.js

@ -2,8 +2,13 @@ const setup = require("./utilities")
const { EmailTemplatePurpose } = require("../../../constants") const { EmailTemplatePurpose } = require("../../../constants")
// mock the email system // mock the email system
const sendMailMock = jest.fn()
jest.mock("nodemailer") jest.mock("nodemailer")
const sendMailMock = setup.emailMock() const nodemailer = require("nodemailer")
nodemailer.createTransport.mockReturnValue({
sendMail: sendMailMock,
verify: jest.fn()
})
describe("/api/admin/email", () => { describe("/api/admin/email", () => {
let request = setup.getRequest() let request = setup.getRequest()

92
packages/worker/src/constants/index.js

@ -35,22 +35,46 @@ const EmailTemplatePurpose = {
} }
const TemplateBindings = { const TemplateBindings = {
PLATFORM_URL: "platformUrl", PLATFORM_URL: {
COMPANY: "company", name: "platformUrl",
LOGO_URL: "logoUrl", description: "The URL used to access the budibase platform",
STYLES: "styles", },
BODY: "body", COMPANY: {
INVITE_URL: "inviteUrl", name: "company",
EMAIL: "email", description: "The name of your organization",
RESET_URL: "resetUrl", },
USER: "user", LOGO_URL: {
REQUEST: "request", name: "logoUrl",
DOCS_URL: "docsUrl", description: "The URL of your organizations logo.",
LOGIN_URL: "loginUrl", },
CURRENT_YEAR: "currentYear", EMAIL: {
CURRENT_DATE: "currentDate", name: "email",
RESET_CODE: "resetCode", description: "The recipients email address.",
INVITE_CODE: "inviteCode", },
USER: {
name: "user",
description: "The recipients user object.",
},
REQUEST: {
name: "request",
description: "Additional request metadata.",
},
DOCS_URL: {
name: "docsUrl",
description: "Organization documentation URL.",
},
LOGIN_URL: {
name: "loginUrl",
description: "The URL used to log into the organization budibase instance.",
},
CURRENT_YEAR: {
name: "currentYear",
description: "The current year.",
},
CURRENT_DATE: {
name: "currentDate",
description: "The current date.",
},
} }
const TemplateMetadata = { const TemplateMetadata = {
@ -58,20 +82,52 @@ const TemplateMetadata = {
{ {
name: "Styling", name: "Styling",
purpose: EmailTemplatePurpose.STYLES, purpose: EmailTemplatePurpose.STYLES,
bindings: ["url", "company", "companyUrl", "styles", "body"],
}, },
{ {
name: "Base Format", name: "Base Format",
purpose: EmailTemplatePurpose.BASE, purpose: EmailTemplatePurpose.BASE,
bindings: ["company", "inviteUrl"], bindings: [
{
name: "body",
description: "The main body of another email template.",
},
{
name: "styles",
description: "The contents of the Styling email template.",
},
],
}, },
{ {
name: "Password Recovery", name: "Password Recovery",
purpose: EmailTemplatePurpose.PASSWORD_RECOVERY, purpose: EmailTemplatePurpose.PASSWORD_RECOVERY,
bindings: [
{
name: "resetUrl",
description:
"The URL the recipient must click to reset their password.",
},
{
name: "resetCode",
description:
"The temporary password reset code used in the recipients password reset URL.",
},
],
}, },
{ {
name: "New User Invitation", name: "New User Invitation",
purpose: EmailTemplatePurpose.INVITATION, purpose: EmailTemplatePurpose.INVITATION,
bindings: [
{
name: "inviteUrl",
description:
"The URL the recipient must click to accept the invitation and activate their account.",
},
{
name: "inviteCode",
description:
"The temporary invite code used in the recipients invitation URL.",
},
],
}, },
{ {
name: "Custom", name: "Custom",

Loading…
Cancel
Save