Initial commit of home screen modifications and template browsing
This commit is contained in:
parent
d2e7b28304
commit
6c269bf091
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue