Browse Source

Merge pull request #1472 from Budibase/app-list

App list screen
gh-pages
Andrew Kingston 5 years ago
committed by GitHub
parent
commit
5aebceb608
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      package.json
  2. 2
      packages/bbui/package.json
  3. 97
      packages/bbui/src/Form/Core/Dropzone.svelte
  4. 1
      packages/bbui/src/Form/Core/TextField.svelte
  5. 2
      packages/bbui/src/Form/Dropzone.svelte
  6. 1
      packages/bbui/src/Form/Input.svelte
  7. 2
      packages/bbui/src/Layout/Layout.svelte
  8. 9
      packages/bbui/src/Layout/Page.svelte
  9. 21
      packages/bbui/src/Modal/Modal.svelte
  10. 76
      packages/bbui/src/Table/Table.svelte
  11. 8
      packages/bbui/yarn.lock
  12. 4
      packages/builder/package.json
  13. 66
      packages/builder/src/actions.js
  14. 9
      packages/builder/src/components/common/ConfirmDialog.svelte
  15. 88
      packages/builder/src/components/start/AppCard.svelte
  16. 25
      packages/builder/src/components/start/AppList.svelte
  17. 80
      packages/builder/src/components/start/AppRow.svelte
  18. 239
      packages/builder/src/components/start/CreateAppModal.svelte
  19. 83
      packages/builder/src/components/start/Indicator.svelte
  20. 121
      packages/builder/src/components/start/Steps/Info.svelte
  21. 29
      packages/builder/src/components/start/Steps/User.svelte
  22. 2
      packages/builder/src/components/start/Steps/index.js
  23. 0
      packages/builder/src/pages/builder/app/[application]/_layout.svelte
  24. 10
      packages/builder/src/pages/builder/portal/_layout.svelte
  25. 7
      packages/builder/src/pages/builder/portal/apps/_layout.svelte
  26. 221
      packages/builder/src/pages/builder/portal/apps/index.svelte
  27. 10
      packages/builder/yarn.lock
  28. 2
      packages/client/package.json
  29. 2
      packages/server/package.json
  30. 1321
      packages/server/yarn.lock
  31. 2
      packages/standard-components/package.json

2
package.json

@ -14,7 +14,7 @@
"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", "setup": "./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",

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>

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
} }

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

10
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" },
@ -161,5 +161,7 @@
} }
.content { .content {
overflow: auto; overflow: auto;
display: grid;
grid-template-rows: 1fr;
} }
</style> </style>

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>

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",

1321
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": [

Loading…
Cancel
Save