Merge remote-tracking branch 'origin/master' into feature/screen-deselect
This commit is contained in:
commit
ff5c7ceda8
371 changed files with 4445 additions and 5436 deletions
|
@ -12,4 +12,5 @@ packages/sdk/sdk
|
||||||
packages/account-portal/packages/server/build
|
packages/account-portal/packages/server/build
|
||||||
packages/account-portal/packages/ui/.routify
|
packages/account-portal/packages/ui/.routify
|
||||||
packages/account-portal/packages/ui/build
|
packages/account-portal/packages/ui/build
|
||||||
**/*.ivm.bundle.js
|
**/*.ivm.bundle.js
|
||||||
|
packages/server/build/oldClientVersions/**/**
|
||||||
|
|
|
@ -34,18 +34,43 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": ["**/*.ts"],
|
"files": ["**/*.ts"],
|
||||||
|
"excludedFiles": ["qa-core/**"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
"extends": ["eslint:recommended"],
|
"extends": ["eslint:recommended"],
|
||||||
|
"globals": {
|
||||||
|
"NodeJS": true
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"no-inner-declarations": "off",
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
"no-case-declarations": "off",
|
"local-rules/no-budibase-imports": "error"
|
||||||
"no-useless-escape": "off",
|
}
|
||||||
"no-undef": "off",
|
},
|
||||||
"no-prototype-builtins": "off",
|
{
|
||||||
"local-rules/no-budibase-imports": "error",
|
"files": ["**/*.spec.ts"],
|
||||||
|
"excludedFiles": ["qa-core/**"],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": ["jest", "@typescript-eslint"],
|
||||||
|
"extends": ["eslint:recommended", "plugin:jest/recommended"],
|
||||||
|
"env": {
|
||||||
|
"jest/globals": true
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"NodeJS": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
"local-rules/no-test-com": "error",
|
"local-rules/no-test-com": "error",
|
||||||
"local-rules/email-domain-example-com": "error"
|
"local-rules/email-domain-example-com": "error",
|
||||||
|
"no-console": "warn",
|
||||||
|
// We have a lot of tests that don't have assertions, they use our test
|
||||||
|
// API client that does the assertions for them
|
||||||
|
"jest/expect-expect": "off",
|
||||||
|
// We do this in some tests where the behaviour of internal tables
|
||||||
|
// differs to external, but the API is broadly the same
|
||||||
|
"jest/no-conditional-expect": "off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
66
.github/workflows/budibase_ci.yml
vendored
66
.github/workflows/budibase_ci.yml
vendored
|
@ -66,7 +66,8 @@ jobs:
|
||||||
# Run build all the projects
|
# Run build all the projects
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
yarn build
|
yarn build:oss
|
||||||
|
yarn build:account-portal
|
||||||
# Check the types of the projects built via esbuild
|
# Check the types of the projects built via esbuild
|
||||||
- name: Check types
|
- name: Check types
|
||||||
run: |
|
run: |
|
||||||
|
@ -138,6 +139,8 @@ jobs:
|
||||||
|
|
||||||
test-server:
|
test-server:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
DEBUG: testcontainers,testcontainers:exec,testcontainers:build,testcontainers:pull
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
@ -151,7 +154,19 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
|
||||||
|
- name: Pull testcontainers images
|
||||||
|
run: |
|
||||||
|
docker pull mcr.microsoft.com/mssql/server:2022-latest
|
||||||
|
docker pull mysql:8.3
|
||||||
|
docker pull postgres:16.1-bullseye
|
||||||
|
docker pull mongo:7.0-jammy
|
||||||
|
docker pull mariadb:lts
|
||||||
|
docker pull testcontainers/ryuk:0.5.1
|
||||||
|
docker pull budibase/couchdb
|
||||||
|
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
|
|
||||||
- name: Test server
|
- name: Test server
|
||||||
run: |
|
run: |
|
||||||
if ${{ env.USE_NX_AFFECTED }}; then
|
if ${{ env.USE_NX_AFFECTED }}; then
|
||||||
|
@ -217,27 +232,34 @@ jobs:
|
||||||
echo "pro_commit=$pro_commit"
|
echo "pro_commit=$pro_commit"
|
||||||
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
|
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
|
||||||
echo "base_commit=$base_commit"
|
echo "base_commit=$base_commit"
|
||||||
echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT"
|
|
||||||
|
base_commit_excluding_merges=$(git log --no-merges -n 1 --format=format:%H $base_commit)
|
||||||
|
echo "base_commit_excluding_merges=$base_commit_excluding_merges"
|
||||||
|
echo "base_commit_excluding_merges=$base_commit_excluding_merges" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "Nothing to do - branch to branch merge."
|
echo "Nothing to do - branch to branch merge."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Check submodule merged to base branch
|
- name: Check submodule merged and latest on base branch
|
||||||
if: ${{ steps.get_pro_commits.outputs.base_commit != '' }}
|
if: ${{ steps.get_pro_commits.outputs.base_commit_excluding_merges != '' }}
|
||||||
uses: actions/github-script@v7
|
run: |
|
||||||
with:
|
cd packages/pro
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
base_commit_excluding_merges='${{ steps.get_pro_commits.outputs.base_commit_excluding_merges }}'
|
||||||
script: |
|
pro_commit='${{ steps.get_pro_commits.outputs.pro_commit }}'
|
||||||
const submoduleCommit = '${{ steps.get_pro_commits.outputs.pro_commit }}';
|
|
||||||
const baseCommit = '${{ steps.get_pro_commits.outputs.base_commit }}';
|
|
||||||
|
|
||||||
if (submoduleCommit !== baseCommit) {
|
any_commit=$(git log --no-merges $base_commit_excluding_merges...$pro_commit)
|
||||||
console.error('Submodule commit does not match the latest commit on the "${{ steps.get_pro_commits.outputs.target_branch }}" branch.');
|
|
||||||
console.error('Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/master/docs/getting_started.md')
|
if [ -n "$any_commit" ]; then
|
||||||
process.exit(1);
|
echo $any_commit
|
||||||
} else {
|
|
||||||
console.log('All good, the submodule had been merged and setup correctly!')
|
echo "An error occurred: <error_message>"
|
||||||
}
|
echo 'Submodule commit does not match the latest commit on the "${{ steps.get_pro_commits.outputs.target_branch }}" branch.'
|
||||||
|
echo 'Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/master/docs/getting_started.md'
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo 'All good, the submodule had been merged and setup correctly!'
|
||||||
|
fi
|
||||||
|
|
||||||
check-accountportal-submodule:
|
check-accountportal-submodule:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -250,7 +272,15 @@ jobs:
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Check account portal commit
|
- uses: dorny/paths-filter@v3
|
||||||
|
id: changes
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
src:
|
||||||
|
- packages/account-portal/**
|
||||||
|
|
||||||
|
- if: steps.changes.outputs.src == 'true'
|
||||||
|
name: Check account portal commit
|
||||||
id: get_accountportal_commits
|
id: get_accountportal_commits
|
||||||
run: |
|
run: |
|
||||||
cd packages/account-portal
|
cd packages/account-portal
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -5,6 +5,9 @@ packages/server/runtime_apps/
|
||||||
bb-airgapped.tar.gz
|
bb-airgapped.tar.gz
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
|
packages/server/build/oldClientVersions/**/*
|
||||||
|
packages/builder/src/components/deploy/clientVersions.json
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
@ -107,3 +110,4 @@ budibase-component
|
||||||
budibase-datasource
|
budibase-datasource
|
||||||
|
|
||||||
*.iml
|
*.iml
|
||||||
|
.nx
|
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
{
|
{
|
||||||
// Use IntelliSense to learn about possible attributes.
|
// Use IntelliSense to learn about possible attributes.
|
||||||
// Hover to view descriptions of existing attributes.
|
// Hover to view descriptions of existing attributes.
|
||||||
|
@ -20,6 +19,13 @@
|
||||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
|
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
|
||||||
"args": ["${workspaceFolder}/packages/worker/src/index.ts"],
|
"args": ["${workspaceFolder}/packages/worker/src/index.ts"],
|
||||||
"cwd": "${workspaceFolder}/packages/worker"
|
"cwd": "${workspaceFolder}/packages/worker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Chrome against localhost",
|
||||||
|
"url": "http://localhost:10000",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"compounds": [
|
"compounds": [
|
||||||
|
|
|
@ -140,7 +140,7 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml
|
||||||
| ingress.className | string | `""` | What ingress class to use. |
|
| ingress.className | string | `""` | What ingress class to use. |
|
||||||
| ingress.enabled | bool | `true` | Whether to create an Ingress resource pointing to the Budibase proxy. |
|
| ingress.enabled | bool | `true` | Whether to create an Ingress resource pointing to the Budibase proxy. |
|
||||||
| ingress.hosts | list | `[]` | Standard hosts block for the Ingress resource. Defaults to pointing to the Budibase proxy. |
|
| ingress.hosts | list | `[]` | Standard hosts block for the Ingress resource. Defaults to pointing to the Budibase proxy. |
|
||||||
| nameOverride | string | `""` | Override the name of the deploymen. Defaults to {{ .Chart.Name }}. |
|
| nameOverride | string | `""` | Override the name of the deployment. Defaults to {{ .Chart.Name }}. |
|
||||||
| service.port | int | `10000` | Port to expose on the service. |
|
| service.port | int | `10000` | Port to expose on the service. |
|
||||||
| service.type | string | `"ClusterIP"` | Service type for the service that points to the main Budibase proxy pod. |
|
| service.type | string | `"ClusterIP"` | Service type for the service that points to the main Budibase proxy pod. |
|
||||||
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
|
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# -- Passed to all pods created by this chart. Should not ordinarily need to be changed.
|
# -- Passed to all pods created by this chart. Should not ordinarily need to be changed.
|
||||||
imagePullSecrets: []
|
imagePullSecrets: []
|
||||||
# -- Override the name of the deploymen. Defaults to {{ .Chart.Name }}.
|
# -- Override the name of the deployment. Defaults to {{ .Chart.Name }}.
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
|
|
||||||
serviceAccount:
|
serviceAccount:
|
||||||
|
|
|
@ -25,11 +25,9 @@ module.exports = {
|
||||||
docs: {
|
docs: {
|
||||||
description:
|
description:
|
||||||
"disallow the use of 'test.com' in strings and replace it with 'example.com'",
|
"disallow the use of 'test.com' in strings and replace it with 'example.com'",
|
||||||
category: "Possible Errors",
|
|
||||||
recommended: false,
|
|
||||||
},
|
},
|
||||||
schema: [], // no options
|
schema: [],
|
||||||
fixable: "code", // Indicates that this rule supports automatic fixing
|
fixable: "code",
|
||||||
},
|
},
|
||||||
create: function (context) {
|
create: function (context) {
|
||||||
return {
|
return {
|
||||||
|
@ -58,8 +56,6 @@ module.exports = {
|
||||||
docs: {
|
docs: {
|
||||||
description:
|
description:
|
||||||
"enforce using the example.com domain for generator.email calls",
|
"enforce using the example.com domain for generator.email calls",
|
||||||
category: "Possible Errors",
|
|
||||||
recommended: false,
|
|
||||||
},
|
},
|
||||||
fixable: "code",
|
fixable: "code",
|
||||||
schema: [],
|
schema: [],
|
||||||
|
|
25
globalSetup.ts
Normal file
25
globalSetup.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { GenericContainer, Wait } from "testcontainers"
|
||||||
|
|
||||||
|
export default async function setup() {
|
||||||
|
await new GenericContainer("budibase/couchdb")
|
||||||
|
.withExposedPorts(5984)
|
||||||
|
.withEnvironment({
|
||||||
|
COUCHDB_PASSWORD: "budibase",
|
||||||
|
COUCHDB_USER: "budibase",
|
||||||
|
})
|
||||||
|
.withCopyContentToContainer([
|
||||||
|
{
|
||||||
|
content: `
|
||||||
|
[log]
|
||||||
|
level = warn
|
||||||
|
`,
|
||||||
|
target: "/opt/couchdb/etc/local.d/test-couchdb.ini",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.withWaitStrategy(
|
||||||
|
Wait.forSuccessfulCommand(
|
||||||
|
"curl http://budibase:budibase@localhost:5984/_up"
|
||||||
|
).withStartupTimeout(20000)
|
||||||
|
)
|
||||||
|
.start()
|
||||||
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
module.exports = () => {
|
|
||||||
return {
|
|
||||||
couchdb: {
|
|
||||||
image: "budibase/couchdb",
|
|
||||||
ports: [5984],
|
|
||||||
env: {
|
|
||||||
COUCHDB_PASSWORD: "budibase",
|
|
||||||
COUCHDB_USER: "budibase",
|
|
||||||
},
|
|
||||||
wait: {
|
|
||||||
type: "ports",
|
|
||||||
timeout: 20000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.22.1",
|
"version": "2.22.13",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"esbuild-node-externals": "^1.8.0",
|
"esbuild-node-externals": "^1.8.0",
|
||||||
"eslint": "^8.52.0",
|
"eslint": "^8.52.0",
|
||||||
"eslint-plugin-import": "^2.29.0",
|
"eslint-plugin-import": "^2.29.0",
|
||||||
|
"eslint-plugin-jest": "^27.9.0",
|
||||||
"eslint-plugin-local-rules": "^2.0.0",
|
"eslint-plugin-local-rules": "^2.0.0",
|
||||||
"eslint-plugin-svelte": "^2.34.0",
|
"eslint-plugin-svelte": "^2.34.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
|
@ -25,12 +26,16 @@
|
||||||
"svelte": "^4.2.10",
|
"svelte": "^4.2.10",
|
||||||
"svelte-eslint-parser": "^0.33.1",
|
"svelte-eslint-parser": "^0.33.1",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
|
"typescript-eslint": "^7.3.1",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "node scripts/syncProPackage.js",
|
"preinstall": "node scripts/syncProPackage.js",
|
||||||
|
"get-past-client-version": "node scripts/getPastClientVersion.js",
|
||||||
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
|
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
|
||||||
"build": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream",
|
"build": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream",
|
||||||
|
"build:oss": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --ignore @budibase/account-portal --ignore @budibase/account-portal-server --ignore @budibase/account-portal-ui",
|
||||||
|
"build:account-portal": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --scope @budibase/account-portal --scope @budibase/account-portal-server --scope @budibase/account-portal-ui",
|
||||||
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
||||||
"check:types": "lerna run check:types",
|
"check:types": "lerna run check:types",
|
||||||
"build:sdk": "lerna run --stream build:sdk",
|
"build:sdk": "lerna run --stream build:sdk",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 23a1219732bd778654c0bcc4f49910c511e2d51f
|
Subproject commit 63ce32bca871f0a752323f5f7ebb5ec16bbbacc3
|
|
@ -1,8 +0,0 @@
|
||||||
const { join } = require("path")
|
|
||||||
require("dotenv").config({
|
|
||||||
path: join(__dirname, "..", "..", "hosting", ".env"),
|
|
||||||
})
|
|
||||||
|
|
||||||
const jestTestcontainersConfigGenerator = require("../../jestTestcontainersConfigGenerator")
|
|
||||||
|
|
||||||
module.exports = jestTestcontainersConfigGenerator()
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Config } from "@jest/types"
|
import { Config } from "@jest/types"
|
||||||
|
|
||||||
const baseConfig: Config.InitialProjectOptions = {
|
const baseConfig: Config.InitialProjectOptions = {
|
||||||
preset: "@trendyol/jest-testcontainers",
|
|
||||||
setupFiles: ["./tests/jestEnv.ts"],
|
setupFiles: ["./tests/jestEnv.ts"],
|
||||||
|
globalSetup: "./../../globalSetup.ts",
|
||||||
setupFilesAfterEnv: ["./tests/jestSetup.ts"],
|
setupFilesAfterEnv: ["./tests/jestSetup.ts"],
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.ts?$": "@swc/jest",
|
"^.+\\.ts?$": "@swc/jest",
|
||||||
|
|
|
@ -60,7 +60,6 @@
|
||||||
"@shopify/jest-koa-mocks": "5.1.1",
|
"@shopify/jest-koa-mocks": "5.1.1",
|
||||||
"@swc/core": "1.3.71",
|
"@swc/core": "1.3.71",
|
||||||
"@swc/jest": "0.2.27",
|
"@swc/jest": "0.2.27",
|
||||||
"@trendyol/jest-testcontainers": "^2.1.1",
|
|
||||||
"@types/chance": "1.1.3",
|
"@types/chance": "1.1.3",
|
||||||
"@types/cookies": "0.7.8",
|
"@types/cookies": "0.7.8",
|
||||||
"@types/jest": "29.5.5",
|
"@types/jest": "29.5.5",
|
||||||
|
|
|
@ -4,10 +4,10 @@ set -e
|
||||||
if [[ -n $CI ]]
|
if [[ -n $CI ]]
|
||||||
then
|
then
|
||||||
# --runInBand performs better in ci where resources are limited
|
# --runInBand performs better in ci where resources are limited
|
||||||
echo "jest --coverage --runInBand --forceExit"
|
echo "jest --coverage --runInBand --forceExit $@"
|
||||||
jest --coverage --runInBand --forceExit
|
jest --coverage --runInBand --forceExit $@
|
||||||
else
|
else
|
||||||
# --maxWorkers performs better in development
|
# --maxWorkers performs better in development
|
||||||
echo "jest --coverage --detectOpenHandles"
|
echo "jest --coverage --forceExit --detectOpenHandles $@"
|
||||||
jest --coverage --detectOpenHandles
|
jest --coverage --forceExit --detectOpenHandles $@
|
||||||
fi
|
fi
|
|
@ -133,7 +133,7 @@ export async function refreshOAuthToken(
|
||||||
configId?: string
|
configId?: string
|
||||||
): Promise<RefreshResponse> {
|
): Promise<RefreshResponse> {
|
||||||
switch (providerType) {
|
switch (providerType) {
|
||||||
case SSOProviderType.OIDC:
|
case SSOProviderType.OIDC: {
|
||||||
if (!configId) {
|
if (!configId) {
|
||||||
return { err: { data: "OIDC config id not provided" } }
|
return { err: { data: "OIDC config id not provided" } }
|
||||||
}
|
}
|
||||||
|
@ -142,12 +142,14 @@ export async function refreshOAuthToken(
|
||||||
return { err: { data: "OIDC configuration not found" } }
|
return { err: { data: "OIDC configuration not found" } }
|
||||||
}
|
}
|
||||||
return refreshOIDCAccessToken(oidcConfig, refreshToken)
|
return refreshOIDCAccessToken(oidcConfig, refreshToken)
|
||||||
case SSOProviderType.GOOGLE:
|
}
|
||||||
|
case SSOProviderType.GOOGLE: {
|
||||||
let googleConfig = await configs.getGoogleConfig()
|
let googleConfig = await configs.getGoogleConfig()
|
||||||
if (!googleConfig) {
|
if (!googleConfig) {
|
||||||
return { err: { data: "Google configuration not found" } }
|
return { err: { data: "Google configuration not found" } }
|
||||||
}
|
}
|
||||||
return refreshGoogleAccessToken(googleConfig, refreshToken)
|
return refreshGoogleAccessToken(googleConfig, refreshToken)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ describe("platformLogout", () => {
|
||||||
await testEnv.withTenant(async () => {
|
await testEnv.withTenant(async () => {
|
||||||
const ctx = structures.koa.newContext()
|
const ctx = structures.koa.newContext()
|
||||||
await auth.platformLogout({ ctx, userId: "test" })
|
await auth.platformLogout({ ctx, userId: "test" })
|
||||||
expect(events.auth.logout).toBeCalledTimes(1)
|
expect(events.auth.logout).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -129,7 +129,7 @@ export default class BaseCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async bustCache(key: string, opts = { client: null }) {
|
async bustCache(key: string) {
|
||||||
const client = await this.getClient()
|
const client = await this.getClient()
|
||||||
try {
|
try {
|
||||||
await client.delete(generateTenantKey(key))
|
await client.delete(generateTenantKey(key))
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { AnyDocument, Database } from "@budibase/types"
|
import { AnyDocument, Database } from "@budibase/types"
|
||||||
|
|
||||||
import { JobQueue, createQueue } from "../queue"
|
import { JobQueue, Queue, createQueue } from "../queue"
|
||||||
import * as dbUtils from "../db"
|
import * as dbUtils from "../db"
|
||||||
|
|
||||||
interface ProcessDocMessage {
|
interface ProcessDocMessage {
|
||||||
|
@ -12,18 +12,26 @@ interface ProcessDocMessage {
|
||||||
const PERSIST_MAX_ATTEMPTS = 100
|
const PERSIST_MAX_ATTEMPTS = 100
|
||||||
let processor: DocWritethroughProcessor | undefined
|
let processor: DocWritethroughProcessor | undefined
|
||||||
|
|
||||||
export const docWritethroughProcessorQueue = createQueue<ProcessDocMessage>(
|
export class DocWritethroughProcessor {
|
||||||
JobQueue.DOC_WRITETHROUGH_QUEUE,
|
private static _queue: Queue
|
||||||
{
|
|
||||||
jobOptions: {
|
public static get queue() {
|
||||||
attempts: PERSIST_MAX_ATTEMPTS,
|
if (!DocWritethroughProcessor._queue) {
|
||||||
},
|
DocWritethroughProcessor._queue = createQueue<ProcessDocMessage>(
|
||||||
}
|
JobQueue.DOC_WRITETHROUGH_QUEUE,
|
||||||
)
|
{
|
||||||
|
jobOptions: {
|
||||||
|
attempts: PERSIST_MAX_ATTEMPTS,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DocWritethroughProcessor._queue
|
||||||
|
}
|
||||||
|
|
||||||
class DocWritethroughProcessor {
|
|
||||||
init() {
|
init() {
|
||||||
docWritethroughProcessorQueue.process(async message => {
|
DocWritethroughProcessor.queue.process(async message => {
|
||||||
try {
|
try {
|
||||||
await this.persistToDb(message.data)
|
await this.persistToDb(message.data)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
@ -76,7 +84,7 @@ export class DocWritethrough {
|
||||||
}
|
}
|
||||||
|
|
||||||
async patch(data: Record<string, any>) {
|
async patch(data: Record<string, any>) {
|
||||||
await docWritethroughProcessorQueue.add({
|
await DocWritethroughProcessor.queue.add({
|
||||||
dbName: this.db.name,
|
dbName: this.db.name,
|
||||||
docId: this.docId,
|
docId: this.docId,
|
||||||
data,
|
data,
|
||||||
|
|
2
packages/backend-core/src/cache/invite.ts
vendored
2
packages/backend-core/src/cache/invite.ts
vendored
|
@ -1,5 +1,5 @@
|
||||||
import * as utils from "../utils"
|
import * as utils from "../utils"
|
||||||
import { Duration, DurationType } from "../utils"
|
import { Duration } from "../utils"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { getTenantId } from "../context"
|
import { getTenantId } from "../context"
|
||||||
import * as redis from "../redis/init"
|
import * as redis from "../redis/init"
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { getDB } from "../../db"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DocWritethrough,
|
DocWritethrough,
|
||||||
docWritethroughProcessorQueue,
|
DocWritethroughProcessor,
|
||||||
init,
|
init,
|
||||||
} from "../docWritethrough"
|
} from "../docWritethrough"
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ import InMemoryQueue from "../../queue/inMemoryQueue"
|
||||||
const initialTime = Date.now()
|
const initialTime = Date.now()
|
||||||
|
|
||||||
async function waitForQueueCompletion() {
|
async function waitForQueueCompletion() {
|
||||||
const queue: InMemoryQueue = docWritethroughProcessorQueue as never
|
const queue: InMemoryQueue = DocWritethroughProcessor.queue as never
|
||||||
await queue.waitForCompletion()
|
await queue.waitForCompletion()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,11 +235,11 @@ describe("docWritethrough", () => {
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
const queueMessageSpy = jest.spyOn(docWritethroughProcessorQueue, "add")
|
const queueMessageSpy = jest.spyOn(DocWritethroughProcessor.queue, "add")
|
||||||
|
|
||||||
await config.doInTenant(async () => {
|
await config.doInTenant(async () => {
|
||||||
let patches = await parallelPatch(5)
|
let patches = await parallelPatch(5)
|
||||||
expect(queueMessageSpy).toBeCalledTimes(5)
|
expect(queueMessageSpy).toHaveBeenCalledTimes(5)
|
||||||
|
|
||||||
await waitForQueueCompletion()
|
await waitForQueueCompletion()
|
||||||
expect(await db.get(documentId)).toEqual(
|
expect(await db.get(documentId)).toEqual(
|
||||||
|
@ -247,7 +247,7 @@ describe("docWritethrough", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
patches = { ...patches, ...(await parallelPatch(40)) }
|
patches = { ...patches, ...(await parallelPatch(40)) }
|
||||||
expect(queueMessageSpy).toBeCalledTimes(45)
|
expect(queueMessageSpy).toHaveBeenCalledTimes(45)
|
||||||
|
|
||||||
await waitForQueueCompletion()
|
await waitForQueueCompletion()
|
||||||
expect(await db.get(documentId)).toEqual(
|
expect(await db.get(documentId)).toEqual(
|
||||||
|
@ -255,7 +255,7 @@ describe("docWritethrough", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
patches = { ...patches, ...(await parallelPatch(10)) }
|
patches = { ...patches, ...(await parallelPatch(10)) }
|
||||||
expect(queueMessageSpy).toBeCalledTimes(55)
|
expect(queueMessageSpy).toHaveBeenCalledTimes(55)
|
||||||
|
|
||||||
await waitForQueueCompletion()
|
await waitForQueueCompletion()
|
||||||
expect(await db.get(documentId)).toEqual(
|
expect(await db.get(documentId)).toEqual(
|
||||||
|
@ -265,6 +265,7 @@ describe("docWritethrough", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// This is not yet supported
|
// This is not yet supported
|
||||||
|
// eslint-disable-next-line jest/no-disabled-tests
|
||||||
it.skip("patches will execute in order", async () => {
|
it.skip("patches will execute in order", async () => {
|
||||||
let incrementalValue = 0
|
let incrementalValue = 0
|
||||||
const keyToOverride = generator.word()
|
const keyToOverride = generator.word()
|
||||||
|
|
|
@ -55,8 +55,8 @@ describe("user cache", () => {
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(UserDB.bulkGet).toBeCalledTimes(1)
|
expect(UserDB.bulkGet).toHaveBeenCalledTimes(1)
|
||||||
expect(UserDB.bulkGet).toBeCalledWith(userIdsToRequest)
|
expect(UserDB.bulkGet).toHaveBeenCalledWith(userIdsToRequest)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("on a second all, all of them are retrieved from cache", async () => {
|
it("on a second all, all of them are retrieved from cache", async () => {
|
||||||
|
@ -82,7 +82,7 @@ describe("user cache", () => {
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(UserDB.bulkGet).toBeCalledTimes(1)
|
expect(UserDB.bulkGet).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("when some users are cached, only the missing ones are retrieved from db", async () => {
|
it("when some users are cached, only the missing ones are retrieved from db", async () => {
|
||||||
|
@ -110,8 +110,8 @@ describe("user cache", () => {
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(UserDB.bulkGet).toBeCalledTimes(1)
|
expect(UserDB.bulkGet).toHaveBeenCalledTimes(1)
|
||||||
expect(UserDB.bulkGet).toBeCalledWith([
|
expect(UserDB.bulkGet).toHaveBeenCalledWith([
|
||||||
userIdsToRequest[1],
|
userIdsToRequest[1],
|
||||||
userIdsToRequest[2],
|
userIdsToRequest[2],
|
||||||
userIdsToRequest[4],
|
userIdsToRequest[4],
|
||||||
|
|
|
@ -8,7 +8,7 @@ const DEFAULT_WRITE_RATE_MS = 10000
|
||||||
let CACHE: BaseCache | null = null
|
let CACHE: BaseCache | null = null
|
||||||
|
|
||||||
interface CacheItem<T extends Document> {
|
interface CacheItem<T extends Document> {
|
||||||
doc: any
|
doc: T
|
||||||
lastWrite: number
|
lastWrite: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -246,7 +246,7 @@ describe("context", () => {
|
||||||
context.doInAppMigrationContext(db.generateAppID(), async () => {
|
context.doInAppMigrationContext(db.generateAppID(), async () => {
|
||||||
await otherContextCall()
|
await otherContextCall()
|
||||||
})
|
})
|
||||||
).rejects.toThrowError(
|
).rejects.toThrow(
|
||||||
"The context cannot be changed, a migration is currently running"
|
"The context cannot be changed, a migration is currently running"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Replication {
|
||||||
return resolve(info)
|
return resolve(info)
|
||||||
})
|
})
|
||||||
.on("error", function (err) {
|
.on("error", function (err) {
|
||||||
throw new Error(`Replication Error: ${err}`)
|
throw err
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,6 @@ interface SearchResponse<T> {
|
||||||
totalRows: number
|
totalRows: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaginatedSearchResponse<T> extends SearchResponse<T> {
|
|
||||||
hasNextPage: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SearchParams<T> = {
|
export type SearchParams<T> = {
|
||||||
tableId?: string
|
tableId?: string
|
||||||
sort?: string
|
sort?: string
|
||||||
|
@ -247,7 +243,7 @@ export class QueryBuilder<T> {
|
||||||
}
|
}
|
||||||
// Escape characters
|
// Escape characters
|
||||||
if (!this.#noEscaping && escape && originalType === "string") {
|
if (!this.#noEscaping && escape && originalType === "string") {
|
||||||
value = `${value}`.replace(/[ \/#+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
|
value = `${value}`.replace(/[ /#+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap in quotes
|
// Wrap in quotes
|
||||||
|
|
|
@ -34,12 +34,12 @@ export async function createUserIndex() {
|
||||||
}
|
}
|
||||||
let idxKey = prev != null ? `${prev}.${key}` : key
|
let idxKey = prev != null ? `${prev}.${key}` : key
|
||||||
if (typeof input[key] === "string") {
|
if (typeof input[key] === "string") {
|
||||||
|
// @ts-expect-error index is available in a CouchDB map function
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
// @ts-ignore
|
|
||||||
index(idxKey, input[key].toLowerCase(), { facet: true })
|
index(idxKey, input[key].toLowerCase(), { facet: true })
|
||||||
} else if (typeof input[key] !== "object") {
|
} else if (typeof input[key] !== "object") {
|
||||||
|
// @ts-expect-error index is available in a CouchDB map function
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
// @ts-ignore
|
|
||||||
index(idxKey, input[key], { facet: true })
|
index(idxKey, input[key], { facet: true })
|
||||||
} else {
|
} else {
|
||||||
idx(input[key], idxKey)
|
idx(input[key], idxKey)
|
||||||
|
|
|
@ -17,13 +17,8 @@ export function init(processors: ProcessorMap) {
|
||||||
// if not processing in this instance, kick it off
|
// if not processing in this instance, kick it off
|
||||||
if (!processingPromise) {
|
if (!processingPromise) {
|
||||||
processingPromise = asyncEventQueue.process(async job => {
|
processingPromise = asyncEventQueue.process(async job => {
|
||||||
const { event, identity, properties, timestamp } = job.data
|
const { event, identity, properties } = job.data
|
||||||
await documentProcessor.processEvent(
|
await documentProcessor.processEvent(event, identity, properties)
|
||||||
event,
|
|
||||||
identity,
|
|
||||||
properties,
|
|
||||||
timestamp
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Event } from "@budibase/types"
|
import { Event, Identity } from "@budibase/types"
|
||||||
import { processors } from "./processors"
|
import { processors } from "./processors"
|
||||||
import identification from "./identification"
|
import identification from "./identification"
|
||||||
import * as backfill from "./backfill"
|
import * as backfill from "./backfill"
|
||||||
|
@ -7,12 +7,19 @@ import { publishAsyncEvent } from "./asyncEvents"
|
||||||
export const publishEvent = async (
|
export const publishEvent = async (
|
||||||
event: Event,
|
event: Event,
|
||||||
properties: any,
|
properties: any,
|
||||||
timestamp?: string | number
|
timestamp?: string | number,
|
||||||
|
identityOverride?: Identity
|
||||||
) => {
|
) => {
|
||||||
// in future this should use async events via a distributed queue.
|
// in future this should use async events via a distributed queue.
|
||||||
const identity = await identification.getCurrentIdentity()
|
const identity =
|
||||||
|
identityOverride || (await identification.getCurrentIdentity())
|
||||||
|
|
||||||
|
// Backfilling is get from the user cache, but when we override the identity cache is not available. Overrides are
|
||||||
|
// normally performed in automatic actions or operations in async flows (BPM) where the user session is not available.
|
||||||
|
const backfilling = identityOverride
|
||||||
|
? false
|
||||||
|
: await backfill.isBackfillingEvent(event)
|
||||||
|
|
||||||
const backfilling = await backfill.isBackfillingEvent(event)
|
|
||||||
// no backfill - send the event and exit
|
// no backfill - send the event and exit
|
||||||
if (!backfilling) {
|
if (!backfilling) {
|
||||||
// send off async events if required
|
// send off async events if required
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {
|
import {
|
||||||
Event,
|
Event,
|
||||||
Identity,
|
Identity,
|
||||||
Group,
|
|
||||||
IdentityType,
|
IdentityType,
|
||||||
AuditLogQueueEvent,
|
AuditLogQueueEvent,
|
||||||
AuditLogFn,
|
AuditLogFn,
|
||||||
|
@ -79,11 +78,11 @@ export default class AuditLogsProcessor implements EventProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async identify(identity: Identity, timestamp?: string | number) {
|
async identify() {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
async identifyGroup(group: Group, timestamp?: string | number) {
|
async identifyGroup() {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,7 @@ export default class LoggingProcessor implements EventProcessor {
|
||||||
async processEvent(
|
async processEvent(
|
||||||
event: Event,
|
event: Event,
|
||||||
identity: Identity,
|
identity: Identity,
|
||||||
properties: any,
|
properties: any
|
||||||
timestamp?: string
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (skipLogging) {
|
if (skipLogging) {
|
||||||
return
|
return
|
||||||
|
@ -17,14 +16,14 @@ export default class LoggingProcessor implements EventProcessor {
|
||||||
console.log(`[audit] [identityType=${identity.type}] ${event}`, properties)
|
console.log(`[audit] [identityType=${identity.type}] ${event}`, properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
async identify(identity: Identity, timestamp?: string | number) {
|
async identify(identity: Identity) {
|
||||||
if (skipLogging) {
|
if (skipLogging) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log(`[audit] identified`, identity)
|
console.log(`[audit] identified`, identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
async identifyGroup(group: Group, timestamp?: string | number) {
|
async identifyGroup(group: Group) {
|
||||||
if (skipLogging) {
|
if (skipLogging) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,7 @@ export default class DocumentUpdateProcessor implements EventProcessor {
|
||||||
this.processors = processors
|
this.processors = processors
|
||||||
}
|
}
|
||||||
|
|
||||||
async processEvent(
|
async processEvent(event: Event, identity: Identity, properties: any) {
|
||||||
event: Event,
|
|
||||||
identity: Identity,
|
|
||||||
properties: any,
|
|
||||||
timestamp?: string | number
|
|
||||||
) {
|
|
||||||
const tenantId = identity.realTenantId
|
const tenantId = identity.realTenantId
|
||||||
const docId = getDocumentId(event, properties)
|
const docId = getDocumentId(event, properties)
|
||||||
if (!tenantId || !docId) {
|
if (!tenantId || !docId) {
|
||||||
|
|
|
@ -5,13 +5,19 @@ import {
|
||||||
AccountCreatedEvent,
|
AccountCreatedEvent,
|
||||||
AccountDeletedEvent,
|
AccountDeletedEvent,
|
||||||
AccountVerifiedEvent,
|
AccountVerifiedEvent,
|
||||||
|
Identity,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
async function created(account: Account) {
|
async function created(account: Account, identityOverride?: Identity) {
|
||||||
const properties: AccountCreatedEvent = {
|
const properties: AccountCreatedEvent = {
|
||||||
tenantId: account.tenantId,
|
tenantId: account.tenantId,
|
||||||
}
|
}
|
||||||
await publishEvent(Event.ACCOUNT_CREATED, properties)
|
await publishEvent(
|
||||||
|
Event.ACCOUNT_CREATED,
|
||||||
|
properties,
|
||||||
|
undefined,
|
||||||
|
identityOverride
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleted(account: Account) {
|
async function deleted(account: Account) {
|
||||||
|
|
|
@ -10,6 +10,18 @@ import { formats } from "dd-trace/ext"
|
||||||
|
|
||||||
import { localFileDestination } from "../system"
|
import { localFileDestination } from "../system"
|
||||||
|
|
||||||
|
function isPlainObject(obj: any) {
|
||||||
|
return typeof obj === "object" && obj !== null && !(obj instanceof Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isError(obj: any) {
|
||||||
|
return obj instanceof Error
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMessage(obj: any) {
|
||||||
|
return typeof obj === "string"
|
||||||
|
}
|
||||||
|
|
||||||
// LOGGER
|
// LOGGER
|
||||||
|
|
||||||
let pinoInstance: pino.Logger | undefined
|
let pinoInstance: pino.Logger | undefined
|
||||||
|
@ -71,23 +83,11 @@ if (!env.DISABLE_PINO_LOGGER) {
|
||||||
err?: Error
|
err?: Error
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPlainObject(obj: any) {
|
|
||||||
return typeof obj === "object" && obj !== null && !(obj instanceof Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isError(obj: any) {
|
|
||||||
return obj instanceof Error
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMessage(obj: any) {
|
|
||||||
return typeof obj === "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backwards compatibility between console logging statements
|
* Backwards compatibility between console logging statements
|
||||||
* and pino logging requirements.
|
* and pino logging requirements.
|
||||||
*/
|
*/
|
||||||
function getLogParams(args: any[]): [MergingObject, string] {
|
const getLogParams = (args: any[]): [MergingObject, string] => {
|
||||||
let error = undefined
|
let error = undefined
|
||||||
let objects: any[] = []
|
let objects: any[] = []
|
||||||
let message = ""
|
let message = ""
|
||||||
|
|
|
@ -11,7 +11,6 @@ export const buildMatcherRegex = (
|
||||||
return patterns.map(pattern => {
|
return patterns.map(pattern => {
|
||||||
let route = pattern.route
|
let route = pattern.route
|
||||||
const method = pattern.method
|
const method = pattern.method
|
||||||
const strict = pattern.strict ? pattern.strict : false
|
|
||||||
|
|
||||||
// if there is a param in the route
|
// if there is a param in the route
|
||||||
// use a wildcard pattern
|
// use a wildcard pattern
|
||||||
|
@ -24,24 +23,17 @@ export const buildMatcherRegex = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { regex: new RegExp(route), method, strict, route }
|
return { regex: new RegExp(route), method, route }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const matches = (ctx: BBContext, options: RegexMatcher[]) => {
|
export const matches = (ctx: BBContext, options: RegexMatcher[]) => {
|
||||||
return options.find(({ regex, method, strict, route }) => {
|
return options.find(({ regex, method }) => {
|
||||||
let urlMatch
|
const urlMatch = regex.test(ctx.request.url)
|
||||||
if (strict) {
|
|
||||||
urlMatch = ctx.request.url === route
|
|
||||||
} else {
|
|
||||||
urlMatch = regex.test(ctx.request.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
const methodMatch =
|
const methodMatch =
|
||||||
method === "ALL"
|
method === "ALL"
|
||||||
? true
|
? true
|
||||||
: ctx.request.method.toLowerCase() === method.toLowerCase()
|
: ctx.request.method.toLowerCase() === method.toLowerCase()
|
||||||
|
|
||||||
return urlMatch && methodMatch
|
return urlMatch && methodMatch
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Cookie } from "../../../constants"
|
||||||
import * as configs from "../../../configs"
|
import * as configs from "../../../configs"
|
||||||
import * as cache from "../../../cache"
|
import * as cache from "../../../cache"
|
||||||
import * as utils from "../../../utils"
|
import * as utils from "../../../utils"
|
||||||
import { UserCtx, SSOProfile, DatasourceAuthCookie } from "@budibase/types"
|
import { UserCtx, SSOProfile } from "@budibase/types"
|
||||||
import { ssoSaveUserNoOp } from "../sso/sso"
|
import { ssoSaveUserNoOp } from "../sso/sso"
|
||||||
|
|
||||||
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||||
|
|
|
@ -5,7 +5,6 @@ import * as context from "../../../context"
|
||||||
import fetch from "node-fetch"
|
import fetch from "node-fetch"
|
||||||
import {
|
import {
|
||||||
SaveSSOUserFunction,
|
SaveSSOUserFunction,
|
||||||
SaveUserOpts,
|
|
||||||
SSOAuthDetails,
|
SSOAuthDetails,
|
||||||
SSOUser,
|
SSOUser,
|
||||||
User,
|
User,
|
||||||
|
@ -14,10 +13,8 @@ import {
|
||||||
// no-op function for user save
|
// no-op function for user save
|
||||||
// - this allows datasource auth and access token refresh to work correctly
|
// - this allows datasource auth and access token refresh to work correctly
|
||||||
// - prefer no-op over an optional argument to ensure function is provided to login flows
|
// - prefer no-op over an optional argument to ensure function is provided to login flows
|
||||||
export const ssoSaveUserNoOp: SaveSSOUserFunction = (
|
export const ssoSaveUserNoOp: SaveSSOUserFunction = (user: SSOUser) =>
|
||||||
user: SSOUser,
|
Promise.resolve(user)
|
||||||
opts: SaveUserOpts
|
|
||||||
) => Promise.resolve(user)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common authentication logic for third parties. e.g. OAuth, OIDC.
|
* Common authentication logic for third parties. e.g. OAuth, OIDC.
|
||||||
|
|
|
@ -114,11 +114,11 @@ describe("sso", () => {
|
||||||
// tenant id added
|
// tenant id added
|
||||||
ssoUser.tenantId = context.getTenantId()
|
ssoUser.tenantId = context.getTenantId()
|
||||||
|
|
||||||
expect(mockSaveUser).toBeCalledWith(ssoUser, {
|
expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, {
|
||||||
hashPassword: false,
|
hashPassword: false,
|
||||||
requirePassword: false,
|
requirePassword: false,
|
||||||
})
|
})
|
||||||
expect(mockDone).toBeCalledWith(null, ssoUser)
|
expect(mockDone).toHaveBeenCalledWith(null, ssoUser)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -159,11 +159,11 @@ describe("sso", () => {
|
||||||
// existing id preserved
|
// existing id preserved
|
||||||
ssoUser._id = existingUser._id
|
ssoUser._id = existingUser._id
|
||||||
|
|
||||||
expect(mockSaveUser).toBeCalledWith(ssoUser, {
|
expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, {
|
||||||
hashPassword: false,
|
hashPassword: false,
|
||||||
requirePassword: false,
|
requirePassword: false,
|
||||||
})
|
})
|
||||||
expect(mockDone).toBeCalledWith(null, ssoUser)
|
expect(mockDone).toHaveBeenCalledWith(null, ssoUser)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -187,11 +187,11 @@ describe("sso", () => {
|
||||||
// existing id preserved
|
// existing id preserved
|
||||||
ssoUser._id = existingUser._id
|
ssoUser._id = existingUser._id
|
||||||
|
|
||||||
expect(mockSaveUser).toBeCalledWith(ssoUser, {
|
expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, {
|
||||||
hashPassword: false,
|
hashPassword: false,
|
||||||
requirePassword: false,
|
requirePassword: false,
|
||||||
})
|
})
|
||||||
expect(mockDone).toBeCalledWith(null, ssoUser)
|
expect(mockDone).toHaveBeenCalledWith(null, ssoUser)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -24,13 +24,13 @@ function buildUserCtx(user: ContextUser) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function passed(throwFn: jest.Func, nextFn: jest.Func) {
|
function passed(throwFn: jest.Func, nextFn: jest.Func) {
|
||||||
expect(throwFn).not.toBeCalled()
|
expect(throwFn).not.toHaveBeenCalled()
|
||||||
expect(nextFn).toBeCalled()
|
expect(nextFn).toHaveBeenCalled()
|
||||||
}
|
}
|
||||||
|
|
||||||
function threw(throwFn: jest.Func) {
|
function threw(throwFn: jest.Func) {
|
||||||
// cant check next, the throw function doesn't actually throw - so it still continues
|
// cant check next, the throw function doesn't actually throw - so it still continues
|
||||||
expect(throwFn).toBeCalled()
|
expect(throwFn).toHaveBeenCalled()
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("adminOnly middleware", () => {
|
describe("adminOnly middleware", () => {
|
||||||
|
|
|
@ -34,23 +34,6 @@ describe("matchers", () => {
|
||||||
expect(!!matchers.matches(ctx, built)).toBe(true)
|
expect(!!matchers.matches(ctx, built)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("doesn't wildcard path with strict", () => {
|
|
||||||
const pattern = [
|
|
||||||
{
|
|
||||||
route: "/api/tests",
|
|
||||||
method: "POST",
|
|
||||||
strict: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const ctx = structures.koa.newContext()
|
|
||||||
ctx.request.url = "/api/tests/id/something/else"
|
|
||||||
ctx.request.method = "POST"
|
|
||||||
|
|
||||||
const built = matchers.buildMatcherRegex(pattern)
|
|
||||||
|
|
||||||
expect(!!matchers.matches(ctx, built)).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("matches with param", () => {
|
it("matches with param", () => {
|
||||||
const pattern = [
|
const pattern = [
|
||||||
{
|
{
|
||||||
|
@ -67,23 +50,6 @@ describe("matchers", () => {
|
||||||
expect(!!matchers.matches(ctx, built)).toBe(true)
|
expect(!!matchers.matches(ctx, built)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Support the below behaviour
|
|
||||||
// Strict does not work when a param is present
|
|
||||||
// it("matches with param with strict", () => {
|
|
||||||
// const pattern = [{
|
|
||||||
// route: "/api/tests/:testId",
|
|
||||||
// method: "GET",
|
|
||||||
// strict: true
|
|
||||||
// }]
|
|
||||||
// const ctx = structures.koa.newContext()
|
|
||||||
// ctx.request.url = "/api/tests/id"
|
|
||||||
// ctx.request.method = "GET"
|
|
||||||
//
|
|
||||||
// const built = matchers.buildMatcherRegex(pattern)
|
|
||||||
//
|
|
||||||
// expect(!!matchers.matches(ctx, built)).toBe(true)
|
|
||||||
// })
|
|
||||||
|
|
||||||
it("doesn't match by path", () => {
|
it("doesn't match by path", () => {
|
||||||
const pattern = [
|
const pattern = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -45,10 +45,6 @@ export const runMigration = async (
|
||||||
options: MigrationOptions = {}
|
options: MigrationOptions = {}
|
||||||
) => {
|
) => {
|
||||||
const migrationType = migration.type
|
const migrationType = migration.type
|
||||||
let tenantId: string | undefined
|
|
||||||
if (migrationType !== MigrationType.INSTALLATION) {
|
|
||||||
tenantId = context.getTenantId()
|
|
||||||
}
|
|
||||||
const migrationName = migration.name
|
const migrationName = migration.name
|
||||||
const silent = migration.silent
|
const silent = migration.silent
|
||||||
|
|
||||||
|
|
|
@ -126,7 +126,7 @@ describe("app", () => {
|
||||||
|
|
||||||
it("gets url with embedded minio", async () => {
|
it("gets url with embedded minio", async () => {
|
||||||
testEnv.withMinio()
|
testEnv.withMinio()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(() => {
|
||||||
const url = getAppFileUrl()
|
const url = getAppFileUrl()
|
||||||
expect(url).toBe(
|
expect(url).toBe(
|
||||||
"/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
"/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
||||||
|
@ -136,7 +136,7 @@ describe("app", () => {
|
||||||
|
|
||||||
it("gets url with custom S3", async () => {
|
it("gets url with custom S3", async () => {
|
||||||
testEnv.withS3()
|
testEnv.withS3()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(() => {
|
||||||
const url = getAppFileUrl()
|
const url = getAppFileUrl()
|
||||||
expect(url).toBe(
|
expect(url).toBe(
|
||||||
"http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
"http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
||||||
|
@ -146,7 +146,7 @@ describe("app", () => {
|
||||||
|
|
||||||
it("gets url with cloudfront + s3", async () => {
|
it("gets url with cloudfront + s3", async () => {
|
||||||
testEnv.withCloudfront()
|
testEnv.withCloudfront()
|
||||||
await testEnv.withTenant(tenantId => {
|
await testEnv.withTenant(() => {
|
||||||
const url = getAppFileUrl()
|
const url = getAppFileUrl()
|
||||||
// omit rest of signed params
|
// omit rest of signed params
|
||||||
expect(
|
expect(
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { DBTestConfiguration } from "../../../tests/extra"
|
||||||
import * as tenants from "../tenants"
|
import * as tenants from "../tenants"
|
||||||
|
|
||||||
describe("tenants", () => {
|
describe("tenants", () => {
|
||||||
const config = new DBTestConfiguration()
|
new DBTestConfiguration()
|
||||||
|
|
||||||
describe("addTenant", () => {
|
describe("addTenant", () => {
|
||||||
it("concurrently adds multiple tenants safely", async () => {
|
it("concurrently adds multiple tenants safely", async () => {
|
||||||
|
|
|
@ -20,7 +20,7 @@ export async function lookupTenantId(userId: string) {
|
||||||
return user.tenantId
|
return user.tenantId
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserDoc(emailOrId: string): Promise<PlatformUser> {
|
export async function getUserDoc(emailOrId: string): Promise<PlatformUser> {
|
||||||
const db = getPlatformDB()
|
const db = getPlatformDB()
|
||||||
return db.get(emailOrId)
|
return db.get(emailOrId)
|
||||||
}
|
}
|
||||||
|
@ -79,6 +79,17 @@ async function addUserDoc(emailOrId: string, newDocFn: () => PlatformUser) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function addSsoUser(
|
||||||
|
ssoId: string,
|
||||||
|
email: string,
|
||||||
|
userId: string,
|
||||||
|
tenantId: string
|
||||||
|
) {
|
||||||
|
return addUserDoc(ssoId, () =>
|
||||||
|
newUserSsoIdDoc(ssoId, email, userId, tenantId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export async function addUser(
|
export async function addUser(
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -91,9 +102,7 @@ export async function addUser(
|
||||||
]
|
]
|
||||||
|
|
||||||
if (ssoId) {
|
if (ssoId) {
|
||||||
promises.push(
|
promises.push(addSsoUser(ssoId, email, userId, tenantId))
|
||||||
addUserDoc(ssoId, () => newUserSsoIdDoc(ssoId, email, userId, tenantId))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
|
|
|
@ -39,7 +39,7 @@ class InMemoryQueue implements Partial<Queue> {
|
||||||
_opts?: QueueOptions
|
_opts?: QueueOptions
|
||||||
_messages: JobMessage[]
|
_messages: JobMessage[]
|
||||||
_queuedJobIds: Set<string>
|
_queuedJobIds: Set<string>
|
||||||
_emitter: EventEmitter
|
_emitter: NodeJS.EventEmitter
|
||||||
_runCount: number
|
_runCount: number
|
||||||
_addCount: number
|
_addCount: number
|
||||||
|
|
||||||
|
@ -166,7 +166,7 @@ class InMemoryQueue implements Partial<Queue> {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
async removeJobs(pattern: string) {
|
async removeJobs(pattern: string) {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,7 +132,7 @@ function logging(queue: Queue, jobQueue: JobQueue) {
|
||||||
// A Job is waiting to be processed as soon as a worker is idling.
|
// A Job is waiting to be processed as soon as a worker is idling.
|
||||||
console.info(...getLogParams(eventType, BullEvent.WAITING, { jobId }))
|
console.info(...getLogParams(eventType, BullEvent.WAITING, { jobId }))
|
||||||
})
|
})
|
||||||
.on(BullEvent.ACTIVE, async (job: Job, jobPromise: any) => {
|
.on(BullEvent.ACTIVE, async (job: Job) => {
|
||||||
// A job has started. You can use `jobPromise.cancel()`` to abort it.
|
// A job has started. You can use `jobPromise.cancel()`` to abort it.
|
||||||
await doInJobContext(job, () => {
|
await doInJobContext(job, () => {
|
||||||
console.info(...getLogParams(eventType, BullEvent.ACTIVE, { job }))
|
console.info(...getLogParams(eventType, BullEvent.ACTIVE, { job }))
|
||||||
|
|
|
@ -40,6 +40,7 @@ export async function shutdown() {
|
||||||
if (inviteClient) await inviteClient.finish()
|
if (inviteClient) await inviteClient.finish()
|
||||||
if (passwordResetClient) await passwordResetClient.finish()
|
if (passwordResetClient) await passwordResetClient.finish()
|
||||||
if (socketClient) await socketClient.finish()
|
if (socketClient) await socketClient.finish()
|
||||||
|
if (docWritethroughClient) await docWritethroughClient.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on("exit", async () => {
|
process.on("exit", async () => {
|
||||||
|
|
|
@ -120,7 +120,7 @@ describe("redis", () => {
|
||||||
|
|
||||||
await redis.bulkStore(data, ttl)
|
await redis.bulkStore(data, ttl)
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const key of Object.keys(data)) {
|
||||||
expect(await redis.get(key)).toBe(null)
|
expect(await redis.get(key)).toBe(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,17 +147,6 @@ describe("redis", () => {
|
||||||
expect(results).toEqual([1, 2, 3, 4, 5])
|
expect(results).toEqual([1, 2, 3, 4, 5])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("can increment on a new key", async () => {
|
|
||||||
const key1 = structures.uuid()
|
|
||||||
const key2 = structures.uuid()
|
|
||||||
|
|
||||||
const result1 = await redis.increment(key1)
|
|
||||||
expect(result1).toBe(1)
|
|
||||||
|
|
||||||
const result2 = await redis.increment(key2)
|
|
||||||
expect(result2).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("can increment multiple times in parallel", async () => {
|
it("can increment multiple times in parallel", async () => {
|
||||||
const key = structures.uuid()
|
const key = structures.uuid()
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
|
@ -184,7 +173,7 @@ describe("redis", () => {
|
||||||
const key = structures.uuid()
|
const key = structures.uuid()
|
||||||
await redis.store(key, value)
|
await redis.store(key, value)
|
||||||
|
|
||||||
await expect(redis.increment(key)).rejects.toThrowError(
|
await expect(redis.increment(key)).rejects.toThrow(
|
||||||
"ERR value is not an integer or out of range"
|
"ERR value is not an integer or out of range"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -96,8 +96,8 @@ describe("redlockImpl", () => {
|
||||||
task: mockTask,
|
task: mockTask,
|
||||||
executionTimeMs: lockTtl * 2,
|
executionTimeMs: lockTtl * 2,
|
||||||
})
|
})
|
||||||
).rejects.toThrowError(
|
).rejects.toThrow(
|
||||||
`Unable to fully release the lock on resource \"lock:${config.tenantId}_persist_writethrough\".`
|
`Unable to fully release the lock on resource "lock:${config.tenantId}_persist_writethrough".`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -158,8 +158,8 @@ describe("getTenantIDFromCtx", () => {
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
expect(getTenantIDFromCtx(ctx, mockOpts)).toBeUndefined()
|
expect(getTenantIDFromCtx(ctx, mockOpts)).toBeUndefined()
|
||||||
expect(ctx.throw).toBeCalledTimes(1)
|
expect(ctx.throw).toHaveBeenCalledTimes(1)
|
||||||
expect(ctx.throw).toBeCalledWith(403, "Tenant id not set")
|
expect(ctx.throw).toHaveBeenCalledWith(403, "Tenant id not set")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns undefined if allowNoTenant is true", () => {
|
it("returns undefined if allowNoTenant is true", () => {
|
||||||
|
|
|
@ -500,13 +500,13 @@ export class UserDB {
|
||||||
|
|
||||||
static async createAdminUser(
|
static async createAdminUser(
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
|
password?: string,
|
||||||
opts?: CreateAdminUserOpts
|
opts?: CreateAdminUserOpts
|
||||||
) {
|
) {
|
||||||
const user: User = {
|
const user: User = {
|
||||||
email: email,
|
email: email,
|
||||||
password: password,
|
password,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
roles: {},
|
roles: {},
|
||||||
builder: {
|
builder: {
|
||||||
|
|
|
@ -45,7 +45,7 @@ describe("Users", () => {
|
||||||
...{ _id: groupId, roles: { app1: "ADMIN" } },
|
...{ _id: groupId, roles: { app1: "ADMIN" } },
|
||||||
}
|
}
|
||||||
const users: User[] = []
|
const users: User[] = []
|
||||||
for (const _ of Array.from({ length: usersInGroup })) {
|
for (let i = 0; i < usersInGroup; i++) {
|
||||||
const userId = `us_${generator.guid()}`
|
const userId = `us_${generator.guid()}`
|
||||||
const user: User = structures.users.user({
|
const user: User = structures.users.user({
|
||||||
_id: userId,
|
_id: userId,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { generator } from "./generator"
|
||||||
|
|
||||||
export function userGroup(): UserGroup {
|
export function userGroup(): UserGroup {
|
||||||
return {
|
return {
|
||||||
name: generator.word(),
|
name: generator.guid(),
|
||||||
icon: generator.word(),
|
icon: generator.word(),
|
||||||
color: generator.word(),
|
color: generator.word(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,80 +1,58 @@
|
||||||
|
import { DatabaseImpl } from "../../../src/db"
|
||||||
import { execSync } from "child_process"
|
import { execSync } from "child_process"
|
||||||
|
|
||||||
let dockerPsResult: string | undefined
|
interface ContainerInfo {
|
||||||
|
Command: string
|
||||||
function formatDockerPsResult(serverName: string, port: number) {
|
CreatedAt: string
|
||||||
const lines = dockerPsResult?.split("\n")
|
ID: string
|
||||||
let first = true
|
Image: string
|
||||||
if (!lines) {
|
Labels: string
|
||||||
return null
|
LocalVolumes: string
|
||||||
}
|
Mounts: string
|
||||||
for (let line of lines) {
|
Names: string
|
||||||
if (first) {
|
Networks: string
|
||||||
first = false
|
Ports: string
|
||||||
continue
|
RunningFor: string
|
||||||
}
|
Size: string
|
||||||
let toLookFor = serverName.split("-service")[0]
|
State: string
|
||||||
if (!line.includes(toLookFor)) {
|
Status: string
|
||||||
continue
|
|
||||||
}
|
|
||||||
const regex = new RegExp(`0.0.0.0:([0-9]*)->${port}`, "g")
|
|
||||||
const found = line.match(regex)
|
|
||||||
if (found) {
|
|
||||||
return found[0].split(":")[1].split("->")[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTestContainerSettings(
|
function getTestcontainers(): ContainerInfo[] {
|
||||||
serverName: string,
|
return execSync("docker ps --format json")
|
||||||
key: string
|
.toString()
|
||||||
): string | null {
|
.split("\n")
|
||||||
const entry = Object.entries(global).find(
|
.filter(x => x.length > 0)
|
||||||
([k]) =>
|
.map(x => JSON.parse(x) as ContainerInfo)
|
||||||
k.includes(`${serverName.toUpperCase()}`) &&
|
.filter(x => x.Labels.includes("org.testcontainers=true"))
|
||||||
k.includes(`${key.toUpperCase()}`)
|
|
||||||
)
|
|
||||||
if (!entry) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return entry[1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContainerInfo(containerName: string, port: number) {
|
function getContainerByImage(image: string) {
|
||||||
let assignedPort = getTestContainerSettings(
|
return getTestcontainers().find(x => x.Image.startsWith(image))
|
||||||
containerName.toUpperCase(),
|
|
||||||
`PORT_${port}`
|
|
||||||
)
|
|
||||||
if (!dockerPsResult) {
|
|
||||||
try {
|
|
||||||
const outputBuffer = execSync("docker ps")
|
|
||||||
dockerPsResult = outputBuffer.toString("utf8")
|
|
||||||
} catch (err) {
|
|
||||||
//no-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const possiblePort = formatDockerPsResult(containerName, port)
|
|
||||||
if (possiblePort) {
|
|
||||||
assignedPort = possiblePort
|
|
||||||
}
|
|
||||||
const host = getTestContainerSettings(containerName.toUpperCase(), "IP")
|
|
||||||
return {
|
|
||||||
port: assignedPort,
|
|
||||||
host,
|
|
||||||
url: host && assignedPort && `http://${host}:${assignedPort}`,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCouchConfig() {
|
function getExposedPort(container: ContainerInfo, port: number) {
|
||||||
return getContainerInfo("couchdb", 5984)
|
const match = container.Ports.match(new RegExp(`0.0.0.0:(\\d+)->${port}/tcp`))
|
||||||
|
if (!match) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return parseInt(match[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupEnv(...envs: any[]) {
|
export function setupEnv(...envs: any[]) {
|
||||||
const couch = getCouchConfig()
|
const couch = getContainerByImage("budibase/couchdb")
|
||||||
|
if (!couch) {
|
||||||
|
throw new Error("CouchDB container not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
const couchPort = getExposedPort(couch, 5984)
|
||||||
|
if (!couchPort) {
|
||||||
|
throw new Error("CouchDB port not found")
|
||||||
|
}
|
||||||
|
|
||||||
const configs = [
|
const configs = [
|
||||||
{ key: "COUCH_DB_PORT", value: couch.port },
|
{ key: "COUCH_DB_PORT", value: `${couchPort}` },
|
||||||
{ key: "COUCH_DB_URL", value: couch.url },
|
{ key: "COUCH_DB_URL", value: `http://localhost:${couchPort}` },
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const config of configs.filter(x => !!x.value)) {
|
for (const config of configs.filter(x => !!x.value)) {
|
||||||
|
@ -82,4 +60,7 @@ export function setupEnv(...envs: any[]) {
|
||||||
env._set(config.key, config.value)
|
env._set(config.key, config.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
DatabaseImpl.nano = undefined
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,7 @@ process.env.NODE_ENV = "jest"
|
||||||
process.env.MOCK_REDIS = "1"
|
process.env.MOCK_REDIS = "1"
|
||||||
process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error"
|
process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error"
|
||||||
process.env.REDIS_PASSWORD = "budibase"
|
process.env.REDIS_PASSWORD = "budibase"
|
||||||
|
process.env.COUCH_DB_PASSWORD = "budibase"
|
||||||
|
process.env.COUCH_DB_USER = "budibase"
|
||||||
|
process.env.API_ENCRYPTION_KEY = "testsecret"
|
||||||
|
process.env.JWT_SECRET = "testsecret"
|
||||||
|
|
|
@ -12,6 +12,13 @@ export default {
|
||||||
format: "esm",
|
format: "esm",
|
||||||
file: "dist/bbui.es.js",
|
file: "dist/bbui.es.js",
|
||||||
},
|
},
|
||||||
|
onwarn(warning, warn) {
|
||||||
|
// suppress eval warnings
|
||||||
|
if (warning.code === "EVAL") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
warn(warning)
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve(),
|
resolve(),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
|
|
|
@ -39,19 +39,23 @@ const handleClick = event => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (handler.allowedType && event.type !== handler.allowedType) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
handler.callback?.(event)
|
handler.callback?.(event)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
document.documentElement.addEventListener("click", handleClick, true)
|
document.documentElement.addEventListener("click", handleClick, true)
|
||||||
document.documentElement.addEventListener("contextmenu", handleClick, true)
|
document.documentElement.addEventListener("mousedown", handleClick, true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds or updates a click handler
|
* Adds or updates a click handler
|
||||||
*/
|
*/
|
||||||
const updateHandler = (id, element, anchor, callback) => {
|
const updateHandler = (id, element, anchor, callback, allowedType) => {
|
||||||
let existingHandler = clickHandlers.find(x => x.id === id)
|
let existingHandler = clickHandlers.find(x => x.id === id)
|
||||||
if (!existingHandler) {
|
if (!existingHandler) {
|
||||||
clickHandlers.push({ id, element, anchor, callback })
|
clickHandlers.push({ id, element, anchor, callback, allowedType })
|
||||||
} else {
|
} else {
|
||||||
existingHandler.callback = callback
|
existingHandler.callback = callback
|
||||||
}
|
}
|
||||||
|
@ -75,9 +79,11 @@ const removeHandler = id => {
|
||||||
export default (element, opts) => {
|
export default (element, opts) => {
|
||||||
const id = Math.random()
|
const id = Math.random()
|
||||||
const update = newOpts => {
|
const update = newOpts => {
|
||||||
const callback = newOpts?.callback || newOpts
|
const callback =
|
||||||
|
newOpts?.callback || (typeof newOpts === "function" ? newOpts : null)
|
||||||
const anchor = newOpts?.anchor || element
|
const anchor = newOpts?.anchor || element
|
||||||
updateHandler(id, element, anchor, callback)
|
const allowedType = newOpts?.allowedType || "click"
|
||||||
|
updateHandler(id, element, anchor, callback, allowedType)
|
||||||
}
|
}
|
||||||
update(opts)
|
update(opts)
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -42,7 +42,6 @@
|
||||||
.main {
|
.main {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
.padding .main {
|
.padding .main {
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl);
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let schema
|
export let schema
|
||||||
export let value
|
export let value
|
||||||
export let customRenderers = []
|
export let customRenderers = []
|
||||||
|
export let snippets
|
||||||
|
|
||||||
let renderer
|
let renderer
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
|
@ -44,7 +45,7 @@
|
||||||
if (!template) {
|
if (!template) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
return processStringSync(template, { value })
|
return processStringSync(template, { value, snippets })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
export let customPlaceholder = false
|
export let customPlaceholder = false
|
||||||
export let showHeaderBorder = true
|
export let showHeaderBorder = true
|
||||||
export let placeholderText = "No rows found"
|
export let placeholderText = "No rows found"
|
||||||
|
export let snippets = []
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -425,6 +426,7 @@
|
||||||
<CellRenderer
|
<CellRenderer
|
||||||
{customRenderers}
|
{customRenderers}
|
||||||
{row}
|
{row}
|
||||||
|
{snippets}
|
||||||
schema={schema[field]}
|
schema={schema[field]}
|
||||||
value={deepGet(row, field)}
|
value={deepGet(row, field)}
|
||||||
on:clickrelationship
|
on:clickrelationship
|
||||||
|
|
15
packages/builder/.vscode/launch.json
vendored
15
packages/builder/.vscode/launch.json
vendored
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Launch Chrome against localhost",
|
|
||||||
"url": "http://localhost:3000",
|
|
||||||
"webRoot": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
8
packages/builder/.vscode/settings.json
vendored
8
packages/builder/.vscode/settings.json
vendored
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"javascript.format.enable": false,
|
|
||||||
"svelte.plugin.svelte.format.enable": false,
|
|
||||||
"html.format.enable": false,
|
|
||||||
"json.format.enable": false,
|
|
||||||
"editor.trimAutoWhitespace": false,
|
|
||||||
"sass.format.deleteWhitespace": false
|
|
||||||
}
|
|
|
@ -191,8 +191,10 @@
|
||||||
// don't make field IDs for auto types
|
// don't make field IDs for auto types
|
||||||
if (type === AUTO_TYPE || autocolumn) {
|
if (type === AUTO_TYPE || autocolumn) {
|
||||||
return type.toUpperCase()
|
return type.toUpperCase()
|
||||||
} else {
|
} else if (type === FieldType.BB_REFERENCE) {
|
||||||
return `${type}${subtype || ""}`.toUpperCase()
|
return `${type}${subtype || ""}`.toUpperCase()
|
||||||
|
} else {
|
||||||
|
return type.toUpperCase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -703,24 +705,6 @@
|
||||||
thin
|
thin
|
||||||
text="Allow multiple users"
|
text="Allow multiple users"
|
||||||
/>
|
/>
|
||||||
{:else if editableColumn.type === FieldType.ATTACHMENT}
|
|
||||||
<Toggle
|
|
||||||
value={editableColumn.constraints?.length?.maximum !== 1}
|
|
||||||
on:change={e => {
|
|
||||||
if (!e.detail) {
|
|
||||||
editableColumn.constraints ??= { length: {} }
|
|
||||||
editableColumn.constraints.length ??= {}
|
|
||||||
editableColumn.constraints.length.maximum = 1
|
|
||||||
editableColumn.constraints.length.message =
|
|
||||||
"cannot contain multiple files"
|
|
||||||
} else {
|
|
||||||
delete editableColumn.constraints?.length?.maximum
|
|
||||||
delete editableColumn.constraints?.length?.message
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
thin
|
|
||||||
text="Allow multiple"
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
|
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
let deleteTableName
|
let deleteTableName
|
||||||
|
|
||||||
$: externalTable = table?.sourceType === DB_TYPE_EXTERNAL
|
$: externalTable = table?.sourceType === DB_TYPE_EXTERNAL
|
||||||
$: allowDeletion = !externalTable || table?.created
|
|
||||||
|
|
||||||
function showDeleteModal() {
|
function showDeleteModal() {
|
||||||
templateScreens = $screenStore.screens.filter(
|
templateScreens = $screenStore.screens.filter(
|
||||||
|
@ -56,7 +55,7 @@
|
||||||
$goto(`./datasource/${table.datasourceId}`)
|
$goto(`./datasource/${table.datasourceId}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error deleting table")
|
notifications.error(`Error deleting table - ${error.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,17 +85,15 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if allowDeletion}
|
<ActionMenu>
|
||||||
<ActionMenu>
|
<div slot="control" class="icon">
|
||||||
<div slot="control" class="icon">
|
<Icon s hoverable name="MoreSmallList" />
|
||||||
<Icon s hoverable name="MoreSmallList" />
|
</div>
|
||||||
</div>
|
{#if !externalTable}
|
||||||
{#if !externalTable}
|
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
|
||||||
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
|
{/if}
|
||||||
{/if}
|
<MenuItem icon="Delete" on:click={showDeleteModal}>Delete</MenuItem>
|
||||||
<MenuItem icon="Delete" on:click={showDeleteModal}>Delete</MenuItem>
|
</ActionMenu>
|
||||||
</ActionMenu>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Modal bind:this={editorModal} on:show={initForm}>
|
<Modal bind:this={editorModal} on:show={initForm}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
|
|
@ -313,7 +313,7 @@ export const bindingsToCompletions = (bindings, mode) => {
|
||||||
...bindingByCategory[catKey].reduce((acc, binding) => {
|
...bindingByCategory[catKey].reduce((acc, binding) => {
|
||||||
let displayType = binding.fieldSchema?.type || binding.display?.type
|
let displayType = binding.fieldSchema?.type || binding.display?.type
|
||||||
acc.push({
|
acc.push({
|
||||||
label: binding.display?.name || "NO NAME",
|
label: binding.display?.name || binding.readableBinding || "NO NAME",
|
||||||
info: completion => {
|
info: completion => {
|
||||||
return buildBindingInfoNode(completion, binding)
|
return buildBindingInfoNode(completion, binding)
|
||||||
},
|
},
|
||||||
|
|
|
@ -371,6 +371,7 @@
|
||||||
<style>
|
<style>
|
||||||
.binding-panel {
|
.binding-panel {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.binding-panel,
|
.binding-panel,
|
||||||
.tabs {
|
.tabs {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
export let allowJS = false
|
export let allowJS = false
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let autofocusEditor = false
|
export let autofocusEditor = false
|
||||||
|
export let context = null
|
||||||
|
|
||||||
$: enrichedBindings = enrichBindings(bindings)
|
$: enrichedBindings = enrichBindings(bindings)
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@
|
||||||
|
|
||||||
<BindingPanel
|
<BindingPanel
|
||||||
bindings={enrichedBindings}
|
bindings={enrichedBindings}
|
||||||
context={$previewStore.selectedComponentContext}
|
context={{ ...$previewStore.selectedComponentContext, ...context }}
|
||||||
snippets={$snippets}
|
snippets={$snippets}
|
||||||
{value}
|
{value}
|
||||||
{allowJS}
|
{allowJS}
|
||||||
|
|
|
@ -32,10 +32,14 @@
|
||||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||||
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import PosthogClient from "../../analytics/PosthogClient"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
export let loaded
|
export let loaded
|
||||||
|
|
||||||
|
const posthog = new PosthogClient(process.env.POSTHOG_TOKEN)
|
||||||
|
|
||||||
let unpublishModal
|
let unpublishModal
|
||||||
let updateAppModal
|
let updateAppModal
|
||||||
let revertModal
|
let revertModal
|
||||||
|
@ -44,6 +48,8 @@
|
||||||
let appActionPopoverOpen = false
|
let appActionPopoverOpen = false
|
||||||
let appActionPopoverAnchor
|
let appActionPopoverAnchor
|
||||||
let publishing = false
|
let publishing = false
|
||||||
|
let showNpsSurvey = false
|
||||||
|
let lastOpened
|
||||||
|
|
||||||
$: filteredApps = $appsStore.apps.filter(app => app.devId === application)
|
$: filteredApps = $appsStore.apps.filter(app => app.devId === application)
|
||||||
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||||
|
@ -57,7 +63,7 @@
|
||||||
$appStore.version &&
|
$appStore.version &&
|
||||||
$appStore.upgradableVersion !== $appStore.version
|
$appStore.upgradableVersion !== $appStore.version
|
||||||
$: canPublish = !publishing && loaded && $sortedScreens.length > 0
|
$: canPublish = !publishing && loaded && $sortedScreens.length > 0
|
||||||
$: lastDeployed = getLastDeployedString($deploymentStore)
|
$: lastDeployed = getLastDeployedString($deploymentStore, lastOpened)
|
||||||
|
|
||||||
const initialiseApp = async () => {
|
const initialiseApp = async () => {
|
||||||
const applicationPkg = await API.fetchAppPackage($appStore.devId)
|
const applicationPkg = await API.fetchAppPackage($appStore.devId)
|
||||||
|
@ -97,6 +103,7 @@
|
||||||
type: "success",
|
type: "success",
|
||||||
icon: "GlobeCheck",
|
icon: "GlobeCheck",
|
||||||
})
|
})
|
||||||
|
showNpsSurvey = true
|
||||||
await completePublish()
|
await completePublish()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
@ -147,6 +154,10 @@
|
||||||
notifications.error("Error refreshing app")
|
notifications.error("Error refreshing app")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
posthog.init()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
@ -201,6 +212,7 @@
|
||||||
class="app-action-button publish app-action-popover"
|
class="app-action-button publish app-action-popover"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (!appActionPopoverOpen) {
|
if (!appActionPopoverOpen) {
|
||||||
|
lastOpened = new Date()
|
||||||
appActionPopover.show()
|
appActionPopover.show()
|
||||||
} else {
|
} else {
|
||||||
appActionPopover.hide()
|
appActionPopover.hide()
|
||||||
|
@ -343,6 +355,10 @@
|
||||||
<RevertModal bind:this={revertModal} />
|
<RevertModal bind:this={revertModal} />
|
||||||
<VersionModal hideIcon bind:this={versionModal} />
|
<VersionModal hideIcon bind:this={versionModal} />
|
||||||
|
|
||||||
|
{#if showNpsSurvey}
|
||||||
|
<div class="nps-survey" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.app-action-popover-content {
|
.app-action-popover-content {
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl);
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script>
|
||||||
|
import { API } from "api"
|
||||||
|
import clientVersions from "./clientVersions.json"
|
||||||
|
import { appStore } from "stores/builder"
|
||||||
|
import { Select } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let revertableVersion
|
||||||
|
$: appId = $appStore.appId
|
||||||
|
|
||||||
|
const handleChange = e => {
|
||||||
|
const value = e.detail
|
||||||
|
if (value == null) return
|
||||||
|
|
||||||
|
API.setRevertableVersion(appId, value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="select">
|
||||||
|
<Select
|
||||||
|
autoWidth
|
||||||
|
value={revertableVersion}
|
||||||
|
options={clientVersions}
|
||||||
|
on:change={handleChange}
|
||||||
|
footer={"Older versions of the Budibase client can be acquired using `yarn get-past-client-version x.x.x`. This toggle is only available in dev mode."}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.select {
|
||||||
|
width: 120px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { admin } from "stores/portal"
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
|
@ -9,6 +10,7 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { appStore, initialise } from "stores/builder"
|
import { appStore, initialise } from "stores/builder"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
import RevertModalVersionSelect from "./RevertModalVersionSelect.svelte"
|
||||||
|
|
||||||
export function show() {
|
export function show() {
|
||||||
updateModal.show()
|
updateModal.show()
|
||||||
|
@ -28,7 +30,9 @@
|
||||||
$appStore.upgradableVersion &&
|
$appStore.upgradableVersion &&
|
||||||
$appStore.version &&
|
$appStore.version &&
|
||||||
$appStore.upgradableVersion !== $appStore.version
|
$appStore.upgradableVersion !== $appStore.version
|
||||||
$: revertAvailable = $appStore.revertableVersion != null
|
$: revertAvailable =
|
||||||
|
$appStore.revertableVersion != null ||
|
||||||
|
($admin.isDev && $appStore.version === "0.0.0")
|
||||||
|
|
||||||
const refreshAppPackage = async () => {
|
const refreshAppPackage = async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -62,7 +66,9 @@
|
||||||
// Don't wait for the async refresh, since this causes modal flashing
|
// Don't wait for the async refresh, since this causes modal flashing
|
||||||
refreshAppPackage()
|
refreshAppPackage()
|
||||||
notifications.success(
|
notifications.success(
|
||||||
`App reverted successfully to version ${$appStore.revertableVersion}`
|
$appStore.revertableVersion
|
||||||
|
? `App reverted successfully to version ${$appStore.revertableVersion}`
|
||||||
|
: "App reverted successfully"
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error reverting app: ${err}`)
|
notifications.error(`Error reverting app: ${err}`)
|
||||||
|
@ -103,7 +109,13 @@
|
||||||
{#if revertAvailable}
|
{#if revertAvailable}
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
You can revert this app to version
|
You can revert this app to version
|
||||||
<b>{$appStore.revertableVersion}</b>
|
{#if $admin.isDev}
|
||||||
|
<RevertModalVersionSelect
|
||||||
|
revertableVersion={$appStore.revertableVersion}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<b>{$appStore.revertableVersion}</b>
|
||||||
|
{/if}
|
||||||
if you're experiencing issues with the current version.
|
if you're experiencing issues with the current version.
|
||||||
</Body>
|
</Body>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
[]
|
|
@ -7,10 +7,13 @@
|
||||||
Layout,
|
Layout,
|
||||||
Label,
|
Label,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { themeStore } from "stores/builder"
|
import { themeStore, previewStore } from "stores/builder"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
export let column
|
export let column
|
||||||
|
|
||||||
|
$: columnValue =
|
||||||
|
$previewStore.selectedComponentContext?.eventContext?.row?.[column.name]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
|
@ -41,6 +44,9 @@
|
||||||
icon: "TableColumnMerge",
|
icon: "TableColumnMerge",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
context={{
|
||||||
|
value: columnValue,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Label>Background color</Label>
|
<Label>Background color</Label>
|
||||||
|
|
|
@ -279,3 +279,11 @@ export const buildContextTreeLookupMap = rootComponent => {
|
||||||
})
|
})
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get a flat list of ids for all descendants of a component
|
||||||
|
export const getChildIdsForComponent = component => {
|
||||||
|
return [
|
||||||
|
component._id,
|
||||||
|
...(component?._children ?? []).map(getChildIdsForComponent).flat(1),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
@ -129,10 +129,7 @@
|
||||||
filteredUsers = $usersFetch.rows
|
filteredUsers = $usersFetch.rows
|
||||||
.filter(user => user.email !== $auth.user.email)
|
.filter(user => user.email !== $auth.user.email)
|
||||||
.map(user => {
|
.map(user => {
|
||||||
const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder(
|
const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder(user)
|
||||||
user,
|
|
||||||
prodAppId
|
|
||||||
)
|
|
||||||
const isAppBuilder = user.builder?.apps?.includes(prodAppId)
|
const isAppBuilder = user.builder?.apps?.includes(prodAppId)
|
||||||
let role
|
let role
|
||||||
if (isAdminOrGlobalBuilder) {
|
if (isAdminOrGlobalBuilder) {
|
||||||
|
|
|
@ -24,6 +24,13 @@
|
||||||
navigationStore,
|
navigationStore,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import { DefaultAppTheme } from "constants"
|
import { DefaultAppTheme } from "constants"
|
||||||
|
import BarButtonList from "/src/components/design/settings/controls/BarButtonList.svelte"
|
||||||
|
|
||||||
|
$: alignmentOptions = [
|
||||||
|
{ value: "Left", barIcon: "TextAlignLeft" },
|
||||||
|
{ value: "Center", barIcon: "TextAlignCenter" },
|
||||||
|
{ value: "Right", barIcon: "TextAlignRight" },
|
||||||
|
]
|
||||||
|
|
||||||
$: screenRouteOptions = $screenStore.screens
|
$: screenRouteOptions = $screenStore.screens
|
||||||
.map(screen => screen.routing?.route)
|
.map(screen => screen.routing?.route)
|
||||||
|
@ -46,6 +53,10 @@
|
||||||
notifications.error("Error updating navigation settings")
|
notifications.error("Error updating navigation settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateTextAlign = textAlignValue => {
|
||||||
|
navigationStore.syncAppNavigation({ textAlign: textAlignValue })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel
|
<Panel
|
||||||
|
@ -133,6 +144,15 @@
|
||||||
on:change={e => update("title", e.detail)}
|
on:change={e => update("title", e.detail)}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="label">
|
||||||
|
<Label size="M">Text align</Label>
|
||||||
|
</div>
|
||||||
|
<BarButtonList
|
||||||
|
options={alignmentOptions}
|
||||||
|
value={$navigationStore.textAlign}
|
||||||
|
onChange={updateTextAlign}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<Label>Background</Label>
|
<Label>Background</Label>
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
"name": "Blocks",
|
"name": "Blocks",
|
||||||
"icon": "Article",
|
"icon": "Article",
|
||||||
"children": [
|
"children": [
|
||||||
"gridblock",
|
|
||||||
"tableblock",
|
|
||||||
"cardsblock",
|
"cardsblock",
|
||||||
"repeaterblock",
|
"repeaterblock",
|
||||||
"formblock",
|
"formblock",
|
||||||
|
@ -16,7 +14,7 @@
|
||||||
{
|
{
|
||||||
"name": "Layout",
|
"name": "Layout",
|
||||||
"icon": "ClassicGridView",
|
"icon": "ClassicGridView",
|
||||||
"children": ["container", "section", "grid", "sidepanel"]
|
"children": ["container", "section", "sidepanel"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Data",
|
"name": "Data",
|
||||||
|
@ -24,7 +22,7 @@
|
||||||
"children": [
|
"children": [
|
||||||
"dataprovider",
|
"dataprovider",
|
||||||
"repeater",
|
"repeater",
|
||||||
"table",
|
"gridblock",
|
||||||
"spreadsheet",
|
"spreadsheet",
|
||||||
"dynamicfilter",
|
"dynamicfilter",
|
||||||
"daterangepicker"
|
"daterangepicker"
|
||||||
|
|
|
@ -10,20 +10,15 @@
|
||||||
navigationStore,
|
navigationStore,
|
||||||
selectedScreen,
|
selectedScreen,
|
||||||
hoverStore,
|
hoverStore,
|
||||||
|
componentTreeNodesStore,
|
||||||
snippets,
|
snippets,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import {
|
import { Layout, Heading, Body, Icon, notifications } from "@budibase/bbui"
|
||||||
ProgressCircle,
|
|
||||||
Layout,
|
|
||||||
Heading,
|
|
||||||
Body,
|
|
||||||
Icon,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
|
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
|
||||||
import { findComponent, findComponentPath } from "helpers/components"
|
import { findComponent, findComponentPath } from "helpers/components"
|
||||||
import { isActive, goto } from "@roxi/routify"
|
import { isActive, goto } from "@roxi/routify"
|
||||||
|
import { ClientAppSkeleton } from "@budibase/frontend-core"
|
||||||
|
|
||||||
let iframe
|
let iframe
|
||||||
let layout
|
let layout
|
||||||
|
@ -132,6 +127,7 @@
|
||||||
error = event.error || "An unknown error occurred"
|
error = event.error || "An unknown error occurred"
|
||||||
} else if (type === "select-component" && "id" in data) {
|
} else if (type === "select-component" && "id" in data) {
|
||||||
componentStore.select(data.id)
|
componentStore.select(data.id)
|
||||||
|
componentTreeNodesStore.makeNodeVisible(data.id)
|
||||||
} else if (type === "hover-component") {
|
} else if (type === "hover-component") {
|
||||||
hoverStore.hover(data.id, false)
|
hoverStore.hover(data.id, false)
|
||||||
} else if (type === "update-prop") {
|
} else if (type === "update-prop") {
|
||||||
|
@ -252,8 +248,16 @@
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div class="component-container">
|
<div class="component-container">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="center">
|
<div
|
||||||
<ProgressCircle />
|
class={`loading ${$themeStore.baseTheme} ${$themeStore.theme}`}
|
||||||
|
class:tablet={$previewStore.previewDevice === "tablet"}
|
||||||
|
class:mobile={$previewStore.previewDevice === "mobile"}
|
||||||
|
>
|
||||||
|
<ClientAppSkeleton
|
||||||
|
sideNav={$navigationStore?.navigation === "Left"}
|
||||||
|
hideFooter
|
||||||
|
hideDevTools
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<div class="center error">
|
<div class="center error">
|
||||||
|
@ -270,8 +274,6 @@
|
||||||
bind:this={iframe}
|
bind:this={iframe}
|
||||||
src="/app/preview"
|
src="/app/preview"
|
||||||
class:hidden={loading || error}
|
class:hidden={loading || error}
|
||||||
class:tablet={$previewStore.previewDevice === "tablet"}
|
|
||||||
class:mobile={$previewStore.previewDevice === "mobile"}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="add-component"
|
class="add-component"
|
||||||
|
@ -291,6 +293,25 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.loading {
|
||||||
|
position: absolute;
|
||||||
|
container-type: inline-size;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading.tablet {
|
||||||
|
width: calc(1024px + 6px);
|
||||||
|
max-height: calc(768px + 6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading.mobile {
|
||||||
|
width: calc(390px + 6px);
|
||||||
|
max-height: calc(844px + 6px);
|
||||||
|
}
|
||||||
|
|
||||||
.component-container {
|
.component-container {
|
||||||
grid-row-start: middle;
|
grid-row-start: middle;
|
||||||
grid-column-start: middle;
|
grid-column-start: middle;
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
selectedScreen,
|
selectedScreen,
|
||||||
componentStore,
|
componentStore,
|
||||||
selectedComponent,
|
selectedComponent,
|
||||||
|
componentTreeNodesStore,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import { findComponent } from "helpers/components"
|
import { findComponent, getChildIdsForComponent } from "helpers/components"
|
||||||
import { goto, isActive } from "@roxi/routify"
|
import { goto, isActive } from "@roxi/routify"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
|
|
||||||
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
let confirmEjectDialog
|
let confirmEjectDialog
|
||||||
|
@ -63,38 +63,25 @@
|
||||||
componentStore.selectNext()
|
componentStore.selectNext()
|
||||||
},
|
},
|
||||||
["ArrowRight"]: component => {
|
["ArrowRight"]: component => {
|
||||||
componentTreeNodesStore.expandNode(component._id)
|
componentTreeNodesStore.expandNodes([component._id])
|
||||||
},
|
},
|
||||||
["ArrowLeft"]: component => {
|
["ArrowLeft"]: component => {
|
||||||
componentTreeNodesStore.collapseNode(component._id)
|
// Select the collapsing root component to ensure the currently selected component is not
|
||||||
|
// hidden in a collapsed node
|
||||||
|
componentStore.select(component._id)
|
||||||
|
componentTreeNodesStore.collapseNodes([component._id])
|
||||||
},
|
},
|
||||||
["Ctrl+ArrowRight"]: component => {
|
["Ctrl+ArrowRight"]: component => {
|
||||||
componentTreeNodesStore.expandNode(component._id)
|
const childIds = getChildIdsForComponent(component)
|
||||||
|
componentTreeNodesStore.expandNodes(childIds)
|
||||||
const expandChildren = component => {
|
|
||||||
const children = component._children ?? []
|
|
||||||
|
|
||||||
children.forEach(child => {
|
|
||||||
componentTreeNodesStore.expandNode(child._id)
|
|
||||||
expandChildren(child)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
expandChildren(component)
|
|
||||||
},
|
},
|
||||||
["Ctrl+ArrowLeft"]: component => {
|
["Ctrl+ArrowLeft"]: component => {
|
||||||
componentTreeNodesStore.collapseNode(component._id)
|
// Select the collapsing root component to ensure the currently selected component is not
|
||||||
|
// hidden in a collapsed node
|
||||||
|
componentStore.select(component._id)
|
||||||
|
|
||||||
const collapseChildren = component => {
|
const childIds = getChildIdsForComponent(component)
|
||||||
const children = component._children ?? []
|
componentTreeNodesStore.collapseNodes(childIds)
|
||||||
|
|
||||||
children.forEach(child => {
|
|
||||||
componentTreeNodesStore.collapseNode(child._id)
|
|
||||||
collapseChildren(child)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
collapseChildren(component)
|
|
||||||
},
|
},
|
||||||
["Escape"]: () => {
|
["Escape"]: () => {
|
||||||
if ($isActive(`./:componentId/new`)) {
|
if ($isActive(`./:componentId/new`)) {
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
componentStore,
|
componentStore,
|
||||||
userSelectedResourceMap,
|
userSelectedResourceMap,
|
||||||
selectedComponent,
|
selectedComponent,
|
||||||
selectedComponentPath,
|
|
||||||
hoverStore,
|
hoverStore,
|
||||||
|
componentTreeNodesStore,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import {
|
import {
|
||||||
findComponentPath,
|
findComponentPath,
|
||||||
|
@ -17,7 +17,6 @@
|
||||||
} from "helpers/components"
|
} from "helpers/components"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { dndStore } from "./dndStore"
|
import { dndStore } from "./dndStore"
|
||||||
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
|
|
||||||
|
|
||||||
export let components = []
|
export let components = []
|
||||||
export let level = 0
|
export let level = 0
|
||||||
|
@ -64,14 +63,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOpen = (component, selectedComponentPath, openNodes) => {
|
const isOpen = component => {
|
||||||
if (!component?._children?.length) {
|
if (!component?._children?.length) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (selectedComponentPath.slice(0, -1).includes(component._id)) {
|
return componentTreeNodesStore.isNodeExpanded(component._id)
|
||||||
return true
|
|
||||||
}
|
|
||||||
return openNodes[`nodeOpen-${component._id}`]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isChildOfSelectedComponent = component => {
|
const isChildOfSelectedComponent = component => {
|
||||||
|
@ -83,6 +79,11 @@
|
||||||
return findComponentPath($selectedComponent, component._id)?.length > 0
|
return findComponentPath($selectedComponent, component._id)?.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleIconClick = componentId => {
|
||||||
|
componentStore.select(componentId)
|
||||||
|
componentTreeNodesStore.toggleNode(componentId)
|
||||||
|
}
|
||||||
|
|
||||||
const hover = hoverStore.hover
|
const hover = hoverStore.hover
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -90,7 +91,7 @@
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<ul>
|
<ul>
|
||||||
{#each filteredComponents || [] as component, index (component._id)}
|
{#each filteredComponents || [] as component, index (component._id)}
|
||||||
{@const opened = isOpen(component, $selectedComponentPath, openNodes)}
|
{@const opened = isOpen(component, openNodes)}
|
||||||
<li
|
<li
|
||||||
on:click|stopPropagation={() => {
|
on:click|stopPropagation={() => {
|
||||||
componentStore.select(component._id)
|
componentStore.select(component._id)
|
||||||
|
@ -104,7 +105,7 @@
|
||||||
on:dragend={dndStore.actions.reset}
|
on:dragend={dndStore.actions.reset}
|
||||||
on:dragstart={() => dndStore.actions.dragstart(component)}
|
on:dragstart={() => dndStore.actions.dragstart(component)}
|
||||||
on:dragover={dragover(component, index)}
|
on:dragover={dragover(component, index)}
|
||||||
on:iconClick={() => componentTreeNodesStore.toggleNode(component._id)}
|
on:iconClick={() => handleIconClick(component._id)}
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}
|
||||||
hovering={$hoverStore.componentId === component._id}
|
hovering={$hoverStore.componentId === component._id}
|
||||||
on:mouseenter={() => hover(component._id)}
|
on:mouseenter={() => hover(component._id)}
|
||||||
|
|
|
@ -19,7 +19,8 @@
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
import formScreen from "templates/formScreen"
|
import formScreen from "templates/formScreen"
|
||||||
import rowListScreen from "templates/rowListScreen"
|
import gridListScreen from "templates/gridListScreen"
|
||||||
|
import gridDetailsScreen from "templates/gridDetailsScreen"
|
||||||
|
|
||||||
let mode
|
let mode
|
||||||
let pendingScreen
|
let pendingScreen
|
||||||
|
@ -127,7 +128,7 @@
|
||||||
screenAccessRole = Roles.BASIC
|
screenAccessRole = Roles.BASIC
|
||||||
formType = null
|
formType = null
|
||||||
|
|
||||||
if (mode === "table" || mode === "grid" || mode === "form") {
|
if (mode === "grid" || mode === "gridDetails" || mode === "form") {
|
||||||
datasourceModal.show()
|
datasourceModal.show()
|
||||||
} else if (mode === "blank") {
|
} else if (mode === "blank") {
|
||||||
let templates = getTemplates($tables.list)
|
let templates = getTemplates($tables.list)
|
||||||
|
@ -153,7 +154,10 @@
|
||||||
|
|
||||||
// Handler for Datasource Screen Creation
|
// Handler for Datasource Screen Creation
|
||||||
const completeDatasourceScreenCreation = async () => {
|
const completeDatasourceScreenCreation = async () => {
|
||||||
templates = rowListScreen(selectedDatasources, mode)
|
templates =
|
||||||
|
mode === "grid"
|
||||||
|
? gridListScreen(selectedDatasources)
|
||||||
|
: gridDetailsScreen(selectedDatasources)
|
||||||
|
|
||||||
const screens = templates.map(template => {
|
const screens = templates.map(template => {
|
||||||
let screenTemplate = template.create()
|
let screenTemplate = template.create()
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
|
@ -2,8 +2,8 @@
|
||||||
import { Body } from "@budibase/bbui"
|
import { Body } from "@budibase/bbui"
|
||||||
import CreationPage from "components/common/CreationPage.svelte"
|
import CreationPage from "components/common/CreationPage.svelte"
|
||||||
import blankImage from "./images/blank.png"
|
import blankImage from "./images/blank.png"
|
||||||
import tableImage from "./images/table.png"
|
import tableInline from "./images/tableInline.png"
|
||||||
import gridImage from "./images/grid.png"
|
import tableDetails from "./images/tableDetails.png"
|
||||||
import formImage from "./images/form.png"
|
import formImage from "./images/form.png"
|
||||||
import CreateScreenModal from "./CreateScreenModal.svelte"
|
import CreateScreenModal from "./CreateScreenModal.svelte"
|
||||||
import { screenStore } from "stores/builder"
|
import { screenStore } from "stores/builder"
|
||||||
|
@ -38,23 +38,23 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" on:click={() => createScreenModal.show("table")}>
|
<div class="card" on:click={() => createScreenModal.show("grid")}>
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<img alt="" src={tableImage} />
|
<img alt="" src={tableInline} />
|
||||||
</div>
|
</div>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<Body size="S">Table</Body>
|
<Body size="S">Table with inline editing</Body>
|
||||||
<Body size="XS">View, edit and delete rows on a table</Body>
|
<Body size="XS">View, edit and delete rows inline</Body>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" on:click={() => createScreenModal.show("grid")}>
|
<div class="card" on:click={() => createScreenModal.show("gridDetails")}>
|
||||||
<div class="image">
|
<div class="image">
|
||||||
<img alt="" src={gridImage} />
|
<img alt="" src={tableDetails} />
|
||||||
</div>
|
</div>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<Body size="S">Grid</Body>
|
<Body size="S">Table with details panel</Body>
|
||||||
<Body size="XS">View and manipulate rows on a grid</Body>
|
<Body size="XS">Manage your row details in a side panel</Body>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -113,6 +113,11 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card .image {
|
||||||
|
min-height: 130px;
|
||||||
|
min-width: 235px;
|
||||||
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
border: 1px solid var(--grey-4);
|
border: 1px solid var(--grey-4);
|
||||||
border-radius: 0 0 4px 4px;
|
border-radius: 0 0 4px 4px;
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { onMount, onDestroy } from "svelte"
|
||||||
import { params, goto } from "@roxi/routify"
|
import { params, goto } from "@roxi/routify"
|
||||||
import { auth, sideBarCollapsed, enrichedApps } from "stores/portal"
|
import {
|
||||||
|
licensing,
|
||||||
|
auth,
|
||||||
|
sideBarCollapsed,
|
||||||
|
enrichedApps,
|
||||||
|
} from "stores/portal"
|
||||||
import AppRowContext from "components/start/AppRowContext.svelte"
|
import AppRowContext from "components/start/AppRowContext.svelte"
|
||||||
import FavouriteAppButton from "../FavouriteAppButton.svelte"
|
import FavouriteAppButton from "../FavouriteAppButton.svelte"
|
||||||
import {
|
import {
|
||||||
|
@ -14,12 +20,17 @@
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import ErrorSVG from "./ErrorSVG.svelte"
|
import ErrorSVG from "./ErrorSVG.svelte"
|
||||||
|
import { getBaseTheme, ClientAppSkeleton } from "@budibase/frontend-core"
|
||||||
|
|
||||||
$: app = $enrichedApps.find(app => app.appId === $params.appId)
|
$: app = $enrichedApps.find(app => app.appId === $params.appId)
|
||||||
$: iframeUrl = getIframeURL(app)
|
$: iframeUrl = getIframeURL(app)
|
||||||
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
|
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
|
||||||
|
|
||||||
|
let loading = true
|
||||||
|
|
||||||
const getIframeURL = app => {
|
const getIframeURL = app => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
if (app.status === "published") {
|
if (app.status === "published") {
|
||||||
return `/app${app.url}`
|
return `/app${app.url}`
|
||||||
}
|
}
|
||||||
|
@ -37,6 +48,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: fetchScreens(app?.devId)
|
$: fetchScreens(app?.devId)
|
||||||
|
|
||||||
|
const receiveMessage = async message => {
|
||||||
|
if (message.data.type === "docLoaded") {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener("message", receiveMessage)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
window.removeEventListener("message", receiveMessage)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
@ -108,7 +133,26 @@
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<iframe src={iframeUrl} title={app.name} />
|
<div
|
||||||
|
class:hide={!loading || !app?.features?.skeletonLoader}
|
||||||
|
class="loading"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={`loadingThemeWrapper ${getBaseTheme(app.theme)} ${app.theme}`}
|
||||||
|
>
|
||||||
|
<ClientAppSkeleton
|
||||||
|
noAnimation
|
||||||
|
hideDevTools={app?.status === "published"}
|
||||||
|
sideNav={app?.navigation.navigation === "Left"}
|
||||||
|
hideFooter={$licensing.brandingEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<iframe
|
||||||
|
class:hide={loading && app?.features?.skeletonLoader}
|
||||||
|
src={iframeUrl}
|
||||||
|
title={app.name}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -139,6 +183,23 @@
|
||||||
flex: 0 0 50px;
|
flex: 0 0 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
border-radius: var(--spacing-s);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.loadingThemeWrapper {
|
||||||
|
height: 100%;
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
visibility: hidden;
|
||||||
|
height: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
border-radius: var(--spacing-s);
|
border-radius: var(--spacing-s);
|
||||||
|
|
67
packages/builder/src/stores/builder/componentTreeNodes.js
Normal file
67
packages/builder/src/stores/builder/componentTreeNodes.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { createSessionStorageStore } from "@budibase/frontend-core"
|
||||||
|
import { selectedScreen as selectedScreenStore } from "./screens"
|
||||||
|
import { findComponentPath } from "helpers/components"
|
||||||
|
|
||||||
|
const baseStore = createSessionStorageStore("openNodes", {})
|
||||||
|
|
||||||
|
const toggleNode = componentId => {
|
||||||
|
baseStore.update(openNodes => {
|
||||||
|
openNodes[`nodeOpen-${componentId}`] = !openNodes[`nodeOpen-${componentId}`]
|
||||||
|
|
||||||
|
return openNodes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandNodes = componentIds => {
|
||||||
|
baseStore.update(openNodes => {
|
||||||
|
const newNodes = Object.fromEntries(
|
||||||
|
componentIds.map(id => [`nodeOpen-${id}`, true])
|
||||||
|
)
|
||||||
|
|
||||||
|
return { ...openNodes, ...newNodes }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const collapseNodes = componentIds => {
|
||||||
|
baseStore.update(openNodes => {
|
||||||
|
const newNodes = Object.fromEntries(
|
||||||
|
componentIds.map(id => [`nodeOpen-${id}`, false])
|
||||||
|
)
|
||||||
|
|
||||||
|
return { ...openNodes, ...newNodes }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will ensure all parents of a node are expanded so that it is visible in the tree
|
||||||
|
const makeNodeVisible = componentId => {
|
||||||
|
const selectedScreen = get(selectedScreenStore)
|
||||||
|
|
||||||
|
const path = findComponentPath(selectedScreen.props, componentId)
|
||||||
|
|
||||||
|
const componentIds = path.map(component => component._id)
|
||||||
|
|
||||||
|
baseStore.update(openNodes => {
|
||||||
|
const newNodes = Object.fromEntries(
|
||||||
|
componentIds.map(id => [`nodeOpen-${id}`, true])
|
||||||
|
)
|
||||||
|
|
||||||
|
return { ...openNodes, ...newNodes }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNodeExpanded = componentId => {
|
||||||
|
const openNodes = get(baseStore)
|
||||||
|
return !!openNodes[`nodeOpen-${componentId}`]
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = {
|
||||||
|
subscribe: baseStore.subscribe,
|
||||||
|
toggleNode,
|
||||||
|
expandNodes,
|
||||||
|
makeNodeVisible,
|
||||||
|
collapseNodes,
|
||||||
|
isNodeExpanded,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default store
|
|
@ -19,6 +19,7 @@ import {
|
||||||
appStore,
|
appStore,
|
||||||
previewStore,
|
previewStore,
|
||||||
tables,
|
tables,
|
||||||
|
componentTreeNodesStore,
|
||||||
} from "stores/builder/index"
|
} from "stores/builder/index"
|
||||||
import { buildFormSchema, getSchemaForDatasource } from "dataBinding"
|
import { buildFormSchema, getSchemaForDatasource } from "dataBinding"
|
||||||
import {
|
import {
|
||||||
|
@ -29,7 +30,6 @@ import {
|
||||||
} from "constants/backend"
|
} from "constants/backend"
|
||||||
import BudiStore from "../BudiStore"
|
import BudiStore from "../BudiStore"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
|
|
||||||
|
|
||||||
export const INITIAL_COMPONENTS_STATE = {
|
export const INITIAL_COMPONENTS_STATE = {
|
||||||
components: {},
|
components: {},
|
||||||
|
@ -279,12 +279,10 @@ export class ComponentStore extends BudiStore {
|
||||||
else {
|
else {
|
||||||
if (setting.type === "dataProvider") {
|
if (setting.type === "dataProvider") {
|
||||||
// Validate data provider exists, or else clear it
|
// Validate data provider exists, or else clear it
|
||||||
const treeId = parent?._id || component._id
|
const providers = findAllMatchingComponents(
|
||||||
const path = findComponentPath(screen?.props, treeId)
|
screen?.props,
|
||||||
const providers = path.filter(component =>
|
x => x._component === "@budibase/standard-components/dataprovider"
|
||||||
component._component?.endsWith("/dataprovider")
|
|
||||||
)
|
)
|
||||||
// Validate non-empty values
|
|
||||||
const valid = providers?.some(dp => value.includes?.(dp._id))
|
const valid = providers?.some(dp => value.includes?.(dp._id))
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
if (providers.length) {
|
if (providers.length) {
|
||||||
|
@ -653,8 +651,11 @@ export class ComponentStore extends BudiStore {
|
||||||
this.update(state => {
|
this.update(state => {
|
||||||
state.selectedScreenId = targetScreenId
|
state.selectedScreenId = targetScreenId
|
||||||
state.selectedComponentId = newComponentId
|
state.selectedComponentId = newComponentId
|
||||||
|
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
||||||
|
componentTreeNodesStore.makeNodeVisible(newComponentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
getPrevious() {
|
getPrevious() {
|
||||||
|
@ -663,7 +664,6 @@ export class ComponentStore extends BudiStore {
|
||||||
const screen = get(selectedScreen)
|
const screen = get(selectedScreen)
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
const index = parent?._children.findIndex(x => x._id === componentId)
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
const componentTreeNodes = get(componentTreeNodesStore)
|
|
||||||
|
|
||||||
// Check for screen and navigation component edge cases
|
// Check for screen and navigation component edge cases
|
||||||
const screenComponentId = `${screen._id}-screen`
|
const screenComponentId = `${screen._id}-screen`
|
||||||
|
@ -680,16 +680,16 @@ export class ComponentStore extends BudiStore {
|
||||||
|
|
||||||
// If we have siblings above us, choose the sibling or a descendant
|
// If we have siblings above us, choose the sibling or a descendant
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
// If sibling before us accepts children, select a descendant
|
// If sibling before us accepts children, and is not collapsed, select a descendant
|
||||||
const previousSibling = parent._children[index - 1]
|
const previousSibling = parent._children[index - 1]
|
||||||
if (
|
if (
|
||||||
previousSibling._children?.length &&
|
previousSibling._children?.length &&
|
||||||
componentTreeNodes[`nodeOpen-${previousSibling._id}`]
|
componentTreeNodesStore.isNodeExpanded(previousSibling._id)
|
||||||
) {
|
) {
|
||||||
let target = previousSibling
|
let target = previousSibling
|
||||||
while (
|
while (
|
||||||
target._children?.length &&
|
target._children?.length &&
|
||||||
componentTreeNodes[`nodeOpen-${target._id}`]
|
componentTreeNodesStore.isNodeExpanded(target._id)
|
||||||
) {
|
) {
|
||||||
target = target._children[target._children.length - 1]
|
target = target._children[target._children.length - 1]
|
||||||
}
|
}
|
||||||
|
@ -711,7 +711,6 @@ export class ComponentStore extends BudiStore {
|
||||||
const screen = get(selectedScreen)
|
const screen = get(selectedScreen)
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
const index = parent?._children.findIndex(x => x._id === componentId)
|
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||||
const componentTreeNodes = get(componentTreeNodesStore)
|
|
||||||
|
|
||||||
// Check for screen and navigation component edge cases
|
// Check for screen and navigation component edge cases
|
||||||
const screenComponentId = `${screen._id}-screen`
|
const screenComponentId = `${screen._id}-screen`
|
||||||
|
@ -720,11 +719,11 @@ export class ComponentStore extends BudiStore {
|
||||||
return navComponentId
|
return navComponentId
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have children, select first child
|
// If we have children, select first child, and the node is not collapsed
|
||||||
if (
|
if (
|
||||||
component._children?.length &&
|
component._children?.length &&
|
||||||
(state.selectedComponentId === navComponentId ||
|
(state.selectedComponentId === navComponentId ||
|
||||||
componentTreeNodes[`nodeOpen-${component._id}`])
|
componentTreeNodesStore.isNodeExpanded(component._id))
|
||||||
) {
|
) {
|
||||||
return component._children[0]._id
|
return component._children[0]._id
|
||||||
} else if (!parent) {
|
} else if (!parent) {
|
||||||
|
@ -803,7 +802,10 @@ export class ComponentStore extends BudiStore {
|
||||||
// sibling
|
// sibling
|
||||||
const previousSibling = parent._children[index - 1]
|
const previousSibling = parent._children[index - 1]
|
||||||
const definition = this.getDefinition(previousSibling._component)
|
const definition = this.getDefinition(previousSibling._component)
|
||||||
if (definition.hasChildren) {
|
if (
|
||||||
|
definition.hasChildren &&
|
||||||
|
componentTreeNodesStore.isNodeExpanded(previousSibling._id)
|
||||||
|
) {
|
||||||
previousSibling._children.push(originalComponent)
|
previousSibling._children.push(originalComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -852,10 +854,13 @@ export class ComponentStore extends BudiStore {
|
||||||
|
|
||||||
// Move below the next sibling if we are not the last sibling
|
// Move below the next sibling if we are not the last sibling
|
||||||
if (index < parent._children.length) {
|
if (index < parent._children.length) {
|
||||||
// If the next sibling has children, become the first child
|
// If the next sibling has children, and is not collapsed, become the first child
|
||||||
const nextSibling = parent._children[index]
|
const nextSibling = parent._children[index]
|
||||||
const definition = this.getDefinition(nextSibling._component)
|
const definition = this.getDefinition(nextSibling._component)
|
||||||
if (definition.hasChildren) {
|
if (
|
||||||
|
definition.hasChildren &&
|
||||||
|
componentTreeNodesStore.isNodeExpanded(nextSibling._id)
|
||||||
|
) {
|
||||||
nextSibling._children.splice(0, 0, originalComponent)
|
nextSibling._children.splice(0, 0, originalComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1151,13 +1156,3 @@ export const selectedComponent = derived(
|
||||||
return clone
|
return clone
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const selectedComponentPath = derived(
|
|
||||||
[componentStore, selectedScreen],
|
|
||||||
([$store, $selectedScreen]) => {
|
|
||||||
return findComponentPath(
|
|
||||||
$selectedScreen?.props,
|
|
||||||
$store.selectedComponentId
|
|
||||||
).map(component => component._id)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
|
@ -7,12 +7,25 @@ export const INITIAL_HOVER_STATE = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HoverStore extends BudiStore {
|
export class HoverStore extends BudiStore {
|
||||||
|
hoverTimeout
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ ...INITIAL_HOVER_STATE })
|
super({ ...INITIAL_HOVER_STATE })
|
||||||
this.hover = this.hover.bind(this)
|
this.hover = this.hover.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
hover(componentId, notifyClient = true) {
|
hover(componentId, notifyClient = true) {
|
||||||
|
clearTimeout(this.hoverTimeout)
|
||||||
|
if (componentId) {
|
||||||
|
this.processHover(componentId, notifyClient)
|
||||||
|
} else {
|
||||||
|
this.hoverTimeout = setTimeout(() => {
|
||||||
|
this.processHover(componentId, notifyClient)
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processHover(componentId, notifyClient) {
|
||||||
if (componentId === get(this.store).componentId) {
|
if (componentId === get(this.store).componentId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { layoutStore } from "./layouts.js"
|
import { layoutStore } from "./layouts.js"
|
||||||
import { appStore } from "./app.js"
|
import { appStore } from "./app.js"
|
||||||
import {
|
import { componentStore, selectedComponent } from "./components"
|
||||||
componentStore,
|
|
||||||
selectedComponent,
|
|
||||||
selectedComponentPath,
|
|
||||||
} from "./components"
|
|
||||||
import { navigationStore } from "./navigation.js"
|
import { navigationStore } from "./navigation.js"
|
||||||
import { themeStore } from "./theme.js"
|
import { themeStore } from "./theme.js"
|
||||||
import { screenStore, selectedScreen, sortedScreens } from "./screens.js"
|
import { screenStore, selectedScreen, sortedScreens } from "./screens.js"
|
||||||
|
@ -31,8 +27,10 @@ import { integrations } from "./integrations"
|
||||||
import { sortedIntegrations } from "./sortedIntegrations"
|
import { sortedIntegrations } from "./sortedIntegrations"
|
||||||
import { queries } from "./queries"
|
import { queries } from "./queries"
|
||||||
import { flags } from "./flags"
|
import { flags } from "./flags"
|
||||||
|
import componentTreeNodesStore from "./componentTreeNodes"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
componentTreeNodesStore,
|
||||||
layoutStore,
|
layoutStore,
|
||||||
appStore,
|
appStore,
|
||||||
componentStore,
|
componentStore,
|
||||||
|
@ -51,7 +49,6 @@ export {
|
||||||
isOnlyUser,
|
isOnlyUser,
|
||||||
deploymentStore,
|
deploymentStore,
|
||||||
selectedComponent,
|
selectedComponent,
|
||||||
selectedComponentPath,
|
|
||||||
tables,
|
tables,
|
||||||
views,
|
views,
|
||||||
viewsV2,
|
viewsV2,
|
||||||
|
|
|
@ -11,6 +11,7 @@ export const INITIAL_NAVIGATION_STATE = {
|
||||||
hideLogo: null,
|
hideLogo: null,
|
||||||
logoUrl: null,
|
logoUrl: null,
|
||||||
hideTitle: null,
|
hideTitle: null,
|
||||||
|
textAlign: "Left",
|
||||||
navBackground: null,
|
navBackground: null,
|
||||||
navWidth: null,
|
navWidth: null,
|
||||||
navTextColor: null,
|
navTextColor: null,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
import { getBaseTheme } from "@budibase/frontend-core"
|
||||||
|
|
||||||
const INITIAL_THEMES_STATE = {
|
const INITIAL_THEMES_STATE = {
|
||||||
theme: "",
|
theme: "",
|
||||||
|
@ -12,11 +13,15 @@ export const themes = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const syncAppTheme = app => {
|
const syncAppTheme = app => {
|
||||||
store.update(state => ({
|
store.update(state => {
|
||||||
...state,
|
const theme = app.theme || "spectrum--light"
|
||||||
theme: app.theme || "spectrum--light",
|
return {
|
||||||
customTheme: app.customTheme,
|
...state,
|
||||||
}))
|
theme,
|
||||||
|
baseTheme: getBaseTheme(theme),
|
||||||
|
customTheme: app.customTheme,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = async (theme, appId) => {
|
const save = async (theme, appId) => {
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { createSessionStorageStore } from "@budibase/frontend-core"
|
|
||||||
|
|
||||||
const baseStore = createSessionStorageStore("openNodes", {})
|
|
||||||
|
|
||||||
const toggleNode = componentId => {
|
|
||||||
baseStore.update(openNodes => {
|
|
||||||
openNodes[`nodeOpen-${componentId}`] = !openNodes[`nodeOpen-${componentId}`]
|
|
||||||
|
|
||||||
return openNodes
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const expandNode = componentId => {
|
|
||||||
baseStore.update(openNodes => {
|
|
||||||
openNodes[`nodeOpen-${componentId}`] = true
|
|
||||||
|
|
||||||
return openNodes
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const collapseNode = componentId => {
|
|
||||||
baseStore.update(openNodes => {
|
|
||||||
openNodes[`nodeOpen-${componentId}`] = false
|
|
||||||
|
|
||||||
return openNodes
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = {
|
|
||||||
subscribe: baseStore.subscribe,
|
|
||||||
toggleNode,
|
|
||||||
expandNode,
|
|
||||||
collapseNode,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default store
|
|
158
packages/builder/src/templates/gridDetailsScreen.js
Normal file
158
packages/builder/src/templates/gridDetailsScreen.js
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
import sanitizeUrl from "helpers/sanitizeUrl"
|
||||||
|
import { Screen } from "./Screen"
|
||||||
|
import { Component } from "./Component"
|
||||||
|
import { generate } from "shortid"
|
||||||
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
export default function (datasources) {
|
||||||
|
if (!Array.isArray(datasources)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return datasources.map(datasource => {
|
||||||
|
return {
|
||||||
|
name: `${datasource.label} - List with panel`,
|
||||||
|
create: () => createScreen(datasource),
|
||||||
|
id: GRID_DETAILS_TEMPLATE,
|
||||||
|
resourceId: datasource.resourceId,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GRID_DETAILS_TEMPLATE = "GRID_DETAILS_TEMPLATE"
|
||||||
|
export const gridDetailsUrl = datasource => sanitizeUrl(`/${datasource.label}`)
|
||||||
|
|
||||||
|
const createScreen = datasource => {
|
||||||
|
/*
|
||||||
|
Create Row
|
||||||
|
*/
|
||||||
|
const createRowSidePanel = new Component(
|
||||||
|
"@budibase/standard-components/sidepanel"
|
||||||
|
).instanceName("New row side panel")
|
||||||
|
|
||||||
|
const buttonGroup = new Component("@budibase/standard-components/buttongroup")
|
||||||
|
const createButton = new Component("@budibase/standard-components/button")
|
||||||
|
|
||||||
|
createButton.customProps({
|
||||||
|
onClick: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
"##eventHandlerType": "Open Side Panel",
|
||||||
|
parameters: {
|
||||||
|
id: createRowSidePanel._json._id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
text: "Create row",
|
||||||
|
type: "cta",
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonGroup.instanceName(`${datasource.label} - Create`).customProps({
|
||||||
|
hAlign: "right",
|
||||||
|
buttons: [createButton.json()],
|
||||||
|
})
|
||||||
|
|
||||||
|
const gridHeader = new Component("@budibase/standard-components/container")
|
||||||
|
.instanceName("Heading container")
|
||||||
|
.customProps({
|
||||||
|
direction: "row",
|
||||||
|
hAlign: "stretch",
|
||||||
|
})
|
||||||
|
|
||||||
|
const heading = new Component("@budibase/standard-components/heading")
|
||||||
|
.instanceName("Table heading")
|
||||||
|
.customProps({
|
||||||
|
text: datasource?.label,
|
||||||
|
})
|
||||||
|
|
||||||
|
gridHeader.addChild(heading)
|
||||||
|
gridHeader.addChild(buttonGroup)
|
||||||
|
|
||||||
|
const createFormBlock = new Component(
|
||||||
|
"@budibase/standard-components/formblock"
|
||||||
|
)
|
||||||
|
createFormBlock.instanceName("Create row form block").customProps({
|
||||||
|
dataSource: datasource,
|
||||||
|
labelPosition: "left",
|
||||||
|
buttonPosition: "top",
|
||||||
|
actionType: "Create",
|
||||||
|
title: "Create row",
|
||||||
|
buttons: Utils.buildFormBlockButtonConfig({
|
||||||
|
_id: createFormBlock._json._id,
|
||||||
|
showDeleteButton: false,
|
||||||
|
showSaveButton: true,
|
||||||
|
saveButtonLabel: "Save",
|
||||||
|
actionType: "Create",
|
||||||
|
dataSource: datasource,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
createRowSidePanel.addChild(createFormBlock)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Edit Row
|
||||||
|
*/
|
||||||
|
const stateKey = `ID_${generate()}`
|
||||||
|
const detailsSidePanel = new Component(
|
||||||
|
"@budibase/standard-components/sidepanel"
|
||||||
|
).instanceName("Edit row side panel")
|
||||||
|
|
||||||
|
const editFormBlock = new Component("@budibase/standard-components/formblock")
|
||||||
|
editFormBlock.instanceName("Edit row form block").customProps({
|
||||||
|
dataSource: datasource,
|
||||||
|
labelPosition: "left",
|
||||||
|
buttonPosition: "top",
|
||||||
|
actionType: "Update",
|
||||||
|
title: "Edit",
|
||||||
|
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
|
||||||
|
buttons: Utils.buildFormBlockButtonConfig({
|
||||||
|
_id: editFormBlock._json._id,
|
||||||
|
showDeleteButton: true,
|
||||||
|
showSaveButton: true,
|
||||||
|
saveButtonLabel: "Save",
|
||||||
|
deleteButtonLabel: "Delete",
|
||||||
|
actionType: "Update",
|
||||||
|
dataSource: datasource,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
detailsSidePanel.addChild(editFormBlock)
|
||||||
|
|
||||||
|
const gridBlock = new Component("@budibase/standard-components/gridblock")
|
||||||
|
gridBlock
|
||||||
|
.customProps({
|
||||||
|
table: datasource,
|
||||||
|
allowAddRows: false,
|
||||||
|
allowEditRows: false,
|
||||||
|
allowDeleteRows: false,
|
||||||
|
onRowClick: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
"##eventHandlerType": "Update State",
|
||||||
|
parameters: {
|
||||||
|
key: stateKey,
|
||||||
|
type: "set",
|
||||||
|
persist: false,
|
||||||
|
value: `{{ ${safe("eventContext")}.${safe("row")}._id }}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
"##eventHandlerType": "Open Side Panel",
|
||||||
|
parameters: {
|
||||||
|
id: detailsSidePanel._json._id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.instanceName(`${datasource.label} - Table`)
|
||||||
|
|
||||||
|
return new Screen()
|
||||||
|
.route(gridDetailsUrl(datasource))
|
||||||
|
.instanceName(`${datasource.label} - List and details`)
|
||||||
|
.addChild(gridHeader)
|
||||||
|
.addChild(gridBlock)
|
||||||
|
.addChild(createRowSidePanel)
|
||||||
|
.addChild(detailsSidePanel)
|
||||||
|
.json()
|
||||||
|
}
|
41
packages/builder/src/templates/gridListScreen.js
Normal file
41
packages/builder/src/templates/gridListScreen.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import sanitizeUrl from "helpers/sanitizeUrl"
|
||||||
|
import { Screen } from "./Screen"
|
||||||
|
import { Component } from "./Component"
|
||||||
|
|
||||||
|
export default function (datasources) {
|
||||||
|
if (!Array.isArray(datasources)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return datasources.map(datasource => {
|
||||||
|
return {
|
||||||
|
name: `${datasource.label} - List`,
|
||||||
|
create: () => createScreen(datasource),
|
||||||
|
id: GRID_LIST_TEMPLATE,
|
||||||
|
resourceId: datasource.resourceId,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GRID_LIST_TEMPLATE = "GRID_LIST_TEMPLATE"
|
||||||
|
export const gridListUrl = datasource => sanitizeUrl(`/${datasource.label}`)
|
||||||
|
|
||||||
|
const createScreen = datasource => {
|
||||||
|
const heading = new Component("@budibase/standard-components/heading")
|
||||||
|
.instanceName("Table heading")
|
||||||
|
.customProps({
|
||||||
|
text: datasource?.label,
|
||||||
|
})
|
||||||
|
|
||||||
|
const gridBlock = new Component("@budibase/standard-components/gridblock")
|
||||||
|
.instanceName(`${datasource.label} - Table`)
|
||||||
|
.customProps({
|
||||||
|
table: datasource,
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Screen()
|
||||||
|
.route(gridListUrl(datasource))
|
||||||
|
.instanceName(`${datasource.label} - List`)
|
||||||
|
.addChild(heading)
|
||||||
|
.addChild(gridBlock)
|
||||||
|
.json()
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
import rowListScreen from "./rowListScreen"
|
import gridListScreen from "./gridListScreen"
|
||||||
|
import gridDetailsScreen from "./gridDetailsScreen"
|
||||||
import createFromScratchScreen from "./createFromScratchScreen"
|
import createFromScratchScreen from "./createFromScratchScreen"
|
||||||
import formScreen from "./formScreen"
|
import formScreen from "./formScreen"
|
||||||
|
|
||||||
const allTemplates = datasources => [
|
const allTemplates = datasources => [
|
||||||
...rowListScreen(datasources),
|
...gridListScreen(datasources),
|
||||||
|
...gridDetailsScreen(datasources),
|
||||||
...formScreen(datasources),
|
...formScreen(datasources),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
import sanitizeUrl from "helpers/sanitizeUrl"
|
|
||||||
import { Screen } from "./Screen"
|
|
||||||
import { Component } from "./Component"
|
|
||||||
|
|
||||||
export default function (datasources, mode = "table") {
|
|
||||||
if (!Array.isArray(datasources)) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return datasources.map(datasource => {
|
|
||||||
return {
|
|
||||||
name: `${datasource.label} - List`,
|
|
||||||
create: () => createScreen(datasource, mode),
|
|
||||||
id: ROW_LIST_TEMPLATE,
|
|
||||||
resourceId: datasource.resourceId,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ROW_LIST_TEMPLATE = "ROW_LIST_TEMPLATE"
|
|
||||||
export const rowListUrl = datasource => sanitizeUrl(`/${datasource.label}`)
|
|
||||||
|
|
||||||
const generateTableBlock = datasource => {
|
|
||||||
const tableBlock = new Component("@budibase/standard-components/tableblock")
|
|
||||||
tableBlock
|
|
||||||
.customProps({
|
|
||||||
title: datasource.label,
|
|
||||||
dataSource: datasource,
|
|
||||||
sortOrder: "Ascending",
|
|
||||||
size: "spectrum--medium",
|
|
||||||
paginate: true,
|
|
||||||
rowCount: 8,
|
|
||||||
clickBehaviour: "details",
|
|
||||||
showTitleButton: true,
|
|
||||||
titleButtonText: "Create row",
|
|
||||||
titleButtonClickBehaviour: "new",
|
|
||||||
sidePanelSaveLabel: "Save",
|
|
||||||
sidePanelDeleteLabel: "Delete",
|
|
||||||
})
|
|
||||||
.instanceName(`${datasource.label} - Table block`)
|
|
||||||
return tableBlock
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateGridBlock = datasource => {
|
|
||||||
const gridBlock = new Component("@budibase/standard-components/gridblock")
|
|
||||||
gridBlock
|
|
||||||
.customProps({
|
|
||||||
table: datasource,
|
|
||||||
})
|
|
||||||
.instanceName(`${datasource.label} - Grid block`)
|
|
||||||
return gridBlock
|
|
||||||
}
|
|
||||||
|
|
||||||
const createScreen = (datasource, mode) => {
|
|
||||||
return new Screen()
|
|
||||||
.route(rowListUrl(datasource))
|
|
||||||
.instanceName(`${datasource.label} - List`)
|
|
||||||
.addChild(
|
|
||||||
mode === "table"
|
|
||||||
? generateTableBlock(datasource)
|
|
||||||
: generateGridBlock(datasource)
|
|
||||||
)
|
|
||||||
.json()
|
|
||||||
}
|
|
|
@ -4,6 +4,16 @@
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": "."
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"assets/*": ["./assets/*"],
|
||||||
|
"@budibase/*": [
|
||||||
|
"../*/src/index.ts",
|
||||||
|
"../*/src/index.js",
|
||||||
|
"../*",
|
||||||
|
"../../node_modules/@budibase/*"
|
||||||
|
],
|
||||||
|
"*": ["./src/*"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"types": ["node", "jest"],
|
"types": ["node", "jest"],
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@budibase/types": ["../types/src"],
|
"@budibase/types": ["../types/src"],
|
||||||
"@budibase/backend-core": ["../backend-core/src"],
|
"@budibase/backend-core": ["../backend-core/src"],
|
||||||
|
|
|
@ -10,7 +10,8 @@
|
||||||
"rowSelection": true,
|
"rowSelection": true,
|
||||||
"continueIfAction": true,
|
"continueIfAction": true,
|
||||||
"showNotificationAction": true,
|
"showNotificationAction": true,
|
||||||
"sidePanel": true
|
"sidePanel": true,
|
||||||
|
"skeletonLoader": true
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
"name": "Layout",
|
"name": "Layout",
|
||||||
|
@ -4673,6 +4674,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
"deprecated": true,
|
||||||
"name": "Table",
|
"name": "Table",
|
||||||
"icon": "Table",
|
"icon": "Table",
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
|
@ -5418,6 +5420,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"tableblock": {
|
"tableblock": {
|
||||||
|
"deprecated": true,
|
||||||
"block": true,
|
"block": true,
|
||||||
"name": "Table Block",
|
"name": "Table Block",
|
||||||
"icon": "Table",
|
"icon": "Table",
|
||||||
|
@ -6595,7 +6598,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"gridblock": {
|
"gridblock": {
|
||||||
"name": "Grid Block",
|
"name": "Table",
|
||||||
"icon": "Table",
|
"icon": "Table",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
"size": {
|
"size": {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue