Merge branch 'master' into BUDI-9296/new-screen-as-modal
This commit is contained in:
commit
b16f4d301d
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
|
||||
<Modal bind:this={revertModal}>
|
||||
<ModalContent
|
||||
title="Revert Changes"
|
||||
title="Revert changes"
|
||||
confirmText="Revert"
|
||||
onConfirm={revert}
|
||||
disabled={appName !== $appStore.name}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("../settings/automations")
|
||||
$redirect("./general")
|
||||
</script>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
|
@ -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() }
|
||||
})
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -639,6 +639,7 @@
|
|||
"hAlign": "center",
|
||||
"vAlign": "center"
|
||||
},
|
||||
"styles": ["margin", "background", "font"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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>[]
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue