Add redesign for apps pages
This commit is contained in:
parent
d016ee9775
commit
0cdc814bcc
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
<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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
<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>
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue