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

View File

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

View File

@ -10,7 +10,7 @@ it("should rename an unpublished application", () => {
cy.get(".home-logo").click()
renameApp(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)
})
@ -29,7 +29,7 @@ xit("Should rename a published application", () => {
cy.get(".home-logo").click()
renameApp(appRename, true)
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", () => {
@ -38,7 +38,7 @@ it("Should try to rename an application to have no name", () => {
// Close modal and confirm name has not been changed
cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
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", () => {
@ -64,7 +64,7 @@ it("should validate application names", () => {
cy.get(".home-logo").click()
renameApp(numberName)
cy.searchForApplication(numberName)
cy.get(".appGrid").find(".wrapper").should("have.length", 1)
cy.get(".appTable").find(".title").should("have.length", 1)
renameApp(specialCharName)
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")
.then(val => {
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
if (published == true){
// Should not have Edit as option, will unpublish app
cy.should("not.have.value", "Edit")
cy.get(".spectrum-Menu").contains("Unpublish").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.get(".spectrum-Modal")

View File

@ -50,7 +50,9 @@ Cypress.Commands.add("deleteApp", appName => {
.its("body")
.then(val => {
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.get(".spectrum-Modal").within(() => {
cy.get("input").type(appName)

View File

@ -1,5 +1,4 @@
<script>
import { gradient } from "actions"
import {
Heading,
Button,
@ -18,16 +17,21 @@
export let deleteApp
export let unpublishApp
export let releaseLock
export let editIcon
</script>
<div class="title">
<div class="preview" use:gradient={{ seed: app.name }} />
<div style="display: flex;">
<div style="color: {app.icon?.color || ''}">
<Icon size="XL" name={app.icon?.name || "Apps"} />
</div>
<div class="name" on:click={() => editApp(app)}>
<Heading size="XS">
{app.name}
</Heading>
</div>
</div>
</div>
<div class="desktop">
{#if app.updatedAt}
{processStringSync("Updated {{ duration time 'millisecond' }} ago", {
@ -62,6 +66,7 @@
disabled={app.lockedOther}
on:click={() => editApp(app)}
size="S"
quiet
secondary>Open</Button
>
<ActionMenu align="right">
@ -86,15 +91,11 @@
<MenuItem on:click={() => updateApp(app)} icon="Edit">Edit</MenuItem>
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
{/if}
<MenuItem on:click={() => editIcon(app)} icon="Brush">Edit Icon</MenuItem>
</ActionMenu>
</div>
<style>
.preview {
height: 40px;
width: 40px;
border-radius: var(--border-radius-s);
}
.name {
text-decoration: none;
overflow: hidden;
@ -103,6 +104,7 @@
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-left: calc(1.5 * var(--spacing-xl));
}
.title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600);

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

View File

@ -1,46 +1,10 @@
<script>
import { Heading, Layout, Icon, Body } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import api from "builderStore/api"
import { Heading, Layout, Icon } from "@budibase/bbui"
export let onSelect
async function fetchTemplates() {
const response = await api.get("/api/templates?type=app")
return await response.json()
}
let templatesPromise = fetchTemplates()
</script>
<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="background-icon"
@ -67,15 +31,6 @@
</Layout>
<style>
.templates {
display: grid;
width: 100%;
grid-gap: var(--spacing-m);
grid-template-columns: 1fr;
justify-content: start;
margin-top: 15px;
}
.background-icon {
padding: 10px;
border-radius: 4px;

View File

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

View File

@ -2,33 +2,33 @@
import {
Heading,
Layout,
Detail,
Button,
ActionButton,
ActionGroup,
ButtonGroup,
Input,
Select,
Modal,
Page,
notifications,
Body,
Search,
} from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
import ChooseIconModal from "components/start/ChooseIconModal.svelte"
import { store, automationStore } from "builderStore"
import api, { del, post, get } from "builderStore/api"
import { onMount } from "svelte"
import { apps, auth, admin } from "stores/portal"
import { apps, auth, admin, templates } from "stores/portal"
import download from "downloadjs"
import { goto } from "@roxi/routify"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import AppCard from "components/start/AppCard.svelte"
import AppRow from "components/start/AppRow.svelte"
import { AppStatus } from "constants"
import analytics, { Events } from "analytics"
let layout = "grid"
let sortBy = "name"
let template
let selectedApp
@ -36,13 +36,13 @@
let updatingModal
let deletionModal
let unpublishModal
let iconModal
let creatingApp = false
let loaded = false
let searchTerm = ""
let cloud = $admin.cloud
let appName = ""
let creatingFromTemplate = false
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
$: filteredApps = enrichedApps.filter(app =>
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
@ -172,6 +172,11 @@
$goto(`../../app/${app.devId}`)
}
const editIcon = app => {
selectedApp = app
iconModal.show()
}
const exportApp = app => {
const id = app.deployed ? app.prodId : app.devId
const appName = encodeURIComponent(app.name)
@ -262,6 +267,7 @@
onMount(async () => {
await apps.load()
await templates.load()
// if the portal is loaded from an external URL with a template param
const initInfo = await auth.getInitInfo()
if (initInfo?.init_template) {
@ -274,21 +280,66 @@
</script>
<Page wide>
{#if loaded && enrichedApps.length}
<Layout noPadding>
<div class="title">
<Heading>Apps</Heading>
<Heading size="S">Welcome to Budibase</Heading>
<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>
<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">
<Detail>My Apps</Detail>
</div>
<div class="filter">
<div class="select">
<Select
quiet
autoWidth
bind:value={sortBy}
placeholder={null}
@ -299,35 +350,18 @@
]}
/>
<div class="desktop-search">
<Search placeholder="Search" bind:value={searchTerm} />
<Search quiet placeholder="Search" bind:value={searchTerm} />
</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 class="mobile-search">
<Search placeholder="Search" bind:value={searchTerm} />
</div>
<div
class:appGrid={layout === "grid"}
class:appTable={layout === "table"}
>
<div class="appTable">
{#each filteredApps as app (app.appId)}
<svelte:component
this={layout === "grid" ? AppCard : AppRow}
<AppRow
{releaseLock}
{editIcon}
{app}
{unpublishApp}
{viewApp}
@ -338,7 +372,6 @@
/>
{/each}
</div>
</Layout>
{/if}
{#if !enrichedApps.length && !creatingApp && loaded}
<div class="empty-wrapper">
@ -353,7 +386,9 @@
<Spinner size="10" />
</div>
{/if}
</Layout>
</Page>
<Modal
bind:this={creationModal}
padding={false}
@ -389,6 +424,7 @@
</ConfirmDialog>
<UpdateAppModal app={selectedApp} bind:this={updatingModal} />
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
<style>
.title,
@ -397,7 +433,7 @@
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 10px;
gap: 5px;
}
@media only screen and (max-width: 560px) {
@ -405,12 +441,48 @@
flex-direction: column;
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 {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
grid-template-columns: auto auto;
grid-gap: 30px;
}
.filter :global(.spectrum-ActionGroup) {
flex-wrap: nowrap;
@ -419,11 +491,6 @@
display: none;
}
.appGrid {
display: grid;
grid-gap: 50px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.appTable {
display: grid;
grid-template-rows: auto;
@ -464,4 +531,8 @@
display: block;
}
}
.template-card:hover {
background: var(--spectrum-alias-background-color-tertiary);
}
</style>

View File

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

View File

@ -5,3 +5,4 @@ export { apps } from "./apps"
export { email } from "./email"
export { auth } from "./auth"
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()