Merge pull request #3737 from Budibase/feature/home-screen-redesign
Home Screen Redesign
This commit is contained in:
commit
841feac5fa
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue