New App Onboarding (#9489)
* New App Onboarding * Lint * Move app creation onboarding into its own route * Fix quiet action button variant * Fix alt attribute background image flashing * Update routing logic and redirects to show app creation onboarding * Navigate to data rather than design upon initial app creation to allow tour to function properly * Tidy up popover logic and remove tip functionality * Fix binding popovers * Lint * Silence spammy warnings from the builder * Exclude SVG files from rollup plugin image to fix spectrum icons * Fix help menu icon colours not working in light themes * Tweak help menu styles --------- Co-authored-by: Andrew Kingston <andrew@kingston.dev>
This commit is contained in:
parent
9f3cb5934b
commit
dd343a5946
|
@ -86,7 +86,7 @@
|
|||
margin-left: 0;
|
||||
transition: color ease-out 130ms;
|
||||
}
|
||||
.is-selected:not(.spectrum-ActionButton--emphasized) {
|
||||
.is-selected:not(.spectrum-ActionButton--emphasized):not(.spectrum-ActionButton--quiet) {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
border-color: var(--spectrum-global-color-gray-700);
|
||||
}
|
||||
|
|
|
@ -16,10 +16,10 @@ export default function positionDropdown(
|
|||
top: null,
|
||||
}
|
||||
|
||||
let popoverLeftPad = 20
|
||||
|
||||
// Determine vertical styles
|
||||
if (window.innerHeight - anchorBounds.bottom < 100) {
|
||||
if (align === "right-outside") {
|
||||
styles.top = anchorBounds.top
|
||||
} else if (window.innerHeight - anchorBounds.bottom < 100) {
|
||||
styles.top = anchorBounds.top - elementBounds.height - 5
|
||||
} else {
|
||||
styles.top = anchorBounds.bottom + 5
|
||||
|
@ -34,15 +34,9 @@ export default function positionDropdown(
|
|||
styles.minWidth = anchorBounds.width
|
||||
}
|
||||
if (align === "right") {
|
||||
let left =
|
||||
anchorBounds.left + anchorBounds.width / 2 - elementBounds.width
|
||||
// Accommodate margin on popover: 1.25rem; ~20px
|
||||
if (left + elementBounds.width + popoverLeftPad > window.innerWidth) {
|
||||
left -= 20
|
||||
}
|
||||
styles.left = left
|
||||
} else if (align === "right-side") {
|
||||
styles.left = anchorBounds.left + anchorBounds.width
|
||||
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width
|
||||
} else if (align === "right-outside") {
|
||||
styles.left = anchorBounds.right + 10
|
||||
} else {
|
||||
styles.left = anchorBounds.left
|
||||
}
|
||||
|
|
|
@ -247,10 +247,6 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.no-variables-height {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.no-variables-text {
|
||||
padding: var(--spacing-m);
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
|
|
|
@ -15,15 +15,10 @@
|
|||
export let portalTarget
|
||||
export let dataCy
|
||||
export let maxWidth
|
||||
export let direction = "bottom"
|
||||
export let showTip = false
|
||||
export let open = false
|
||||
export let useAnchorWidth = false
|
||||
export let dismissible = true
|
||||
|
||||
$: tooltipClasses = showTip
|
||||
? `spectrum-Popover--withTip spectrum-Popover--${direction}`
|
||||
: ""
|
||||
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
|
||||
|
||||
export const show = () => {
|
||||
|
@ -62,29 +57,31 @@
|
|||
</script>
|
||||
|
||||
{#if open}
|
||||
<Portal {target}>
|
||||
<div
|
||||
tabindex="0"
|
||||
use:positionDropdown={{
|
||||
anchor,
|
||||
align,
|
||||
maxWidth,
|
||||
useAnchorWidth,
|
||||
showTip: false,
|
||||
}}
|
||||
use:clickOutside={{
|
||||
callback: dismissible ? handleOutsideClick : () => {},
|
||||
anchor,
|
||||
}}
|
||||
on:keydown={handleEscape}
|
||||
class={"spectrum-Popover is-open " + (tooltipClasses || "")}
|
||||
role="presentation"
|
||||
data-cy={dataCy}
|
||||
transition:fly|local={{ y: -20, duration: 200 }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</Portal>
|
||||
{#key anchor}
|
||||
<Portal {target}>
|
||||
<div
|
||||
tabindex="0"
|
||||
use:positionDropdown={{
|
||||
anchor,
|
||||
align,
|
||||
maxWidth,
|
||||
useAnchorWidth,
|
||||
showTip: false,
|
||||
}}
|
||||
use:clickOutside={{
|
||||
callback: dismissible ? handleOutsideClick : () => {},
|
||||
anchor,
|
||||
}}
|
||||
on:keydown={handleEscape}
|
||||
class="spectrum-Popover is-open"
|
||||
role="presentation"
|
||||
data-cy={dataCy}
|
||||
transition:fly|local={{ y: -20, duration: 200 }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</Portal>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
@ -93,13 +90,4 @@
|
|||
border-color: var(--spectrum-global-color-gray-300);
|
||||
overflow: auto;
|
||||
}
|
||||
.spectrum-Popover.is-open.spectrum-Popover--withTip {
|
||||
margin-top: var(--spacing-xs);
|
||||
margin-left: var(--spacing-xl);
|
||||
}
|
||||
:global(.spectrum-Popover--bottom .spectrum-Popover-tip),
|
||||
:global(.spectrum-Popover--top .spectrum-Popover-tip) {
|
||||
left: 90%;
|
||||
margin-left: calc(var(--spectrum-global-dimension-size-150) * -1);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -182,3 +182,9 @@
|
|||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
padding-top: 3px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
|
||||
<style>
|
||||
img {
|
||||
padding-top: 1px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -29,6 +29,18 @@
|
|||
: BUDIBASE_INTERNAL_DB_ID
|
||||
|
||||
export let name
|
||||
export let beforeSave = async () => {}
|
||||
export let afterSave = async table => {
|
||||
notifications.success(`Table ${name} created successfully.`)
|
||||
|
||||
// Navigate to new table
|
||||
const currentUrl = $url()
|
||||
const path = currentUrl.endsWith("data")
|
||||
? `./table/${table._id}`
|
||||
: `../../table/${table._id}`
|
||||
$goto(path)
|
||||
}
|
||||
|
||||
let error = ""
|
||||
let autoColumns = getAutoColumnInformation()
|
||||
let schema = {}
|
||||
|
@ -78,15 +90,9 @@
|
|||
// Create table
|
||||
let table
|
||||
try {
|
||||
await beforeSave()
|
||||
table = await tables.save(newTable)
|
||||
notifications.success(`Table ${name} created successfully.`)
|
||||
|
||||
// Navigate to new table
|
||||
const currentUrl = $url()
|
||||
const path = currentUrl.endsWith("data")
|
||||
? `./table/${table._id}`
|
||||
: `../../table/${table._id}`
|
||||
$goto(path)
|
||||
await afterSave(table)
|
||||
} catch (e) {
|
||||
notifications.error(e)
|
||||
// reload in case the table was created
|
||||
|
|
|
@ -6,10 +6,22 @@
|
|||
faBook,
|
||||
faPlay,
|
||||
faLock,
|
||||
faFileArrowUp,
|
||||
faChevronLeft,
|
||||
} from "@fortawesome/free-solid-svg-icons"
|
||||
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
||||
|
||||
library.add(faXmark, faBook, faPlay, faLock, faGithub, faDiscord, faEnvelope)
|
||||
library.add(
|
||||
faXmark,
|
||||
faBook,
|
||||
faPlay,
|
||||
faLock,
|
||||
faGithub,
|
||||
faDiscord,
|
||||
faEnvelope,
|
||||
faFileArrowUp,
|
||||
faChevronLeft
|
||||
)
|
||||
dom.watch()
|
||||
</script>
|
||||
|
||||
|
@ -24,8 +36,6 @@
|
|||
<style>
|
||||
span {
|
||||
display: contents;
|
||||
}
|
||||
.svg-inline--fa {
|
||||
color: var(--spectrum-body-m-text-color, var(--spectrum-alias-text-color));
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -143,11 +143,11 @@
|
|||
display: flex;
|
||||
padding: 12px;
|
||||
align-items: center;
|
||||
transition: filter 0.5s;
|
||||
transition: filter 0.5s, background 0.13s ease-out;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: var(--spectrum-global-color-gray-300);
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
|
||||
a:last-child {
|
||||
|
@ -168,14 +168,14 @@
|
|||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 70%;
|
||||
opacity: 60%;
|
||||
}
|
||||
|
||||
.premiumBadge {
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
border: var(--border-light);
|
||||
border: 1px solid var(--spectrum-global-color-gray-400);
|
||||
border-radius: 4px;
|
||||
padding: 4px 7px 5px 8px;
|
||||
}
|
||||
|
|
|
@ -179,7 +179,7 @@
|
|||
|
||||
<span class="detailPopover">
|
||||
<Popover
|
||||
align="right-side"
|
||||
align="right-outside"
|
||||
bind:this={popover}
|
||||
anchor={popoverAnchor}
|
||||
maxWidth={300}
|
||||
|
|
|
@ -125,7 +125,6 @@
|
|||
align="right"
|
||||
disabled={!isPublished}
|
||||
dataCy="publish-popover-menu"
|
||||
showTip={true}
|
||||
anchor={publishPopoverAnchor}
|
||||
>
|
||||
<Layout gap="M">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { isActive, redirect, goto, url } from "@roxi/routify"
|
||||
import { Icon, notifications, Tabs, Tab } from "@budibase/bbui"
|
||||
import { organisation, auth, menu } from "stores/portal"
|
||||
import { organisation, auth, menu, apps } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import UpgradeButton from "./_components/UpgradeButton.svelte"
|
||||
import MobileMenu from "./_components/MobileMenu.svelte"
|
||||
|
@ -14,6 +14,7 @@
|
|||
let activeTab = "Apps"
|
||||
|
||||
$: $url(), updateActiveTab($menu)
|
||||
$: fullScreen = !$apps?.length
|
||||
|
||||
const updateActiveTab = menu => {
|
||||
for (let entry of menu) {
|
||||
|
@ -46,7 +47,9 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
{#if $auth.user && loaded}
|
||||
{#if fullScreen}
|
||||
<slot />
|
||||
{:else if $auth.user && loaded}
|
||||
<HelpMenu />
|
||||
<div class="container">
|
||||
<div class="nav">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { notifications } from "@budibase/bbui"
|
||||
import { apps, templates, licensing, groups } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
// Don't block loading if we've already hydrated state
|
||||
let loaded = $apps.length > 0
|
||||
|
@ -24,7 +24,7 @@
|
|||
|
||||
// Go to new app page if no apps exists
|
||||
if (!$apps.length) {
|
||||
$goto("./create")
|
||||
$redirect("./onboarding")
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error loading apps and templates")
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { url } from "@roxi/routify"
|
||||
import FirstAppOnboarding from "./onboarding/index.svelte"
|
||||
import { Layout, Page, Button, Modal } from "@budibase/bbui"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||
|
@ -38,42 +39,45 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Page>
|
||||
<Layout noPadding gap="L">
|
||||
<Breadcrumbs>
|
||||
<Breadcrumb url={$url("./")} text="Apps" />
|
||||
<Breadcrumb text="Create new app" />
|
||||
</Breadcrumbs>
|
||||
<Header title={$apps.length ? "Create new app" : "Create your first app"}>
|
||||
<div slot="buttons">
|
||||
<Button
|
||||
dataCy="import-app-btn"
|
||||
size="M"
|
||||
secondary
|
||||
on:click={initiateAppImport}
|
||||
>
|
||||
Import app
|
||||
</Button>
|
||||
<Button
|
||||
dataCy="create-app-btn"
|
||||
size="M"
|
||||
cta
|
||||
on:click={initiateAppCreation}
|
||||
>
|
||||
Start from scratch
|
||||
</Button>
|
||||
</div>
|
||||
</Header>
|
||||
<TemplateDisplay templates={$templates} />
|
||||
</Layout>
|
||||
</Page>
|
||||
|
||||
<Modal
|
||||
bind:this={creationModal}
|
||||
padding={false}
|
||||
width="600px"
|
||||
on:hide={stopAppCreation}
|
||||
>
|
||||
<CreateAppModal {template} />
|
||||
</Modal>
|
||||
<AppLimitModal bind:this={appLimitModal} />
|
||||
{#if !$apps.length}
|
||||
<FirstAppOnboarding />
|
||||
{:else}
|
||||
<Page>
|
||||
<Layout noPadding gap="L">
|
||||
<Breadcrumbs>
|
||||
<Breadcrumb url={$url("./")} text="Apps" />
|
||||
<Breadcrumb text="Create new app" />
|
||||
</Breadcrumbs>
|
||||
<Header title={"Create new app"}>
|
||||
<div slot="buttons">
|
||||
<Button
|
||||
dataCy="import-app-btn"
|
||||
size="M"
|
||||
secondary
|
||||
on:click={initiateAppImport}
|
||||
>
|
||||
Import app
|
||||
</Button>
|
||||
<Button
|
||||
dataCy="create-app-btn"
|
||||
size="M"
|
||||
cta
|
||||
on:click={initiateAppCreation}
|
||||
>
|
||||
Start from scratch
|
||||
</Button>
|
||||
</div>
|
||||
</Header>
|
||||
<TemplateDisplay templates={$templates} />
|
||||
</Layout>
|
||||
</Page>
|
||||
<Modal
|
||||
bind:this={creationModal}
|
||||
padding={false}
|
||||
width="600px"
|
||||
on:hide={stopAppCreation}
|
||||
>
|
||||
<CreateAppModal {template} />
|
||||
</Modal>
|
||||
<AppLimitModal bind:this={appLimitModal} />
|
||||
{/if}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<script>
|
||||
import PanelHeader from "./PanelHeader.svelte"
|
||||
export let onBack = () => {}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PanelHeader
|
||||
title="Give it some data"
|
||||
subtitle="Not ready to add yours? Get started with sample data!"
|
||||
{onBack}
|
||||
/>
|
||||
<slot />
|
||||
</div>
|
|
@ -0,0 +1,109 @@
|
|||
<script>
|
||||
import { Button, FancyForm, FancyInput, FancyCheckbox } from "@budibase/bbui"
|
||||
import { capitalise } from "helpers/helpers"
|
||||
import PanelHeader from "./PanelHeader.svelte"
|
||||
|
||||
export let title = ""
|
||||
export let onBack = null
|
||||
export let onNext = () => {}
|
||||
export let fields = {}
|
||||
|
||||
let errors = {}
|
||||
|
||||
const formatName = name => {
|
||||
if (name === "ca") {
|
||||
return "CA"
|
||||
}
|
||||
|
||||
if (name === "ssl") {
|
||||
return "SSL"
|
||||
}
|
||||
|
||||
if (name === "rejectUnauthorized") {
|
||||
return "Reject Unauthorized"
|
||||
}
|
||||
|
||||
return capitalise(name)
|
||||
}
|
||||
|
||||
const getDefaultValues = fields => {
|
||||
const newValues = {}
|
||||
|
||||
Object.entries(fields).forEach(([name, { default: defaultValue }]) => {
|
||||
if (defaultValue) {
|
||||
newValues[name] = defaultValue
|
||||
}
|
||||
})
|
||||
|
||||
return newValues
|
||||
}
|
||||
|
||||
const values = getDefaultValues(fields)
|
||||
|
||||
const validateRequired = value => {
|
||||
if (value.length < 1) {
|
||||
return "Required field"
|
||||
}
|
||||
}
|
||||
|
||||
const getIsValid = (fields, errors, values) => {
|
||||
for (const [name, { required }] of Object.entries(fields)) {
|
||||
if (required && !values[name]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(errors).every(error => !error)
|
||||
}
|
||||
|
||||
$: isValid = getIsValid(fields, errors, values)
|
||||
|
||||
const handleNext = () => {
|
||||
const parsedValues = {}
|
||||
|
||||
Object.entries(values).forEach(([name, value]) => {
|
||||
if (fields[name].type === "number") {
|
||||
parsedValues[name] = parseInt(value, 10)
|
||||
} else {
|
||||
parsedValues[name] = value
|
||||
}
|
||||
})
|
||||
|
||||
return onNext(parsedValues)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PanelHeader
|
||||
{title}
|
||||
subtitle="Fill in the required fields to fetch your tables"
|
||||
{onBack}
|
||||
/>
|
||||
<div class="form">
|
||||
<FancyForm>
|
||||
{#each Object.entries(fields) as [name, { type, default: defaultValue, required }]}
|
||||
{#if type !== "boolean"}
|
||||
<FancyInput
|
||||
bind:value={values[name]}
|
||||
bind:error={errors[name]}
|
||||
validate={required ? validateRequired : () => {}}
|
||||
label={formatName(name)}
|
||||
{type}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{#each Object.entries(fields) as [name, { type, default: defaultValue, required }]}
|
||||
{#if type === "boolean"}
|
||||
<FancyCheckbox bind:value={values[name]} text={formatName(name)} />
|
||||
{/if}
|
||||
{/each}
|
||||
</FancyForm>
|
||||
</div>
|
||||
<Button cta disabled={!isValid} on:click={handleNext}>Connect</Button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,254 @@
|
|||
<script>
|
||||
export let name = ""
|
||||
export let showData = false
|
||||
|
||||
const rows = [
|
||||
{
|
||||
firstName: "Julie",
|
||||
lastName: "Jimenez",
|
||||
email: "julie.jimenez@example.com",
|
||||
address: "4250 New Street",
|
||||
city: "Stevenage",
|
||||
postcode: "EE32 3SE",
|
||||
phone: "01754 13523",
|
||||
},
|
||||
{
|
||||
firstName: "Mandy",
|
||||
lastName: "Clark",
|
||||
email: "mandy.clark@example.com",
|
||||
address: "8632 North Street",
|
||||
city: "Hereford",
|
||||
postcode: "GT81 7DG",
|
||||
phone: "016973 32814",
|
||||
},
|
||||
{
|
||||
firstName: "Holly",
|
||||
lastName: "Carroll",
|
||||
email: "holly.carroll@example.com",
|
||||
address: "5976 Springfield Road",
|
||||
city: "Edinburgh",
|
||||
postcode: "Y4 2LH",
|
||||
phone: "016977 73053",
|
||||
},
|
||||
{
|
||||
firstName: "Francis",
|
||||
lastName: "Castro",
|
||||
email: "francis.castro@example.com",
|
||||
address: "3970 High Street",
|
||||
city: "Wells",
|
||||
postcode: "X12 6QA",
|
||||
phone: "017684 23551",
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<div tabindex="-1" class="exampleApp">
|
||||
<div class="page">
|
||||
<div class="header">
|
||||
<img alt="Budibase Logo" src={"https://i.imgur.com/Xhdt1YP.png"} />
|
||||
<h1>{name}</h1>
|
||||
</div>
|
||||
<div class="nav">Home</div>
|
||||
<table class={`table ${showData ? "tableVisible" : ""}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FIRST NAME</th>
|
||||
<th>LAST NAME</th>
|
||||
<th>EMAIL</th>
|
||||
<th>ADDRESS</th>
|
||||
<th>CITY</th>
|
||||
<th>POSTCODE</th>
|
||||
<th>PHONE</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each rows as row}
|
||||
<tr>
|
||||
{#each Object.values(row) as value}
|
||||
<td>{value}</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class={`sidePanel ${showData ? "sidePanelVisible" : ""}`}>
|
||||
<h2>{rows[0].firstName}</h2>
|
||||
<div class="field">
|
||||
<label for="exampleLastName">lastName</label>
|
||||
<input
|
||||
id="exampleLastName"
|
||||
tabIndex="-1"
|
||||
readonly
|
||||
value={rows[0].lastName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="exampleEmail">Email</label>
|
||||
<input id="exampleEmail" tabIndex="-1" readonly value={rows[0].email} />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="exampleAddress">Address</label>
|
||||
<input
|
||||
id="exampleAddress"
|
||||
tabIndex="-1"
|
||||
readonly
|
||||
value={rows[0].address}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="exampleCity">City</label>
|
||||
<input id="exampleCity" tabIndex="-1" readonly value={rows[0].city} />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="examplePostcode">Postcode</label>
|
||||
<input
|
||||
id="examplePostcode"
|
||||
tabIndex="-1"
|
||||
readonly
|
||||
value={rows[0].postcode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="examplePhone">Phone</label>
|
||||
<input id="examplePhone" tabIndex="-1" readonly value={rows[0].phone} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.exampleApp {
|
||||
box-sizing: border-box;
|
||||
height: 100vh;
|
||||
padding: 100px 0 100px 100px;
|
||||
--text: #191919;
|
||||
--lightText: #303030;
|
||||
--extraLightText: #646464;
|
||||
--backgroundLight: #ffffff;
|
||||
--background: #f5f5f5;
|
||||
--tableBorder: 1px solid #e6e6e6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.page {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-color: var(--background);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: var(--backgroundLight);
|
||||
display: flex;
|
||||
padding: 32px 0 20px 32px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header img {
|
||||
height: 36px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
background-color: var(--backgroundLight);
|
||||
padding: 20px 0 20px 32px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
border-bottom: 1px solid #d0d0d0;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 32px;
|
||||
border: var(--tableBorder);
|
||||
border-collapse: collapse;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
thead {
|
||||
border-bottom: var(--tableBorder);
|
||||
}
|
||||
|
||||
thead th {
|
||||
font-family: "Source Sans Pro";
|
||||
color: var(--lightText);
|
||||
white-space: nowrap;
|
||||
padding: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
tbody td {
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
background-color: var(--backgroundLight);
|
||||
padding: 12px;
|
||||
color: var(--extraLightText);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tableVisible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidePanel {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
background-color: var(--backgroundLight);
|
||||
box-shadow: 0px 4px 25px 0px #00000040;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: -364px;
|
||||
padding: 42px 32px;
|
||||
}
|
||||
|
||||
.sidePanelVisible {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sidePanel h2 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: #b0b0b0;
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
.field input {
|
||||
border: 1px solid #d0d0d0;
|
||||
border-radius: 4px;
|
||||
color: var(--lightText);
|
||||
padding: 7.5px 12px;
|
||||
font-size: 13px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import { Button, FancyForm, FancyInput } from "@budibase/bbui"
|
||||
import PanelHeader from "./PanelHeader.svelte"
|
||||
|
||||
export let name = ""
|
||||
export let url = ""
|
||||
export let onNext = () => {}
|
||||
let nameError = null
|
||||
let urlError = null
|
||||
|
||||
$: isValid = name.length && url.length && !nameError && !urlError
|
||||
|
||||
const validateName = name => {
|
||||
if (name.length < 1) {
|
||||
return "Name must be provided"
|
||||
}
|
||||
}
|
||||
|
||||
const validateUrl = url => {
|
||||
if (url.length < 1) {
|
||||
return "URL must be provided"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PanelHeader
|
||||
title="Build your first app"
|
||||
subtitle="Name your app and set the URL"
|
||||
/>
|
||||
<FancyForm>
|
||||
<FancyInput
|
||||
bind:value={name}
|
||||
bind:error={nameError}
|
||||
validate={validateName}
|
||||
label="Name"
|
||||
/>
|
||||
<FancyInput
|
||||
bind:value={url}
|
||||
bind:error={urlError}
|
||||
validate={validateUrl}
|
||||
label="URL"
|
||||
/>
|
||||
</FancyForm>
|
||||
{#if url}
|
||||
<p><span class="host">{window.location.origin}/app/</span>{url}</p>
|
||||
{:else}
|
||||
<p></p>
|
||||
{/if}
|
||||
<Button size="L" cta disabled={!isValid} on:click={onNext}>Lets go!</Button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
p {
|
||||
color: white;
|
||||
}
|
||||
.host {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,49 @@
|
|||
<script>
|
||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||
import { Heading, Body } from "@budibase/bbui"
|
||||
|
||||
export let onBack = null
|
||||
export let title = ""
|
||||
export let subtitle = ""
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<img
|
||||
alt="Budibase Logo"
|
||||
class="budibaseLogo"
|
||||
src={"https://i.imgur.com/Xhdt1YP.png"}
|
||||
/>
|
||||
<div class="headingAndBack">
|
||||
{#if onBack}
|
||||
<button on:click={onBack}>
|
||||
<FontAwesomeIcon name="fa-solid fa-chevron-left" />
|
||||
</button>
|
||||
{/if}
|
||||
<Heading>{title}</Heading>
|
||||
</div>
|
||||
<Body>{subtitle}</Body>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.budibaseLogo {
|
||||
width: 42px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.headingAndBack {
|
||||
display: flex;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
padding-right: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,202 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import NamePanel from "./_components/NamePanel.svelte"
|
||||
import DataPanel from "./_components/DataPanel.svelte"
|
||||
import DatasourceConfigPanel from "./_components/DatasourceConfigPanel.svelte"
|
||||
import ExampleApp from "./_components/ExampleApp.svelte"
|
||||
import { FancyButton, notifications, Modal } from "@budibase/bbui"
|
||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||
import { SplitPage } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { saveDatasource } from "builderStore/datasource"
|
||||
import { integrations } from "stores/backend"
|
||||
import { auth, admin } from "stores/portal"
|
||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
||||
import { Roles } from "constants/backend"
|
||||
|
||||
let name = ""
|
||||
let url = ""
|
||||
let stage = "name"
|
||||
let appId = null
|
||||
|
||||
let plusIntegrations = {}
|
||||
let integrationsLoading = true
|
||||
$: getIntegrations()
|
||||
|
||||
let uploadModal
|
||||
|
||||
const createApp = async useSampleData => {
|
||||
// Create form data to create app
|
||||
// This is form based and not JSON
|
||||
let data = new FormData()
|
||||
data.append("name", name.trim())
|
||||
data.append("url", url.trim())
|
||||
data.append("useTemplate", false)
|
||||
|
||||
if (useSampleData) {
|
||||
data.append("sampleData", true)
|
||||
}
|
||||
|
||||
const createdApp = await API.createApp(data)
|
||||
|
||||
// Select Correct Application/DB in prep for creating user
|
||||
const pkg = await API.fetchAppPackage(createdApp.instance._id)
|
||||
await store.actions.initialise(pkg)
|
||||
await automationStore.actions.fetch()
|
||||
// Update checklist - in case first app
|
||||
await admin.init()
|
||||
|
||||
// Create user
|
||||
await auth.setInitInfo({})
|
||||
|
||||
let defaultScreenTemplate = createFromScratchScreen.create()
|
||||
defaultScreenTemplate.routing.route = "/home"
|
||||
defaultScreenTemplate.routing.roldId = Roles.BASIC
|
||||
await store.actions.screens.save(defaultScreenTemplate)
|
||||
|
||||
return createdApp.instance._id
|
||||
}
|
||||
|
||||
const getIntegrations = async () => {
|
||||
try {
|
||||
await integrations.init()
|
||||
const newPlusIntegrations = {}
|
||||
|
||||
Object.entries($integrations).forEach(([integrationType, schema]) => {
|
||||
if (schema?.plus) {
|
||||
newPlusIntegrations[integrationType] = schema
|
||||
}
|
||||
})
|
||||
|
||||
plusIntegrations = newPlusIntegrations
|
||||
} catch (e) {
|
||||
notifications.error("There was a problem communicating with the server.")
|
||||
} finally {
|
||||
integrationsLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
const goToApp = appId => {
|
||||
$goto(`/builder/app/${appId}`)
|
||||
notifications.success(`App created successfully`)
|
||||
}
|
||||
|
||||
const handleCreateApp = async ({ datasourceConfig, useSampleData }) => {
|
||||
try {
|
||||
appId = await createApp(useSampleData)
|
||||
|
||||
if (datasourceConfig) {
|
||||
await saveDatasource({
|
||||
plus: true,
|
||||
auth: undefined,
|
||||
name: plusIntegrations[stage].friendlyName,
|
||||
schema: plusIntegrations[stage].datasource,
|
||||
config: datasourceConfig,
|
||||
type: stage,
|
||||
})
|
||||
}
|
||||
|
||||
goToApp(appId)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notifications.error("There was a problem creating your app")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={uploadModal}>
|
||||
<CreateTableModal
|
||||
name="Your Data"
|
||||
beforeSave={createApp}
|
||||
afterSave={() => goToApp(appId)}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<SplitPage>
|
||||
{#if stage === "name"}
|
||||
<NamePanel bind:name bind:url onNext={() => (stage = "data")} />
|
||||
{:else if integrationsLoading}
|
||||
<p>loading...</p>
|
||||
{:else if stage === "data"}
|
||||
<DataPanel onBack={() => (stage = "name")}>
|
||||
<div class="dataButton">
|
||||
<FancyButton on:click={() => handleCreateApp({ useSampleData: true })}>
|
||||
<div class="dataButtonContent">
|
||||
<div class="dataButtonIcon">
|
||||
<img
|
||||
alt="Budibase Logo"
|
||||
class="budibaseLogo"
|
||||
src={"https://i.imgur.com/Xhdt1YP.png"}
|
||||
/>
|
||||
</div>
|
||||
Budibase Sample data
|
||||
</div>
|
||||
</FancyButton>
|
||||
</div>
|
||||
<div class="dataButton">
|
||||
<FancyButton on:click={uploadModal.show}>
|
||||
<div class="dataButtonContent">
|
||||
<div class="dataButtonIcon">
|
||||
<FontAwesomeIcon name="fa-solid fa-file-arrow-up" />
|
||||
</div>
|
||||
Upload file
|
||||
</div>
|
||||
</FancyButton>
|
||||
</div>
|
||||
{#each Object.entries(plusIntegrations) as [integrationType, schema]}
|
||||
<div class="dataButton">
|
||||
<FancyButton on:click={() => (stage = integrationType)}>
|
||||
<div class="dataButtonContent">
|
||||
<div class="dataButtonIcon">
|
||||
<IntegrationIcon {integrationType} {schema} />
|
||||
</div>
|
||||
{schema.friendlyName}
|
||||
</div>
|
||||
</FancyButton>
|
||||
</div>
|
||||
{/each}
|
||||
</DataPanel>
|
||||
{:else if stage in plusIntegrations}
|
||||
<DatasourceConfigPanel
|
||||
title={plusIntegrations[stage].friendlyName}
|
||||
fields={plusIntegrations[stage].datasource}
|
||||
onBack={() => (stage = "data")}
|
||||
onNext={data => handleCreateApp({ datasourceConfig: data })}
|
||||
/>
|
||||
{:else}
|
||||
<p>There was an problem. Please refresh the page and try again.</p>
|
||||
{/if}
|
||||
<div slot="right">
|
||||
<ExampleApp {name} showData={stage !== "name"} />
|
||||
</div>
|
||||
</SplitPage>
|
||||
|
||||
<style>
|
||||
.dataButton {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.dataButtonContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.budibaseLogo {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.dataButtonIcon {
|
||||
width: 22px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.dataButtonContent :global(svg) {
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
|
@ -1,9 +1,16 @@
|
|||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import replace from "@rollup/plugin-replace"
|
||||
import { defineConfig, loadEnv } from "vite"
|
||||
|
||||
import path from "path"
|
||||
|
||||
const ignoredWarnings = [
|
||||
"unused-export-let",
|
||||
"css-unused-selector",
|
||||
"module-script-reactive-declaration",
|
||||
"a11y-no-onchange",
|
||||
"a11y-click-events-have-key-events",
|
||||
]
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const isProduction = mode === "production"
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
|
@ -29,6 +36,12 @@ export default defineConfig(({ mode }) => {
|
|||
svelte({
|
||||
hot: !isProduction,
|
||||
emitCss: true,
|
||||
onwarn: (warning, handler) => {
|
||||
// Ignore some warnings
|
||||
if (!ignoredWarnings.includes(warning.code)) {
|
||||
handler(warning)
|
||||
}
|
||||
},
|
||||
}),
|
||||
replace({
|
||||
preventAssignment: true,
|
||||
|
|
|
@ -17,6 +17,7 @@ const ignoredWarnings = [
|
|||
"css-unused-selector",
|
||||
"module-script-reactive-declaration",
|
||||
"a11y-no-onchange",
|
||||
"a11y-click-events-have-key-events",
|
||||
]
|
||||
|
||||
export default {
|
||||
|
@ -88,7 +89,9 @@ export default {
|
|||
dedupe: ["svelte", "svelte/internal"],
|
||||
}),
|
||||
svg(),
|
||||
image(),
|
||||
image({
|
||||
exclude: "**/*.svg",
|
||||
}),
|
||||
json(),
|
||||
production && terser(),
|
||||
!production && visualizer(),
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="right spectrum spectrum--darkest">
|
||||
<img src={BG} alt="background" />
|
||||
<!-- No alt attribute to avoid flash -->
|
||||
<img src={BG} alt=" " />
|
||||
<slot name="right" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -30,6 +31,7 @@
|
|||
overflow-y: auto;
|
||||
}
|
||||
.right {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.right img {
|
||||
|
|
Loading…
Reference in New Issue