Merge pull request #3737 from Budibase/feature/home-screen-redesign

Home Screen Redesign
This commit is contained in:
Peter Clement 2021-12-15 09:47:03 +00:00 committed by GitHub
commit 841feac5fa
13 changed files with 317 additions and 154 deletions

View File

@ -7,7 +7,7 @@
export let disabled = false export let disabled = false
export let id = null export let id = null
export let updateOnChange = true export let updateOnChange = true
export let quiet = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let focus = false let focus = false
@ -41,6 +41,7 @@
<div class="spectrum-Search" class:is-disabled={disabled}> <div class="spectrum-Search" class:is-disabled={disabled}>
<div <div
class="spectrum-Textfield" class="spectrum-Textfield"
class:spectrum-Textfield--quiet={quiet}
class:is-focused={focus} class:is-focused={focus}
class:is-disabled={disabled} class:is-disabled={disabled}
> >

View File

@ -9,6 +9,7 @@
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let updateOnChange = true export let updateOnChange = true
export let quiet = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -23,6 +24,7 @@
{disabled} {disabled}
{value} {value}
{placeholder} {placeholder}
{quiet}
on:change={onChange} on:change={onChange}
on:click on:click
on:input on:input

View File

@ -10,7 +10,7 @@ it("should rename an unpublished application", () => {
cy.get(".home-logo").click() cy.get(".home-logo").click()
renameApp(appRename) renameApp(appRename)
cy.searchForApplication(appRename) cy.searchForApplication(appRename)
cy.get(".appGrid").find(".wrapper").should("have.length", 1) cy.get(".appTable").find(".title").should("have.length", 1)
cy.deleteApp(appRename) cy.deleteApp(appRename)
}) })
@ -29,7 +29,7 @@ xit("Should rename a published application", () => {
cy.get(".home-logo").click() cy.get(".home-logo").click()
renameApp(appRename, true) renameApp(appRename, true)
cy.searchForApplication(appRename) cy.searchForApplication(appRename)
cy.get(".appGrid").find(".wrapper").should("have.length", 1) cy.get(".appTable").find(".title").should("have.length", 1)
}) })
it("Should try to rename an application to have no name", () => { it("Should try to rename an application to have no name", () => {
@ -38,7 +38,7 @@ it("Should try to rename an application to have no name", () => {
// Close modal and confirm name has not been changed // Close modal and confirm name has not been changed
cy.get(".spectrum-Dialog-grid").contains("Cancel").click() cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
cy.searchForApplication("Cypress Tests") cy.searchForApplication("Cypress Tests")
cy.get(".appGrid").find(".wrapper").should("have.length", 1) cy.get(".appTable").find(".title").should("have.length", 1)
}) })
xit("Should create two applications with the same name", () => { xit("Should create two applications with the same name", () => {
@ -64,7 +64,7 @@ it("should validate application names", () => {
cy.get(".home-logo").click() cy.get(".home-logo").click()
renameApp(numberName) renameApp(numberName)
cy.searchForApplication(numberName) cy.searchForApplication(numberName)
cy.get(".appGrid").find(".wrapper").should("have.length", 1) cy.get(".appTable").find(".title").should("have.length", 1)
renameApp(specialCharName) renameApp(specialCharName)
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only") cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only")
}) })
@ -74,14 +74,14 @@ it("should validate application names", () => {
.its("body") .its("body")
.then(val => { .then(val => {
if (val.length > 0) { if (val.length > 0) {
cy.get(".title > :nth-child(3) > .spectrum-Icon").click() cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
// Check for when an app is published // Check for when an app is published
if (published == true){ if (published == true){
// Should not have Edit as option, will unpublish app // Should not have Edit as option, will unpublish app
cy.should("not.have.value", "Edit") cy.should("not.have.value", "Edit")
cy.get(".spectrum-Menu").contains("Unpublish").click() cy.get(".spectrum-Menu").contains("Unpublish").click()
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
cy.get(".title > :nth-child(3) > .spectrum-Icon").click() cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
} }
cy.contains("Edit").click() cy.contains("Edit").click()
cy.get(".spectrum-Modal") cy.get(".spectrum-Modal")

View File

@ -50,7 +50,9 @@ Cypress.Commands.add("deleteApp", appName => {
.its("body") .its("body")
.then(val => { .then(val => {
if (val.length > 0) { if (val.length > 0) {
cy.get(".title > :nth-child(3) > .spectrum-Icon").click() cy.get(
".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon"
).click()
cy.contains("Delete").click() cy.contains("Delete").click()
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal").within(() => {
cy.get("input").type(appName) cy.get("input").type(appName)

View File

@ -1,5 +1,4 @@
<script> <script>
import { gradient } from "actions"
import { import {
Heading, Heading,
Button, Button,
@ -18,14 +17,19 @@
export let deleteApp export let deleteApp
export let unpublishApp export let unpublishApp
export let releaseLock export let releaseLock
export let editIcon
</script> </script>
<div class="title"> <div class="title">
<div class="preview" use:gradient={{ seed: app.name }} /> <div style="display: flex;">
<div class="name" on:click={() => editApp(app)}> <div style="color: {app.icon?.color || ''}">
<Heading size="XS"> <Icon size="XL" name={app.icon?.name || "Apps"} />
{app.name} </div>
</Heading> <div class="name" on:click={() => editApp(app)}>
<Heading size="XS">
{app.name}
</Heading>
</div>
</div> </div>
</div> </div>
<div class="desktop"> <div class="desktop">
@ -62,6 +66,7 @@
disabled={app.lockedOther} disabled={app.lockedOther}
on:click={() => editApp(app)} on:click={() => editApp(app)}
size="S" size="S"
quiet
secondary>Open</Button secondary>Open</Button
> >
<ActionMenu align="right"> <ActionMenu align="right">
@ -86,15 +91,11 @@
<MenuItem on:click={() => updateApp(app)} icon="Edit">Edit</MenuItem> <MenuItem on:click={() => updateApp(app)} icon="Edit">Edit</MenuItem>
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem> <MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
{/if} {/if}
<MenuItem on:click={() => editIcon(app)} icon="Brush">Edit Icon</MenuItem>
</ActionMenu> </ActionMenu>
</div> </div>
<style> <style>
.preview {
height: 40px;
width: 40px;
border-radius: var(--border-radius-s);
}
.name { .name {
text-decoration: none; text-decoration: none;
overflow: hidden; overflow: hidden;
@ -103,6 +104,7 @@
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

@ -0,0 +1,127 @@
<script>
import { ModalContent, Modal, Icon, ColorPicker, Label } from "@budibase/bbui"
import { apps } from "stores/portal"
export let app
let modal
$: selectedIcon = app?.icon?.name
$: selectedColor = app?.icon?.color
let iconsList = [
"Actions",
"ConversionFunnel",
"App",
"Briefcase",
"Money",
"ShoppingCart",
"Form",
"Help",
"Monitoring",
"Sandbox",
"Project",
"Organisations",
"Magnify",
"Launch",
"Car",
"Camera",
"Bug",
"Channel",
"Calculator",
"Calendar",
"GraphDonut",
"GraphBarHorizontal",
"Demographic",
"Apps",
]
export const show = () => {
modal.show()
}
export const hide = () => {
modal.hide()
}
const onCancel = () => {
selectedIcon = ""
selectedColor = ""
hide()
}
const changeColor = val => {
selectedColor = val
}
const save = async () => {
await apps.update(app.instance._id, {
icon: {
name: selectedIcon,
color: selectedColor,
},
})
}
</script>
<Modal bind:this={modal} on:hide={onCancel}>
<ModalContent
title={"Edit Icon"}
confirmText={"Save"}
onConfirm={() => save()}
>
<div class="scrollable-icons">
<div class="title-spacing">
<Label>Select an Icon</Label>
</div>
<div class="grid">
{#each iconsList as item}
<div
class="icon-item"
style="color: {item === selectedIcon ? selectedColor : ''}"
on:click={() => (selectedIcon = item)}
>
<Icon name={item} />
</div>
{/each}
</div>
</div>
<div class="color-selection">
<div>
<Label>Select a Color</Label>
</div>
<div class="color-selection-item">
<ColorPicker
bind:value={selectedColor}
on:change={e => changeColor(e.detail)}
/>
</div>
</div>
</ModalContent>
</Modal>
<style>
.scrollable-icons {
overflow-y: auto;
height: 230px;
}
.grid {
display: grid;
grid-gap: 20px;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
}
.color-selection {
display: flex;
align-items: center;
}
.color-selection-item {
margin-left: 20px;
}
.title-spacing {
margin-bottom: 20px;
}
.icon-item {
cursor: pointer;
}
</style>

View File

@ -1,13 +1,7 @@
<script> <script>
import { writable, get as svelteGet } from "svelte/store" import { writable, get as svelteGet } from "svelte/store"
import {
notifications, import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
Input,
ModalContent,
Dropzone,
Body,
Checkbox,
} from "@budibase/bbui"
import { store, automationStore, hostingStore } from "builderStore" import { store, automationStore, hostingStore } from "builderStore"
import { admin, auth } from "stores/portal" import { admin, auth } from "stores/portal"
import { string, mixed, object } from "yup" import { string, mixed, object } from "yup"
@ -147,16 +141,6 @@
} }
} }
function getModalTitle() {
let title = "Create App"
if (template.fromFile) {
title = "Import App"
} else if (template.key) {
title = "Create app from template"
}
return title
}
async function onCancel() { async function onCancel() {
template = null template = null
await auth.setInitInfo({}) await auth.setInitInfo({})
@ -187,7 +171,7 @@
</ModalContent> </ModalContent>
{:else} {:else}
<ModalContent <ModalContent
title={getModalTitle()} title={"Name your app"}
confirmText={template?.fromFile ? "Import app" : "Create app"} confirmText={template?.fromFile ? "Import app" : "Create app"}
onConfirm={createNewApp} onConfirm={createNewApp}
onCancel={inline ? onCancel : null} onCancel={inline ? onCancel : null}
@ -207,16 +191,14 @@
}} }}
/> />
{/if} {/if}
<Body size="S">
Give your new app a name, and choose which groups have access (paid plans
only).
</Body>
<Input <Input
bind:value={$values.name} bind:value={$values.name}
error={$touched.name && $errors.name} error={$touched.name && $errors.name}
on:blur={() => ($touched.name = true)} on:blur={() => ($touched.name = true)}
label="Name" label="Name"
placeholder={$auth.user.firstName
? `${$auth.user.firstName}'s app`
: "My app"}
/> />
<Checkbox label="Group access" disabled value={true} text="All users" />
</ModalContent> </ModalContent>
{/if} {/if}

View File

@ -1,46 +1,10 @@
<script> <script>
import { Heading, Layout, Icon, Body } from "@budibase/bbui" import { Heading, Layout, Icon } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import api from "builderStore/api"
export let onSelect export let onSelect
async function fetchTemplates() {
const response = await api.get("/api/templates?type=app")
return await response.json()
}
let templatesPromise = fetchTemplates()
</script> </script>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
{#await templatesPromise}
<div class="spinner-container">
<Spinner size="30" />
</div>
{:then templates}
{#if templates?.length > 0}
<Body size="M">Select a template below, or start from scratch.</Body>
{:else}
<Body size="M">Start your app from scratch below.</Body>
{/if}
<div class="templates">
{#each templates as template}
<div class="template" on:click={() => onSelect(template)}>
<div
class="background-icon"
style={`background: ${template.background};`}
>
<Icon name={template.icon} />
</div>
<Heading size="XS">{template.name}</Heading>
<p class="detail">{template?.category?.toUpperCase()}</p>
</div>
{/each}
</div>
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
<div class="template start-from-scratch" on:click={() => onSelect(null)}> <div class="template start-from-scratch" on:click={() => onSelect(null)}>
<div <div
class="background-icon" class="background-icon"
@ -67,15 +31,6 @@
</Layout> </Layout>
<style> <style>
.templates {
display: grid;
width: 100%;
grid-gap: var(--spacing-m);
grid-template-columns: 1fr;
justify-content: start;
margin-top: 15px;
}
.background-icon { .background-icon {
padding: 10px; padding: 10px;
border-radius: 4px; border-radius: 4px;

View File

@ -77,7 +77,7 @@
async function updateApp() { async function updateApp() {
try { try {
// Update App // Update App
await apps.update(app.instance._id, $values.name.trim()) await apps.update(app.instance._id, { name: $values.name.trim() })
hide() hide()
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View File

@ -2,33 +2,33 @@
import { import {
Heading, Heading,
Layout, Layout,
Detail,
Button, Button,
ActionButton,
ActionGroup,
ButtonGroup, ButtonGroup,
Input, Input,
Select, Select,
Modal, Modal,
Page, Page,
notifications, notifications,
Body,
Search, Search,
} from "@budibase/bbui" } from "@budibase/bbui"
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"
import ChooseIconModal from "components/start/ChooseIconModal.svelte"
import { store, automationStore } from "builderStore" import { store, automationStore } from "builderStore"
import api, { del, post, get } from "builderStore/api" import api, { del, post, get } from "builderStore/api"
import { onMount } from "svelte" import { onMount } from "svelte"
import { apps, auth, admin } from "stores/portal" import { apps, auth, admin, templates } from "stores/portal"
import download from "downloadjs" import download from "downloadjs"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import AppCard from "components/start/AppCard.svelte"
import AppRow from "components/start/AppRow.svelte" import AppRow from "components/start/AppRow.svelte"
import { AppStatus } from "constants" import { AppStatus } from "constants"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
let layout = "grid"
let sortBy = "name" let sortBy = "name"
let template let template
let selectedApp let selectedApp
@ -36,13 +36,13 @@
let updatingModal let updatingModal
let deletionModal let deletionModal
let unpublishModal let unpublishModal
let iconModal
let creatingApp = false let creatingApp = false
let loaded = false let loaded = false
let searchTerm = "" let searchTerm = ""
let cloud = $admin.cloud let cloud = $admin.cloud
let appName = "" let appName = ""
let creatingFromTemplate = false let creatingFromTemplate = false
$: enrichedApps = enrichApps($apps, $auth.user, sortBy) $: enrichedApps = enrichApps($apps, $auth.user, sortBy)
$: filteredApps = enrichedApps.filter(app => $: filteredApps = enrichedApps.filter(app =>
app?.name?.toLowerCase().includes(searchTerm.toLowerCase()) app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
@ -172,6 +172,11 @@
$goto(`../../app/${app.devId}`) $goto(`../../app/${app.devId}`)
} }
const editIcon = app => {
selectedApp = app
iconModal.show()
}
const exportApp = app => { const exportApp = app => {
const id = app.deployed ? app.prodId : app.devId const id = app.deployed ? app.prodId : app.devId
const appName = encodeURIComponent(app.name) const appName = encodeURIComponent(app.name)
@ -262,6 +267,7 @@
onMount(async () => { onMount(async () => {
await apps.load() await apps.load()
await templates.load()
// 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) {
@ -274,21 +280,66 @@
</script> </script>
<Page wide> <Page wide>
{#if loaded && enrichedApps.length} <Layout noPadding>
<Layout noPadding> <div class="title">
<Heading size="S">Welcome to Budibase</Heading>
<ButtonGroup>
{#if cloud}
<Button secondary on:click={initiateAppsExport}>Export apps</Button>
{/if}
<Button icon="Import" quiet secondary on:click={initiateAppImport}
>Import app</Button
>
<Button icon="Add" cta on:click={initiateAppCreation}>Create app</Button
>
</ButtonGroup>
</div>
<div class="title-text">
<Body size="S">Manage your apps and get a head start with templates</Body>
</div>
<Detail>Quick Start Templates</Detail>
<div class="grid">
{#each $templates as item}
<div
on:click={() => {
template = item
creationModal.show()
creatingApp = true
}}
class="template-card"
>
<div class="card-body">
<div style="color: {item.background}" class="iconAlign">
<svg
width="26px"
height="26px"
class="spectrum-Icon"
style="color:{item.background};"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-{item.icon}" />
</svg>
</div>
<div class="iconAlign">
<Body weight="900" size="S">{item.name}</Body>
<div style="font-size: 10px;">
<Body size="S">{item.category.toUpperCase()}</Body>
</div>
</div>
</div>
</div>
{/each}
</div>
{#if loaded && enrichedApps.length}
<div class="title"> <div class="title">
<Heading>Apps</Heading> <Detail>My Apps</Detail>
<ButtonGroup>
{#if cloud}
<Button secondary on:click={initiateAppsExport}>Export apps</Button>
{/if}
<Button secondary on:click={initiateAppImport}>Import app</Button>
<Button cta on:click={initiateAppCreation}>Create app</Button>
</ButtonGroup>
</div> </div>
<div class="filter"> <div class="filter">
<div class="select"> <div class="select">
<Select <Select
quiet
autoWidth autoWidth
bind:value={sortBy} bind:value={sortBy}
placeholder={null} placeholder={null}
@ -299,35 +350,18 @@
]} ]}
/> />
<div class="desktop-search"> <div class="desktop-search">
<Search placeholder="Search" bind:value={searchTerm} /> <Search quiet placeholder="Search" bind:value={searchTerm} />
</div> </div>
</div> </div>
<ActionGroup>
<ActionButton
on:click={() => (layout = "grid")}
selected={layout === "grid"}
quiet
icon="ClassicGridView"
/>
<ActionButton
on:click={() => (layout = "table")}
selected={layout === "table"}
quiet
icon="ViewRow"
/>
</ActionGroup>
</div> </div>
<div class="mobile-search"> <div class="mobile-search">
<Search placeholder="Search" bind:value={searchTerm} /> <Search placeholder="Search" bind:value={searchTerm} />
</div> </div>
<div <div class="appTable">
class:appGrid={layout === "grid"}
class:appTable={layout === "table"}
>
{#each filteredApps as app (app.appId)} {#each filteredApps as app (app.appId)}
<svelte:component <AppRow
this={layout === "grid" ? AppCard : AppRow}
{releaseLock} {releaseLock}
{editIcon}
{app} {app}
{unpublishApp} {unpublishApp}
{viewApp} {viewApp}
@ -338,22 +372,23 @@
/> />
{/each} {/each}
</div> </div>
</Layout> {/if}
{/if} {#if !enrichedApps.length && !creatingApp && loaded}
{#if !enrichedApps.length && !creatingApp && loaded} <div class="empty-wrapper">
<div class="empty-wrapper"> <Modal inline>
<Modal inline> <CreateAppModal {template} inline={true} />
<CreateAppModal {template} inline={true} /> </Modal>
</Modal> </div>
</div> {/if}
{/if} {#if creatingFromTemplate}
{#if creatingFromTemplate} <div class="empty-wrapper">
<div class="empty-wrapper"> <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>
</Page> </Page>
<Modal <Modal
bind:this={creationModal} bind:this={creationModal}
padding={false} padding={false}
@ -389,6 +424,7 @@
</ConfirmDialog> </ConfirmDialog>
<UpdateAppModal app={selectedApp} bind:this={updatingModal} /> <UpdateAppModal app={selectedApp} bind:this={updatingModal} />
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
<style> <style>
.title, .title,
@ -397,7 +433,7 @@
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 10px; gap: 5px;
} }
@media only screen and (max-width: 560px) { @media only screen and (max-width: 560px) {
@ -405,12 +441,48 @@
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
} }
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
.iconAlign {
padding: 0 0 0 var(--spacing-m);
display: inline-block;
}
.template-card {
height: 80px;
width: 270px;
border-radius: var(--border-radius-s);
margin-bottom: var(--spacing-m);
border: 1px solid var(--spectrum-global-color-gray-300);
cursor: pointer;
display: flex;
}
.title-text {
margin-top: calc(var(--spacing-xl) * -1);
}
.card-body {
display: flex;
align-items: center;
padding: 12px;
}
.grid {
display: grid;
grid-gap: 5px;
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
}
@media (min-width: 200px) {
} }
.select { .select {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: auto auto;
grid-gap: 10px; grid-gap: 30px;
} }
.filter :global(.spectrum-ActionGroup) { .filter :global(.spectrum-ActionGroup) {
flex-wrap: nowrap; flex-wrap: nowrap;
@ -419,11 +491,6 @@
display: none; display: none;
} }
.appGrid {
display: grid;
grid-gap: 50px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.appTable { .appTable {
display: grid; display: grid;
grid-template-rows: auto; grid-template-rows: auto;
@ -464,4 +531,8 @@
display: block; display: block;
} }
} }
.template-card:hover {
background: var(--spectrum-alias-background-color-tertiary);
}
</style> </style>

View File

@ -65,16 +65,17 @@ export function createAppStore() {
} }
} }
async function update(appId, name) { async function update(appId, value) {
const response = await api.put(`/api/applications/${appId}`, { name }) console.log({ value })
const response = await api.put(`/api/applications/${appId}`, { ...value })
if (response.status === 200) { if (response.status === 200) {
store.update(state => { store.update(state => {
const updatedAppIndex = state.findIndex( const updatedAppIndex = state.findIndex(
app => app.instance._id === appId app => app.instance._id === appId
) )
if (updatedAppIndex !== -1) { if (updatedAppIndex !== -1) {
const updatedApp = state[updatedAppIndex] let updatedApp = state[updatedAppIndex]
updatedApp.name = name updatedApp = { ...updatedApp, ...value }
state.apps = state.splice(updatedAppIndex, 1, updatedApp) state.apps = state.splice(updatedAppIndex, 1, updatedApp)
} }
return state return state

View File

@ -5,3 +5,4 @@ export { apps } from "./apps"
export { email } from "./email" export { email } from "./email"
export { auth } from "./auth" export { auth } from "./auth"
export { oidc } from "./oidc" export { oidc } from "./oidc"
export { templates } from "./templates"

View File

@ -0,0 +1,19 @@
import { writable } from "svelte/store"
import api from "builderStore/api"
export function templatesStore() {
const { subscribe, set } = writable([])
async function load() {
const response = await api.get("/api/templates?type=app")
const json = await response.json()
set(json)
}
return {
subscribe,
load,
}
}
export const templates = templatesStore()