Merge branch 'master' into BUDI-9296/new-screen-as-modal

This commit is contained in:
Adria Navarro 2025-05-15 13:20:26 +02:00 committed by GitHub
commit b16f4d301d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 704 additions and 786 deletions

View File

@ -4,7 +4,7 @@
# Dockerfile. Only modifications related to upgrading from Debian bullseye to
# bookworm have been included. The `runner` image contains Budibase's
# customisations to the image, e.g. adding Clouseau.
FROM node:20-slim AS base
FROM node:22-slim AS base
# Add CouchDB user account to make sure the IDs are assigned consistently
RUN groupadd -g 5984 -r couchdb && useradd -u 5984 -d /opt/couchdb -g couchdb couchdb

View File

@ -1,5 +1,5 @@
ARG BASEIMG=budibase/couchdb:v3.3.3-sqs-v2.1.1
FROM node:20-slim AS build
FROM node:22-slim AS build
# install node-gyp dependencies
RUN apt-get update && apt-get install -y --no-install-recommends g++ make python3 jq

View File

@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.10.6",
"version": "3.10.7",
"npmClient": "yarn",
"concurrency": 20,
"command": {

View File

@ -1,9 +1,12 @@
<script>
import { Router } from "@roxi/routify"
import { routes } from "../.routify/routes"
import { NotificationDisplay, BannerDisplay } from "@budibase/bbui"
import { NotificationDisplay, BannerDisplay, Context } from "@budibase/bbui"
import { parse, stringify } from "qs"
import LicensingOverlays from "@/components/portal/licensing/LicensingOverlays.svelte"
import { setContext } from "svelte"
setContext(Context.PopoverRoot, "body")
const queryHandler = { parse, stringify }
</script>

View File

@ -1,5 +1,12 @@
<script>
import { Button, Label, Icon, Input, notifications } from "@budibase/bbui"
import {
Button,
Label,
Icon,
Input,
notifications,
Body,
} from "@budibase/bbui"
import { AppStatus } from "@/constants"
import { appStore, initialise } from "@/stores/builder"
import { appsStore } from "@/stores/portal"
@ -8,7 +15,6 @@
import { createValidationStore } from "@budibase/frontend-core/src/utils/validation/yup"
import * as appValidation from "@budibase/frontend-core/src/utils/validation/yup/app"
import EditableIcon from "@/components/common/EditableIcon.svelte"
import { isEqual } from "lodash"
import { createEventDispatcher } from "svelte"
export let alignActions = "left"
@ -18,10 +24,9 @@
const dispatch = createEventDispatcher()
let updating = false
let edited = false
let initialised = false
$: filteredApps = $appsStore.apps.filter(app => app.devId == $appStore.appId)
$: filteredApps = $appsStore.apps.filter(app => app.devId === $appStore.appId)
$: app = filteredApps.length ? filteredApps[0] : {}
$: appDeployed = app?.status === AppStatus.DEPLOYED
@ -38,7 +43,6 @@
}
const initForm = appMeta => {
edited = false
values.set({
...appMeta,
})
@ -49,18 +53,17 @@
}
}
const validate = (vals, appMeta) => {
const validate = vals => {
const { url } = vals || {}
validation.check({
...vals,
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
})
edited = !isEqual(vals, appMeta)
}
// On app/apps update, reset the state.
$: initForm(appMeta)
$: validate($values, appMeta)
$: validate($values)
const resolveAppUrl = (template, name) => {
let parsedName
@ -177,13 +180,14 @@
updating = false
dispatch("updated")
}}
disabled={appDeployed || updating || !edited || !$validation.valid}
disabled={appDeployed || updating || !$validation.valid}
>
Save
</Button>
{:else}
<div class="edit-info">
<Icon size="S" name="Info" /> Unpublish your app to edit name and URL
<Icon size="M" name="InfoOutline" />
<Body size="S">Unpublish your app to edit name and URL</Body>
</div>
{/if}
</div>
@ -209,6 +213,6 @@
}
.edit-info {
display: flex;
gap: var(--spacing-s);
gap: var(--spacing-m);
}
</style>

View File

