Merge branch 'develop' into backmerge-master-20230727

This commit is contained in:
Adria Navarro 2023-07-27 15:51:42 +01:00 committed by GitHub
commit 25019aa31e
343 changed files with 8489 additions and 3813 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

@ -12,9 +12,6 @@ on:
- master
- develop
pull_request:
branches:
- master
- develop
workflow_dispatch:
env:
@ -162,7 +159,7 @@ jobs:
run: |
cd qa-core
yarn setup
yarn test:ci
yarn serve:test:self:ci
env:
BB_ADMIN_USER_EMAIL: admin
BB_ADMIN_USER_PASSWORD: admin
@ -185,7 +182,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

View File

@ -1,2 +1,2 @@
nodejs 14.20.1
nodejs 14.21.3
python 3.10.0

3
babel.config.json Normal file
View File

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

View File

@ -40,6 +40,24 @@ spec:
- image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
imagePullPolicy: Always
name: proxy-service
livenessProbe:
httpGet:
path: /health
port: {{ .Values.services.proxy.port }}
initialDelaySeconds: 0
periodSeconds: 5
successThreshold: 1
failureThreshold: 2
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /health
port: {{ .Values.services.proxy.port }}
initialDelaySeconds: 0
periodSeconds: 5
successThreshold: 1
failureThreshold: 2
timeoutSeconds: 3
ports:
- containerPort: {{ .Values.services.proxy.port }}
env:

View File

@ -231,18 +231,33 @@ An overview of the CI pipelines can be found [here](../.github/workflows/README.
### Pro
@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you require to update code inside the pro package it can be cloned to the same root level as budibase, e.g.
@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you need to make an update to pro and have access to the repo, then you can update your submodule within the mono-repo by running `git submodule update --init` - from here you can use normal submodule flow to develop a change within pro.
Once you have updated to use the pro submodule, it will be linked into all of your local dependencies by NX as with all other monorepo packages. If you have been using the NPM version of `@budibase/pro` then you may need to run a `git reset --hard` to fix all of the pro versions back to `0.0.0` to be monorepo aware.
From here - to develop a change in pro, you can follow the below flow:
```
.
|_ budibase
|_ budibase-pro
# enter the pro submodule
cd packages/pro
# get the base branch you are working from (same as monorepo)
git fetch
git checkout <develop | master>
# create a branch, named the same as the branch in your monorepo
git checkout -b <some branch>
... make changes
# commit the changes you've made, with a message for pro
git commit <something>
# within the monorepo, add the pro reference to your branch, commit it with a message like "Update pro ref"
cd ../..
git add packages/pro
git commit <add the new reference to main repo>
```
From here, you will have created a branch in the pro repository and commited the reference to your branch on the monorepo. When you eventually PR this work back into the mainline branch, you will need to first merge your pro PR to the pro mainline, then go into your PR in the monorepo and update the reference again to the new mainline.
Note that only budibase maintainers will be able to access the pro repo.
By default, NX will make sure that dependencies are replaced with local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev.
### Troubleshooting
Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above to create a fresh Budibase installation.

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=

View File

@ -1,9 +1,10 @@
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"runner": "nx-cloud",
"options": {
"cacheableOperations": ["build", "test"]
"cacheableOperations": ["build", "test"],
"accessToken": "MmM4OGYxNzItMDBlYy00ZmE3LTk4MTYtNmJhYWMyZjBjZTUyfHJlYWQ="
}
}
},

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.18.17",
"esbuild-node-externals": "^1.8.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,7 +46,7 @@
"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",
@ -49,13 +54,13 @@
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder",
"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 && 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: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",
@ -103,5 +108,8 @@
"@budibase/string-templates": "0.0.0",
"@budibase/types": "0.0.0"
},
"engines": {
"node": ">=14.0.0 <15.0.0"
},
"dependencies": {}
}

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

