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/ui/.routify
|
||||
packages/account-portal/packages/ui/build
|
||||
**/*.ivm.bundle.js
|
||||
**/*.ivm.bundle.js
|
||||
packages/server/build/oldClientVersions/**/**
|
||||
|
|
|
@ -34,18 +34,43 @@
|
|||
},
|
||||
{
|
||||
"files": ["**/*.ts"],
|
||||
"excludedFiles": ["qa-core/**"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["eslint:recommended"],
|
||||
"globals": {
|
||||
"NodeJS": true
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"no-inner-declarations": "off",
|
||||
"no-case-declarations": "off",
|
||||
"no-useless-escape": "off",
|
||||
"no-undef": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"local-rules/no-budibase-imports": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"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/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
|
||||
- name: Build
|
||||
run: |
|
||||
yarn build
|
||||
yarn build:oss
|
||||
yarn build:account-portal
|
||||
# Check the types of the projects built via esbuild
|
||||
- name: Check types
|
||||
run: |
|
||||
|
@ -138,6 +139,8 @@ jobs:
|
|||
|
||||
test-server:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DEBUG: testcontainers,testcontainers:exec,testcontainers:build,testcontainers:pull
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
@ -151,7 +154,19 @@ jobs:
|
|||
with:
|
||||
node-version: 20.x
|
||||
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
|
||||
|
||||
- name: Test server
|
||||
run: |
|
||||
if ${{ env.USE_NX_AFFECTED }}; then
|
||||
|
@ -217,27 +232,34 @@ jobs:
|
|||
echo "pro_commit=$pro_commit"
|
||||
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
|
||||
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
|
||||
echo "Nothing to do - branch to branch merge."
|
||||
fi
|
||||
|
||||
- name: Check submodule merged to base branch
|
||||
if: ${{ steps.get_pro_commits.outputs.base_commit != '' }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const submoduleCommit = '${{ steps.get_pro_commits.outputs.pro_commit }}';
|
||||
const baseCommit = '${{ steps.get_pro_commits.outputs.base_commit }}';
|
||||
- name: Check submodule merged and latest on base branch
|
||||
if: ${{ steps.get_pro_commits.outputs.base_commit_excluding_merges != '' }}
|
||||
run: |
|
||||
cd packages/pro
|
||||
base_commit_excluding_merges='${{ steps.get_pro_commits.outputs.base_commit_excluding_merges }}'
|
||||
pro_commit='${{ steps.get_pro_commits.outputs.pro_commit }}'
|
||||
|
||||
if (submoduleCommit !== baseCommit) {
|
||||
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')
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('All good, the submodule had been merged and setup correctly!')
|
||||
}
|
||||
any_commit=$(git log --no-merges $base_commit_excluding_merges...$pro_commit)
|
||||
|
||||
if [ -n "$any_commit" ]; then
|
||||
echo $any_commit
|
||||
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -250,7 +272,15 @@ jobs:
|
|||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
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
|
||||
run: |
|
||||
cd packages/account-portal
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -5,6 +5,9 @@ packages/server/runtime_apps/
|
|||
bb-airgapped.tar.gz
|
||||
*.iml
|
||||
|
||||
packages/server/build/oldClientVersions/**/*
|
||||
packages/builder/src/components/deploy/clientVersions.json
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
@ -107,3 +110,4 @@ budibase-component
|
|||
budibase-datasource
|
||||
|
||||
*.iml
|
||||
.nx
|
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
|
@ -1,4 +1,3 @@
|
|||
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
|
@ -20,6 +19,13 @@
|
|||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
|
||||
"args": ["${workspaceFolder}/packages/worker/src/index.ts"],
|
||||
"cwd": "${workspaceFolder}/packages/worker"
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:10000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
|
|
|
@ -140,7 +140,7 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml
|
|||
| ingress.className | string | `""` | What ingress class to use. |
|
||||
| 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. |
|
||||
| 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.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 |
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -- Passed to all pods created by this chart. Should not ordinarily need to be changed.
|
||||
imagePullSecrets: []
|
||||
# -- Override the name of the deploymen. Defaults to {{ .Chart.Name }}.
|
||||
# -- Override the name of the deployment. Defaults to {{ .Chart.Name }}.
|
||||
nameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
|
|
|
@ -25,11 +25,9 @@ module.exports = {
|
|||
docs: {
|
||||
description:
|
||||
"disallow the use of 'test.com' in strings and replace it with 'example.com'",
|
||||
category: "Possible Errors",
|
||||
recommended: false,
|
||||
},
|
||||
schema: [], // no options
|
||||
fixable: "code", // Indicates that this rule supports automatic fixing
|
||||
schema: [],
|
||||
fixable: "code",
|
||||
},
|
||||
create: function (context) {
|
||||
return {
|
||||
|
@ -58,8 +56,6 @@ module.exports = {
|
|||
docs: {
|
||||
description:
|
||||
"enforce using the example.com domain for generator.email calls",
|
||||
category: "Possible Errors",
|
||||
recommended: false,
|
||||
},
|
||||
fixable: "code",
|
||||
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",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"esbuild-node-externals": "^1.8.0",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-jest": "^27.9.0",
|
||||
"eslint-plugin-local-rules": "^2.0.0",
|
||||
"eslint-plugin-svelte": "^2.34.0",
|
||||
"husky": "^8.0.3",
|
||||
|
@ -25,12 +26,16 @@
|
|||
"svelte": "^4.2.10",
|
||||
"svelte-eslint-parser": "^0.33.1",
|
||||
"typescript": "5.2.2",
|
||||
"typescript-eslint": "^7.3.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"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",
|
||||
"check:types": "lerna run check:types",
|
||||
"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"
|
||||
|
||||
const baseConfig: Config.InitialProjectOptions = {
|
||||
preset: "@trendyol/jest-testcontainers",
|
||||
setupFiles: ["./tests/jestEnv.ts"],
|
||||
globalSetup: "./../../globalSetup.ts",
|
||||
setupFilesAfterEnv: ["./tests/jestSetup.ts"],
|
||||
transform: {
|
||||
"^.+\\.ts?$": "@swc/jest",
|
||||
|
|
|
@ -60,7 +60,6 @@
|
|||
"@shopify/jest-koa-mocks": "5.1.1",
|
||||
"@swc/core": "1.3.71",
|
||||
"@swc/jest": "0.2.27",
|
||||
"@trendyol/jest-testcontainers": "^2.1.1",
|
||||
"@types/chance": "1.1.3",
|
||||
"@types/cookies": "0.7.8",
|
||||
"@types/jest": "29.5.5",
|
||||
|
|
|
@ -4,10 +4,10 @@ set -e
|
|||
if [[ -n $CI ]]
|
||||
then
|
||||
# --runInBand performs better in ci where resources are limited
|
||||
echo "jest --coverage --runInBand --forceExit"
|
||||
jest --coverage --runInBand --forceExit
|
||||
echo "jest --coverage --runInBand --forceExit $@"
|
||||
jest --coverage --runInBand --forceExit $@
|
||||
else
|
||||
# --maxWorkers performs better in development
|
||||
echo "jest --coverage --detectOpenHandles"
|
||||
jest --coverage --detectOpenHandles
|
||||
echo "jest --coverage --forceExit --detectOpenHandles $@"
|
||||
jest --coverage --forceExit --detectOpenHandles $@
|
||||
fi
|
|
@ -133,7 +133,7 @@ export async function refreshOAuthToken(
|
|||
configId?: string
|
||||
): Promise<RefreshResponse> {
|
||||
switch (providerType) {
|
||||
case SSOProviderType.OIDC:
|
||||
case SSOProviderType.OIDC: {
|
||||
if (!configId) {
|
||||
return { err: { data: "OIDC config id not provided" } }
|
||||
}
|
||||
|
@ -142,12 +142,14 @@ export async function refreshOAuthToken(
|
|||
return { err: { data: "OIDC configuration not found" } }
|
||||
}
|
||||
return refreshOIDCAccessToken(oidcConfig, refreshToken)
|
||||
case SSOProviderType.GOOGLE:
|
||||
}
|
||||
case SSOProviderType.GOOGLE: {
|
||||
let googleConfig = await configs.getGoogleConfig()
|
||||
if (!googleConfig) {
|
||||
return { err: { data: "Google configuration not found" } }
|
||||
}
|
||||
return refreshGoogleAccessToken(googleConfig, refreshToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ describe("platformLogout", () => {
|
|||
await testEnv.withTenant(async () => {
|
||||
const ctx = structures.koa.newContext()
|
||||
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()
|
||||
try {
|
||||
await client.delete(generateTenantKey(key))
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AnyDocument, Database } from "@budibase/types"
|
||||
|
||||
import { JobQueue, createQueue } from "../queue"
|
||||
import { JobQueue, Queue, createQueue } from "../queue"
|
||||
import * as dbUtils from "../db"
|
||||
|
||||
interface ProcessDocMessage {
|
||||
|
@ -12,18 +12,26 @@ interface ProcessDocMessage {
|
|||
const PERSIST_MAX_ATTEMPTS = 100
|
||||
let processor: DocWritethroughProcessor | undefined
|
||||
|
||||
export const docWritethroughProcessorQueue = createQueue<ProcessDocMessage>(
|
||||
JobQueue.DOC_WRITETHROUGH_QUEUE,
|
||||
{
|
||||
jobOptions: {
|
||||
attempts: PERSIST_MAX_ATTEMPTS,
|
||||
},
|
||||
}
|
||||
)
|
||||
export class DocWritethroughProcessor {
|
||||
private static _queue: Queue
|
||||
|
||||
public static get queue() {
|
||||
if (!DocWritethroughProcessor._queue) {
|
||||
DocWritethroughProcessor._queue = createQueue<ProcessDocMessage>(
|
||||
JobQueue.DOC_WRITETHROUGH_QUEUE,
|
||||
{
|
||||
jobOptions: {
|
||||
attempts: PERSIST_MAX_ATTEMPTS,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return DocWritethroughProcessor._queue
|
||||
}
|
||||
|
||||
class DocWritethroughProcessor {
|
||||
init() {
|
||||
docWritethroughProcessorQueue.process(async message => {
|
||||
DocWritethroughProcessor.queue.process(async message => {
|
||||
try {
|
||||
await this.persistToDb(message.data)
|
||||
} catch (err: any) {
|
||||
|
@ -76,7 +84,7 @@ export class DocWritethrough {
|
|||
}
|
||||
|
||||
async patch(data: Record<string, any>) {
|
||||
await docWritethroughProcessorQueue.add({
|
||||
await DocWritethroughProcessor.queue.add({
|
||||
dbName: this.db.name,
|
||||
docId: this.docId,
|
||||
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 { Duration, DurationType } from "../utils"
|
||||
import { Duration } from "../utils"
|
||||
import env from "../environment"
|
||||
import { getTenantId } from "../context"
|
||||
import * as redis from "../redis/init"
|
||||
|
|
|
@ -6,7 +6,7 @@ import { getDB } from "../../db"
|
|||
|
||||
import {
|
||||
DocWritethrough,
|
||||
docWritethroughProcessorQueue,
|
||||
DocWritethroughProcessor,
|
||||
init,
|
||||
} from "../docWritethrough"
|
||||
|
||||
|
@ -15,7 +15,7 @@ import InMemoryQueue from "../../queue/inMemoryQueue"
|
|||
const initialTime = Date.now()
|
||||
|
||||
async function waitForQueueCompletion() {
|
||||
const queue: InMemoryQueue = docWritethroughProcessorQueue as never
|
||||
const queue: InMemoryQueue = DocWritethroughProcessor.queue as never
|
||||
await queue.waitForCompletion()
|
||||
}
|
||||
|
||||
|
@ -235,11 +235,11 @@ describe("docWritethrough", () => {
|
|||
return acc
|
||||
}, {})
|
||||
}
|
||||
const queueMessageSpy = jest.spyOn(docWritethroughProcessorQueue, "add")
|
||||
const queueMessageSpy = jest.spyOn(DocWritethroughProcessor.queue, "add")
|
||||
|
||||
await config.doInTenant(async () => {
|
||||
let patches = await parallelPatch(5)
|
||||
expect(queueMessageSpy).toBeCalledTimes(5)
|
||||
expect(queueMessageSpy).toHaveBeenCalledTimes(5)
|
||||
|
||||
await waitForQueueCompletion()
|
||||
expect(await db.get(documentId)).toEqual(
|
||||
|
@ -247,7 +247,7 @@ describe("docWritethrough", () => {
|
|||
)
|
||||
|
||||
patches = { ...patches, ...(await parallelPatch(40)) }
|
||||
expect(queueMessageSpy).toBeCalledTimes(45)
|
||||
expect(queueMessageSpy).toHaveBeenCalledTimes(45)
|
||||
|
||||
await waitForQueueCompletion()
|
||||
expect(await db.get(documentId)).toEqual(
|
||||
|
@ -255,7 +255,7 @@ describe("docWritethrough", () => {
|
|||
)
|
||||
|
||||
patches = { ...patches, ...(await parallelPatch(10)) }
|
||||
expect(queueMessageSpy).toBeCalledTimes(55)
|
||||
expect(queueMessageSpy).toHaveBeenCalledTimes(55)
|
||||
|
||||
await waitForQueueCompletion()
|
||||
expect(await db.get(documentId)).toEqual(
|
||||
|
@ -265,6 +265,7 @@ describe("docWritethrough", () => {
|
|||
})
|
||||
|
||||
// This is not yet supported
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip("patches will execute in order", async () => {
|
||||
let incrementalValue = 0
|
||||
const keyToOverride = generator.word()
|
||||
|
|
|
@ -55,8 +55,8 @@ describe("user cache", () => {
|
|||
})),
|
||||
})
|
||||
|
||||
expect(UserDB.bulkGet).toBeCalledTimes(1)
|
||||
expect(UserDB.bulkGet).toBeCalledWith(userIdsToRequest)
|
||||
expect(UserDB.bulkGet).toHaveBeenCalledTimes(1)
|
||||
expect(UserDB.bulkGet).toHaveBeenCalledWith(userIdsToRequest)
|
||||
})
|
||||
|
||||
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 () => {
|
||||
|
@ -110,8 +110,8 @@ describe("user cache", () => {
|
|||
),
|
||||
})
|
||||
|
||||
expect(UserDB.bulkGet).toBeCalledTimes(1)
|
||||
expect(UserDB.bulkGet).toBeCalledWith([
|
||||
expect(UserDB.bulkGet).toHaveBeenCalledTimes(1)
|
||||
expect(UserDB.bulkGet).toHaveBeenCalledWith([
|
||||
userIdsToRequest[1],
|
||||
userIdsToRequest[2],
|
||||
userIdsToRequest[4],
|
||||
|
|
|
@ -8,7 +8,7 @@ const DEFAULT_WRITE_RATE_MS = 10000
|
|||
let CACHE: BaseCache | null = null
|
||||
|
||||
interface CacheItem<T extends Document> {
|
||||
doc: any
|
||||
doc: T
|
||||
lastWrite: number
|
||||
}
|
||||
|
||||
|
|
|
@ -246,7 +246,7 @@ describe("context", () => {
|
|||
context.doInAppMigrationContext(db.generateAppID(), async () => {
|
||||
await otherContextCall()
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
).rejects.toThrow(
|
||||
"The context cannot be changed, a migration is currently running"
|
||||
)
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ class Replication {
|
|||
return resolve(info)
|
||||
})
|
||||
.on("error", function (err) {
|
||||
throw new Error(`Replication Error: ${err}`)
|
||||
throw err
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,10 +10,6 @@ interface SearchResponse<T> {
|
|||
totalRows: number
|
||||
}
|
||||
|
||||
interface PaginatedSearchResponse<T> extends SearchResponse<T> {
|
||||
hasNextPage: boolean
|
||||
}
|
||||
|
||||
export type SearchParams<T> = {
|
||||
tableId?: string
|
||||
sort?: string
|
||||
|
@ -247,7 +243,7 @@ export class QueryBuilder<T> {
|
|||
}
|
||||
// Escape characters
|
||||
if (!this.#noEscaping && escape && originalType === "string") {
|
||||
value = `${value}`.replace(/[ \/#+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
|
||||
value = `${value}`.replace(/[ /#+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
|
||||
}
|
||||
|
||||
// Wrap in quotes
|
||||
|
|
|
@ -34,12 +34,12 @@ export async function createUserIndex() {
|
|||
}
|
||||
let idxKey = prev != null ? `${prev}.${key}` : key
|
||||
if (typeof input[key] === "string") {
|
||||
// @ts-expect-error index is available in a CouchDB map function
|
||||
// eslint-disable-next-line no-undef
|
||||
// @ts-ignore
|
||||
index(idxKey, input[key].toLowerCase(), { facet: true })
|
||||
} else if (typeof input[key] !== "object") {
|
||||
// @ts-expect-error index is available in a CouchDB map function
|
||||
// eslint-disable-next-line no-undef
|
||||
// @ts-ignore
|
||||
index(idxKey, input[key], { facet: true })
|
||||
} else {
|
||||
idx(input[key], idxKey)
|
||||
|
|
|
@ -17,13 +17,8 @@ export function init(processors: ProcessorMap) {
|
|||
// if not processing in this instance, kick it off
|
||||
if (!processingPromise) {
|
||||
processingPromise = asyncEventQueue.process(async job => {
|
||||
const { event, identity, properties, timestamp } = job.data
|
||||
await documentProcessor.processEvent(
|
||||
event,
|
||||
identity,
|
||||
properties,
|
||||
timestamp
|
||||
)
|
||||
const { event, identity, properties } = job.data
|
||||
await documentProcessor.processEvent(event, identity, properties)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Event } from "@budibase/types"
|
||||
import { Event, Identity } from "@budibase/types"
|
||||
import { processors } from "./processors"
|
||||
import identification from "./identification"
|
||||
import * as backfill from "./backfill"
|
||||
|
@ -7,12 +7,19 @@ import { publishAsyncEvent } from "./asyncEvents"
|
|||
export const publishEvent = async (
|
||||
event: Event,
|
||||
properties: any,
|
||||
timestamp?: string | number
|
||||
timestamp?: string | number,
|
||||
identityOverride?: Identity
|
||||
) => {
|
||||
// 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
|
||||
if (!backfilling) {
|
||||
// send off async events if required
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
Event,
|
||||
Identity,
|
||||
Group,
|
||||
IdentityType,
|
||||
AuditLogQueueEvent,
|
||||
AuditLogFn,
|
||||
|
@ -79,11 +78,11 @@ export default class AuditLogsProcessor implements EventProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
async identify(identity: Identity, timestamp?: string | number) {
|
||||
async identify() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
async identifyGroup(group: Group, timestamp?: string | number) {
|
||||
async identifyGroup() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,7 @@ export default class LoggingProcessor implements EventProcessor {
|
|||
async processEvent(
|
||||
event: Event,
|
||||
identity: Identity,
|
||||
properties: any,
|
||||
timestamp?: string
|
||||
properties: any
|
||||
): Promise<void> {
|
||||
if (skipLogging) {
|
||||
return
|
||||
|
@ -17,14 +16,14 @@ export default class LoggingProcessor implements EventProcessor {
|
|||
console.log(`[audit] [identityType=${identity.type}] ${event}`, properties)
|
||||
}
|
||||
|
||||
async identify(identity: Identity, timestamp?: string | number) {
|
||||
async identify(identity: Identity) {
|
||||
if (skipLogging) {
|
||||
return
|
||||
}
|
||||
console.log(`[audit] identified`, identity)
|
||||
}
|
||||
|
||||
async identifyGroup(group: Group, timestamp?: string | number) {
|
||||
async identifyGroup(group: Group) {
|
||||
if (skipLogging) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -14,12 +14,7 @@ export default class DocumentUpdateProcessor implements EventProcessor {
|
|||
this.processors = processors
|
||||
}
|
||||
|
||||
async processEvent(
|
||||
event: Event,
|
||||
identity: Identity,
|
||||
properties: any,
|
||||
timestamp?: string | number
|
||||
) {
|
||||
async processEvent(event: Event, identity: Identity, properties: any) {
|
||||
const tenantId = identity.realTenantId
|
||||
const docId = getDocumentId(event, properties)
|
||||
if (!tenantId || !docId) {
|
||||
|
|
|
@ -5,13 +5,19 @@ import {
|
|||
AccountCreatedEvent,
|
||||
AccountDeletedEvent,
|
||||
AccountVerifiedEvent,
|
||||
Identity,
|
||||
} from "@budibase/types"
|
||||
|
||||
async function created(account: Account) {
|
||||
async function created(account: Account, identityOverride?: Identity) {
|
||||
const properties: AccountCreatedEvent = {
|
||||
tenantId: account.tenantId,
|
||||
}
|
||||
await publishEvent(Event.ACCOUNT_CREATED, properties)
|
||||
await publishEvent(
|
||||
Event.ACCOUNT_CREATED,
|
||||
properties,
|
||||
undefined,
|
||||
identityOverride
|
||||
)
|
||||
}
|
||||
|
||||
async function deleted(account: Account) {
|
||||
|
|
|
@ -10,6 +10,18 @@ import { formats } from "dd-trace/ext"
|
|||
|
||||
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
|
||||
|
||||
let pinoInstance: pino.Logger | undefined
|
||||
|
@ -71,23 +83,11 @@ if (!env.DISABLE_PINO_LOGGER) {
|
|||
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
|
||||
* and pino logging requirements.
|
||||
*/
|
||||
function getLogParams(args: any[]): [MergingObject, string] {
|
||||
const getLogParams = (args: any[]): [MergingObject, string] => {
|
||||
let error = undefined
|
||||
let objects: any[] = []
|
||||
let message = ""
|
||||
|
|
|
@ -11,7 +11,6 @@ export const buildMatcherRegex = (
|
|||
return patterns.map(pattern => {
|
||||
let route = pattern.route
|
||||
const method = pattern.method
|
||||
const strict = pattern.strict ? pattern.strict : false
|
||||
|
||||
// if there is a param in the route
|
||||
// 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[]) => {
|
||||
return options.find(({ regex, method, strict, route }) => {
|
||||
let urlMatch
|
||||
if (strict) {
|
||||
urlMatch = ctx.request.url === route
|
||||
} else {
|
||||
urlMatch = regex.test(ctx.request.url)
|
||||
}
|
||||
|
||||
return options.find(({ regex, method }) => {
|
||||
const urlMatch = regex.test(ctx.request.url)
|
||||
const methodMatch =
|
||||
method === "ALL"
|
||||
? true
|
||||
: ctx.request.method.toLowerCase() === method.toLowerCase()
|
||||
|
||||
return urlMatch && methodMatch
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Cookie } from "../../../constants"
|
|||
import * as configs from "../../../configs"
|
||||
import * as cache from "../../../cache"
|
||||
import * as utils from "../../../utils"
|
||||
import { UserCtx, SSOProfile, DatasourceAuthCookie } from "@budibase/types"
|
||||
import { UserCtx, SSOProfile } from "@budibase/types"
|
||||
import { ssoSaveUserNoOp } from "../sso/sso"
|
||||
|
||||
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||
|
|
|
@ -5,7 +5,6 @@ import * as context from "../../../context"
|
|||
import fetch from "node-fetch"
|
||||
import {
|
||||
SaveSSOUserFunction,
|
||||
SaveUserOpts,
|
||||
SSOAuthDetails,
|
||||
SSOUser,
|
||||
User,
|
||||
|
@ -14,10 +13,8 @@ import {
|
|||
// no-op function for user save
|
||||
// - 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
|
||||
export const ssoSaveUserNoOp: SaveSSOUserFunction = (
|
||||
user: SSOUser,
|
||||
opts: SaveUserOpts
|
||||
) => Promise.resolve(user)
|
||||
export const ssoSaveUserNoOp: SaveSSOUserFunction = (user: SSOUser) =>
|
||||
Promise.resolve(user)
|
||||
|
||||
/**
|
||||
* Common authentication logic for third parties. e.g. OAuth, OIDC.
|
||||
|
|
|
@ -114,11 +114,11 @@ describe("sso", () => {
|
|||
// tenant id added
|
||||
ssoUser.tenantId = context.getTenantId()
|
||||
|
||||
expect(mockSaveUser).toBeCalledWith(ssoUser, {
|
||||
expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, {
|
||||
hashPassword: false,
|
||||
requirePassword: false,
|
||||
})
|
||||
expect(mockDone).toBeCalledWith(null, ssoUser)
|
||||
expect(mockDone).toHaveBeenCalledWith(null, ssoUser)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -159,11 +159,11 @@ describe("sso", () => {
|
|||
// existing id preserved
|
||||
ssoUser._id = existingUser._id
|
||||
|
||||
expect(mockSaveUser).toBeCalledWith(ssoUser, {
|
||||
expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, {
|
||||
hashPassword: false,
|
||||
requirePassword: false,
|
||||
})
|
||||
expect(mockDone).toBeCalledWith(null, ssoUser)
|
||||
expect(mockDone).toHaveBeenCalledWith(null, ssoUser)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -187,11 +187,11 @@ describe("sso", () => {
|
|||
// existing id preserved
|
||||
ssoUser._id = existingUser._id
|
||||
|
||||
expect(mockSaveUser).toBeCalledWith(ssoUser, {
|
||||
expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, {
|
||||
hashPassword: 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) {
|
||||
expect(throwFn).not.toBeCalled()
|
||||
expect(nextFn).toBeCalled()
|
||||
expect(throwFn).not.toHaveBeenCalled()
|
||||
expect(nextFn).toHaveBeenCalled()
|
||||
}
|
||||
|
||||
function threw(throwFn: jest.Func) {
|
||||
// cant check next, the throw function doesn't actually throw - so it still continues
|
||||
expect(throwFn).toBeCalled()
|
||||
expect(throwFn).toHaveBeenCalled()
|
||||
}
|
||||
|
||||
describe("adminOnly middleware", () => {
|
||||
|
|
|
@ -34,23 +34,6 @@ describe("matchers", () => {
|
|||
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", () => {
|
||||
const pattern = [
|
||||
{
|
||||
|
@ -67,23 +50,6 @@ describe("matchers", () => {
|
|||
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", () => {
|
||||
const pattern = [
|
||||
{
|
||||
|
|
|
@ -45,10 +45,6 @@ export const runMigration = async (
|
|||
options: MigrationOptions = {}
|
||||
) => {
|
||||
const migrationType = migration.type
|
||||
let tenantId: string | undefined
|
||||
if (migrationType !== MigrationType.INSTALLATION) {
|
||||
tenantId = context.getTenantId()
|
||||
}
|
||||
const migrationName = migration.name
|
||||
const silent = migration.silent
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ describe("app", () => {
|
|||
|
||||
it("gets url with embedded minio", async () => {
|
||||
testEnv.withMinio()
|
||||
await testEnv.withTenant(tenantId => {
|
||||
await testEnv.withTenant(() => {
|
||||
const url = getAppFileUrl()
|
||||
expect(url).toBe(
|
||||
"/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg"
|
||||
|
@ -136,7 +136,7 @@ describe("app", () => {
|
|||
|
||||
it("gets url with custom S3", async () => {
|
||||
testEnv.withS3()
|
||||
await testEnv.withTenant(tenantId => {
|
||||
await testEnv.withTenant(() => {
|
||||
const url = getAppFileUrl()
|
||||
expect(url).toBe(
|
||||
"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 () => {
|
||||
testEnv.withCloudfront()
|
||||
await testEnv.withTenant(tenantId => {
|
||||
await testEnv.withTenant(() => {
|
||||
const url = getAppFileUrl()
|
||||
// omit rest of signed params
|
||||
expect(
|
||||
|
|
|
@ -3,7 +3,7 @@ import { DBTestConfiguration } from "../../../tests/extra"
|
|||
import * as tenants from "../tenants"
|
||||
|
||||
describe("tenants", () => {
|
||||
const config = new DBTestConfiguration()
|
||||
new DBTestConfiguration()
|
||||
|
||||
describe("addTenant", () => {
|
||||
it("concurrently adds multiple tenants safely", async () => {
|
||||
|
|
|
@ -20,7 +20,7 @@ export async function lookupTenantId(userId: string) {
|
|||
return user.tenantId
|
||||
}
|
||||
|
||||
async function getUserDoc(emailOrId: string): Promise<PlatformUser> {
|
||||
export async function getUserDoc(emailOrId: string): Promise<PlatformUser> {
|
||||
const db = getPlatformDB()
|
||||
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(
|
||||
tenantId: string,
|
||||
userId: string,
|
||||
|
@ -91,9 +102,7 @@ export async function addUser(
|
|||
]
|
||||
|
||||
if (ssoId) {
|
||||
promises.push(
|
||||
addUserDoc(ssoId, () => newUserSsoIdDoc(ssoId, email, userId, tenantId))
|
||||
)
|
||||
promises.push(addSsoUser(ssoId, email, userId, tenantId))
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
|
|
@ -39,7 +39,7 @@ class InMemoryQueue implements Partial<Queue> {
|
|||
_opts?: QueueOptions
|
||||
_messages: JobMessage[]
|
||||
_queuedJobIds: Set<string>
|
||||
_emitter: EventEmitter
|
||||
_emitter: NodeJS.EventEmitter
|
||||
_runCount: number
|
||||
_addCount: number
|
||||
|
||||
|
@ -166,7 +166,7 @@ class InMemoryQueue implements Partial<Queue> {
|
|||
return []
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async removeJobs(pattern: string) {
|
||||
// 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.
|
||||
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.
|
||||
await doInJobContext(job, () => {
|
||||
console.info(...getLogParams(eventType, BullEvent.ACTIVE, { job }))
|
||||
|
|
|
@ -40,6 +40,7 @@ export async function shutdown() {
|
|||
if (inviteClient) await inviteClient.finish()
|
||||
if (passwordResetClient) await passwordResetClient.finish()
|
||||
if (socketClient) await socketClient.finish()
|
||||
if (docWritethroughClient) await docWritethroughClient.finish()
|
||||
}
|
||||
|
||||
process.on("exit", async () => {
|
||||
|
|
|
@ -120,7 +120,7 @@ describe("redis", () => {
|
|||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -147,17 +147,6 @@ describe("redis", () => {
|
|||
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 () => {
|
||||
const key = structures.uuid()
|
||||
const results = await Promise.all(
|
||||
|
@ -184,7 +173,7 @@ describe("redis", () => {
|
|||
const key = structures.uuid()
|
||||
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"
|
||||
)
|
||||
})
|
||||
|
|
|
@ -96,8 +96,8 @@ describe("redlockImpl", () => {
|
|||
task: mockTask,
|
||||
executionTimeMs: lockTtl * 2,
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
`Unable to fully release the lock on resource \"lock:${config.tenantId}_persist_writethrough\".`
|
||||
).rejects.toThrow(
|
||||
`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(ctx.throw).toBeCalledTimes(1)
|
||||
expect(ctx.throw).toBeCalledWith(403, "Tenant id not set")
|
||||
expect(ctx.throw).toHaveBeenCalledTimes(1)
|
||||
expect(ctx.throw).toHaveBeenCalledWith(403, "Tenant id not set")
|
||||
})
|
||||
|
||||
it("returns undefined if allowNoTenant is true", () => {
|
||||
|
|
|
@ -500,13 +500,13 @@ export class UserDB {
|
|||
|
||||
static async createAdminUser(
|
||||
email: string,
|
||||
password: string,
|
||||
tenantId: string,
|
||||
password?: string,
|
||||
opts?: CreateAdminUserOpts
|
||||
) {
|
||||
const user: User = {
|
||||
email: email,
|
||||
password: password,
|
||||
password,
|
||||
createdAt: Date.now(),
|
||||
roles: {},
|
||||
builder: {
|
||||
|
|
|
@ -45,7 +45,7 @@ describe("Users", () => {
|
|||
...{ _id: groupId, roles: { app1: "ADMIN" } },
|
||||
}
|
||||
const users: User[] = []
|
||||
for (const _ of Array.from({ length: usersInGroup })) {
|
||||
for (let i = 0; i < usersInGroup; i++) {
|
||||
const userId = `us_${generator.guid()}`
|
||||
const user: User = structures.users.user({
|
||||
_id: userId,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { generator } from "./generator"
|
|||
|
||||
export function userGroup(): UserGroup {
|
||||
return {
|
||||
name: generator.word(),
|
||||
name: generator.guid(),
|
||||
icon: generator.word(),
|
||||
color: generator.word(),
|
||||
}
|
||||
|
|
|
@ -1,80 +1,58 @@
|
|||
import { DatabaseImpl } from "../../../src/db"
|
||||
import { execSync } from "child_process"
|
||||
|
||||
let dockerPsResult: string | undefined
|
||||
|
||||
function formatDockerPsResult(serverName: string, port: number) {
|
||||
const lines = dockerPsResult?.split("\n")
|
||||
let first = true
|
||||
if (!lines) {
|
||||
return null
|
||||
}
|
||||
for (let line of lines) {
|
||||
if (first) {
|
||||
first = false
|
||||
continue
|
||||
}
|
||||
let toLookFor = serverName.split("-service")[0]
|
||||
if (!line.includes(toLookFor)) {
|
||||
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
|
||||
interface ContainerInfo {
|
||||
Command: string
|
||||
CreatedAt: string
|
||||
ID: string
|
||||
Image: string
|
||||
Labels: string
|
||||
LocalVolumes: string
|
||||
Mounts: string
|
||||
Names: string
|
||||
Networks: string
|
||||
Ports: string
|
||||
RunningFor: string
|
||||
Size: string
|
||||
State: string
|
||||
Status: string
|
||||
}
|
||||
|
||||
function getTestContainerSettings(
|
||||
serverName: string,
|
||||
key: string
|
||||
): string | null {
|
||||
const entry = Object.entries(global).find(
|
||||
([k]) =>
|
||||
k.includes(`${serverName.toUpperCase()}`) &&
|
||||
k.includes(`${key.toUpperCase()}`)
|
||||
)
|
||||
if (!entry) {
|
||||
return null
|
||||
}
|
||||
return entry[1]
|
||||
function getTestcontainers(): ContainerInfo[] {
|
||||
return execSync("docker ps --format json")
|
||||
.toString()
|
||||
.split("\n")
|
||||
.filter(x => x.length > 0)
|
||||
.map(x => JSON.parse(x) as ContainerInfo)
|
||||
.filter(x => x.Labels.includes("org.testcontainers=true"))
|
||||
}
|
||||
|
||||
function getContainerInfo(containerName: string, port: number) {
|
||||
let assignedPort = getTestContainerSettings(
|
||||
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 getContainerByImage(image: string) {
|
||||
return getTestcontainers().find(x => x.Image.startsWith(image))
|
||||
}
|
||||
|
||||
function getCouchConfig() {
|
||||
return getContainerInfo("couchdb", 5984)
|
||||
function getExposedPort(container: ContainerInfo, port: number) {
|
||||
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[]) {
|
||||
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 = [
|
||||
{ key: "COUCH_DB_PORT", value: couch.port },
|
||||
{ key: "COUCH_DB_URL", value: couch.url },
|
||||
{ key: "COUCH_DB_PORT", value: `${couchPort}` },
|
||||
{ key: "COUCH_DB_URL", value: `http://localhost:${couchPort}` },
|
||||
]
|
||||
|
||||
for (const config of configs.filter(x => !!x.value)) {
|
||||
|
@ -82,4 +60,7 @@ export function setupEnv(...envs: any[]) {
|
|||
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.LOG_LEVEL = process.env.LOG_LEVEL || "error"
|
||||
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",
|
||||
file: "dist/bbui.es.js",
|
||||
},
|
||||
onwarn(warning, warn) {
|
||||
// suppress eval warnings
|
||||
if (warning.code === "EVAL") {
|
||||
return
|
||||
}
|
||||
warn(warning)
|
||||
},
|
||||
plugins: [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
|
|
|
@ -39,19 +39,23 @@ const handleClick = event => {
|
|||
return
|
||||
}
|
||||
|
||||
if (handler.allowedType && event.type !== handler.allowedType) {
|
||||
return
|
||||
}
|
||||
|
||||
handler.callback?.(event)
|
||||
})
|
||||
}
|
||||
document.documentElement.addEventListener("click", handleClick, true)
|
||||
document.documentElement.addEventListener("contextmenu", handleClick, true)
|
||||
document.documentElement.addEventListener("mousedown", handleClick, true)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
if (!existingHandler) {
|
||||
clickHandlers.push({ id, element, anchor, callback })
|
||||
clickHandlers.push({ id, element, anchor, callback, allowedType })
|
||||
} else {
|
||||
existingHandler.callback = callback
|
||||
}
|
||||
|
@ -75,9 +79,11 @@ const removeHandler = id => {
|
|||
export default (element, opts) => {
|
||||
const id = Math.random()
|
||||
const update = newOpts => {
|
||||
const callback = newOpts?.callback || newOpts
|
||||
const callback =
|
||||
newOpts?.callback || (typeof newOpts === "function" ? newOpts : null)
|
||||
const anchor = newOpts?.anchor || element
|
||||
updateHandler(id, element, anchor, callback)
|
||||
const allowedType = newOpts?.allowedType || "click"
|
||||
updateHandler(id, element, anchor, callback, allowedType)
|
||||
}
|
||||
update(opts)
|
||||
return {
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
.main {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.padding .main {
|
||||
padding: var(--spacing-xl);
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export let schema
|
||||
export let value
|
||||
export let customRenderers = []
|
||||
export let snippets
|
||||
|
||||
let renderer
|
||||
const typeMap = {
|
||||
|
@ -44,7 +45,7 @@
|
|||
if (!template) {
|
||||
return value
|
||||
}
|
||||
return processStringSync(template, { value })
|
||||
return processStringSync(template, { value, snippets })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
export let customPlaceholder = false
|
||||
export let showHeaderBorder = true
|
||||
export let placeholderText = "No rows found"
|
||||
export let snippets = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -425,6 +426,7 @@
|
|||
<CellRenderer
|
||||
{customRenderers}
|
||||
{row}
|
||||
{snippets}
|
||||
schema={schema[field]}
|
||||
value={deepGet(row, field)}
|
||||
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
|
||||
if (type === AUTO_TYPE || autocolumn) {
|
||||
return type.toUpperCase()
|
||||
} else {
|
||||
} else if (type === FieldType.BB_REFERENCE) {
|
||||
return `${type}${subtype || ""}`.toUpperCase()
|
||||
} else {
|
||||
return type.toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -703,24 +705,6 @@
|
|||
thin
|
||||
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 editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
|
||||
<Select
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
let deleteTableName
|
||||
|
||||
$: externalTable = table?.sourceType === DB_TYPE_EXTERNAL
|
||||
$: allowDeletion = !externalTable || table?.created
|
||||
|
||||
function showDeleteModal() {
|
||||
templateScreens = $screenStore.screens.filter(
|
||||
|
@ -56,7 +55,7 @@
|
|||
$goto(`./datasource/${table.datasourceId}`)
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting table")
|
||||
notifications.error(`Error deleting table - ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,17 +85,15 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#if allowDeletion}
|
||||
<ActionMenu>
|
||||
<div slot="control" class="icon">
|
||||
<Icon s hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
{#if !externalTable}
|
||||
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
|
||||
{/if}
|
||||
<MenuItem icon="Delete" on:click={showDeleteModal}>Delete</MenuItem>
|
||||
</ActionMenu>
|
||||
{/if}
|
||||
<ActionMenu>
|
||||
<div slot="control" class="icon">
|
||||
<Icon s hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
{#if !externalTable}
|
||||
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
|
||||
{/if}
|
||||
<MenuItem icon="Delete" on:click={showDeleteModal}>Delete</MenuItem>
|
||||
</ActionMenu>
|
||||
|
||||
<Modal bind:this={editorModal} on:show={initForm}>
|
||||
<ModalContent
|
||||
|
|
|
@ -313,7 +313,7 @@ export const bindingsToCompletions = (bindings, mode) => {
|
|||
...bindingByCategory[catKey].reduce((acc, binding) => {
|
||||
let displayType = binding.fieldSchema?.type || binding.display?.type
|
||||
acc.push({
|
||||
label: binding.display?.name || "NO NAME",
|
||||
label: binding.display?.name || binding.readableBinding || "NO NAME",
|
||||
info: completion => {
|
||||
return buildBindingInfoNode(completion, binding)
|
||||
},
|
||||
|
|
|
@ -371,6 +371,7 @@
|
|||
<style>
|
||||
.binding-panel {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.binding-panel,
|
||||
.tabs {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
export let allowJS = false
|
||||
export let allowHelpers = true
|
||||
export let autofocusEditor = false
|
||||
export let context = null
|
||||
|
||||
$: enrichedBindings = enrichBindings(bindings)
|
||||
|
||||
|
@ -27,7 +28,7 @@
|
|||
|
||||
<BindingPanel
|
||||
bindings={enrichedBindings}
|
||||
context={$previewStore.selectedComponentContext}
|
||||
context={{ ...$previewStore.selectedComponentContext, ...context }}
|
||||
snippets={$snippets}
|
||||
{value}
|
||||
{allowJS}
|
||||
|
|
|
@ -32,10 +32,14 @@
|
|||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { onMount } from "svelte"
|
||||
import PosthogClient from "../../analytics/PosthogClient"
|
||||
|
||||
export let application
|
||||
export let loaded
|
||||
|
||||
const posthog = new PosthogClient(process.env.POSTHOG_TOKEN)
|
||||
|
||||
let unpublishModal
|
||||
let updateAppModal
|
||||
let revertModal
|
||||
|
@ -44,6 +48,8 @@
|
|||
let appActionPopoverOpen = false
|
||||
let appActionPopoverAnchor
|
||||
let publishing = false
|
||||
let showNpsSurvey = false
|
||||
let lastOpened
|
||||
|
||||
$: filteredApps = $appsStore.apps.filter(app => app.devId === application)
|
||||
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||
|
@ -57,7 +63,7 @@
|
|||
$appStore.version &&
|
||||
$appStore.upgradableVersion !== $appStore.version
|
||||
$: canPublish = !publishing && loaded && $sortedScreens.length > 0
|
||||
$: lastDeployed = getLastDeployedString($deploymentStore)
|
||||
$: lastDeployed = getLastDeployedString($deploymentStore, lastOpened)
|
||||
|
||||
const initialiseApp = async () => {
|
||||
const applicationPkg = await API.fetchAppPackage($appStore.devId)
|
||||
|
@ -97,6 +103,7 @@
|
|||
type: "success",
|
||||
icon: "GlobeCheck",
|
||||
})
|
||||
showNpsSurvey = true
|
||||
await completePublish()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -147,6 +154,10 @@
|
|||
notifications.error("Error refreshing app")
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
posthog.init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
|
@ -201,6 +212,7 @@
|
|||
class="app-action-button publish app-action-popover"
|
||||
on:click={() => {
|
||||
if (!appActionPopoverOpen) {
|
||||
lastOpened = new Date()
|
||||
appActionPopover.show()
|
||||
} else {
|
||||
appActionPopover.hide()
|
||||
|
@ -343,6 +355,10 @@
|
|||
<RevertModal bind:this={revertModal} />
|
||||
<VersionModal hideIcon bind:this={versionModal} />
|
||||
|
||||
{#if showNpsSurvey}
|
||||
<div class="nps-survey" />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.app-action-popover-content {
|
||||
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>
|
||||
import { admin } from "stores/portal"
|
||||
import {
|
||||
Modal,
|
||||
notifications,
|
||||
|
@ -9,6 +10,7 @@
|
|||
} from "@budibase/bbui"
|
||||
import { appStore, initialise } from "stores/builder"
|
||||
import { API } from "api"
|
||||
import RevertModalVersionSelect from "./RevertModalVersionSelect.svelte"
|
||||
|
||||
export function show() {
|
||||
updateModal.show()
|
||||
|
@ -28,7 +30,9 @@
|
|||
$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 () => {
|
||||
try {
|
||||
|
@ -62,7 +66,9 @@
|
|||
// Don't wait for the async refresh, since this causes modal flashing
|
||||
refreshAppPackage()
|
||||
notifications.success(
|
||||
`App reverted successfully to version ${$appStore.revertableVersion}`
|
||||
$appStore.revertableVersion
|
||||
? `App reverted successfully to version ${$appStore.revertableVersion}`
|
||||
: "App reverted successfully"
|
||||
)
|
||||
} catch (err) {
|
||||
notifications.error(`Error reverting app: ${err}`)
|
||||
|
@ -103,7 +109,13 @@
|
|||
{#if revertAvailable}
|
||||
<Body size="S">
|
||||
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.
|
||||
</Body>
|
||||
{/if}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -7,10 +7,13 @@
|
|||
Layout,
|
||||
Label,
|
||||
} from "@budibase/bbui"
|
||||
import { themeStore } from "stores/builder"
|
||||
import { themeStore, previewStore } from "stores/builder"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
|
||||
export let column
|
||||
|
||||
$: columnValue =
|
||||
$previewStore.selectedComponentContext?.eventContext?.row?.[column.name]
|
||||
</script>
|
||||
|
||||
<DrawerContent>
|
||||
|
@ -41,6 +44,9 @@
|
|||
icon: "TableColumnMerge",
|
||||
},
|
||||
]}
|
||||
context={{
|
||||
value: columnValue,
|
||||
}}
|
||||
/>
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Background color</Label>
|
||||
|
|
|
@ -279,3 +279,11 @@ export const buildContextTreeLookupMap = rootComponent => {
|
|||
})
|
||||
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
|
||||
.filter(user => user.email !== $auth.user.email)
|
||||
.map(user => {
|
||||
const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder(
|
||||
user,
|
||||
prodAppId
|
||||
)
|
||||
const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder(user)
|
||||
const isAppBuilder = user.builder?.apps?.includes(prodAppId)
|
||||
let role
|
||||
if (isAdminOrGlobalBuilder) {
|
||||
|
|
|
@ -24,6 +24,13 @@
|
|||
navigationStore,
|
||||
} from "stores/builder"
|
||||
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
|
||||
.map(screen => screen.routing?.route)
|
||||
|
@ -46,6 +53,10 @@
|
|||
notifications.error("Error updating navigation settings")
|
||||
}
|
||||
}
|
||||
|
||||
const updateTextAlign = textAlignValue => {
|
||||
navigationStore.syncAppNavigation({ textAlign: textAlignValue })
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel
|
||||
|
@ -133,6 +144,15 @@
|
|||
on:change={e => update("title", e.detail)}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
|
||||
<div class="label">
|
||||
<Label size="M">Text align</Label>
|
||||
</div>
|
||||
<BarButtonList
|
||||
options={alignmentOptions}
|
||||
value={$navigationStore.textAlign}
|
||||
onChange={updateTextAlign}
|
||||
/>
|
||||
{/if}
|
||||
<div class="label">
|
||||
<Label>Background</Label>
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
"name": "Blocks",
|
||||
"icon": "Article",
|
||||
"children": [
|
||||
"gridblock",
|
||||
"tableblock",
|
||||
"cardsblock",
|
||||
"repeaterblock",
|
||||
"formblock",
|
||||
|
@ -16,7 +14,7 @@
|
|||
{
|
||||
"name": "Layout",
|
||||
"icon": "ClassicGridView",
|
||||
"children": ["container", "section", "grid", "sidepanel"]
|
||||
"children": ["container", "section", "sidepanel"]
|
||||
},
|
||||
{
|
||||
"name": "Data",
|
||||
|
@ -24,7 +22,7 @@
|
|||
"children": [
|
||||
"dataprovider",
|
||||
"repeater",
|
||||
"table",
|
||||
"gridblock",
|
||||
"spreadsheet",
|
||||
"dynamicfilter",
|
||||
"daterangepicker"
|
||||
|
|
|
@ -10,20 +10,15 @@
|
|||
navigationStore,
|
||||
selectedScreen,
|
||||
hoverStore,
|
||||
componentTreeNodesStore,
|
||||
snippets,
|
||||
} from "stores/builder"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import {
|
||||
ProgressCircle,
|
||||
Layout,
|
||||
Heading,
|
||||
Body,
|
||||
Icon,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { Layout, Heading, Body, Icon, notifications } from "@budibase/bbui"
|
||||
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
|
||||
import { findComponent, findComponentPath } from "helpers/components"
|
||||
import { isActive, goto } from "@roxi/routify"
|
||||
import { ClientAppSkeleton } from "@budibase/frontend-core"
|
||||
|
||||
let iframe
|
||||
let layout
|
||||
|
@ -132,6 +127,7 @@
|
|||
error = event.error || "An unknown error occurred"
|
||||
} else if (type === "select-component" && "id" in data) {
|
||||
componentStore.select(data.id)
|
||||
componentTreeNodesStore.makeNodeVisible(data.id)
|
||||
} else if (type === "hover-component") {
|
||||
hoverStore.hover(data.id, false)
|
||||
} else if (type === "update-prop") {
|
||||
|
@ -252,8 +248,16 @@
|
|||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="component-container">
|
||||
{#if loading}
|
||||
<div class="center">
|
||||
<ProgressCircle />
|
||||
<div
|
||||
class={`loading ${$themeStore.baseTheme} ${$themeStore.theme}`}
|
||||
class:tablet={$previewStore.previewDevice === "tablet"}
|
||||
class:mobile={$previewStore.previewDevice === "mobile"}
|
||||
>
|
||||
<ClientAppSkeleton
|
||||
sideNav={$navigationStore?.navigation === "Left"}
|
||||
hideFooter
|
||||
hideDevTools
|
||||
/>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="center error">
|
||||
|
@ -270,8 +274,6 @@
|
|||
bind:this={iframe}
|
||||
src="/app/preview"
|
||||
class:hidden={loading || error}
|
||||
class:tablet={$previewStore.previewDevice === "tablet"}
|
||||
class:mobile={$previewStore.previewDevice === "mobile"}
|
||||
/>
|
||||
<div
|
||||
class="add-component"
|
||||
|
@ -291,6 +293,25 @@
|
|||
/>
|
||||
|
||||
<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 {
|
||||
grid-row-start: middle;
|
||||
grid-column-start: middle;
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
selectedScreen,
|
||||
componentStore,
|
||||
selectedComponent,
|
||||
componentTreeNodesStore,
|
||||
} from "stores/builder"
|
||||
import { findComponent } from "helpers/components"
|
||||
import { findComponent, getChildIdsForComponent } from "helpers/components"
|
||||
import { goto, isActive } from "@roxi/routify"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
|
||||
|
||||
let confirmDeleteDialog
|
||||
let confirmEjectDialog
|
||||
|
@ -63,38 +63,25 @@
|
|||
componentStore.selectNext()
|
||||
},
|
||||
["ArrowRight"]: component => {
|
||||
componentTreeNodesStore.expandNode(component._id)
|
||||
componentTreeNodesStore.expandNodes([component._id])
|
||||
},
|
||||
["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 => {
|
||||
componentTreeNodesStore.expandNode(component._id)
|
||||
|
||||
const expandChildren = component => {
|
||||
const children = component._children ?? []
|
||||
|
||||
children.forEach(child => {
|
||||
componentTreeNodesStore.expandNode(child._id)
|
||||
expandChildren(child)
|
||||
})
|
||||
}
|
||||
|
||||
expandChildren(component)
|
||||
const childIds = getChildIdsForComponent(component)
|
||||
componentTreeNodesStore.expandNodes(childIds)
|
||||
},
|
||||
["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 children = component._children ?? []
|
||||
|
||||
children.forEach(child => {
|
||||
componentTreeNodesStore.collapseNode(child._id)
|
||||
collapseChildren(child)
|
||||
})
|
||||
}
|
||||
|
||||
collapseChildren(component)
|
||||
const childIds = getChildIdsForComponent(component)
|
||||
componentTreeNodesStore.collapseNodes(childIds)
|
||||
},
|
||||
["Escape"]: () => {
|
||||
if ($isActive(`./:componentId/new`)) {
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
componentStore,
|
||||
userSelectedResourceMap,
|
||||
selectedComponent,
|
||||
selectedComponentPath,
|
||||
hoverStore,
|
||||
componentTreeNodesStore,
|
||||
} from "stores/builder"
|
||||
import {
|
||||
findComponentPath,
|
||||
|
@ -17,7 +17,6 @@
|
|||
} from "helpers/components"
|
||||
import { get } from "svelte/store"
|
||||
import { dndStore } from "./dndStore"
|
||||
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
|
||||
|
||||
export let components = []
|
||||
export let level = 0
|
||||
|
@ -64,14 +63,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
const isOpen = (component, selectedComponentPath, openNodes) => {
|
||||
const isOpen = component => {
|
||||
if (!component?._children?.length) {
|
||||
return false
|
||||
}
|
||||
if (selectedComponentPath.slice(0, -1).includes(component._id)) {
|
||||
return true
|
||||
}
|
||||
return openNodes[`nodeOpen-${component._id}`]
|
||||
return componentTreeNodesStore.isNodeExpanded(component._id)
|
||||
}
|
||||
|
||||
const isChildOfSelectedComponent = component => {
|
||||
|
@ -83,6 +79,11 @@
|
|||
return findComponentPath($selectedComponent, component._id)?.length > 0
|
||||
}
|
||||
|
||||
const handleIconClick = componentId => {
|
||||
componentStore.select(componentId)
|
||||
componentTreeNodesStore.toggleNode(componentId)
|
||||
}
|
||||
|
||||
const hover = hoverStore.hover
|
||||
</script>
|
||||
|
||||
|
@ -90,7 +91,7 @@
|
|||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<ul>
|
||||
{#each filteredComponents || [] as component, index (component._id)}
|
||||
{@const opened = isOpen(component, $selectedComponentPath, openNodes)}
|
||||
{@const opened = isOpen(component, openNodes)}
|
||||
<li
|
||||
on:click|stopPropagation={() => {
|
||||
componentStore.select(component._id)
|
||||
|
@ -104,7 +105,7 @@
|
|||
on:dragend={dndStore.actions.reset}
|
||||
on:dragstart={() => dndStore.actions.dragstart(component)}
|
||||
on:dragover={dragover(component, index)}
|
||||
on:iconClick={() => componentTreeNodesStore.toggleNode(component._id)}
|
||||
on:iconClick={() => handleIconClick(component._id)}
|
||||
on:drop={onDrop}
|
||||
hovering={$hoverStore.componentId === component._id}
|
||||
on:mouseenter={() => hover(component._id)}
|
||||
|
|
|
@ -19,7 +19,8 @@
|
|||
import { goto } from "@roxi/routify"
|
||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||
import formScreen from "templates/formScreen"
|
||||
import rowListScreen from "templates/rowListScreen"
|
||||
import gridListScreen from "templates/gridListScreen"
|
||||
import gridDetailsScreen from "templates/gridDetailsScreen"
|
||||
|
||||
let mode
|
||||
let pendingScreen
|
||||
|
@ -127,7 +128,7 @@
|
|||
screenAccessRole = Roles.BASIC
|
||||
formType = null
|
||||
|
||||
if (mode === "table" || mode === "grid" || mode === "form") {
|
||||
if (mode === "grid" || mode === "gridDetails" || mode === "form") {
|
||||
datasourceModal.show()
|
||||
} else if (mode === "blank") {
|
||||
let templates = getTemplates($tables.list)
|
||||
|
@ -153,7 +154,10 @@
|
|||
|
||||
// Handler for Datasource Screen Creation
|
||||
const completeDatasourceScreenCreation = async () => {
|
||||
templates = rowListScreen(selectedDatasources, mode)
|
||||
templates =
|
||||
mode === "grid"
|
||||
? gridListScreen(selectedDatasources)
|
||||
: gridDetailsScreen(selectedDatasources)
|
||||
|
||||
const screens = templates.map(template => {
|
||||
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 CreationPage from "components/common/CreationPage.svelte"
|
||||
import blankImage from "./images/blank.png"
|
||||
import tableImage from "./images/table.png"
|
||||
import gridImage from "./images/grid.png"
|
||||
import tableInline from "./images/tableInline.png"
|
||||
import tableDetails from "./images/tableDetails.png"
|
||||
import formImage from "./images/form.png"
|
||||
import CreateScreenModal from "./CreateScreenModal.svelte"
|
||||
import { screenStore } from "stores/builder"
|
||||
|
@ -38,23 +38,23 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" on:click={() => createScreenModal.show("table")}>
|
||||
<div class="card" on:click={() => createScreenModal.show("grid")}>
|
||||
<div class="image">
|
||||
<img alt="" src={tableImage} />
|
||||
<img alt="" src={tableInline} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Table</Body>
|
||||
<Body size="XS">View, edit and delete rows on a table</Body>
|
||||
<Body size="S">Table with inline editing</Body>
|
||||
<Body size="XS">View, edit and delete rows inline</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" on:click={() => createScreenModal.show("grid")}>
|
||||
<div class="card" on:click={() => createScreenModal.show("gridDetails")}>
|
||||
<div class="image">
|
||||
<img alt="" src={gridImage} />
|
||||
<img alt="" src={tableDetails} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Grid</Body>
|
||||
<Body size="XS">View and manipulate rows on a grid</Body>
|
||||
<Body size="S">Table with details panel</Body>
|
||||
<Body size="XS">Manage your row details in a side panel</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -113,6 +113,11 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.card .image {
|
||||
min-height: 130px;
|
||||
min-width: 235px;
|
||||
}
|
||||
|
||||
.text {
|
||||
border: 1px solid var(--grey-4);
|
||||
border-radius: 0 0 4px 4px;
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
<script>
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
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 FavouriteAppButton from "../FavouriteAppButton.svelte"
|
||||
import {
|
||||
|
@ -14,12 +20,17 @@
|
|||
import { sdk } from "@budibase/shared-core"
|
||||
import { API } from "api"
|
||||
import ErrorSVG from "./ErrorSVG.svelte"
|
||||
import { getBaseTheme, ClientAppSkeleton } from "@budibase/frontend-core"
|
||||
|
||||
$: app = $enrichedApps.find(app => app.appId === $params.appId)
|
||||
$: iframeUrl = getIframeURL(app)
|
||||
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
|
||||
|
||||
let loading = true
|
||||
|
||||
const getIframeURL = app => {
|
||||
loading = true
|
||||
|
||||
if (app.status === "published") {
|
||||
return `/app${app.url}`
|
||||
}
|
||||
|
@ -37,6 +48,20 @@
|
|||
}
|
||||
|
||||
$: 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>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
|
@ -108,7 +133,26 @@
|
|||
</Body>
|
||||
</div>
|
||||
{: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}
|
||||
</div>
|
||||
|
||||
|
@ -139,6 +183,23 @@
|
|||
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 {
|
||||
flex: 1 1 auto;
|
||||
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,
|
||||
previewStore,
|
||||
tables,
|
||||
componentTreeNodesStore,
|
||||
} from "stores/builder/index"
|
||||
import { buildFormSchema, getSchemaForDatasource } from "dataBinding"
|
||||
import {
|
||||
|
@ -29,7 +30,6 @@ import {
|
|||
} from "constants/backend"
|
||||
import BudiStore from "../BudiStore"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
|
||||
|
||||
export const INITIAL_COMPONENTS_STATE = {
|
||||
components: {},
|
||||
|
@ -279,12 +279,10 @@ export class ComponentStore extends BudiStore {
|
|||
else {
|
||||
if (setting.type === "dataProvider") {
|
||||
// Validate data provider exists, or else clear it
|
||||
const treeId = parent?._id || component._id
|
||||
const path = findComponentPath(screen?.props, treeId)
|
||||
const providers = path.filter(component =>
|
||||
component._component?.endsWith("/dataprovider")
|
||||
const providers = findAllMatchingComponents(
|
||||
screen?.props,
|
||||
x => x._component === "@budibase/standard-components/dataprovider"
|
||||
)
|
||||
// Validate non-empty values
|
||||
const valid = providers?.some(dp => value.includes?.(dp._id))
|
||||
if (!valid) {
|
||||
if (providers.length) {
|
||||
|
@ -653,8 +651,11 @@ export class ComponentStore extends BudiStore {
|
|||
this.update(state => {
|
||||
state.selectedScreenId = targetScreenId
|
||||
state.selectedComponentId = newComponentId
|
||||
|
||||
return state
|
||||
})
|
||||
|
||||
componentTreeNodesStore.makeNodeVisible(newComponentId)
|
||||
}
|
||||
|
||||
getPrevious() {
|
||||
|
@ -663,7 +664,6 @@ export class ComponentStore extends BudiStore {
|
|||
const screen = get(selectedScreen)
|
||||
const parent = findComponentParent(screen.props, componentId)
|
||||
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||
const componentTreeNodes = get(componentTreeNodesStore)
|
||||
|
||||
// Check for screen and navigation component edge cases
|
||||
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 (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]
|
||||
if (
|
||||
previousSibling._children?.length &&
|
||||
componentTreeNodes[`nodeOpen-${previousSibling._id}`]
|
||||
componentTreeNodesStore.isNodeExpanded(previousSibling._id)
|
||||
) {
|
||||
let target = previousSibling
|
||||
while (
|
||||
target._children?.length &&
|
||||
componentTreeNodes[`nodeOpen-${target._id}`]
|
||||
componentTreeNodesStore.isNodeExpanded(target._id)
|
||||
) {
|
||||
target = target._children[target._children.length - 1]
|
||||
}
|
||||
|
@ -711,7 +711,6 @@ export class ComponentStore extends BudiStore {
|
|||
const screen = get(selectedScreen)
|
||||
const parent = findComponentParent(screen.props, componentId)
|
||||
const index = parent?._children.findIndex(x => x._id === componentId)
|
||||
const componentTreeNodes = get(componentTreeNodesStore)
|
||||
|
||||
// Check for screen and navigation component edge cases
|
||||
const screenComponentId = `${screen._id}-screen`
|
||||
|
@ -720,11 +719,11 @@ export class ComponentStore extends BudiStore {
|
|||
return navComponentId
|
||||
}
|
||||
|
||||
// If we have children, select first child
|
||||
// If we have children, select first child, and the node is not collapsed
|
||||
if (
|
||||
component._children?.length &&
|
||||
(state.selectedComponentId === navComponentId ||
|
||||
componentTreeNodes[`nodeOpen-${component._id}`])
|
||||
componentTreeNodesStore.isNodeExpanded(component._id))
|
||||
) {
|
||||
return component._children[0]._id
|
||||
} else if (!parent) {
|
||||
|
@ -803,7 +802,10 @@ export class ComponentStore extends BudiStore {
|
|||
// sibling
|
||||
const previousSibling = parent._children[index - 1]
|
||||
const definition = this.getDefinition(previousSibling._component)
|
||||
if (definition.hasChildren) {
|
||||
if (
|
||||
definition.hasChildren &&
|
||||
componentTreeNodesStore.isNodeExpanded(previousSibling._id)
|
||||
) {
|
||||
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
|
||||
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 definition = this.getDefinition(nextSibling._component)
|
||||
if (definition.hasChildren) {
|
||||
if (
|
||||
definition.hasChildren &&
|
||||
componentTreeNodesStore.isNodeExpanded(nextSibling._id)
|
||||
) {
|
||||
nextSibling._children.splice(0, 0, originalComponent)
|
||||
}
|
||||
|
||||
|
@ -1151,13 +1156,3 @@ export const selectedComponent = derived(
|
|||
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 {
|
||||
hoverTimeout
|
||||
|
||||
constructor() {
|
||||
super({ ...INITIAL_HOVER_STATE })
|
||||
this.hover = this.hover.bind(this)
|
||||
}
|
||||
|
||||
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) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { layoutStore } from "./layouts.js"
|
||||
import { appStore } from "./app.js"
|
||||
import {
|
||||
componentStore,
|
||||
selectedComponent,
|
||||
selectedComponentPath,
|
||||
} from "./components"
|
||||
import { componentStore, selectedComponent } from "./components"
|
||||
import { navigationStore } from "./navigation.js"
|
||||
import { themeStore } from "./theme.js"
|
||||
import { screenStore, selectedScreen, sortedScreens } from "./screens.js"
|
||||
|
@ -31,8 +27,10 @@ import { integrations } from "./integrations"
|
|||
import { sortedIntegrations } from "./sortedIntegrations"
|
||||
import { queries } from "./queries"
|
||||
import { flags } from "./flags"
|
||||
import componentTreeNodesStore from "./componentTreeNodes"
|
||||
|
||||
export {
|
||||
componentTreeNodesStore,
|
||||
layoutStore,
|
||||
appStore,
|
||||
componentStore,
|
||||
|
@ -51,7 +49,6 @@ export {
|
|||
isOnlyUser,
|
||||
deploymentStore,
|
||||
selectedComponent,
|
||||
selectedComponentPath,
|
||||
tables,
|
||||
views,
|
||||
viewsV2,
|
||||
|
|
|
@ -11,6 +11,7 @@ export const INITIAL_NAVIGATION_STATE = {
|
|||
hideLogo: null,
|
||||
logoUrl: null,
|
||||
hideTitle: null,
|
||||
textAlign: "Left",
|
||||
navBackground: null,
|
||||
navWidth: null,
|
||||
navTextColor: null,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { API } from "api"
|
||||
import { getBaseTheme } from "@budibase/frontend-core"
|
||||
|
||||
const INITIAL_THEMES_STATE = {
|
||||
theme: "",
|
||||
|
@ -12,11 +13,15 @@ export const themes = () => {
|
|||
})
|
||||
|
||||
const syncAppTheme = app => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
theme: app.theme || "spectrum--light",
|
||||
customTheme: app.customTheme,
|
||||
}))
|
||||
store.update(state => {
|
||||
const theme = app.theme || "spectrum--light"
|
||||
return {
|
||||
...state,
|
||||
theme,
|
||||
baseTheme: getBaseTheme(theme),
|
||||
customTheme: app.customTheme,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 formScreen from "./formScreen"
|
||||
|
||||
const allTemplates = datasources => [
|
||||
...rowListScreen(datasources),
|
||||
...gridListScreen(datasources),
|
||||
...gridDetailsScreen(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,
|
||||
"declaration": 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"],
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@budibase/types": ["../types/src"],
|
||||
"@budibase/backend-core": ["../backend-core/src"],
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
"rowSelection": true,
|
||||
"continueIfAction": true,
|
||||
"showNotificationAction": true,
|
||||
"sidePanel": true
|
||||
"sidePanel": true,
|
||||
"skeletonLoader": true
|
||||
},
|
||||
"layout": {
|
||||
"name": "Layout",
|
||||
|
@ -4673,6 +4674,7 @@
|
|||
}
|
||||
},
|
||||
"table": {
|
||||
"deprecated": true,
|
||||
"name": "Table",
|
||||
"icon": "Table",
|
||||
"illegalChildren": ["section"],
|
||||
|
@ -5418,6 +5420,7 @@
|
|||
]
|
||||
},
|
||||
"tableblock": {
|
||||
"deprecated": true,
|
||||
"block": true,
|
||||
"name": "Table Block",
|
||||
"icon": "Table",
|
||||
|
@ -6595,7 +6598,7 @@
|
|||
]
|
||||
},
|
||||
"gridblock": {
|
||||
"name": "Grid Block",
|
||||
"name": "Table",
|
||||
"icon": "Table",
|
||||
"styles": ["size"],
|
||||
"size": {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue