Initial commit of home screen modifications and template browsing

This commit is contained in:
Dean 2022-03-22 11:38:17 +00:00
parent d2e7b28304
commit 6c269bf091
6 changed files with 649 additions and 226 deletions

View File

@ -0,0 +1,124 @@
<script>
export let backgroundColour
export let imageSrc
export let name
export let icon
export let overlayEnabled = true
let imageError = false
let imageLoaded = false
const imageRenderError = () => {
imageError = true
}
const imageLoadSuccess = () => {
imageLoaded = true
}
</script>
<div class="template-card" style="background-color:{backgroundColour};">
<div class="template-thumbnail card-body">
<img
alt={name}
src={imageSrc}
on:error={imageRenderError}
on:load={imageLoadSuccess}
class={`${imageLoaded ? "loaded" : ""}`}
/>
<div style={`display:${imageError ? "block" : "none"}`}>
<svg
width="26px"
height="26px"
class="spectrum-Icon"
style="color: white"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
</div>
<div class={overlayEnabled ? "template-thumbnail-action-overlay" : ""}>
<slot />
</div>
</div>
<div class="template-thumbnail-text">
<div>{name}</div>
</div>
</div>
<style>
.template-thumbnail {
position: relative;
}
.template-card:hover .template-thumbnail-action-overlay {
display: flex;
flex-direction: column;
}
.template-thumbnail-action-overlay {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 70%;
display: none;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.7);
border-top-right-radius: inherit;
border-top-left-radius: inherit;
}
.template-thumbnail-text {
position: absolute;
bottom: 0px;
display: flex;
align-items: center;
height: 30%;
width: 100%;
color: var(
--spectrum-heading-xs-text-color,
var(--spectrum-alias-heading-text-color)
);
background-color: var(--spectrum-global-color-gray-50);
}
.template-thumbnail-text > div {
padding-left: 1rem;
padding-right: 1rem;
}
.template-card {
position: relative;
display: flex;
border-radius: var(--border-radius-s);
border: 1px solid var(--spectrum-global-color-gray-300);
overflow: hidden;
min-height: 200px;
}
.template-card > *{
width : 100%
}
.template-card img.loaded {
display: block;
}
.template-card img {
display: none;
max-width: 100%;
border-radius: var(--border-radius-s) 0px var(--border-radius-s) 0px;
}
.template-card:hover {
background: var(--spectrum-alias-background-color-tertiary);
}
.card-body {
padding-left: 1rem;
padding-top: 1rem;
}
</style>

View File

