Merge pull request #2896 from Budibase/feature/onboarding-templates

Feature/onboarding templates
This commit is contained in:
Martin McKeaveney 2021-10-06 14:50:12 +01:00 committed by GitHub
commit 3cb088e930
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 121 additions and 1073 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

View File

@ -30,7 +30,7 @@ Cypress.Commands.add("login", () => {
Cypress.Commands.add("createApp", name => { Cypress.Commands.add("createApp", name => {
cy.visit(`localhost:${Cypress.env("PORT")}/builder`) cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
cy.wait(500) cy.wait(500)
cy.contains(/Create (new )?app/).click() cy.contains(/Start from scratch/).click()
cy.get(".spectrum-Modal") cy.get(".spectrum-Modal")
.within(() => { .within(() => {
cy.get("input").eq(0).type(name).should("have.value", name).blur() cy.get("input").eq(0).type(name).should("have.value", name).blur()

View File

@ -17,6 +17,7 @@
import { capitalise } from "helpers" import { capitalise } from "helpers"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { APP_NAME_REGEX } from "constants" import { APP_NAME_REGEX } from "constants"
import TemplateList from "./TemplateList.svelte"
export let template export let template
@ -31,12 +32,16 @@
APP_NAME_REGEX, APP_NAME_REGEX,
"App name must be letters, numbers and spaces only" "App name must be letters, numbers and spaces only"
), ),
file: template ? mixed().required("Please choose a file to import") : null, file: template?.fromFile
? mixed().required("Please choose a file to import")
: null,
} }
let submitting = false let submitting = false
let valid = false let valid = false
$: checkValidity($values, validator) $: checkValidity($values, validator)
$: showTemplateSelection = !template?.fromFile && !template?.key
onMount(async () => { onMount(async () => {
await hostingStore.actions.fetchDeployedApps() await hostingStore.actions.fetchDeployedApps()
@ -73,7 +78,7 @@
submitting = true submitting = true
// Check a template exists if we are important // Check a template exists if we are important
if (template && !$values.file) { if (template?.fromFile && !$values.file) {
$errors.file = "Please choose a file to import" $errors.file = "Please choose a file to import"
valid = false valid = false
submitting = false submitting = false
@ -133,13 +138,38 @@
} }
</script> </script>
{#if showTemplateSelection}
<ModalContent <ModalContent
title={template ? "Import app" : "Create app"} title={"Get started quickly"}
confirmText={template ? "Import app" : "Create app"} showConfirmButton={false}
size="L"
onConfirm={() => {
showTemplateSelection = false
return false
}}
showCancelButton={false}
showCloseIcon={false}
>
<Body size="M">Select a template below, or start from scratch.</Body>
<TemplateList
onSelect={selected => {
if (!selected) {
showTemplateSelection = false
return
}
template = selected
}}
/>
</ModalContent>
{:else}
<ModalContent
title={template?.fromFile ? "Import app" : "Create app"}
confirmText={template?.fromFile ? "Import app" : "Create app"}
onConfirm={createNewApp} onConfirm={createNewApp}
disabled={!valid} disabled={!valid}
> >
{#if template} {#if template?.fromFile}
<Dropzone <Dropzone
error={$touched.file && $errors.file} error={$touched.file && $errors.file}
gallery={false} gallery={false}
@ -163,3 +193,4 @@
/> />
<Checkbox label="Group access" disabled value={true} text="All users" /> <Checkbox label="Group access" disabled value={true} text="All users" />
</ModalContent> </ModalContent>
{/if}

View File

@ -1,5 +1,5 @@
<script> <script>
import { Button, Heading, Body } from "@budibase/bbui" import { Heading, Layout, Icon } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import api from "builderStore/api" import api from "builderStore/api"
@ -13,8 +13,7 @@
let templatesPromise = fetchTemplates() let templatesPromise = fetchTemplates()
</script> </script>
<div class="root"> <Layout gap="XS" noPadding>
<Heading size="M">Start With a Template</Heading>
{#await templatesPromise} {#await templatesPromise}
<div class="spinner-container"> <div class="spinner-container">
<Spinner size="30" /> <Spinner size="30" />
@ -22,41 +21,69 @@
{:then templates} {:then templates}
<div class="templates"> <div class="templates">
{#each templates as template} {#each templates as template}
<div class="templates-card"> <div class="template" on:click={() => onSelect(template)}>
<Heading size="S">{template.name}</Heading> <div
<Body size="M" grey>{template.category}</Body> class="background-icon"
<Body size="S" black>{template.description}</Body> style={`background: ${template.background};`}
<div><img alt="template" src={template.image} width="100%" /></div> >
<div class="card-footer"> <Icon name={template.icon} />
<Button secondary on:click={() => onSelect(template)}>
Create
{template.name}
</Button>
</div> </div>
<Heading size="XS">{template.name}</Heading>
<p class="detail">{template?.category?.toUpperCase()}</p>
</div> </div>
{/each} {/each}
<div class="template start-from-scratch" on:click={() => onSelect(null)}>
<div class="background-icon" style={`background: var(--background);`}>
<Icon name="Add" />
</div>
<Heading size="XS">Start from scratch</Heading>
<p class="detail">BLANK</p>
</div>
</div> </div>
{:catch err} {:catch err}
<h1 style="color:red">{err}</h1> <h1 style="color:red">{err}</h1>
{/await} {/await}
</div> </Layout>
<style> <style>
.templates { .templates {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); width: 100%;
grid-gap: var(--layout-m); grid-gap: var(--spacing-m);
grid-template-columns: 1fr;
justify-content: start; justify-content: start;
margin-top: 15px;
} }
.templates-card { .background-icon {
background-color: var(--background); padding: 10px;
padding: var(--spacing-xl); border-radius: 4px;
border-radius: var(--border-radius-m); display: flex;
border: var(--border-dark); align-items: center;
justify-content: center;
width: 18px;
color: white;
} }
.card-footer { .template {
margin-top: var(--spacing-m); height: 60px;
display: grid;
grid-gap: var(--layout-m);
grid-template-columns: 5% 1fr 15%;
border: 1px solid #494949;
align-items: center;
cursor: pointer;
border-radius: 4px;
background: #1a1a1a;
padding: 8px 16px;
}
.detail {
text-align: right;
}
.start-from-scratch {
background: var(--spectrum-global-color-gray-50);
margin-top: 20px;
} }
</style> </style>

View File

@ -159,8 +159,6 @@
cursor: pointer; cursor: pointer;
filter: brightness(110%); filter: brightness(110%);
} }
.group {
}
.app { .app {
display: grid; display: grid;
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto;

View File

@ -8,10 +8,8 @@
ButtonGroup, ButtonGroup,
Select, Select,
Modal, Modal,
ModalContent,
Page, Page,
notifications, notifications,
Body,
Search, Search,
} from "@budibase/bbui" } from "@budibase/bbui"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
@ -270,22 +268,7 @@
{#if !enrichedApps.length && !creatingApp && loaded} {#if !enrichedApps.length && !creatingApp && loaded}
<div class="empty-wrapper"> <div class="empty-wrapper">
<Modal inline> <Modal inline>
<ModalContent <CreateAppModal {template} />
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> </Modal>
</div> </div>
{/if} {/if}

File diff suppressed because it is too large Load Diff

View File

@ -76,8 +76,9 @@ exports.getTemplateStream = async template => {
if (template.file) { if (template.file) {
return fs.createReadStream(template.file.path) return fs.createReadStream(template.file.path)
} else { } else {
const tmpPath = await exports.downloadTemplate(...template.key.split("/")) const [type, name] = template.key.split("/")
return fs.createReadStream(join(tmpPath, "db", "dump.txt")) const tmpPath = await exports.downloadTemplate(type, name)
return fs.createReadStream(join(tmpPath, name, "db", "dump.txt"))
} }
} }