1
0
Fork 0
mirror of synced 2024-07-07 07:15:43 +12:00

Merge branch 'develop' into backmerge-20230711

This commit is contained in:
Adria Navarro 2023-07-11 15:34:58 +01:00 committed by GitHub
commit c7f6f6b73c
150 changed files with 2784 additions and 1735 deletions

View file

@ -5,7 +5,7 @@
"jest": true,
"node": true
},
"parser": "babel-eslint",
"parser": "@babel/eslint-parser",
"parserOptions": {
"ecmaVersion": 2019,
"sourceType": "module",
@ -18,17 +18,23 @@
"*.spec.js",
"bundle.js"
],
"plugins": ["svelte3"],
"extends": ["eslint:recommended"],
"overrides": [
{
"files": ["*.svelte"],
"processor": "svelte3/svelte3"
"files": ["**/*.svelte"],
"extends": "plugin:svelte/recommended",
"parser": "svelte-eslint-parser",
"parserOptions": {
"parser": "@babel/eslint-parser",
"ecmaVersion": 2019,
"sourceType": "module",
"allowImportExportEverywhere": true
}
},
{
"files": ["**/*.ts"],
"parser": "@typescript-eslint/parser",
"plugins": [],
"extends": ["eslint:recommended"],
"rules": {
"no-unused-vars": "off",
@ -41,7 +47,8 @@
}
],
"rules": {
"no-self-assign": "off"
"no-self-assign": "off",
"no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_", "destructuredArrayIgnorePattern": "^_" }]
},
"globals": {
"GeolocationPositionError": true

19
.github/stale.yml vendored
View file

@ -1,19 +0,0 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 60
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: false
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- roadmap
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View file

@ -185,7 +185,7 @@ jobs:
pro_commit=$(git rev-parse HEAD)
branch="${{ github.base_ref || github.ref_name }}"
echo "Running on branch `$branch` (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})"
echo "Running on branch '$branch' (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})"
if [[ $branch == "master" ]]; then
base_commit=$(git rev-parse origin/master)

View file

@ -34,7 +34,6 @@ jobs:
exit 1
fi
- uses: actions/setup-node@v1
with:
node-version: 14.x
@ -58,9 +57,12 @@ jobs:
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
yarn release
- name: "Get Previous tag"
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
- name: "Get Current tag"
id: currenttag
run: |
version=v$(./scripts/getCurrentVersion.sh)
echo 'Using tag $version'
echo "::set-output name=tag::$resversionult"
- name: Build/release Docker images
run: |
@ -69,7 +71,7 @@ jobs:
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.tag }}
release-helm-chart:
needs: [release-images]

29
.github/workflows/stale_bot.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Close stale issues and PRs # https://github.com/actions/stale
on:
workflow_dispatch:
schedule:
- cron: '30 1 * * *' # 1:30 every morning
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8
with:
# stale rules
days-before-stale: 60
days-before-pr-stale: 7
stale-issue-label: stale
stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for 60 days."
# close rules
# days after being marked as stale to close
days-before-close: 30
close-issue-label: closed-stale
close-issue-message: This issue has been automatically closed it has not had any activity in 90 days."
days-before-pr-close: 7
# exemptions
exempt-pr-labels: pinned,security,roadmap

3
babel.config.json Normal file
View file

@ -0,0 +1,3 @@
{
"presets": [["@babel/preset-env", { "targets": { "node": "current" } }]]
}

View file

@ -28,3 +28,4 @@ BB_ADMIN_USER_PASSWORD=
# A path that is watched for plugin bundles. Any bundles found are imported automatically/
PLUGINS_DIR=
ROLLING_LOG_MAX_SIZE=

12
nx.json
View file

