Rewrite create app modal to work with new spectrum fields

This commit is contained in:
Andrew Kingston 2021-04-15 19:28:50 +01:00
parent 591d1c0fc5
commit 910ac855cf
4 changed files with 82 additions and 137 deletions

View File

@ -4,7 +4,6 @@
import { store, automationStore, hostingStore } from "builderStore" import { store, automationStore, hostingStore } from "builderStore"
import { string, object } from "yup" import { string, object } from "yup"
import api, { get } from "builderStore/api" import api, { get } from "builderStore/api"
import Form from "@svelteschool/svelte-forms"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import { Info, User } from "./Steps" import { Info, User } from "./Steps"
import Indicator from "./Indicator.svelte" import Indicator from "./Indicator.svelte"
@ -16,44 +15,40 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import Logo from "/assets/bb-logo.svg" import Logo from "/assets/bb-logo.svg"
//Move this to context="module" once svelte-forms is updated so that it can bind to stores correctly
const createAppStore = writable({ currentStep: 0, values: {} })
export let template export let template
const infoValidation = { const currentStep = writable(0)
const values = writable({ roleId: "ADMIN" })
const errors = writable({})
const touched = writable({})
const steps = [Info, User]
let validators = [
{
applicationName: string().required("Your application must have a name."), applicationName: string().required("Your application must have a name."),
} },
const userValidation = { {
email: string() email: string()
.email() .email()
.required("Your application needs a first user."), .required("Your application needs a first user."),
password: string().required("Please enter a password for your first user."), password: string().required(
roleId: string().required("You need to select a role for your user."), "Please enter a password for your first user."
} ),
roleId: string()
.nullable()
.required("You need to select a role for your user."),
},
]
let submitting = false let submitting = false
let errors = {} let valid = false
let validationErrors = {} $: checkValidity($values, validators[$currentStep])
let validationSchemas = [infoValidation, userValidation]
function buildStep(component) {
return {
component,
errors,
}
}
// steps need to be initialized for cypress from the get go
let steps = [buildStep(Info), buildStep(User)]
onMount(async () => { onMount(async () => {
let hostingInfo = await hostingStore.actions.fetch() const hostingInfo = await hostingStore.actions.fetch()
// re-init the steps based on whether self hosting or cloud hosted
if (hostingInfo.type === "self") { if (hostingInfo.type === "self") {
await hostingStore.actions.fetchDeployedApps() await hostingStore.actions.fetchDeployedApps()
const existingAppNames = svelteGet(hostingStore).deployedAppNames const existingAppNames = svelteGet(hostingStore).deployedAppNames
infoValidation.applicationName = string() validators[0].applicationName = string()
.required("Your application must have a name.") .required("Your application must have a name.")
.test( .test(
"non-existing-app-name", "non-existing-app-name",
@ -63,54 +58,20 @@
appName => appName.toLowerCase() === value.toLowerCase() appName => appName.toLowerCase() === value.toLowerCase()
) )
) )
steps = [buildStep(Info), buildStep(User)]
validationSchemas = [infoValidation, userValidation]
} }
}) })
// Handles form navigation const checkValidity = async (values, validator) => {
const back = () => { const obj = object().shape(validator)
if ($createAppStore.currentStep > 0) { Object.keys(validator).forEach(key => ($errors[key] = null))
$createAppStore.currentStep -= 1
}
}
const next = () => {
$createAppStore.currentStep += 1
}
// $: errors = validationSchemas.validate(values);
$: getErrors(
$createAppStore.values,
validationSchemas[$createAppStore.currentStep]
)
async function getErrors(values, schema) {
try { try {
validationErrors = {} await obj.validate(values, { abortEarly: false })
await object(schema).validate(values, { abortEarly: false }) } catch (validationErrors) {
} catch (error) { validationErrors.inner.forEach(error => {
validationErrors = extractErrors(error) $errors[error.path] = error.message
} })
}
const checkValidity = async (values, currentStep) => {
const validity = await object()
.shape(validationSchemas[currentStep])
.isValid(values)
currentStepIsValid = validity
// Check full form on last step
if (currentStep === steps.length - 1) {
// Make one big schema from all the small ones
const fullSchema = Object.assign({}, ...validationSchemas)
// Check full form schema
const formIsValid = await object()
.shape(fullSchema)
.isValid(values)
fullFormIsValid = formIsValid
} }
valid = await obj.isValid(values)
} }
async function createNewApp() { async function createNewApp() {
@ -118,7 +79,7 @@
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", $createAppStore.values.applicationName) data.append("name", $values.applicationName)
data.append("useTemplate", template != null) data.append("useTemplate", template != null)
if (template) { if (template) {
data.append("templateName", template.name) data.append("templateName", template.name)
@ -134,7 +95,7 @@
} }
analytics.captureEvent("App Created", { analytics.captureEvent("App Created", {
name: $createAppStore.values.applicationName, name: $values.applicationName,
appId: appJson._id, appId: appJson._id,
template, template,
}) })
@ -153,12 +114,12 @@
// Create user // Create user
const user = { const user = {
email: $createAppStore.values.email, email: $values.email,
password: $createAppStore.values.password, password: $values.password,
roleId: $createAppStore.values.roleId, roleId: $values.roleId,
} }
const userResp = await api.post(`/api/users`, user) const userResp = await api.post(`/api/users`, user)
const json = await userResp.json() await userResp.json()
$goto(`./${appJson._id}`) $goto(`./${appJson._id}`)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -166,33 +127,14 @@
submitting = false submitting = false
} }
} }
async function updateKey([key, value]) {
const response = await api.put(`/api/keys/${key}`, { value })
const res = await response.json()
return res
}
function extractErrors({ inner }) {
if (!inner) return {}
return inner.reduce((acc, err) => {
return { ...acc, [err.path]: err.message }
}, {})
}
let currentStepIsValid = false
let fullFormIsValid = false
$: checkValidity($createAppStore.values, $createAppStore.currentStep)
let onChange = () => {}
</script> </script>
<div class="container"> <div class="container">
<div class="sidebar"> <div class="sidebar">
{#each steps as { active, done }, i} {#each steps as component, i}
<Indicator <Indicator
active={$createAppStore.currentStep === i} active={$currentStep === i}
done={i < $createAppStore.currentStep} done={i < $currentStep}
step={i + 1} /> step={i + 1} />
{/each} {/each}
</div> </div>
@ -201,34 +143,32 @@
<h3 class="header">Get Started with Budibase</h3> <h3 class="header">Get Started with Budibase</h3>
</div> </div>
<div class="step"> <div class="step">
<Form bind:values={$createAppStore.values}> {#each steps as component, i (i)}
{#each steps as step, i (i)} <div class:hidden={$currentStep !== i}>
<div class:hidden={$createAppStore.currentStep !== i}>
<svelte:component <svelte:component
this={step.component} this={component}
{template} {template}
{validationErrors} {values}
options={step.options} {errors}
name={step.name} /> {touched} />
</div> </div>
{/each} {/each}
</Form>
</div> </div>
<div class="footer"> <div class="footer">
{#if $createAppStore.currentStep > 0} {#if $currentStep > 0}
<Button medium secondary on:click={back}>Back</Button> <Button medium secondary on:click={() => $currentStep--}>Back</Button>
{/if} {/if}
{#if $createAppStore.currentStep < steps.length - 1} {#if $currentStep < steps.length - 1}
<Button medium blue on:click={next} disabled={!currentStepIsValid}> <Button medium blue on:click={() => $currentStep++} disabled={!valid}>
Next Next
</Button> </Button>
{/if} {/if}
{#if $createAppStore.currentStep === steps.length - 1} {#if $currentStep === steps.length - 1}
<Button <Button
medium medium
blue blue
on:click={createNewApp} on:click={createNewApp}
disabled={!fullFormIsValid || submitting}> disabled={!valid || submitting}>
{submitting ? 'Loading...' : 'Submit'} {submitting ? 'Loading...' : 'Submit'}
</Button> </Button>
{/if} {/if}

View File

@ -1,11 +1,14 @@
<script> <script>
import { Label, Heading, Input } from "@budibase/bbui" import { Label, Heading, Input } from "@budibase/bbui"
import { notifications } from "@budibase/bbui"
const BYTES_IN_MB = 1000000 const BYTES_IN_MB = 1000000
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5 const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
export let validationErrors
export let template export let template
export let values
export let errors
export let touched
let blurred = { appName: false } let blurred = { appName: false }
let file let file
@ -51,12 +54,11 @@
</div> </div>
{/if} {/if}
<Input <Input
on:input={() => (blurred.appName = true)} on:change={() => ($touched.applicationName = true)}
bind:value={$values.applicationName}
label="Web App Name" label="Web App Name"
name="applicationName"
placeholder="Enter name of your web application" placeholder="Enter name of your web application"
type="name" error={$touched.applicationName && $errors.applicationName} />
error={blurred.appName && validationErrors.applicationName} />
</div> </div>
<style> <style>

View File

@ -1,30 +1,34 @@
<script> <script>
import { Input, Select } from "@budibase/bbui" import { Input, Select } from "@budibase/bbui"
export let validationErrors
let blurred = { email: false, password: false } export let values
export let errors
export let touched
</script> </script>
<h2>Create your first User</h2> <h2>Create your first User</h2>
<div class="container"> <div class="container">
<Input <Input
on:input={() => (blurred.email = true)} bind:value={$values.email}
on:change={() => ($touched.email = true)}
label="Email" label="Email"
name="email"
placeholder="Email" placeholder="Email"
type="email" type="email"
error={blurred.email && validationErrors.email} /> error={$touched.email && $errors.email} />
<Input <Input
on:input={() => (blurred.password = true)} bind:value={$values.password}
on:change={() => ($touched.password = true)}
label="Password" label="Password"
name="password"
placeholder="Password" placeholder="Password"
type="password" type="password"
error={blurred.password && validationErrors.password} /> error={$touched.password && $errors.password} />
<Select label="Role" secondary name="roleId"> <Select
<option value="ADMIN">Admin</option> bind:value={$values.roleId}
<option value="POWER_USER">Power User</option> label="Role"
</Select> options={[{ label: 'Admin', value: 'ADMIN' }, { label: 'Power User', value: 'POWER_USER' }]}
getOptionLabel={option => option.label}
getOptionValue={option => option.value}
error={$errors.roleId} />
</div> </div>
<style> <style>

View File

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