Add redesign for apps pages

This commit is contained in:
Andrew Kingston 2022-10-27 19:20:55 +01:00
parent d016ee9775
commit 0cdc814bcc
16 changed files with 466 additions and 445 deletions

View File

@ -1,6 +1,6 @@
<script> <script>
export let wide = false export let wide = false
export let maxWidth = "80ch" export let maxWidth = "1080px"
export let noPadding = false export let noPadding = false
</script> </script>
@ -16,8 +16,8 @@
align-items: stretch; align-items: stretch;
max-width: var(--max-width); max-width: var(--max-width);
margin: 0 auto; margin: 0 auto;
padding: calc(var(--spacing-xl) * 2); flex: 1 1 auto;
min-height: calc(100% - var(--spacing-xl) * 4); padding-bottom: 50px;
} }
.wide { .wide {

View File

@ -8,6 +8,7 @@
ProgressCircle, ProgressCircle,
Layout, Layout,
Body, Body,
Icon,
} from "@budibase/bbui" } from "@budibase/bbui"
import { auth, apps } from "stores/portal" import { auth, apps } from "stores/portal"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
@ -58,19 +59,14 @@
<div class="lock-status"> <div class="lock-status">
{#if lockedBy} {#if lockedBy}
<Button <Icon
quiet name="LockClosed"
secondary hoverable
icon="LockClosed"
size={buttonSize} size={buttonSize}
on:click={() => { on:click={() => {
appLockModal.show() appLockModal.show()
}} }}
> />
<span class="lock-status-text">
{lockedByHeading}
</span>
</Button>
{/if} {/if}
</div> </div>

View File

@ -1,128 +1,110 @@
<script> <script>
import { import { Layout, Detail, Button, Modal } from "@budibase/bbui"
Layout,
Detail,
Heading,
Button,
Modal,
ActionGroup,
ActionButton,
} from "@budibase/bbui"
import TemplateCard from "components/common/TemplateCard.svelte" import TemplateCard from "components/common/TemplateCard.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
import { licensing } from "stores/portal" import { licensing } from "stores/portal"
import { Content, SideNav, SideNavItem } from "components/portal/page"
export let templates export let templates
let selectedTemplateCategory let selectedCategory
let creationModal let creationModal
let template let template
const groupTemplatesByCategory = (templates, categoryFilter) => { $: categories = getCategories(templates)
let grouped = templates.reduce((acc, template) => { $: filteredCategories = getFilteredCategories(categories, selectedCategory)
if (
typeof categoryFilter === "string" && const getCategories = templates => {
[categoryFilter].indexOf(template.category) < 0 let categories = {}
) { templates?.forEach(template => {
return acc if (!categories[template.category]) {
categories[template.category] = []
} }
categories[template.category].push(template)
acc[template.category] = !acc[template.category] })
? [] categories = Object.entries(categories).map(
: acc[template.category] ([category, categoryTemplates]) => {
acc[template.category].push(template) return {
name: category,
return acc templates: categoryTemplates,
}, {}) }
return grouped }
)
categories.sort((a, b) => {
return a.name < b.name ? -1 : 1
})
return categories
} }
$: filteredTemplates = groupTemplatesByCategory( const getFilteredCategories = (categories, selectedCategory) => {
templates, if (!selectedCategory) {
selectedTemplateCategory return categories
) }
return categories.filter(x => x.name === selectedCategory)
$: filteredTemplateCategories = filteredTemplates }
? Object.keys(filteredTemplates).sort()
: []
$: templateCategories = templates
? Object.keys(groupTemplatesByCategory(templates)).sort()
: []
const stopAppCreation = () => { const stopAppCreation = () => {
template = null template = null
} }
</script> </script>
<div class="template-header"> <Content>
<Layout noPadding gap="S"> <div slot="side-nav">
<Heading size="S">Templates</Heading> <SideNav title="Templates">
<div class="template-category-filters spectrum-ActionGroup"> <SideNavItem
<ActionGroup> on:click={() => (selectedCategory = null)}
<ActionButton text="All"
selected={!selectedTemplateCategory} active={selectedCategory == null}
on:click={() => { />
selectedTemplateCategory = null {#each categories as category}
}} <SideNavItem
> on:click={() => (selectedCategory = category.name)}
All text={category.name}
</ActionButton> active={selectedCategory === category.name}
{#each templateCategories as templateCategoryKey} />
<ActionButton {/each}
dataCy={templateCategoryKey} </SideNav>
selected={templateCategoryKey == selectedTemplateCategory} </div>
on:click={() => { <div class="template-categories">
selectedTemplateCategory = templateCategoryKey <Layout gap="XL" noPadding>
}} {#each filteredCategories as category}
> <div class="template-category" data-cy={category.name}>
{templateCategoryKey} <Detail size="M">{category.name}</Detail>
</ActionButton> <div class="template-grid">
{/each} {#each category.templates as templateEntry}
</ActionGroup> <TemplateCard
</div> name={templateEntry.name}
</Layout> imageSrc={templateEntry.image}
</div> backgroundColour={templateEntry.background}
icon={templateEntry.icon}
<div class="template-categories">
<Layout gap="XL" noPadding>
{#each filteredTemplateCategories as templateCategoryKey}
<div class="template-category" data-cy={templateCategoryKey}>
<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}
>
{#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 {#if !($licensing?.usageMetrics?.apps >= 100)}
</a> <Button
</TemplateCard> cta
{/each} 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> </div>
</div> {/each}
{/each} </Layout>
</Layout> </div>
</div> </Content>
<Modal <Modal
bind:this={creationModal} bind:this={creationModal}

View File

@ -0,0 +1,34 @@
<script>
import { Icon } from "@budibase/bbui"
export let url
export let text
</script>
<div>
<a href={url}>
{text}
</a>
<Icon name="ChevronRight" />
</div>
<style>
div {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
color: var(--spectrum-global-color-gray-700);
gap: var(--spacing-m);
font-size: 16px;
font-weight: 600;
}
div :global(.spectrum-Icon),
a {
color: inherit;
transition: color 130ms ease-out;
}
a:hover {
color: var(--spectrum-global-color-gray-900);
}
</style>

View File

@ -0,0 +1,20 @@
<div>
<slot />
</div>
<style>
div {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-m);
}
div :global(> *:last-child .spectrum-Icon) {
display: none;
}
div :global(> *:last-child) {
color: var(--spectrum-global-color-gray-900);
}
</style>

View File

@ -0,0 +1,27 @@
<script>
</script>
<div class="content">
<div class="side-nav">
<slot name="side-nav" />
</div>
<div class="main">
<slot />
</div>
</div>
<style>
.content {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
gap: 40px;
}
.side-nav {
flex: 0 0 200px;
}
.main {
flex: 1 1 auto;
}
</style>

View File

@ -0,0 +1,31 @@
<script>
import { Heading } from "@budibase/bbui"
export let title
</script>
<div class="header">
<Heading size="L">{title}</Heading>
<div class="buttons">
<slot name="buttons" />
</div>
</div>
<style>
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xl);
}
.buttons :global(> div) {
display: contents;
}
</style>

View File

@ -0,0 +1,25 @@
<script>
export let title
</script>
<div class="side-nav">
{#if title}
<div class="title">{title}</div>
{/if}
<slot />
</div>
<style>
.side-nav {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.title {
margin-left: var(--spacing-m);
font-size: 12px;
color: var(--spectrum-global-color-gray-700);
margin-bottom: var(--spacing-m);
}
</style>

View File

@ -0,0 +1,23 @@
<script>
export let text
export let url
export let active = false
</script>
<a on:click {url} class:active>
{text}
</a>
<style>
a {
padding: var(--spacing-s) var(--spacing-m);
color: var(--spectrum-global-color-gray-900);
border-radius: 4px;
transition: background 130ms ease-out;
}
.active,
a:hover {
background-color: var(--spectrum-global-color-gray-200);
cursor: pointer;
}
</style>

View File

@ -0,0 +1,6 @@
export { default as Breadcrumb } from "./Breadcrumb.svelte"
export { default as Breadcrumbs } from "./Breadcrumbs.svelte"
export { default as Header } from "./Header.svelte"
export { default as Content } from "./Content.svelte"
export { default as SideNavItem } from "./SideNavItem.svelte"
export { default as SideNav } from "./SideNav.svelte"

View File

@ -1,5 +1,5 @@
<script> <script>
import { Heading, Button, Icon } from "@budibase/bbui" import { Heading, Body, Button, Icon } from "@budibase/bbui"
import AppLockModal from "../common/AppLockModal.svelte" import AppLockModal from "../common/AppLockModal.svelte"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
@ -8,78 +8,106 @@
export let appOverview export let appOverview
</script> </script>
<div class="title" data-cy={`${app.devId}`}> <div class="app-row">
<div> <div class="header">
<div class="app-icon" style="color: {app.icon?.color || ''}"> <div class="title" data-cy={`${app.devId}`}>
<Icon size="XL" name={app.icon?.name || "Apps"} /> <div class="app-icon" style="color: {app.icon?.color || ''}">
<Icon size="L" name={app.icon?.name || "Apps"} />
</div>
<div class="name" data-cy="app-name-link" on:click={() => editApp(app)}>
<Heading size="S">
{app.name}
</Heading>
</div>
</div> </div>
<div class="name" data-cy="app-name-link" on:click={() => editApp(app)}>
<Heading size="XS"> <div class="updated">
{app.name} {#if app.updatedAt}
</Heading> {processStringSync("Updated {{ duration time 'millisecond' }} ago", {
time: new Date().getTime() - new Date(app.updatedAt).getTime(),
})}
{:else}
Never updated
{/if}
</div> </div>
</div> </div>
</div>
<div class="desktop"> <div class="title app-status" class:deployed={app.deployed}>
{#if app.updatedAt} <Icon size="L" name={app.deployed ? "GlobeCheck" : "GlobeStrike"} />
{processStringSync("Updated {{ duration time 'millisecond' }} ago", { <Body size="S">{`${window.origin}/app${app.url}`}</Body>
time: new Date().getTime() - new Date(app.updatedAt).getTime(),
})}
{:else}
Never updated
{/if}
</div>
<div class="desktop">
<span><AppLockModal {app} buttonSize="M" /></span>
</div>
<div class="desktop">
<div class="app-status">
{#if app.deployed}
<Icon name="Globe" disabled={false} />
Published
{:else}
<Icon name="GlobeStrike" disabled={true} />
<span class="disabled"> Unpublished </span>
{/if}
</div> </div>
</div>
<div data-cy={`row_actions_${app.appId}`}> <div data-cy={`row_actions_${app.appId}`}>
<div class="app-row-actions"> <div class="app-row-actions">
<Button size="S" secondary newStyles on:click={() => appOverview(app)}> <Button
Manage size="S"
</Button> primary
<Button newStyles
size="S" disabled={app.lockedOther}
primary on:click={() => editApp(app)}
newStyles >
disabled={app.lockedOther} Edit
on:click={() => editApp(app)} </Button>
> <Button size="S" secondary newStyles on:click={() => appOverview(app)}>
Edit Manage
</Button> </Button>
<AppLockModal {app} buttonSize="M" />
</div>
</div> </div>
</div> </div>
<style> <style>
div.title, .app-row {
div.title > div { background: var(--background);
padding: 24px 32px;
border-radius: 8px;
display: flex; display: flex;
max-width: 100%; flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-m);
} }
.app-row-actions {
grid-gap: var(--spacing-s); .header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: space-between;
align-items: center;
} }
.updated {
color: var(--spectrum-global-color-gray-700);
}
.title,
.app-status { .app-status {
display: grid; display: flex;
grid-gap: var(--spacing-s); flex-direction: row;
grid-template-columns: 24px 100px; justify-content: flex-start;
align-items: center;
gap: 10px;
} }
.app-status span.disabled {
opacity: 0.3; .title :global(.spectrum-Heading),
.title :global(.spectrum-Icon),
.title :global(.spectrum-Body) {
color: var(--spectrum-global-color-gray-900);
} }
.app-status:not(.deployed) :global(.spectrum-Icon),
.app-status:not(.deployed) :global(.spectrum-Body) {
color: var(--spectrum-global-color-gray-700);
}
.app-row-actions {
gap: var(--spacing-m);
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
margin-top: var(--spacing-m);
}
.name { .name {
text-decoration: none; text-decoration: none;
overflow: hidden; overflow: hidden;
@ -88,7 +116,6 @@
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-left: calc(1.5 * var(--spacing-xl));
} }
.title :global(h1:hover) { .title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600); color: var(--spectrum-global-color-blue-600);

View File

@ -246,9 +246,7 @@
</div> </div>
</div> </div>
<div class="main"> <div class="main">
<div class="content"> <slot />
<slot />
</div>
</div> </div>
</div> </div>
<Modal bind:this={userInfoModal}> <Modal bind:this={userInfoModal}>
@ -333,10 +331,7 @@
justify-content: center; justify-content: center;
align-items: stretch; align-items: stretch;
overflow: auto; overflow: auto;
padding: 50px; padding: 50px 50px 0 50px;
}
.content {
max-width: 1080px;
} }
@media (max-width: 640px) { @media (max-width: 640px) {

View File

@ -0,0 +1,33 @@
<script>
import { notifications } from "@budibase/bbui"
import { apps, templates, licensing } from "stores/portal"
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
let loaded = false
onMount(async () => {
try {
// Always load latest
await apps.load()
await licensing.init()
await templates.load()
if ($templates?.length === 0) {
notifications.error("There was a problem loading quick start templates")
}
// Go to new app page if no apps exists
if (!$apps.length) {
$goto("./create")
}
} catch (error) {
notifications.error("Error loading apps and templates")
}
loaded = true
})
</script>
{#if loaded}
<slot />
{/if}

View File

@ -1,49 +1,17 @@
<script> <script>
import { goto } from "@roxi/routify" import { url } from "@roxi/routify"
import { import { Layout, Page, Button, Modal } from "@budibase/bbui"
Layout,
Page,
notifications,
Button,
Heading,
Body,
Modal,
Divider,
ActionButton,
} from "@budibase/bbui"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
import TemplateDisplay from "components/common/TemplateDisplay.svelte" import TemplateDisplay from "components/common/TemplateDisplay.svelte"
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte" import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
import { onMount } from "svelte" import { apps, templates, licensing } from "stores/portal"
import { templates, licensing } from "stores/portal" import { Breadcrumbs, Breadcrumb, Header } from "components/portal/page"
let loaded = $templates?.length
let template let template
let creationModal = false let creationModal = false
let appLimitModal let appLimitModal
let creatingApp = 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()
// always load latest
await licensing.init()
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 = () => { const initiateAppCreation = () => {
if ($licensing?.usageMetrics?.apps >= 100) { if ($licensing?.usageMetrics?.apps >= 100) {
appLimitModal.show() appLimitModal.show()
@ -70,58 +38,34 @@
} }
</script> </script>
<Page wide> <Page>
<Layout noPadding gap="XL"> <Layout noPadding gap="L">
<span> <Breadcrumbs>
<ActionButton <Breadcrumb url={$url("./")} text="Apps" />
secondary <Breadcrumb text="Create new app" />
icon={"ArrowLeft"} </Breadcrumbs>
on:click={() => { <Header title={$apps.length ? "Create new app" : "Create your first app"}>
$goto("../") <div slot="buttons">
}} <Button
> dataCy="import-app-btn"
Back size="M"
</ActionButton> newStyles
</span> secondary
on:click={initiateAppImport}
<div class="title"> >
<div class="welcome"> Import app
<Layout noPadding gap="XS"> </Button>
<Heading size="L">{createAppTitle}</Heading> <Button
<Body size="M"> dataCy="create-app-btn"
{welcomeBody} size="M"
</Body> cta
</Layout> on:click={initiateAppCreation}
>
<div class="buttons"> Start from scratch
<Button </Button>
dataCy="create-app-btn"
size="M"
icon="Add"
cta
on:click={initiateAppCreation}
>
{createAppButtonText}
</Button>
<Button
dataCy="import-app-btn"
icon="Import"
size="M"
quiet
secondary
on:click={initiateAppImport}
>
Import app
</Button>
</div>
</div> </div>
</div> </Header>
<TemplateDisplay templates={$templates} />
<Divider />
{#if loaded && $templates?.length}
<TemplateDisplay templates={$templates} />
{/if}
</Layout> </Layout>
</Page> </Page>

View File

@ -9,9 +9,9 @@
notifications, notifications,
Notification, Notification,
Body, Body,
Icon,
Search, Search,
} 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"
@ -20,12 +20,11 @@
import { store, automationStore } from "builderStore" import { store, automationStore } from "builderStore"
import { API } from "api" import { API } from "api"
import { onMount } from "svelte" import { onMount } from "svelte"
import { apps, auth, admin, templates, licensing } from "stores/portal" import { apps, auth, admin, licensing } from "stores/portal"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import AppRow from "components/start/AppRow.svelte" import AppRow from "components/start/AppRow.svelte"
import { AppStatus } from "constants" import { AppStatus } from "constants"
import Logo from "assets/bb-space-man.svg" import Logo from "assets/bb-space-man.svg"
import AccessFilter from "./_components/AcessFilter.svelte"
let sortBy = "name" let sortBy = "name"
let template let template
@ -34,28 +33,13 @@
let updatingModal let updatingModal
let appLimitModal let appLimitModal
let creatingApp = false let creatingApp = false
let loaded = $apps?.length || $templates?.length
let searchTerm = "" let searchTerm = ""
let cloud = $admin.cloud let cloud = $admin.cloud
let creatingFromTemplate = false let creatingFromTemplate = false
let automationErrors let automationErrors
let accessFilterList = null let accessFilterList = null
const resolveWelcomeMessage = (auth, apps) => { $: welcomeHeader = `Welcome ${auth?.user?.firstName || "back"}`
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( $: filteredApps = enrichedApps.filter(
app => app =>
@ -207,10 +191,6 @@
$goto(`../../app/${app.devId}`) $goto(`../../app/${app.devId}`)
} }
const accessFilterAction = accessFilter => {
accessFilterList = accessFilter.detail
}
function createAppFromTemplateUrl(templateKey) { function createAppFromTemplateUrl(templateKey) {
// validate the template key just to make sure // validate the template key just to make sure
const templateParts = templateKey.split("/") const templateParts = templateKey.split("/")
@ -226,33 +206,21 @@
onMount(async () => { onMount(async () => {
try { try {
await apps.load()
await templates.load()
// always load latest
await licensing.init()
if ($templates?.length === 0) {
notifications.error(
"There was a problem loading quick start templates."
)
}
// If the portal is loaded from an external URL with a template param // If the portal is loaded from an external URL with a template param
const initInfo = await auth.getInitInfo() const initInfo = await auth.getInitInfo()
if (initInfo?.init_template) { if (initInfo?.init_template) {
creatingFromTemplate = true creatingFromTemplate = true
createAppFromTemplateUrl(initInfo.init_template) createAppFromTemplateUrl(initInfo.init_template)
return
} }
} catch (error) { } catch (error) {
notifications.error("Error loading apps and templates") notifications.error("Error getting init info")
} }
loaded = true
}) })
</script> </script>
<Page wide> {#if $apps.length}
<Layout noPadding gap="M"> <Page>
{#if loaded} <Layout noPadding gap="L">
{#each Object.keys(automationErrors || {}) as appId} {#each Object.keys(automationErrors || {}) as appId}
<Notification <Notification
wide wide
@ -272,42 +240,15 @@
{/each} {/each}
<div class="title"> <div class="title">
<div class="welcome"> <div class="welcome">
<Layout noPadding gap="XS"> <Layout noPadding gap="S">
<Heading size="L">{welcomeHeader}</Heading> <Heading size="L">{welcomeHeader}</Heading>
<Body size="M"> <Body size="M">
{welcomeBody} Manage your apps and get a head start with templates
</Body> </Body>
</Layout> </Layout>
{#if !$apps?.length}
<div class="buttons">
<Button
dataCy="create-app-btn"
size="M"
icon="Add"
cta
on:click={initiateAppCreation}
>
{createAppButtonText}
</Button>
<Button
dataCy="import-app-btn"
icon="Import"
size="L"
quiet
secondary
on:click={initiateAppImport}
>
Import app
</Button>
</div>
{/if}
</div> </div>
</div> </div>
{#if !$apps?.length && $templates?.length}
<TemplateDisplay templates={$templates} />
{/if}
{#if enrichedApps.length} {#if enrichedApps.length}
<Layout noPadding gap="L"> <Layout noPadding gap="L">
<div class="title"> <div class="title">
@ -315,27 +256,24 @@
<Button <Button
dataCy="create-app-btn" dataCy="create-app-btn"
size="M" size="M"
icon="Add"
cta cta
on:click={initiateAppCreation} on:click={initiateAppCreation}
> >
{createAppButtonText} Create new app
</Button> </Button>
{#if $apps?.length > 0} {#if $apps?.length > 0}
<Button <Button
icon="Experience"
size="M" size="M"
quiet newStyles
secondary secondary
on:click={$goto("/builder/portal/apps/templates")} on:click={$goto("/builder/portal/apps/templates")}
> >
Templates View templates
</Button> </Button>
{/if} {/if}
{#if !$apps?.length} {#if !$apps?.length}
<Button <Button
dataCy="import-app-btn" dataCy="import-app-btn"
icon="Import"
size="L" size="L"
quiet quiet
secondary secondary
@ -348,33 +286,23 @@
{#if enrichedApps.length > 1} {#if enrichedApps.length > 1}
<div class="app-actions"> <div class="app-actions">
{#if cloud} {#if cloud}
<Button <Icon
size="M" name="Download"
icon="Export" hoverable
quiet
secondary
on:click={initiateAppsExport} on:click={initiateAppsExport}
>
Export apps
</Button>
{/if}
<div class="filter">
{#if $licensing.groupsEnabled}
<AccessFilter on:change={accessFilterAction} />
{/if}
<Select
quiet
autoWidth
bind:value={sortBy}
placeholder={null}
options={[
{ label: "Sort by name", value: "name" },
{ label: "Sort by recently updated", value: "updated" },
{ label: "Sort by status", value: "status" },
]}
/> />
<Search placeholder="Search" bind:value={searchTerm} /> {/if}
</div> <Select
autoWidth
bind:value={sortBy}
placeholder={null}
options={[
{ label: "Sort by name", value: "name" },
{ label: "Sort by recently updated", value: "updated" },
{ label: "Sort by status", value: "status" },
]}
/>
<Search placeholder="Search" bind:value={searchTerm} />
</div> </div>
{/if} {/if}
</div> </div>
@ -386,17 +314,17 @@
</div> </div>
</Layout> </Layout>
{/if} {/if}
{/if}
{#if creatingFromTemplate} {#if creatingFromTemplate}
<div class="empty-wrapper"> <div class="empty-wrapper">
<img class="img-logo img-size" alt="logo" src={Logo} /> <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>
{/if} {/if}
</Layout> </Layout>
</Page> </Page>
{/if}
<Modal <Modal
bind:this={creationModal} bind:this={creationModal}
@ -414,18 +342,6 @@
<AppLimitModal bind:this={appLimitModal} /> <AppLimitModal bind:this={appLimitModal} />
<style> <style>
.appTable {
border-top: var(--border-light);
}
.app-actions {
display: flex;
}
.app-actions :global(> button) {
margin-right: 10px;
}
.title .welcome > .buttons {
padding-top: var(--spacing-l);
}
.title { .title {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -447,34 +363,20 @@
display: none; display: none;
} }
} }
.filter { .app-actions {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
gap: var(--spacing-xl); gap: var(--spacing-xl);
} }
.appTable { .appTable {
display: grid; display: flex;
grid-template-rows: auto; flex-direction: column;
grid-template-columns: 1fr 1fr 1fr 1fr auto; justify-content: flex-start;
align-items: center; align-items: stretch;
} gap: 24px;
.appTable.unlocked {
grid-template-columns: 1fr 1fr auto 1fr auto;
}
.appTable :global(> div) {
height: 70px;
display: grid;
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.appTable :global(> div) {
border-bottom: var(--border-light);
} }
@media (max-width: 640px) { @media (max-width: 640px) {

View File

@ -1,42 +1,18 @@
<script> <script>
import { goto } from "@roxi/routify" import { url } from "@roxi/routify"
import { Layout, Page, notifications, ActionButton } from "@budibase/bbui" import { Layout, Page } from "@budibase/bbui"
import TemplateDisplay from "components/common/TemplateDisplay.svelte" import TemplateDisplay from "components/common/TemplateDisplay.svelte"
import { onMount } from "svelte"
import { templates } from "stores/portal" import { templates } from "stores/portal"
import { Breadcrumbs, Breadcrumb, Header } from "components/portal/page"
let loaded = $templates?.length
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> </script>
<Page wide> <Page>
<Layout noPadding gap="XL"> <Layout noPadding gap="L">
<span> <Breadcrumbs>
<ActionButton <Breadcrumb url={$url("./")} text="Apps" />
secondary <Breadcrumb text="Templates" />
icon={"ArrowLeft"} </Breadcrumbs>
on:click={() => { <Header title="Templates" />
$goto("../") <TemplateDisplay templates={$templates} />
}}
>
Back
</ActionButton>
</span>
{#if loaded && $templates?.length}
<TemplateDisplay templates={$templates} />
{/if}
</Layout> </Layout>
</Page> </Page>