Merge branch 'develop' into test/qa-20-Add-relationship-tests-to-datasources

This commit is contained in:
Pedro Silva 2023-05-30 09:50:20 +01:00
commit 8ee3f083e8
209 changed files with 4780 additions and 2066 deletions

View File

@ -44,9 +44,12 @@ jobs:
node-version: 14.x
cache: "yarn"
- run: yarn
# Run build all the projects
- run: yarn build
# Check the types of the projects built via esbuild
- run: yarn check:types
test:
test-libraries:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@ -59,8 +62,27 @@ jobs:
node-version: 14.x
cache: "yarn"
- run: yarn
- run: yarn build --scope=@budibase/types --scope=@budibase/shared-core --scope=@budibase/string-templates
- run: yarn test --ignore=@budibase/pro
- run: yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
name: codecov-umbrella
verbose: true
test-services:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Use Node.js 14.x
uses: actions/setup-node@v3
with:
node-version: 14.x
cache: "yarn"
- run: yarn
- run: yarn test --scope=@budibase/worker --scope=@budibase/server
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos

View File

@ -37,7 +37,7 @@ jobs:
with:
node-version: 14.x
- run: yarn
- run: yarn install --frozen-lockfile
- name: Update versions
run: |
version=$(cat lerna.json \
@ -51,7 +51,7 @@ jobs:
node scripts/syncLocalDependencies.js $version
echo "Syncing yarn workspace"
yarn
- run: yarn build
- run: yarn build --configuration=production
- run: yarn build:sdk
- name: Publish budibase packages to NPM

View File

@ -42,7 +42,7 @@ jobs:
with:
node-version: 14.x
- run: yarn
- run: yarn install --frozen-lockfile
- name: Update versions
run: |
version=$(cat lerna.json \
@ -57,7 +57,7 @@ jobs:
echo "Syncing yarn workspace"
yarn
- run: yarn lint
- run: yarn build
- run: yarn build --configuration=production
- run: yarn build:sdk
- name: Publish budibase packages to NPM

2
.nvmrc
View File

@ -1 +1 @@
v14.20.1
v14.20.1

View File

@ -1 +1 @@
3.10.0
3.10.0

View File

@ -144,8 +144,6 @@ The following commands can be executed to manually get Budibase up and running (
`yarn` to install project dependencies
`yarn bootstrap` will install all budibase modules and symlink them together using lerna.
`yarn build` will build all budibase packages.
#### 4. Running
@ -243,7 +241,7 @@ An overview of the CI pipelines can be found [here](../.github/workflows/README.
Note that only budibase maintainers will be able to access the pro repo.
The `yarn bootstrap` command can be used to replace the NPM supplied dependency with the 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.
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

View File

@ -0,0 +1,77 @@
version: "3"
# optional ports are specified throughout for more advanced use cases.
services:
app-service:
build: ../packages/server
container_name: build-bbapps
environment:
SELF_HOSTED: 1
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
WORKER_URL: http://worker-service:4003
MINIO_URL: http://minio-service:9000
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
PORT: 4002
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
JWT_SECRET: ${JWT_SECRET}
LOG_LEVEL: info
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
ENABLE_ANALYTICS: "true"
REDIS_URL: redis-service:6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
PLUGINS_DIR: ${PLUGINS_DIR}
depends_on:
- worker-service
- redis-service
# volumes:
# - /some/path/to/plugins:/plugins
worker-service:
build: ../packages/worker
container_name: build-bbworker
environment:
SELF_HOSTED: 1
PORT: 4003
CLUSTER_PORT: ${MAIN_PORT}
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
JWT_SECRET: ${JWT_SECRET}
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
MINIO_URL: http://minio-service:9000
APPS_URL: http://app-service:4002
COUCH_DB_USERNAME: ${COUCH_DB_USER}
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
REDIS_URL: redis-service:6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
depends_on:
- redis-service
- minio-service
proxy-service-docker:
ports:
- "${MAIN_PORT}:10000"
container_name: build-bbproxy
image: budibase/proxy
environment:
- PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
- PROXY_RATE_LIMIT_API_PER_SECOND=20
- APPS_UPSTREAM_URL=http://app-service:4002
- WORKER_UPSTREAM_URL=http://worker-service:4003
- MINIO_UPSTREAM_URL=http://minio-service:9000
- COUCHDB_UPSTREAM_URL=http://couchdb-service:5984
- WATCHTOWER_UPSTREAM_URL=http://watchtower-service:8080
- RESOLVER=127.0.0.11
depends_on:
- minio-service
- worker-service
- app-service
- couchdb-service

View File

@ -1,22 +1,22 @@
FROM node:14-slim as build
FROM node:16-slim as build
# install node-gyp dependencies
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python
# add pin script
WORKDIR /
ADD scripts/pinVersions.js scripts/cleanup.sh ./
ADD scripts/cleanup.sh ./
RUN chmod +x /cleanup.sh
# build server
WORKDIR /app
ADD packages/server .
RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh
RUN yarn install --frozen-lockfile --production=true && /cleanup.sh
# build worker
WORKDIR /worker
ADD packages/worker .
RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh
RUN yarn install --frozen-lockfile --production=true && /cleanup.sh
FROM budibase/couchdb
ARG TARGETARCH
@ -31,9 +31,7 @@ COPY --from=build /worker /worker
# install base dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server && \
apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \
apt-get update
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
WORKDIR /nodejs

View File

@ -1,5 +1,5 @@
{
"version": "2.6.19-alpha.2",
"version": "2.6.19-alpha.27",
"npmClient": "yarn",
"packages": [
"packages/backend-core",

10
nx.json
View File

@ -6,5 +6,15 @@
"cacheableOperations": ["build", "test"]
}
}
},
"targetDefaults": {
"dev:builder": {
"dependsOn": [
{
"projects": ["@budibase/string-templates"],
"target": "build"
}
]
}
}
}

View File

@ -2,17 +2,23 @@
"name": "root",
"private": true,
"devDependencies": {
"@esbuild-plugins/node-resolve": "^0.2.2",
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
"@nx/js": "16.2.1",
"@rollup/plugin-json": "^4.0.2",
"@typescript-eslint/parser": "5.45.0",
"babel-eslint": "^10.0.3",
"esbuild": "^0.17.18",
"eslint": "^7.28.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": "^6.6.1",
"lerna": "7.0.0-alpha.0",
"madge": "^6.0.0",
"minimist": "^1.2.8",
"nx": "^16.2.1",
"prettier": "^2.3.1",
"prettier-plugin-svelte": "^2.3.0",
"rimraf": "^3.0.2",
@ -23,10 +29,11 @@
},
"scripts": {
"preinstall": "node scripts/syncProPackage.js",
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
"bootstrap": "./scripts/bootstrap.sh && lerna link && ./scripts/link-dependencies.sh",
"build": "lerna run --stream build",
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
"bootstrap": "./scripts/link-dependencies.sh && echo '***BOOTSTRAP ONLY REQUIRED FOR USE WITH ACCOUNT PORTAL***'",
"build": "yarn nx run-many -t=build",
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
"check:types": "lerna run check:types --skip-nx-cache",
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
"build:sdk": "lerna run --stream build:sdk",
@ -41,10 +48,11 @@
"kill-builder": "kill-port 3000",
"kill-server": "kill-port 4001 4002",
"kill-all": "yarn run kill-builder && yarn run kill-server",
"dev": "yarn run kill-all && lerna link && lerna run --stream --parallel dev:builder --concurrency 1 --stream",
"dev:noserver": "yarn run kill-builder && lerna link && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder --stream",
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && 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:docker": "yarn build && 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:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
@ -53,16 +61,16 @@
"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",
"build:docker": "lerna run --stream build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
"build:docker": "lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
"build:docker:pre": "lerna run --stream build && lerna run --stream predocker",
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
"build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
"build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .",
"build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .",
"build:docker:single": "npm run build:docker:pre && npm run build:docker:single:image",
"build:docker:single": "yarn build && lerna run --concurrency 1 predocker && yarn build:docker:single:image",
"build:docker:dependencies": "docker build -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest ./hosting",
"publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.2.1 --push ./hosting/couchdb",
"publish:docker:dependencies": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest -t budibase/dependencies:v3.2.1 --push ./hosting",
@ -101,5 +109,12 @@
"packages/worker",
"packages/pro/packages/pro"
]
}
},
"resolutions": {
"@budibase/backend-core": "0.0.0",
"@budibase/shared-core": "0.0.0",
"@budibase/string-templates": "0.0.0",
"@budibase/types": "0.0.0"
},
"dependencies": {}
}

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "0.0.1",
"version": "0.0.0",
"description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@ -22,7 +22,7 @@
"dependencies": {
"@budibase/nano": "10.1.2",
"@budibase/pouchdb-replication-stream": "1.2.10",
"@budibase/types": "0.0.1",
"@budibase/types": "0.0.0",
"@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2",
"aws-cloudfront-sign": "2.2.0",
@ -88,5 +88,19 @@
"tsconfig-paths": "4.0.0",
"typescript": "4.7.3"
},
"nx": {
"targets": {
"build": {
"dependsOn": [
{
"projects": [
"@budibase/types"
],
"target": "build"
}
]
}
}
},
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
}

View File

