App Preview Skeleton (#12898)

* wip

* wip

* wip

* wip

* wip

* wip

* builder design skeleton

* pre doc loading skeleton for portal preview

* feature complete?

* wip

* linting

* linting

* linting

* fix ts error?

* ts types fix

* linting

* remove unnecessary build stuff

* include skeleton in server app directly

* linting

* linting

* fix builder preview aspect ratio for skeleton

* remove dev tools skeleton

* linting
This commit is contained in:
Gerard Burns 2024-02-27 15:29:35 +00:00 committed by GitHub
parent 2a8bb872ad
commit 9a9b745795
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 563 additions and 152 deletions

View File

@ -12,17 +12,11 @@
hoverStore, hoverStore,
} from "stores/builder" } from "stores/builder"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { import { Layout, Heading, Body, Icon, notifications } from "@budibase/bbui"
ProgressCircle,
Layout,
Heading,
Body,
Icon,
notifications,
} from "@budibase/bbui"
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw" import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
import { findComponent, findComponentPath } from "helpers/components" import { findComponent, findComponentPath } from "helpers/components"
import { isActive, goto } from "@roxi/routify" import { isActive, goto } from "@roxi/routify"
import { ClientAppSkeleton } from "@budibase/frontend-core"
let iframe let iframe
let layout let layout
@ -240,8 +234,16 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="component-container"> <div class="component-container">
{#if loading} {#if loading}
<div class="center"> <div
<ProgressCircle /> class={`loading ${$builderStore.theme}`}
class:tablet={$previewStore.previewDevice === "tablet"}
class:mobile={$previewStore.previewDevice === "mobile"}
>
<ClientAppSkeleton
sideNav={$builderStore.navigation?.navigation === "Left"}
hideFooter
hideDevTools
/>
</div> </div>
{:else if error} {:else if error}
<div class="center error"> <div class="center error">
@ -258,8 +260,6 @@
bind:this={iframe} bind:this={iframe}
src="/app/preview" src="/app/preview"
class:hidden={loading || error} class:hidden={loading || error}
class:tablet={$previewStore.previewDevice === "tablet"}
class:mobile={$previewStore.previewDevice === "mobile"}
/> />
<div <div
class="add-component" class="add-component"
@ -279,6 +279,25 @@
/> />
<style> <style>
.loading {
position: absolute;
container-type: inline-size;
width: 100%;
height: 100%;
border: 2px solid transparent;
box-sizing: border-box;
}
.loading.tablet {
width: calc(1024px + 6px);
max-height: calc(768px + 6px);
}
.loading.mobile {
width: calc(390px + 6px);
max-height: calc(844px + 6px);
}
.component-container { .component-container {
grid-row-start: middle; grid-row-start: middle;
grid-column-start: middle; grid-column-start: middle;

View File

@ -1,16 +1,22 @@
<script> <script>
import { onMount, onDestroy } from "svelte"
import { params, goto } from "@roxi/routify" import { params, goto } from "@roxi/routify"
import { apps, auth, sideBarCollapsed } from "stores/portal" import { licensing, apps, auth, sideBarCollapsed } from "stores/portal"
import { Link, Body, ActionButton } from "@budibase/bbui" import { Link, Body, ActionButton } from "@budibase/bbui"
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { API } from "api" import { API } from "api"
import ErrorSVG from "./ErrorSVG.svelte" import ErrorSVG from "./ErrorSVG.svelte"
import { ClientAppSkeleton } from "@budibase/frontend-core"
$: app = $apps.find(app => app.appId === $params.appId) $: app = $apps.find(app => app.appId === $params.appId)
$: iframeUrl = getIframeURL(app) $: iframeUrl = getIframeURL(app)
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId) $: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
let loading = true
const getIframeURL = app => { const getIframeURL = app => {
loading = true
if (app.status === "published") { if (app.status === "published") {
return `/app${app.url}` return `/app${app.url}`
} }
@ -28,6 +34,20 @@
} }
$: fetchScreens(app?.devId) $: fetchScreens(app?.devId)
const receiveMessage = async message => {
if (message.data.type === "docLoaded") {
loading = false
}
}
onMount(() => {
window.addEventListener("message", receiveMessage)
})
onDestroy(() => {
window.removeEventListener("message", receiveMessage)
})
</script> </script>
<div class="container"> <div class="container">
@ -78,7 +98,17 @@
</Body> </Body>
</div> </div>
{:else} {:else}
<iframe src={iframeUrl} title={app.name} /> <div class:hide={!loading} class="loading">
<div class={`loadingThemeWrapper ${app.theme}`}>
<ClientAppSkeleton
noAnimation
hideDevTools={app?.status === "published"}
sideNav={app?.navigation.navigation === "Left"}
hideFooter={$licensing.brandingEnabled}
/>
</div>
</div>
<iframe class:hide={loading} src={iframeUrl} title={app.name} />
{/if} {/if}
</div> </div>
@ -100,6 +130,23 @@
flex: 0 0 50px; flex: 0 0 50px;
} }
.loading {
height: 100%;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: var(--spacing-s);
overflow: hidden;
}
.loadingThemeWrapper {
height: 100%;
container-type: inline-size;
}
.hide {
visibility: hidden;
height: 0;
border: none;
}
iframe { iframe {
flex: 1 1 auto; flex: 1 1 auto;
border-radius: var(--spacing-s); border-radius: var(--spacing-s);

View File

@ -80,11 +80,18 @@
} }
} }
let fontsLoaded = false
// Load app config // Load app config
onMount(async () => { onMount(async () => {
document.fonts.ready.then(() => {
fontsLoaded = true
})
await initialise() await initialise()
await authStore.actions.fetchUser() await authStore.actions.fetchUser()
dataLoaded = true dataLoaded = true
if (get(builderStore).inBuilder) { if (get(builderStore).inBuilder) {
builderStore.actions.notifyLoaded() builderStore.actions.notifyLoaded()
} else { } else {
@ -93,6 +100,12 @@
}) })
} }
}) })
$: {
if (dataLoaded && fontsLoaded) {
document.getElementById("clientAppSkeletonLoader")?.remove()
}
}
</script> </script>
<svelte:head> <svelte:head>
@ -103,140 +116,140 @@
{/if} {/if}
</svelte:head> </svelte:head>
{#if dataLoaded} <div
<div id="spectrum-root"
id="spectrum-root" lang="en"
lang="en" dir="ltr"
dir="ltr" class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}" class:builder={$builderStore.inBuilder}
class:builder={$builderStore.inBuilder} class:show={fontsLoaded && dataLoaded}
> >
<DeviceBindingsProvider> <DeviceBindingsProvider>
<UserBindingsProvider> <UserBindingsProvider>
<StateBindingsProvider> <StateBindingsProvider>
<RowSelectionProvider> <RowSelectionProvider>
<QueryParamsProvider> <QueryParamsProvider>
<!-- Settings bar can be rendered outside of device preview --> <!-- Settings bar can be rendered outside of device preview -->
<!-- Key block needs to be outside the if statement or it breaks --> <!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId} {#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder} {#if $builderStore.inBuilder}
<SettingsBar /> <SettingsBar />
{/if} {/if}
{/key} {/key}
<!-- Clip boundary for selection indicators --> <!-- Clip boundary for selection indicators -->
<div <div
id="clip-root" id="clip-root"
class:preview={$builderStore.inBuilder} class:preview={$builderStore.inBuilder}
class:tablet-preview={$builderStore.previewDevice === "tablet"} class:tablet-preview={$builderStore.previewDevice === "tablet"}
class:mobile-preview={$builderStore.previewDevice === "mobile"} class:mobile-preview={$builderStore.previewDevice === "mobile"}
> >
<!-- Actual app --> <!-- Actual app -->
<div id="app-root"> <div id="app-root">
{#if showDevTools} {#if showDevTools}
<DevToolsHeader /> <DevToolsHeader />
{/if}
<div id="app-body">
{#if permissionError}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
You don't have permission to use this app
</Heading>
<Body size="S">
Ask your administrator to grant you access
</Body>
</Layout>
</div>
{:else if !$screenStore.activeLayout}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
Something went wrong rendering your app
</Heading>
<Body size="S">
Get in touch with support if this issue persists
</Body>
</Layout>
</div>
{:else if embedNoScreens}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
This Budibase app is not publicly accessible
</Heading>
</Layout>
</div>
{:else}
<CustomThemeWrapper>
{#key $screenStore.activeLayout._id}
<Component
isLayout
instance={$screenStore.activeLayout.props}
/>
{/key}
<!--
Flatpickr needs to be inside the theme wrapper.
It also needs its own container because otherwise it hijacks
key events on the whole page. It is painful to work with.
-->
<div id="flatpickr-root" />
<!-- Modal container to ensure they sit on top -->
<div class="modal-container" />
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
</CustomThemeWrapper>
{/if} {/if}
<div id="app-body"> {#if showDevTools}
{#if permissionError} <DevTools />
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
You don't have permission to use this app
</Heading>
<Body size="S">
Ask your administrator to grant you access
</Body>
</Layout>
</div>
{:else if !$screenStore.activeLayout}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
Something went wrong rendering your app
</Heading>
<Body size="S">
Get in touch with support if this issue persists
</Body>
</Layout>
</div>
{:else if embedNoScreens}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
This Budibase app is not publicly accessible
</Heading>
</Layout>
</div>
{:else}
<CustomThemeWrapper>
{#key $screenStore.activeLayout._id}
<Component
isLayout
instance={$screenStore.activeLayout.props}
/>
{/key}
<!--
Flatpickr needs to be inside the theme wrapper.
It also needs its own container because otherwise it hijacks
key events on the whole page. It is painful to work with.
-->
<div id="flatpickr-root" />
<!-- Modal container to ensure they sit on top -->
<div class="modal-container" />
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
</CustomThemeWrapper>
{/if}
{#if showDevTools}
<DevTools />
{/if}
</div>
{#if !$builderStore.inBuilder && licensing.logoEnabled()}
<FreeFooter />
{/if} {/if}
</div> </div>
<!-- Preview and dev tools utilities --> {#if !$builderStore.inBuilder && licensing.logoEnabled()}
{#if $appStore.isDevApp} <FreeFooter />
<SelectionIndicator />
{/if}
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
<HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
{/if} {/if}
</div> </div>
</QueryParamsProvider>
</RowSelectionProvider> <!-- Preview and dev tools utilities -->
</StateBindingsProvider> {#if $appStore.isDevApp}
</UserBindingsProvider> <SelectionIndicator />
</DeviceBindingsProvider> {/if}
</div> {#if $builderStore.inBuilder || $devToolsStore.allowSelection}
<KeyboardManager /> <HoverIndicator />
{/if} {/if}
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
{/if}
</div>
</QueryParamsProvider>
</RowSelectionProvider>
</StateBindingsProvider>
</UserBindingsProvider>
</DeviceBindingsProvider>
</div>
<KeyboardManager />
<style> <style>
#spectrum-root { #spectrum-root {
height: 0;
visibility: hidden;
padding: 0; padding: 0;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -257,6 +270,11 @@
background-color: transparent; background-color: transparent;
} }
#spectrum-root.show {
height: 100%;
visibility: visible;
}
#app-root { #app-root {
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;

View File

@ -13,6 +13,7 @@
<style> <style>
.free-footer { .free-footer {
min-height: 51px;
flex: 0 0 auto; flex: 0 0 auto;
padding: 16px 20px; padding: 16px 20px;
border-top: 1px solid var(--spectrum-global-color-gray-300); border-top: 1px solid var(--spectrum-global-color-gray-300);

View File

@ -0,0 +1,244 @@
<script>
export let sideNav = false
export let hideDevTools = false
export let hideFooter = false
export let noAnimation = false
</script>
<div class:sideNav id="clientAppSkeletonLoader" class="skeleton">
<div class="animation" class:noAnimation />
{#if !hideDevTools}
<div class="devTools" />
{/if}
<div class="main">
<div class="nav" />
<div class="body">
<div class="bodyVerticalPadding" />
<div class="bodyHorizontal">
<div class="bodyHorizontalPadding" />
<svg
class="svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="240"
height="256"
>
<mask id="mask">
<rect x="0" y="0" width="240" height="256" fill="white" />
<rect x="0" y="0" width="240" height="32" rx="6" fill="black" />
<rect x="0" y="56" width="240" height="32" rx="6" fill="black" />
<rect x="0" y="112" width="240" height="32" rx="6" fill="black" />
<rect x="0" y="168" width="240" height="32" rx="6" fill="black" />
<rect x="71" y="224" width="98" height="32" rx="6" fill="black" />
</mask>
<rect
x="0"
y="0"
width="240"
height="256"
fill="black"
mask="url(#mask)"
/>
</svg>
<div class="bodyHorizontalPadding" />
</div>
<div class="bodyVerticalPadding" />
</div>
</div>
{#if !hideFooter}
<div class="footer" />
{/if}
</div>
<style>
.skeleton {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
border-radius: 4px;
overflow: hidden;
background-color: var(--spectrum-global-color-gray-200);
}
.animation {
position: absolute;
height: 100%;
width: 100%;
background: linear-gradient(
to right,
transparent 0%,
var(--spectrum-global-color-gray-300) 20%,
transparent 40%,
transparent 100%
);
animation-duration: 1.3s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: shimmer;
animation-timing-function: linear;
}
.noAnimation {
animation-name: none;
background: transparent;
}
.devTools {
display: flex;
box-sizing: border-box;
background-color: black;
height: 60px;
padding: 1px 24px 1px 20px;
display: flex;
align-items: center;
z-index: 1;
flex-shrink: 0;
color: white;
mix-blend-mode: multiply;
background: rgb(0 0 0);
font-size: 30px;
font-family: Source Sans Pro;
-webkit-font-smoothing: antialiased;
}
.main {
height: 100%;
display: flex;
flex-direction: column;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .main {
flex-direction: column;
width: initial;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .main {
flex-direction: column;
width: initial;
}
}
.sideNav .main {
flex-direction: row;
width: 100%;
}
.nav {
flex-shrink: 0;
width: 100%;
height: 141px;
background-color: transparent;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .nav {
height: 61px;
width: initial;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .nav {
height: 61px;
width: initial;
}
}
.sideNav .nav {
height: 100%;
width: 251px;
}
.body {
z-index: 2;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .body {
width: initial;
height: 100%;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .body {
width: initial;
height: 100%;
}
}
.sideNav .body {
width: 100%;
height: initial;
}
.body :global(svg > rect) {
fill: var(--spectrum-alias-background-color-primary);
}
.body :global(svg) {
flex-shrink: 0;
}
.bodyHorizontal {
display: flex;
flex-shrink: 0;
}
.bodyHorizontalPadding {
height: 100%;
flex-grow: 1;
background-color: var(--spectrum-alias-background-color-primary);
}
.bodyVerticalPadding {
width: 100%;
flex-grow: 1;
background-color: var(--spectrum-alias-background-color-primary);
}
.footer {
flex-shrink: 0;
box-sizing: border-box;
z-index: 1;
height: 52px;
width: 100%;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .footer {
border-top: none;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .footer {
border-top: none;
}
}
.sideNav .footer {
border-top: 3px solid var(--spectrum-alias-background-color-primary);
}
@keyframes shimmer {
0% {
left: -170%;
}
100% {
left: 170%;
}
}
</style>

View File

@ -5,3 +5,4 @@ export { default as UserAvatar } from "./UserAvatar.svelte"
export { default as UserAvatars } from "./UserAvatars.svelte" export { default as UserAvatars } from "./UserAvatars.svelte"
export { default as Updating } from "./Updating.svelte" export { default as Updating } from "./Updating.svelte"
export { Grid } from "./grid" export { Grid } from "./grid"
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"

View File

@ -17,5 +17,8 @@
--modal-background: var(--spectrum-global-color-gray-50); --modal-background: var(--spectrum-global-color-gray-50);
--drop-shadow: rgba(0, 0, 0, 0.25) !important; --drop-shadow: rgba(0, 0, 0, 0.25) !important;
--spectrum-global-color-blue-100: rgba(35, 40, 50) !important; --spectrum-global-color-blue-100: rgba(35, 40, 50) !important;
--spectrum-alias-background-color-secondary: var(--spectrum-global-color-gray-75);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
} }

View File

@ -50,4 +50,7 @@
--modal-background: var(--spectrum-global-color-gray-50); --modal-background: var(--spectrum-global-color-gray-50);
--drop-shadow: rgba(0, 0, 0, 0.15) !important; --drop-shadow: rgba(0, 0, 0, 0.15) !important;
--spectrum-global-color-blue-100: rgb(56, 65, 84) !important; --spectrum-global-color-blue-100: rgb(56, 65, 84) !important;
--spectrum-alias-background-color-secondary: var(--spectrum-global-color-gray-75);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
} }

View File

@ -52,6 +52,7 @@
"@budibase/pro": "0.0.0", "@budibase/pro": "0.0.0",
"@budibase/shared-core": "0.0.0", "@budibase/shared-core": "0.0.0",
"@budibase/string-templates": "0.0.0", "@budibase/string-templates": "0.0.0",
"@budibase/frontend-core": "0.0.0",
"@budibase/types": "0.0.0", "@budibase/types": "0.0.0",
"@bull-board/api": "5.10.2", "@bull-board/api": "5.10.2",
"@bull-board/koa": "5.10.2", "@bull-board/koa": "5.10.2",

View File

@ -1,7 +1,5 @@
import { InvalidFileExtensions } from "@budibase/shared-core" import { InvalidFileExtensions } from "@budibase/shared-core"
import AppComponent from "./templates/BudibaseApp.svelte" import AppComponent from "./templates/BudibaseApp.svelte"
import { join } from "../../../utilities/centralPath" import { join } from "../../../utilities/centralPath"
import * as uuid from "uuid" import * as uuid from "uuid"
import { ObjectStoreBuckets } from "../../../constants" import { ObjectStoreBuckets } from "../../../constants"
@ -24,7 +22,13 @@ import AWS from "aws-sdk"
import fs from "fs" import fs from "fs"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import * as pro from "@budibase/pro" import * as pro from "@budibase/pro"
import { App, Ctx, ProcessAttachmentResponse } from "@budibase/types" import {
UserCtx,
App,
Ctx,
ProcessAttachmentResponse,
Feature,
} from "@budibase/types"
import { import {
getAppMigrationVersion, getAppMigrationVersion,
getLatestMigrationId, getLatestMigrationId,
@ -32,6 +36,61 @@ import {
import send from "koa-send" import send from "koa-send"
const getThemeVariables = (theme: string) => {
if (theme === "spectrum--lightest") {
return `
--spectrum-global-color-gray-50: rgb(255, 255, 255);
--spectrum-global-color-gray-200: rgb(244, 244, 244);
--spectrum-global-color-gray-300: rgb(234, 234, 234);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
`
}
if (theme === "spectrum--light") {
return `
--spectrum-global-color-gray-50: rgb(255, 255, 255);
--spectrum-global-color-gray-200: rgb(234, 234, 234);
--spectrum-global-color-gray-300: rgb(225, 225, 225);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
`
}
if (theme === "spectrum--dark") {
return `
--spectrum-global-color-gray-100: rgb(50, 50, 50);
--spectrum-global-color-gray-200: rgb(62, 62, 62);
--spectrum-global-color-gray-300: rgb(74, 74, 74);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
if (theme === "spectrum--darkest") {
return `
--spectrum-global-color-gray-100: rgb(30, 30, 30);
--spectrum-global-color-gray-200: rgb(44, 44, 44);
--spectrum-global-color-gray-300: rgb(57, 57, 57);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
if (theme === "spectrum--nord") {
return `
--spectrum-global-color-gray-100: #3b4252;
--spectrum-global-color-gray-200: #424a5c;
--spectrum-global-color-gray-300: #4c566a;
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
if (theme === "spectrum--midnight") {
return `
--hue: 220;
--sat: 10%;
--spectrum-global-color-gray-100: hsl(var(--hue), var(--sat), 17%);
--spectrum-global-color-gray-200: hsl(var(--hue), var(--sat), 20%);
--spectrum-global-color-gray-300: hsl(var(--hue), var(--sat), 24%);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
}
export const toggleBetaUiFeature = async function (ctx: Ctx) { export const toggleBetaUiFeature = async function (ctx: Ctx) {
const cookieName = `beta:${ctx.params.feature}` const cookieName = `beta:${ctx.params.feature}`
@ -146,7 +205,7 @@ const requiresMigration = async (ctx: Ctx) => {
return requiresMigrations return requiresMigrations
} }
export const serveApp = async function (ctx: Ctx) { export const serveApp = async function (ctx: UserCtx) {
const needMigrations = await requiresMigration(ctx) const needMigrations = await requiresMigration(ctx)
const bbHeaderEmbed = const bbHeaderEmbed =
@ -167,9 +226,19 @@ export const serveApp = async function (ctx: Ctx) {
const appInfo = await db.get<any>(DocumentType.APP_METADATA) const appInfo = await db.get<any>(DocumentType.APP_METADATA)
let appId = context.getAppId() let appId = context.getAppId()
const hideDevTools = !!ctx.params.appUrl
const sideNav = appInfo.navigation.navigation === "Left"
const hideFooter =
ctx?.user?.license?.features?.includes(Feature.BRANDING) || false
const themeVariables = getThemeVariables(appInfo?.theme)
if (!env.isJest()) { if (!env.isJest()) {
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins) const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
const { head, html, css } = AppComponent.render({ const { head, html, css } = AppComponent.render({
hideDevTools,
sideNav,
hideFooter,
metaImage: metaImage:
branding?.metaImageUrl || branding?.metaImageUrl ||
"https://res.cloudinary.com/daog6scxm/image/upload/v1698759482/meta-images/plain-branded-meta-image-coral_ocxmgu.png", "https://res.cloudinary.com/daog6scxm/image/upload/v1698759482/meta-images/plain-branded-meta-image-coral_ocxmgu.png",
@ -194,7 +263,7 @@ export const serveApp = async function (ctx: Ctx) {
ctx.body = await processString(appHbs, { ctx.body = await processString(appHbs, {
head, head,
body: html, body: html,
style: css.code, css: `:root{${themeVariables}} ${css.code}`,
appId, appId,
embedded: bbHeaderEmbed, embedded: bbHeaderEmbed,
}) })

View File

@ -1,4 +1,6 @@
<script> <script>
import ClientAppSkeleton from "@budibase/frontend-core/src/components/ClientAppSkeleton.svelte"
export let title = "" export let title = ""
export let favicon = "" export let favicon = ""
@ -9,6 +11,10 @@
export let clientLibPath export let clientLibPath
export let usedPlugins export let usedPlugins
export let appMigrating export let appMigrating
export let hideDevTools
export let sideNav
export let hideFooter
</script> </script>
<svelte:head> <svelte:head>
@ -96,6 +102,7 @@
</svelte:head> </svelte:head>
<body id="app"> <body id="app">
<ClientAppSkeleton {hideDevTools} {sideNav} {hideFooter} />
<div id="error"> <div id="error">
{#if clientLibPath} {#if clientLibPath}
<h1>There was an error loading your app</h1> <h1>There was an error loading your app</h1>

View File

@ -1,8 +1,12 @@
<html> <html>
<script>
document.fonts.ready.then(() => {
window.parent.postMessage({ type: "docLoaded" });
})
</script>
<head> <head>
{{{head}}} {{{head}}}
<style>{{{style}}}</style> <style>{{{css}}}</style>
</head> </head>
<script> <script>

View File

@ -51,8 +51,8 @@ router
controller.deleteObjects controller.deleteObjects
) )
.get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview) .get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview)
.get("/:appId/:path*", controller.serveApp)
.get("/app/:appUrl/:path*", controller.serveApp) .get("/app/:appUrl/:path*", controller.serveApp)
.get("/:appId/:path*", controller.serveApp)
.post( .post(
"/api/attachments/:datasourceId/url", "/api/attachments/:datasourceId/url",
authorized(PermissionType.TABLE, PermissionLevel.READ), authorized(PermissionType.TABLE, PermissionLevel.READ),

View File

@ -17,12 +17,6 @@ const { nodeExternalsPlugin } = require("esbuild-node-externals")
const svelteCompilePlugin = { const svelteCompilePlugin = {
name: 'svelteCompile', name: 'svelteCompile',
setup(build) { setup(build) {
// This resolve handler is necessary to bundle the Svelte runtime into the the final output,
// otherwise the bundled script will attempt to resolve it at runtime
build.onResolve({ filter: /svelte\/internal/ }, async () => {
return { path: `${process.cwd()}/../../node_modules/svelte/src/runtime/internal/ssr.js` }
})
// Compiles `.svelte` files into JS classes so that they can be directly imported into our // Compiles `.svelte` files into JS classes so that they can be directly imported into our
// Typescript packages // Typescript packages
build.onLoad({ filter: /\.svelte$/ }, async (args) => { build.onLoad({ filter: /\.svelte$/ }, async (args) => {
@ -37,7 +31,7 @@ const svelteCompilePlugin = {
contents: js.code, contents: js.code,
// The loader this is passed to, basically how the above provided content is "treated", // The loader this is passed to, basically how the above provided content is "treated",
// the contents provided above will be transpiled and bundled like any other JS file. // the contents provided above will be transpiled and bundled like any other JS file.
loader: 'js', loader: 'js',
// Where to resolve any imports present in the loaded file // Where to resolve any imports present in the loaded file
resolveDir: dir resolveDir: dir
} }
@ -80,11 +74,11 @@ async function runBuild(entry, outfile) {
plugins: [ plugins: [
svelteCompilePlugin, svelteCompilePlugin,
TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }), TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }),
nodeExternalsPlugin(), nodeExternalsPlugin({
allowList: ["@budibase/frontend-core", "svelte"]
}),
], ],
preserveSymlinks: true, preserveSymlinks: true,
loader: {
},
metafile: true, metafile: true,
external: [ external: [
"deasync", "deasync",