Add redesign for apps pages
This commit is contained in:
parent
d016ee9775
commit
0cdc814bcc
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
export let wide = false
|
||||
export let maxWidth = "80ch"
|
||||
export let maxWidth = "1080px"
|
||||
export let noPadding = false
|
||||
</script>
|
||||
|
||||
|
@ -16,8 +16,8 @@
|
|||
align-items: stretch;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: calc(var(--spacing-xl) * 2);
|
||||
min-height: calc(100% - var(--spacing-xl) * 4);
|
||||
flex: 1 1 auto;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.wide {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
ProgressCircle,
|
||||
Layout,
|
||||
Body,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { auth, apps } from "stores/portal"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
|
@ -58,19 +59,14 @@
|
|||
|
||||
<div class="lock-status">
|
||||
{#if lockedBy}
|
||||
<Button
|
||||
quiet
|
||||
secondary
|
||||
icon="LockClosed"
|
||||
<Icon
|
||||
name="LockClosed"
|
||||
hoverable
|
||||
size={buttonSize}
|
||||
on:click={() => {
|
||||
appLockModal.show()
|
||||
}}
|
||||
>
|
||||
<span class="lock-status-text">
|
||||
{lockedByHeading}
|
||||
</span>
|
||||
</Button>
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,128 +1,110 @@
|
|||
<script>
|
||||
import {
|
||||
Layout,
|
||||
Detail,
|
||||
Heading,
|
||||
Button,
|
||||
Modal,
|
||||
ActionGroup,
|
||||
ActionButton,
|
||||
} from "@budibase/bbui"
|
||||
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 selectedTemplateCategory
|
||||
let selectedCategory
|
||||
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
|
||||
$: categories = getCategories(templates)
|
||||
$: filteredCategories = getFilteredCategories(categories, selectedCategory)
|
||||
|
||||
const getCategories = templates => {
|
||||
let categories = {}
|
||||
templates?.forEach(template => {
|
||||
if (!categories[template.category]) {
|
||||
categories[template.category] = []
|
||||
}
|
||||
|
||||
acc[template.category] = !acc[template.category]
|
||||
? []
|
||||
: acc[template.category]
|
||||
acc[template.category].push(template)
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
return grouped
|
||||
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
|
||||
}
|
||||
|
||||
$: filteredTemplates = groupTemplatesByCategory(
|
||||
templates,
|
||||
selectedTemplateCategory
|
||||
)
|
||||
|
||||
$: filteredTemplateCategories = filteredTemplates
|
||||
? Object.keys(filteredTemplates).sort()
|
||||
: []
|
||||
|
||||
$: templateCategories = templates
|
||||
? Object.keys(groupTemplatesByCategory(templates)).sort()
|
||||
: []
|
||||
const getFilteredCategories = (categories, selectedCategory) => {
|
||||
if (!selectedCategory) {
|
||||
return categories
|
||||
}
|
||||
return categories.filter(x => x.name === selectedCategory)
|
||||
}
|
||||
|
||||
const stopAppCreation = () => {
|
||||
template = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="template-header">
|
||||
<Layout noPadding gap="S">
|
||||
<Heading size="S">Templates</Heading>
|
||||
<div class="template-category-filters spectrum-ActionGroup">
|
||||
<ActionGroup>
|
||||
<ActionButton
|
||||
selected={!selectedTemplateCategory}
|
||||
on:click={() => {
|
||||
selectedTemplateCategory = null
|
||||
}}
|
||||
>
|
||||
All
|
||||
</ActionButton>
|
||||
{#each templateCategories as templateCategoryKey}
|
||||
<ActionButton
|
||||
dataCy={templateCategoryKey}
|
||||
selected={templateCategoryKey == selectedTemplateCategory}
|
||||
on:click={() => {
|
||||
selectedTemplateCategory = templateCategoryKey
|
||||
}}
|
||||
>
|
||||
{templateCategoryKey}
|
||||
</ActionButton>
|
||||
{/each}
|
||||
</ActionGroup>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<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
|
||||
<Content>
|
||||
<div slot="side-nav">
|
||||
<SideNav title="Templates">
|
||||
<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" data-cy={category.name}>
|
||||
<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}
|
||||
>
|
||||
Details
|
||||
</a>
|
||||
</TemplateCard>
|
||||
{/each}
|
||||
{#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>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
</div>
|
||||
</Content>
|
||||
|
||||
<Modal
|
||||
bind:this={creationModal}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Heading, Button, Icon } from "@budibase/bbui"
|
||||
import { Heading, Body, Button, Icon } from "@budibase/bbui"
|
||||
import AppLockModal from "../common/AppLockModal.svelte"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
|
||||
|
@ -8,78 +8,106 @@
|
|||
export let appOverview
|
||||
</script>
|
||||
|
||||
<div class="title" data-cy={`${app.devId}`}>
|
||||
<div>
|
||||
<div class="app-icon" style="color: {app.icon?.color || ''}">
|
||||
<Icon size="XL" name={app.icon?.name || "Apps"} />
|
||||
<div class="app-row">
|
||||
<div class="header">
|
||||
<div class="title" data-cy={`${app.devId}`}>
|
||||
<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 class="name" data-cy="app-name-link" on:click={() => editApp(app)}>
|
||||
<Heading size="XS">
|
||||
{app.name}
|
||||
</Heading>
|
||||
|
||||
<div class="updated">
|
||||
{#if app.updatedAt}
|
||||
{processStringSync("Updated {{ duration time 'millisecond' }} ago", {
|
||||
time: new Date().getTime() - new Date(app.updatedAt).getTime(),
|
||||
})}
|
||||
{:else}
|
||||
Never updated
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desktop">
|
||||
{#if app.updatedAt}
|
||||
{processStringSync("Updated {{ duration time 'millisecond' }} ago", {
|
||||
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 class="title app-status" class:deployed={app.deployed}>
|
||||
<Icon size="L" name={app.deployed ? "GlobeCheck" : "GlobeStrike"} />
|
||||
<Body size="S">{`${window.origin}/app${app.url}`}</Body>
|
||||
</div>
|
||||
</div>
|
||||
<div data-cy={`row_actions_${app.appId}`}>
|
||||
<div class="app-row-actions">
|
||||
<Button size="S" secondary newStyles on:click={() => appOverview(app)}>
|
||||
Manage
|
||||
</Button>
|
||||
<Button
|
||||
size="S"
|
||||
primary
|
||||
newStyles
|
||||
disabled={app.lockedOther}
|
||||
on:click={() => editApp(app)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<div data-cy={`row_actions_${app.appId}`}>
|
||||
<div class="app-row-actions">
|
||||
<Button
|
||||
size="S"
|
||||
primary
|
||||
newStyles
|
||||
disabled={app.lockedOther}
|
||||
on:click={() => editApp(app)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button size="S" secondary newStyles on:click={() => appOverview(app)}>
|
||||
Manage
|
||||
</Button>
|
||||
<AppLockModal {app} buttonSize="M" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.title,
|
||||
div.title > div {
|
||||
.app-row {
|
||||
background: var(--background);
|
||||
padding: 24px 32px;
|
||||
border-radius: 8px;
|
||||
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;
|
||||
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 {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-s);
|
||||
grid-template-columns: 24px 100px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
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 {
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
|
@ -88,7 +116,6 @@
|
|||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: calc(1.5 * var(--spacing-xl));
|
||||
}
|
||||
.title :global(h1:hover) {
|
||||
color: var(--spectrum-global-color-blue-600);
|
||||
|
|
|
@ -246,9 +246,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<Modal bind:this={userInfoModal}>
|
||||
|
@ -333,10 +331,7 @@
|
|||
justify-content: center;
|
||||
align-items: stretch;
|
||||
overflow: auto;
|
||||
padding: 50px;
|
||||
}
|
||||
.content {
|
||||
max-width: 1080px;
|
||||
padding: 50px 50px 0 50px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
|
|
|
@ -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}
|
|
@ -1,49 +1,17 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import {
|
||||
Layout,
|
||||
Page,
|
||||
notifications,
|
||||
Button,
|
||||
Heading,
|
||||
Body,
|
||||
Modal,
|
||||
Divider,
|
||||
ActionButton,
|
||||
} from "@budibase/bbui"
|
||||
import { url } from "@roxi/routify"
|
||||
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 { onMount } from "svelte"
|
||||
import { templates, licensing } from "stores/portal"
|
||||
import { apps, templates, licensing } from "stores/portal"
|
||||
import { Breadcrumbs, Breadcrumb, Header } from "components/portal/page"
|
||||
|
||||
let loaded = $templates?.length
|
||||
let template
|
||||
let creationModal = false
|
||||
let appLimitModal
|
||||
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 = () => {
|
||||
if ($licensing?.usageMetrics?.apps >= 100) {
|
||||
appLimitModal.show()
|
||||
|
@ -70,58 +38,34 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Page wide>
|
||||
<Layout noPadding gap="XL">
|
||||
<span>
|
||||
<ActionButton
|
||||
secondary
|
||||
icon={"ArrowLeft"}
|
||||
on:click={() => {
|
||||
$goto("../")
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</ActionButton>
|
||||
</span>
|
||||
|
||||
<div class="title">
|
||||
<div class="welcome">
|
||||
<Layout noPadding gap="XS">
|
||||
<Heading size="L">{createAppTitle}</Heading>
|
||||
<Body size="M">
|
||||
{welcomeBody}
|
||||
</Body>
|
||||
</Layout>
|
||||
|
||||
<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="M"
|
||||
quiet
|
||||
secondary
|
||||
on:click={initiateAppImport}
|
||||
>
|
||||
Import app
|
||||
</Button>
|
||||
</div>
|
||||
<Page>
|
||||
<Layout noPadding gap="L">
|
||||
<Breadcrumbs>
|
||||
<Breadcrumb url={$url("./")} text="Apps" />
|
||||
<Breadcrumb text="Create new app" />
|
||||
</Breadcrumbs>
|
||||
<Header title={$apps.length ? "Create new app" : "Create your first app"}>
|
||||
<div slot="buttons">
|
||||
<Button
|
||||
dataCy="import-app-btn"
|
||||
size="M"
|
||||
newStyles
|
||||
secondary
|
||||
on:click={initiateAppImport}
|
||||
>
|
||||
Import app
|
||||
</Button>
|
||||
<Button
|
||||
dataCy="create-app-btn"
|
||||
size="M"
|
||||
cta
|
||||
on:click={initiateAppCreation}
|
||||
>
|
||||
Start from scratch
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{#if loaded && $templates?.length}
|
||||
<TemplateDisplay templates={$templates} />
|
||||
{/if}
|
||||
</Header>
|
||||
<TemplateDisplay templates={$templates} />
|
||||
</Layout>
|
||||
</Page>
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
notifications,
|
||||
Notification,
|
||||
Body,
|
||||
Icon,
|
||||
Search,
|
||||
} from "@budibase/bbui"
|
||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||
|
@ -20,12 +20,11 @@
|
|||
import { store, automationStore } from "builderStore"
|
||||
import { API } from "api"
|
||||
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 AppRow from "components/start/AppRow.svelte"
|
||||
import { AppStatus } from "constants"
|
||||
import Logo from "assets/bb-space-man.svg"
|
||||
import AccessFilter from "./_components/AcessFilter.svelte"
|
||||
|
||||
let sortBy = "name"
|
||||
let template
|
||||
|
@ -34,28 +33,13 @@
|
|||
let updatingModal
|
||||
let appLimitModal
|
||||
let creatingApp = false
|
||||
let loaded = $apps?.length || $templates?.length
|
||||
let searchTerm = ""
|
||||
let cloud = $admin.cloud
|
||||
let creatingFromTemplate = false
|
||||
let automationErrors
|
||||
let accessFilterList = null
|
||||
|
||||
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"
|
||||
|
||||
$: welcomeHeader = `Welcome ${auth?.user?.firstName || "back"}`
|
||||
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
|
||||
$: filteredApps = enrichedApps.filter(
|
||||
app =>
|
||||
|
@ -207,10 +191,6 @@
|
|||
$goto(`../../app/${app.devId}`)
|
||||
}
|
||||
|
||||
const accessFilterAction = accessFilter => {
|
||||
accessFilterList = accessFilter.detail
|
||||
}
|
||||
|
||||
function createAppFromTemplateUrl(templateKey) {
|
||||
// validate the template key just to make sure
|
||||
const templateParts = templateKey.split("/")
|
||||
|
@ -226,33 +206,21 @@
|
|||
|
||||
onMount(async () => {
|
||||
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
|
||||
const initInfo = await auth.getInitInfo()
|
||||
if (initInfo?.init_template) {
|
||||
creatingFromTemplate = true
|
||||
createAppFromTemplateUrl(initInfo.init_template)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error loading apps and templates")
|
||||
notifications.error("Error getting init info")
|
||||
}
|
||||
loaded = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<Page wide>
|
||||
<Layout noPadding gap="M">
|
||||
{#if loaded}
|
||||
{#if $apps.length}
|
||||
<Page>
|
||||
<Layout noPadding gap="L">
|
||||
{#each Object.keys(automationErrors || {}) as appId}
|
||||
<Notification
|
||||
wide
|
||||
|
@ -272,42 +240,15 @@
|
|||
{/each}
|
||||
<div class="title">
|
||||
<div class="welcome">
|
||||
<Layout noPadding gap="XS">
|
||||
<Layout noPadding gap="S">
|
||||
<Heading size="L">{welcomeHeader}</Heading>
|
||||
<Body size="M">
|
||||
{welcomeBody}
|
||||
Manage your apps and get a head start with templates
|
||||
</Body>
|
||||
</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>
|
||||
|
||||
{#if !$apps?.length && $templates?.length}
|
||||
<TemplateDisplay templates={$templates} />
|
||||
{/if}
|
||||
|
||||
{#if enrichedApps.length}
|
||||
<Layout noPadding gap="L">
|
||||
<div class="title">
|
||||
|
@ -315,27 +256,24 @@
|
|||
<Button
|
||||
dataCy="create-app-btn"
|
||||
size="M"
|
||||
icon="Add"
|
||||
cta
|
||||
on:click={initiateAppCreation}
|
||||
>
|
||||
{createAppButtonText}
|
||||
Create new app
|
||||
</Button>
|
||||
{#if $apps?.length > 0}
|
||||
<Button
|
||||
icon="Experience"
|
||||
size="M"
|
||||
quiet
|
||||
newStyles
|
||||
secondary
|
||||
on:click={$goto("/builder/portal/apps/templates")}
|
||||
>
|
||||
Templates
|
||||
View templates
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !$apps?.length}
|
||||
<Button
|
||||
dataCy="import-app-btn"
|
||||
icon="Import"
|
||||
size="L"
|
||||
quiet
|
||||
secondary
|
||||
|
@ -348,33 +286,23 @@
|
|||
{#if enrichedApps.length > 1}
|
||||
<div class="app-actions">
|
||||
{#if cloud}
|
||||
<Button
|
||||
size="M"
|
||||
icon="Export"
|
||||
quiet
|
||||
secondary
|
||||
<Icon
|
||||
name="Download"
|
||||
hoverable
|
||||
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} />
|
||||
</div>
|
||||
{/if}
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -386,17 +314,17 @@
|
|||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if creatingFromTemplate}
|
||||
<div class="empty-wrapper">
|
||||
<img class="img-logo img-size" alt="logo" src={Logo} />
|
||||
<p>Creating your Budibase app from your selected template...</p>
|
||||
<Spinner size="10" />
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
</Page>
|
||||
{#if creatingFromTemplate}
|
||||
<div class="empty-wrapper">
|
||||
<img class="img-logo img-size" alt="logo" src={Logo} />
|
||||
<p>Creating your Budibase app from your selected template...</p>
|
||||
<Spinner size="10" />
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
</Page>
|
||||
{/if}
|
||||
|
||||
<Modal
|
||||
bind:this={creationModal}
|
||||
|
@ -414,18 +342,6 @@
|
|||
<AppLimitModal bind:this={appLimitModal} />
|
||||
|
||||
<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 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -447,34 +363,20 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
.filter {
|
||||
.app-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.appTable {
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
|
|
|
@ -1,42 +1,18 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { Layout, Page, notifications, ActionButton } from "@budibase/bbui"
|
||||
import { url } from "@roxi/routify"
|
||||
import { Layout, Page } from "@budibase/bbui"
|
||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||
import { onMount } from "svelte"
|
||||
import { templates } from "stores/portal"
|
||||
|
||||
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
|
||||
})
|
||||
import { Breadcrumbs, Breadcrumb, Header } from "components/portal/page"
|
||||
</script>
|
||||
|
||||
<Page wide>
|
||||
<Layout noPadding gap="XL">
|
||||
<span>
|
||||
<ActionButton
|
||||
secondary
|
||||
icon={"ArrowLeft"}
|
||||
on:click={() => {
|
||||
$goto("../")
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</ActionButton>
|
||||
</span>
|
||||
{#if loaded && $templates?.length}
|
||||
<TemplateDisplay templates={$templates} />
|
||||
{/if}
|
||||
<Page>
|
||||
<Layout noPadding gap="L">
|
||||
<Breadcrumbs>
|
||||
<Breadcrumb url={$url("./")} text="Apps" />
|
||||
<Breadcrumb text="Templates" />
|
||||
</Breadcrumbs>
|
||||
<Header title="Templates" />
|
||||
<TemplateDisplay templates={$templates} />
|
||||
</Layout>
|
||||
</Page>
|
||||
|
|
Loading…
Reference in New Issue