@ -1,57 +1,24 @@
<script>
import { Popover, Layout, Icon } from "@budibase/bbui"
import UpdateAppForm from "./UpdateAppForm.svelte"
let formPopover
let formPopoverAnchor
let formPopoverOpen = false
import { Icon } from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { appStore } from "@/stores/builder"
</script>
<div bind:this={formPopoverAnchor}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="app-heading"
class:editing={formPopoverOpen}
on:click={() => {
formPopover.show()
}}
>
<slot />
<span class="edit-icon">
<Icon size="S" name="Edit" color={"var(--grey-7)"} />
</span>
</div>
</div>
<Popover
customZIndex={998}
bind:this={formPopover}
align="center"
anchor={formPopoverAnchor}
offset={20}
on:close={() => {
formPopoverOpen = false
}}
on:open={() => {
formPopoverOpen = true
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="app-heading"
on:click={() => {
$goto(`/builder/app/${$appStore.appId}/settings/general`)
}}
>
<Layout noPadding gap="M">
<div class="popover-content">
<UpdateAppForm
on:updated={() => {
formPopover.hide()
}}
/>
</div>
</Layout>
</Popover>
<slot />
<span class="edit-icon">
<Icon size="S" name="Edit" color={"var(--grey-7)"} />
</span>
</div>
<style>
.popover-content {
padding: var(--spacing-xl);
}
.app-heading {
display: flex;
cursor: pointer;
@ -61,8 +28,7 @@
.edit-icon {
display: none;
}
.app-heading:hover .edit-icon,
.app-heading.editing .edit-icon {
.app-heading:hover .edit-icon {
display: inline;
}
</style>

View File

@ -1,139 +1,34 @@
<script>
import {
notifications,
Popover,
Layout,
Body,
Button,
ActionButton,
Icon,
Link,
StatusLight,
AbsTooltip,
Popover,
PopoverAlignment,
Body,
Icon,
} from "@budibase/bbui"
import RevertModal from "@/components/deploy/RevertModal.svelte"
import VersionModal from "@/components/deploy/VersionModal.svelte"
import { processStringSync } from "@budibase/string-templates"
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
import analytics, { Events, EventSource } from "@/analytics"
import { API } from "@/api"
import { appsStore } from "@/stores/portal"
import {
previewStore,
builderStore,
isOnlyUser,
appStore,
deploymentStore,
sortedScreens,
appPublished,
} from "@/stores/builder"
import { goto } from "@roxi/routify"
import VersionModal from "@/components/deploy/VersionModal.svelte"
export let application
export let loaded
let unpublishModal
let revertModal
let versionModal
let appActionPopover
let appActionPopoverOpen = false
let appActionPopoverAnchor
let publishing = false
let showNpsSurvey = false
let lastOpened
let publishButton
let publishSuccessPopover
$: filteredApps = $appsStore.apps.filter(app => app.devId === application)
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
$: updateAvailable =
$appStore.upgradableVersion &&
$appStore.version &&
$appStore.upgradableVersion !== $appStore.version
$: canPublish = !publishing && loaded && $sortedScreens.length > 0
$: lastDeployed = getLastDeployedString($deploymentStore, lastOpened)
const getLastDeployedString = deployments => {
return deployments?.length
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
time:
new Date().getTime() - new Date(deployments[0].updatedAt).getTime(),
})
: ""
}
const previewApp = () => {
previewStore.showPreview(true)
}
const viewApp = () => {
analytics.captureEvent(Events.APP_VIEW_PUBLISHED, {
appId: selectedApp.appId,
eventSource: EventSource.PORTAL,
})
if (selectedApp.url) {
window.open(`/app${selectedApp.url}`)
} else {
window.open(`/${selectedApp.prodId}`)
}
}
async function publishApp() {
try {
publishing = true
await API.publishAppChanges($appStore.appId)
notifications.send("App published successfully", {
type: "success",
icon: "GlobeCheck",
})
showNpsSurvey = true
await completePublish()
} catch (error) {
console.error(error)
analytics.captureException(error)
const baseMsg = "Error publishing app"
const message = error.message
if (message) {
notifications.error(`${baseMsg} - ${message}`)
} else {
notifications.error(baseMsg)
}
}
publishing = false
}
const unpublishApp = () => {
appActionPopover.hide()
unpublishModal.show()
}
const revertApp = () => {
appActionPopover.hide()
revertModal.show()
}
const confirmUnpublishApp = async () => {
if (!application || !$appPublished) {
//confirm the app has loaded.
return
}
try {
await API.unpublishApp(selectedApp.prodId)
await appsStore.load()
notifications.send("App unpublished", {
type: "success",
icon: "GlobeStrike",
})
} catch (err) {
notifications.error("Error unpublishing app")
}
}
const completePublish = async () => {
try {
await appsStore.load()
await deploymentStore.load()
} catch (err) {
notifications.error("Error refreshing app")
}
const publish = async () => {
await deploymentStore.publishApp(false)
publishSuccessPopover?.show()
}
</script>
@ -164,161 +59,50 @@
</ActionButton>
</div>
</div>
<div class="app-action-button preview">
<div class="app-action">
<ActionButton
disabled={$sortedScreens.length === 0}
quiet
icon="PlayCircle"
on:click={previewApp}
>
Preview
</ActionButton>
</div>
</div>
<div
class="app-action-button publish app-action-popover"
on:click={() => {
if (!appActionPopoverOpen) {
lastOpened = new Date()
appActionPopover.show()
} else {
appActionPopover.hide()
}
}}
>
<div bind:this={appActionPopoverAnchor}>
<div class="app-action">
<Icon name={$appPublished ? "GlobeCheck" : "GlobeStrike"} />
<span class="publish-open" id="builder-app-publish-button">
Publish
<Icon
name={appActionPopoverOpen ? "ChevronUp" : "ChevronDown"}
size="M"
/>
</span>
</div>
</div>
<Popover
bind:this={appActionPopover}
align="right"
disabled={!$appPublished}
anchor={appActionPopoverAnchor}
offset={35}
on:close={() => {
appActionPopoverOpen = false
}}
on:open={() => {
appActionPopoverOpen = true
}}
<div class="app-action-button publish">
<Button
cta
on:click={publish}
disabled={$deploymentStore.isPublishing}
bind:ref={publishButton}
>
<div class="app-action-popover-content">
<Layout noPadding gap="M">
<Body size="M">
<span
class="app-link"
on:click={() => {
appActionPopover.hide()
if ($appPublished) {
viewApp()
}
}}
>
{$appStore.url}
{#if $appPublished}
<Icon size="S" name="LinkOut" />
{/if}
</span>
</Body>
<Body size="S">
<span class="publish-popover-status">
{#if $appPublished}
<span class="status-text">
{lastDeployed}
</span>
<span class="unpublish-link">
<Link quiet on:click={unpublishApp}>Unpublish</Link>
</span>
<span class="revert-link">
<AbsTooltip
text={$isOnlyUser
? null
: "Unavailable - another user is editing this app"}
>
<Link
disabled={!$isOnlyUser}
quiet
secondary
on:click={revertApp}
>
Revert
</Link>
</AbsTooltip>
</span>
{:else}
<span class="status-text unpublished">Not published</span>
{/if}
</span>
</Body>
<div class="action-buttons">
{#if $appPublished}
<ActionButton
quiet
icon="Code"
on:click={() => {
$goto("./settings/embed")
appActionPopover.hide()
}}
>
Embed
</ActionButton>
{/if}
<Button
cta
on:click={publishApp}
id={"builder-app-publish-button"}
disabled={!canPublish}
>
Publish
</Button>
</div>
</Layout>
</div>
</Popover>
Publish
</Button>
</div>
</div>
</div>
<!-- Modals -->
<ConfirmDialog
bind:this={unpublishModal}
title="Confirm unpublish"
okText="Unpublish app"
onOk={confirmUnpublishApp}
>
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
</ConfirmDialog>
<RevertModal bind:this={revertModal} />
<VersionModal hideIcon bind:this={versionModal} />
{#if showNpsSurvey}
<div class="nps-survey" />
{/if}
<VersionModal hideIcon bind:this={versionModal} />
<Popover
anchor={publishButton}
bind:this={publishSuccessPopover}
align={PopoverAlignment.Right}
offset={6}
>
<div class="popover-content">
<Icon
name="CheckmarkCircle"
color="var(--spectrum-global-color-green-400)"
size="L"
/>
<Body size="S">
App published successfully
<br />
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="link" on:click={deploymentStore.viewPublishedApp}>
View app
</div>
</Body>
</div>
</Popover>
<style>
.app-action-popover-content {
padding: var(--spacing-xl);
width: 360px;
}
.app-action-popover-content :global(.icon svg.spectrum-Icon) {
height: 0.8em;
}
.action-buttons {
display: flex;
flex-direction: row;
@ -326,74 +110,40 @@
align-items: center;
height: 100%;
}
.action-top-nav {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
height: 100%;
padding-right: var(--spacing-s);
}
.app-link {
display: flex;
align-items: center;
gap: var(--spacing-s);
cursor: pointer;
}
.app-action-popover-content .status-text {
color: var(--spectrum-global-color-green-500);
border-right: 1px solid var(--spectrum-global-color-gray-500);
padding-right: var(--spacing-m);
}
.app-action-popover-content .status-text.unpublished {
color: var(--spectrum-global-color-gray-600);
border-right: 0px;
padding-right: 0px;
}
.app-action-popover-content .action-buttons {
gap: var(--spacing-m);
}
.app-action-popover-content
.publish-popover-status
.unpublish-link
:global(.spectrum-Link) {
color: var(--spectrum-global-color-red-400);
}
.publish-popover-status {
display: flex;
gap: var(--spacing-m);
}
.app-action-popover .publish-open {
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.app-action-button {
height: 100%;
display: flex;
align-items: center;
padding-right: var(--spacing-m);
}
.app-action-button.publish:hover {
background-color: var(--spectrum-global-color-gray-200);
cursor: pointer;
}
.app-action-button.publish {
border-left: var(--border-light);
padding: 0px var(--spacing-l);
}
.app-action-button.version :global(.spectrum-ActionButton-label) {
display: flex;
gap: var(--spectrum-actionbutton-icon-gap);
}
.app-action {
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.popover-content {
display: flex;
gap: var(--spacing-m);
padding: var(--spacing-xl);
}
.link {
text-decoration: underline;
color: var(--spectrum-global-color-gray-900);
}
.link:hover {
cursor: pointer;
filter: brightness(110%);
}
</style>

View File

@ -35,7 +35,7 @@
<Modal bind:this={revertModal}>
<ModalContent
title="Revert Changes"
title="Revert changes"
confirmText="Revert"
onConfirm={revert}
disabled={appName !== $appStore.name}

View File

@ -5,26 +5,28 @@
runtimeToReadableBinding,
} from "@/dataBinding"
import { builderStore } from "@/stores/builder"
import { createEventDispatcher } from "svelte"
export let label = ""
export let labelHidden = false
export let componentInstance = {}
export let control = null
export let control = undefined
export let key = ""
export let type = ""
export let value = null
export let defaultValue = null
export let value = undefined
export let defaultValue = undefined
export let props = {}
export let onChange = () => {}
export let onChange = undefined
export let bindings = []
export let componentBindings = []
export let nested = false
export let propertyFocus = false
export let info = null
export let info = undefined
export let disableBindings = false
export let wide = false
export let contextAccess = null
export let contextAccess = undefined
const dispatch = createEventDispatcher()
let highlightType
let domElement
@ -93,10 +95,11 @@
innerVal = parseInt(innerVal)
}
if (typeof innerVal === "string") {
onChange(replaceBindings(innerVal))
} else {
onChange(innerVal)
const dispatchVal =
typeof innerVal === "string" ? replaceBindings(innerVal) : innerVal
dispatch("change", dispatchVal)
if (onChange) {
onChange(dispatchVal)
}
}

View File

@ -14,7 +14,12 @@
Button,
FancySelect,
} from "@budibase/bbui"
import { builderStore, appStore, roles, appPublished } from "@/stores/builder"
import {
builderStore,
appStore,
roles,
deploymentStore,
} from "@/stores/builder"
import {
groups,
licensing,
@ -620,7 +625,7 @@
</div>
<div class="body">
{#if !$appPublished}
{#if !$deploymentStore.isPublished}
<div class="alert">
<InfoDisplay
icon="AlertCircleFilled"

View File

@ -191,6 +191,17 @@ export const size = {
],
}
export const font = {
label: "Font",
settings: [
{
label: "Color",
key: "color",
control: ColorPicker,
},
],
}
export const background = {
label: "Background",
settings: [

View File

@ -1,76 +0,0 @@
<script>
import {
Layout,
Label,
ColorPicker,
notifications,
Icon,
Body,
} from "@budibase/bbui"
import { themeStore, appStore } from "@/stores/builder"
import { DefaultAppTheme } from "@/constants"
import AppThemeSelect from "./AppThemeSelect.svelte"
import ButtonRoundnessSelect from "./ButtonRoundnessSelect.svelte"
import PropertyControl from "@/components/design/settings/controls/PropertyControl.svelte"
$: customTheme = $themeStore.customTheme || {}
const update = async (property, value) => {
try {
themeStore.saveCustom({ [property]: value }, $appStore.appId)
} catch (error) {
notifications.error("Error updating custom theme")
}
}
</script>
<div class="info">
<Icon name="InfoOutline" size="S" />
<Body size="S">
These settings apply to all screens. PDFs are always light theme.
</Body>
</div>
<Layout noPadding gap="S">
<Layout noPadding gap="XS">
<AppThemeSelect />
</Layout>
<Layout noPadding gap="XS">
<Label>Button roundness</Label>
<ButtonRoundnessSelect
{customTheme}
on:change={e => update("buttonBorderRadius", e.detail)}
/>
</Layout>
<PropertyControl
label="Accent color"
control={ColorPicker}
value={customTheme.primaryColor || DefaultAppTheme.primaryColor}
onChange={val => update("primaryColor", val)}
props={{
spectrumTheme: $themeStore.theme,
}}
/>
<PropertyControl
label="Hover"
control={ColorPicker}
value={customTheme.primaryColorHover || DefaultAppTheme.primaryColorHover}
onChange={val => update("primaryColorHover", val)}
props={{
spectrumTheme: $themeStore.theme,
}}
/>
</Layout>
<style>
.info {
background-color: var(--background-alt);
padding: 12px;
display: flex;
border-radius: 4px;
gap: 4px;
}
.info :global(svg) {
margin-right: 5px;
color: var(--spectrum-global-color-gray-600);
}
</style>

View File

@ -1,13 +1,8 @@
<script>
import GeneralPanel from "./GeneralPanel.svelte"
import ThemePanel from "./ThemePanel.svelte"
import { selectedScreen } from "@/stores/builder"
import Panel from "@/components/design/Panel.svelte"
import { capitalise } from "@/helpers"
import { ActionButton, Layout } from "@budibase/bbui"
let activeTab = "general"
const tabs = ["general", "theme"]
import { Layout } from "@budibase/bbui"
</script>
{#if $selectedScreen}
@ -17,37 +12,8 @@
borderLeft
wide
>
<div slot="panel-header-content">
<div class="settings-tabs">
{#each tabs as tab}
<ActionButton
size="M"
quiet
selected={activeTab === tab}
on:click={() => {
activeTab = tab
}}
>
{capitalise(tab)}
</ActionButton>
{/each}
</div>
</div>
<Layout gap="XS" paddingX="XL" paddingY="XL">
{#if activeTab === "theme"}
<ThemePanel />
{:else}
<GeneralPanel />
{/if}
<GeneralPanel />
</Layout>
</Panel>
{/if}
<style>
.settings-tabs {
display: flex;
gap: var(--spacing-s);
padding: 0 var(--spacing-l);
padding-bottom: var(--spacing-l);
}
</style>

View File

@ -1,13 +1,27 @@
<script>
import DevicePreviewSelect from "./DevicePreviewSelect.svelte"
import AppPreview from "./AppPreview.svelte"
import { screenStore, appStore, selectedScreen } from "@/stores/builder"
import {
screenStore,
appStore,
selectedScreen,
previewStore,
} from "@/stores/builder"
import UndoRedoControl from "@/components/common/UndoRedoControl.svelte"
import ScreenErrorsButton from "./ScreenErrorsButton.svelte"
import { Divider } from "@budibase/bbui"
import { ActionButton, Divider } from "@budibase/bbui"
import { ScreenVariant } from "@budibase/types"
import ThemeSettings from "./Theme/ThemeSettings.svelte"
$: mobile = $previewStore.previewDevice === "mobile"
$: isPDF = $selectedScreen?.variant === ScreenVariant.PDF
const previewApp = () => {
previewStore.showPreview(true)
}
const togglePreviewDevice = () => {
previewStore.setDevice(mobile ? "desktop" : "mobile")
}
</script>
<div class="app-panel">
@ -17,13 +31,24 @@
<UndoRedoControl store={screenStore.history} />
</div>
<div class="header-right">
{#if !isPDF}
{#if $appStore.clientFeatures.devicePreview}
<DevicePreviewSelect />
<div class="actions">
{#if !isPDF}
{#if $appStore.clientFeatures.devicePreview}
<ActionButton
quiet
icon={mobile ? "DevicePhone" : "DeviceDesktop"}
selected
on:click={togglePreviewDevice}
/>
{/if}
<ThemeSettings />
{/if}
<Divider vertical />
{/if}
<ScreenErrorsButton />
<ScreenErrorsButton />
</div>
<Divider vertical />
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
Preview
</ActionButton>
</div>
</div>
<div class="content">
@ -56,7 +81,7 @@
}
.header {
display: flex;
margin-bottom: 9px;
margin: 0 6px 4px 0;
}
.header-left {
@ -76,4 +101,10 @@
.content {
flex: 1 1 auto;
}
.actions {
display: flex;
flex-direction: row;
gap: 4px;
align-items: center;
}
</style>

View File

@ -1,25 +0,0 @@
<script>
import { ActionGroup, ActionButton } from "@budibase/bbui"
import { previewStore } from "@/stores/builder"
</script>
<ActionGroup compact quiet>
<ActionButton
quiet
icon="DeviceDesktop"
selected={$previewStore.previewDevice === "desktop"}
on:click={() => previewStore.setDevice("desktop")}
/>
<ActionButton
quiet
icon="DeviceTablet"
selected={$previewStore.previewDevice === "tablet"}
on:click={() => previewStore.setDevice("tablet")}
/>
<ActionButton
quiet
icon="DevicePhone"
selected={$previewStore.previewDevice === "mobile"}
on:click={() => previewStore.setDevice("mobile")}
/>
</ActionGroup>

View File

@ -0,0 +1,101 @@
<script lang="ts">
import {
ActionButton,
Body,
ColorPicker,
Icon,
Label,
Layout,
PopoverAlignment,
notifications,
} from "@budibase/bbui"
import DetailPopover from "@/components/common/DetailPopover.svelte"
import { themeStore, appStore } from "@/stores/builder"
import { DefaultAppTheme } from "@/constants"
import AppThemeSelect from "./AppThemeSelect.svelte"
import ButtonRoundnessSelect from "./ButtonRoundnessSelect.svelte"
import PropertyControl from "@/components/design/settings/controls/PropertyControl.svelte"
let popover: any
$: customTheme = $themeStore.customTheme || {}
export function show() {
popover?.show()
}
export function hide() {
popover?.hide()
}
const update = async (property: string, value: any) => {
try {
await themeStore.saveCustom({ [property]: value }, $appStore.appId)
} catch (error) {
notifications.error("Error updating custom theme")
}
}
</script>
<DetailPopover
title="Theme"
bind:this={popover}
align={PopoverAlignment.Right}
width={320}
>
<svelte:fragment slot="anchor" let:open>
<ActionButton icon="ColorPalette" quiet selected={open} on:click={show} />
</svelte:fragment>
<div class="info">
<Icon name="InfoOutline" size="S" />
<Body size="S">
These settings apply to all screens.<br />
PDFs are always light theme.
</Body>
</div>
<Layout noPadding gap="S">
<Layout noPadding gap="XS">
<AppThemeSelect />
</Layout>
<Layout noPadding gap="XS">
<Label>Button roundness</Label>
<ButtonRoundnessSelect
{customTheme}
on:change={e => update("buttonBorderRadius", e.detail)}
/>
</Layout>
<PropertyControl
label="Accent color"
control={ColorPicker}
value={customTheme.primaryColor || DefaultAppTheme.primaryColor}
on:change={e => update("primaryColor", e.detail)}
props={{
spectrumTheme: $themeStore.theme,
}}
/>
<PropertyControl
label="Hover"
control={ColorPicker}
value={customTheme.primaryColorHover || DefaultAppTheme.primaryColorHover}
on:change={e => update("primaryColorHover", e.detail)}
props={{
spectrumTheme: $themeStore.theme,
}}
/>
</Layout>
</DetailPopover>
<style>
.info {
background-color: var(--background-alt);
padding: 12px;
display: flex;
border-radius: 4px;
gap: 4px;
}
.info :global(svg) {
margin-right: 5px;
color: var(--spectrum-global-color-gray-600);
}
</style>

View File

@ -1,11 +1,7 @@
<script lang="ts">
import { Content, SideNav, SideNavItem } from "@/components/portal/page"
import { Page, Layout, AbsTooltip, TooltipPosition } from "@budibase/bbui"
import { Page, Layout } from "@budibase/bbui"
import { url, isActive } from "@roxi/routify"
import DeleteModal from "@/components/deploy/DeleteModal.svelte"
import { isOnlyUser, appStore } from "@/stores/builder"
let deleteModal: DeleteModal
</script>
<!-- routify:options index=4 -->
@ -14,6 +10,11 @@
<Layout noPadding gap="L">
<Content showMobileNav>
<SideNav slot="side-nav">
<SideNavItem
text="General"
url={$url("./general")}
active={$isActive("./general")}
/>
<SideNavItem
text="Automations"
url={$url("./automations")}
@ -30,25 +31,10 @@
active={$isActive("./embed")}
/>
<SideNavItem
text="Progressive Web App"
text="Progressive web app"
url={$url("./pwa")}
active={$isActive("./pwa")}
/>
<SideNavItem
text="Export/Import"
url={$url("./exportImport")}
active={$isActive("./exportImport")}
/>
<SideNavItem
text="Name and URL"
url={$url("./name-and-url")}
active={$isActive("./name-and-url")}
/>
<SideNavItem
text="Version"
url={$url("./version")}
active={$isActive("./version")}
/>
<SideNavItem
text="OAuth2"
url={$url("./oauth2")}
@ -59,22 +45,6 @@
url={$url("./scripts")}
active={$isActive("./scripts")}
/>
<div class="delete-action">
<AbsTooltip
position={TooltipPosition.Bottom}
text={$isOnlyUser
? undefined
: "Unavailable - another user is editing this app"}
>
<SideNavItem
text="Delete app"
disabled={!$isOnlyUser}
on:click={() => {
deleteModal.show()
}}
/>
</AbsTooltip>
</div>
</SideNav>
<slot />
</Content>
@ -82,19 +52,7 @@
</Page>
</div>
<DeleteModal
bind:this={deleteModal}
appId={$appStore.appId}
appName={$appStore.name}
/>
<style>
.delete-action :global(.text) {
color: var(--spectrum-global-color-red-400);
}
.delete-action {
display: contents;
}
.settings {
flex: 1 1 auto;
display: flex;

View File

@ -1,75 +0,0 @@
<script>
import {
Layout,
Body,
Heading,
Divider,
ActionButton,
Modal,
} from "@budibase/bbui"
import { AppStatus } from "@/constants"
import { appsStore } from "@/stores/portal"
import { appStore } from "@/stores/builder"
import ExportAppModal from "@/components/start/ExportAppModal.svelte"
import ImportAppModal from "@/components/start/ImportAppModal.svelte"
$: filteredApps = $appsStore.apps.filter(app => app.devId === $appStore.appId)
$: app = filteredApps.length ? filteredApps[0] : {}
$: appDeployed = app?.status === AppStatus.DEPLOYED
let exportModal, importModal
let exportPublishedVersion = false
const exportApp = opts => {
exportPublishedVersion = !!opts?.published
exportModal.show()
}
const importApp = () => {
importModal.show()
}
</script>
<Modal bind:this={exportModal} padding={false}>
<ExportAppModal {app} published={exportPublishedVersion} />
</Modal>
<Modal bind:this={importModal} padding={false}>
<ImportAppModal {app} />
</Modal>
<Layout noPadding>
<Layout gap="XS" noPadding>
<Heading>Export your app</Heading>
<Body>Export your latest edited or published app</Body>
</Layout>
<div class="body">
<ActionButton secondary on:click={() => exportApp({ published: false })}>
Export latest edited app
</ActionButton>
<ActionButton
secondary
disabled={!appDeployed}
on:click={() => exportApp({ published: true })}
>
Export latest published app
</ActionButton>
</div>
<Divider />
<Layout gap="XS" noPadding>
<Heading>Import your app</Heading>
<Body>Import an export to update this app</Body>
</Layout>
<div class="body">
<ActionButton secondary on:click={() => importApp()}>
Import app
</ActionButton>
</div>
</Layout>
<style>
.body {
display: flex;
gap: var(--spacing-l);
}
</style>

View File

@ -0,0 +1,220 @@
<script>
import {
Layout,
Divider,
Heading,
Body,
Button,
Modal,
Icon,
} from "@budibase/bbui"
import UpdateAppForm from "@/components/common/UpdateAppForm.svelte"
import { isOnlyUser, appStore, deploymentStore } from "@/stores/builder"
import VersionModal from "@/components/deploy/VersionModal.svelte"
import { appsStore } from "@/stores/portal"
import ExportAppModal from "@/components/start/ExportAppModal.svelte"
import ImportAppModal from "@/components/start/ImportAppModal.svelte"
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
import RevertModal from "@/components/deploy/RevertModal.svelte"
import DeleteModal from "@/components/deploy/DeleteModal.svelte"
let versionModal
let exportModal
let importModal
let exportPublishedVersion = false
let unpublishModal
let revertModal
let deleteModal
$: filteredApps = $appsStore.apps.filter(app => app.devId === $appStore.appId)
$: selectedApp = filteredApps.length ? filteredApps[0] : {}
$: updateAvailable = $appStore.upgradableVersion !== $appStore.version
const exportApp = opts => {
exportPublishedVersion = !!opts?.published
exportModal.show()
}
</script>
<Layout noPadding>
<Layout gap="XS" noPadding>
<Heading>General settings</Heading>
<Body>Control app version, deployment and settings</Body>
</Layout>
<Divider />
<Heading size="S">App info</Heading>
<UpdateAppForm />
<Divider />
<Heading size="S">Deployment</Heading>
{#if $deploymentStore.isPublished}
<div class="row top">
<Icon
name="CheckmarkCircle"
color="var(--spectrum-global-color-green-400)"
size="L"
/>
<Body size="S">
{$deploymentStore.lastPublished}
<br />
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="link" on:click={deploymentStore.viewPublishedApp}>
View app
</div>
</Body>
</div>
<div class="row">
<Button warning on:click={unpublishModal?.show}>Unpublish</Button>
<Button secondary on:click={revertModal?.show}>Revert changes</Button>
</div>
{:else}
<div class="row">
<Icon
name="Alert"
color="var(--spectrum-global-color-yellow-400)"
size="M"
/>
<Body size="S">
Your app hasn't been published yet and isn't available to users
</Body>
</div>
<div class="row">
<Button
cta
disabled={$deploymentStore.isPublishing}
on:click={deploymentStore.publishApp}
>
Publish
</Button>
</div>
{/if}
<Divider />
<Layout gap="XS" noPadding>
<Heading size="S">App version</Heading>
{#if updateAvailable}
<Body size="S">
The app is currently using version <strong>{$appStore.version}</strong>
but version <strong>{$appStore.upgradableVersion}</strong> is available.
<br />
Updates can contain new features, performance improvements and bug fixes.
</Body>
<div class="buttons">
<Button
cta
on:click={versionModal.show}
disabled={!$isOnlyUser}
tooltip={$isOnlyUser
? null
: "Unavailable - another user is editing this app"}
>
Update version
</Button>
</div>
{:else}
<Body size="S">
The app is currently using version <strong>{$appStore.version}</strong>.
<br />
You're running the latest!
</Body>
<div class="buttons">
<Button
secondary
on:click={versionModal.show}
tooltip={$isOnlyUser
? null
: "Unavailable - another user is editing this app"}
>
Revert version
</Button>
</div>
{/if}
</Layout>
<Divider />
<Layout noPadding gap="XS">
<Heading size="S">Export</Heading>
<Body size="S">
Export your app for backup or to share it with someone else
</Body>
</Layout>
<div class="row">
<Button secondary on:click={() => exportApp({ published: false })}>
Export latest edited app
</Button>
<Button
secondary
disabled={!$deploymentStore.isPublished}
on:click={() => exportApp({ published: true })}
>
Export latest published app
</Button>
</div>
<Divider />
<Layout noPadding gap="XS">
<Heading size="S">Import</Heading>
<Body size="S">Import an app export bundle to update this app</Body>
</Layout>
<div class="row">
<Button secondary on:click={importModal?.show}>Import app</Button>
</div>
<Divider />
<Heading size="S">Danger zone</Heading>
<div class="row">
<Button
warning
disabled={!$isOnlyUser}
on:click={() => {
deleteModal.show()
}}
tooltip={$isOnlyUser
? undefined
: "Unavailable - another user is editing this app"}
>
Delete app
</Button>
</div>
</Layout>
<VersionModal bind:this={versionModal} hideIcon={true} />
<Modal bind:this={exportModal} padding={false}>
<ExportAppModal app={selectedApp} published={exportPublishedVersion} />
</Modal>
<Modal bind:this={importModal} padding={false}>
<ImportAppModal app={selectedApp} />
</Modal>
<ConfirmDialog
bind:this={unpublishModal}
title="Confirm unpublish"
okText="Unpublish app"
onOk={deploymentStore.unpublishApp}
>
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
</ConfirmDialog>
<RevertModal bind:this={revertModal} />
<DeleteModal
bind:this={deleteModal}
appId={$appStore.appId}
appName={$appStore.name}
/>
<style>
.link {
text-decoration: underline;
color: var(--spectrum-global-color-gray-900);
}
.link:hover {
cursor: pointer;
filter: brightness(110%);
}
.row {
display: flex;
gap: var(--spacing-m);
}
.buttons {
margin-top: var(--spacing-xl);
}
</style>

View File

@ -1,5 +1,5 @@
<script>
import { redirect } from "@roxi/routify"
$redirect("../settings/automations")
$redirect("./general")
</script>

View File

@ -1,13 +0,0 @@
<script>
import { Layout, Divider, Heading, Body } from "@budibase/bbui"
import UpdateAppForm from "@/components/common/UpdateAppForm.svelte"
</script>
<Layout noPadding>
<Layout gap="XS" noPadding>
<Heading>Name and URL</Heading>
<Body>Edit your app's name and URL</Body>
</Layout>
<Divider />
<UpdateAppForm />
</Layout>

View File

@ -107,7 +107,7 @@
<Layout noPadding>
<Layout gap="XS" noPadding>
<div class="title-section">
<Heading>Progressive Web App</Heading>
<Heading>Progressive web app</Heading>
{#if !pwaEnabled}
<Tags>
<Tag icon="LockClosed">Enterprise</Tag>

View File

@ -1,56 +0,0 @@
<script>
import { Layout, Heading, Body, Divider, Button } from "@budibase/bbui"
import { isOnlyUser, appStore } from "@/stores/builder"
import VersionModal from "@/components/deploy/VersionModal.svelte"
let versionModal
$: updateAvailable = $appStore.upgradableVersion !== $appStore.version
</script>
<Layout noPadding>
<Layout gap="XS" noPadding>
<Heading>Version</Heading>
<Body>See the current version of your app and check for updates</Body>
</Layout>
<Divider />
{#if updateAvailable}
<Body>
The app is currently using version <strong>{$appStore.version}</strong>
but version <strong>{$appStore.upgradableVersion}</strong> is available.
<br />
Updates can contain new features, performance improvements and bug fixes.
</Body>
<div>
<Button
cta
on:click={versionModal.show}
disabled={!$isOnlyUser}
tooltip={$isOnlyUser
? null
: "Unavailable - another user is editing this app"}
>
Update app
</Button>
</div>
{:else}
<Body>
The app is currently using version <strong>{$appStore.version}</strong>.
<br />
You're running the latest!
</Body>
<div>
<Button
secondary
on:click={versionModal.show}
tooltip={$isOnlyUser
? null
: "Unavailable - another user is editing this app"}
>
Revert app
</Button>
</div>
{/if}
</Layout>
<VersionModal bind:this={versionModal} hideIcon={true} />

View File

@ -149,7 +149,7 @@
// check if access via group for creator
const foundGroup = $groups?.find(
group => group.roles[prodAppId] || group.builder?.apps[prodAppId]
group => group.roles?.[prodAppId] || group.builder?.apps[prodAppId]
)
if (foundGroup.builder?.apps[prodAppId]) {
return Constants.Roles.CREATOR

View File

@ -0,0 +1,143 @@
import { type Writable, get, type Readable, derived } from "svelte/store"
import { API } from "@/api"
import { notifications } from "@budibase/bbui"
import { DeploymentProgressResponse, DeploymentStatus } from "@budibase/types"
import analytics, { Events, EventSource } from "@/analytics"
import { appsStore } from "@/stores/portal/apps"
import { DerivedBudiStore } from "@/stores/BudiStore"
import { appStore } from "./app"
import { processStringSync } from "@budibase/string-templates"
interface DeploymentState {
deployments: DeploymentProgressResponse[]
isPublishing: boolean
}
interface DerivedDeploymentState extends DeploymentState {
isPublished: boolean
lastPublished?: string
}
class DeploymentStore extends DerivedBudiStore<
DeploymentState,
DerivedDeploymentState
> {
constructor() {
const makeDerivedStore = (
store: Writable<DeploymentState>
): Readable<DerivedDeploymentState> => {
return derived(
[store, appStore, appsStore],
([$store, $appStore, $appsStore]) => {
// Determine whether the app is published
const app = $appsStore.apps.find(app => app.devId === $appStore.appId)
const deployments = $store.deployments.filter(
x => x.status === DeploymentStatus.SUCCESS
)
const isPublished =
app?.status === "published" && !!deployments.length
// Generate last published string
let lastPublished = undefined
if (isPublished) {
lastPublished = processStringSync(
"Your app was last published {{ duration time 'millisecond' }} ago",
{
time:
new Date().getTime() -
new Date(deployments[0].updatedAt).getTime(),
}
)
}
return {
...$store,
isPublished,
lastPublished,
}
}
)
}
super(
{
deployments: [],
isPublishing: false,
},
makeDerivedStore
)
this.load = this.load.bind(this)
this.publishApp = this.publishApp.bind(this)
this.completePublish = this.completePublish.bind(this)
this.unpublishApp = this.unpublishApp.bind(this)
}
async load() {
try {
const deployments = await API.getAppDeployments()
this.update(state => ({
...state,
deployments,
}))
} catch (err) {
notifications.error("Error fetching deployments")
}
}
async publishApp(showNotification = true) {
try {
this.update(state => ({ ...state, isPublishing: true }))
await API.publishAppChanges(get(appStore).appId)
if (showNotification) {
notifications.send("App published successfully", {
type: "success",
icon: "GlobeCheck",
})
}
await this.completePublish()
} catch (error: any) {
analytics.captureException(error)
const message = error?.message ? ` - ${error.message}` : ""
notifications.error(`Error publishing app${message}`)
}
this.update(state => ({ ...state, isPublishing: false }))
}
async completePublish() {
try {
await appsStore.load()
await this.load()
} catch (err) {
notifications.error("Error refreshing app")
}
}
async unpublishApp() {
if (!get(this.derivedStore).isPublished) {
return
}
try {
await API.unpublishApp(get(appStore).appId)
await appsStore.load()
notifications.send("App unpublished", {
type: "success",
icon: "GlobeStrike",
})
} catch (err) {
notifications.error("Error unpublishing app")
}
}
viewPublishedApp() {
const app = get(appStore)
analytics.captureEvent(Events.APP_VIEW_PUBLISHED, {
appId: app.appId,
eventSource: EventSource.PORTAL,
})
if (get(appStore).url) {
window.open(`/app${app.url}`)
} else {
window.open(`/${app.appId}`)
}
}
}
export const deploymentStore = new DeploymentStore()

View File

@ -1,23 +0,0 @@
import { writable, type Writable } from "svelte/store"
import { API } from "@/api"
import { notifications } from "@budibase/bbui"
import { DeploymentProgressResponse } from "@budibase/types"
export const createDeploymentStore = () => {
let store: Writable<DeploymentProgressResponse[]> = writable([])
const load = async (): Promise<void> => {
try {
store.set(await API.getAppDeployments())
} catch (err) {
notifications.error("Error fetching deployments")
}
}
return {
subscribe: store.subscribe,
load,
}
}
export const deploymentStore = createDeploymentStore()

View File

@ -14,7 +14,7 @@ import {
evaluationContext,
} from "./automations"
import { userStore, userSelectedResourceMap, isOnlyUser } from "./users"
import { deploymentStore } from "./deployments"
import { deploymentStore } from "./deployment"
import { contextMenuStore } from "./contextMenu"
import { snippets } from "./snippets"
import {
@ -36,7 +36,6 @@ import { queries } from "./queries"
import { flags } from "./flags"
import { rowActions } from "./rowActions"
import componentTreeNodesStore from "./componentTreeNodes"
import { appPublished } from "./published"
import { oauth2 } from "./oauth2"
import { FetchAppPackageResponse } from "@budibase/types"
@ -75,7 +74,6 @@ export {
hoverStore,
snippets,
rowActions,
appPublished,
evaluationContext,
screenComponentsList,
screenComponentErrors,

View File

@ -1,16 +0,0 @@
import { appStore } from "./app"
import { appsStore } from "@/stores/portal/apps"
import { deploymentStore } from "./deployments"
import { derived, type Readable } from "svelte/store"
import { DeploymentProgressResponse, DeploymentStatus } from "@budibase/types"
export const appPublished: Readable<boolean> = derived(
[appStore, appsStore, deploymentStore],
([$appStore, $appsStore, $deploymentStore]) => {
const app = $appsStore.apps.find(app => app.devId === $appStore.appId)
const deployments = $deploymentStore.filter(
(x: DeploymentProgressResponse) => x.status === DeploymentStatus.SUCCESS
)
return app?.status === "published" && deployments.length > 0
}
)

View File

@ -2,10 +2,10 @@ import { it, expect, describe, beforeEach, vi } from "vitest"
import { AdminStore } from "./admin"
import { writable, get } from "svelte/store"
import { API } from "@/api"
import { auth } from "@/stores/portal"
import { auth } from "./auth"
import { banner } from "@budibase/bbui"
vi.mock("@/stores/portal", () => {
vi.mock("./auth", () => {
return { auth: vi.fn() }
})

View File

@ -1,6 +1,6 @@
import { get } from "svelte/store"
import { API } from "@/api"
import { auth } from "@/stores/portal"
import { auth } from "./auth"
import { banner } from "@budibase/bbui"
import {
ConfigChecklistResponse,

View File

@ -1,6 +1,6 @@
import { get } from "svelte/store"
import { API } from "@/api"
import { admin } from "@/stores/portal"
import { admin } from "./admin"
import analytics from "@/analytics"
import { BudiStore } from "@/stores/BudiStore"
import {

View File

@ -639,6 +639,7 @@
"hAlign": "center",
"vAlign": "center"
},
"styles": ["margin", "background", "font"],
"settings": [
{
"type": "text",

View File

@ -23,6 +23,9 @@
$: $component.editing && node?.focus()
$: componentText = getComponentText(text, $builderStore, $component)
$: customBg =
$component.styles?.normal?.background ||
$component.styles?.normal?.["background-image"]
const getComponentText = (text, builderState, componentState) => {
if (componentState.editing) {
@ -49,16 +52,17 @@
{#key $component.editing}
<button
use:styleable={$component.styles}
class={`spectrum-Button spectrum-Button--size${size} spectrum-Button--${type} gap-${gap}`}
class:spectrum-Button--quiet={quiet}
disabled={disabled || handlingOnClick}
use:styleable={$component.styles}
on:click={handleOnClick}
contenteditable={$component.editing && !icon}
on:blur={$component.editing ? updateText : null}
bind:this={node}
class:custom={customBg}
class:active
bind:this={node}
on:click={handleOnClick}
on:blur={$component.editing ? updateText : null}
on:input={() => (touched = true)}
disabled={disabled || handlingOnClick}
contenteditable={$component.editing && !icon}
>
{#if icon}
<i class="{icon} {size}" />
@ -75,6 +79,13 @@
overflow: hidden;
text-overflow: ellipsis;
}
button.custom {
transition: filter 130ms ease-out;
border-width: 0;
}
button.custom:hover {
filter: brightness(1.1);
}
.spectrum-Button--overBackground:hover {
color: #555;
}

View File

@ -1,4 +1,4 @@
FROM node:20-alpine
FROM node:22-alpine
LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="scripts/watchtower-hooks/pre-check.sh"
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-hooks/pre-update.sh"

View File

@ -169,8 +169,10 @@ function enrichParameters(
validateQueryInputs(requestParameters)
// make sure parameters are fully enriched with defaults
for (const parameter of query.parameters) {
let value: string | null =
requestParameters[parameter.name] || parameter.default
let value = requestParameters[parameter.name]
if (value == null) {
value = parameter.default
}
if (query.nullDefaultSupport && paramNotSet(value)) {
value = null
}

View File

@ -642,6 +642,35 @@ if (descriptions.length) {
expect(rows).toHaveLength(1)
}
)
it("should be able to create a new row with a JS value of 0 without falling back to the default", async () => {
const query = await createQuery({
name: "New Query",
queryVerb: "create",
fields: {
sql: client(tableName)
.insert({ number: client.raw("{{ number }}") })
.toString(),
},
parameters: [
{
name: "number",
default: "999",
},
],
})
await config.api.query.execute(query._id!, {
parameters: { number: 0 },
})
const rows = await client(tableName).select("*")
expect(rows).toHaveLength(6)
expect(rows[5].number).toEqual(0)
const rowsWith999 = rows.filter(row => row.number === 999)
expect(rowsWith999).toHaveLength(0)
})
})
describe("read", () => {

View File

@ -16,7 +16,7 @@ export interface QueryEvent
ctx?: any
}
export type QueryEventParameters = Record<string, string | null>
export type QueryEventParameters = Record<string, string | number | null>
export interface QueryResponse {
rows: Row[]

View File

@ -34,7 +34,7 @@ export interface PreviewQueryResponse {
}
export interface ExecuteQueryRequest {
parameters?: Record<string, string>
parameters?: Record<string, string | number | null>
pagination?: any
}
export type ExecuteV1QueryResponse = Record<string, any>[]

View File

@ -1,4 +1,4 @@
FROM node:20-alpine
FROM node:22-alpine
LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="scripts/watchtower-hooks/pre-check.sh"
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-hooks/pre-update.sh"