@ -1,9 +1,13 @@
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"runner": "nx-cloud",
"options": {
"cacheableOperations": ["build", "test"]
"cacheableOperations": [
"build",
"test"
],
"accessToken": "YWNiYzc5NTEtMzMzZC00NDhjLTgyNjktZTllMjI1MzM4OGQxfHJlYWQtd3JpdGU="
}
}
},
@ -11,7 +15,9 @@
"dev:builder": {
"dependsOn": [
{
"projects": ["@budibase/string-templates"],
"projects": [
"@budibase/string-templates"
],
"target": "build"
}
]

View file

@ -3,27 +3,32 @@
"private": true,
"devDependencies": {
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
"@nx/js": "16.2.1",
"@nx/js": "16.4.3",
"@rollup/plugin-json": "^4.0.2",
"@typescript-eslint/parser": "5.45.0",
"babel-eslint": "^10.0.3",
"esbuild": "^0.17.18",
"esbuild-node-externals": "^1.7.0",
"eslint": "^7.28.0",
"eslint": "^8.44.0",
"eslint-plugin-cypress": "^2.11.3",
"eslint-plugin-svelte3": "^3.2.0",
"husky": "^8.0.3",
"js-yaml": "^4.1.0",
"kill-port": "^1.6.1",
"lerna": "7.0.2",
"lerna": "7.1.1",
"madge": "^6.0.0",
"minimist": "^1.2.8",
"nx": "16.4.3",
"nx-cloud": "16.0.5",
"prettier": "2.8.8",
"prettier-plugin-svelte": "^2.3.0",
"rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0",
"svelte": "^3.38.2",
"typescript": "4.7.3"
"typescript": "4.7.3",
"@babel/core": "^7.22.5",
"@babel/eslint-parser": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"eslint-plugin-svelte": "^2.32.2",
"svelte-eslint-parser": "^0.32.0"
},
"scripts": {
"preinstall": "node scripts/syncProPackage.js",
@ -41,21 +46,21 @@
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
"nuke:packages": "yarn run restore",
"nuke:docker": "lerna run --stream --parallel dev:stack:nuke",
"nuke:docker": "lerna run --stream dev:stack:nuke",
"clean": "lerna clean",
"kill-builder": "kill-port 3000",
"kill-server": "kill-port 4001 4002",
"kill-all": "yarn run kill-builder && yarn run kill-server",
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder --stream",
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && yarn build --projects=@budibase/client && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
"dev": "yarn run kill-all && lerna run --stream dev:builder --stream",
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && yarn build --projects=@budibase/client && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server",
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
"dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
"test": "lerna run --stream test --stream",
"lint:eslint": "eslint packages && eslint qa-core",
"lint:eslint": "eslint packages qa-core --max-warnings=0",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
"lint": "yarn run lint:eslint && yarn run lint:prettier",
"lint:fix:eslint": "eslint --fix packages qa-core",
"lint:fix:eslint": "eslint --fix --max-warnings=0 packages qa-core",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"build:specs": "lerna run --stream specs",

View file

@ -51,6 +51,7 @@
"pouchdb": "7.3.0",
"pouchdb-find": "7.2.2",
"redlock": "4.2.0",
"rotating-file-stream": "3.1.0",
"sanitize-s3-objectkey": "0.0.1",
"semver": "7.3.7",
"tar-fs": "2.1.1",

View file

@ -47,7 +47,10 @@ function httpLogging() {
return process.env.HTTP_LOGGING
}
function findVersion() {
function getPackageJsonFields(): {
VERSION: string
SERVICE_NAME: string
} {
function findFileInAncestors(
fileName: string,
currentDir: string
@ -69,10 +72,14 @@ function findVersion() {
try {
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
const content = readFileSync(packageJsonFile!, "utf-8")
return JSON.parse(content).version
const parsedContent = JSON.parse(content)
return {
VERSION: parsedContent.version,
SERVICE_NAME: parsedContent.name,
}
} catch {
// throwing an error here is confusing/causes backend-core to be hard to import
return undefined
return { VERSION: "", SERVICE_NAME: "" }
}
}
@ -154,13 +161,14 @@ const environment = {
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
? process.env.ENABLE_SSO_MAINTENANCE_MODE
: false,
VERSION: findVersion(),
...getPackageJsonFields(),
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
_set(key: any, value: any) {
process.env[key] = value
// @ts-ignore
environment[key] = value
},
ROLLING_LOG_MAX_SIZE: process.env.ROLLING_LOG_MAX_SIZE || "10M",
}
// clean up any environment variable edge cases

View file

@ -1,6 +1,7 @@
export * as correlation from "./correlation/correlation"
export { logger } from "./pino/logger"
export * from "./alerts"
export * as system from "./system"
// turn off or on context logging i.e. tenantId, appId etc
export let LOG_CONTEXT = true

View file

@ -1,10 +1,15 @@
import env from "../../environment"
import pino, { LoggerOptions } from "pino"
import pinoPretty from "pino-pretty"
import { IdentityType } from "@budibase/types"
import env from "../../environment"
import * as context from "../../context"
import * as correlation from "../correlation"
import { IdentityType } from "@budibase/types"
import { LOG_CONTEXT } from "../index"
import { localFileDestination } from "../system"
// LOGGER
let pinoInstance: pino.Logger | undefined
@ -16,22 +21,27 @@ if (!env.DISABLE_PINO_LOGGER) {
return { level: label.toUpperCase() }
},
bindings: () => {
return {}
return {
service: env.SERVICE_NAME,
}
},
},
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
}
const destinations: pino.DestinationStream[] = []
if (env.isDev()) {
pinoOptions.transport = {
target: "pino-pretty",
options: {
singleLine: true,
},
}
destinations.push(pinoPretty({ singleLine: true }))
}
pinoInstance = pino(pinoOptions)
if (env.SELF_HOSTED) {
destinations.push(localFileDestination())
}
pinoInstance = destinations.length
? pino(pinoOptions, pino.multistream(destinations))
: pino(pinoOptions)
// CONSOLE OVERRIDES

View file

@ -0,0 +1,81 @@
import fs from "fs"
import path from "path"
import * as rfs from "rotating-file-stream"
import env from "../environment"
import { budibaseTempDir } from "../objectStore"
const logsFileName = `budibase.log`
const budibaseLogsHistoryFileName = "budibase-logs-history.txt"
const logsPath = path.join(budibaseTempDir(), "systemlogs")
function getFullPath(fileName: string) {
return path.join(logsPath, fileName)
}
export function getSingleFileMaxSizeInfo(totalMaxSize: string) {
const regex = /(\d+)([A-Za-z])/
const match = totalMaxSize?.match(regex)
if (!match) {
console.warn(`totalMaxSize does not have a valid value`, {
totalMaxSize,
})
return undefined
}
const size = +match[1]
const unit = match[2]
if (size === 1) {
switch (unit) {
case "B":
return { size: `${size}B`, totalHistoryFiles: 1 }
case "K":
return { size: `${(size * 1000) / 2}B`, totalHistoryFiles: 1 }
case "M":
return { size: `${(size * 1000) / 2}K`, totalHistoryFiles: 1 }
case "G":
return { size: `${(size * 1000) / 2}M`, totalHistoryFiles: 1 }
default:
return undefined
}
}
if (size % 2 === 0) {
return { size: `${size / 2}${unit}`, totalHistoryFiles: 1 }
}
return { size: `1${unit}`, totalHistoryFiles: size - 1 }
}
export function localFileDestination() {
const fileInfo = getSingleFileMaxSizeInfo(env.ROLLING_LOG_MAX_SIZE)
const outFile = rfs.createStream(logsFileName, {
// As we have a rolling size, we want to half the max size
size: fileInfo?.size,
path: logsPath,
maxFiles: fileInfo?.totalHistoryFiles || 1,
immutable: true,
history: budibaseLogsHistoryFileName,
initialRotation: false,
})
return outFile
}
export function getLogReadStream() {
const streams = []
const historyFile = getFullPath(budibaseLogsHistoryFileName)
if (fs.existsSync(historyFile)) {
const fileContent = fs.readFileSync(historyFile, "utf-8")
const historyFiles = fileContent.split("\n")
for (const historyFile of historyFiles.filter(x => x)) {
streams.push(fs.readFileSync(historyFile))
}
}
streams.push(fs.readFileSync(getFullPath(logsFileName)))
const combinedContent = Buffer.concat(streams)
return combinedContent
}

View file

@ -0,0 +1,61 @@
import { getSingleFileMaxSizeInfo } from "../system"
describe("system", () => {
describe("getSingleFileMaxSizeInfo", () => {
it.each([
["100B", "50B"],
["200K", "100K"],
["20M", "10M"],
["4G", "2G"],
])(
"Halving even number (%s) returns halved size and 1 history file (%s)",
(totalValue, expectedMaxSize) => {
const result = getSingleFileMaxSizeInfo(totalValue)
expect(result).toEqual({
size: expectedMaxSize,
totalHistoryFiles: 1,
})
}
)
it.each([
["5B", "1B", 4],
["17K", "1K", 16],
["21M", "1M", 20],
["3G", "1G", 2],
])(
"Halving an odd number (%s) returns as many files as size (-1) (%s)",
(totalValue, expectedMaxSize, totalHistoryFiles) => {
const result = getSingleFileMaxSizeInfo(totalValue)
expect(result).toEqual({
size: expectedMaxSize,
totalHistoryFiles,
})
}
)
it.each([
["1B", "1B"],
["1K", "500B"],
["1M", "500K"],
["1G", "500M"],
])(
"Halving '%s' returns halved unit (%s)",
(totalValue, expectedMaxSize) => {
const result = getSingleFileMaxSizeInfo(totalValue)
expect(result).toEqual({
size: expectedMaxSize,
totalHistoryFiles: 1,
})
}
)
it.each([[undefined], [""], ["50"], ["wrongvalue"]])(
"Halving wrongly formatted value ('%s') returns undefined",
totalValue => {
const result = getSingleFileMaxSizeInfo(totalValue!)
expect(result).toBeUndefined()
}
)
})
})

View file

@ -16,8 +16,6 @@
export let tooltip = undefined
export let newStyles = true
export let id
let showTooltip = false
</script>
<button
@ -35,9 +33,6 @@
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
{disabled}
on:click|preventDefault
on:mouseover={() => (showTooltip = true)}
on:focus={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)}
>
{#if icon}
<svg
@ -52,19 +47,7 @@
{#if $$slots}
<span class="spectrum-Button-label"><slot /></span>
{/if}
{#if !disabled && tooltip}
<div class="tooltip-icon">
<svg
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
focusable="false"
aria-hidden="true"
aria-label="Info"
>
<use xlink:href="#spectrum-icon-18-InfoOutline" />
</svg>
</div>
{/if}
{#if showTooltip && tooltip}
{#if tooltip}
<div class="tooltip">
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
</div>
@ -75,7 +58,6 @@
button {
position: relative;
}
.spectrum-Button-label {
white-space: nowrap;
overflow: hidden;
@ -93,11 +75,13 @@
text-align: center;
transform: translateX(-50%);
left: 50%;
top: calc(100% - 3px);
top: 100%;
opacity: 0;
transition: opacity 130ms ease-out;
pointer-events: none;
}
.tooltip-icon {
padding-left: var(--spacing-m);
line-height: 0;
button:hover .tooltip {
opacity: 1;
}
.spectrum-Button--primary.new-styles {
background: var(--spectrum-global-color-gray-800);

View file

@ -15,8 +15,6 @@
const dispatch = createEventDispatcher()
$: placeholder = !value
const extractProperty = (value, property) => {
if (value && typeof value === "object") {
return value[property]

View file

@ -150,7 +150,7 @@
</div>
{:else if variables.length}
<div style="max-height: 100px">
{#each variables as variable, idx}
{#each variables as variable}
<li
class="spectrum-Menu-item"
role="option"

View file

@ -1,6 +1,7 @@
<script>
import "@spectrum-css/link/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
import Tooltip from "../Tooltip/Tooltip.svelte"
export let href = "#"
export let size = "M"
@ -10,18 +11,61 @@
export let overBackground = false
export let target
export let download
export let disabled = false
export let tooltip = null
const dispatch = createEventDispatcher()
const onClick = e => {
if (!disabled) {
dispatch("click")
e.stopPropagation()
}
}
</script>
<a
on:click={e => dispatch("click") && e.stopPropagation()}
on:click={onClick}
{href}
{target}
{download}
class:disabled
class:spectrum-Link--primary={primary}
class:spectrum-Link--secondary={secondary}
class:spectrum-Link--overBackground={overBackground}
class:spectrum-Link--quiet={quiet}
class="spectrum-Link spectrum-Link--size{size}"><slot /></a
class="spectrum-Link spectrum-Link--size{size}"
>
<slot />
{#if tooltip}
<div class="tooltip">
<Tooltip textWrapping direction="bottom" text={tooltip} />
</div>
{/if}
</a>
<style>
a {
position: relative;
}
a.disabled {
color: var(--spectrum-global-color-gray-500);
}
a.disabled:hover {
text-decoration: none;
cursor: default;
}
.tooltip {
position: absolute;
left: 50%;
top: 100%;
transform: translateX(-50%);
opacity: 0;
transition: 130ms ease-out;
pointer-events: none;
z-index: 100;
}
a:hover .tooltip {
opacity: 1;
}
</style>

View file

@ -1,3 +1,7 @@
<script context="module">
export const keepOpen = Symbol("keepOpen")
</script>
<script>
import "@spectrum-css/dialog/dist/index-vars.css"
import { getContext } from "svelte"
@ -30,7 +34,7 @@
async function secondary(e) {
loading = true
if (!secondaryAction || (await secondaryAction(e)) !== false) {
if (!secondaryAction || (await secondaryAction(e)) !== keepOpen) {
hide()
}
loading = false
@ -38,7 +42,7 @@
async function confirm() {
loading = true
if (!onConfirm || (await onConfirm()) !== false) {
if (!onConfirm || (await onConfirm()) !== keepOpen) {
hide()
}
loading = false
@ -46,7 +50,7 @@
async function close() {
loading = true
if (!onCancel || (await onCancel()) !== false) {
if (!onCancel || (await onCancel()) !== keepOpen) {
cancel()
}
loading = false

View file

@ -90,6 +90,6 @@
.spectrum-Popover {
min-width: var(--spectrum-global-dimension-size-2000);
border-color: var(--spectrum-global-color-gray-300);
overflow: auto;
overflow: visible;
}
</style>

View file

@ -29,7 +29,6 @@
$: type = getType(schema)
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
$: width = schema?.width || "150px"
$: cellValue = getCellValue(value, schema.template)
const getType = schema => {

View file

@ -379,7 +379,7 @@
</div>
{/if}
{#if sortedRows?.length}
{#each sortedRows as row, idx}
{#each sortedRows as row}
<div class="spectrum-Table-row" class:clickable={allowClickRows}>
{#if showEditColumn}
<div

View file

@ -42,7 +42,7 @@ export { default as MenuSection } from "./Menu/Section.svelte"
export { default as MenuSeparator } from "./Menu/Separator.svelte"
export { default as MenuItem } from "./Menu/Item.svelte"
export { default as Modal } from "./Modal/Modal.svelte"
export { default as ModalContent } from "./Modal/ModalContent.svelte"
export { default as ModalContent, keepOpen } from "./Modal/ModalContent.svelte"
export { default as NotificationDisplay } from "./Notification/NotificationDisplay.svelte"
export { default as Notification } from "./Notification/Notification.svelte"
export { default as SideNavigation } from "./SideNavigation/Navigation.svelte"

View file

@ -3,6 +3,7 @@ import { getAutomationStore } from "./store/automation"
import { getTemporalStore } from "./store/temporal"
import { getThemeStore } from "./store/theme"
import { getUserStore } from "./store/users"
import { getDeploymentStore } from "./store/deployments"
import { derived } from "svelte/store"
import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core"
@ -14,6 +15,7 @@ export const automationStore = getAutomationStore()
export const themeStore = getThemeStore()
export const temporalStore = getTemporalStore()
export const userStore = getUserStore()
export const deploymentStore = getDeploymentStore()
// Setup history for screens
export const screenHistoryStore = createHistoryStore({
@ -118,3 +120,20 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
x => x._id === $automationStore.selectedAutomationId
)
})
// Derive map of resource IDs to other users.
// We only ever care about a single user in each resource, so if multiple users
// share the same datasource we can just overwrite them.
export const userSelectedResourceMap = derived(userStore, $userStore => {
let map = {}
$userStore.forEach(user => {
if (user.builderMetadata?.selectedResourceId) {
map[user.builderMetadata?.selectedResourceId] = user
}
})
return map
})
export const isOnlyUser = derived(userStore, $userStore => {
return $userStore.length < 2
})

View file

@ -0,0 +1,22 @@
import { writable } from "svelte/store"
import { API } from "api"
import { notifications } from "@budibase/bbui"
export const getDeploymentStore = () => {
let store = writable([])
const load = async () => {
try {
store.set(await API.getAppDeployments())
} catch (err) {
notifications.error("Error fetching deployments")
}
}
return {
subscribe: store.subscribe,
actions: {
load,
},
}
}

View file

@ -38,6 +38,7 @@ import {
import { makePropSafe as safe } from "@budibase/string-templates"
import { getComponentFieldOptions } from "helpers/formFields"
import { createBuilderWebsocket } from "builderStore/websocket"
import { BuilderSocketEvent } from "@budibase/shared-core"
const INITIAL_FRONTEND_STATE = {
initialised: false,
@ -353,6 +354,33 @@ export const getFrontendStore = () => {
}
return await sequentialScreenPatch(patchFn, screenId)
},
replace: async (screenId, screen) => {
if (!screenId) {
return
}
if (!screen) {
// Screen deletion
store.update(state => ({
...state,
screens: state.screens.filter(x => x._id !== screenId),
}))
} else {
const index = get(store).screens.findIndex(x => x._id === screen._id)
if (index === -1) {
// Screen addition
store.update(state => ({
...state,
screens: [...state.screens, screen],
}))
} else {
// Screen update
store.update(state => {
state.screens[index] = screen
return state
})
}
}
},
delete: async screens => {
const screensToDelete = Array.isArray(screens) ? screens : [screens]
@ -1305,7 +1333,7 @@ export const getFrontendStore = () => {
links: {
save: async (url, title) => {
const navigation = get(store).navigation
let links = [...navigation?.links]
let links = [...(navigation?.links ?? [])]
// Skip if we have an identical link
if (links.find(link => link.url === url && link.text === title)) {
@ -1365,6 +1393,21 @@ export const getFrontendStore = () => {
})
},
},
websocket: {
selectResource: id => {
websocket.emit(BuilderSocketEvent.SelectResource, {
resourceId: id,
})
},
},
metadata: {
replace: metadata => {
store.update(state => ({
...state,
...metadata,
}))
},
},
}
return store

View file

@ -1,10 +1,12 @@
import { createWebsocket } from "@budibase/frontend-core"
import { userStore, store } from "builderStore"
import { userStore, store, deploymentStore } from "builderStore"
import { datasources, tables } from "stores/backend"
import { get } from "svelte/store"
import { auth } from "stores/portal"
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
import { apps } from "stores/portal"
import { notifications } from "@budibase/bbui"
import { helpers } from "@budibase/shared-core"
export const createBuilderWebsocket = appId => {
const socket = createWebsocket("/socket/builder")
@ -31,7 +33,6 @@ export const createBuilderWebsocket = appId => {
})
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
if (userId === get(auth)?.user?._id) {
notifications.success("You can now edit screens and automations")
store.update(state => ({
...state,
hasLock: true,
@ -39,15 +40,32 @@ export const createBuilderWebsocket = appId => {
}
})
// Table events
// Data section events
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
tables.replaceTable(id, table)
})
// Datasource events
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
datasources.replaceDatasource(id, datasource)
})
// Design section events
socket.onOther(BuilderSocketEvent.ScreenChange, ({ id, screen }) => {
store.actions.screens.replace(id, screen)
})
socket.onOther(BuilderSocketEvent.AppMetadataChange, ({ metadata }) => {
store.actions.metadata.replace(metadata)
})
socket.onOther(
BuilderSocketEvent.AppPublishChange,
async ({ user, published }) => {
await apps.load()
if (published) {
await deploymentStore.actions.load()
}
const verb = published ? "published" : "unpublished"
notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`)
}
)
return socket
}

View file

@ -168,7 +168,7 @@
<Layout noPadding gap="XS">
<Detail size="S">Plugins</Detail>
<div class="item-list">
{#each Object.entries(plugins) as [idx, action]}
{#each Object.entries(plugins) as [_, action]}
<div
class="item"
class:selected={selectedAction === action.name}

View file

@ -60,6 +60,7 @@
</script>
<div>
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
{@html html}
</div>

View file

@ -71,7 +71,7 @@
<Layout noPadding gap="XS">
<Label size="S">Trigger</Label>
<div class="item-list">
{#each triggers as [idx, trigger]}
{#each triggers as [_, trigger]}
<div
class="item"
class:selected={selectedTrigger === trigger.name}

View file

@ -3,8 +3,6 @@
import { goto, params } from "@roxi/routify"
import { Table, Heading, Layout } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import CreateEditRow from "./modals/CreateEditRow.svelte"
import CreateEditUser from "./modals/CreateEditUser.svelte"
import {
TableNames,
UNEDITABLE_USER_FIELDS,
@ -33,7 +31,6 @@
$: selectedRows, dispatch("selectionUpdated", selectedRows)
$: isUsersTable = tableId === TableNames.USERS
$: data && resetSelectedRows()
$: editRowComponent = isUsersTable ? CreateEditUser : CreateEditRow
$: {
UNSORTABLE_TYPES.forEach(type => {
Object.values(schema || {}).forEach(col => {

View file

@ -57,7 +57,6 @@
let table = $tables.selected
let confirmDeleteDialog
let deletion
let savingColumn
let deleteColName
let jsonSchemaModal
@ -215,7 +214,6 @@
notifications.success(`Column ${editableColumn.name} deleted`)
confirmDeleteDialog.hide()
hide()
deletion = false
dispatch("updatecolumns")
}
} catch (error) {
@ -266,13 +264,11 @@
function confirmDelete() {
confirmDeleteDialog.show()
deletion = true
}
function hideDeleteDialog() {
confirmDeleteDialog.hide()
deleteColName = ""
deletion = false
}
function getRelationshipOptions(field) {

View file

@ -1,10 +1,9 @@
<script>
import { createEventDispatcher } from "svelte"
import { tables } from "stores/backend"
import { notifications } from "@budibase/bbui"
import { ModalContent, keepOpen, notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte"
import { API } from "api"
import { ModalContent } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
const FORMULA_TYPE = FIELDS.FORMULA.type
@ -41,8 +40,8 @@
} else {
notifications.error(`Failed to save row - ${error.message}`)
}
// Prevent modal closing if there were errors
return false
return keepOpen
}
}
</script>

View file

@ -5,7 +5,7 @@
import { notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte"
import { API } from "api"
import { ModalContent, Select, Link } from "@budibase/bbui"
import { keepOpen, ModalContent, Select, Link } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import { goto } from "@roxi/routify"
@ -51,7 +51,7 @@
errors = [...errors, { message: "Role is required" }]
}
if (errors.length) {
return false
return keepOpen
}
try {
@ -79,8 +79,8 @@
} else {
notifications.error("Error saving user")
}
// Prevent closing the modal on errors
return false
return keepOpen
}
}
</script>

View file

@ -1,5 +1,5 @@
<script>
import { ModalContent, Select, Input, Button } from "@budibase/bbui"
import { keepOpen, ModalContent, Select, Input, Button } from "@budibase/bbui"
import { onMount } from "svelte"
import { API } from "api"
import { notifications } from "@budibase/bbui"
@ -76,7 +76,7 @@
errors.push({ message: "Please choose permissions" })
}
if (errors.length) {
return false
return keepOpen
}
// Save/create the role
@ -85,7 +85,7 @@
notifications.success("Role saved successfully")
} catch (error) {
notifications.error(`Error saving role - ${error.message}`)
return false
return keepOpen
}
}

View file

@ -13,6 +13,7 @@
} from "helpers/data/utils"
import IntegrationIcon from "./IntegrationIcon.svelte"
import { TableNames } from "constants"
import { userSelectedResourceMap } from "builderStore"
let openDataSources = []
@ -166,8 +167,9 @@
selected={$isActive("./table/:tableId") &&
$tables.selected?._id === TableNames.USERS}
on:click={() => selectTable(TableNames.USERS)}
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
/>
{#each enrichedDataSources as datasource, idx}
{#each enrichedDataSources as datasource}
<NavItem
border
text={datasource.name}
@ -176,6 +178,7 @@
withArrow={true}
on:click={() => selectDatasource(datasource)}
on:iconClick={() => toggleNode(datasource)}
selectedBy={$userSelectedResourceMap[datasource._id]}
>
<div class="datasource-icon" slot="icon">
<IntegrationIcon
@ -201,6 +204,7 @@
selected={$isActive("./query/:queryId") &&
$queries.selectedQueryId === query._id}
on:click={() => $goto(`./query/${query._id}`)}
selectedBy={$userSelectedResourceMap[query._id]}
>
<EditQueryPopover {query} />
</NavItem>
@ -212,7 +216,7 @@
<style>
.hierarchy-items-container {
margin: 0 calc(-1 * var(--spacing-xl));
margin: 0 calc(-1 * var(--spacing-l));
}
.datasource-icon {
display: grid;

View file

@ -31,65 +31,65 @@
<path
class="st1"
d="M-83.19,48h-41.79c-1.76,0-3.19-1.43-3.19-3.19V3.02c0-1.76,1.43-3.19,3.19-3.19h41.79
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z"
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z"
/>
<g>
<g>
<path
class="st0"
d="M-99.62,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-93.55,28.92-93.46,28.52-93.46,28.11z"
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-93.55,28.92-93.46,28.52-93.46,28.11z"
/>
</g>
<g>
<path
class="st0"
d="M-114.76,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-108.68,28.92-108.6,28.52-108.6,28.11z"
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-108.68,28.92-108.6,28.52-108.6,28.11z"
/>
</g>
</g>
<path
class="st2"
d="M44.81,159H3.02c-1.76,0-3.19-1.43-3.19-3.19v-41.79c0-1.76,1.43-3.19,3.19-3.19h41.79
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z"
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z"
/>
<g>
<g>
<path
class="st1"
d="M28.38,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C34.45,139.92,34.54,139.52,34.54,139.11z"
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C34.45,139.92,34.54,139.52,34.54,139.11z"
/>
</g>
<g>
<path
class="st1"
d="M13.24,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z"
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z"
/>
</g>
</g>
@ -102,24 +102,24 @@
<path
class="st1"
d="M28.48,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z"
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z"
/>
</g>
<g>
<path
class="st1"
d="M13,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z"
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z"
/>
</g>
</g>

View file

@ -1,6 +1,7 @@
<script>
import { goto } from "@roxi/routify"
import {
keepOpen,
ModalContent,
notifications,
Body,
@ -70,10 +71,9 @@
}
notifications.success(`Imported successfully.`)
return true
} catch (error) {
notifications.error("Error importing queries")
return false
return keepOpen
}
}
</script>

View file

@ -1,5 +1,6 @@
<script>
import {
keepOpen,
Modal,
notifications,
Body,
@ -36,7 +37,7 @@
})
}
return false
return keepOpen
}
let createVariableModal

View file

@ -1,6 +1,7 @@
<script>
import { RelationshipTypes } from "constants/backend"
import {
keepOpen,
Button,
Input,
ModalContent,
@ -277,7 +278,7 @@
async function saveRelationship() {
if (!validate()) {
return false
return keepOpen
}
buildRelationships()
removeExistingRelationship()

View file

@ -1,5 +1,5 @@
import { derived, writable, get } from "svelte/store"
import { notifications } from "@budibase/bbui"
import { keepOpen, notifications } from "@budibase/bbui"
import { datasources, ImportTableError, tables } from "stores/backend"
export const createTableSelectionStore = (integration, datasource) => {
@ -36,8 +36,7 @@ export const createTableSelectionStore = (integration, datasource) => {
notifications.error("Error fetching tables.")
}
// Prevent modal closing
return false
return keepOpen
}
}

View file

@ -6,7 +6,6 @@
let error = null
let fileName = null
let fileType = null
let loading = false
let updateExistingRows = false
@ -74,7 +73,6 @@
const response = await parseFile(e)
rows = response.rows
fileName = response.fileName
fileType = response.fileType
} catch (e) {
loading = false
error = e

View file

@ -7,7 +7,6 @@
let fileInput
let error = null
let fileName = null
let fileType = null
let loading = false
let validation = {}
@ -60,7 +59,6 @@
rows = response.rows
schema = response.schema
fileName = response.fileName
fileType = response.fileType
} catch (e) {
loading = false
error = e

View file

@ -5,6 +5,7 @@
import EditViewPopover from "./popovers/EditViewPopover.svelte"
import NavItem from "components/common/NavItem.svelte"
import { goto, isActive } from "@roxi/routify"
import { userSelectedResourceMap } from "builderStore"
const alphabetical = (a, b) =>
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
@ -30,6 +31,7 @@
selected={$isActive("./table/:tableId") &&
$tables.selected?._id === table._id}
on:click={() => selectTable(table._id)}
selectedBy={$userSelectedResourceMap[table._id]}
>
{#if table._id !== TableNames.USERS}
<EditTablePopover {table} />
@ -42,6 +44,7 @@
text={viewName}
selected={$isActive("./view") && $views.selected?.name === viewName}
on:click={() => $goto(`./view/${encodeURIComponent(viewName)}`)}
selectedBy={$userSelectedResourceMap[viewName]}
>
<EditViewPopover
view={{ name: viewName, ...table.views[viewName] }}

View file

@ -2,21 +2,13 @@
import { goto, url } from "@roxi/routify"
import { tables } from "stores/backend"
import { notifications } from "@budibase/bbui"
import {
Input,
Label,
ModalContent,
Toggle,
Divider,
Layout,
} from "@budibase/bbui"
import { Input, Label, ModalContent, Layout } from "@budibase/bbui"
import { datasources } from "stores/backend"
import TableDataImport from "../TableDataImport.svelte"
import {
BUDIBASE_INTERNAL_DB_ID,
BUDIBASE_DATASOURCE_TYPE,
} from "constants/backend"
import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils"
$: tableNames = $tables.list.map(table => table.name)
$: selectedSource = $datasources.list.find(
@ -43,28 +35,12 @@
}
let error = ""
let autoColumns = getAutoColumnInformation()
let schema = {}
let rows = []
let allValid = true
let displayColumn = null
function getAutoColumns() {
const selectedAutoColumns = {}
Object.entries(autoColumns).forEach(([subtype, column]) => {
if (column.enabled) {
selectedAutoColumns[column.name] = buildAutoColumn(
name,
column.name,
subtype
)
}
})
return selectedAutoColumns
}
function checkValid(evt) {
const tableName = evt.target.value
if (tableNames.includes(tableName)) {
@ -77,7 +53,7 @@
async function saveTable() {
let newTable = {
name,
schema: { ...schema, ...getAutoColumns() },
schema: { ...schema },
rows,
type: "internal",
sourceId: targetDatasourceId,
@ -118,21 +94,6 @@
bind:value={name}
{error}
/>
<div class="autocolumns">
<Label extraSmall grey>Auto Columns</Label>
<div class="toggles">
<div class="toggle-1">
<Toggle text="Created by" bind:value={autoColumns.createdBy.enabled} />
<Toggle text="Created at" bind:value={autoColumns.createdAt.enabled} />
<Toggle text="Auto ID" bind:value={autoColumns.autoID.enabled} />
</div>
<div class="toggle-2">
<Toggle text="Updated by" bind:value={autoColumns.updatedBy.enabled} />
<Toggle text="Updated at" bind:value={autoColumns.updatedAt.enabled} />
</div>
</div>
<Divider />
</div>
<div>
<Layout gap="XS" noPadding>
<Label grey extraSmall
@ -148,24 +109,3 @@
</Layout>
</div>
</ModalContent>
<style>
.autocolumns {
margin-bottom: -10px;
}
.toggles {
display: flex;
width: 100%;
margin-top: 6px;
}
.toggle-1 :global(> *) {
margin-bottom: 10px;
}
.toggle-2 :global(> *) {
margin-bottom: 10px;
margin-left: 20px;
}
</style>

View file

@ -93,42 +93,42 @@
`https://github.com/Budibase/budibase/issues/new?assignees=&labels=bug&template=bug_report.md&title=`
),
},
...$datasources?.list.map(datasource => ({
...($datasources?.list?.map(datasource => ({
type: "Datasource",
name: `${datasource.name}`,
icon: "Data",
action: () => $goto(`./data/datasource/${datasource._id}`),
})),
...$tables?.list.map(table => ({
})) ?? []),
...($tables?.list?.map(table => ({
type: "Table",
name: table.name,
icon: "Table",
action: () => $goto(`./data/table/${table._id}`),
})),
...$views?.list.map(view => ({
})) ?? []),
...($views?.list?.map(view => ({
type: "View",
name: view.name,
icon: "Remove",
action: () => $goto(`./data/view/${view.name}`),
})),
...$queries?.list.map(query => ({
})) ?? []),
...($queries?.list?.map(query => ({
type: "Query",
name: query.name,
icon: "SQLQuery",
action: () => $goto(`./data/query/${query._id}`),
})),
})) ?? []),
...$sortedScreens.map(screen => ({
type: "Screen",
name: screen.routing.route,
icon: "WebPage",
action: () => $goto(`./design/${screen._id}/components`),
})),
...$automationStore?.automations.map(automation => ({
...($automationStore?.automations?.map(automation => ({
type: "Automation",
name: automation.name,
icon: "ShareAndroid",
action: () => $goto(`./automation/${automation._id}`),
})),
})) ?? []),
...Constants.Themes.map(theme => ({
type: "Change Builder Theme",
name: theme.name,
@ -208,8 +208,8 @@
async function deployApp() {
try {
await API.deployAppChanges()
notifications.success("Application published successfully")
await API.publishAppChanges($store.appId)
notifications.success("App published successfully")
} catch (error) {
notifications.error("Error publishing app")
}
@ -237,11 +237,11 @@
<Input bind:value={search} quiet placeholder="Search for command" />
</div>
<div class="commands">
{#each categories as [name, results], catIdx}
{#each categories as [name, results]}
<div class="category">
<Detail>{name}</Detail>
<div class="options">
{#each results as command, cmdIdx}
{#each results as command}
<div
class="command"
on:click={() => runAction(command)}

View file

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

View file

@ -20,4 +20,5 @@
}
</script>
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
{@html substituteSize(svgHtml)}

View file

@ -1,6 +1,7 @@
<script>
import { Icon } from "@budibase/bbui"
import { createEventDispatcher, getContext } from "svelte"
import { helpers } from "@budibase/shared-core"
export let icon
export let withArrow = false
@ -18,12 +19,15 @@
export let rightAlignIcon = false
export let id
export let showTooltip = false
export let selectedBy = null
const scrollApi = getContext("scroll")
const dispatch = createEventDispatcher()
let contentRef
$: selected && contentRef && scrollToView()
$: style = getStyle(indentLevel, selectedBy)
const onClick = () => {
scrollToView()
@ -42,6 +46,14 @@
const bounds = contentRef.getBoundingClientRect()
scrollApi.scrollTo(bounds)
}
const getStyle = (indentLevel, selectedBy) => {
let style = `padding-left:calc(${indentLevel * 14}px);`
if (selectedBy) {
style += `--selected-by-color:${helpers.getUserColor(selectedBy)};`
}
return style
}
</script>
<div
@ -51,8 +63,7 @@
class:withActions
class:scrollable
class:highlighted
style={`padding-left: calc(${indentLevel * 14}px)`}
{draggable}
class:selectedBy
on:dragend
on:dragstart
on:dragover
@ -61,6 +72,8 @@
ondragover="return false"
ondragenter="return false"
{id}
{style}
{draggable}
>
<div class="nav-item-content" bind:this={contentRef}>
{#if withArrow}
@ -97,6 +110,9 @@
</div>
{/if}
</div>
{#if selectedBy}
<div class="selected-by-label">{helpers.getUserLabel(selectedBy)}</div>
{/if}
</div>
<style>
@ -111,6 +127,7 @@
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
position: relative;
}
.nav-item.scrollable {
flex-direction: column;
@ -142,6 +159,37 @@
padding-left: var(--spacing-l);
}
/* Selected user styles */
.nav-item.selectedBy:after {
content: "";
position: absolute;
width: calc(100% - 4px);
height: 28px;
border: 2px solid var(--selected-by-color);
left: 0;
top: 0;
border-radius: 2px;
pointer-events: none;
}
.selected-by-label {
position: absolute;
top: 0;
right: 0;
background: var(--selected-by-color);
padding: 2px 4px;
font-size: 12px;
color: white;
transform: translateY(calc(1px - 100%));
border-top-right-radius: 2px;
border-top-left-radius: 2px;
pointer-events: none;
opacity: 0;
transition: opacity 130ms ease-out;
}
.nav-item.selectedBy:hover .selected-by-label {
opacity: 1;
}
/* Needed to fully display the actions icon */
.nav-item.scrollable .nav-item-content {
padding-right: 1px;

View file

@ -208,7 +208,9 @@
<div class="syntax-error">
Current Handlebars syntax is invalid, please check the
guide
<a href="https://handlebarsjs.com/guide/">here</a>
<a href="https://handlebarsjs.com/guide/" target="_blank"
>here</a
>
for more details.
</div>
{:else}

View file

@ -88,6 +88,7 @@
{/if}
{#if hoverTarget.description}
<div class="helper__description">
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
{@html hoverTarget.description}
</div>
{/if}
@ -124,7 +125,6 @@
/>
</span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
class="search-input-icon"
on:click={() => {
@ -162,7 +162,6 @@
</div>
<ul>
{#each category.bindings as binding}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<li
class="binding"
on:mouseenter={e => {

View file

@ -14,15 +14,12 @@
import RevertModal from "components/deploy/RevertModal.svelte"
import VersionModal from "components/deploy/VersionModal.svelte"
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
import { processStringSync } from "@budibase/string-templates"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import analytics, { Events, EventSource } from "analytics"
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
import { API } from "api"
import { onMount } from "svelte"
import { apps } from "stores/portal"
import { store } from "builderStore"
import { deploymentStore, store, isOnlyUser } from "builderStore"
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
import { goto } from "@roxi/routify"
@ -34,37 +31,31 @@
let updateAppModal
let revertModal
let versionModal
let appActionPopover
let appActionPopoverOpen = false
let appActionPopoverAnchor
let publishing = false
$: filteredApps = $apps.filter(app => app.devId === application)
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
$: deployments = []
$: latestDeployments = deployments
$: latestDeployments = $deploymentStore
.filter(deployment => deployment.status === "SUCCESS")
.sort((a, b) => a.updatedAt > b.updatedAt)
$: isPublished =
selectedApp?.status === "published" && latestDeployments?.length > 0
$: updateAvailable =
$store.upgradableVersion &&
$store.version &&
$store.upgradableVersion !== $store.version
$: canPublish = !publishing && loaded
$: lastDeployed = getLastDeployedString($deploymentStore)
const initialiseApp = async () => {
const applicationPkg = await API.fetchAppPackage($store.devId)
await store.actions.initialise(applicationPkg)
}
const updateDeploymentString = () => {
const getLastDeployedString = deployments => {
return deployments?.length
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
time:
@ -73,27 +64,6 @@
: ""
}
const reviewPendingDeployments = (deployments, newDeployments) => {
if (deployments.length > 0) {
const pending = checkIncomingDeploymentStatus(deployments, newDeployments)
if (pending.length) {
notifications.warning(
"Deployment has been queued and will be processed shortly"
)
}
}
}
async function fetchDeployments() {
try {
const newDeployments = await API.getAppDeployments()
reviewPendingDeployments(deployments, newDeployments)
return newDeployments
} catch (err) {
notifications.error("Error fetching deployment overview")
}
}
const previewApp = () => {
store.update(state => ({
...state,
@ -116,14 +86,11 @@
async function publishApp() {
try {
publishing = true
await API.publishAppChanges($store.appId)
notifications.send("App published", {
notifications.send("App published successfully", {
type: "success",
icon: "GlobeCheck",
})
await completePublish()
} catch (error) {
console.error(error)
@ -163,210 +130,196 @@
const completePublish = async () => {
try {
await apps.load()
deployments = await fetchDeployments()
await deploymentStore.actions.load()
} catch (err) {
notifications.error("Error refreshing app")
}
}
onMount(async () => {
if (!$apps.length) {
await apps.load()
}
deployments = await fetchDeployments()
})
</script>
{#if $store.hasLock}
<div class="action-top-nav" class:has-lock={$store.hasLock}>
<div class="action-buttons">
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#if updateAvailable}
<div class="app-action-button version" on:click={versionModal.show}>
<div class="app-action">
<ActionButton quiet>
<StatusLight notice />
Update
</ActionButton>
</div>
</div>
{/if}
<TourWrap
tourStepKey={$store.onboarding
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
>
<div class="app-action-button users">
<div class="app-action" id="builder-app-users-button">
<ActionButton
quiet
icon="UserGroup"
on:click={() => {
store.update(state => {
state.builderSidePanel = true
return state
})
}}
>
Users
</ActionButton>
</div>
</div>
</TourWrap>
<div class="app-action-button preview">
<div class="action-top-nav">
<div class="action-buttons">
{#if updateAvailable && $isOnlyUser}
<div class="app-action-button version" on:click={versionModal.show}>
<div class="app-action">
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
Preview
<ActionButton quiet>
<StatusLight notice />
Update
</ActionButton>
</div>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="app-action-button publish app-action-popover"
on:click={() => {
if (!appActionPopoverOpen) {
appActionPopover.show()
} else {
appActionPopover.hide()
}
}}
>
<div bind:this={appActionPopoverAnchor}>
<div class="app-action">
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
<span class="publish-open" id="builder-app-publish-button">
Publish
<Icon
name={appActionPopoverOpen ? "ChevronUp" : "ChevronDown"}
size="M"
/>
</span>
</TourWrap>
</div>
{/if}
<TourWrap
tourStepKey={$store.onboarding
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
>
<div class="app-action-button users">
<div class="app-action" id="builder-app-users-button">
<ActionButton
quiet
icon="UserGroup"
on:click={() => {
store.update(state => {
state.builderSidePanel = true
return state
})
}}
>
Users
</ActionButton>
</div>
<Popover
bind:this={appActionPopover}
align="right"
disabled={!isPublished}
anchor={appActionPopoverAnchor}
offset={35}
on:close={() => {
appActionPopoverOpen = false
}}
on:open={() => {
appActionPopoverOpen = true
}}
>
<div class="app-action-popover-content">
<Layout noPadding gap="M">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<Body size="M">
<span
class="app-link"
on:click={() => {
if (isPublished) {
viewApp()
} else {
appActionPopover.hide()
updateAppModal.show()
}
}}
>
{$store.url}
{#if isPublished}
<Icon size="S" name="LinkOut" />
{:else}
<Icon size="S" name="Edit" />
{/if}
</span>
</Body>
</div>
</TourWrap>
<Body size="S">
<span class="publish-popover-status">
{#if isPublished}
<span class="status-text">
{updateDeploymentString(deployments)}
</span>
<span class="unpublish-link">
<Link quiet on:click={unpublishApp}>Unpublish</Link>
</span>
<span class="revert-link">
<Link quiet secondary on:click={revertApp}>Revert</Link>
</span>
{:else}
<span class="status-text unpublished">Not published</span>
{/if}
</span>
</Body>
<div class="action-buttons">
{#if $store.hasLock}
{#if isPublished}
<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>
{/if}
</div>
</Layout>
</div>
</Popover>
<div class="app-action-button preview">
<div class="app-action">
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
Preview
</ActionButton>
</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>
<Modal bind:this={updateAppModal} padding={false} width="600px">
<UpdateAppModal
app={{
name: $store.name,
url: $store.url,
icon: $store.icon,
appId: $store.appId,
<div
class="app-action-button publish app-action-popover"
on:click={() => {
if (!appActionPopoverOpen) {
appActionPopover.show()
} else {
appActionPopover.hide()
}
}}
onUpdateComplete={async () => {
await initialiseApp()
}}
/>
</Modal>
>
<div bind:this={appActionPopoverAnchor}>
<div class="app-action">
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
<span class="publish-open" id="builder-app-publish-button">
Publish
<Icon
name={appActionPopoverOpen ? "ChevronUp" : "ChevronDown"}
size="M"
/>
</span>
</TourWrap>
</div>
</div>
<Popover
bind:this={appActionPopover}
align="right"
disabled={!isPublished}
anchor={appActionPopoverAnchor}
offset={35}
on:close={() => {
appActionPopoverOpen = false
}}
on:open={() => {
appActionPopoverOpen = true
}}
>
<div class="app-action-popover-content">
<Layout noPadding gap="M">
<Body size="M">
<span
class="app-link"
on:click={() => {
if (isPublished) {
viewApp()
} else {
appActionPopover.hide()
updateAppModal.show()
}
}}
>
{$store.url}
{#if isPublished}
<Icon size="S" name="LinkOut" />
{:else}
<Icon size="S" name="Edit" />
{/if}
</span>
</Body>
<RevertModal bind:this={revertModal} />
<VersionModal hideIcon bind:this={versionModal} />
{:else}
<div class="app-action-button preview-locked">
<div class="app-action">
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
Preview
</ActionButton>
<Body size="S">
<span class="publish-popover-status">
{#if isPublished}
<span class="status-text">
{lastDeployed}
</span>
<span class="unpublish-link">
<Link quiet on:click={unpublishApp}>Unpublish</Link>
</span>
<span class="revert-link">
<Link
disabled={!$isOnlyUser}
quiet
secondary
on:click={revertApp}
tooltip="Unavailable - another user is editing this app"
>
Revert
</Link>
</span>
{:else}
<span class="status-text unpublished">Not published</span>
{/if}
</span>
</Body>
<div class="action-buttons">
{#if isPublished}
<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>
</div>
</div>
{/if}
</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>
<Modal bind:this={updateAppModal} padding={false} width="600px">
<UpdateAppModal
app={{
name: $store.name,
url: $store.url,
icon: $store.icon,
appId: $store.appId,
}}
onUpdateComplete={async () => {
await initialiseApp()
}}
/>
</Modal>
<RevertModal bind:this={revertModal} />
<VersionModal hideIcon bind:this={versionModal} />
<style>
.app-action-popover-content {
@ -450,10 +403,6 @@
gap: var(--spectrum-actionbutton-icon-gap);
}
.app-action-button.preview-locked {
padding-right: 0px;
}
.app-action {
display: flex;
align-items: center;

View file

@ -1,118 +0,0 @@
<script>
import {
Button,
Modal,
notifications,
ModalContent,
Layout,
ProgressCircle,
CopyInput,
} from "@budibase/bbui"
import { API } from "api"
import analytics, { Events, EventSource } from "analytics"
import { store } from "builderStore"
import TourWrap from "../portal/onboarding/TourWrap.svelte"
import { TOUR_STEP_KEYS } from "../portal/onboarding/tours.js"
let publishModal
let asyncModal
let publishCompleteModal
let published
$: publishedUrl = published ? `${window.origin}/app${published.appUrl}` : ""
export let onOk
async function publishApp() {
try {
//In Progress
asyncModal.show()
publishModal.hide()
published = await API.publishAppChanges($store.appId)
if (typeof onOk === "function") {
await onOk()
}
//Request completed
asyncModal.hide()
publishCompleteModal.show()
} catch (error) {
analytics.captureException(error)
notifications.error("Error publishing app")
}
}
const viewApp = () => {
if (published) {
analytics.captureEvent(Events.APP_VIEW_PUBLISHED, {
appId: $store.appId,
eventSource: EventSource.PORTAL,
})
window.open(publishedUrl, "_blank")
}
}
</script>
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
<Button cta on:click={publishModal.show} id={"builder-app-publish-button"}>
Publish
</Button>
</TourWrap>
<Modal bind:this={publishModal}>
<ModalContent
title="Publish to production"
confirmText="Publish"
onConfirm={publishApp}
>
The changes you have made will be published to the production version of the
application.
</ModalContent>
</Modal>
<!-- Publish in progress -->
<Modal bind:this={asyncModal}>
<ModalContent
showCancelButton={false}
showConfirmButton={false}
showCloseIcon={false}
>
<Layout justifyItems="center">
<ProgressCircle size="XL" />
</Layout>
</ModalContent>
</Modal>
<!-- Publish complete -->
<Modal bind:this={publishCompleteModal}>
<ModalContent confirmText="Done" cancelText="View App" onCancel={viewApp}>
<div slot="header" class="app-published-header">
<svg
width="26px"
height="26px"
class="spectrum-Icon success-icon"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-GlobeCheck" />
</svg>
<span class="app-published-header-text">App Published!</span>
</div>
<CopyInput value={publishedUrl} label="You can view your app at:" />
</ModalContent>
</Modal>
<style>
.app-published-header {
display: flex;
flex-direction: row;
align-items: center;
}
.success-icon {
color: var(--spectrum-global-color-green-600);
}
.app-published-header .app-published-header-text {
padding-left: var(--spacing-l);
}
</style>

View file

@ -1,236 +0,0 @@
<script>
import { onMount, onDestroy } from "svelte"
import Spinner from "components/common/Spinner.svelte"
import { slide } from "svelte/transition"
import { Heading, Button, Modal, ModalContent } from "@budibase/bbui"
import { API } from "api"
import { notifications } from "@budibase/bbui"
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
import { store } from "builderStore"
import {
checkIncomingDeploymentStatus,
DeploymentStatus,
} from "components/deploy/utils"
const DATE_OPTIONS = {
fullDate: {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
},
timeOnly: {
hour: "numeric",
minute: "numeric",
hourCycle: "h12",
},
}
const POLL_INTERVAL = 5000
export let appId
let modal
let errorReasonModal
let errorReason
let poll
let deployments = []
let urlComponent = $store.url || `/${appId}`
let deploymentUrl = `${urlComponent}`
const formatDate = (date, format) =>
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)
async function fetchDeployments() {
try {
const newDeployments = await API.getAppDeployments()
if (deployments.length > 0) {
const pendingDeployments = checkIncomingDeploymentStatus(
deployments,
newDeployments
)
if (pendingDeployments.length) {
showErrorReasonModal(pendingDeployments[0].err)
}
}
deployments = newDeployments
} catch (err) {
clearInterval(poll)
notifications.error("Error fetching deployment overview")
}
}
function showErrorReasonModal(err) {
if (!err) return
errorReason = err
errorReasonModal.show()
}
onMount(() => {
fetchDeployments()
poll = setInterval(fetchDeployments, POLL_INTERVAL)
})
onDestroy(() => clearInterval(poll))
</script>
{#if deployments.length > 0}
<section class="deployment-history" in:slide>
<header>
<Heading>Deployment History</Heading>
<div class="deploy-div">
{#if deployments.some(deployment => deployment.status === DeploymentStatus.SUCCESS)}
<a target="_blank" href={deploymentUrl}> View Your Deployed App </a>
<Button primary on:click={() => modal.show()}>View webhooks</Button>
{/if}
</div>
</header>
<div class="deployment-list">
{#each deployments as deployment}
<article class="deployment">
<div class="deployment-info">
<span class="deploy-date">
{formatDate(deployment.updatedAt, "fullDate")}
</span>
<span class="deploy-time">
{formatDate(deployment.updatedAt, "timeOnly")}
</span>
</div>
<div class="deployment-right">
{#if deployment.status.toLowerCase() === "pending"}
<Spinner size="10" />
{/if}
<div
on:click={() => showErrorReasonModal(deployment.err)}
class={`deployment-status ${deployment.status}`}
>
<span>
{deployment.status}
{#if deployment.status === DeploymentStatus.FAILURE}
<i class="ri-information-line" />
{/if}
</span>
</div>
</div>
</article>
{/each}
</div>
</section>
{/if}
<Modal bind:this={modal} width="30%">
<CreateWebhookDeploymentModal />
</Modal>
<Modal bind:this={errorReasonModal} width="30%">
<ModalContent
title="Deployment Error"
confirmText="OK"
showCancelButton={false}
>
{errorReason}
</ModalContent>
</Modal>
<style>
section {
padding: var(--spacing-xl) 0;
}
.deployment-list {
height: 40vh;
overflow-y: auto;
}
header {
padding-left: var(--spacing-l);
padding-bottom: var(--spacing-xl);
padding-right: var(--spacing-l);
border-bottom: var(--border-light);
}
.deploy-div {
display: flex;
justify-content: space-between;
align-items: center;
}
.deployment-history {
position: absolute;
bottom: 0;
width: 100%;
background: var(--background);
}
.deployment {
padding: var(--spacing-l);
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: var(--border-light);
}
.deployment:last-child {
border-bottom: none;
}
.deployment-info {
display: flex;
flex-direction: column;
margin-right: var(--spacing-s);
}
.deploy-date {
font-size: var(--font-size-m);
}
.deploy-time {
color: var(--grey-7);
font-weight: 600;
font-size: var(--font-size-s);
}
.deployment-right {
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
}
.deployment-status {
font-size: var(--font-size-s);
padding: var(--spacing-s);
border-radius: var(--border-radius-s);
font-weight: 600;
text-transform: lowercase;
width: 80px;
text-align: center;
}
.deployment-status:first-letter {
text-transform: uppercase;
}
a {
color: var(--blue);
font-weight: 600;
font-size: var(--font-size-s);
}
.SUCCESS {
color: var(--green);
background: var(--green-light);
}
.PENDING {
color: var(--yellow);
background: var(--yellow-light);
}
.FAILURE {
color: var(--red);
background: var(--red-light);
cursor: pointer;
}
i {
position: relative;
top: 2px;
}
</style>

View file

@ -1,25 +0,0 @@
export const DeploymentStatus = {
SUCCESS: "SUCCESS",
PENDING: "PENDING",
FAILURE: "FAILURE",
}
// Required to check any updated deployment statuses between polls
export function checkIncomingDeploymentStatus(current, incoming) {
return incoming.reduce((acc, incomingDeployment) => {
if (incomingDeployment.status === DeploymentStatus.FAILURE) {
const currentDeployment = current.find(
deployment => deployment._id === incomingDeployment._id
)
//We have just been notified of an ongoing deployments failure
if (
!currentDeployment ||
currentDeployment.status === DeploymentStatus.PENDING
) {
acc.push(incomingDeployment)
}
}
return acc
}, [])
}

View file

@ -16,6 +16,7 @@
makeStateBinding,
} from "builderStore/dataBinding"
import { currentAsset, store } from "builderStore"
import { cloneDeep } from "lodash/fp"
const flipDurationMs = 150
const EVENT_TYPE_KEY = "##eventHandlerType"
@ -29,6 +30,26 @@
let actionQuery
let selectedAction = actions?.length ? actions[0] : null
const setUpdateActions = actions => {
return actions
? cloneDeep(actions)
.filter(action => {
return (
action[EVENT_TYPE_KEY] === "Update State" &&
action.parameters?.type === "set" &&
action.parameters.key
)
})
.reduce((acc, action) => {
acc[action.id] = action
return acc
}, {})
: []
}
// Snapshot original action state
let updateStateActions = setUpdateActions(actions)
$: {
// Ensure parameters object is never null
if (selectedAction && !selectedAction.parameters) {
@ -125,8 +146,9 @@
actions = e.detail.items
}
const getAllBindings = (bindings, eventContextBindings, actions) => {
const getAllBindings = (actionBindings, eventContextBindings, actions) => {
let allBindings = []
let cloneActionBindings = cloneDeep(actionBindings)
if (!actions) {
return []
}
@ -144,11 +166,19 @@
.forEach(action => {
// Check we have a binding for this action, and generate one if not
const stateBinding = makeStateBinding(action.parameters.key)
const hasKey = bindings.some(binding => {
const hasKey = actionBindings.some(binding => {
return binding.runtimeBinding === stateBinding.runtimeBinding
})
if (!hasKey) {
bindings.push(stateBinding)
let existing = updateStateActions[action.id]
if (existing) {
const existingBinding = makeStateBinding(existing.parameters.key)
cloneActionBindings = cloneActionBindings.filter(
binding =>
binding.runtimeBinding !== existingBinding.runtimeBinding
)
}
allBindings.push(stateBinding)
}
})
// Get which indexes are asynchronous automations as we want to filter them out from the bindings
@ -164,15 +194,16 @@
.filter(index => index !== undefined)
// Based on the above, filter out the asynchronous automations from the bindings
if (asynchronousAutomationIndexes) {
allBindings = eventContextBindings
.filter((binding, index) => {
let contextBindings = asynchronousAutomationIndexes
? eventContextBindings.filter((binding, index) => {
return !asynchronousAutomationIndexes.includes(index)
})
.concat(bindings)
} else {
allBindings = eventContextBindings.concat(bindings)
}
: eventContextBindings
allBindings = contextBindings
.concat(cloneActionBindings)
.concat(allBindings)
return allBindings
}
</script>

View file

@ -70,8 +70,9 @@
} set`
</script>
<div class="action-count">{actionText}</div>
<ActionButton on:click={openDrawer}>Define actions</ActionButton>
<div class="action-editor">
<ActionButton on:click={openDrawer}>{actionText}</ActionButton>
</div>
<Drawer bind:this={drawer} title={"Actions"}>
<svelte:fragment slot="description">
@ -89,9 +90,7 @@
</Drawer>
<style>
.action-count {
padding-top: 6px;
padding-bottom: var(--spacing-s);
font-weight: 600;
.action-editor :global(.spectrum-ActionButton) {
width: 100%;
}
</style>

View file

@ -42,7 +42,6 @@
}
})
$: hasAutomations = automations && automations.length > 0
$: selectedAutomation = automations?.find(
a => a._id === parameters?.automationId
)
@ -145,12 +144,6 @@
padding-bottom: 20px;
}
.params {
display: flex;
flex-wrap: nowrap;
gap: 25px;
}
.synchronous-info {
display: flex;
gap: var(--spacing-s);

View file

@ -20,6 +20,7 @@
let drawer
let boundValue
$: text = getText(value)
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchema($currentAsset, datasource)
$: options = allowCellEditing
@ -31,6 +32,17 @@
allowLinks: true,
})
const getText = value => {
if (!value?.length) {
return "All columns"
}
let text = `${value.length} column`
if (value.length !== 1) {
text += "s"
}
return text
}
const getSchema = (asset, datasource) => {
const schema = getSchemaForDatasource(asset, datasource).schema
@ -76,7 +88,7 @@
</script>
<div class="column-editor">
<ActionButton on:click={open}>Configure columns</ActionButton>
<ActionButton on:click={open}>{text}</ActionButton>
</div>
<Drawer bind:this={drawer} title="Columns">
<Button cta slot="buttons" on:click={save}>Save</Button>

View file

@ -8,32 +8,39 @@
getSchemaForDatasource,
} from "builderStore/dataBinding"
import { currentAsset } from "builderStore"
import { getFields } from "helpers/searchFields"
export let componentInstance
export let value = []
const convertOldColumnFormat = oldColumns => {
if (typeof oldColumns?.[0] === "string") {
value = oldColumns.map(field => ({ name: field, displayName: field }))
}
}
$: convertOldColumnFormat(value)
const dispatch = createEventDispatcher()
let drawer
let boundValue
$: text = getText(value)
$: convertOldColumnFormat(value)
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchema($currentAsset, datasource)
$: options = Object.keys(schema || {})
$: sanitisedValue = getValidColumns(value, options)
$: updateBoundValue(sanitisedValue)
$: enrichedSchemaFields = getFields(Object.values(schema || {}), {
allowLinks: true,
})
const getText = value => {
if (!value?.length) {
return "All fields"
}
let text = `${value.length} field`
if (value.length !== 1) {
text += "s"
}
return text
}
const convertOldColumnFormat = oldColumns => {
if (typeof oldColumns?.[0] === "string") {
value = oldColumns.map(field => ({ name: field, displayName: field }))
}
}
const getSchema = (asset, datasource) => {
const schema = getSchemaForDatasource(asset, datasource).schema
@ -79,7 +86,10 @@
}
</script>
<ActionButton on:click={open}>Configure fields</ActionButton>
<div class="field-configuration">
<ActionButton on:click={open}>{text}</ActionButton>
</div>
<Drawer bind:this={drawer} title="Form Fields">
<svelte:fragment slot="description">
Configure the fields in your form.
@ -87,3 +97,9 @@
<Button cta slot="buttons" on:click={save}>Save</Button>
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
</Drawer>
<style>
.field-configuration :global(.spectrum-ActionButton) {
width: 100%;
}
</style>

View file

@ -192,7 +192,7 @@
<Label>Filters</Label>
</div>
<div class="fields">
{#each rawFilters as filter, idx}
{#each rawFilters as filter}
<Select
bind:value={filter.field}
options={fieldOptions}

View file

@ -13,6 +13,7 @@
<i class={icon} />
{:else}
<span>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html text}
</span>
{/if}

View file

@ -27,7 +27,6 @@
$: nullishValue = value == null || value === ""
$: allBindings = getAllBindings(bindings, componentBindings, nested)
$: safeValue = getSafeValue(value, defaultValue, allBindings)
$: tempValue = safeValue
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
const getAllBindings = (bindings, componentBindings, nested) => {
@ -104,6 +103,7 @@
/>
</div>
{#if info}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
<div class="text">{@html info}</div>
{/if}
</div>

View file

@ -1,32 +1,61 @@
<script>
import { Tooltip } from "@budibase/bbui"
export let text
export let url
export let active = false
export let disabled = false
export let tooltip = null
</script>
{#if url}
<a on:click href={url} class:active>
{text || ""}
</a>
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span on:click class:active>
{text || ""}
</span>
{/if}
<div class="side-nav-item">
{#if url}
<a class="text" on:click href={url} class:active class:disabled>
{text || ""}
</a>
{:else}
<div class="text" on:click class:active class:disabled>
{text || ""}
</div>
{/if}
{#if tooltip}
<div class="tooltip">
<Tooltip textWrapping direction="right" text={tooltip} />
</div>
{/if}
</div>
<style>
a,
span {
.side-nav-item {
position: relative;
}
.text {
display: block;
padding: var(--spacing-s) var(--spacing-m);
color: var(--spectrum-global-color-gray-900);
border-radius: 4px;
transition: background 130ms ease-out;
}
.active,
span:hover,
a:hover {
.text:hover {
background-color: var(--spectrum-global-color-gray-200);
cursor: pointer;
}
.disabled {
pointer-events: none;
color: var(--spectrum-global-color-gray-500) !important;
}
.tooltip {
position: absolute;
transform: translateY(-50%);
left: 100%;
top: 50%;
opacity: 0;
pointer-events: none;
transition: opacity 130ms ease-out;
z-index: 100;
}
.side-nav-item:hover .tooltip {
opacity: 1;
}
</style>

View file

@ -1,5 +1,11 @@
<script>
import { ModalContent, Body, notifications, CopyInput } from "@budibase/bbui"
import {
ModalContent,
keepOpen,
Body,
notifications,
CopyInput,
} from "@budibase/bbui"
import { auth } from "stores/portal"
import { onMount } from "svelte"
@ -12,8 +18,8 @@
} catch (err) {
notifications.error("Unable to generate new API key")
}
// need to return false to keep modal open
return false
return keepOpen
}
onMount(async () => {

View file

@ -1,6 +1,12 @@
<script>
import { writable, get as svelteGet } from "svelte/store"
import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
import {
notifications,
keepOpen,
Input,
ModalContent,
Dropzone,
} from "@budibase/bbui"
import { store, automationStore } from "builderStore"
import { API } from "api"
import { apps, admin, auth } from "stores/portal"
@ -9,8 +15,6 @@
import { createValidationStore } from "helpers/validation/yup"
import * as appValidation from "helpers/validation/yup/app"
import TemplateCard from "components/common/TemplateCard.svelte"
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
import { Roles } from "constants/backend"
import { lowercase } from "helpers"
export let template
@ -136,21 +140,6 @@
// Create user
await auth.setInitInfo({})
// Create a default home screen if no template was selected
if (template == null) {
let defaultScreenTemplate = createFromScratchScreen.create()
defaultScreenTemplate.routing.route = "/home"
defaultScreenTemplate.routing.roldId = Roles.BASIC
try {
await store.actions.screens.save(defaultScreenTemplate)
} catch (err) {
console.error("Could not create a default application screen", err)
notifications.warning(
"Encountered an issue creating the default screen."
)
}
}
$goto(`/builder/app/${createdApp.instance._id}`)
} catch (error) {
creating = false
@ -167,7 +156,7 @@
onConfirm: async () => {
if (encryptedFile) {
currentStep = Step.SET_PASSWORD
return false
return keepOpen
} else {
try {
await createNewApp()
@ -190,7 +179,7 @@
message += `: ${lowercase(e.message)}`
}
notifications.error(message)
return false
return keepOpen
}
},
isValid: $encryptionValidation.valid,

View file

@ -1,6 +1,7 @@
<script>
import {
ModalContent,
keepOpen,
Toggle,
Body,
InlineAlert,
@ -32,7 +33,7 @@
exportApp()
} else {
currentStep = Step.SET_PASSWORD
return false
return keepOpen
}
},
isValid: true,
@ -43,7 +44,7 @@
onConfirm: async () => {
await validation.check({ password })
if (!$validation.valid) {
return false
return keepOpen
}
exportApp(password)
},

View file

@ -114,26 +114,24 @@ export const syncURLToState = options => {
// Updates the URL with new state values
const mapStateToUrl = state => {
let needsUpdate = false
const urlValue = cachedParams?.[urlParam]
const stateValue = state?.[stateKey]
if (stateValue !== urlValue) {
needsUpdate = true
log(`url.${urlParam} (${urlValue}) <= state.${stateKey} (${stateValue})`)
if (validate && fallbackUrl) {
if (!validate(stateValue)) {
log("Invalid state param!", stateValue)
redirectUrl(fallbackUrl)
return
}
// As the store updated, validate that the current state value is valid
if (validate && fallbackUrl) {
if (!validate(stateValue)) {
log("Invalid state param!", stateValue)
redirectUrl(fallbackUrl)
return
}
}
// Avoid updating the URL if not necessary to prevent a wasted render
// cycle
if (!needsUpdate) {
if (stateValue === urlValue) {
return
}
log(`url.${urlParam} (${urlValue}) <= state.${stateKey} (${stateValue})`)
// Navigate to the new URL
if (!get(isChangingPage)) {

View file

@ -14,17 +14,16 @@
import { groups, licensing, apps, users, auth, admin } from "stores/portal"
import { fetchData } from "@budibase/frontend-core"
import { API } from "api"
import { onMount } from "svelte"
import GroupIcon from "../../../portal/users/groups/_components/GroupIcon.svelte"
import RoleSelect from "components/common/RoleSelect.svelte"
import UpgradeModal from "components/common/users/UpgradeModal.svelte"
import { Constants, Utils } from "@budibase/frontend-core"
import { emailValidator } from "helpers/validation"
import { roles } from "stores/backend"
import { fly } from "svelte/transition"
let query = null
let loaded = false
let rendered = false
let inviting = false
let searchFocus = false
@ -383,10 +382,6 @@
$: initSidePanel($store.builderSidePanel)
onMount(() => {
rendered = true
})
function handleKeyDown(evt) {
if (evt.key === "Enter" && queryIsEmail && !inviting) {
onInviteUser()
@ -418,16 +413,14 @@
<svelte:window on:keydown={handleKeyDown} />
<div
transition:fly={{ x: 400, duration: 260 }}
id="builder-side-panel-container"
class:open={$store.builderSidePanel}
use:clickOutside={$store.builderSidePanel
? () => {
store.update(state => {
state.builderSidePanel = false
return state
})
}
: () => {}}
use:clickOutside={() => {
store.update(state => {
state.builderSidePanel = false
return state
})
}}
>
<div class="builder-side-panel-header">
<Heading size="S">Users</Heading>
@ -737,12 +730,11 @@
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
transition: transform 130ms ease-out;
position: absolute;
width: 400px;
right: 0;
transform: translateX(100%);
height: 100%;
box-shadow: 0 0 40px 10px rgba(0, 0, 0, 0.1);
}
.builder-side-panel-header,
@ -792,11 +784,6 @@
font-style: normal;
}
#builder-side-panel-container.open {
transform: translateX(0);
box-shadow: 0 0 40px 10px rgba(0, 0, 0, 0.1);
}
.builder-side-panel-header {
display: flex;
flex-direction: row;

View file

@ -1,7 +1,12 @@
<script>
import { store, automationStore, userStore } from "builderStore"
import {
store,
automationStore,
userStore,
deploymentStore,
} from "builderStore"
import { roles, flags } from "stores/backend"
import { auth } from "stores/portal"
import { auth, apps } from "stores/portal"
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
import {
Icon,
@ -44,6 +49,8 @@
await automationStore.actions.fetch()
await roles.fetch()
await flags.fetch()
await apps.load()
await deploymentStore.actions.load()
loaded = true
return pkg
} catch (error) {
@ -69,18 +76,13 @@
// Event handler for the command palette
const handleKeyDown = e => {
if (e.key === "k" && (e.ctrlKey || e.metaKey) && $store.hasLock) {
if (e.key === "k" && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
commandPaletteModal.toggle()
}
}
const initTour = async () => {
// Skip tour if we don't have the lock
if (!$store.hasLock) {
return
}
// Check if onboarding is enabled.
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
if (!$auth.user?.onboardedAt) {
@ -140,7 +142,7 @@
{/if}
<div class="root" class:blur={$store.showPreview}>
<div class="top-nav" class:has-lock={$store.hasLock}>
<div class="top-nav">
{#if $store.initialised}
<div class="topleftnav">
<span class="back-to-apps">
@ -151,37 +153,25 @@
on:click={() => $goto("../../portal/apps")}
/>
</span>
{#if $store.hasLock}
<Tabs {selected} size="M">
{#each $layout.children as { path, title }}
<TourWrap tourStepKey={`builder-${title}-section`}>
<Tab
quiet
selected={$isActive(path)}
on:click={topItemNavigate(path)}
title={capitalise(title)}
id={`builder-${title}-tab`}
/>
</TourWrap>
{/each}
</Tabs>
{:else}
<div class="secondary-editor">
<Icon name="LockClosed" />
<div
class="secondary-editor-body"
title="Another user is currently editing your screens and automations"
>
Another user is currently editing your screens and automations
</div>
</div>
{/if}
<Tabs {selected} size="M">
{#each $layout.children as { path, title }}
<TourWrap tourStepKey={`builder-${title}-section`}>
<Tab
quiet
selected={$isActive(path)}
on:click={topItemNavigate(path)}
title={capitalise(title)}
id={`builder-${title}-tab`}
/>
</TourWrap>
{/each}
</Tabs>
</div>
<div class="topcenternav">
<Heading size="XS">{$store.name}</Heading>
</div>
<div class="toprightnav">
<span class:nav-lock={!$store.hasLock}>
<span>
<UserAvatars users={$userStore} />
</span>
<AppActions {application} {loaded} />
@ -248,10 +238,6 @@
z-index: 2;
}
.top-nav.has-lock {
padding-right: 0px;
}
.topcenternav {
display: flex;
flex-direction: row;
@ -290,23 +276,6 @@
margin-right: var(--spacing-l);
}
.secondary-editor {
align-self: center;
display: flex;
flex-direction: row;
gap: 8px;
min-width: 0;
overflow: hidden;
margin-left: var(--spacing-xl);
}
.secondary-editor-body {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0px;
}
.body {
flex: 1 1 auto;
z-index: 1;

View file

@ -8,15 +8,6 @@
import { onDestroy, onMount } from "svelte"
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
import { store } from "builderStore"
import { redirect } from "@roxi/routify"
// Prevent access for other users than the lock holder
$: {
if (!$store.hasLock) {
$redirect("../data")
}
}
// Keep URL and state in sync for selected screen ID
const stopSyncing = syncURLToState({

View file

@ -1,5 +1,5 @@
<script>
import { Modal } from "@budibase/bbui"
import { Modal, keepOpen } from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { IntegrationTypes } from "constants/backend"
import GoogleAuthPrompt from "./GoogleAuthPrompt.svelte"
@ -75,8 +75,7 @@
notifications.error(`Error creating datasource: ${e.message}`)
}
// Prevent modal closing
return false
return keepOpen
}
</script>

View file

@ -1,5 +1,5 @@
<script>
import { Modal, notifications } from "@budibase/bbui"
import { keepOpen, Modal, notifications } from "@budibase/bbui"
import { integrationForDatasource } from "stores/selectors"
import { datasources, integrations } from "stores/backend"
import DatasourceConfigEditor from "components/backend/Datasources/ConfigEditor/index.svelte"
@ -23,8 +23,8 @@
)
} catch (err) {
notifications.error(err?.message ?? "Error saving datasource")
// prevent the modal from closing
return false
return keepOpen
}
}
</script>

View file

@ -49,7 +49,6 @@
return datasource.config.spreadsheetId
}
}
$: subtitle = getSubtitle(datasource)
</script>
<div class="button" on:click>

View file

@ -1,6 +1,7 @@
<script>
import { goto } from "@roxi/routify"
import {
keepOpen,
ModalContent,
notifications,
Body,
@ -70,10 +71,10 @@
}
notifications.success("Imported successfully")
return true
} catch (error) {
notifications.error("Error importing queries")
return false
return keepOpen
}
}
</script>

View file

@ -4,6 +4,10 @@
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
import { store } from "builderStore"
$: datasourceId = $datasources.selectedDatasourceId
$: store.actions.websocket.selectResource(datasourceId)
const stopSyncing = syncURLToState({
urlParam: "datasourceId",

View file

@ -7,9 +7,11 @@
import { onMount } from "svelte"
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
import { TableNames } from "constants"
import { store } from "builderStore"
let modal
$: store.actions.websocket.selectResource(BUDIBASE_INTERNAL_DB_ID)
$: internalTablesBySourceId = $tables.list.filter(
table =>
table.type !== "external" &&

View file

@ -6,8 +6,11 @@
import { goto } from "@roxi/routify"
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
import { onMount } from "svelte"
import { store } from "builderStore"
let modal
$: store.actions.websocket.selectResource(DEFAULT_BB_DATASOURCE_ID)
$: internalTablesBySourceId = $tables.list.filter(
table =>
table.type !== "external" && table.sourceId === DEFAULT_BB_DATASOURCE_ID

View file

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

View file

@ -3,6 +3,10 @@
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
import { store } from "builderStore"
$: queryId = $queries.selectedQueryId
$: store.actions.websocket.selectResource(queryId)
const stopSyncing = syncURLToState({
urlParam: "queryId",

View file

@ -3,6 +3,10 @@
import { tables } from "stores/backend"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
import { store } from "builderStore"
$: tableId = $tables.selectedTableId
$: store.actions.websocket.selectResource(tableId)
const stopSyncing = syncURLToState({
urlParam: "tableId",

View file

@ -3,6 +3,10 @@
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
import { store } from "builderStore"
$: viewName = $views.selectedViewName
$: store.actions.websocket.selectResource(viewName)
const stopSyncing = syncURLToState({
urlParam: "viewName",

View file

@ -272,6 +272,7 @@
{:else if error}
<div class="center error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">App preview failed to load</Heading>
<Body size="S">{error}</Body>

View file

@ -7,6 +7,9 @@
import { onDestroy } from "svelte"
const { isActive, goto } = routify
$: screenId = $store.selectedScreenId
$: store.actions.websocket.selectResource(screenId)
// Keep URL and state in sync for selected screen ID
const stopSyncing = syncURLToState({
urlParam: "screenId",

View file

@ -3,7 +3,7 @@
import ComponentTree from "./ComponentTree.svelte"
import { dndStore } from "./dndStore.js"
import { goto } from "@roxi/routify"
import { store, selectedScreen } from "builderStore"
import { store, selectedScreen, userSelectedResourceMap } from "builderStore"
import NavItem from "components/common/NavItem.svelte"
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
@ -41,6 +41,7 @@
$store.selectedComponentId = $selectedScreen?.props._id
}}
id={`component-${$selectedScreen?.props._id}`}
selectedBy={$userSelectedResourceMap[$selectedScreen?.props._id]}
>
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
</NavItem>

View file

@ -1,5 +1,5 @@
<script>
import { store } from "builderStore"
import { store, userSelectedResourceMap } from "builderStore"
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import NavItem from "components/common/NavItem.svelte"
import { capitalise } from "helpers"
@ -18,8 +18,6 @@
let closedNodes = {}
$: currentScreen = get(selectedScreen)
$: filteredComponents = components?.filter(component => {
return (
!$store.componentToPaste?.isCut ||
@ -123,6 +121,7 @@
selected={$store.selectedComponentId === component._id}
{opened}
highlighted={isChildOfSelectedComponent(component)}
selectedBy={$userSelectedResourceMap[component._id]}
>
<ComponentDropdownMenu {component} />
</NavItem>

View file

@ -7,6 +7,9 @@
import ComponentListPanel from "./_components/navigation/ComponentListPanel.svelte"
import ComponentSettingsPanel from "./_components/settings/ComponentSettingsPanel.svelte"
$: componentId = $store.selectedComponentId
$: store.actions.websocket.selectResource(componentId)
const cleanUrl = url => {
// Strip trailing slashes
if (url?.endsWith("/index")) {

View file

@ -1,165 +0,0 @@
<script>
import { tables } from "stores/backend"
import { ModalContent, Body, Layout, Icon, Heading } from "@budibase/bbui"
import blankScreenPreview from "./blankScreenPreview.png"
import listScreenPreview from "./listScreenPreview.png"
export let onConfirm
export let onCancel
let listScreenModeKey = "autoCreate"
let blankScreenModeKey = "blankScreen"
let selectedScreenMode
const confirmScreenSelection = async () => {
await onConfirm(selectedScreenMode)
}
</script>
<div>
<ModalContent
title="Add screens"
confirmText="Continue"
cancelText="Cancel"
onConfirm={confirmScreenSelection}
{onCancel}
disabled={!selectedScreenMode}
size="M"
>
<Layout noPadding gap="S">
<div
class="screen-type item blankView"
class:selected={selectedScreenMode == blankScreenModeKey}
on:click={() => {
selectedScreenMode = blankScreenModeKey
}}
>
<div class="content screen-type-wrap">
<img
alt="blank screen preview"
class="preview"
src={blankScreenPreview}
/>
<div class="screen-type-text">
<Heading size="XS">Blank screen</Heading>
<Body size="S">Add an empty blank screen</Body>
</div>
</div>
<div
style="color: var(--spectrum-global-color-green-600); float: right"
>
<div
class={`checkmark-spacing ${
selectedScreenMode == blankScreenModeKey ? "visible" : ""
}`}
>
<Icon size="S" name="CheckmarkCircle" />
</div>
</div>
</div>
<div class="listViewTitle">
<Heading size="XS">Quickly create a screen from your data</Heading>
</div>
<div
class="screen-type item"
class:selected={selectedScreenMode == listScreenModeKey}
on:click={() => {
selectedScreenMode = listScreenModeKey
}}
class:disabled={!$tables.list.filter(table => table._id !== "ta_users")
.length}
>
<div class="content screen-type-wrap">
<img
alt="list screen preview"
class="preview"
src={listScreenPreview}
/>
<div class="screen-type-text">
<Heading size="XS">List view</Heading>
<Body size="S">
Create, edit and view your data in a list view screen with side
panel
</Body>
</div>
</div>
<div
style="color: var(--spectrum-global-color-green-600); float: right"
>
<div
class={`checkmark-spacing ${
selectedScreenMode == listScreenModeKey ? "visible" : ""
}`}
>
<Icon size="S" name="CheckmarkCircle" />
</div>
</div>
</div>
</Layout>
</ModalContent>
</div>
<style>
.screen-type-wrap {
display: flex;
flex-direction: row;
align-items: center;
}
.disabled {
opacity: 0.3;
pointer-events: none;
}
.checkmark-spacing {
margin-right: var(--spacing-m);
opacity: 0;
}
.content {
letter-spacing: 0px;
}
.item {
cursor: pointer;
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
background: var(--spectrum-alias-background-color-secondary);
transition: 0.3s all;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
border-width: 1px;
display: flex;
justify-content: space-between;
align-items: center;
}
.item:hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
.screen-type-wrap .screen-type-text {
padding-left: var(--spectrum-alias-item-padding-xl);
}
.screen-type-wrap .screen-type-text :global(h1) {
padding-bottom: var(--spacing-xs);
}
.screen-type-wrap :global(.spectrum-Icon) {
min-width: var(--spectrum-icon-size-m);
}
.screen-type-wrap :global(.spectrum-Heading) {
padding-bottom: var(--spectrum-alias-item-padding-s);
}
.preview {
width: 140px;
}
.listViewTitle {
margin-top: 35px;
}
.blankView {
margin-top: 10px;
}
.visible {
opacity: 1;
}
</style>

View file

@ -12,8 +12,8 @@
$: role = $roles.find(role => role._id === roleId)
$: tooltip =
roleId === Roles.PUBLIC
? "This screen is open to the public"
: `Requires at least ${role?.name} access`
? "Open to the public"
: `Requires ${role?.name} access`
</script>
<div
@ -44,14 +44,14 @@
display: flex;
flex-direction: row;
justify-content: flex-end;
width: 130px;
width: 200px;
pointer-events: none;
}
.tooltip :global(.spectrum-Tooltip) {
background: var(--color);
color: white;
font-weight: 600;
max-width: 130px;
max-width: 200px;
}
.tooltip :global(.spectrum-Tooltip-tip) {
border-top-color: var(--color);

View file

@ -9,7 +9,7 @@
Helpers,
notifications,
} from "@budibase/bbui"
import ScreenDetailsModal from "./ScreenDetailsModal.svelte"
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { makeComponentUnique } from "builderStore/componentUtils"

View file

@ -1,17 +1,16 @@
<script>
import { Search, Layout, Select, Body, Button } from "@budibase/bbui"
import Panel from "components/design/Panel.svelte"
import { goto } from "@roxi/routify"
import { roles } from "stores/backend"
import { store, sortedScreens } from "builderStore"
import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
import NavItem from "components/common/NavItem.svelte"
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
import ScreenWizard from "./ScreenWizard.svelte"
import RoleIndicator from "./RoleIndicator.svelte"
import { RoleUtils } from "@budibase/frontend-core"
let searchString
let accessRole = "all"
let showNewScreenModal
$: filteredScreens = getFilteredScreens(
$sortedScreens,
@ -31,7 +30,7 @@
<Panel title="Screens" borderRight>
<Layout paddingX="L" paddingY="XL" gap="S">
<Button on:click={showNewScreenModal} cta>Add screen</Button>
<Button on:click={() => $goto("../../new")} cta>Add screen</Button>
<Search
placeholder="Search"
value={searchString}
@ -60,6 +59,7 @@
on:click={() => store.actions.screens.select(screen._id)}
rightAlignIcon
showTooltip
selectedBy={$userSelectedResourceMap[screen._id]}
>
<ScreenDropdownMenu screenId={screen._id} />
<RoleIndicator slot="right" roleId={screen.routing.roleId} />
@ -73,5 +73,3 @@
</Layout>
{/if}
</Panel>
<ScreenWizard bind:showModal={showNewScreenModal} />

View file

@ -1,6 +1,5 @@
<script>
import ScreenDetailsModal from "./ScreenDetailsModal.svelte"
import NewScreenModal from "./NewScreenModal.svelte"
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
import DatasourceModal from "./DatasourceModal.svelte"
import ScreenRoleModal from "./ScreenRoleModal.svelte"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
@ -11,11 +10,11 @@
import { tables } from "stores/backend"
import { Roles } from "constants/backend"
import { capitalise } from "helpers"
import { goto } from "@roxi/routify"
let pendingScreen
// Modal refs
let newScreenModal
let screenDetailsModal
let datasourceModal
let screenAccessRoleModal
@ -26,16 +25,6 @@
let blankScreenUrl = null
let screenMode = null
// External handler to show the screen wizard
export const showModal = () => {
selectedTemplates = null
blankScreenUrl = null
screenMode = null
pendingScreen = null
screenAccessRole = Roles.BASIC
newScreenModal.show()
}
// Creates an array of screens, checking and sanitising their URLs
const createScreens = async ({ screens, screenAccessRole }) => {
if (!screens?.length) {
@ -43,6 +32,8 @@
}
try {
let screenId
for (let screen of screens) {
// Check we aren't clashing with an existing URL
if (hasExistingUrl(screen.routing.route)) {
@ -64,7 +55,8 @@
screen.routing.roleId = screenAccessRole
// Create the screen
await store.actions.screens.save(screen)
const response = await store.actions.screens.save(screen)
screenId = response._id
// Add link in layout for list screens
if (screen.props._instanceName.endsWith("List")) {
@ -74,7 +66,10 @@
)
}
}
$goto(`./${screenId}`)
} catch (error) {
console.log(error)
notifications.error("Error creating screens")
}
}
@ -104,18 +99,24 @@
}
// Handler for NewScreenModal
const confirmScreenSelection = async mode => {
export const show = mode => {
selectedTemplates = null
blankScreenUrl = null
screenMode = mode
pendingScreen = null
screenAccessRole = Roles.BASIC
if (mode === "autoCreate") {
if (mode === "table") {
datasourceModal.show()
} else {
} else if (mode === "blank") {
let templates = getTemplates($store, $tables.list)
const blankScreenTemplate = templates.find(
t => t.id === "createFromScratch"
)
pendingScreen = blankScreenTemplate.create()
screenDetailsModal.show()
} else {
throw new Error("Invalid mode provided")
}
}
@ -155,7 +156,7 @@
// Submit screen config for creation.
const confirmScreenCreation = async () => {
if (screenMode === "blankScreen") {
if (screenMode === "blank") {
confirmBlankScreenCreation({
screenUrl: blankScreenUrl,
screenAccessRole,
@ -166,7 +167,7 @@
}
const roleSelectBack = () => {
if (screenMode === "blankScreen") {
if (screenMode === "blank") {
screenDetailsModal.show()
} else {
datasourceModal.show()
@ -174,14 +175,9 @@
}
</script>
<Modal bind:this={newScreenModal}>
<NewScreenModal onConfirm={confirmScreenSelection} />
</Modal>
<Modal bind:this={datasourceModal}>
<DatasourceModal
onConfirm={confirmScreenDatasources}
onCancel={() => newScreenModal.show()}
initalScreens={!selectedTemplates ? [] : [...selectedTemplates]}
/>
</Modal>
@ -198,7 +194,6 @@
<Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal
onConfirm={confirmScreenBlank}
onCancel={() => newScreenModal.show()}
initialUrl={blankScreenUrl}
/>
</Modal>

View file

@ -40,14 +40,14 @@
</script>
<ModalContent
title="Autogenerated screens"
title="Access"
confirmText="Done"
cancelText="Back"
{onConfirm}
{onCancel}
disabled={!!error}
>
Select which level of access you want your screens to have
Select the level of access required to see these screens
<Select
bind:value={screenAccessRole}
on:change={onChangeRole}

View file

@ -1,14 +1,2 @@
<script>
import { store } from "builderStore"
import { redirect } from "@roxi/routify"
// Prevent access for other users than the lock holder
$: {
if (!$store.hasLock) {
$redirect("../data")
}
}
</script>
<!-- routify:options index=2 -->
<slot />

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Some files were not shown because too many files have changed in this diff Show more