1
0
Fork 0
mirror of synced 2024-06-29 03:20:34 +12:00

Merge pull request #1472 from Budibase/app-list

App list screen
This commit is contained in:
Andrew Kingston 2021-05-11 08:43:47 +01:00 committed by GitHub
commit 63c22494fd
31 changed files with 622 additions and 1924 deletions

View file

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

View file

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

View file

@ -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 class="footer">File {selectedImageIdx + 1} of {fileCount}</div>
</div> </div>
<div {:else if value?.length}
class="nav right" {#each value as file}
class:visible={selectedImageIdx < fileCount - 1} <div class="gallery">
on:click={navigateRight} <div class="title">
> <div class="filename">{file.name}</div>
<Icon name="ChevronRight" /> <div class="filesize">
</div> {#if file.size <= BYTES_IN_MB}
<div class="footer">File {selectedImageIdx + 1} of {fileCount}</div> {`${file.size / BYTES_IN_KB} KB`}
</div> {: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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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;

View file

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

View file

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

View file

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

View file

@ -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 />

View file

@ -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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = () => {
template = { fromFile: true } creationModal.show()
modal.show() creatingApp = true
} }
onMount(checkKeys) const initiateAppImport = () => {
template = { fromFile: true }
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(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> <div class="select">
<ActionGroup> <Select quiet placeholder="Filter by groups" />
<ActionButton </div>
on:click={() => (layout = "grid")} <ActionGroup>
selected={layout === "grid"} <ActionButton
quiet on:click={() => (layout = "grid")}
icon="ClassicGridView" selected={layout === "grid"}
/> quiet
<ActionButton icon="ClassicGridView"
on:click={() => (layout = "table")} />
selected={layout === "table"} <ActionButton
quiet on:click={() => (layout = "table")}
icon="ViewRow" selected={layout === "table"}
/> quiet
</ActionGroup> icon="ViewRow"
</div> />
{#if layout === "grid"} </ActionGroup>
<AppList /> </div>
{:else} <div
Table view. 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}
</Layout> {#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>
{/if}
</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>

View file

@ -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==

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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