@ -0,0 +1,147 @@
<script>
import { Layout, Detail, Heading, Button, Modal } from "@budibase/bbui"
import TemplateCard from "components/common/TemplateCard.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte"
export let templates
let selectedTemplateCategory
let creationModal
let template
const groupTemplatesByCategory = (templates, categoryFilter) => {
let grouped = templates.reduce((acc, template) => {
if (
typeof categoryFilter === "string" &&
[categoryFilter].indexOf(template.category) < 0
) {
return acc
}
acc[template.category] = !acc[template.category]
? []
: acc[template.category]
acc[template.category].push(template)
return acc
}, {})
return grouped
}
$: filteredTemplates = groupTemplatesByCategory(
templates,
selectedTemplateCategory
)
$: filteredTemplateCategories = filteredTemplates
? Object.keys(filteredTemplates).sort()
: []
$: templateCategories = templates
? Object.keys(groupTemplatesByCategory(templates)).sort()
: []
const stopAppCreation = () => {
template = null
}
</script>
<div class="template-category-filters">
<Layout noPadding gap="S">
<Heading size="S">Templates</Heading>
<div class="template-category-filters spectrum-ActionGroup">
<button
on:click={() => {
selectedTemplateCategory = null
}}
class="template-category-filter-all template-category-filter spectrum-ActionButton spectrum-ActionButton--sizeM
spectrum-ActionGroup-item {!selectedTemplateCategory ||
'is-selected'}"
>
<span class="spectrum-ActionButton-label">All</span>
</button>
{#each templateCategories as templateCategoryKey}
<button
on:click={() => {
selectedTemplateCategory = templateCategoryKey
}}
class="template-category-filter spectrum-ActionButton spectrum-ActionButton--sizeM
spectrum-ActionGroup-item {templateCategoryKey ==
selectedTemplateCategory || 'is-selected'}"
>
<span class="spectrum-ActionButton-label">{templateCategoryKey}</span>
</button>
{/each}
</div>
</Layout>
</div>
<div class="template-categories">
<Layout gap="XL" noPadding>
{#each filteredTemplateCategories as templateCategoryKey}
<div class="template-category">
<Detail size="M">{templateCategoryKey}</Detail>
<div class="template-grid">
{#each filteredTemplates[templateCategoryKey] as templateEntry}
<TemplateCard
name={templateEntry.name}
imageSrc={templateEntry.image}
backgroundColour={templateEntry.background}
icon={templateEntry.icon}
>
<Button
cta
on:click={() => {
template = templateEntry
creationModal.show()
}}
>
Use template
</Button>
<a
href={templateEntry.url}
target="_blank"
class="overlay-preview-link spectrum-Button spectrum-Button--sizeM spectrum-Button--secondary"
on:click|stopPropagation
>
Details
</a>
</TemplateCard>
{/each}
</div>
</div>
{/each}
</Layout>
</div>
<Modal
bind:this={creationModal}
padding={false}
width="600px"
on:hide={stopAppCreation}
>
<CreateAppModal {template} />
</Modal>
<style>
.template-grid {
padding-top: 10px;
display: grid;
grid-gap: var(--spacing-xl);
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
a:hover.spectrum-Button.spectrum-Button--secondary.overlay-preview-link {
background-color: #c8c8c8;
border-color: #c8c8c8;
color: #505050;
}
a.spectrum-Button--secondary.overlay-preview-link {
margin-top: 20px;
border-color: #c8c8c8;
color: #c8c8c8;
}
</style>

View File

@ -9,6 +9,7 @@
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { createValidationStore } from "helpers/validation/yup" import { createValidationStore } from "helpers/validation/yup"
import * as appValidation from "helpers/validation/yup/app" import * as appValidation from "helpers/validation/yup/app"
import TemplateCard from "components/common/TemplateCard.svelte"
export let template export let template
@ -17,9 +18,30 @@
$: validation.check($values) $: validation.check($values)
onMount(async () => { onMount(async () => {
$values.url = resolveAppUrl(template, $values.name, $values.url)
$values.name = resolveAppName(template, $values.name)
await setupValidation() await setupValidation()
}) })
$: appUrl = `${window.location.origin}${
$values.url ? $values.url : `/${resolveAppUrl(template, $values.name)}`
}`
const resolveAppUrl = (template, name) => {
let parsedName
const resolvedName = resolveAppName(template, name)
parsedName = resolvedName ? resolvedName.toLowerCase() : ""
const parsedUrl = parsedName ? parsedName.replace(/\s+/g, "-") : ""
return encodeURI(parsedUrl)
}
const resolveAppName = (template, name) => {
if (template && !name) {
return template.name
}
return name.trim()
}
const setupValidation = async () => { const setupValidation = async () => {
const applications = svelteGet(apps) const applications = svelteGet(apps)
appValidation.name(validation, { apps: applications }) appValidation.name(validation, { apps: applications })
@ -83,6 +105,15 @@
onConfirm={createNewApp} onConfirm={createNewApp}
disabled={!$validation.valid} disabled={!$validation.valid}
> >
{#if template && !template?.fromFile}
<TemplateCard
name={template.name}
imageSrc={template.image}
backgroundColour={template.background}
overlayEnabled={false}
icon={template.icon}
/>
{/if}
{#if template?.fromFile} {#if template?.fromFile}
<Dropzone <Dropzone
error={$validation.touched.file && $validation.errors.file} error={$validation.touched.file && $validation.errors.file}
@ -104,13 +135,38 @@
? `${$auth.user.firstName}s app` ? `${$auth.user.firstName}s app`
: "My app"} : "My app"}
/> />
<Input <span>
bind:value={$values.url} <Input
error={$validation.touched.url && $validation.errors.url} bind:value={$values.url}
on:blur={() => ($validation.touched.url = true)} error={$validation.touched.url && $validation.errors.url}
label="URL" on:blur={() => ($validation.touched.url = true)}
placeholder={$values.name label="URL"
? "/" + encodeURIComponent($values.name).toLowerCase() placeholder={$values.url
: "/"} ? $values.url
/> : `/${resolveAppUrl(template, $values.name)}`}
/>
{#if $values.name}
<div class="app-server-wrap" title={appUrl}>
<span class="app-server-prefix">
{window.location.origin}
</span>
{$values.url
? $values.url
: `/${resolveAppUrl(template, $values.name)}`}
</div>
{/if}
</span>
</ModalContent> </ModalContent>
<style>
.app-server-prefix {
color: var(--spectrum-global-color-gray-500);
}
.app-server-wrap {
margin-top: 10px;
width: 320px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@ -0,0 +1,141 @@
<script>
import { goto } from "@roxi/routify"
import {
Layout,
Page,
notifications,
Button,
Heading,
Body,
Modal,
Divider,
} from "@budibase/bbui"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
import { onMount } from "svelte"
import { templates } from "stores/portal"
let loaded = false
let template
let creationModal = false
let creatingApp = false
const welcomeBody =
"Start from scratch or get a head start with one of our templates"
const createAppTitle = "Create new app"
const createAppButtonText = "Start from scratch"
onMount(async () => {
try {
await templates.load()
if ($templates?.length === 0) {
notifications.error(
"There was a problem loading quick start templates."
)
}
} catch (error) {
notifications.error("Error loading apps and templates")
}
loaded = true
})
const initiateAppCreation = () => {
template = null
creationModal.show()
creatingApp = true
}
const stopAppCreation = () => {
template = null
creatingApp = false
}
const initiateAppImport = () => {
template = { fromFile: true }
creationModal.show()
creatingApp = true
}
</script>
<Page wide>
<Layout noPadding gap="XL">
<span>
<Button
primary
on:click={() => {
$goto("../")
}}
>
Back
</Button>
</span>
<div class="title">
<div class="welcome">
<Layout noPadding gap="XS">
<Heading size="M">{createAppTitle}</Heading>
<Body size="S">
{welcomeBody}
</Body>
</Layout>
<div class="buttons">
<Button size="L" icon="Add" cta on:click={initiateAppCreation}>
{createAppButtonText}
</Button>
<Button
icon="Import"
size="L"
quiet
secondary
on:click={initiateAppImport}
>
Import app
</Button>
</div>
</div>
<Divider size="S" />
</div>
{#if loaded && $templates?.length}
<TemplateDisplay templates={$templates} />
{/if}
</Layout>
</Page>
<Modal
bind:this={creationModal}
padding={false}
width="600px"
on:hide={stopAppCreation}
>
<CreateAppModal {template} />
</Modal>
<style>
.title .welcome > .buttons {
padding-top: 30px;
}
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--spacing-xl);
flex-wrap: wrap;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xl);
flex-wrap: wrap;
}
@media (max-width: 640px) {
.buttons {
flex-direction: row-reverse;
justify-content: flex-end;
}
}
</style>

View File

@ -11,8 +11,9 @@
notifications, notifications,
Body, Body,
Search, Search,
Icon, Divider,
} from "@budibase/bbui" } from "@budibase/bbui"
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
import UpdateAppModal from "components/start/UpdateAppModal.svelte" import UpdateAppModal from "components/start/UpdateAppModal.svelte"
@ -45,6 +46,21 @@
let appName = "" let appName = ""
let creatingFromTemplate = false let creatingFromTemplate = false
const resolveWelcomeMessage = (auth, apps) => {
const userWelcome = auth?.user?.firstName
? `Welcome ${auth?.user?.firstName}!`
: "Welcome back!"
return apps?.length ? userWelcome : "Let's create your first app!"
}
$: welcomeHeader = resolveWelcomeMessage($auth, $apps)
$: welcomeBody = $apps?.length
? "Manage your apps and get a head start with templates"
: "Start from scratch or get a head start with one of our templates"
$: createAppButtonText = $apps?.length
? "Create new app"
: "Start from scratch"
$: enrichedApps = enrichApps($apps, $auth.user, sortBy) $: enrichedApps = enrichApps($apps, $auth.user, sortBy)
$: filteredApps = enrichedApps.filter(app => $: filteredApps = enrichedApps.filter(app =>
app?.name?.toLowerCase().includes(searchTerm.toLowerCase()) app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
@ -79,9 +95,13 @@
} }
const initiateAppCreation = () => { const initiateAppCreation = () => {
template = null if ($apps?.length) {
creationModal.show() $goto("/builder/portal/apps/create")
creatingApp = true } else {
template = null
creationModal.show()
creatingApp = true
}
} }
const initiateAppsExport = () => { const initiateAppsExport = () => {
@ -268,148 +288,113 @@
<Page wide> <Page wide>
<Layout noPadding gap="XL"> <Layout noPadding gap="XL">
<div class="title"> {#if loaded}
<Layout noPadding gap="XS"> <div class="title">
<Heading size="M">Welcome to Budibase</Heading> <div class="welcome">
<Body size="S"> <Layout noPadding gap="XS">
Manage your apps and get a head start with templates <Heading size="M">{welcomeHeader}</Heading>
</Body> <Body size="S">
</Layout> {welcomeBody}
</Body>
</Layout>
<div class="buttons"> <div class="buttons">
{#if cloud} <Button size="L" icon="Add" cta on:click={initiateAppCreation}>
<Button {createAppButtonText}
size="L" </Button>
icon="Export" {#if $apps?.length > 0}
quiet <Button
secondary icon="Experience"
on:click={initiateAppsExport} size="L"
> quiet
Export apps secondary
</Button> on:click={$goto("/builder/portal/apps/templates")}
{/if} >
<Button Templates
icon="Import" </Button>
size="L" {/if}
quiet {#if !$apps?.length}
secondary <Button
on:click={initiateAppImport} icon="Import"
> size="L"
Import app quiet
</Button> secondary
<Button size="L" icon="Add" cta on:click={initiateAppCreation}> on:click={initiateAppImport}
Create app >
</Button> Import app
</Button>
{/if}
</div>
</div>
<div>
<Layout gap="S" justifyItems="center">
<img class="img-logo img-size" alt="logo" src={Logo} />
</Layout>
</div>
<Divider size="S" />
</div> </div>
</div>
<Layout noPadding gap="S"> {#if !$apps?.length && $templates?.length}
<Detail size="L">Quick start templates</Detail> <TemplateDisplay templates={$templates} />
<div class="grid"> {/if}
{#each $templates as item}
<div {#if enrichedApps.length}
on:click={() => { <Layout noPadding gap="S">
template = item <div class="title">
creationModal.show() <Detail size="L">My apps</Detail>
creatingApp = true {#if enrichedApps.length > 1}
}} <div class="app-actions">
class="template-card" {#if cloud}
> <Button
<a size="M"
href={item.url} icon="Export"
target="_blank" quiet
class="external-link" secondary
on:click|stopPropagation on:click={initiateAppsExport}
> >
<Icon name="LinkOut" size="S" /> Export apps
</a> </Button>
<div class="card-body"> {/if}
<div style="color: {item.background}" class="iconAlign"> <div class="filter">
<svg <Select
width="26px" quiet
height="26px" autoWidth
class="spectrum-Icon" bind:value={sortBy}
style="color:{item.background};" placeholder={null}
focusable="false" options={[
> { label: "Sort by name", value: "name" },
<use xlink:href="#spectrum-icon-18-{item.icon}" /> { label: "Sort by recently updated", value: "updated" },
</svg> { label: "Sort by status", value: "status" },
</div> ]}
<div class="iconAlign"> />
<Body weight="900" size="S">{item.name}</Body> <Search placeholder="Search" bind:value={searchTerm} />
<div style="font-size: 10px;">
<Body size="S">{item.category.toUpperCase()}</Body>
</div> </div>
</div> </div>
</div> {/if}
</div> </div>
{/each}
</div>
</Layout>
{#if loaded && enrichedApps.length} <div class="appTable">
<Layout noPadding gap="S"> {#each filteredApps as app (app.appId)}
<div class="title"> <AppRow
<Detail size="L">My apps</Detail> {releaseLock}
<div class="filter"> {editIcon}
<Select {app}
quiet {unpublishApp}
autoWidth {viewApp}
bind:value={sortBy} {editApp}
placeholder={null} {exportApp}
options={[ {deleteApp}
{ label: "Sort by name", value: "name" }, {updateApp}
{ label: "Sort by recently updated", value: "updated" }, />
{ label: "Sort by status", value: "status" }, {/each}
]}
/>
<Search placeholder="Search" bind:value={searchTerm} />
</div> </div>
</div> </Layout>
{/if}
<div class="appTable">
{#each filteredApps as app (app.appId)}
<AppRow
{releaseLock}
{editIcon}
{app}
{unpublishApp}
{viewApp}
{editApp}
{exportApp}
{deleteApp}
{updateApp}
/>
{/each}
</div>
</Layout>
{/if}
{#if !enrichedApps.length && !creatingApp && loaded}
<div class="empty-wrapper">
<div class="centered">
<div class="main">
<Layout gap="S" justifyItems="center">
<img class="img-size" alt="logo" src={Logo} />
<div class="new-screen-text">
<Detail size="M">Create a business app in minutes!</Detail>
</div>
<Button on:click={() => initiateAppCreation()} size="M" cta>
<div class="new-screen-button">
<div class="background-icon" style="color: white;">
<Icon name="Add" />
</div>
Create App
</div></Button
>
</Layout>
</div>
</div>
</div>
{/if} {/if}
{#if creatingFromTemplate} {#if creatingFromTemplate}
<div class="empty-wrapper"> <div class="empty-wrapper">
<img class="img-logo img-size" alt="logo" src={Logo} />
<p>Creating your Budibase app from your selected template...</p> <p>Creating your Budibase app from your selected template...</p>
<Spinner size="10" /> <Spinner size="10" />
</div> </div>
@ -459,6 +444,15 @@
<ChooseIconModal app={selectedApp} bind:this={iconModal} /> <ChooseIconModal app={selectedApp} bind:this={iconModal} />
<style> <style>
.app-actions {
display: flex;
}
.app-actions :global(> button) {
margin-right: 10px
}
.title .welcome > .buttons {
padding-top: 30px;
}
.title { .title {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -475,13 +469,11 @@
gap: var(--spacing-xl); gap: var(--spacing-xl);
flex-wrap: wrap; flex-wrap: wrap;
} }
@media (max-width: 640px) { @media (max-width: 1000px) {
.buttons { .img-logo {
flex-direction: row-reverse; display: none;
justify-content: flex-end;
} }
} }
.filter { .filter {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -489,49 +481,6 @@
align-items: center; align-items: center;
gap: var(--spacing-xl); gap: var(--spacing-xl);
} }
.grid {
height: 200px;
display: grid;
overflow: hidden;
grid-gap: var(--spacing-xl);
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
grid-template-rows: minmax(70px, 1fr) minmax(100px, 1fr) minmax(0px, 0);
}
.template-card {
height: 70px;
border-radius: var(--border-radius-s);
border: 1px solid var(--spectrum-global-color-gray-300);
cursor: pointer;
display: flex;
position: relative;
}
.template-card:hover {
background: var(--spectrum-alias-background-color-tertiary);
}
.card-body {
display: flex;
align-items: center;
padding: 12px;
}
.external-link {
position: absolute;
top: 5px;
right: 5px;
color: var(--spectrum-global-color-gray-300);
z-index: 99;
}
.external-link:hover {
color: var(--spectrum-global-color-gray-500);
}
.iconAlign {
padding: 0 0 0 var(--spacing-m);
display: inline-block;
}
.appTable { .appTable {
display: grid; display: grid;
grid-template-rows: auto; grid-template-rows: auto;
@ -557,7 +506,6 @@
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
} }
} }
.empty-wrapper { .empty-wrapper {
flex: 1 1 auto; flex: 1 1 auto;
height: 100%; height: 100%;
@ -566,42 +514,8 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.centered {
width: calc(100% - 350px);
height: calc(100% - 100px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 300px;
}
.new-screen-text {
width: 160px;
text-align: center;
color: #2c2c2c;
font-weight: 600;
}
.new-screen-button {
margin-left: 5px;
height: 20px;
width: 100px;
display: flex;
align-items: center;
}
.img-size { .img-size {
width: 160px; width: 160px;
height: 160px; height: 160px;
} }
.background-icon {
margin-top: 4px;
margin-right: 4px;
}
</style> </style>

View File

@ -0,0 +1,41 @@
<script>
import { goto } from "@roxi/routify"
import { Layout, Page, notifications, Button } from "@budibase/bbui"
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
import { onMount } from "svelte"
import { templates } from "stores/portal"
let loaded = false
onMount(async () => {
try {
await templates.load()
if ($templates?.length === 0) {
notifications.error(
"There was a problem loading quick start templates."
)
}
} catch (error) {
notifications.error("Error loading apps and templates")
}
loaded = true
})
</script>
<Page wide>
<Layout noPadding gap="XL">
<span>
<Button
primary
on:click={() => {
$goto("../")
}}
>
Back
</Button>
</span>
{#if loaded && $templates?.length}
<TemplateDisplay templates={$templates} />
{/if}
</Layout>
</Page>