@ -159,7 +159,7 @@ export async function updateUserOAuth(userId: string, oAuthConfig: any) {
try {
const db = getGlobalDB()
const dbUser = await db.get(userId)
const dbUser = await db.get<any>(userId)
//Do not overwrite the refresh token if a valid one is not provided.
if (typeof details.refreshToken !== "string") {

View File

@ -2,9 +2,14 @@ import { getAppClient } from "../redis/init"
import { doWithDB, DocumentType } from "../db"
import { Database, App } from "@budibase/types"
const AppState = {
INVALID: "invalid",
export enum AppState {
INVALID = "invalid",
}
export interface DeletedApp {
state: AppState
}
const EXPIRY_SECONDS = 3600
/**
@ -31,7 +36,7 @@ function isInvalid(metadata?: { state: string }) {
* @param {string} appId the id of the app to get metadata from.
* @returns {object} the app metadata.
*/
export async function getAppMetadata(appId: string) {
export async function getAppMetadata(appId: string): Promise<App | DeletedApp> {
const client = await getAppClient()
// try cache
let metadata = await client.get(appId)
@ -61,11 +66,8 @@ export async function getAppMetadata(appId: string) {
}
await client.store(appId, metadata, expiry)
}
// we've stored in the cache an object to tell us that it is currently invalid
if (isInvalid(metadata)) {
throw { status: 404, message: "No app metadata found" }
}
return metadata as App
return metadata
}
/**

View File

@ -12,7 +12,7 @@ const EXPIRY_SECONDS = 3600
*/
async function populateFromDB(userId: string, tenantId: string) {
const db = tenancy.getTenantDB(tenantId)
const user = await db.get(userId)
const user = await db.get<any>(userId)
user.budibaseAccess = true
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const account = await accounts.getAccount(user.email)

View File

@ -20,6 +20,8 @@ export enum Header {
TYPE = "x-budibase-type",
PREVIEW_ROLE = "x-budibase-role",
TENANT_ID = "x-budibase-tenant-id",
VERIFICATION_CODE = "x-budibase-verification-code",
RETURN_VERIFICATION_CODE = "x-budibase-return-verification-code",
TOKEN = "x-budibase-token",
CSRF_TOKEN = "x-csrf-token",
CORRELATION_ID = "x-budibase-correlation-id",

View File

@ -0,0 +1,10 @@
export const CONSTANT_INTERNAL_ROW_COLS = [
"_id",
"_rev",
"type",
"createdAt",
"updatedAt",
"tableId",
] as const
export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const

View File

@ -2,3 +2,4 @@ export * from "./connections"
export * from "./DatabaseImpl"
export * from "./utils"
export { init, getPouch, getPouchDB, closePouchDB } from "./pouchDB"
export * from "../constants"

View File

@ -5,7 +5,7 @@ export async function createUserIndex() {
const db = getGlobalDB()
let designDoc
try {
designDoc = await db.get("_design/database")
designDoc = await db.get<any>("_design/database")
} catch (err: any) {
if (err.status === 404) {
designDoc = { _id: "_design/database" }

View File

@ -2,7 +2,7 @@ import env from "../environment"
import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants"
import { getTenantId, getGlobalDBName } from "../context"
import { doWithDB, directCouchAllDbs } from "./db"
import { getAppMetadata } from "../cache/appMetadata"
import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata"
import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions"
import { App, Database } from "@budibase/types"
import { getStartEndKeyURL } from "../docIds"
@ -101,7 +101,9 @@ export async function getAllApps({
const response = await Promise.allSettled(appPromises)
const apps = response
.filter(
(result: any) => result.status === "fulfilled" && result.value != null
(result: any) =>
result.status === "fulfilled" &&
result.value?.state !== AppState.INVALID
)
.map(({ value }: any) => value)
if (!all) {
@ -126,7 +128,11 @@ export async function getAppsByIDs(appIds: string[]) {
)
// have to list the apps which exist, some may have been deleted
return settled
.filter(promise => promise.status === "fulfilled")
.filter(
promise =>
promise.status === "fulfilled" &&
(promise.value as DeletedApp).state !== AppState.INVALID
)
.map(promise => (promise as PromiseFulfilledResult<App>).value)
}

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,7 +161,7 @@ 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,
OFFLINE_MODE: process.env.OFFLINE_MODE,
_set(key: any, value: any) {
@ -162,6 +169,7 @@ const environment = {
// @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,4 @@
export * as correlation from "./correlation/correlation"
export { logger } from "./pino/logger"
export * from "./alerts"
// turn off or on context logging i.e. tenantId, appId etc
export let LOG_CONTEXT = true
export * as system from "./system"

View File

@ -1,37 +1,60 @@
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
if (!env.DISABLE_PINO_LOGGER) {
const level = env.LOG_LEVEL
const pinoOptions: LoggerOptions = {
level: env.LOG_LEVEL,
level,
formatters: {
level: label => {
return { level: label.toUpperCase() }
level: level => {
return { level: level.toUpperCase() }
},
bindings: () => {
return {}
if (env.SELF_HOSTED) {
// "service" is being injected in datadog using the pod names,
// so we should leave it blank to allow the default behaviour if it's not running self-hosted
return {
service: env.SERVICE_NAME,
}
} else {
return {}
}
},
},
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
}
if (env.isDev()) {
pinoOptions.transport = {
target: "pino-pretty",
options: {
singleLine: true,
},
}
const destinations: pino.StreamEntry[] = []
destinations.push(
env.isDev()
? {
stream: pinoPretty({ singleLine: true }),
level: level as pino.Level,
}
: { stream: process.stdout, level: level as pino.Level }
)
if (env.SELF_HOSTED) {
destinations.push({
stream: localFileDestination(),
level: level as pino.Level,
})
}
pinoInstance = pino(pinoOptions)
pinoInstance = destinations.length
? pino(pinoOptions, pino.multistream(destinations))
: pino(pinoOptions)
// CONSOLE OVERRIDES
@ -83,15 +106,13 @@ if (!env.DISABLE_PINO_LOGGER) {
let contextObject = {}
if (LOG_CONTEXT) {
contextObject = {
tenantId: getTenantId(),
appId: getAppId(),
automationId: getAutomationId(),
identityId: identity?._id,
identityType: identity?.type,
correlationId: correlation.getId(),
}
contextObject = {
tenantId: getTenantId(),
appId: getAppId(),
automationId: getAutomationId(),
identityId: identity?._id,
identityType: identity?.type,
correlationId: correlation.getId(),
}
const mergingObject: any = {

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

@ -67,9 +67,9 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => {
export async function getById(id: string, opts?: GetOpts): Promise<User> {
const db = context.getGlobalDB()
let user = await db.get(id)
let user = await db.get<User>(id)
if (opts?.cleanup) {
user = removeUserPassword(user)
user = removeUserPassword(user) as User
}
return user
}

View File

@ -1,3 +1,5 @@
import { db } from "../../../src"
export function expectFunctionWasCalledTimesWith(
jestFunction: any,
times: number,
@ -7,3 +9,22 @@ export function expectFunctionWasCalledTimesWith(
jestFunction.mock.calls.filter((call: any) => call[0] === argument).length
).toBe(times)
}
export const expectAnyInternalColsAttributes: {
[K in (typeof db.CONSTANT_INTERNAL_ROW_COLS)[number]]: any
} = {
tableId: expect.anything(),
type: expect.anything(),
_id: expect.anything(),
_rev: expect.anything(),
createdAt: expect.anything(),
updatedAt: expect.anything(),
}
export const expectAnyExternalColsAttributes: {
[K in (typeof db.CONSTANT_EXTERNAL_ROW_COLS)[number]]: any
} = {
tableId: expect.anything(),
_id: expect.anything(),
_rev: expect.anything(),
}

View File

@ -96,7 +96,8 @@
"dependsOn": [
{
"projects": [
"@budibase/string-templates"
"@budibase/string-templates",
"@budibase/shared-core"
],
"target": "build"
}

View File

@ -1,6 +1,7 @@
<script>
import "@spectrum-css/button/dist/index-vars.css"
import Tooltip from "../Tooltip/Tooltip.svelte"
import AbsTooltip from "../Tooltip/AbsTooltip.svelte"
import { createEventDispatcher } from "svelte"
export let type
export let disabled = false
@ -17,65 +18,52 @@
export let newStyles = true
export let id
let showTooltip = false
const dispatch = createEventDispatcher()
</script>
<button
{id}
{type}
class:spectrum-Button--cta={cta}
class:spectrum-Button--primary={primary}
class:spectrum-Button--secondary={secondary}
class:spectrum-Button--warning={warning}
class:spectrum-Button--overBackground={overBackground}
class:spectrum-Button--quiet={quiet}
class:new-styles={newStyles}
class:active
class:disabled
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
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
focusable="false"
aria-hidden="true"
aria-label={icon}
>
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
{/if}
{#if $$slots}
<span class="spectrum-Button-label"><slot /></span>
{/if}
{#if !disabled && tooltip}
<div class="tooltip-icon">
<AbsTooltip text={tooltip}>
<button
{id}
{type}
class:spectrum-Button--cta={cta}
class:spectrum-Button--primary={primary}
class:spectrum-Button--secondary={secondary}
class:spectrum-Button--warning={warning}
class:spectrum-Button--overBackground={overBackground}
class:spectrum-Button--quiet={quiet}
class:new-styles={newStyles}
class:active
class:is-disabled={disabled}
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
on:click|preventDefault={() => {
if (!disabled) {
dispatch("click")
}
}}
>
{#if icon}
<svg
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
focusable="false"
aria-hidden="true"
aria-label="Info"
aria-label={icon}
>
<use xlink:href="#spectrum-icon-18-InfoOutline" />
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
</div>
{/if}
{#if showTooltip && tooltip}
<div class="tooltip">
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
</div>
{/if}
</button>
{/if}
{#if $$slots}
<span class="spectrum-Button-label"><slot /></span>
{/if}
</button>
</AbsTooltip>
<style>
button {
position: relative;
}
button.is-disabled {
cursor: default;
}
.spectrum-Button-label {
white-space: nowrap;
overflow: hidden;
@ -84,21 +72,6 @@
.active {
color: var(--spectrum-global-color-blue-600) !important;
}
.tooltip {
position: absolute;
display: flex;
justify-content: center;
z-index: 100;
width: 160px;
text-align: center;
transform: translateX(-50%);
left: 50%;
top: calc(100% - 3px);
}
.tooltip-icon {
padding-left: var(--spacing-m);
line-height: 0;
}
.spectrum-Button--primary.new-styles {
background: var(--spectrum-global-color-gray-800);
border-color: transparent;
@ -112,10 +85,10 @@
border-color: transparent;
color: var(--spectrum-global-color-gray-900);
}
.spectrum-Button--secondary.new-styles:not(.disabled):hover {
.spectrum-Button--secondary.new-styles:not(.is-disabled):hover {
background: var(--spectrum-global-color-gray-300);
}
.spectrum-Button--secondary.new-styles.disabled {
.spectrum-Button--secondary.new-styles.is-disabled {
color: var(--spectrum-global-color-gray-500);
}
</style>

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

@ -12,23 +12,24 @@
export let getOptionValue = option => option
const dispatch = createEventDispatcher()
const onChange = e => {
let tempValue = value
let isChecked = e.target.checked
if (!tempValue.includes(e.target.value) && isChecked) {
tempValue.push(e.target.value)
const optionValue = e.target.value
if (e.target.checked && !value.includes(optionValue)) {
dispatch("change", [...value, optionValue])
} else {
dispatch(
"change",
value.filter(x => x !== optionValue)
)
}
value = tempValue
dispatch(
"change",
tempValue.filter(val => val !== e.target.value || isChecked)
)
}
</script>
<div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}>
{#if options && Array.isArray(options)}
{#each options as option}
{@const optionValue = getOptionValue(option)}
<div
title={getOptionLabel(option)}
class="spectrum-Checkbox spectrum-FieldGroup-item"
@ -39,11 +40,11 @@
>
<input
on:change={onChange}
value={getOptionValue(option)}
type="checkbox"
class="spectrum-Checkbox-input"
value={optionValue}
checked={value.includes(optionValue)}
{disabled}
checked={value.includes(getOptionValue(option))}
/>
<span class="spectrum-Checkbox-box">
<svg

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

@ -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

@ -0,0 +1,157 @@
<script context="module">
export const TooltipPosition = {
Top: "top",
Right: "right",
Bottom: "bottom",
Left: "left",
}
export const TooltipType = {
Default: "default",
Info: "info",
Positive: "positive",
Negative: "negative",
}
</script>
<script>
import Portal from "svelte-portal"
import { fade } from "svelte/transition"
import "@spectrum-css/tooltip/dist/index-vars.css"
import { onDestroy } from "svelte"
export let position = TooltipPosition.Top
export let type = TooltipType.Default
export let text = ""
export let fixed = false
export let color = null
let wrapper
let hovered = false
let left
let top
let visible = false
let timeout
let interval
$: {
if (hovered || fixed) {
// Debounce showing by 200ms to avoid flashing tooltip
timeout = setTimeout(show, 200)
} else {
hide()
}
}
$: tooltipStyle = color ? `background:${color};` : null
$: tipStyle = color ? `border-top-color:${color};` : null
// Computes the position of the tooltip
const updateTooltipPosition = () => {
const node = wrapper?.children?.[0]
if (!node) {
left = null
top = null
return
}
const bounds = node.getBoundingClientRect()
// Determine where to render tooltip based on position prop
if (position === TooltipPosition.Top) {
left = bounds.left + bounds.width / 2
top = bounds.top
} else if (position === TooltipPosition.Right) {
left = bounds.left + bounds.width
top = bounds.top + bounds.height / 2
} else if (position === TooltipPosition.Bottom) {
left = bounds.left + bounds.width / 2
top = bounds.top + bounds.height
} else if (position === TooltipPosition.Left) {
left = bounds.left
top = bounds.top + bounds.height / 2
}
}
// Computes the position of the tooltip then shows it.
// We set up a poll to frequently update the position of the tooltip in case
// the target moves.
const show = () => {
updateTooltipPosition()
interval = setInterval(updateTooltipPosition, 100)
visible = true
}
// Hides the tooltip
const hide = () => {
clearTimeout(timeout)
clearInterval(interval)
visible = false
}
// Ensure we clean up interval and timeout
onDestroy(hide)
</script>
<div
bind:this={wrapper}
class="abs-tooltip"
on:focus={null}
on:mouseover={() => (hovered = true)}
on:mouseleave={() => (hovered = false)}
>
<slot />
</div>
{#if visible && text && left != null && top != null}
<Portal target=".spectrum">
<span
class="spectrum-Tooltip spectrum-Tooltip--{type} spectrum-Tooltip--{position} is-open"
style={`left:${left}px;top:${top}px;${tooltipStyle}`}
transition:fade|local={{ duration: 130 }}
>
<span class="spectrum-Tooltip-label">{text}</span>
<span class="spectrum-Tooltip-tip" style={tipStyle} />
</span>
</Portal>
{/if}
<style>
.abs-tooltip {
display: contents;
}
.spectrum-Tooltip {
position: absolute;
z-index: 9999;
pointer-events: none;
margin: 0;
max-width: 280px;
transition: top 130ms ease-out, left 130ms ease-out;
}
.spectrum-Tooltip-label {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
font-size: 12px;
font-weight: 600;
}
/* Colour overrides for default type */
.spectrum-Tooltip--default {
background: var(--spectrum-global-color-gray-500);
}
.spectrum-Tooltip--default .spectrum-Tooltip-tip {
border-top-color: var(--spectrum-global-color-gray-500);
}
/* Position styles */
.spectrum-Tooltip--top {
transform: translateX(-50%) translateY(calc(-100% - 8px));
}
.spectrum-Tooltip--right {
transform: translateX(8px) translateY(-50%);
}
.spectrum-Tooltip--bottom {
transform: translateX(-50%) translateY(8px);
}
.spectrum-Tooltip--left {
transform: translateX(calc(-100% - 8px)) translateY(-50%);
}
</style>

View File

@ -0,0 +1,39 @@
<script>
import AbsTooltip from "./AbsTooltip.svelte"
import { onDestroy } from "svelte"
export let text = null
export let condition = true
export let duration = 3000
export let position
export let type
let visible = false
let timeout
$: {
if (condition) {
showTooltip()
} else {
hideTooltip()
}
}
const showTooltip = () => {
visible = true
timeout = setTimeout(() => {
visible = false
}, duration)
}
const hideTooltip = () => {
visible = false
clearTimeout(timeout)
}
onDestroy(hideTooltip)
</script>
<AbsTooltip {position} {type} text={visible ? text : null} fixed={visible}>
<slot />
</AbsTooltip>

View File

@ -36,13 +36,19 @@ export { default as Layout } from "./Layout/Layout.svelte"
export { default as Page } from "./Layout/Page.svelte"
export { default as Link } from "./Link/Link.svelte"
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
export { default as TempTooltip } from "./Tooltip/TempTooltip.svelte"
export {
default as AbsTooltip,
TooltipPosition,
TooltipType,
} from "./Tooltip/AbsTooltip.svelte"
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
export { default as Menu } from "./Menu/Menu.svelte"
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

@ -491,6 +491,7 @@ const getSelectedRowsBindings = asset => {
readableBinding: `${table._instanceName}.Selected rows`,
category: "Selected rows",
icon: "ViewRow",
display: { name: table._instanceName },
}))
)
@ -506,6 +507,7 @@ const getSelectedRowsBindings = asset => {
)}.${makePropSafe("selectedRows")}`,
readableBinding: `${block._instanceName}.Selected rows`,
category: "Selected rows",
display: { name: block._instanceName },
}))
)
}

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,24 @@ 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 => {
const resource = user.builderMetadata?.selectedResourceId
if (resource) {
if (!map[resource]) {
map[resource] = []
}
map[resource].push(user)
}
})
return map
})
export const isOnlyUser = derived(userStore, $userStore => {
return $userStore.length < 2
})

View File

@ -248,4 +248,36 @@ const automationActions = store => ({
}
await store.actions.save(newAutomation)
},
replace: async (automationId, automation) => {
if (!automation) {
store.update(state => {
// Remove the automation
state.automations = state.automations.filter(
x => x._id !== automationId
)
// Select a new automation if required
if (automationId === state.selectedAutomationId) {
store.actions.select(state.automations[0]?._id)
}
return state
})
} else {
const index = get(store).automations.findIndex(
x => x._id === automation._id
)
if (index === -1) {
// Automation addition
store.update(state => ({
...state,
automations: [...state.automations, automation],
}))
} else {
// Automation update
store.update(state => {
state.automations[index] = automation
return state
})
}
}
},
})

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,17 @@
import { createWebsocket } from "@budibase/frontend-core"
import { userStore, store } from "builderStore"
import {
userStore,
store,
deploymentStore,
automationStore,
} 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 +38,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 +45,37 @@ 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`)
}
)
// Automations
socket.onOther(BuilderSocketEvent.AutomationChange, ({ id, automation }) => {
automationStore.actions.replace(id, automation)
})
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

@ -1,6 +1,10 @@
<script>
import { onMount } from "svelte"
import { automationStore, selectedAutomation } from "builderStore"
import {
automationStore,
selectedAutomation,
userSelectedResourceMap,
} from "builderStore"
import NavItem from "components/common/NavItem.svelte"
import EditAutomationPopover from "./EditAutomationPopover.svelte"
import { notifications } from "@budibase/bbui"
@ -21,13 +25,13 @@
</script>
<div class="automations-list">
{#each $automationStore.automations.sort(aut => aut.name) as automation, idx}
{#each $automationStore.automations.sort(aut => aut.name) as automation}
<NavItem
border={idx > 0}
icon="ShareAndroid"
text={automation.name}
selected={automation._id === selectedAutomationId}
on:click={() => selectAutomation(automation._id)}
selectedBy={$userSelectedResourceMap[automation._id]}
>
<EditAutomationPopover {automation} />
</NavItem>
@ -40,6 +44,5 @@
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
margin: 0 calc(-1 * var(--spacing-xl));
}
</style>

View File

@ -11,8 +11,8 @@
<Panel title="Automations" borderRight>
<Layout paddingX="L" paddingY="XL" gap="S">
<Button cta on:click={modal.show}>Add automation</Button>
<AutomationList />
</Layout>
<AutomationList />
</Panel>
<Modal bind:this={modal}>

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 => {
@ -112,6 +109,7 @@
{disableSorting}
{customPlaceholder}
allowEditRows={allowEditing}
allowEditColumns={allowEditing}
showAutoColumns={!hideAutocolumns}
{allowClickRows}
on:clickrelationship={e => selectRelationship(e.detail)}

View File

@ -18,7 +18,7 @@
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
import {
FIELDS,
RelationshipTypes,
RelationshipType,
ALLOWABLE_STRING_OPTIONS,
ALLOWABLE_NUMBER_OPTIONS,
ALLOWABLE_STRING_TYPES,
@ -58,7 +58,6 @@
let table = $tables.selected
let confirmDeleteDialog
let deletion
let savingColumn
let deleteColName
let jsonSchemaModal
@ -185,7 +184,7 @@
dispatch("updatecolumns")
if (
saveColumn.type === LINK_TYPE &&
saveColumn.relationshipType === RelationshipTypes.MANY_TO_MANY
saveColumn.relationshipType === RelationshipType.MANY_TO_MANY
) {
// Fetching the new tables
tables.fetch()
@ -216,7 +215,6 @@
notifications.success(`Column ${editableColumn.name} deleted`)
confirmDeleteDialog.hide()
hide()
deletion = false
dispatch("updatecolumns")
}
} catch (error) {
@ -240,7 +238,7 @@
// Default relationships many to many
if (editableColumn.type === LINK_TYPE) {
editableColumn.relationshipType = RelationshipTypes.MANY_TO_MANY
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
}
if (editableColumn.type === FORMULA_TYPE) {
editableColumn.formulaType = "dynamic"
@ -267,13 +265,11 @@
function confirmDelete() {
confirmDeleteDialog.show()
deletion = true
}
function hideDeleteDialog() {
confirmDeleteDialog.hide()
deleteColName = ""
deletion = false
}
function getRelationshipOptions(field) {
@ -290,17 +286,17 @@
{
name: `Many ${thisName} rows → many ${linkName} rows`,
alt: `Many ${table.name} rows → many ${linkTable.name} rows`,
value: RelationshipTypes.MANY_TO_MANY,
value: RelationshipType.MANY_TO_MANY,
},
{
name: `One ${linkName} row → many ${thisName} rows`,
alt: `One ${linkTable.name} rows → many ${table.name} rows`,
value: RelationshipTypes.ONE_TO_MANY,
value: RelationshipType.ONE_TO_MANY,
},
{
name: `One ${thisName} row → many ${linkName} rows`,
alt: `One ${table.name} rows → many ${linkTable.name} rows`,
value: RelationshipTypes.MANY_TO_ONE,
value: RelationshipType.MANY_TO_ONE,
},
]
}

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>
@ -95,9 +95,9 @@
{#if !creating}
<div>
A user's email, role, first and last names cannot be changed from within
the app builder. Please go to the <Link
on:click={$goto("/builder/portal/manage/users")}>user portal</Link
> to do this.
the app builder. Please go to the
<Link on:click={$goto("/builder/portal/users/users")}>user portal</Link>
to do this.
</div>
{/if}
<RowFieldControl

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 { RelationshipType } from "constants/backend"
import {
keepOpen,
Button,
Input,
ModalContent,
@ -24,11 +25,11 @@
const relationshipTypes = [
{
label: "One to Many",
value: RelationshipTypes.MANY_TO_ONE,
value: RelationshipType.MANY_TO_ONE,
},
{
label: "Many to Many",
value: RelationshipTypes.MANY_TO_MANY,
value: RelationshipType.MANY_TO_MANY,
},
]
@ -57,8 +58,8 @@
value: table._id,
}))
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
$: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY
$: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
$: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE
function getTable(id) {
return plusTables.find(table => table._id === id)
@ -115,7 +116,7 @@
function allRequiredAttributesSet() {
const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn
if (relationshipType === RelationshipTypes.MANY_TO_ONE) {
if (relationshipType === RelationshipType.MANY_TO_ONE) {
return base && fromPrimary && fromForeign
} else {
return base && getTable(throughId) && throughFromKey && throughToKey
@ -180,12 +181,12 @@
}
function otherRelationshipType(type) {
if (type === RelationshipTypes.MANY_TO_ONE) {
return RelationshipTypes.ONE_TO_MANY
} else if (type === RelationshipTypes.ONE_TO_MANY) {
return RelationshipTypes.MANY_TO_ONE
} else if (type === RelationshipTypes.MANY_TO_MANY) {
return RelationshipTypes.MANY_TO_MANY
if (type === RelationshipType.MANY_TO_ONE) {
return RelationshipType.ONE_TO_MANY
} else if (type === RelationshipType.ONE_TO_MANY) {
return RelationshipType.MANY_TO_ONE
} else if (type === RelationshipType.MANY_TO_MANY) {
return RelationshipType.MANY_TO_MANY
}
}
@ -217,7 +218,7 @@
// if any to many only need to check from
const manyToMany =
relateFrom.relationshipType === RelationshipTypes.MANY_TO_MANY
relateFrom.relationshipType === RelationshipType.MANY_TO_MANY
if (!manyToMany) {
delete relateFrom.through
@ -252,7 +253,7 @@
}
relateTo = {
...relateTo,
relationshipType: RelationshipTypes.ONE_TO_MANY,
relationshipType: RelationshipType.ONE_TO_MANY,
foreignKey: relateFrom.fieldName,
fieldName: fromPrimary,
}
@ -277,7 +278,7 @@
async function saveRelationship() {
if (!validate()) {
return false
return keepOpen
}
buildRelationships()
removeExistingRelationship()
@ -320,7 +321,7 @@
fromColumn = toRelationship.name
}
relationshipType =
fromRelationship.relationshipType || RelationshipTypes.MANY_TO_ONE
fromRelationship.relationshipType || RelationshipType.MANY_TO_ONE
if (selectedFromTable) {
fromId = selectedFromTable._id
fromColumn = selectedFromTable.name

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

@ -1,4 +1,4 @@
import { RelationshipTypes } from "constants/backend"
import { RelationshipType } from "constants/backend"
const typeMismatch = "Column type of the foreign key must match the primary key"
const columnBeingUsed = "Column name cannot be an existing column"
@ -40,7 +40,7 @@ export class RelationshipErrorChecker {
}
isMany() {
return this.type === RelationshipTypes.MANY_TO_MANY
return this.type === RelationshipType.MANY_TO_MANY
}
relationshipTypeSet(type) {

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

@ -44,7 +44,6 @@
let fileInput
let error = null
let fileName = null
let fileType = null
let loading = false
let validation = {}
let validateHash = ""
@ -73,7 +72,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

@ -35,9 +35,8 @@
try {
const isSelected =
decodeURIComponent($params.viewName) === $views.selectedViewName
const name = view.name
const id = view.tableId
await views.delete(name)
await views.delete(view)
notifications.success("View deleted")
if (isSelected) {
$goto(`./table/${id}`)

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

@ -8,6 +8,8 @@
closeBrackets,
completionKeymap,
closeBracketsKeymap,
acceptCompletion,
completionStatus,
} from "@codemirror/autocomplete"
import {
EditorView,
@ -35,7 +37,8 @@
defaultKeymap,
historyKeymap,
history,
indentWithTab,
indentMore,
indentLess,
} from "@codemirror/commands"
import { Compartment } from "@codemirror/state"
import { javascript } from "@codemirror/lang-javascript"
@ -109,6 +112,22 @@
let isDark = !currentTheme.includes("light")
let themeConfig = new Compartment()
const indentWithTabCustom = {
key: "Tab",
run: view => {
if (completionStatus(view.state) == "active") {
acceptCompletion(view)
return true
}
indentMore(view)
return true
},
shift: view => {
indentLess(view)
return true
},
}
const buildKeymap = () => {
const baseMap = [
...closeBracketsKeymap,
@ -116,7 +135,7 @@
...historyKeymap,
...foldKeymap,
...completionKeymap,
indentWithTab,
indentWithTabCustom,
]
return baseMap
}

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

@ -4,7 +4,8 @@
import { licensing } from "stores/portal"
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
$: isPremiumUser = $licensing.license && !$licensing.isFreePlan
$: isBusinessAndAbove =
$licensing.isBusinessPlan || $licensing.isEnterprisePlan
let show
let hide
@ -55,22 +56,22 @@
<div class="divider" />
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)}
<a
href={isPremiumUser
href={isBusinessAndAbove
? "mailto:support@budibase.com"
: "/builder/portal/account/usage"}
>
<div class="premiumLinkContent" class:disabled={!isPremiumUser}>
<div class="premiumLinkContent" class:disabled={!isBusinessAndAbove}>
<div class="icon">
<FontAwesomeIcon name="fa-solid fa-envelope" />
</div>
<Body size="S">Email support</Body>
</div>
{#if !isPremiumUser}
{#if !isBusinessAndAbove}
<div class="premiumBadge">
<div class="icon">
<FontAwesomeIcon name="fa-solid fa-lock" />
</div>
<Body size="XS">Premium</Body>
<Body size="XS">Business</Body>
</div>
{/if}
</a>

View File

@ -1,6 +1,8 @@
<script>
import { Icon } from "@budibase/bbui"
import { createEventDispatcher, getContext } from "svelte"
import { helpers } from "@budibase/shared-core"
import { UserAvatars } from "@budibase/frontend-core"
export let icon
export let withArrow = false
@ -18,12 +20,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 +47,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 +64,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 +73,8 @@
ondragover="return false"
ondragenter="return false"
{id}
{style}
{draggable}
>
<div class="nav-item-content" bind:this={contentRef}>
{#if withArrow}
@ -85,12 +99,19 @@
<Icon color={iconColor} size="S" name={icon} />
</div>
{/if}
<div class="text" title={showTooltip ? text : null}>{text}</div>
<div class="text" title={showTooltip ? text : null}>
{text}
{#if selectedBy}
<UserAvatars size="XS" users={selectedBy} />
{/if}
</div>
{#if withActions}
<div class="actions">
<slot />
</div>
{/if}
{#if $$slots.right}
<div class="right">
<slot name="right" />
@ -119,13 +140,16 @@
}
.nav-item.highlighted {
background-color: var(--spectrum-global-color-gray-200);
--avatars-background: var(--spectrum-global-color-gray-200);
}
.nav-item.selected {
background-color: var(--spectrum-global-color-gray-300);
--avatars-background: var(--spectrum-global-color-gray-300);
color: var(--ink);
}
.nav-item:hover {
background-color: var(--spectrum-global-color-gray-300);
--avatars-background: var(--spectrum-global-color-gray-300);
}
.nav-item:hover .actions {
visibility: visible;
@ -197,6 +221,9 @@
color: var(--spectrum-global-color-gray-900);
order: 2;
width: 0;
display: flex;
align-items: center;
gap: 8px;
}
.scrollable .text {
flex: 0 0 auto;

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

@ -10,19 +10,17 @@
Link,
Modal,
StatusLight,
AbsTooltip,
} from "@budibase/bbui"
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 +32,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 +65,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 +87,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 +131,201 @@
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">
<AbsTooltip
text={$isOnlyUser
? null
: "Unavailable - another user is editing this app"}
>
<Link
disabled={!$isOnlyUser}
quiet
secondary
on:click={revertApp}
>
Revert
</Link>
</AbsTooltip>
</span>
{:else}
<span class="status-text unpublished">Not published</span>
{/if}
</span>
</Body>
<div class="action-buttons">
{#if 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 +409,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

@ -23,6 +23,7 @@ import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
import GridColumnEditor from "./controls/ColumnEditor/GridColumnEditor.svelte"
import BarButtonList from "./controls/BarButtonList.svelte"
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte"
const componentMap = {
text: DrawerBindableInput,
@ -44,6 +45,7 @@ const componentMap = {
schema: SchemaSelect,
section: SectionSelect,
filter: FilterEditor,
"filter/relationship": RelationshipFilterEditor,
url: URLSelect,
fieldConfiguration: FieldConfiguration,
columns: ColumnEditor,

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,17 +194,23 @@
.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
}
const toDisplay = eventKey => {
const type = actionTypes.find(action => action.name == eventKey)
return type?.displayName || type?.name
}
</script>
<DrawerContent>
@ -200,7 +236,9 @@
<ul>
{#each category as actionType}
<li on:click={onAddAction(actionType)}>
<span class="action-name">{actionType.name}</span>
<span class="action-name">
{actionType.displayName || actionType.name}
</span>
</li>
{/each}
</ul>
@ -231,7 +269,7 @@
>
<Icon name="DragHandle" size="XL" />
<div class="action-header">
{index + 1}.&nbsp;{action[EVENT_TYPE_KEY]}
{index + 1}.&nbsp;{toDisplay(action[EVENT_TYPE_KEY])}
</div>
<Icon
name="Close"

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

@ -1,5 +1,5 @@
<script>
import { Select, Label, Checkbox, Input } from "@budibase/bbui"
import { Select, Label, Checkbox, Input, Body } from "@budibase/bbui"
import { tables } from "stores/backend"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
@ -10,47 +10,59 @@
</script>
<div class="root">
<Label>Table</Label>
<Select
bind:value={parameters.tableId}
options={tableOptions}
getOptionLabel={table => table.name}
getOptionValue={table => table._id}
/>
<Label small>Row ID</Label>
<DrawerBindableInput
{bindings}
title="Row ID to delete"
value={parameters.rowId}
on:change={value => (parameters.rowId = value.detail)}
/>
<Label small />
<Checkbox
text="Do not display default notification"
bind:value={parameters.notificationOverride}
/>
<br />
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
{#if parameters.confirm}
<Label small>Confirm text</Label>
<Input
placeholder="Are you sure you want to delete this row?"
bind:value={parameters.confirmText}
<Body size="small">Please specify one or more rows to delete.</Body>
<div class="params">
<Label>Table</Label>
<Select
bind:value={parameters.tableId}
options={tableOptions}
getOptionLabel={table => table.name}
getOptionValue={table => table._id}
/>
{/if}
<Label small>Row IDs</Label>
<DrawerBindableInput
{bindings}
title="Rows to delete"
value={parameters.rowId}
on:change={value => (parameters.rowId = value.detail)}
/>
<Label small />
<Checkbox
text="Do not display default notification"
bind:value={parameters.notificationOverride}
/>
<br />
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
{#if parameters.confirm}
<Label small>Confirm text</Label>
<Input
placeholder="Are you sure you want to delete?"
bind:value={parameters.confirmText}
/>
{/if}
</div>
</div>
<style>
.root {
width: 100%;
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-xl);
}
.params {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: 60px 1fr;
align-items: center;
max-width: 800px;
margin: 0 auto;
}
</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

@ -24,6 +24,7 @@
},
{
"name": "Delete Row",
"displayName": "Delete Rows",
"type": "data",
"component": "DeleteRow"
},

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,13 +13,14 @@
export let value = []
export let componentInstance
export let bindings = []
export let schema = null
let drawer
$: tempValue = value
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchemaForDatasource($currentAsset, datasource)?.schema
$: schemaFields = Object.values(schema || {})
$: dsSchema = getSchemaForDatasource($currentAsset, datasource)?.schema
$: schemaFields = Object.values(schema || dsSchema || {})
$: text = getText(value?.filter(filter => filter.field))
async function saveFilter() {

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

@ -0,0 +1,35 @@
<script>
import { currentAsset } from "builderStore"
import { findClosestMatchingComponent } from "builderStore/componentUtils"
import {
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
import { tables } from "stores/backend"
import FilterEditor from "./FilterEditor/FilterEditor.svelte"
export let componentInstance
// Extract which relationship column we're using
$: column = componentInstance.field
// Find the closest parent form
$: form = findClosestMatchingComponent(
$currentAsset.props,
componentInstance._id,
component => component._component.endsWith("/form")
)
// Get that form's schema
$: datasource = getDatasourceForProvider($currentAsset, form)
$: formSchema = getSchemaForDatasource($currentAsset, datasource)?.schema
// Get the schema for the relationship field that this picker is using
$: columnSchema = formSchema?.[column]
// Get the schema for the table on the other side of this relationship
$: linkedTable = $tables.list.find(x => x._id === columnSchema?.tableId)
$: schema = linkedTable?.schema
</script>
<FilterEditor on:change {...$$props} {schema} />

View File

@ -8,16 +8,29 @@
export let componentDefinition
export let type
const dispatch = createEventDispatcher()
let drawer
const dispatch = createEventDispatcher()
$: text = getText(value)
const save = () => {
dispatch("change", value)
drawer.hide()
}
const getText = rules => {
if (!rules?.length) {
return "No rules set"
} else {
return `${rules.length} rule${rules.length === 1 ? "" : "s"} set`
}
}
</script>
<ActionButton on:click={drawer.show}>Configure validation</ActionButton>
<div class="validation-editor">
<ActionButton on:click={drawer.show}>{text}</ActionButton>
</div>
<Drawer bind:this={drawer} title="Validation Rules">
<svelte:fragment slot="description">
Configure validation rules for this field.
@ -31,3 +44,9 @@
{componentDefinition}
/>
</Drawer>
<style>
.validation-editor :global(.spectrum-ActionButton) {
width: 100%;
}
</style>

View File

@ -14,8 +14,9 @@
Tab,
Modal,
ModalContent,
notifications,
Divider,
} from "@budibase/bbui"
import { notifications, Divider } from "@budibase/bbui"
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
@ -28,6 +29,7 @@
import KeyValueBuilder from "./KeyValueBuilder.svelte"
import { fieldsToSchema, schemaToFields } from "helpers/data/utils"
import AccessLevelSelect from "./AccessLevelSelect.svelte"
import { ValidQueryNameRegex } from "@budibase/shared-core"
export let query
@ -47,6 +49,7 @@
let saveModal
let override = false
let navigateTo = null
let nameError = null
// seed the transformer
if (query && !query.transformer) {
@ -77,7 +80,7 @@
$: queryConfig = integrationInfo?.query
$: shouldShowQueryConfig = queryConfig && query.queryVerb
$: readQuery = query.queryVerb === "read" || query.readable
$: queryInvalid = !query.name || (readQuery && data.length === 0)
$: queryInvalid = !query.name || nameError || (readQuery && data.length === 0)
//Cast field in query preview response to number if specified by schema
$: {
@ -139,9 +142,10 @@
queryStr = JSON.stringify(query)
}
notifications.success("Query saved successfully")
return response
} catch (error) {
notifications.error("Error saving query")
notifications.error(error.message || "Error saving query")
}
}
</script>
@ -183,8 +187,14 @@
value={query.name}
on:input={e => {
let newValue = e.target.value || ""
query.name = newValue.trim()
if (newValue.match(ValidQueryNameRegex)) {
query.name = newValue.trim()
nameError = null
} else {
nameError = "Invalid query name"
}
}}
error={nameError}
/>
</div>
{#if queryConfig}
@ -250,9 +260,9 @@
size="L"
/>
</div>
<Body size="S"
>Add a JavaScript function to transform the query result.</Body
>
<Body size="S">
Add a JavaScript function to transform the query result.
</Body>
<CodeMirrorEditor
height={200}
label="Transformer"
@ -264,13 +274,12 @@
</div>
<div class="viewer-controls">
<Heading size="S">Results</Heading>
<ButtonGroup gap="XS">
<ButtonGroup gap="S">
<Button
cta
disabled={queryInvalid}
on:click={async () => {
await saveQuery()
notifications.success(`Query saved successfully`)
// Go to the correct URL if we just created a new query
if (!query._rev) {
$goto(`../../${query._id}`)

View File

@ -1,5 +1,5 @@
<script>
import { Popover, Layout, Heading, Body, Button } from "@budibase/bbui"
import { Popover, Layout, Heading, Body, Button, Link } from "@budibase/bbui"
import { store } from "builderStore"
import { TOURS } from "./tours.js"
import { goto, layout, isActive } from "@roxi/routify"
@ -10,17 +10,20 @@
let tourStep
let tourStepIdx
let lastStep
let skipping = false
$: tourNodes = { ...$store.tourNodes }
$: tourKey = $store.tourKey
$: tourStepKey = $store.tourStepKey
$: tour = TOURS[tourKey]
$: tourOnSkip = tour?.onSkip
const updateTourStep = (targetStepKey, tourKey) => {
if (!tourKey) {
return
}
if (!tourSteps?.length) {
tourSteps = [...TOURS[tourKey]]
tourSteps = [...tour.steps]
}
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
lastStep = tourStepIdx + 1 == tourSteps.length
@ -71,23 +74,8 @@
tourStep.onComplete()
}
popover.hide()
if (tourStep.endRoute) {
$goto(tourStep.endRoute)
}
}
}
const previousStep = async () => {
if (tourStepIdx > 0) {
let target = tourSteps[tourStepIdx - 1]
if (target) {
store.update(state => ({
...state,
tourStepKey: target.id,
}))
navigateStep(target)
} else {
console.log("Could not retrieve step")
if (tour.endRoute) {
$goto(tour.endRoute)
}
}
}
@ -132,16 +120,23 @@
</Body>
<div class="tour-footer">
<div class="tour-navigation">
{#if tourStepIdx > 0}
<Button
{#if typeof tourOnSkip === "function"}
<Link
secondary
on:click={previousStep}
disabled={tourStepIdx == 0}
quiet
on:click={() => {
skipping = true
tourOnSkip()
if (tour.endRoute) {
$goto(tour.endRoute)
}
}}
disabled={skipping}
>
<div>Back</div>
</Button>
Skip
</Link>
{/if}
<Button cta on:click={nextStep}>
<Button cta on:click={nextStep} disabled={skipping}>
<div>{lastStep ? "Finish" : "Next"}</div>
</Button>
</div>
@ -157,9 +152,10 @@
padding: var(--spacing-xl);
}
.tour-navigation {
grid-gap: var(--spectrum-alias-grid-baseline);
grid-gap: var(--spacing-xl);
display: flex;
justify-content: end;
align-items: center;
}
.tour-body :global(.feature-list) {
margin-bottom: 0px;

View File

@ -13,7 +13,7 @@
const registerTourNode = (tourKey, stepKey) => {
if (ready && !registered && tourKey) {
currentTourStep = TOURS[tourKey].find(step => step.id === stepKey)
currentTourStep = TOURS[tourKey].steps.find(step => step.id === stepKey)
if (!currentTourStep) {
return
}

View File

@ -4,6 +4,7 @@ import { auth } from "stores/portal"
import analytics from "analytics"
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
import { API } from "api"
const ONBOARDING_EVENT_PREFIX = "onboarding"
export const TOUR_STEP_KEYS = {
@ -20,6 +21,37 @@ export const TOUR_KEYS = {
FEATURE_ONBOARDING: "feature-onboarding",
}
const endUserOnboarding = async ({ skipped = false } = {}) => {
// Mark the users onboarding as complete
// Clear all tour related state
if (get(auth).user) {
try {
await API.updateSelf({
onboardedAt: new Date().toISOString(),
})
if (skipped) {
tourEvent("skipped")
}
// Update the cached user
await auth.getSelf()
store.update(state => ({
...state,
tourNodes: undefined,
tourKey: undefined,
tourKeyStep: undefined,
onboarding: false,
}))
} catch (e) {
console.log("Onboarding failed", e)
return false
}
return true
}
}
const tourEvent = eventKey => {
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
eventSource: EventSource.PORTAL,
@ -28,111 +60,81 @@ const tourEvent = eventKey => {
const getTours = () => {
return {
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: [
{
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
title: "Data",
route: "/builder/app/:application/data",
layout: OnboardingData,
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
onLoad: async () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: {
steps: [
{
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
title: "Data",
route: "/builder/app/:application/data",
layout: OnboardingData,
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
onLoad: async () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
},
align: "left",
},
align: "left",
{
id: TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION,
title: "Design",
route: "/builder/app/:application/design",
layout: OnboardingDesign,
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
},
align: "left",
},
{
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
title: "Automations",
route: "/builder/app/:application/automation",
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
},
align: "left",
},
{
id: TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
title: "Users",
query: ".toprightnav #builder-app-users-button",
body: "Add users to your app and control what level of access they have.",
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT)
},
},
{
id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
title: "Publish",
layout: OnboardingPublish,
route: "/builder/app/:application/design",
query: ".toprightnav #builder-app-publish-button",
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
},
onComplete: endUserOnboarding,
},
],
onSkip: async () => {
await endUserOnboarding({ skipped: true })
},
{
id: TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION,
title: "Design",
route: "/builder/app/:application/design",
layout: OnboardingDesign,
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
endRoute: "/builder/app/:application/data",
},
[TOUR_KEYS.FEATURE_ONBOARDING]: {
steps: [
{
id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
title: "Users",
query: ".toprightnav #builder-app-users-button",
body: "Add users to your app and control what level of access they have.",
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT)
},
onComplete: endUserOnboarding,
},
align: "left",
},
{
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
title: "Automations",
route: "/builder/app/:application/automation",
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
},
align: "left",
},
{
id: TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
title: "Users",
query: ".toprightnav #builder-app-users-button",
body: "Add users to your app and control what level of access they have.",
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT)
},
},
{
id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
title: "Publish",
layout: OnboardingPublish,
route: "/builder/app/:application/design",
endRoute: "/builder/app/:application/data",
query: ".toprightnav #builder-app-publish-button",
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
},
onComplete: async () => {
// Mark the users onboarding as complete
// Clear all tour related state
if (get(auth).user) {
await API.updateSelf({
onboardedAt: new Date().toISOString(),
})
// Update the cached user
await auth.getSelf()
store.update(state => ({
...state,
tourNodes: undefined,
tourKey: undefined,
tourKeyStep: undefined,
onboarding: false,
}))
}
},
},
],
[TOUR_KEYS.FEATURE_ONBOARDING]: [
{
id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
title: "Users",
query: ".toprightnav #builder-app-users-button",
body: "Add users to your app and control what level of access they have.",
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT)
},
onComplete: async () => {
// Push the onboarding forward
if (get(auth).user) {
await API.updateSelf({
onboardedAt: new Date().toISOString(),
})
// Update the cached user
await auth.getSelf()
store.update(state => ({
...state,
tourNodes: undefined,
tourKey: undefined,
tourKeyStep: undefined,
onboarding: false,
}))
}
},
},
],
],
},
}
}

View File

@ -2,31 +2,39 @@
export let text
export let url
export let active = false
export let disabled = false
</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}
</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;
}
</style>

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