New Design Empty State

This commit is contained in:
Gerard Burns 2023-07-03 13:36:02 +01:00
parent 71e44b1eab
commit 1274bc32c3
12 changed files with 670 additions and 100 deletions

View File

@ -0,0 +1,39 @@
<script>
import { Icon, Heading } from "@budibase/bbui"
export let showClose = false
export let onClose = () => {}
export let heading = ""
</script>
<section class="page">
<div class="closeButton">
{#if showClose}
<Icon hoverable name="Close" on:click={onClose} />
{/if}
</div>
<div class="heading">
<Heading weight="light">{heading}</Heading>
</div>
<slot />
</section>
<style>
.page {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.heading {
text-align: center;
}
.closeButton {
height: 38px;
display: flex;
justify-content: right;
width: 100%;
}
</style>

View File

@ -145,12 +145,6 @@
padding-bottom: 20px;
}
.params {
display: flex;
flex-wrap: nowrap;
gap: 25px;
}
.synchronous-info {
display: flex;
gap: var(--spacing-s);

View File

@ -9,8 +9,6 @@
import { createValidationStore } from "helpers/validation/yup"
import * as appValidation from "helpers/validation/yup/app"
import TemplateCard from "components/common/TemplateCard.svelte"
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
import { Roles } from "constants/backend"
import { lowercase } from "helpers"
export let template
@ -136,21 +134,6 @@
// Create user
await auth.setInitInfo({})
// Create a default home screen if no template was selected
if (template == null) {
let defaultScreenTemplate = createFromScratchScreen.create()
defaultScreenTemplate.routing.route = "/home"
defaultScreenTemplate.routing.roldId = Roles.BASIC
try {
await store.actions.screens.save(defaultScreenTemplate)
} catch (err) {
console.error("Could not create a default application screen", err)
notifications.warning(
"Encountered an issue creating the default screen."
)
}
}
$goto(`/builder/app/${createdApp.instance._id}`)
} catch (error) {
creating = false

View File

@ -7,12 +7,13 @@
} from "stores/backend"
import { hasData } from "stores/selectors"
import { Icon, notifications, Heading, Body } from "@budibase/bbui"
import { notifications, Body } from "@budibase/bbui"
import { params, goto } from "@roxi/routify"
import CreateExternalDatasourceModal from "./_components/CreateExternalDatasourceModal/index.svelte"
import CreateInternalTableModal from "./_components/CreateInternalTableModal.svelte"
import DatasourceOption from "./_components/DatasourceOption.svelte"
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
import CreationPage from "components/common/CreationPage.svelte"
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
@ -46,16 +47,11 @@
bind:this={externalDatasourceModal}
/>
<div class="page">
<div class="closeButton">
{#if hasData($datasources, $tables)}
<Icon hoverable name="Close" on:click={$goto("./table")} />
{/if}
</div>
<div class="heading">
<Heading weight="light">Add new data source</Heading>
</div>
<CreationPage
showClose={hasData($datasources, $tables)}
onClose={() => $goto("./table")}
heading="Add new data source"
>
<div class="subHeading">
<Body>Get started with our Budibase DB</Body>
<div
@ -113,30 +109,13 @@
</DatasourceOption>
{/each}
</div>
</div>
</CreationPage>
<style>
.page {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.closeButton {
height: 38px;
display: flex;
justify-content: right;
width: 100%;
}
.heading {
margin-bottom: 12px;
}
.subHeading {
display: flex;
align-items: center;
margin-top: 12px;
margin-bottom: 24px;
}

View File

@ -0,0 +1,199 @@
<script>
import ScreenDetailsModal from "./ScreenDetailsModal.svelte"
import DatasourceModal from "./DatasourceModal.svelte"
import ScreenRoleModal from "./ScreenRoleModal.svelte"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { Modal, notifications } from "@budibase/bbui"
import { store } from "builderStore"
import { get } from "svelte/store"
import getTemplates from "builderStore/store/screenTemplates"
import { tables } from "stores/backend"
import { Roles } from "constants/backend"
import { capitalise } from "helpers"
import { goto } from "@roxi/routify"
let pendingScreen
// Modal refs
let screenDetailsModal
let datasourceModal
let screenAccessRoleModal
// Cache variables for workflow
let screenAccessRole = Roles.BASIC
let selectedTemplates = null
let blankScreenUrl = null
let screenMode = null
// Creates an array of screens, checking and sanitising their URLs
const createScreens = async ({ screens, screenAccessRole }) => {
if (!screens?.length) {
return
}
try {
let screenId
for (let screen of screens) {
// Check we aren't clashing with an existing URL
if (hasExistingUrl(screen.routing.route)) {
let suffix = 2
let candidateUrl = makeCandidateUrl(screen, suffix)
while (hasExistingUrl(candidateUrl)) {
candidateUrl = makeCandidateUrl(screen, ++suffix)
}
screen.routing.route = candidateUrl
}
// Sanitise URL
screen.routing.route = sanitizeUrl(screen.routing.route)
// Use the currently selected role
if (!screenAccessRole) {
return
}
screen.routing.roleId = screenAccessRole
// Create the screen
const response = await store.actions.screens.save(screen)
screenId = response._id
// Add link in layout for list screens
if (screen.props._instanceName.endsWith("List")) {
await store.actions.links.save(
screen.routing.route,
capitalise(screen.routing.route.split("/")[1])
)
}
}
$goto(`./${screenId}`)
} catch (error) {
console.log(error)
notifications.error("Error creating screens")
}
}
// Checks if any screens exist in the store with the given route and
// currently selected role
const hasExistingUrl = url => {
const roleId = screenAccessRole
const screens = get(store).screens.filter(s => s.routing.roleId === roleId)
return !!screens.find(s => s.routing?.route === url)
}
// Constructs a candidate URL for a new screen, suffixing the base of the
// screen's URL with a given suffix.
// e.g. "/sales/:id" => "/sales-1/:id"
const makeCandidateUrl = (screen, suffix) => {
let url = screen.routing?.route || ""
if (url.startsWith("/")) {
url = url.slice(1)
}
if (!url.includes("/")) {
return `/${url}-${suffix}`
} else {
const split = url.split("/")
return `/${split[0]}-${suffix}/${split.slice(1).join("/")}`
}
}
// Handler for NewScreenModal
export const show = mode => {
selectedTemplates = null
blankScreenUrl = null
screenMode = mode
pendingScreen = null
screenAccessRole = Roles.BASIC
if (mode === "table") {
datasourceModal.show()
} else if (mode === "blank") {
let templates = getTemplates($store, $tables.list)
const blankScreenTemplate = templates.find(
t => t.id === "createFromScratch"
)
pendingScreen = blankScreenTemplate.create()
screenDetailsModal.show()
} else {
throw new Error("Invalid mode provided")
}
}
// Handler for DatasourceModal confirmation, move to screen access select
const confirmScreenDatasources = async ({ templates }) => {
selectedTemplates = templates
screenAccessRoleModal.show()
}
// Handler for Datasource Screen Creation
const completeDatasourceScreenCreation = async () => {
const screens = selectedTemplates.map(template => {
let screenTemplate = template.create()
screenTemplate.datasource = template.datasource
screenTemplate.autoTableId = template.table
return screenTemplate
})
await createScreens({ screens, screenAccessRole })
}
const confirmScreenBlank = async ({ screenUrl }) => {
blankScreenUrl = screenUrl
screenAccessRoleModal.show()
}
// Submit request for a blank screen
const confirmBlankScreenCreation = async ({
screenUrl,
screenAccessRole,
}) => {
if (!pendingScreen) {
return
}
pendingScreen.routing.route = screenUrl
await createScreens({ screens: [pendingScreen], screenAccessRole })
}
// Submit screen config for creation.
const confirmScreenCreation = async () => {
if (screenMode === "blank") {
confirmBlankScreenCreation({
screenUrl: blankScreenUrl,
screenAccessRole,
})
} else {
completeDatasourceScreenCreation()
}
}
const roleSelectBack = () => {
if (screenMode === "blank") {
screenDetailsModal.show()
} else {
datasourceModal.show()
}
}
</script>
<Modal bind:this={datasourceModal}>
<DatasourceModal
onConfirm={confirmScreenDatasources}
initalScreens={!selectedTemplates ? [] : [...selectedTemplates]}
/>
</Modal>
<Modal bind:this={screenAccessRoleModal}>
<ScreenRoleModal
onConfirm={confirmScreenCreation}
onCancel={roleSelectBack}
bind:screenAccessRole
screenUrl={blankScreenUrl}
/>
</Modal>
<Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal
onConfirm={confirmScreenBlank}
initialUrl={blankScreenUrl}
/>
</Modal>

View File

@ -0,0 +1,201 @@
<script>
import { store } from "builderStore"
import {
ModalContent,
Layout,
notifications,
Icon,
Body,
} from "@budibase/bbui"
import { tables, datasources } from "stores/backend"
import getTemplates from "builderStore/store/screenTemplates"
import ICONS from "components/backend/DatasourceNavigator/icons"
import { IntegrationNames } from "constants"
import { onMount } from "svelte"
export let onCancel
export let onConfirm
export let initalScreens = []
let selectedScreens = [...initalScreens]
const toggleScreenSelection = (table, datasource) => {
if (selectedScreens.find(s => s.table === table._id)) {
selectedScreens = selectedScreens.filter(
screen => screen.table !== table._id
)
} else {
let partialTemplates = getTemplates($store, $tables.list).reduce(
(acc, template) => {
if (template.table === table._id) {
template.datasource = datasource.name
acc.push(template)
}
return acc
},
[]
)
selectedScreens = [...partialTemplates, ...selectedScreens]
}
}
const confirmDatasourceSelection = async () => {
await onConfirm({
templates: selectedScreens,
})
}
$: filteredSources = Array.isArray($datasources.list)
? $datasources.list.reduce((acc, datasource) => {
if (
datasource.source !== IntegrationNames.REST &&
datasource["entities"]
) {
acc.push(datasource)
}
return acc
}, [])
: []
onMount(async () => {
try {
await datasources.fetch()
} catch (error) {
notifications.error("Error fetching datasources")
}
})
</script>
<span>
<ModalContent
title="Autogenerated screens"
confirmText="Confirm"
cancelText="Back"
onConfirm={confirmDatasourceSelection}
{onCancel}
disabled={!selectedScreens.length}
size="L"
>
<Body size="S">
Select which datasources you would like to use to create your screens
</Body>
<Layout noPadding gap="S">
{#each filteredSources as datasource}
<div class="data-source-wrap">
<div class="data-source-header">
<svelte:component
this={ICONS[datasource.source]}
height="24"
width="24"
/>
<div class="data-source-name">{datasource.name}</div>
</div>
{#if Array.isArray(datasource.entities)}
{#each datasource.entities.filter(table => table._id !== "ta_users") as table}
<div
class="data-source-entry"
class:selected={selectedScreens.find(
x => x.table === table._id
)}
on:click={() => toggleScreenSelection(table, datasource)}
>
<svg
width="16px"
height="16px"
class="spectrum-Icon"
style="color: white"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Table" />
</svg>
{table.name}
{#if selectedScreens.find(x => x.table === table._id)}
<span class="data-source-check">
<Icon size="S" name="CheckmarkCircle" />
</span>
{/if}
</div>
{/each}
{/if}
{#if datasource["entities"] && !Array.isArray(datasource.entities)}
{#each Object.keys(datasource.entities).filter(table => table._id !== "ta_users") as table_key}
<div
class="data-source-entry"
class:selected={selectedScreens.find(
x => x.table === datasource.entities[table_key]._id
)}
on:click={() =>
toggleScreenSelection(
datasource.entities[table_key],
datasource
)}
>
<svg
width="16px"
height="16px"
class="spectrum-Icon"
style="color: white"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Table" />
</svg>
{datasource.entities[table_key].name}
{#if selectedScreens.find(x => x.table === datasource.entities[table_key]._id)}
<span class="data-source-check">
<Icon size="S" name="CheckmarkCircle" />
</span>
{/if}
</div>
{/each}
{/if}
</div>
{/each}
</Layout>
</ModalContent>
</span>
<style>
.data-source-wrap {
padding-bottom: var(--spectrum-alias-item-padding-s);
display: grid;
grid-gap: var(--spacing-s);
}
.data-source-header {
display: flex;
align-items: center;
gap: var(--spacing-m);
padding-bottom: var(--spacing-xs);
}
.data-source-entry {
cursor: pointer;
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
padding: var(--spectrum-alias-item-padding-s);
background: var(--spectrum-alias-background-color-secondary);
transition: 0.3s all;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
border-width: 1px;
display: flex;
align-items: center;
}
.data-source-entry:hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
.data-source-entry .data-source-check {
margin-left: auto;
}
.data-source-entry :global(.spectrum-Icon) {
min-width: 16px;
}
.data-source-entry .data-source-check :global(.spectrum-Icon) {
color: var(--spectrum-global-color-green-600);
display: block;
}
</style>

View File

@ -0,0 +1,80 @@
<script>
import { ModalContent, Input } from "@budibase/bbui"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { get } from "svelte/store"
import { store } from "builderStore"
export let onConfirm
export let onCancel
export let screenUrl
export let screenRole
export let confirmText = "Continue"
const appPrefix = "/app"
let touched = false
let error
$: appUrl = screenUrl
? `${window.location.origin}${appPrefix}${screenUrl}`
: `${window.location.origin}${appPrefix}`
const routeChanged = event => {
if (!event.detail.startsWith("/")) {
screenUrl = "/" + event.detail
}
touched = true
screenUrl = sanitizeUrl(screenUrl)
if (routeExists(screenUrl)) {
error = "This URL is already taken for this access role"
} else {
error = null
}
}
const routeExists = url => {
if (!screenRole) {
return false
}
return get(store).screens.some(
screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === screenRole
)
}
const confirmScreenDetails = async () => {
await onConfirm({
screenUrl,
})
}
</script>
<ModalContent
size="M"
title={"Screen details"}
{confirmText}
onConfirm={confirmScreenDetails}
{onCancel}
cancelText={"Back"}
disabled={!screenUrl || error || !touched}
>
<Input
label="Enter a URL for the new screen"
{error}
bind:value={screenUrl}
on:change={routeChanged}
/>
<div class="app-server" title={appUrl}>
{appUrl}
</div>
</ModalContent>
<style>
.app-server {
color: var(--spectrum-global-color-gray-600);
width: 320px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@ -0,0 +1,62 @@
<script>
import { Select, ModalContent } from "@budibase/bbui"
import { RoleUtils } from "@budibase/frontend-core"
import { roles } from "stores/backend"
import { get } from "svelte/store"
import { store } from "builderStore"
import { onMount } from "svelte"
export let onConfirm
export let onCancel
export let screenUrl
export let screenAccessRole
let error
const onChangeRole = e => {
const roleId = e.detail
if (routeExists(screenUrl, roleId)) {
error = "This URL is already taken for this access role"
} else {
error = null
}
}
const routeExists = (url, role) => {
if (!url || !role) {
return false
}
return get(store).screens.some(
screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === role
)
}
onMount(() => {
// Validate the initial role
onChangeRole({ detail: screenAccessRole })
})
</script>
<ModalContent
title="Autogenerated screens"
confirmText="Done"
cancelText="Back"
{onConfirm}
{onCancel}
disabled={!!error}
>
Select which level of access you want your screens to have
<Select
bind:value={screenAccessRole}
on:change={onChangeRole}
label="Access"
{error}
getOptionLabel={role => role.name}
getOptionValue={role => role._id}
getOptionColour={role => RoleUtils.getRoleColour(role._id)}
options={$roles}
placeholder={null}
/>
</ModalContent>

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -2,29 +2,17 @@
import { store, selectedScreen } from "builderStore"
import { onMount } from "svelte"
import { redirect } from "@roxi/routify"
import { Layout, Button, Detail, notifications } from "@budibase/bbui"
import Logo from "assets/bb-space-man.svg"
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
import { Roles } from "constants/backend"
import { Body } from "@budibase/bbui"
import CreationPage from "components/common/CreationPage.svelte"
import blankImage from "./blank.png"
import tableImage from "./table.png"
import CreateScreenModal from "./_components/CreateScreenModal.svelte"
let loaded = false
const createFirstScreen = async () => {
let screen = createFromScratchScreen.create()
screen.routing.route = "/home"
screen.routing.roldId = Roles.BASIC
screen.routing.homeScreen = true
try {
const savedScreen = await store.actions.screens.save(screen)
notifications.success("Screen created successfully")
$redirect(`./${savedScreen._id}`)
} catch (err) {
console.error("Could not create screen", err)
notifications.error("Error creating screen")
}
}
let createScreenModal
onMount(() => {
loaded = true
if ($selectedScreen) {
$redirect(`./${$selectedScreen._id}`)
} else if ($store.screens?.length) {
@ -36,32 +24,84 @@
</script>
{#if loaded}
<div class="centered">
<Layout gap="S" justifyItems="center">
<img class="img-size" alt="logo" src={Logo} />
<div class="new-screen-text">
<Detail size="L">LETS BRING THIS APP TO LIFE</Detail>
<CreationPage showClose={false} heading="Create your first screen">
<div class="subHeading">
<Body size="L">Start from scratch or create screens from your data</Body>
</div>
<div class="cards">
<div class="card" on:click={() => createScreenModal.show("blank")}>
<div class="image">
<img alt="" src={blankImage} />
</div>
<div class="text">
<Body size="S">Blank screen</Body>
<Body size="XS">Add an empty blank screen</Body>
</div>
</div>
<Button on:click={createFirstScreen} size="M" cta icon="Add">
Create first screen
</Button>
</Layout>
</div>
<div class="card" on:click={() => createScreenModal.show("table")}>
<div class="image">
<img alt="" src={tableImage} />
</div>
<div class="text">
<Body size="S">Table</Body>
<Body size="XS">View, edit and delete rows on a table</Body>
</div>
</div>
</div>
</CreationPage>
{/if}
<CreateScreenModal bind:this={createScreenModal} />
<style>
.centered {
width: 100%;
height: 100%;
display: grid;
place-items: center;
}
.new-screen-text {
width: 150px;
.subHeading :global(p) {
text-align: center;
font-weight: 600;
margin-top: 12px;
margin-bottom: 24px;
color: var(--grey-6);
}
.img-size {
width: 170px;
.cards {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.card {
margin: 12px;
max-width: 235px;
transition: filter 150ms;
}
.card:hover {
filter: brightness(1.1);
cursor: pointer;
}
.image {
border-radius: 4px 4px 0 0;
width: 100%;
max-height: 127px;
overflow: hidden;
}
.image img {
width: 100%;
}
.text {
border: 1px solid var(--grey-4);
border-radius: 0 0 4px 4px;
padding: 8px 16px 13px 16px;
}
.text :global(p:nth-child(1)) {
margin-bottom: 6px;
}
.text :global(p:nth-child(2)) {
color: var(--grey-6);
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -7,8 +7,6 @@
import { API } from "api"
import { store, automationStore } from "builderStore"
import { auth, admin } from "stores/portal"
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
import { Roles } from "constants/backend"
let name = "My first app"
let url = "my-first-app"
@ -38,11 +36,6 @@
// Create user
await auth.setInitInfo({})
let defaultScreenTemplate = createFromScratchScreen.create()
defaultScreenTemplate.routing.route = "/home"
defaultScreenTemplate.routing.roldId = Roles.BASIC
await store.actions.screens.save(defaultScreenTemplate)
appId = createdApp.instance._id
return createdApp
}