@ -27,6 +27,7 @@ export enum Databases {
GENERIC_CACHE = "data_cache",
WRITE_THROUGH = "writeThrough",
LOCKS = "locks",
SOCKET_IO = "socket_io",
}
/**

View File

@ -90,6 +90,10 @@ export const useScimIntegration = () => {
return useFeature(Feature.SCIM)
}
export const useSyncAutomations = () => {
return useFeature(Feature.SYNC_AUTOMATIONS)
}
// QUOTAS
export const setAutomationLogsQuota = (value: number) => {

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "0.0.1",
"version": "0.0.0",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@ -38,8 +38,8 @@
],
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "1.2.1",
"@budibase/shared-core": "0.0.1",
"@budibase/string-templates": "0.0.1",
"@budibase/shared-core": "0.0.0",
"@budibase/string-templates": "0.0.0",
"@spectrum-css/accordion": "3.0.24",
"@spectrum-css/actionbutton": "1.0.1",
"@spectrum-css/actiongroup": "1.0.1",
@ -90,5 +90,19 @@
"resolutions": {
"loader-utils": "1.4.1"
},
"nx": {
"targets": {
"build": {
"dependsOn": [
{
"projects": [
"@budibase/string-templates"
],
"target": "build"
}
]
}
}
},
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
}

View File

@ -102,7 +102,9 @@
margin-left: 0;
transition: color ease-out 130ms;
}
.is-selected:not(.spectrum-ActionButton--emphasized):not(.spectrum-ActionButton--quiet) {
.is-selected:not(.spectrum-ActionButton--emphasized):not(
.spectrum-ActionButton--quiet
) {
background: var(--spectrum-global-color-gray-300);
border-color: var(--spectrum-global-color-gray-500);
}

View File

@ -13,10 +13,12 @@
export let url = ""
export let disabled = false
export let initials = "JD"
export let color = null
const DefaultColor = "#3aab87"
$: color = getColor(initials)
$: avatarColor = color || getColor(initials)
$: style = getStyle(size, avatarColor)
const getColor = initials => {
if (!initials?.length) {
@ -26,6 +28,12 @@
const hue = ((code % 26) / 26) * 360
return `hsl(${hue}, 50%, 50%)`
}
const getStyle = (sizeKey, color) => {
const size = `var(${sizes.get(sizeKey)})`
const fontSize = `calc(${size} / 2)`
return `width:${size}; height:${size}; font-size:${fontSize}; background:${color};`
}
</script>
{#if url}
@ -37,13 +45,7 @@
style="width: var({sizes.get(size)}); height: var({sizes.get(size)});"
/>
{:else}
<div
class="spectrum-Avatar"
class:is-disabled={disabled}
style="width: var({sizes.get(size)}); height: var({sizes.get(
size
)}); font-size: calc(var({sizes.get(size)}) / 2); background: {color};"
>
<div class="spectrum-Avatar" class:is-disabled={disabled} {style}>
{initials || ""}
</div>
{/if}

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.0.1",
"version": "0.0.0",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -9,7 +9,7 @@
"dev:builder": "routify -c dev:vite",
"dev:vite": "vite --host 0.0.0.0",
"rollup": "rollup -c -w",
"test": "vitest"
"test": "vitest run"
},
"jest": {
"globals": {
@ -58,11 +58,11 @@
}
},
"dependencies": {
"@budibase/bbui": "0.0.1",
"@budibase/frontend-core": "0.0.1",
"@budibase/shared-core": "0.0.1",
"@budibase/string-templates": "0.0.1",
"@budibase/types": "0.0.1",
"@budibase/bbui": "0.0.0",
"@budibase/frontend-core": "0.0.0",
"@budibase/shared-core": "0.0.0",
"@budibase/string-templates": "0.0.0",
"@budibase/types": "0.0.0",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
@ -117,5 +117,31 @@
"vite": "^3.0.8",
"vitest": "^0.29.2"
},
"nx": {
"targets": {
"build": {
"dependsOn": [
{
"projects": [
"@budibase/string-templates",
"@budibase/shared-core"
],
"target": "build"
}
]
},
"test": {
"dependsOn": [
{
"projects": [
"@budibase/shared-core",
"@budibase/string-templates"
],
"target": "build"
}
]
}
}
},
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
}

View File

@ -2,6 +2,7 @@ import { datasources, tables } from "../stores/backend"
import { IntegrationNames } from "../constants/backend"
import { get } from "svelte/store"
import cloneDeep from "lodash/cloneDeepWith"
import { API } from "api"
function prepareData(config) {
let datasource = {}
@ -37,3 +38,9 @@ export async function createRestDatasource(integration) {
const config = cloneDeep(integration)
return saveDatasource(config)
}
export async function validateDatasourceConfig(config) {
const datasource = prepareData(config)
const resp = await API.validateDatasource(datasource)
return resp
}

View File

@ -2,6 +2,7 @@ import { getFrontendStore } from "./store/frontend"
import { getAutomationStore } from "./store/automation"
import { getTemporalStore } from "./store/temporal"
import { getThemeStore } from "./store/theme"
import { getUserStore } from "./store/users"
import { derived } from "svelte/store"
import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core"
@ -12,6 +13,7 @@ export const store = getFrontendStore()
export const automationStore = getAutomationStore()
export const themeStore = getThemeStore()
export const temporalStore = getTemporalStore()
export const userStore = getUserStore()
// Setup history for screens
export const screenHistoryStore = createHistoryStore({

View File

@ -37,8 +37,10 @@ import {
} from "builderStore/dataBinding"
import { makePropSafe as safe } from "@budibase/string-templates"
import { getComponentFieldOptions } from "helpers/formFields"
import { createBuilderWebsocket } from "builderStore/websocket"
const INITIAL_FRONTEND_STATE = {
initialised: false,
apps: [],
name: "",
url: "",
@ -70,6 +72,7 @@ const INITIAL_FRONTEND_STATE = {
previewDevice: "desktop",
highlightedSettingKey: null,
builderSidePanel: false,
hasLock: true,
// URL params
selectedScreenId: null,
@ -86,6 +89,7 @@ const INITIAL_FRONTEND_STATE = {
export const getFrontendStore = () => {
const store = writable({ ...INITIAL_FRONTEND_STATE })
let websocket
// This is a fake implementation of a "patch" API endpoint to try and prevent
// 409s. All screen doc mutations (aside from creation) use this function,
@ -110,10 +114,11 @@ export const getFrontendStore = () => {
store.actions = {
reset: () => {
store.set({ ...INITIAL_FRONTEND_STATE })
websocket?.disconnect()
},
initialise: async pkg => {
const { layouts, screens, application, clientLibPath } = pkg
const { layouts, screens, application, clientLibPath, hasLock } = pkg
websocket = createBuilderWebsocket()
await store.actions.components.refreshDefinitions(application.appId)
// Reset store state
@ -137,6 +142,8 @@ export const getFrontendStore = () => {
upgradableVersion: application.upgradableVersion,
navigation: application.navigation || {},
usedPlugins: application.usedPlugins || [],
hasLock,
initialised: true,
}))
screenHistoryStore.reset()
automationHistoryStore.reset()

View File

@ -0,0 +1,42 @@
import { writable, get } from "svelte/store"
export const getUserStore = () => {
const store = writable([])
const init = users => {
store.set(users)
}
const updateUser = user => {
const $users = get(store)
if (!$users.some(x => x.sessionId === user.sessionId)) {
store.set([...$users, user])
} else {
store.update(state => {
const index = state.findIndex(x => x.sessionId === user.sessionId)
state[index] = user
return state.slice()
})
}
}
const removeUser = user => {
store.update(state => {
return state.filter(x => x.sessionId !== user.sessionId)
})
}
const reset = () => {
store.set([])
}
return {
...store,
actions: {
init,
updateUser,
removeUser,
reset,
},
}
}

View File

@ -1,3 +1,4 @@
import { ActionStepID } from "constants/backend/automations"
import { TableNames } from "../constants"
import {
AUTO_COLUMN_DISPLAY_NAMES,
@ -53,3 +54,9 @@ export function buildAutoColumn(tableName, name, subtype) {
}
return base
}
export function checkForCollectStep(automation) {
return automation.definition.steps.some(
step => step.stepId === ActionStepID.COLLECT
)
}

View File

@ -0,0 +1,39 @@
import { createWebsocket } from "@budibase/frontend-core"
import { userStore } from "builderStore"
import { datasources, tables } from "stores/backend"
export const createBuilderWebsocket = () => {
const socket = createWebsocket("/socket/builder")
// Connection events
socket.on("connect", () => {
socket.emit("get-users", null, response => {
userStore.actions.init(response.users)
})
})
socket.on("connect_error", err => {
console.log("Failed to connect to builder websocket:", err.message)
})
// User events
socket.on("user-update", userStore.actions.updateUser)
socket.on("user-disconnect", userStore.actions.removeUser)
// Table events
socket.on("table-change", ({ id, table }) => {
tables.replaceTable(id, table)
})
// Table events
socket.on("datasource-change", ({ id, datasource }) => {
datasources.replaceDatasource(id, datasource)
})
return {
...socket,
disconnect: () => {
socket?.disconnect()
userStore.actions.reset()
},
}
}

View File

@ -6,24 +6,48 @@
Body,
Icon,
notifications,
Tags,
Tag,
} from "@budibase/bbui"
import { automationStore } from "builderStore"
import { admin } from "stores/portal"
import { automationStore, selectedAutomation } from "builderStore"
import { admin, licensing } from "stores/portal"
import { externalActions } from "./ExternalActions"
import { TriggerStepID } from "constants/backend/automations"
import { checkForCollectStep } from "builderStore/utils"
export let blockIdx
export let lastStep
const disabled = {
SEND_EMAIL_SMTP: {
disabled: !$admin.checklist.smtp.checked,
message: "Please configure SMTP",
},
}
let syncAutomationsEnabled = $licensing.syncAutomationsEnabled
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
let selectedAction
let actionVal
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
$: collectBlockExists = checkForCollectStep($selectedAutomation)
const disabled = () => {
return {
SEND_EMAIL_SMTP: {
disabled: !$admin.checklist.smtp.checked,
message: "Please configure SMTP",
},
COLLECT: {
disabled: !lastStep || !syncAutomationsEnabled || collectBlockExists,
message: collectDisabledMessage(),
},
}
}
const collectDisabledMessage = () => {
if (collectBlockExists) {
return "Only one Collect step allowed"
}
if (!lastStep) {
return "Only available as the last step"
}
}
const external = actions.reduce((acc, elm) => {
const [k, v] = elm
if (!v.internal && !v.custom) {
@ -38,6 +62,15 @@
acc[k] = v
}
delete acc.LOOP
// Filter out Collect block if not App Action or Webhook
if (
!collectBlockAllowedSteps.includes(
$selectedAutomation.definition.trigger.stepId
)
) {
delete acc.COLLECT
}
return acc
}, {})
@ -48,7 +81,6 @@
}
return acc
}, {})
console.log(plugins)
const selectAction = action => {
actionVal = action
@ -72,7 +104,7 @@
<ModalContent
title="Add automation step"
confirmText="Save"
size="M"
size="L"
disabled={!selectedAction}
onConfirm={addBlockToAutomation}
>
@ -107,7 +139,7 @@
<Detail size="S">Actions</Detail>
<div class="item-list">
{#each Object.entries(internal) as [idx, action]}
{@const isDisabled = disabled[idx] && disabled[idx].disabled}
{@const isDisabled = disabled()[idx] && disabled()[idx].disabled}
<div
class="item"
class:disabled={isDisabled}
@ -117,8 +149,14 @@
<div class="item-body">
<Icon name={action.icon} />
<Body size="XS">{action.name}</Body>
{#if isDisabled}
<Icon name="Help" tooltip={disabled[idx].message} />
{#if isDisabled && !syncAutomationsEnabled}
<div class="tag-color">
<Tags>
<Tag icon="LockClosed">Business</Tag>
</Tags>
</div>
{:else if isDisabled}
<Icon name="Help" tooltip={disabled()[idx].message} />
{/if}
</div>
</div>
@ -152,6 +190,7 @@
display: flex;
margin-left: var(--spacing-m);
gap: var(--spacing-m);
align-items: center;
}
.item-list {
display: grid;
@ -181,4 +220,8 @@
.disabled :global(.spectrum-Body) {
color: var(--spectrum-global-color-gray-600);
}
.tag-color :global(.spectrum-Tags-item) {
background: var(--spectrum-global-color-gray-200);
}
</style>

View File

@ -17,7 +17,11 @@
import ActionModal from "./ActionModal.svelte"
import FlowItemHeader from "./FlowItemHeader.svelte"
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
import { ActionStepID, TriggerStepID } from "constants/backend/automations"
import {
ActionStepID,
TriggerStepID,
Features,
} from "constants/backend/automations"
import { permissions } from "stores/backend"
export let block
@ -31,6 +35,9 @@
let showLooping = false
let role
$: collectBlockExists = $selectedAutomation.definition.steps.some(
step => step.stepId === ActionStepID.COLLECT
)
$: automationId = $selectedAutomation?._id
$: showBindingPicker =
block.stepId === ActionStepID.CREATE_ROW ||
@ -184,7 +191,7 @@
{#if !isTrigger}
<div>
<div class="block-options">
{#if !loopBlock}
{#if block?.features?.[Features.LOOPING] || !block.features}
<ActionButton on:click={() => addLooping()} icon="Reuse">
Add Looping
</ActionButton>
@ -224,21 +231,28 @@
</Layout>
</div>
{/if}
<Modal bind:this={actionModal} width="30%">
<ActionModal {blockIdx} />
</Modal>
<Modal bind:this={webhookModal} width="30%">
<CreateWebhookModal />
</Modal>
</div>
<div class="separator" />
<Icon on:click={() => actionModal.show()} hoverable name="AddCircle" size="S" />
{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2}
{#if !collectBlockExists || !lastStep}
<div class="separator" />
<Icon
on:click={() => actionModal.show()}
hoverable
name="AddCircle"
size="S"
/>
{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2}
<div class="separator" />
{/if}
{/if}
<Modal bind:this={actionModal} width="30%">
<ActionModal {lastStep} {blockIdx} />
</Modal>
<Modal bind:this={webhookModal} width="30%">
<CreateWebhookModal />
</Modal>
<style>
.delete-padding {
padding-left: 30px;

View File

@ -16,11 +16,11 @@
import GridEditColumnModal from "components/backend/DataTable/modals/grid/GridEditColumnModal.svelte"
const userSchemaOverrides = {
firstName: { name: "First name", disabled: true },
lastName: { name: "Last name", disabled: true },
email: { name: "Email", disabled: true },
roleId: { name: "Role", disabled: true },
status: { name: "Status", disabled: true },
firstName: { displayName: "First name", disabled: true },
lastName: { displayName: "Last name", disabled: true },
email: { displayName: "Email", disabled: true },
roleId: { displayName: "Role", disabled: true },
status: { displayName: "Status", disabled: true },
}
$: id = $tables.selected?._id
@ -32,10 +32,11 @@
<Grid
{API}
tableId={id}
tableType={$tables.selected?.type}
allowAddRows={!isUsersTable}
allowDeleteRows={!isUsersTable}
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
on:updatetable={e => tables.updateTable(e.detail)}
showAvatars={false}
>
<svelte:fragment slot="controls">
{#if isInternal}

View File

@ -3,6 +3,7 @@
import ImportModal from "../modals/ImportModal.svelte"
export let tableId
export let tableType
export let disabled
let modal
@ -12,5 +13,5 @@
Import
</ActionButton>
<Modal bind:this={modal}>
<ImportModal {tableId} on:importrows />
<ImportModal {tableId} {tableType} on:importrows />
</Modal>

View File

@ -4,11 +4,12 @@
export let disabled = false
const { rows, tableId } = getContext("grid")
const { rows, tableId, tableType } = getContext("grid")
</script>
<ImportButton
{disabled}
tableId={$tableId}
{tableType}
on:importrows={rows.actions.refreshData}
/>

View File

@ -113,17 +113,26 @@
})
download(data, `export.${exportFormat}`)
} else if (filters || sorting) {
const data = await API.exportRows({
tableId: view,
format: exportFormat,
search: {
query: luceneFilter,
sort: sorting?.sortColumn,
sortOrder: sorting?.sortOrder,
paginate: false,
},
})
download(data, `export.${exportFormat}`)
let response
try {
response = await API.exportRows({
tableId: view,
format: exportFormat,
search: {
query: luceneFilter,
sort: sorting?.sortColumn,
sortOrder: sorting?.sortOrder,
paginate: false,
},
})
} catch (e) {
console.error("Failed to export", e)
notifications.error("Export Failed")
}
if (response) {
download(response, `export.${exportFormat}`)
notifications.success("Export Successful")
}
} else {
await exportView()
}

View File

@ -13,15 +13,18 @@
const dispatch = createEventDispatcher()
export let tableId
export let tableType
let rows = []
let allValid = false
let displayColumn = null
let identifierFields = []
async function importData() {
try {
await API.importTableData({
tableId,
rows,
identifierFields,
})
notifications.success("Rows successfully imported")
} catch (error) {
@ -45,6 +48,13 @@
</Body>
<Layout gap="XS" noPadding>
<Label grey extraSmall>CSV or JSON file to import</Label>
<TableDataImport {tableId} bind:rows bind:allValid bind:displayColumn />
<TableDataImport
{tableId}
{tableType}
bind:rows
bind:allValid
bind:displayColumn
bind:identifierFields
/>
</Layout>
</ModalContent>

View File

@ -53,6 +53,7 @@
config,
schema: selected.datasource,
auth: selected.auth,
features: selected.features || [],
}
if (selected.friendlyName) {
integration.name = selected.friendlyName

View File

@ -4,55 +4,68 @@
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
import { IntegrationNames } from "constants/backend"
import cloneDeep from "lodash/cloneDeepWith"
import { saveDatasource as save } from "builderStore/datasource"
import { onMount } from "svelte"
import {
saveDatasource as save,
validateDatasourceConfig,
} from "builderStore/datasource"
import { DatasourceFeature } from "@budibase/types"
export let integration
export let modal
// kill the reference so the input isn't saved
let datasource = cloneDeep(integration)
let skipFetch = false
let isValid = false
$: name =
IntegrationNames[datasource.type] || datasource.name || datasource.type
async function validateConfig() {
const displayError = message =>
notifications.error(message ?? "Error validating datasource")
let connected = false
try {
const resp = await validateDatasourceConfig(datasource)
if (!resp.connected) {
displayError(`Unable to connect - ${resp.error}`)
}
connected = resp.connected
} catch (err) {
displayError(err?.message)
}
return connected
}
async function saveDatasource() {
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
const valid = await validateConfig()
if (!valid) {
return false
}
}
try {
if (!datasource.name) {
datasource.name = name
}
const resp = await save(datasource, skipFetch)
const resp = await save(datasource)
$goto(`./datasource/${resp._id}`)
notifications.success(`Datasource updated successfully.`)
notifications.success(`Datasource created successfully.`)
} catch (err) {
notifications.error(err?.message ?? "Error saving datasource")
// prevent the modal from closing
return false
}
}
onMount(() => {
skipFetch = false
})
</script>
<ModalContent
title={`Connect to ${name}`}
onConfirm={() => saveDatasource()}
onCancel={() => modal.show()}
confirmText={datasource.plus
? "Save and fetch tables"
: "Save and continue to query"}
confirmText={datasource.plus ? "Connect" : "Save and continue to query"}
cancelText="Back"
showSecondaryButton={datasource.plus}
secondaryButtonText={datasource.plus ? "Skip table fetch" : undefined}
secondaryAction={() => {
skipFetch = true
saveDatasource()
return true
}}
size="L"
disabled={!isValid}
>

View File

@ -1,5 +1,5 @@
<script>
import { Select } from "@budibase/bbui"
import { Select, Toggle, Multiselect } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import { API } from "api"
import { parseFile } from "./utils"
@ -9,14 +9,17 @@
let fileType = null
let loading = false
let updateExistingRows = false
let validation = {}
let validateHash = ""
let schema = null
let invalidColumns = []
export let tableId = null
export let tableType
export let rows = []
export let allValid = false
export let identifierFields = []
const typeOptions = [
{
@ -159,6 +162,22 @@
</div>
{/each}
</div>
{#if tableType === "internal"}
<br />
<Toggle
bind:value={updateExistingRows}
on:change={() => (identifierFields = [])}
thin
text="Update existing rows"
/>
{#if updateExistingRows}
<Multiselect
label="Identifier field(s)"
options={Object.keys(validation)}
bind:value={identifierFields}
/>
{/if}
{/if}
{#if invalidColumns.length > 0}
<p class="spectrum-FieldLabel spectrum-FieldLabel--sizeM">
The following columns are present in the data you wish to import, but do

View File

@ -1,143 +0,0 @@
<script>
import {
Button,
ButtonGroup,
ModalContent,
Modal,
notifications,
ProgressCircle,
Layout,
Body,
Icon,
} from "@budibase/bbui"
import { auth, apps } from "stores/portal"
import { processStringSync } from "@budibase/string-templates"
import { API } from "api"
export let app
export let buttonSize = "M"
let APP_DEV_LOCK_SECONDS = 600 //common area for this?
let appLockModal
let processing = false
$: lockedBy = app?.lockedBy
$: lockedByYou = $auth.user.email === lockedBy?.email
$: lockIdentifer = `${
lockedBy && lockedBy.firstName ? lockedBy?.firstName : lockedBy?.email
}`
$: lockedByHeading =
lockedBy && lockedByYou ? "Locked by you" : `Locked by ${lockIdentifer}`
const getExpiryDuration = app => {
if (!app?.lockedBy?.lockedAt) {
return -1
}
let expiry =
new Date(app.lockedBy.lockedAt).getTime() + APP_DEV_LOCK_SECONDS * 1000
return expiry - new Date().getTime()
}
const releaseLock = async () => {
processing = true
if (app) {
try {
await API.releaseAppLock(app.devId)
await apps.load()
notifications.success("Lock released successfully")
} catch (err) {
notifications.error("Error releasing lock")
}
} else {
notifications.error("No application is selected")
}
processing = false
}
</script>
{#if lockedBy}
<div class="lock-status">
<Icon
name="LockClosed"
hoverable
size={buttonSize}
on:click={e => {
e.stopPropagation()
appLockModal.show()
}}
/>
</div>
{/if}
<Modal bind:this={appLockModal}>
<ModalContent
title={lockedByHeading}
showConfirmButton={false}
showCancelButton={false}
>
<Layout noPadding>
<Body size="S">
Apps are locked to prevent work being lost from overlapping changes
between your team.
</Body>
{#if lockedByYou && getExpiryDuration(app) > 0}
<span class="lock-expiry-body">
{processStringSync(
"This lock will expire in {{ duration time 'millisecond' }} from now.",
{
time: getExpiryDuration(app),
}
)}
</span>
{/if}
<div class="lock-modal-actions">
<ButtonGroup>
<Button
secondary
quiet={lockedBy && lockedByYou}
disabled={processing}
on:click={() => {
appLockModal.hide()
}}
>
<span class="cancel"
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span
>
</Button>
{#if lockedByYou}
<Button
cta
disabled={processing}
on:click={() => {
releaseLock()
appLockModal.hide()
}}
>
{#if processing}
<ProgressCircle overBackground={true} size="S" />
{:else}
<span class="unlock">Release Lock</span>
{/if}
</Button>
{/if}
</ButtonGroup>
</div>
</Layout>
</ModalContent>
</Modal>
<style>
.lock-modal-actions {
display: flex;
justify-content: flex-end;
margin-top: var(--spacing-l);
gap: var(--spacing-xl);
}
.lock-status {
display: flex;
gap: var(--spacing-s);
max-width: 175px;
}
</style>

View File

@ -146,15 +146,18 @@
/* Override default active line highlight colour in dark theme */
div
:global(.CodeMirror-focused.cm-s-tomorrow-night-eighties
.CodeMirror-activeline-background) {
:global(
.CodeMirror-focused.cm-s-tomorrow-night-eighties
.CodeMirror-activeline-background
) {
background: rgba(255, 255, 255, 0.075);
}
/* Remove active line styling when not focused */
div
:global(.CodeMirror:not(.CodeMirror-focused)
.CodeMirror-activeline-background) {
:global(
.CodeMirror:not(.CodeMirror-focused) .CodeMirror-activeline-background
) {
background: unset;
}

View File

@ -113,109 +113,113 @@
})
</script>
<div class="action-top-nav">
<div class="action-buttons">
<div class="version">
<VersionModal />
</div>
<RevertModal />
{#if isPublished}
<div class="publish-popover">
<div bind:this={publishPopoverAnchor}>
<ActionButton
quiet
icon="Globe"
size="M"
tooltip="Your published app"
on:click={publishPopover.show()}
/>
</div>
<Popover
bind:this={publishPopover}
align="right"
disabled={!isPublished}
anchor={publishPopoverAnchor}
offset={10}
>
<div class="popover-content">
<Layout noPadding gap="M">
<Heading size="XS">Your published app</Heading>
<Body size="S">
<span class="publish-popover-message">
{processStringSync(
"Last published {{ duration time 'millisecond' }} ago",
{
time:
new Date().getTime() -
new Date(latestDeployments[0].updatedAt).getTime(),
}
)}
</span>
</Body>
<div class="buttons">
<Button
warning={true}
icon="GlobeStrike"
disabled={!isPublished}
on:click={unpublishApp}
>
Unpublish
</Button>
<Button cta on:click={viewApp}>View app</Button>
</div>
</Layout>
</div>
</Popover>
{#if $store.hasLock}
<div class="action-top-nav">
<div class="action-buttons">
<div class="version">
<VersionModal />
</div>
{/if}
<RevertModal />
{#if !isPublished}
<ActionButton
quiet
icon="GlobeStrike"
size="M"
tooltip="Your app has not been published yet"
disabled
/>
{/if}
{#if isPublished}
<div class="publish-popover">
<div bind:this={publishPopoverAnchor}>
<ActionButton
quiet
icon="Globe"
size="M"
tooltip="Your published app"
on:click={publishPopover.show()}
/>
</div>
<Popover
bind:this={publishPopover}
align="right"
disabled={!isPublished}
anchor={publishPopoverAnchor}
offset={10}
>
<div class="popover-content">
<Layout noPadding gap="M">
<Heading size="XS">Your published app</Heading>
<Body size="S">
<span class="publish-popover-message">
{processStringSync(
"Last published {{ duration time 'millisecond' }} ago",
{
time:
new Date().getTime() -
new Date(latestDeployments[0].updatedAt).getTime(),
}
)}
</span>
</Body>
<div class="buttons">
<Button
warning={true}
icon="GlobeStrike"
disabled={!isPublished}
on:click={unpublishApp}
>
Unpublish
</Button>
<Button cta on:click={viewApp}>View app</Button>
</div>
</Layout>
</div>
</Popover>
</div>
{/if}
<TourWrap
tourStepKey={$store.onboarding
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
>
<span id="builder-app-users-button">
{#if !isPublished}
<ActionButton
quiet
icon="UserGroup"
icon="GlobeStrike"
size="M"
on:click={() => {
store.update(state => {
state.builderSidePanel = true
return state
})
}}
>
Users
</ActionButton>
</span>
</TourWrap>
</div>
</div>
tooltip="Your app has not been published yet"
disabled
/>
{/if}
<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>
<TourWrap
tourStepKey={$store.onboarding
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
>
<span id="builder-app-users-button">
<ActionButton
quiet
icon="UserGroup"
size="M"
on:click={() => {
store.update(state => {
state.builderSidePanel = true
return state
})
}}
>
Users
</ActionButton>
</span>
</TourWrap>
</div>
</div>
<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>
{/if}
<div class="buttons">
<Button on:click={previewApp} secondary>Preview</Button>
<DeployModal onOk={completePublish} />
{#if $store.hasLock}
<DeployModal onOk={completePublish} />
{/if}
</div>
<style>

View File

@ -126,8 +126,7 @@
}
const getAllBindings = (bindings, eventContextBindings, actions) => {
let allBindings = eventContextBindings.concat(bindings)
let allBindings = []
if (!actions) {
return []
}
@ -145,14 +144,35 @@
.forEach(action => {
// Check we have a binding for this action, and generate one if not
const stateBinding = makeStateBinding(action.parameters.key)
const hasKey = allBindings.some(binding => {
const hasKey = bindings.some(binding => {
return binding.runtimeBinding === stateBinding.runtimeBinding
})
if (!hasKey) {
allBindings.push(stateBinding)
bindings.push(stateBinding)
}
})
// Get which indexes are asynchronous automations as we want to filter them out from the bindings
const asynchronousAutomationIndexes = actions
.map((action, index) => {
if (
action[EVENT_TYPE_KEY] === "Trigger Automation" &&
!action.parameters?.synchronous
) {
return index
}
})
.filter(index => index !== undefined)
// Based on the above, filter out the asynchronous automations from the bindings
if (asynchronousAutomationIndexes) {
allBindings = eventContextBindings
.filter((binding, index) => {
return !asynchronousAutomationIndexes.includes(index)
})
.concat(bindings)
} else {
allBindings = eventContextBindings.concat(bindings)
}
return allBindings
}
</script>

View File

@ -1,8 +1,8 @@
<script>
import { Select, Label, Input, Checkbox } from "@budibase/bbui"
import { Select, Label, Input, Checkbox, Icon } from "@budibase/bbui"
import { automationStore } from "builderStore"
import SaveFields from "./SaveFields.svelte"
import { TriggerStepID } from "constants/backend/automations"
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
export let parameters = {}
export let bindings = []
@ -16,6 +16,14 @@
? AUTOMATION_STATUS.EXISTING
: AUTOMATION_STATUS.NEW
$: {
if (automationStatus === AUTOMATION_STATUS.NEW) {
parameters.synchronous = false
}
parameters.synchronous = automations.find(
automation => automation._id === parameters.automationId
)?.synchronous
}
$: automations = $automationStore.automations
.filter(a => a.definition.trigger?.stepId === TriggerStepID.APP)
.map(automation => {
@ -23,10 +31,15 @@
automation.definition.trigger.inputs.fields || {}
).map(([name, type]) => ({ name, type }))
let hasCollectBlock = automation.definition.steps.some(
step => step.stepId === ActionStepID.COLLECT
)
return {
name: automation.name,
_id: automation._id,
schema,
synchronous: hasCollectBlock,
}
})
$: hasAutomations = automations && automations.length > 0
@ -35,6 +48,8 @@
)
$: selectedSchema = selectedAutomation?.schema
$: error = parameters.timeout > 120 ? "Timeout must be less than 120s" : null
const onFieldsChanged = e => {
parameters.fields = Object.entries(e.detail || {}).reduce(
(acc, [key, value]) => {
@ -57,6 +72,14 @@
parameters.fields = {}
parameters.automationId = automations[0]?._id
}
const onChange = value => {
let automationId = value.detail
parameters.synchronous = automations.find(
automation => automation._id === automationId
)?.synchronous
parameters.automationId = automationId
}
</script>
<div class="root">
@ -85,6 +108,7 @@
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
<Select
on:change={onChange}
bind:value={parameters.automationId}
placeholder="Choose automation"
options={automations}
@ -98,6 +122,29 @@
/>
{/if}
{#if parameters.synchronous}
<Label small />
<div class="synchronous-info">
<Icon name="Info" />
<div>
<i
>This automation will run synchronously as it contains a Collect
step</i
>
</div>
</div>
<Label small />
<div class="timeout-width">
<Input
label="Timeout in seconds (120 max)"
type="number"
{error}
bind:value={parameters.timeout}
/>
</div>
{/if}
<Label small />
<Checkbox
text="Do not display default notification"
@ -133,6 +180,9 @@
max-width: 800px;
margin: 0 auto;
}
.timeout-width {
width: 30%;
}
.params {
display: grid;
@ -142,6 +192,11 @@
align-items: center;
}
.synchronous-info {
display: flex;
gap: var(--spacing-s);
}
.fields {
margin-top: var(--spacing-l);
display: grid;

View File

@ -57,7 +57,13 @@
{
"name": "Trigger Automation",
"type": "application",
"component": "TriggerAutomation"
"component": "TriggerAutomation",
"context": [
{
"label": "Automation Result",
"value": "result"
}
]
},
{
"name": "Update Field Value",

View File

@ -1,13 +1,16 @@
<script>
import { Heading, Body, Button, Icon, notifications } from "@budibase/bbui"
import AppLockModal from "../common/AppLockModal.svelte"
import { Heading, Body, Button, Icon } from "@budibase/bbui"
import { processStringSync } from "@budibase/string-templates"
import { goto } from "@roxi/routify"
import { helpers } from "@budibase/shared-core"
import { UserAvatar } from "@budibase/frontend-core"
export let app
export let lockedAction
$: editing = app?.lockedBy != null
$: initials = helpers.getUserInitials(app?.lockedBy)
const handleDefaultClick = () => {
if (window.innerWidth < 640) {
goToOverview()
@ -17,12 +20,6 @@
}
const goToBuilder = () => {
if (app.lockedOther) {
notifications.error(
`App locked by ${app.lockedBy.email}. Please allow lock to expire or have them unlock this app.`
)
return
}
$goto(`../../app/${app.devId}`)
}
@ -44,7 +41,10 @@
</div>
<div class="updated">
{#if app.updatedAt}
{#if editing}
Currently editing
<UserAvatar user={app.lockedBy} />
{:else if app.updatedAt}
{processStringSync("Updated {{ duration time 'millisecond' }} ago", {
time: new Date().getTime() - new Date(app.updatedAt).getTime(),
})}
@ -59,12 +59,12 @@
</div>
<div class="app-row-actions">
<AppLockModal {app} buttonSize="M" />
<Button size="S" secondary on:click={lockedAction || goToOverview}
>Manage</Button
>
<Button size="S" primary on:click={lockedAction || goToBuilder}>Edit</Button
>
<Button size="S" secondary on:click={lockedAction || goToOverview}>
Manage
</Button>
<Button size="S" primary on:click={lockedAction || goToBuilder}>
Edit
</Button>
</div>
</div>
@ -87,6 +87,9 @@
.updated {
color: var(--spectrum-global-color-gray-700);
display: flex;
align-items: center;
gap: 8px;
}
.title,

View File

@ -20,9 +20,14 @@ export const ActionStepID = {
FILTER: "FILTER",
QUERY_ROWS: "QUERY_ROWS",
LOOP: "LOOP",
COLLECT: "COLLECT",
// these used to be lowercase step IDs, maintain for backwards compat
discord: "discord",
slack: "slack",
zapier: "zapier",
integromat: "integromat",
}
export const Features = {
LOOPING: "LOOPING",
}

View File

@ -0,0 +1,28 @@
<script>
import { UserAvatar } from "@budibase/frontend-core"
export let users = []
$: uniqueUsers = unique(users)
const unique = users => {
let uniqueUsers = {}
users?.forEach(user => {
uniqueUsers[user.email] = user
})
return Object.values(uniqueUsers)
}
</script>
<div class="avatars">
{#each uniqueUsers as user}
<UserAvatar {user} tooltipDirection="bottom" />
{/each}
</div>
<style>
.avatars {
display: flex;
gap: 4px;
}
</style>

View File

@ -1,5 +1,5 @@
<script>
import { store, automationStore } from "builderStore"
import { store, automationStore, userStore } from "builderStore"
import { roles, flags } from "stores/backend"
import { auth } from "stores/portal"
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
@ -13,7 +13,6 @@
Modal,
notifications,
} from "@budibase/bbui"
import AppActions from "components/deploy/AppActions.svelte"
import { API } from "api"
import { isActive, goto, layout, redirect } from "@roxi/routify"
@ -23,6 +22,7 @@
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
import UserAvatars from "./_components/UserAvatars.svelte"
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
export let application
@ -30,7 +30,9 @@
let promise = getPackage()
let hasSynced = false
let commandPaletteModal
let loaded = false
$: loaded && initTour()
$: selected = capitalise(
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
)
@ -43,6 +45,7 @@
await automationStore.actions.fetch()
await roles.fetch()
await flags.fetch()
loaded = true
return pkg
} catch (error) {
notifications.error(`Error initialising app: ${error?.message}`)
@ -67,13 +70,18 @@
// Event handler for the command palette
const handleKeyDown = e => {
if (e.key === "k" && (e.ctrlKey || e.metaKey)) {
if (e.key === "k" && (e.ctrlKey || e.metaKey) && $store.hasLock) {
e.preventDefault()
commandPaletteModal.toggle()
}
}
const initTour = async () => {
// Skip tour if we don't have the lock
if (!$store.hasLock) {
return
}
// Check if onboarding is enabled.
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
if (!$auth.user?.onboardedAt) {
@ -110,7 +118,6 @@
// check if user has beta access
// const betaResponse = await API.checkBetaAccess($auth?.user?.email)
// betaAccess = betaResponse.access
initTour()
} catch (error) {
notifications.error("Failed to sync with production database")
}
@ -119,10 +126,7 @@
})
onDestroy(() => {
store.update(state => {
state.appId = null
return state
})
store.actions.reset()
})
</script>
@ -134,74 +138,89 @@
<div class="root">
<div class="top-nav">
<div class="topleftnav">
<ActionMenu>
<div slot="control">
<Icon size="M" hoverable name="ShowMenu" />
</div>
<MenuItem on:click={() => $goto("../../portal/apps")}>
Exit to portal
</MenuItem>
<MenuItem
on:click={() => $goto(`../../portal/overview/${application}`)}
>
Overview
</MenuItem>
<MenuItem
on:click={() => $goto(`../../portal/overview/${application}/access`)}
>
Access
</MenuItem>
<MenuItem
on:click={() =>
$goto(`../../portal/overview/${application}/automation-history`)}
>
Automation history
</MenuItem>
<MenuItem
on:click={() => $goto(`../../portal/overview/${application}/backups`)}
>
Backups
</MenuItem>
{#if $store.initialised}
<div class="topleftnav">
<ActionMenu>
<div slot="control">
<Icon size="M" hoverable name="ShowMenu" />
</div>
<MenuItem on:click={() => $goto("../../portal/apps")}>
Exit to portal
</MenuItem>
<MenuItem
on:click={() => $goto(`../../portal/overview/${application}`)}
>
Overview
</MenuItem>
<MenuItem
on:click={() =>
$goto(`../../portal/overview/${application}/access`)}
>
Access
</MenuItem>
<MenuItem
on:click={() =>
$goto(`../../portal/overview/${application}/automation-history`)}
>
Automation history
</MenuItem>
<MenuItem
on:click={() =>
$goto(`../../portal/overview/${application}/backups`)}
>
Backups
</MenuItem>
<MenuItem
on:click={() =>
$goto(`../../portal/overview/${application}/name-and-url`)}
>
Name and URL
</MenuItem>
<MenuItem
on:click={() => $goto(`../../portal/overview/${application}/version`)}
>
Version
</MenuItem>
</ActionMenu>
<Heading size="XS">{$store.name}</Heading>
</div>
<div class="topcenternav">
<Tabs {selected} size="M">
{#each $layout.children as { path, title }}
<TourWrap tourStepKey={`builder-${title}-section`}>
<Tab
quiet
selected={$isActive(path)}
on:click={topItemNavigate(path)}
title={capitalise(title)}
id={`builder-${title}-tab`}
/>
</TourWrap>
{/each}
</Tabs>
</div>
<div class="toprightnav">
<AppActions {application} />
</div>
<MenuItem
on:click={() =>
$goto(`../../portal/overview/${application}/name-and-url`)}
>
Name and URL
</MenuItem>
<MenuItem
on:click={() =>
$goto(`../../portal/overview/${application}/version`)}
>
Version
</MenuItem>
</ActionMenu>
<Heading size="XS">{$store.name}</Heading>
</div>
<div class="topcenternav">
{#if $store.hasLock}
<Tabs {selected} size="M">
{#each $layout.children as { path, title }}
<TourWrap tourStepKey={`builder-${title}-section`}>
<Tab
quiet
selected={$isActive(path)}
on:click={topItemNavigate(path)}
title={capitalise(title)}
id={`builder-${title}-tab`}
/>
</TourWrap>
{/each}
</Tabs>
{:else}
<div class="secondary-editor">
<Icon name="LockClosed" />
Another user is currently editing your screens and automations
</div>
{/if}
</div>
<div class="toprightnav">
<UserAvatars users={$userStore} />
<AppActions {application} />
</div>
{/if}
</div>
{#await promise}
<!-- This should probably be some kind of loading state? -->
<div class="loading" />
{:then _}
<slot />
<div class="body">
<slot />
</div>
{:catch error}
<p>Something went wrong: {error.message}</p>
{/await}
@ -237,6 +256,7 @@
box-sizing: border-box;
align-items: stretch;
border-bottom: var(--border-light);
z-index: 2;
}
.topleftnav {
@ -270,4 +290,18 @@
align-items: center;
gap: var(--spacing-l);
}
.secondary-editor {
align-self: center;
display: flex;
flex-direction: row;
gap: 8px;
}
.body {
flex: 1 1 auto;
z-index: 1;
display: flex;
flex-direction: column;
}
</style>

View File

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

View File

@ -20,6 +20,8 @@
import { isEqual } from "lodash"
import { cloneDeep } from "lodash/fp"
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
import { API } from "api"
import { DatasourceFeature } from "@budibase/types"
const querySchema = {
name: {},
@ -45,7 +47,30 @@
}
}
async function validateConfig() {
const displayError = message =>
notifications.error(message ?? "Error validating datasource")
let connected = false
try {
const resp = await API.validateDatasource(datasource)
if (!resp.connected) {
displayError(`Unable to connect - ${resp.error}`)
}
connected = resp.connected
} catch (err) {
displayError(err?.message)
}
return connected
}
const saveDatasource = async () => {
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
const valid = await validateConfig()
if (!valid) {
return false
}
}
try {
// Create datasource
await datasources.save(datasource)

View File

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

View File

@ -5,7 +5,6 @@
Divider,
ActionMenu,
MenuItem,
Avatar,
Page,
Icon,
Body,
@ -22,6 +21,8 @@
import { processStringSync } from "@budibase/string-templates"
import Spaceman from "assets/bb-space-man.svg"
import Logo from "assets/bb-emblem.svg"
import { UserAvatar } from "@budibase/frontend-core"
import { helpers } from "@budibase/shared-core"
let loaded = false
let userInfoModal
@ -96,11 +97,7 @@
<img class="logo" alt="logo" src={$organisation.logoUrl || Logo} />
<ActionMenu align="right">
<div slot="control" class="avatar">
<Avatar
size="M"
initials={$auth.initials}
url={$auth.user.pictureUrl}
/>
<UserAvatar user={$auth.user} showTooltip={false} />
<Icon size="XL" name="ChevronDown" />
</div>
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
@ -125,7 +122,7 @@
</div>
<Layout noPadding gap="XS">
<Heading size="M">
Hey {$auth.user.firstName || $auth.user.email}
Hey {helpers.getUserLabel($auth.user)}
</Heading>
<Body>
Welcome to the {$organisation.company} portal. Below you'll find the

View File

@ -1,11 +1,12 @@
<script>
import { auth } from "stores/portal"
import { ActionMenu, Avatar, MenuItem, Icon, Modal } from "@budibase/bbui"
import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui"
import { goto } from "@roxi/routify"
import ProfileModal from "components/settings/ProfileModal.svelte"
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
import ThemeModal from "components/settings/ThemeModal.svelte"
import APIKeyModal from "components/settings/APIKeyModal.svelte"
import { UserAvatar } from "@budibase/frontend-core"
let themeModal
let profileModal
@ -23,7 +24,7 @@
<ActionMenu align="right">
<div slot="control" class="user-dropdown">
<Avatar size="M" initials={$auth.initials} url={$auth.user.pictureUrl} />
<UserAvatar user={$auth.user} showTooltip={false} />
<Icon size="XL" name="ChevronDown" />
</div>
<MenuItem icon="UserEdit" on:click={() => profileModal.show()}>

View File

@ -1,15 +1,10 @@
<script>
import { Avatar, Tooltip } from "@budibase/bbui"
import { Tooltip } from "@budibase/bbui"
import { UserAvatar } from "@budibase/frontend-core"
export let row
let showTooltip
const getInitials = user => {
let initials = ""
initials += user.firstName ? user.firstName[0] : ""
initials += user.lastName ? user.lastName[0] : ""
return initials === "" ? user.email[0] : initials
}
</script>
{#if row?.user?.email}
@ -19,7 +14,7 @@
on:focus={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)}
>
<Avatar size="M" initials={getInitials(row.user)} />
<UserAvatar user={row.user} />
</div>
{#if showTooltip}
<div class="tooltip">

View File

@ -31,6 +31,18 @@
return "Invalid URL"
}
}
$: urlManuallySet = false
const updateUrl = event => {
const appName = event.detail
if (urlManuallySet) {
return
}
const parsedUrl = appName.toLowerCase().replace(/\s+/g, "-")
url = encodeURI(parsedUrl)
}
</script>
<div>
@ -43,11 +55,13 @@
bind:value={name}
bind:error={nameError}
validate={validateName}
on:change={updateUrl}
label="Name"
/>
<FancyInput
bind:value={url}
bind:error={urlError}
on:change={() => (urlManuallySet = true)}
validate={validateUrl}
label="URL"
/>

View File

@ -18,6 +18,8 @@
import { Roles } from "constants/backend"
import Spinner from "components/common/Spinner.svelte"
import { helpers } from "@budibase/shared-core"
import { validateDatasourceConfig } from "builderStore/datasource"
import { DatasourceFeature } from "@budibase/types"
let name = "My first app"
let url = "my-first-app"
@ -108,7 +110,24 @@
isGoogle,
}) => {
let app
try {
if (
datasourceConfig &&
plusIntegrations[stage].features[DatasourceFeature.CONNECTION_CHECKING]
) {
const resp = await validateDatasourceConfig({
config: datasourceConfig,
type: stage,
})
if (!resp.connected) {
notifications.error(
`Unable to connect - ${resp.error ?? "Error validating datasource"}`
)
return false
}
}
app = await createApp(useSampleData)
let datasource

View File

@ -24,7 +24,6 @@
import { AppStatus } from "constants"
import analytics, { Events, EventSource } from "analytics"
import { store } from "builderStore"
import AppLockModal from "components/common/AppLockModal.svelte"
import EditableIcon from "components/common/EditableIcon.svelte"
import { API } from "api"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -80,13 +79,6 @@
}
const editApp = () => {
if (appLocked && !lockedByYou) {
const identifier = app?.lockedBy?.firstName || app?.lockedBy?.email
notifications.warning(
`App locked by ${identifier}. Please allow lock to expire or have them unlock this app.`
)
return
}
$goto(`../../../app/${app.devId}`)
}
@ -135,7 +127,6 @@
/>
</div>
<div slot="buttons">
<AppLockModal {app} />
<span class="desktop">
<Button
size="M"

View File

@ -1,14 +1,11 @@
<script>
import getUserInitials from "helpers/userInitials.js"
import { Avatar } from "@budibase/bbui"
import { UserAvatar } from "@budibase/frontend-core"
export let value
$: initials = getUserInitials(value)
</script>
<div title={value.email} class="cell">
<Avatar size="M" {initials} />
<div class="cell">
<UserAvatar user={value} />
</div>
<style>

View File

@ -7,7 +7,6 @@
Icon,
Heading,
Link,
Avatar,
Layout,
Body,
notifications,
@ -15,7 +14,7 @@
import { store } from "builderStore"
import { processStringSync } from "@budibase/string-templates"
import { users, auth, apps, groups, overview } from "stores/portal"
import { fetchData } from "@budibase/frontend-core"
import { fetchData, UserAvatar } from "@budibase/frontend-core"
import { API } from "api"
import GroupIcon from "../../users/groups/_components/GroupIcon.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -56,14 +55,6 @@
appEditor = await users.get(editorId)
}
const getInitials = user => {
let initials = ""
initials += user.firstName ? user.firstName[0] : ""
initials += user.lastName ? user.lastName[0] : ""
return initials === "" ? user.email[0] : initials
}
const confirmUnpublishApp = async () => {
try {
await API.unpublishApp(app.prodId)
@ -140,7 +131,7 @@
<div class="last-edited-content">
<div class="updated-by">
{#if appEditor}
<Avatar size="M" initials={getInitials(appEditor)} />
<UserAvatar user={appEditor} showTooltip={false} />
<div class="editor-name">
{appEditor._id === $auth.user._id ? "You" : appEditorText}
</div>
@ -201,7 +192,7 @@
<div class="users">
<div class="list">
{#each appUsers.slice(0, 4) as user}
<Avatar size="M" initials={getInitials(user)} />
<UserAvatar {user} />
{/each}
</div>
<div class="text">

View File

@ -115,27 +115,6 @@
align-items: center;
}
input[type="file"] {
display: none;
}
.sso-link-icon {
padding-top: 4px;
margin-left: 3px;
}
.sso-link {
margin-top: 12px;
display: flex;
flex-direction: row;
align-items: center;
}
.enforce-sso-title {
margin-right: 10px;
}
.enforce-sso-heading-container {
display: flex;
flex-direction: row;
align-items: start;
}
.provider-title {
display: flex;
flex-direction: row;
@ -143,9 +122,6 @@
align-items: center;
gap: var(--spacing-m);
}
.provider-title span {
flex: 1 1 auto;
}
.inputContainer {
display: flex;
flex-direction: row;

View File

@ -2,7 +2,6 @@
import { goto, url } from "@roxi/routify"
import {
ActionMenu,
Avatar,
Button,
Layout,
Heading,
@ -25,13 +24,14 @@
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
import DeleteUserModal from "./_components/DeleteUserModal.svelte"
import GroupIcon from "../groups/_components/GroupIcon.svelte"
import { Constants } from "@budibase/frontend-core"
import { Constants, UserAvatar } from "@budibase/frontend-core"
import { Breadcrumbs, Breadcrumb } from "components/portal/page"
import RemoveGroupTableRenderer from "./_components/RemoveGroupTableRenderer.svelte"
import GroupNameTableRenderer from "../groups/_components/GroupNameTableRenderer.svelte"
import AppNameTableRenderer from "./_components/AppNameTableRenderer.svelte"
import AppRoleTableRenderer from "./_components/AppRoleTableRenderer.svelte"
import ScimBanner from "../_components/SCIMBanner.svelte"
import { helpers } from "@budibase/shared-core"
export let userId
@ -91,7 +91,7 @@
$: readonly = !$auth.isAdmin || scimEnabled
$: privileged = user?.admin?.global || user?.builder?.global
$: nameLabel = getNameLabel(user)
$: initials = getInitials(nameLabel)
$: initials = helpers.getUserInitials(user)
$: filteredGroups = getFilteredGroups($groups, searchTerm)
$: availableApps = getAvailableApps($apps, privileged, user?.roles)
$: userGroups = $groups.filter(x => {
@ -150,17 +150,6 @@
return label
}
const getInitials = nameLabel => {
if (!nameLabel) {
return "?"
}
return nameLabel
.split(" ")
.slice(0, 2)
.map(x => x[0])
.join("")
}
async function updateUserFirstName(evt) {
try {
await users.save({ ...user, firstName: evt.target.value })
@ -238,7 +227,7 @@
<div class="title">
<div class="user-info">
<Avatar size="XXL" {initials} />
<UserAvatar size="XXL" {user} showTooltip={false} />
<div class="subtitle">
<Heading size="M">{nameLabel}</Heading>
{#if nameLabel !== user?.email}

View File

@ -1,4 +1,4 @@
import { writable, derived } from "svelte/store"
import { writable, derived, get } from "svelte/store"
import { queries, tables } from "./"
import { API } from "api"
@ -25,6 +25,8 @@ export function createDatasourcesStore() {
store.update(state => ({
...state,
selectedDatasourceId: id,
// Remove any possible schema error
schemaError: null,
}))
}
@ -89,6 +91,39 @@ export function createDatasourcesStore() {
})
}
// Handles external updates of datasources
const replaceDatasource = (datasourceId, datasource) => {
if (!datasourceId) {
return
}
// Handle deletion
if (!datasource) {
store.update(state => ({
...state,
list: state.list.filter(x => x._id !== datasourceId),
}))
return
}
// Add new datasource
const index = get(store).list.findIndex(x => x._id === datasource._id)
if (index === -1) {
store.update(state => ({
...state,
list: [...state.list, datasource],
}))
}
// Update existing datasource
else if (datasource) {
store.update(state => {
state.list[index] = datasource
return state
})
}
}
return {
subscribe: derivedStore.subscribe,
fetch,
@ -98,6 +133,7 @@ export function createDatasourcesStore() {
save,
delete: deleteDatasource,
removeSchemaError,
replaceDatasource,
}
}

View File

@ -22,18 +22,6 @@ export function createTablesStore() {
}))
}
const fetchTable = async tableId => {
const table = await API.fetchTableDefinition(tableId)
store.update(state => {
const indexToUpdate = state.list.findIndex(t => t._id === table._id)
state.list[indexToUpdate] = table
return {
...state,
}
})
}
const select = tableId => {
store.update(state => ({
...state,
@ -74,20 +62,23 @@ export function createTablesStore() {
}
const savedTable = await API.saveTable(updatedTable)
await fetch()
replaceTable(table._id, savedTable)
if (table.type === "external") {
await datasources.fetch()
}
await select(savedTable._id)
select(savedTable._id)
return savedTable
}
const deleteTable = async table => {
if (!table?._id || !table?._rev) {
return
}
await API.deleteTable({
tableId: table?._id,
tableRev: table?._rev,
tableId: table._id,
tableRev: table._rev,
})
await fetch()
replaceTable(table._id, null)
}
const saveField = async ({
@ -135,35 +126,56 @@ export function createTablesStore() {
await save(draft)
}
const updateTable = table => {
const index = get(store).list.findIndex(x => x._id === table._id)
if (index === -1) {
// Handles external updates of tables
const replaceTable = (tableId, table) => {
if (!tableId) {
return
}
// This function has to merge state as there discrepancies with the table
// API endpoints. The table list endpoint and get table endpoint use the
// "type" property to mean different things.
store.update(state => {
state.list[index] = {
...table,
type: state.list[index].type,
}
return state
})
// Handle deletion
if (!table) {
store.update(state => ({
...state,
list: state.list.filter(x => x._id !== tableId),
}))
return
}
// Add new table
const index = get(store).list.findIndex(x => x._id === table._id)
if (index === -1) {
store.update(state => ({
...state,
list: [...state.list, table],
}))
}
// Update existing table
else if (table) {
// This function has to merge state as there discrepancies with the table
// API endpoints. The table list endpoint and get table endpoint use the
// "type" property to mean different things.
store.update(state => {
state.list[index] = {
...table,
type: state.list[index].type,
}
return state
})
}
}
return {
...store,
subscribe: derivedStore.subscribe,
fetch,
fetchTable,
init: fetch,
select,
save,
delete: deleteTable,
saveField,
deleteField,
updateTable,
replaceTable,
}
}

View File

@ -1,4 +1,4 @@
import { writable, get, derived } from "svelte/store"
import { writable, derived } from "svelte/store"
import { tables } from "./"
import { API } from "api"
@ -27,21 +27,31 @@ export function createViewsStore() {
const deleteView = async view => {
await API.deleteView(view)
await tables.fetch()
// Update tables
tables.update(state => {
const table = state.list.find(table => table._id === view.tableId)
if (table) {
delete table.views[view.name]
}
return { ...state }
})
}
const save = async view => {
const savedView = await API.saveView(view)
const viewMeta = {
name: view.name,
...savedView,
}
const viewTable = get(tables).list.find(table => table._id === view.tableId)
if (view.originalName) delete viewTable.views[view.originalName]
viewTable.views[view.name] = viewMeta
await tables.save(viewTable)
// Update tables
tables.update(state => {
const table = state.list.find(table => table._id === view.tableId)
if (table) {
if (view.originalName) {
delete table.views[view.originalName]
}
table.views[view.name] = savedView
}
return { ...state }
})
}
return {

View File

@ -1,7 +1,7 @@
import { writable } from "svelte/store"
import { API } from "api"
import { licensing } from "./licensing"
import { ConfigType } from "../../../../types/src/documents"
import { ConfigType } from "@budibase/types"
export const createFeatureStore = () => {
const internalStore = writable({

View File

@ -116,6 +116,9 @@ export const createLicensingStore = () => {
const auditLogsEnabled = license.features.includes(
Constants.Features.AUDIT_LOGS
)
const syncAutomationsEnabled = license.features.includes(
Constants.Features.SYNC_AUTOMATIONS
)
store.update(state => {
return {
...state,
@ -130,6 +133,7 @@ export const createLicensingStore = () => {
environmentVariablesEnabled,
auditLogsEnabled,
enforceableSSO,
syncAutomationsEnabled,
}
})
},

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "0.0.1",
"version": "0.0.0",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "dist/src/index.js",
"bin": {
@ -29,9 +29,9 @@
"outputPath": "build"
},
"dependencies": {
"@budibase/backend-core": "0.0.1",
"@budibase/string-templates": "0.0.1",
"@budibase/types": "0.0.1",
"@budibase/backend-core": "0.0.0",
"@budibase/string-templates": "0.0.0",
"@budibase/types": "0.0.0",
"axios": "0.21.2",
"chalk": "4.1.0",
"cli-progress": "3.11.2",
@ -63,5 +63,19 @@
"renamer": "^4.0.0",
"ts-node": "^10.9.1",
"typescript": "4.7.3"
},
"nx": {
"targets": {
"build": {
"dependsOn": [
{
"projects": [
"@budibase/backend-core"
],
"target": "build"
}
]
}
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.0.1",
"version": "0.0.0",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -19,11 +19,11 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "0.0.1",
"@budibase/frontend-core": "0.0.1",
"@budibase/shared-core": "0.0.1",
"@budibase/string-templates": "0.0.1",
"@budibase/types": "0.0.1",
"@budibase/bbui": "0.0.0",
"@budibase/frontend-core": "0.0.0",
"@budibase/shared-core": "0.0.0",
"@budibase/string-templates": "0.0.0",
"@budibase/types": "0.0.0",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",
@ -65,5 +65,20 @@
"resolutions": {
"loader-utils": "1.4.1"
},
"nx": {
"targets": {
"build": {
"dependsOn": [
{
"projects": [
"@budibase/string-templates",
"@budibase/shared-core"
],
"target": "build"
}
]
}
}
},
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
}

View File

@ -54,8 +54,9 @@
color: white;
}
div
:global(.apexcharts-theme-dark
.apexcharts-tooltip-series-group.apexcharts-active) {
:global(
.apexcharts-theme-dark .apexcharts-tooltip-series-group.apexcharts-active
) {
padding-bottom: 0;
}
</style>

View File

@ -72,9 +72,11 @@
:global(.spectrum-Form-itemField .spectrum-Textfield--multiline) {
min-height: calc(var(--height) - 24px);
}
:global(.spectrum-Form--labelsAbove
.spectrum-Form-itemField
.spectrum-Textfield--multiline) {
:global(
.spectrum-Form--labelsAbove
.spectrum-Form-itemField
.spectrum-Textfield--multiline
) {
min-height: calc(var(--height) - 24px);
}
</style>

View File

@ -122,13 +122,23 @@ const deleteRowHandler = async action => {
}
const triggerAutomationHandler = async action => {
const { fields, notificationOverride } = action.parameters
const { fields, notificationOverride, timeout } = action.parameters
if (fields) {
try {
await API.triggerAutomation({
const result = await API.triggerAutomation({
automationId: action.parameters.automationId,
fields,
timeout,
})
// Value will exist if automation is synchronous, so return it.
if (result.value) {
if (!notificationOverride) {
notificationStore.actions.success("Automation ran successfully")
}
return { result }
}
if (!notificationOverride) {
notificationStore.actions.success("Automation triggered")
}
@ -138,7 +148,6 @@ const triggerAutomationHandler = async action => {
}
}
}
const navigationHandler = action => {
const { url, peek, externalNewTab } = action.parameters
routeStore.actions.navigate(url, peek, externalNewTab)

View File

@ -1,13 +1,13 @@
{
"name": "@budibase/frontend-core",
"version": "0.0.1",
"version": "0.0.0",
"description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase",
"license": "MPL-2.0",
"svelte": "src/index.js",
"dependencies": {
"@budibase/bbui": "0.0.1",
"@budibase/shared-core": "0.0.1",
"@budibase/bbui": "0.0.0",
"@budibase/shared-core": "0.0.0",
"dayjs": "^1.11.7",
"lodash": "^4.17.21",
"socket.io-client": "^4.6.1",

View File

@ -4,10 +4,10 @@ export const buildAutomationEndpoints = API => ({
* @param automationId the ID of the automation to trigger
* @param fields the fields to trigger the automation with
*/
triggerAutomation: async ({ automationId, fields }) => {
triggerAutomation: async ({ automationId, fields, timeout }) => {
return await API.post({
url: `/api/automations/${automationId}/trigger`,
body: { fields },
body: { fields, timeout },
})
},

View File

@ -58,4 +58,15 @@ export const buildDatasourceEndpoints = API => ({
url: `/api/datasources/${datasourceId}/${datasourceRev}`,
})
},
/**
* Validate a datasource configuration
* @param datasource the datasource configuration to validate
*/
validateDatasource: async datasource => {
return await API.post({
url: `/api/datasources/verify`,
body: { datasource },
})
},
})

View File

@ -62,13 +62,15 @@ export const buildTableEndpoints = API => ({
/**
* Imports data into an existing table
* @param tableId the table ID to import to
* @param data the data import object
* @param rows the data import object
* @param identifierFields column names to be used as keys for overwriting existing rows
*/
importTableData: async ({ tableId, rows }) => {
importTableData: async ({ tableId, rows, identifierFields }) => {
return await API.post({
url: `/api/tables/${tableId}/import`,
body: {
rows,
identifierFields,
},
})
},

View File

@ -0,0 +1,58 @@
<script>
import { Avatar, Tooltip } from "@budibase/bbui"
import { helpers } from "@budibase/shared-core"
export let user
export let size
export let tooltipDirection = "top"
export let showTooltip = true
$: tooltipStyle = getTooltipStyle(tooltipDirection)
const getTooltipStyle = direction => {
if (!direction) {
return ""
}
if (direction === "top") {
return "transform: translateX(-50%) translateY(-100%);"
} else if (direction === "bottom") {
return "transform: translateX(-50%) translateY(100%);"
}
}
</script>
{#if user}
<div class="user-avatar">
<Avatar
{size}
initials={helpers.getUserInitials(user)}
color={helpers.getUserColor(user)}
/>
{#if showTooltip}
<div class="tooltip" style={tooltipStyle}>
<Tooltip
direction={tooltipDirection}
textWrapping
text={user.email}
size="S"
/>
</div>
{/if}
</div>
{/if}
<style>
.user-avatar {
position: relative;
}
.tooltip {
display: none;
position: absolute;
top: 0;
left: 50%;
white-space: nowrap;
}
.user-avatar:hover .tooltip {
display: block;
}
</style>

View File

@ -37,6 +37,9 @@
.boolean-cell {
padding: 2px var(--cell-padding);
pointer-events: none;
flex: 1 1 auto;
display: flex;
justify-content: center;
}
.boolean-cell.editable {
pointer-events: all;

View File

@ -11,6 +11,7 @@
export let selected
export let rowFocused
export let rowIdx
export let topRow = false
export let focused
export let selectedUser
export let column
@ -68,6 +69,7 @@
{highlighted}
{selected}
{rowIdx}
{topRow}
{focused}
{selectedUser}
{readonly}

View File

@ -6,6 +6,7 @@
export let selectedUser = null
export let error = null
export let rowIdx
export let topRow = false
export let defaultHeight = false
export let center = false
export let readonly = false
@ -15,7 +16,7 @@
const getStyle = (width, selectedUser) => {
let style = `flex: 0 0 ${width}px;`
if (selectedUser) {
style += `--cell-color:${selectedUser.color};`
style += `--user-color:${selectedUser.color};`
}
return style
}
@ -31,13 +32,14 @@
class:readonly
class:default-height={defaultHeight}
class:selected-other={selectedUser != null}
class:alt={rowIdx % 2 === 1}
class:top={topRow}
on:focus
on:mousedown
on:mouseup
on:click
on:contextmenu
{style}
data-row={rowIdx}
>
{#if error}
<div class="label">
@ -70,6 +72,9 @@
width: 0;
--cell-color: transparent;
}
.cell.alt {
--cell-background: var(--cell-background-alt);
}
.cell.default-height {
height: var(--default-row-height);
}
@ -94,14 +99,15 @@
}
/* Cell border for cells with labels */
.cell.error:after,
.cell.selected-other:not(.focused):after {
.cell.error:after {
border-radius: 0 2px 2px 2px;
}
.cell[data-row="0"].error:after,
.cell[data-row="0"].selected-other:not(.focused):after {
.cell.top.error:after {
border-radius: 2px 2px 2px 0;
}
.cell.selected-other:not(.focused):after {
border-radius: 2px;
}
/* Cell z-index */
.cell.error,
@ -111,14 +117,8 @@
.cell.focused {
z-index: 2;
}
.cell.focused {
--cell-color: var(--spectrum-global-color-blue-400);
}
.cell.error {
--cell-color: var(--spectrum-global-color-red-500);
}
.cell.readonly {
--cell-color: var(--spectrum-global-color-gray-600);
.cell.selected-other:hover {
z-index: 2;
}
.cell:not(.focused) {
user-select: none;
@ -126,6 +126,21 @@
.cell:hover {
cursor: default;
}
/* Cell color overrides */
.cell.selected-other {
--cell-color: var(--user-color);
}
.cell.focused {
--cell-color: var(--spectrum-global-color-blue-400);
}
.cell.error {
--cell-color: var(--spectrum-global-color-red-500);
}
.cell.focused.readonly {
--cell-color: var(--spectrum-global-color-gray-600);
}
.cell.highlighted:not(.focused),
.cell.focused.readonly {
--cell-background: var(--cell-background-hover);
@ -141,7 +156,7 @@
left: 0;
padding: 1px 4px 3px 4px;
margin: 0 0 -2px 0;
background: var(--user-color);
background: var(--cell-color);
border-radius: 2px;
display: block;
color: white;
@ -152,14 +167,19 @@
overflow: hidden;
user-select: none;
}
.cell[data-row="0"] .label {
.cell.top .label {
bottom: auto;
top: 100%;
border-radius: 0 2px 2px 2px;
padding: 2px 4px 2px 4px;
margin: -2px 0 0 0;
}
.error .label {
background: var(--spectrum-global-color-red-500);
}
.selected-other:not(.error) .label {
display: none;
}
.selected-other:not(.error):hover .label {
display: block;
}
</style>

View File

@ -21,16 +21,7 @@
svelteDispatch("select")
const id = row?._id
if (id) {
selectedRows.update(state => {
let newState = {
...state,
[id]: !state[id],
}
if (!newState[id]) {
delete newState[id]
}
return newState
})
selectedRows.actions.toggleRow(id)
}
}
@ -47,6 +38,7 @@
highlighted={rowFocused || rowHovered}
selected={rowSelected}
{defaultHeight}
rowIdx={row?.__idx}
>
<div class="gutter">
{#if $$slots.default}

View File

@ -196,7 +196,11 @@
<MenuItem disabled={!canMoveRight} icon="ChevronRight" on:click={moveRight}>
Move right
</MenuItem>
<MenuItem icon="VisibilityOff" on:click={hideColumn}>Hide column</MenuItem>
<MenuItem
disabled={idx === "sticky"}
icon="VisibilityOff"
on:click={hideColumn}>Hide column</MenuItem
>
</Menu>
</Popover>

View File

@ -52,7 +52,7 @@
{:else}
<div class="text-cell" class:number={type === "number"}>
<div class="value">
{value || ""}
{value ?? ""}
</div>
</div>
{/if}

View File

@ -1,24 +0,0 @@
<script>
export let user
</script>
<div class="user" style="background:{user.color};" title={user.email}>
{user.email[0]}
</div>
<style>
div {
width: 24px;
height: 24px;
display: grid;
place-items: center;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
}
div:hover {
cursor: pointer;
}
</style>

View File

@ -1,5 +1,5 @@
<script>
import { setContext } from "svelte"
import { setContext, onMount } from "svelte"
import { writable } from "svelte/store"
import { fade } from "svelte/transition"
import { clickOutside, ProgressCircle } from "@budibase/bbui"
@ -24,6 +24,7 @@
import RowHeightButton from "../controls/RowHeightButton.svelte"
import ColumnWidthButton from "../controls/ColumnWidthButton.svelte"
import NewRow from "./NewRow.svelte"
import { createGridWebsocket } from "../lib/websocket"
import {
MaxCellRenderHeight,
MaxCellRenderWidthOverflow,
@ -33,6 +34,7 @@
export let API = null
export let tableId = null
export let tableType = null
export let schemaOverrides = null
export let allowAddRows = true
export let allowAddColumns = true
@ -40,6 +42,9 @@
export let allowExpandRows = true
export let allowEditRows = true
export let allowDeleteRows = true
export let stripeRows = false
export let collaboration = true
export let showAvatars = true
// Unique identifier for DOM nodes inside this instance
const rand = Math.random()
@ -54,6 +59,7 @@
allowExpandRows,
allowEditRows,
allowDeleteRows,
stripeRows,
})
// Build up context
@ -62,6 +68,7 @@
rand,
config,
tableId: tableIdStore,
tableType,
schemaOverrides: schemaOverridesStore,
}
context = { ...context, ...createEventManagers() }
@ -88,6 +95,7 @@
allowExpandRows,
allowEditRows,
allowDeleteRows,
stripeRows,
})
// Set context for children to consume
@ -97,7 +105,11 @@
export const getContext = () => context
// Initialise websocket for multi-user
// onMount(() => createWebsocket(context))
onMount(() => {
if (collaboration) {
return createGridWebsocket(context)
}
})
</script>
<div
@ -105,6 +117,7 @@
id="grid-{rand}"
class:is-resizing={$isResizing}
class:is-reordering={$isReordering}
class:stripe={$config.stripeRows}
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};"
>
<div class="controls">
@ -118,7 +131,9 @@
<RowHeightButton />
</div>
<div class="controls-right">
<UserAvatars />
{#if showAvatars}
<UserAvatars />
{/if}
</div>
</div>
{#if $loaded}
@ -167,6 +182,7 @@
/* Variables */
--cell-background: var(--spectrum-global-color-gray-50);
--cell-background-hover: var(--spectrum-global-color-gray-100);
--cell-background-alt: var(--cell-background);
--cell-padding: 8px;
--cell-spacing: 4px;
--cell-border: 1px solid var(--spectrum-global-color-gray-200);
@ -183,6 +199,9 @@
.grid.is-reordering :global(*) {
cursor: grabbing !important;
}
.grid.stripe {
--cell-background-alt: var(--spectrum-global-color-gray-75);
}
.grid-data-outer,
.grid-data-inner {

View File

@ -36,7 +36,11 @@
<div bind:this={body} class="grid-body">
<GridScrollWrapper scrollHorizontally scrollVertically wheelInteractive>
{#each $renderedRows as row, idx}
<GridRow {row} {idx} invertY={idx >= $rowVerticalInversionIndex} />
<GridRow
{row}
top={idx === 0}
invertY={idx >= $rowVerticalInversionIndex}
/>
{/each}
{#if $config.allowAddRows && $renderedColumns.length}
<div

View File

@ -3,7 +3,7 @@
import DataCell from "../cells/DataCell.svelte"
export let row
export let idx
export let top = false
export let invertY = false
const {
@ -41,7 +41,8 @@
invertX={columnIdx >= $columnHorizontalInversionIndex}
highlighted={rowHovered || rowFocused || reorderSource === column.name}
selected={rowSelected}
rowIdx={idx}
rowIdx={row.__idx}
topRow={top}
focused={$focusedCellId === cellId}
selectedUser={$selectedCellMap[cellId]}
width={column.width}

View File

@ -61,7 +61,7 @@
border-right: var(--cell-border);
border-bottom: var(--cell-border);
background: var(--spectrum-global-color-gray-100);
z-index: 20;
z-index: 1;
}
.add:hover {
background: var(--spectrum-global-color-gray-200);

View File

@ -38,7 +38,7 @@
padding: 2px 6px;
font-size: 12px;
font-weight: 600;
background-color: var(--spectrum-global-color-gray-200);
background-color: var(--spectrum-global-color-gray-300);
color: var(--spectrum-global-color-gray-700);
border-radius: 4px;
text-align: center;

View File

@ -167,7 +167,7 @@
focused={$focusedCellId === cellId}
width={$stickyColumn.width}
{updateValue}
rowIdx={0}
topRow={offset === 0}
{invertY}
>
{#if $stickyColumn?.schema?.autocolumn}
@ -193,7 +193,7 @@
row={newRow}
focused={$focusedCellId === cellId}
width={column.width}
rowIdx={0}
topRow={offset === 0}
invertX={columnIdx >= $columnHorizontalInversionIndex}
{invertY}
>
@ -219,7 +219,7 @@
<Button size="M" secondary newStyles on:click={clear}>
<div class="button-with-keys">
Cancel
<KeyboardShortcut overlay keybind="Esc" />
<KeyboardShortcut keybind="Esc" />
</div>
</Button>
</div>

View File

@ -82,7 +82,8 @@
{rowFocused}
selected={rowSelected}
highlighted={rowHovered || rowFocused}
rowIdx={idx}
rowIdx={row.__idx}
topRow={idx === 0}
focused={$focusedCellId === cellId}
selectedUser={$selectedCellMap[cellId]}
width={$stickyColumn.width}

View File

@ -1,13 +1,23 @@
<script>
import { getContext } from "svelte"
import Avatar from "./Avatar.svelte"
import UserAvatar from "../../UserAvatar.svelte"
const { users } = getContext("grid")
$: uniqueUsers = unique($users)
const unique = users => {
let uniqueUsers = {}
users?.forEach(user => {
uniqueUsers[user.email] = user
})
return Object.values(uniqueUsers)
}
</script>
<div class="users">
{#each $users as user}
<Avatar {user} />
{#each uniqueUsers as user}
<UserAvatar {user} />
{/each}
</div>
@ -15,6 +25,6 @@
.users {
display: flex;
flex-direction: row;
gap: 8px;
gap: 4px;
}
</style>

View File

@ -1,24 +1,9 @@
import { get } from "svelte/store"
import { io } from "socket.io-client"
import { createWebsocket } from "../../../utils"
export const createWebsocket = context => {
const { rows, tableId, users, userId, focusedCellId } = context
// Determine connection info
const tls = location.protocol === "https:"
const proto = tls ? "wss:" : "ws:"
const host = location.hostname
const port = location.port || (tls ? 443 : 80)
const socket = io(`${proto}//${host}:${port}`, {
path: "/socket/grid",
// Cap reconnection attempts to 3 (total of 15 seconds before giving up)
reconnectionAttempts: 3,
// Delay reconnection attempt by 5 seconds
reconnectionDelay: 5000,
reconnectionDelayMax: 5000,
// Timeout after 4 seconds so we never stack requests
timeout: 4000,
})
export const createGridWebsocket = context => {
const { rows, tableId, users, focusedCellId, table } = context
const socket = createWebsocket("/socket/grid")
const connectToTable = tableId => {
if (!socket.connected) {
@ -28,27 +13,42 @@ export const createWebsocket = context => {
socket.emit("select-table", tableId, response => {
// handle initial connection info
users.set(response.users)
userId.set(response.id)
})
}
// Event handlers
// Connection events
socket.on("connect", () => {
connectToTable(get(tableId))
})
socket.on("row-update", data => {
if (data.id) {
rows.actions.refreshRow(data.id)
}
socket.on("connect_error", err => {
console.log("Failed to connect to grid websocket:", err.message)
})
// User events
socket.on("user-update", user => {
users.actions.updateUser(user)
})
socket.on("user-disconnect", user => {
users.actions.removeUser(user)
})
socket.on("connect_error", err => {
console.log("Failed to connect to grid websocket:", err.message)
// Row events
socket.on("row-change", async data => {
if (data.id) {
rows.actions.replaceRow(data.id, data.row)
} else if (data.row.id) {
// Handle users table edge case
await rows.actions.refreshRow(data.row.id)
}
})
// Table events
socket.on("table-change", data => {
// Only update table if one exists. If the table was deleted then we don't
// want to know - let the builder navigate away
if (data.table) {
table.set(data.table)
}
})
// Change websocket connection when table changes

View File

@ -14,6 +14,7 @@
dispatch,
selectedRows,
config,
menu,
} = getContext("grid")
const ignoredOriginSelectors = [
@ -61,6 +62,7 @@
} else {
$focusedCellId = null
}
menu.actions.close()
return
} else if (e.key === "Tab") {
e.preventDefault()
@ -224,10 +226,7 @@
if (!id || id === NewRowID) {
return
}
selectedRows.update(state => {
state[id] = !state[id]
return state
})
selectedRows.actions.toggleRow(id)
}
onMount(() => {

View File

@ -46,7 +46,7 @@ export const createStores = () => {
}
export const deriveStores = context => {
const { table, columns, stickyColumn, API, dispatch } = context
const { table, columns, stickyColumn, API } = context
// Updates the tables primary display column
const changePrimaryDisplay = async column => {
@ -90,10 +90,6 @@ export const deriveStores = context => {
// Update local state
table.set(newTable)
// Broadcast event so that we can keep sync with external state
// (e.g. data section which maintains a list of table definitions)
dispatch("updatetable", newTable)
// Update server
await API.saveTable(newTable)
}
@ -116,10 +112,24 @@ export const initialise = context => {
const schema = derived(
[table, schemaOverrides],
([$table, $schemaOverrides]) => {
let newSchema = $table?.schema
if (!newSchema) {
if (!$table?.schema) {
return null
}
let newSchema = { ...$table?.schema }
// Edge case to temporarily allow deletion of duplicated user
// fields that were saved with the "disabled" flag set.
// By overriding the saved schema we ensure only overrides can
// set the disabled flag.
// TODO: remove in future
Object.keys(newSchema).forEach(field => {
newSchema[field] = {
...newSchema[field],
disabled: false,
}
})
// Apply schema overrides
Object.keys($schemaOverrides || {}).forEach(field => {
if (newSchema[field]) {
newSchema[field] = {
@ -160,7 +170,7 @@ export const initialise = context => {
fields
.map(field => ({
name: field,
label: $schema[field].name || field,
label: $schema[field].displayName || field,
schema: $schema[field],
width: $schema[field].width || DefaultColumnWidth,
visible: $schema[field].visible ?? true,

View File

@ -4,9 +4,10 @@ const reorderInitialState = {
sourceColumn: null,
targetColumn: null,
breakpoints: [],
initialMouseX: null,
scrollLeft: 0,
gridLeft: 0,
width: 0,
latestX: 0,
increment: 0,
}
export const createStores = () => {
@ -23,14 +24,24 @@ export const createStores = () => {
}
export const deriveStores = context => {
const { reorder, columns, visibleColumns, scroll, bounds, stickyColumn, ui } =
context
const {
reorder,
columns,
visibleColumns,
scroll,
bounds,
stickyColumn,
ui,
maxScrollLeft,
} = context
let autoScrollInterval
let isAutoScrolling
// Callback when dragging on a colum header and starting reordering
const startReordering = (column, e) => {
const $visibleColumns = get(visibleColumns)
const $bounds = get(bounds)
const $scroll = get(scroll)
const $stickyColumn = get(stickyColumn)
ui.actions.blur()
@ -51,9 +62,8 @@ export const deriveStores = context => {
sourceColumn: column,
targetColumn: null,
breakpoints,
initialMouseX: e.clientX,
scrollLeft: $scroll.left,
gridLeft: $bounds.left,
width: $bounds.width,
})
// Add listeners to handle mouse movement
@ -66,12 +76,44 @@ export const deriveStores = context => {
// Callback when moving the mouse when reordering columns
const onReorderMouseMove = e => {
// Immediately handle the current position
const x = e.clientX
reorder.update(state => ({
...state,
latestX: x,
}))
considerReorderPosition()
// Check if we need to start auto-scrolling
const $reorder = get(reorder)
const proximityCutoff = 140
const speedFactor = 8
const rightProximity = Math.max(0, $reorder.gridLeft + $reorder.width - x)
const leftProximity = Math.max(0, x - $reorder.gridLeft)
if (rightProximity < proximityCutoff) {
const weight = proximityCutoff - rightProximity
const increment = (weight / proximityCutoff) * speedFactor
reorder.update(state => ({ ...state, increment }))
startAutoScroll()
} else if (leftProximity < proximityCutoff) {
const weight = -1 * (proximityCutoff - leftProximity)
const increment = (weight / proximityCutoff) * speedFactor
reorder.update(state => ({ ...state, increment }))
startAutoScroll()
} else {
stopAutoScroll()
}
}
// Actual logic to consider the current position and determine the new order
const considerReorderPosition = () => {
const $reorder = get(reorder)
const $scroll = get(scroll)
// Compute the closest breakpoint to the current position
let targetColumn
let minDistance = Number.MAX_SAFE_INTEGER
const mouseX = e.clientX - $reorder.gridLeft + $reorder.scrollLeft
const mouseX = $reorder.latestX - $reorder.gridLeft + $scroll.left
$reorder.breakpoints.forEach(point => {
const distance = Math.abs(point.x - mouseX)
if (distance < minDistance) {
@ -79,7 +121,6 @@ export const deriveStores = context => {
targetColumn = point.column
}
})
if (targetColumn !== $reorder.targetColumn) {
reorder.update(state => ({
...state,
@ -88,8 +129,35 @@ export const deriveStores = context => {
}
}
// Commences auto-scrolling in a certain direction, triggered when the mouse
// approaches the edges of the grid
const startAutoScroll = () => {
if (isAutoScrolling) {
return
}
isAutoScrolling = true
autoScrollInterval = setInterval(() => {
const $maxLeft = get(maxScrollLeft)
const { increment } = get(reorder)
scroll.update(state => ({
...state,
left: Math.max(0, Math.min($maxLeft, state.left + increment)),
}))
considerReorderPosition()
}, 10)
}
// Stops auto scrolling
const stopAutoScroll = () => {
isAutoScrolling = false
clearInterval(autoScrollInterval)
}
// Callback when stopping reordering columns
const stopReordering = async () => {
// Ensure auto-scrolling is stopped
stopAutoScroll()
// Swap position of columns
let { sourceColumn, targetColumn } = get(reorder)
moveColumn(sourceColumn, targetColumn)

View File

@ -268,27 +268,25 @@ export const deriveStores = context => {
return res?.rows?.[0]
}
// Refreshes a specific row, handling updates, addition or deletion
const refreshRow = async id => {
// Fetch row from the server again
const newRow = await fetchRow(id)
// Replaces a row in state with the newly defined row, handling updates,
// addition and deletion
const replaceRow = (id, row) => {
// Get index of row to check if it exists
const $rows = get(rows)
const $rowLookupMap = get(rowLookupMap)
const index = $rowLookupMap[id]
// Process as either an update, addition or deletion
if (newRow) {
if (row) {
if (index != null) {
// An existing row was updated
rows.update(state => {
state[index] = { ...newRow }
state[index] = { ...row }
return state
})
} else {
// A new row was created
handleNewRows([newRow])
handleNewRows([row])
}
} else if (index != null) {
// A row was removed
@ -296,6 +294,12 @@ export const deriveStores = context => {
}
}
// Refreshes a specific row
const refreshRow = async id => {
const row = await fetchRow(id)
replaceRow(id, row)
}
// Refreshes all data
const refreshData = () => {
get(fetch)?.getInitialData()
@ -341,10 +345,15 @@ export const deriveStores = context => {
const saved = await API.saveRow({ ...row, ...get(rowChangeCache)[rowId] })
// Update state after a successful change
rows.update(state => {
state[index] = saved
return state.slice()
})
if (saved?._id) {
rows.update(state => {
state[index] = saved
return state.slice()
})
} else if (saved?.id) {
// Handle users table edge case
await refreshRow(saved.id)
}
rowChangeCache.update(state => {
delete state[rowId]
return state
@ -455,6 +464,7 @@ export const deriveStores = context => {
hasRow,
loadNextPage,
refreshRow,
replaceRow,
refreshData,
refreshTableDefinition,
},

View File

@ -25,14 +25,33 @@ export const createStores = () => {
null
)
// Toggles whether a certain row ID is selected or not
const toggleSelectedRow = id => {
selectedRows.update(state => {
let newState = {
...state,
[id]: !state[id],
}
if (!newState[id]) {
delete newState[id]
}
return newState
})
}
return {
focusedCellId,
focusedCellAPI,
focusedRowId,
previousFocusedRowId,
selectedRows,
hoveredRowId,
rowHeight,
selectedRows: {
...selectedRows,
actions: {
toggleRow: toggleSelectedRow,
},
},
}
}

View File

@ -1,95 +1,50 @@
import { writable, get, derived } from "svelte/store"
import { helpers } from "@budibase/shared-core"
export const createStores = () => {
const users = writable([])
const userId = writable(null)
// Enrich users with unique colours
const enrichedUsers = derived(
[users, userId],
([$users, $userId]) => {
return (
$users
.slice()
// Place current user first
.sort((a, b) => {
if (a.id === $userId) {
return -1
} else if (b.id === $userId) {
return 1
} else {
return 0
}
})
// Enrich users with colors
.map((user, idx) => {
// Generate random colour hue
let hue = 1
for (let i = 0; i < user.email.length && i < 5; i++) {
hue *= user.email.charCodeAt(i + 1)
hue /= 17
}
hue = hue % 360
const color =
idx === 0
? "var(--spectrum-global-color-blue-400)"
: `hsl(${hue}, 50%, 40%)`
// Generate friendly label
let label = user.email
if (user.firstName) {
label = user.firstName
if (user.lastName) {
label += ` ${user.lastName}`
}
}
return {
...user,
color,
label,
}
})
)
},
[]
)
const enrichedUsers = derived(users, $users => {
return $users.map(user => ({
...user,
color: helpers.getUserColor(user),
label: helpers.getUserLabel(user),
}))
})
return {
users: {
...users,
subscribe: enrichedUsers.subscribe,
},
userId,
}
}
export const deriveStores = context => {
const { users, userId } = context
const { users, focusedCellId } = context
// Generate a lookup map of cell ID to the user that has it selected, to make
// lookups inside cells extremely fast
const selectedCellMap = derived(
[users, userId],
([$enrichedUsers, $userId]) => {
[users, focusedCellId],
([$users, $focusedCellId]) => {
let map = {}
$enrichedUsers.forEach(user => {
if (user.focusedCellId && user.id !== $userId) {
$users.forEach(user => {
if (user.focusedCellId && user.focusedCellId !== $focusedCellId) {
map[user.focusedCellId] = user
}
})
return map
},
{}
}
)
const updateUser = user => {
const $users = get(users)
const index = $users.findIndex(x => x.id === user.id)
if (index === -1) {
if (!$users.some(x => x.sessionId === user.sessionId)) {
users.set([...$users, user])
} else {
users.update(state => {
const index = state.findIndex(x => x.sessionId === user.sessionId)
state[index] = user
return state.slice()
})
@ -98,7 +53,7 @@ export const deriveStores = context => {
const removeUser = user => {
users.update(state => {
return state.filter(x => x.id !== user.id)
return state.filter(x => x.sessionId !== user.sessionId)
})
}

View File

@ -1,4 +1,5 @@
export { default as SplitPage } from "./SplitPage.svelte"
export { default as TestimonialPage } from "./TestimonialPage.svelte"
export { default as Testimonial } from "./Testimonial.svelte"
export { default as UserAvatar } from "./UserAvatar.svelte"
export { Grid } from "./grid"

View File

@ -70,6 +70,7 @@ export const Features = {
ENFORCEABLE_SSO: "enforceableSSO",
BRANDING: "branding",
SCIM: "scim",
SYNC_AUTOMATIONS: "syncAutomations",
}
// Role IDs

View File

@ -3,3 +3,4 @@ export * as JSONUtils from "./json"
export * as CookieUtils from "./cookies"
export * as RoleUtils from "./roles"
export * as Utils from "./utils"
export { createWebsocket } from "./websocket"

View File

@ -0,0 +1,23 @@
import { io } from "socket.io-client"
export const createWebsocket = path => {
if (!path) {
throw "A websocket path must be provided"
}
// Determine connection info
const tls = location.protocol === "https:"
const proto = tls ? "wss:" : "ws:"
const host = location.hostname
const port = location.port || (tls ? 443 : 80)
return io(`${proto}//${host}:${port}`, {
path,
// Cap reconnection attempts to 3 (total of 15 seconds before giving up)
reconnectionAttempts: 3,
// Delay reconnection attempt by 5 seconds
reconnectionDelay: 5000,
reconnectionDelayMax: 5000,
// Timeout after 4 seconds so we never stack requests
timeout: 4000,
})
}

@ -1 +1 @@
Subproject commit a590dc237a16983b8f39dc8e65005b7736d23467
Subproject commit 2adc101c1ede13f861f282d702f45b94ab91fd41

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/sdk",
"version": "0.0.1",
"version": "0.0.0",
"description": "Budibase Public API SDK",
"author": "Budibase",
"license": "MPL-2.0",

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