This commit is contained in:
Andrew Thompson 2025-05-16 14:02:56 +02:00 committed by GitHub
commit e2326440a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 210 additions and 472 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -3,6 +3,7 @@
export let imageSrc
export let name
export let icon
export let description = ""
export let overlayEnabled = true
let imageError = false
@ -36,7 +37,10 @@
</div>
</div>
<div class="template-thumbnail-text">
<div>{name}</div>
<div class="template-name">{name}</div>
{#if description}
<div class="template-description">{description}</div>
{/if}
</div>
</div>
@ -70,19 +74,40 @@
position: absolute;
bottom: 0px;
display: flex;
align-items: center;
height: 30%;
flex-direction: column;
height: 35%;
width: 100%;
background-color: var(--spectrum-global-color-gray-50);
padding-bottom: 1rem;
}
.template-thumbnail-text > div {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.template-name {
color: var(
--spectrum-heading-xs-text-color,
var(--spectrum-alias-heading-text-color)
);
background-color: var(--spectrum-global-color-gray-50);
font-size: 14px;
font-weight: 600;
margin-top: 0.75rem;
}
.template-thumbnail-text > div {
padding-left: 1rem;
padding-right: 1rem;
.template-description {
color: var(--spectrum-global-color-gray-600);
font-size: 12px;
font-weight: 400;
margin-top: 0.5rem;
margin-bottom: 0;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.5;
}
.template-card {
@ -91,7 +116,7 @@
border-radius: var(--border-radius-s);
border: 1px solid var(--spectrum-global-color-gray-300);
overflow: hidden;
min-height: 200px;
min-height: 220px;
}
.template-card > * {
@ -112,7 +137,7 @@
}
.card-body {
padding-left: 1rem;
padding-top: 1rem;
padding-left: 1.25rem;
padding-top: 1.25rem;
}
</style>

View File

@ -1,137 +0,0 @@
<script>
import { Layout, Detail, Button, Modal } from "@budibase/bbui"
import TemplateCard from "@/components/common/TemplateCard.svelte"
import CreateAppModal from "@/components/start/CreateAppModal.svelte"
import { licensing } from "@/stores/portal"
import { Content, SideNav, SideNavItem } from "@/components/portal/page"
export let templates
let selectedCategory
let creationModal
let template
$: categories = getCategories(templates)
$: filteredCategories = getFilteredCategories(categories, selectedCategory)
const getCategories = templates => {
let categories = {}
templates?.forEach(template => {
if (!categories[template.category]) {
categories[template.category] = []
}
categories[template.category].push(template)
})
categories = Object.entries(categories).map(
([category, categoryTemplates]) => {
return {
name: category,
templates: categoryTemplates,
}
}
)
categories.sort((a, b) => {
return a.name < b.name ? -1 : 1
})
return categories
}
const getFilteredCategories = (categories, selectedCategory) => {
if (!selectedCategory) {
return categories
}
return categories.filter(x => x.name === selectedCategory)
}
const stopAppCreation = () => {
template = null
}
</script>
<Content>
<div slot="side-nav">
<SideNav>
<SideNavItem
on:click={() => (selectedCategory = null)}
text="All"
active={selectedCategory == null}
/>
{#each categories as category}
<SideNavItem
on:click={() => (selectedCategory = category.name)}
text={category.name}
active={selectedCategory === category.name}
/>
{/each}
</SideNav>
</div>
<div class="template-categories">
<Layout gap="XL" noPadding>
{#each filteredCategories as category}
<div class="template-category">
<Detail size="M">{category.name}</Detail>
<div class="template-grid">
{#each category.templates as templateEntry}
<TemplateCard
name={templateEntry.name}
imageSrc={templateEntry.image}
backgroundColour={templateEntry.background}
icon={templateEntry.icon}
>
{#if !($licensing?.usageMetrics?.apps >= 100)}
<Button
cta
on:click={() => {
template = templateEntry
creationModal.show()
}}
>
Use template
</Button>
{/if}
<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>
</Content>
<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

@ -134,6 +134,8 @@
data.append("templateKey", template.key)
}
data.append("isOnboarding", "false")
// Create App
const createdApp = await API.createApp(data)

View File

@ -0,0 +1,63 @@
<script lang="ts">
import { ModalContent, Layout } from "@budibase/bbui"
import TemplateCard from "@/components/common/TemplateCard.svelte"
import { templates } from "@/stores/portal"
import type { TemplateMetadata } from "@budibase/types"
export let onSelectTemplate: (_template: TemplateMetadata) => void
let newTemplates: TemplateMetadata[] = []
$: {
const templateList = $templates as TemplateMetadata[]
newTemplates = templateList?.filter(template => template.new) || []
}
</script>
<ModalContent
title="Choose a starting template"
size="XL"
showCancelButton={false}
showConfirmButton={false}
>
<Layout noPadding gap="M">
<div class="template-grid">
{#each newTemplates as template}
<button
class="template-wrapper"
on:click={() => onSelectTemplate(template)}
>
<TemplateCard
name={template.name}
imageSrc={template.image}
backgroundColour={template.background}
icon={template.icon}
description={template.description}
/>
</button>
{/each}
</div>
</Layout>
</ModalContent>
<style>
.template-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-l);
padding: var(--spacing-m);
}
.template-wrapper {
cursor: pointer;
transition: transform 0.2s ease;
background: none;
border: none;
padding: 0;
text-align: left;
font-family: var(--font-sans);
}
.template-wrapper:hover {
transform: translateY(-4px);
}
</style>

View File

@ -1,69 +0,0 @@
<script>
import { url } from "@roxi/routify"
import FirstAppOnboarding from "./onboarding/index.svelte"
import { Layout, Page, Button, Modal } from "@budibase/bbui"
import CreateAppModal from "@/components/start/CreateAppModal.svelte"
import TemplateDisplay from "@/components/common/TemplateDisplay.svelte"
import AppLimitModal from "@/components/portal/licensing/AppLimitModal.svelte"
import { appsStore, templates, licensing } from "@/stores/portal"
import { Breadcrumbs, Breadcrumb, Header } from "@/components/portal/page"
let template
let creationModal = false
let appLimitModal
const initiateAppCreation = () => {
if ($licensing?.usageMetrics?.apps >= 100) {
appLimitModal.show()
} else {
template = null
creationModal.show()
}
}
const stopAppCreation = () => {
template = null
}
const initiateAppImport = () => {
if ($licensing?.usageMetrics?.apps >= 100) {
appLimitModal.show()
} else {
template = { fromFile: true }
creationModal.show()
}
}
</script>
{#if !$appsStore.apps.length}
<FirstAppOnboarding />
{:else}
<Page>
<Layout noPadding gap="L">
<Breadcrumbs>
<Breadcrumb url={$url("./")} text="Apps" />
<Breadcrumb text="Create new app" />
</Breadcrumbs>
<Header title={"Create new app"}>
<div slot="buttons">
<Button size="M" secondary on:click={initiateAppImport}>
Import app
</Button>
<Button size="M" cta on:click={initiateAppCreation}>
Start from scratch
</Button>
</div>
</Header>
<TemplateDisplay templates={$templates} />
</Layout>
</Page>
<Modal
bind:this={creationModal}
padding={false}
width="600px"
on:hide={stopAppCreation}
>
<CreateAppModal {template} />
</Modal>
<AppLimitModal bind:this={appLimitModal} />
{/if}

View File

@ -26,15 +26,18 @@
licensing,
enrichedApps,
sortBy,
templates,
} from "@/stores/portal"
import { goto } from "@roxi/routify"
import AppRow from "@/components/start/AppRow.svelte"
import Logo from "assets/bb-space-man.svg"
import TemplatesModal from "@/components/start/TemplatesModal.svelte"
let template
let creationModal
let appLimitModal
let accountLockedModal
let templatesModal
let searchTerm = ""
let creatingFromTemplate = false
let automationErrors
@ -91,8 +94,6 @@
const initiateAppCreation = async () => {
if ($licensing?.usageMetrics?.apps >= 100) {
appLimitModal.show()
} else if ($appsStore.apps?.length) {
$goto("/builder/portal/apps/create")
} else {
template = null
creationModal.show()
@ -120,6 +121,7 @@
data.append("name", appName)
data.append("useTemplate", true)
data.append("templateKey", template.key)
data.append("isOnboarding", "false")
// Create App
const createdApp = await API.createApp(data)
@ -159,6 +161,12 @@
}
}
const handleTemplateSelect = selectedTemplate => {
template = selectedTemplate
templatesModal.hide()
autoCreateApp()
}
onMount(async () => {
try {
// If the portal is loaded from an external URL with a template param
@ -170,6 +178,7 @@
if (usersLimitLockAction) {
usersLimitLockAction()
}
await templates.load()
} catch (error) {
notifications.error("Error getting init info")
}
@ -224,21 +233,20 @@
>
Create new app
</Button>
{#if $appsStore.apps?.length > 0 && !$admin.offlineMode}
{#if $appsStore.apps?.length > 0}
{#if !$admin.offlineMode}
<Button
size="M"
secondary
on:click={usersLimitLockAction || templatesModal.show}
>
View templates
</Button>
{/if}
<Button
size="M"
secondary
on:click={usersLimitLockAction ||
$goto("/builder/portal/apps/templates")}
>
View templates
</Button>
{/if}
{#if !$appsStore.apps?.length}
<Button
size="L"
quiet
secondary
on:click={usersLimitLockAction || initiateAppImport}
>
Import app
@ -308,6 +316,10 @@
<CreateAppModal {template} />
</Modal>
<Modal bind:this={templatesModal}>
<TemplatesModal onSelectTemplate={handleTemplateSelect} />
</Modal>
<AppLimitModal bind:this={appLimitModal} />
<AccountLockedModal
bind:this={accountLockedModal}

View File

@ -1,122 +1,47 @@
<script>
export let name = ""
<script lang="ts">
import { onMount } from "svelte"
const rows = [
{
firstName: "Julie",
lastName: "Jimenez",
email: "julie.jimenez@example.com",
address: "4250 New Street",
city: "Stevenage",
postcode: "EE32 3SE",
phone: "01754 13523",
},
{
firstName: "Mandy",
lastName: "Clark",
email: "mandy.clark@example.com",
address: "8632 North Street",
city: "Hereford",
postcode: "GT81 7DG",
phone: "016973 32814",
},
{
firstName: "Holly",
lastName: "Carroll",
email: "holly.carroll@example.com",
address: "5976 Springfield Road",
city: "Edinburgh",
postcode: "Y4 2LH",
phone: "016977 73053",
},
{
firstName: "Francis",
lastName: "Castro",
email: "francis.castro@example.com",
address: "3970 High Street",
city: "Wells",
postcode: "X12 6QA",
phone: "017684 23551",
},
]
let container: HTMLDivElement
let panel: HTMLImageElement
let panelWidth: number = 0
let containerWidth: number = 0
let isPanelCutOff: boolean = false
// side panel screenshot is overlaid over grid screenshot
// it should be anchored to the right until the screen is too narrow
function checkPanelVisibility(): void {
if (container && panel) {
containerWidth = container.clientWidth
panelWidth = panel.clientWidth
isPanelCutOff = panelWidth > containerWidth
}
}
onMount(() => {
checkPanelVisibility()
window.addEventListener("resize", checkPanelVisibility)
return () => {
window.removeEventListener("resize", checkPanelVisibility)
}
})
</script>
<div tabindex="-1" class="exampleApp">
<div class="page">
<div class="header">
<img alt="Budibase Logo" src={"/builder/bblogo.png"} />
<h1>{name}</h1>
</div>
<div class="nav">Home</div>
<table>
<thead>
<tr>
<th>FIRST NAME</th>
<th>LAST NAME</th>
<th>EMAIL</th>
<th>ADDRESS</th>
<th>CITY</th>
<th>POSTCODE</th>
<th>PHONE</th>
</tr>
</thead>
<tbody>
{#each rows as row}
<tr>
{#each Object.values(row) as value}
<td>{value}</td>
{/each}
</tr>
{/each}
</tbody>
</table>
<div class="sidePanel">
<h2>{rows[0].firstName}</h2>
<div class="field">
<label for="exampleLastName">lastName</label>
<input
id="exampleLastName"
tabIndex="-1"
readonly
value={rows[0].lastName}
/>
</div>
<div class="field">
<label for="exampleEmail">Email</label>
<input id="exampleEmail" tabIndex="-1" readonly value={rows[0].email} />
</div>
<div class="field">
<label for="exampleAddress">Address</label>
<input
id="exampleAddress"
tabIndex="-1"
readonly
value={rows[0].address}
/>
</div>
<div class="field">
<label for="exampleCity">City</label>
<input id="exampleCity" tabIndex="-1" readonly value={rows[0].city} />
</div>
<div class="field">
<label for="examplePostcode">Postcode</label>
<input
id="examplePostcode"
tabIndex="-1"
readonly
value={rows[0].postcode}
/>
</div>
<div class="field">
<label for="examplePhone">Phone</label>
<input id="examplePhone" tabIndex="-1" readonly value={rows[0].phone} />
</div>
</div>
<div class="mockupContainer" bind:this={container}>
<img
class="baseScreen"
alt="Base app screen"
src={"/builder/onboarding/grid.png"}
/>
<img
class="overlayPanel"
class:leftAnchored={isPanelCutOff}
bind:this={panel}
alt="Side panel overlay"
src={"/builder/onboarding/sidebar.png"}
/>
</div>
</div>
@ -124,119 +49,44 @@
.exampleApp {
box-sizing: border-box;
height: 100vh;
padding: 100px 0 100px 100px;
--text: #191919;
--lightText: #303030;
--extraLightText: #646464;
--backgroundLight: #ffffff;
--background: #f5f5f5;
--tableBorder: 1px solid #e6e6e6;
padding: 100px 0 100px 5vw;
pointer-events: none;
}
.page {
overflow: hidden;
@media (max-width: 980px) {
.exampleApp {
padding-left: 2vw;
}
}
.mockupContainer {
position: relative;
height: 100%;
background-color: var(--background);
color: var(--text);
}
.header {
background-color: var(--backgroundLight);
display: flex;
padding: 32px 0 20px 32px;
align-items: center;
}
.header img {
height: 36px;
margin-right: 20px;
}
.header h1 {
margin: 0;
font-weight: 600;
font-size: 20px;
}
.nav {
background-color: var(--backgroundLight);
padding: 20px 0 20px 32px;
font-weight: 600;
font-size: 16px;
border-bottom: 1px solid #d0d0d0;
}
table {
margin: 32px;
border: var(--tableBorder);
border-collapse: collapse;
min-width: 100%;
}
thead {
border-bottom: var(--tableBorder);
}
thead th {
font-family: "Source Sans Pro";
color: var(--lightText);
white-space: nowrap;
padding: 12px;
font-weight: 600;
font-size: 12px;
text-align: left;
min-width: 70px;
}
tbody td {
border-bottom: 1px solid #e6e6e6;
background-color: var(--backgroundLight);
padding: 12px;
color: var(--extraLightText);
text-align: left;
}
.sidePanel {
position: absolute;
width: 300px;
background-color: var(--backgroundLight);
box-shadow: 0px 4px 25px 0px #00000040;
height: 100%;
top: 0;
right: -364px;
padding: 42px 32px;
right: 0;
}
.sidePanel h2 {
margin: 0;
font-weight: 600;
font-size: 22px;
margin-bottom: 35px;
}
.field {
display: flex;
width: 100%;
align-items: center;
margin-bottom: 20px;
}
.field label {
font-weight: 500;
font-size: 12px;
color: #b0b0b0;
width: 65px;
}
.field input {
border: 1px solid #d0d0d0;
overflow: hidden;
background: #f5f5f5;
border-radius: 4px;
color: var(--lightText);
padding: 7.5px 12px;
font-size: 13px;
flex-grow: 1;
}
.baseScreen {
width: 100%;
height: 100%;
object-fit: cover;
object-position: left;
}
.overlayPanel {
position: absolute;
top: 0;
right: 0;
height: 100%;
width: auto;
object-fit: cover;
box-shadow: -4px 0 25px rgba(0, 0, 0, 0.1);
}
.overlayPanel.leftAnchored {
right: auto;
left: 0;
}
</style>

View File

@ -22,6 +22,7 @@
data.append("name", name.trim())
data.append("url", url.trim())
data.append("useTemplate", false)
data.append("isOnboarding", "true")
const createdApp = await API.createApp(data)
@ -56,7 +57,7 @@
<SplitPage>
<NamePanel bind:name bind:url disabled={loading} onNext={handleCreateApp} />
<div slot="right">
<ExampleApp {name} />
<ExampleApp />
</div>
</SplitPage>
</div>

View File

@ -1,18 +0,0 @@
<script>
import { url } from "@roxi/routify"
import { Layout, Page } from "@budibase/bbui"
import TemplateDisplay from "@/components/common/TemplateDisplay.svelte"
import { templates } from "@/stores/portal"
import { Breadcrumbs, Breadcrumb, Header } from "@/components/portal/page"
</script>
<Page>
<Layout noPadding gap="L">
<Breadcrumbs>
<Breadcrumb url={$url("./")} text="Apps" />
<Breadcrumb text="Templates" />
</Breadcrumbs>
<Header title="Templates" />
<TemplateDisplay templates={$templates} />
</Layout>
</Page>

View File

@ -289,6 +289,13 @@ async function performAppCreate(
const { body } = ctx.request
const { name, url, encryptionPassword, templateKey } = body
let isOnboarding = false
if (typeof body.isOnboarding === "string") {
isOnboarding = body.isOnboarding === "true"
} else if (typeof body.isOnboarding === "boolean") {
isOnboarding = body.isOnboarding
}
let useTemplate = false
if (typeof body.useTemplate === "string") {
useTemplate = body.useTemplate === "true"
@ -322,7 +329,7 @@ async function performAppCreate(
const instance = await createInstance(appId, instanceConfig)
const db = context.getAppDB()
const isImport = !!instanceConfig.file
const addSampleData = !isImport && !useTemplate
const addSampleData = isOnboarding && !isImport && !useTemplate
if (instanceConfig.useTemplate && !instanceConfig.file) {
await updateUserColumns(appId, db, ctx.user._id!)

View File

@ -15,6 +15,7 @@ export interface CreateAppRequest {
fileToImport?: string
encryptionPassword?: string
file?: { path: string }
isOnboarding?: string
}
export interface CreateAppResponse extends App {}

View File

@ -12,6 +12,7 @@ export interface TemplateMetadata {
type: TemplateType
key: string
image: string
new: boolean
}
export type FetchTemplateResponse = TemplateMetadata[]