1
0
Fork 0
mirror of synced 2024-09-21 11:53:49 +12:00
budibase/packages/builder/src/components/start/CreateAppModal.svelte

339 lines
8.5 KiB
Svelte
Raw Normal View History

<script>
2020-08-04 01:59:50 +12:00
import { writable } from "svelte/store"
import { store, automationStore, backendUiStore } from "builderStore"
2020-08-04 01:59:50 +12:00
import { string, object } from "yup"
2020-08-04 02:26:28 +12:00
import api, { get } from "builderStore/api"
2020-08-04 01:59:50 +12:00
import Form from "@svelteschool/svelte-forms"
2020-05-27 22:54:53 +12:00
import Spinner from "components/common/Spinner.svelte"
2020-07-31 20:46:23 +12:00
import { API, Info, User } from "./Steps"
import Indicator from "./Indicator.svelte"
2020-05-27 03:37:11 +12:00
import { Input, TextArea, Button } from "@budibase/bbui"
2020-05-27 22:54:53 +12:00
import { goto } from "@sveltech/routify"
2020-05-27 02:25:37 +12:00
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
import { getContext } from "svelte"
2020-05-27 22:54:53 +12:00
import { fade } from "svelte/transition"
import { post } from "builderStore/api"
import analytics from "analytics"
2020-05-27 22:54:53 +12:00
const { open, close } = getContext("simple-modal")
//Move this to context="module" once svelte-forms is updated so that it can bind to stores correctly
const createAppStore = writable({ currentStep: 0, values: {} })
2020-05-27 02:25:37 +12:00
2020-08-04 01:59:50 +12:00
export let hasKey
2020-05-27 22:54:53 +12:00
let isApiKeyValid
let lastApiKey
let fetchApiKeyPromise
const validateApiKey = async apiKey => {
if (!apiKey) return false
2020-09-30 03:35:51 +13:00
// make sure we only fetch once, unless API Key is changed
if (isApiKeyValid === undefined || apiKey !== lastApiKey) {
lastApiKey = apiKey
2020-09-30 03:35:51 +13:00
// svelte reactivity was causing a requst to get fired mutiple times
// so, we make everything await the same promise, if one exists
if (!fetchApiKeyPromise) {
fetchApiKeyPromise = analytics.identifyByApiKey(apiKey)
}
isApiKeyValid = await fetchApiKeyPromise
fetchApiKeyPromise = undefined
}
return isApiKeyValid
}
2020-08-04 01:59:50 +12:00
let submitting = false
let errors = {}
let validationErrors = {}
let validationSchemas = [
{
apiKey: string()
.required("Please enter your API key.")
.test("valid-apikey", "This API key is invalid", validateApiKey),
2020-08-04 01:59:50 +12:00
},
{
applicationName: string().required("Your application must have a name."),
},
{
username: string().required("Your application needs a first user."),
password: string().required(
"Please enter a password for your first user."
),
accessLevelId: string().required(
"You need to select an access level for your user."
),
},
]
let steps = [
{
component: API,
errors,
},
{
component: Info,
errors,
},
{
component: User,
errors,
},
]
if (hasKey) {
validationSchemas.shift()
validationSchemas = validationSchemas
steps.shift()
steps = steps
}
// Handles form navigation
const back = () => {
if ($createAppStore.currentStep > 0) {
$createAppStore.currentStep -= 1
}
}
const next = () => {
$createAppStore.currentStep += 1
}
// $: errors = validationSchemas.validate(values);
$: getErrors(
$createAppStore.values,
validationSchemas[$createAppStore.currentStep]
)
async function getErrors(values, schema) {
try {
validationErrors = {}
await object(schema).validate(values, { abortEarly: false })
} catch (error) {
validationErrors = extractErrors(error)
}
}
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
}
}
async function signUp() {
submitting = true
try {
2020-08-04 02:26:28 +12:00
// Add API key if there is none.
2020-08-04 01:59:50 +12:00
if (!hasKey) {
await updateKey(["budibase", $createAppStore.values.apiKey])
2020-05-27 23:48:38 +12:00
}
2020-08-04 02:26:28 +12:00
// Create App
const appResp = await post("/api/applications", {
2020-08-04 01:59:50 +12:00
name: $createAppStore.values.applicationName,
})
2020-08-04 02:26:28 +12:00
const appJson = await appResp.json()
analytics.captureEvent("App Created", {
2020-08-04 01:59:50 +12:00
name,
2020-08-04 02:26:28 +12:00
appId: appJson._id,
2020-08-04 01:59:50 +12:00
})
2020-08-04 02:26:28 +12:00
// Select Correct Application/DB in prep for creating user
const applicationPkg = await get(`/api/${appJson._id}/appPackage`)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
backendUiStore.actions.reset()
await store.setPackage(pkg)
automationStore.actions.fetch()
2020-08-04 02:26:28 +12:00
} else {
throw new Error(pkg)
}
// Create user
const user = {
name: $createAppStore.values.username,
username: $createAppStore.values.username,
password: $createAppStore.values.password,
accessLevelId: $createAppStore.values.accessLevelId,
}
const userResp = await api.post(`/api/users`, user)
const json = await userResp.json()
$goto(`./${appJson._id}`)
2020-08-04 01:59:50 +12:00
} catch (error) {
console.error(error)
2020-05-27 21:44:15 +12:00
}
}
2020-05-27 02:25:37 +12:00
2020-08-04 01:59:50 +12:00
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 {}
2020-08-04 01:59:50 +12:00
return inner.reduce((acc, err) => {
return { ...acc, [err.path]: err.message }
}, {})
}
let currentStepIsValid = false
let fullFormIsValid = false
$: checkValidity($createAppStore.values, $createAppStore.currentStep)
2020-05-27 02:25:37 +12:00
let onChange = () => {}
function _onCancel() {
close()
}
2020-05-27 21:44:15 +12:00
async function _onOkay() {
await createNewApp()
2020-05-27 02:25:37 +12:00
}
</script>
<div class="container">
2020-07-31 20:46:23 +12:00
<div class="sidebar">
{#each steps as { active, done }, i}
<Indicator
2020-08-04 01:59:50 +12:00
active={$createAppStore.currentStep === i}
done={i < $createAppStore.currentStep}
2020-07-31 20:46:23 +12:00
step={i + 1} />
{/each}
</div>
2020-05-27 02:25:37 +12:00
<div class="body">
<div class="heading">
2020-07-31 20:46:23 +12:00
<h3 class="header">Get Started with Budibase</h3>
</div>
<div class="step">
2020-08-04 01:59:50 +12:00
<Form bind:values={$createAppStore.values}>
{#each steps as step, i (i)}
<div class:hidden={$createAppStore.currentStep !== i}>
<svelte:component
this={step.component}
{validationErrors}
options={step.options}
name={step.name} />
</div>
{/each}
</Form>
2020-07-31 20:46:23 +12:00
</div>
<div class="footer">
2020-08-04 01:59:50 +12:00
{#if $createAppStore.currentStep > 0}
<Button medium secondary on:click={back}>Back</Button>
2020-08-04 01:59:50 +12:00
{/if}
{#if $createAppStore.currentStep < steps.length - 1}
<Button medium blue on:click={next} disabled={!currentStepIsValid}>
2020-08-04 01:59:50 +12:00
Next
</Button>
2020-07-31 20:46:23 +12:00
{/if}
2020-08-04 01:59:50 +12:00
{#if $createAppStore.currentStep === steps.length - 1}
<Button
medium
blue
2020-08-04 01:59:50 +12:00
on:click={signUp}
disabled={!fullFormIsValid || submitting}>
{submitting ? 'Loading...' : 'Submit'}
</Button>
2020-07-31 20:46:23 +12:00
{/if}
2020-05-27 02:25:37 +12:00
</div>
2020-05-27 22:54:53 +12:00
</div>
<div class="close-button" on:click={_onCancel}>
<CloseIcon />
2020-05-27 02:25:37 +12:00
</div>
2020-08-04 02:30:26 +12:00
<img src="/_builder/assets/bb-logo.svg" alt="budibase icon" />
2020-08-04 01:59:50 +12:00
{#if submitting}
2020-05-27 22:54:53 +12:00
<div in:fade class="spinner-container">
<Spinner />
<span class="spinner-text">Creating your app...</span>
</div>
{/if}
2020-05-27 02:25:37 +12:00
</div>
<style>
2020-05-27 22:54:53 +12:00
.container {
2020-08-04 01:59:50 +12:00
min-height: 600px;
2020-07-31 20:46:23 +12:00
display: grid;
grid-template-columns: 80px 1fr;
2020-05-27 22:54:53 +12:00
position: relative;
}
2020-07-31 20:46:23 +12:00
.sidebar {
display: grid;
border-bottom-left-radius: 0.5rem;
border-top-left-radius: 0.5rem;
grid-gap: 30px;
align-content: center;
background: #f5f5f5;
}
2020-05-27 02:25:37 +12:00
.close-button {
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
}
.close-button :global(svg) {
width: 24px;
height: 24px;
}
.heading {
display: flex;
flex-direction: row;
align-items: center;
2020-05-27 03:37:11 +12:00
margin-bottom: 20px;
2020-05-27 02:25:37 +12:00
}
.header {
2020-05-27 02:25:37 +12:00
margin: 0;
font-size: 24px;
font-weight: 600;
2020-05-27 02:25:37 +12:00
}
.body {
2020-07-31 20:46:23 +12:00
padding: 40px 60px 60px 60px;
2020-05-27 02:25:37 +12:00
display: grid;
2020-08-04 01:59:50 +12:00
align-items: center;
2020-07-31 20:46:23 +12:00
grid-template-rows: auto 1fr auto;
2020-05-27 02:25:37 +12:00
}
.footer {
display: grid;
2020-07-31 20:46:23 +12:00
grid-gap: 15px;
grid-template-columns: auto auto;
justify-content: end;
2020-05-27 02:25:37 +12:00
}
2020-05-27 22:54:53 +12:00
.spinner-container {
background: white;
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;
}
2020-08-04 01:59:50 +12:00
.hidden {
display: none;
2020-05-27 23:48:38 +12:00
}
2020-08-04 02:30:26 +12:00
img {
position: absolute;
top: 20px;
left: 20px;
height: 40px;
}
2020-05-27 02:25:37 +12:00
</style>