Merge branch 'master' into 12251-budi-7719-invite-new-user-panel-should-pass-the-search-email-address-into-the-email-field-of-the-add-user-page
This commit is contained in:
commit
f7e39f8436
91 changed files with 1172 additions and 961 deletions
38
.github/workflows/budibase_ci.yml
vendored
38
.github/workflows/budibase_ci.yml
vendored
|
@ -33,13 +33,13 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
|
||||||
- name: Use Node.js 20.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
@ -50,14 +50,14 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 20.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
@ -80,7 +80,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
@ -92,14 +92,14 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 20.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
@ -116,14 +116,14 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 20.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
@ -140,14 +140,14 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 20.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
@ -165,14 +165,14 @@ jobs:
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 20.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
@ -189,13 +189,13 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
|
||||||
- name: Use Node.js 20.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
@ -219,7 +219,7 @@ jobs:
|
||||||
if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase')
|
if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase')
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
@ -249,7 +249,7 @@ jobs:
|
||||||
|
|
||||||
- name: Check submodule merged to base branch
|
- name: Check submodule merged to base branch
|
||||||
if: ${{ steps.get_pro_commits.outputs.base_commit != '' }}
|
if: ${{ steps.get_pro_commits.outputs.base_commit != '' }}
|
||||||
uses: actions/github-script@v4
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
@ -269,7 +269,7 @@ jobs:
|
||||||
if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase')
|
if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase')
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
@ -299,7 +299,7 @@ jobs:
|
||||||
|
|
||||||
- name: Check submodule merged to base branch
|
- name: Check submodule merged to base branch
|
||||||
if: ${{ steps.get_accountportal_commits.outputs.base_commit != '' }}
|
if: ${{ steps.get_accountportal_commits.outputs.base_commit != '' }}
|
||||||
uses: actions/github-script@v4
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
|
2
.github/workflows/close-featurebranch.yml
vendored
2
.github/workflows/close-featurebranch.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
||||||
github.event.label.name == 'feature-branch'
|
github.event.label.name == 'feature-branch'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: passeidireto/trigger-external-workflow-action@main
|
- uses: passeidireto/trigger-external-workflow-action@main
|
||||||
env:
|
env:
|
||||||
PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }}
|
PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }}
|
||||||
|
|
2
.github/workflows/deploy-featurebranch.yml
vendored
2
.github/workflows/deploy-featurebranch.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
||||||
contains(github.event.pull_request.labels.*.name, 'feature-branch')
|
contains(github.event.pull_request.labels.*.name, 'feature-branch')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: passeidireto/trigger-external-workflow-action@main
|
- uses: passeidireto/trigger-external-workflow-action@main
|
||||||
env:
|
env:
|
||||||
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
||||||
|
|
4
.github/workflows/tag-release.yml
vendored
4
.github/workflows/tag-release.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
echo "Ref is not master, you must run this job from master."
|
echo "Ref is not master, you must run this job from master."
|
||||||
exit 1
|
exit 1
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
@ -53,7 +53,7 @@ jobs:
|
||||||
needs: [tag-release]
|
needs: [tag-release]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: peter-evans/repository-dispatch@v2
|
- uses: peter-evans/repository-dispatch@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.15.2",
|
"version": "2.15.7",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 05c90ce55144e260da6688335c16783eab79bf96
|
Subproject commit e9af6686ba135c367e9145a53d26c68325b9bf68
|
|
@ -179,6 +179,7 @@ const environment = {
|
||||||
...getPackageJsonFields(),
|
...getPackageJsonFields(),
|
||||||
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
||||||
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
||||||
|
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
|
||||||
_set(key: any, value: any) {
|
_set(key: any, value: any) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -2,6 +2,7 @@ export * as configs from "./configs"
|
||||||
export * as events from "./events"
|
export * as events from "./events"
|
||||||
export * as migrations from "./migrations"
|
export * as migrations from "./migrations"
|
||||||
export * as users from "./users"
|
export * as users from "./users"
|
||||||
|
export * as userUtils from "./users/utils"
|
||||||
export * as roles from "./security/roles"
|
export * as roles from "./security/roles"
|
||||||
export * as permissions from "./security/permissions"
|
export * as permissions from "./security/permissions"
|
||||||
export * as accounts from "./accounts"
|
export * as accounts from "./accounts"
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
const redis = require("../redis/init")
|
import * as redis from "../redis/init"
|
||||||
const { v4: uuidv4 } = require("uuid")
|
import { v4 as uuidv4 } from "uuid"
|
||||||
const { logWarn } = require("../logging")
|
import { logWarn } from "../logging"
|
||||||
|
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
import { Duration } from "../utils"
|
||||||
import {
|
import {
|
||||||
Session,
|
Session,
|
||||||
ScannedSession,
|
ScannedSession,
|
||||||
|
@ -10,8 +10,10 @@ import {
|
||||||
CreateSession,
|
CreateSession,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
// a week in seconds
|
// a week expiry is the default
|
||||||
const EXPIRY_SECONDS = 86400 * 7
|
const EXPIRY_SECONDS = env.SESSION_EXPIRY_SECONDS
|
||||||
|
? parseInt(env.SESSION_EXPIRY_SECONDS)
|
||||||
|
: Duration.fromDays(7).toSeconds()
|
||||||
|
|
||||||
function makeSessionID(userId: string, sessionId: string) {
|
function makeSessionID(userId: string, sessionId: string) {
|
||||||
return `${userId}/${sessionId}`
|
return `${userId}/${sessionId}`
|
||||||
|
|
|
@ -251,7 +251,8 @@ export class UserDB {
|
||||||
}
|
}
|
||||||
|
|
||||||
const change = dbUser ? 0 : 1 // no change if there is existing user
|
const change = dbUser ? 0 : 1 // no change if there is existing user
|
||||||
const creatorsChange = isCreator(dbUser) !== isCreator(user) ? 1 : 0
|
const creatorsChange =
|
||||||
|
(await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0
|
||||||
return UserDB.quotas.addUsers(change, creatorsChange, async () => {
|
return UserDB.quotas.addUsers(change, creatorsChange, async () => {
|
||||||
await validateUniqueUser(email, tenantId)
|
await validateUniqueUser(email, tenantId)
|
||||||
|
|
||||||
|
@ -335,7 +336,7 @@ export class UserDB {
|
||||||
}
|
}
|
||||||
newUser.userGroups = groups || []
|
newUser.userGroups = groups || []
|
||||||
newUsers.push(newUser)
|
newUsers.push(newUser)
|
||||||
if (isCreator(newUser)) {
|
if (await isCreator(newUser)) {
|
||||||
newCreators.push(newUser)
|
newCreators.push(newUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -432,12 +433,16 @@ export class UserDB {
|
||||||
_deleted: true,
|
_deleted: true,
|
||||||
}))
|
}))
|
||||||
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
|
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
|
||||||
const creatorsToDelete = usersToDelete.filter(isCreator)
|
|
||||||
|
const creatorsEval = await Promise.all(usersToDelete.map(isCreator))
|
||||||
|
const creatorsToDeleteCount = creatorsEval.filter(
|
||||||
|
creator => !!creator
|
||||||
|
).length
|
||||||
|
|
||||||
for (let user of usersToDelete) {
|
for (let user of usersToDelete) {
|
||||||
await bulkDeleteProcessing(user)
|
await bulkDeleteProcessing(user)
|
||||||
}
|
}
|
||||||
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDelete.length)
|
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount)
|
||||||
|
|
||||||
// Build Response
|
// Build Response
|
||||||
// index users by id
|
// index users by id
|
||||||
|
@ -486,7 +491,7 @@ export class UserDB {
|
||||||
|
|
||||||
await db.remove(userId, dbUser._rev)
|
await db.remove(userId, dbUser._rev)
|
||||||
|
|
||||||
const creatorsToDelete = isCreator(dbUser) ? 1 : 0
|
const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0
|
||||||
await UserDB.quotas.removeUsers(1, creatorsToDelete)
|
await UserDB.quotas.removeUsers(1, creatorsToDelete)
|
||||||
await eventHelpers.handleDeleteEvents(dbUser)
|
await eventHelpers.handleDeleteEvents(dbUser)
|
||||||
await cache.user.invalidateUser(userId)
|
await cache.user.invalidateUser(userId)
|
||||||
|
|
67
packages/backend-core/src/users/test/utils.spec.ts
Normal file
67
packages/backend-core/src/users/test/utils.spec.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { User, UserGroup } from "@budibase/types"
|
||||||
|
import { generator, structures } from "../../../tests"
|
||||||
|
import { DBTestConfiguration } from "../../../tests/extra"
|
||||||
|
import { getGlobalDB } from "../../context"
|
||||||
|
import { isCreator } from "../utils"
|
||||||
|
|
||||||
|
const config = new DBTestConfiguration()
|
||||||
|
|
||||||
|
describe("Users", () => {
|
||||||
|
it("User is a creator if it is configured as a global builder", async () => {
|
||||||
|
const user: User = structures.users.user({ builder: { global: true } })
|
||||||
|
expect(await isCreator(user)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("User is a creator if it is configured as a global admin", async () => {
|
||||||
|
const user: User = structures.users.user({ admin: { global: true } })
|
||||||
|
expect(await isCreator(user)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("User is a creator if it is configured with creator permission", async () => {
|
||||||
|
const user: User = structures.users.user({ builder: { creator: true } })
|
||||||
|
expect(await isCreator(user)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("User is a creator if it is a builder in some application", async () => {
|
||||||
|
const user: User = structures.users.user({ builder: { apps: ["app1"] } })
|
||||||
|
expect(await isCreator(user)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("User is a creator if it has CREATOR permission in some application", async () => {
|
||||||
|
const user: User = structures.users.user({ roles: { app1: "CREATOR" } })
|
||||||
|
expect(await isCreator(user)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("User is a creator if it has ADMIN permission in some application", async () => {
|
||||||
|
const user: User = structures.users.user({ roles: { app1: "ADMIN" } })
|
||||||
|
expect(await isCreator(user)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("User is a creator if it remains to a group with ADMIN permissions", async () => {
|
||||||
|
const usersInGroup = 10
|
||||||
|
const groupId = "gr_17abffe89e0b40268e755b952f101a59"
|
||||||
|
const group: UserGroup = {
|
||||||
|
...structures.userGroups.userGroup(),
|
||||||
|
...{ _id: groupId, roles: { app1: "ADMIN" } },
|
||||||
|
}
|
||||||
|
const users: User[] = []
|
||||||
|
for (const _ of Array.from({ length: usersInGroup })) {
|
||||||
|
const userId = `us_${generator.guid()}`
|
||||||
|
const user: User = structures.users.user({
|
||||||
|
_id: userId,
|
||||||
|
userGroups: [groupId],
|
||||||
|
})
|
||||||
|
users.push(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
await db.put(group)
|
||||||
|
for (let user of users) {
|
||||||
|
await db.put(user)
|
||||||
|
const creator = await isCreator(user)
|
||||||
|
expect(creator).toBe(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -309,7 +309,8 @@ export async function getCreatorCount() {
|
||||||
let creators = 0
|
let creators = 0
|
||||||
async function iterate(startPage?: string) {
|
async function iterate(startPage?: string) {
|
||||||
const page = await paginatedUsers({ bookmark: startPage })
|
const page = await paginatedUsers({ bookmark: startPage })
|
||||||
creators += page.data.filter(isCreator).length
|
const creatorsEval = await Promise.all(page.data.map(isCreator))
|
||||||
|
creators += creatorsEval.filter(creator => !!creator).length
|
||||||
if (page.hasNextPage) {
|
if (page.hasNextPage) {
|
||||||
await iterate(page.nextPage)
|
await iterate(page.nextPage)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CloudAccount } from "@budibase/types"
|
import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types"
|
||||||
import * as accountSdk from "../accounts"
|
import * as accountSdk from "../accounts"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { getPlatformUser } from "./lookup"
|
import { getPlatformUser } from "./lookup"
|
||||||
|
@ -6,17 +6,48 @@ import { EmailUnavailableError } from "../errors"
|
||||||
import { getTenantId } from "../context"
|
import { getTenantId } from "../context"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
import { getAccountByTenantId } from "../accounts"
|
import { getAccountByTenantId } from "../accounts"
|
||||||
|
import { BUILTIN_ROLE_IDS } from "../security/roles"
|
||||||
|
import * as context from "../context"
|
||||||
|
|
||||||
// extract from shared-core to make easily accessible from backend-core
|
// extract from shared-core to make easily accessible from backend-core
|
||||||
export const isBuilder = sdk.users.isBuilder
|
export const isBuilder = sdk.users.isBuilder
|
||||||
export const isAdmin = sdk.users.isAdmin
|
export const isAdmin = sdk.users.isAdmin
|
||||||
export const isCreator = sdk.users.isCreator
|
|
||||||
export const isGlobalBuilder = sdk.users.isGlobalBuilder
|
export const isGlobalBuilder = sdk.users.isGlobalBuilder
|
||||||
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
|
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
|
||||||
export const hasAdminPermissions = sdk.users.hasAdminPermissions
|
export const hasAdminPermissions = sdk.users.hasAdminPermissions
|
||||||
export const hasBuilderPermissions = sdk.users.hasBuilderPermissions
|
export const hasBuilderPermissions = sdk.users.hasBuilderPermissions
|
||||||
export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions
|
export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions
|
||||||
|
|
||||||
|
export async function isCreator(user?: User | ContextUser) {
|
||||||
|
const isCreatorByUserDefinition = sdk.users.isCreator(user)
|
||||||
|
if (!isCreatorByUserDefinition && user) {
|
||||||
|
return await isCreatorByGroupMembership(user)
|
||||||
|
}
|
||||||
|
return isCreatorByUserDefinition
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isCreatorByGroupMembership(user?: User | ContextUser) {
|
||||||
|
const userGroups = user?.userGroups || []
|
||||||
|
if (userGroups.length > 0) {
|
||||||
|
const db = context.getGlobalDB()
|
||||||
|
const groups: UserGroup[] = []
|
||||||
|
for (let groupId of userGroups) {
|
||||||
|
try {
|
||||||
|
const group = await db.get<UserGroup>(groupId)
|
||||||
|
groups.push(group)
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.error !== "not_found") {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups.some(group =>
|
||||||
|
Object.values(group.roles || {}).includes(BUILTIN_ROLE_IDS.ADMIN)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export async function validateUniqueUser(email: string, tenantId: string) {
|
export async function validateUniqueUser(email: string, tenantId: string) {
|
||||||
// check budibase users in other tenants
|
// check budibase users in other tenants
|
||||||
if (env.MULTI_TENANCY) {
|
if (env.MULTI_TENANCY) {
|
||||||
|
|
|
@ -18,7 +18,6 @@ export default function positionDropdown(element, opts) {
|
||||||
useAnchorWidth,
|
useAnchorWidth,
|
||||||
offset = 5,
|
offset = 5,
|
||||||
customUpdate,
|
customUpdate,
|
||||||
offsetBelow,
|
|
||||||
} = opts
|
} = opts
|
||||||
if (!anchor) {
|
if (!anchor) {
|
||||||
return
|
return
|
||||||
|
@ -48,7 +47,7 @@ export default function positionDropdown(element, opts) {
|
||||||
styles.top = anchorBounds.top - elementBounds.height - offset
|
styles.top = anchorBounds.top - elementBounds.height - offset
|
||||||
styles.maxHeight = maxHeight || 240
|
styles.maxHeight = maxHeight || 240
|
||||||
} else {
|
} else {
|
||||||
styles.top = anchorBounds.bottom + (offsetBelow || offset)
|
styles.top = anchorBounds.bottom + offset
|
||||||
styles.maxHeight =
|
styles.maxHeight =
|
||||||
maxHeight || window.innerHeight - anchorBounds.bottom - 20
|
maxHeight || window.innerHeight - anchorBounds.bottom - 20
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
export let searchTerm = null
|
export let searchTerm = null
|
||||||
export let customPopoverHeight
|
export let customPopoverHeight
|
||||||
export let customPopoverOffsetBelow
|
|
||||||
export let customPopoverMaxHeight
|
|
||||||
export let open = false
|
export let open = false
|
||||||
export let loading
|
export let loading
|
||||||
|
|
||||||
|
@ -98,7 +96,5 @@
|
||||||
{sort}
|
{sort}
|
||||||
{autoWidth}
|
{autoWidth}
|
||||||
{customPopoverHeight}
|
{customPopoverHeight}
|
||||||
{customPopoverOffsetBelow}
|
|
||||||
{customPopoverMaxHeight}
|
|
||||||
{loading}
|
{loading}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -37,8 +37,6 @@
|
||||||
export let sort = false
|
export let sort = false
|
||||||
export let searchTerm = null
|
export let searchTerm = null
|
||||||
export let customPopoverHeight
|
export let customPopoverHeight
|
||||||
export let customPopoverOffsetBelow
|
|
||||||
export let customPopoverMaxHeight
|
|
||||||
export let align = "left"
|
export let align = "left"
|
||||||
export let footer = null
|
export let footer = null
|
||||||
export let customAnchor = null
|
export let customAnchor = null
|
||||||
|
@ -156,9 +154,7 @@
|
||||||
on:close={() => (open = false)}
|
on:close={() => (open = false)}
|
||||||
useAnchorWidth={!autoWidth}
|
useAnchorWidth={!autoWidth}
|
||||||
maxWidth={autoWidth ? 400 : null}
|
maxWidth={autoWidth ? 400 : null}
|
||||||
maxHeight={customPopoverMaxHeight}
|
|
||||||
customHeight={customPopoverHeight}
|
customHeight={customPopoverHeight}
|
||||||
offsetBelow={customPopoverOffsetBelow}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="popover-content"
|
class="popover-content"
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let getOptionIcon = () => null
|
export let getOptionIcon = () => null
|
||||||
export let getOptionColour = () => null
|
export let getOptionColour = () => null
|
||||||
export let getOptionSubtitle = () => null
|
export let getOptionSubtitle = () => null
|
||||||
|
export let compare = null
|
||||||
export let useOptionIconImage = false
|
export let useOptionIconImage = false
|
||||||
export let isOptionEnabled
|
export let isOptionEnabled
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
|
@ -23,8 +24,6 @@
|
||||||
export let footer = null
|
export let footer = null
|
||||||
export let open = false
|
export let open = false
|
||||||
export let tag = null
|
export let tag = null
|
||||||
export let customPopoverOffsetBelow
|
|
||||||
export let customPopoverMaxHeight
|
|
||||||
export let searchTerm = null
|
export let searchTerm = null
|
||||||
export let loading
|
export let loading
|
||||||
|
|
||||||
|
@ -34,13 +33,19 @@
|
||||||
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
|
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
|
||||||
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
|
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
|
||||||
|
|
||||||
|
function compareOptionAndValue(option, value) {
|
||||||
|
return typeof compare === "function"
|
||||||
|
? compare(option, value)
|
||||||
|
: option === value
|
||||||
|
}
|
||||||
|
|
||||||
const getFieldAttribute = (getAttribute, value, options) => {
|
const getFieldAttribute = (getAttribute, value, options) => {
|
||||||
// Wait for options to load if there is a value but no options
|
// Wait for options to load if there is a value but no options
|
||||||
if (!options?.length) {
|
if (!options?.length) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
const index = options.findIndex(
|
const index = options.findIndex((option, idx) =>
|
||||||
(option, idx) => getOptionValue(option, idx) === value
|
compareOptionAndValue(getOptionValue(option, idx), value)
|
||||||
)
|
)
|
||||||
return index !== -1 ? getAttribute(options[index], index) : null
|
return index !== -1 ? getAttribute(options[index], index) : null
|
||||||
}
|
}
|
||||||
|
@ -90,11 +95,9 @@
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
{sort}
|
{sort}
|
||||||
{tag}
|
{tag}
|
||||||
{customPopoverOffsetBelow}
|
|
||||||
{customPopoverMaxHeight}
|
|
||||||
isPlaceholder={value == null || value === ""}
|
isPlaceholder={value == null || value === ""}
|
||||||
placeholderOption={placeholder === false ? null : placeholder}
|
placeholderOption={placeholder === false ? null : placeholder}
|
||||||
isOptionSelected={option => option === value}
|
isOptionSelected={option => compareOptionAndValue(option, value)}
|
||||||
onSelectOption={selectOption}
|
onSelectOption={selectOption}
|
||||||
{loading}
|
{loading}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
export let footer = null
|
export let footer = null
|
||||||
export let tag = null
|
export let tag = null
|
||||||
export let helpText = null
|
export let helpText = null
|
||||||
|
export let compare
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
value = e.detail
|
value = e.detail
|
||||||
|
@ -65,6 +66,7 @@
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
{customPopoverHeight}
|
{customPopoverHeight}
|
||||||
{tag}
|
{tag}
|
||||||
|
{compare}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:click
|
on:click
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
export let useAnchorWidth = false
|
export let useAnchorWidth = false
|
||||||
export let dismissible = true
|
export let dismissible = true
|
||||||
export let offset = 5
|
export let offset = 5
|
||||||
export let offsetBelow
|
|
||||||
export let customHeight
|
export let customHeight
|
||||||
export let animate = true
|
export let animate = true
|
||||||
export let customZindex
|
export let customZindex
|
||||||
|
@ -89,7 +88,6 @@
|
||||||
maxWidth,
|
maxWidth,
|
||||||
useAnchorWidth,
|
useAnchorWidth,
|
||||||
offset,
|
offset,
|
||||||
offsetBelow,
|
|
||||||
customUpdate: handlePostionUpdate,
|
customUpdate: handlePostionUpdate,
|
||||||
}}
|
}}
|
||||||
use:clickOutside={{
|
use:clickOutside={{
|
||||||
|
|
|
@ -92,14 +92,7 @@ export const findAllMatchingComponents = (rootComponent, selector) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recurses through the component tree and finds all components.
|
* Finds the closes parent component which matches certain criteria
|
||||||
*/
|
|
||||||
export const findAllComponents = rootComponent => {
|
|
||||||
return findAllMatchingComponents(rootComponent, () => true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the closest parent component which matches certain criteria
|
|
||||||
*/
|
*/
|
||||||
export const findClosestMatchingComponent = (
|
export const findClosestMatchingComponent = (
|
||||||
rootComponent,
|
rootComponent,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import {
|
import {
|
||||||
findAllComponents,
|
|
||||||
findAllMatchingComponents,
|
findAllMatchingComponents,
|
||||||
findComponent,
|
findComponent,
|
||||||
findComponentPath,
|
findComponentPath,
|
||||||
|
@ -103,9 +102,6 @@ export const getAuthBindings = () => {
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets all bindings for environment variables
|
|
||||||
*/
|
|
||||||
export const getEnvironmentBindings = () => {
|
export const getEnvironmentBindings = () => {
|
||||||
let envVars = get(environment).variables
|
let envVars = get(environment).variables
|
||||||
return envVars.map(variable => {
|
return envVars.map(variable => {
|
||||||
|
@ -134,22 +130,26 @@ export const toBindingsArray = (valueMap, prefix, category) => {
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = {
|
let config = {
|
||||||
type: "context",
|
type: "context",
|
||||||
runtimeBinding: binding,
|
runtimeBinding: binding,
|
||||||
readableBinding: `${prefix}.${binding}`,
|
readableBinding: `${prefix}.${binding}`,
|
||||||
icon: "Brackets",
|
icon: "Brackets",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category) {
|
if (category) {
|
||||||
config.category = category
|
config.category = category
|
||||||
}
|
}
|
||||||
|
|
||||||
acc.push(config)
|
acc.push(config)
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to covert a map of readable bindings to runtime
|
* Utility - coverting a map of readable bindings to runtime
|
||||||
*/
|
*/
|
||||||
export const readableToRuntimeMap = (bindings, ctx) => {
|
export const readableToRuntimeMap = (bindings, ctx) => {
|
||||||
if (!bindings || !ctx) {
|
if (!bindings || !ctx) {
|
||||||
|
@ -162,7 +162,7 @@ export const readableToRuntimeMap = (bindings, ctx) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to covert a map of runtime bindings to readable bindings
|
* Utility - coverting a map of runtime bindings to readable
|
||||||
*/
|
*/
|
||||||
export const runtimeToReadableMap = (bindings, ctx) => {
|
export const runtimeToReadableMap = (bindings, ctx) => {
|
||||||
if (!bindings || !ctx) {
|
if (!bindings || !ctx) {
|
||||||
|
@ -188,23 +188,15 @@ export const getComponentBindableProperties = (asset, componentId) => {
|
||||||
if (!def?.context) {
|
if (!def?.context) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
|
||||||
|
|
||||||
// Get the bindings for the component
|
// Get the bindings for the component
|
||||||
const componentContext = {
|
return getProviderContextBindings(asset, component)
|
||||||
component,
|
|
||||||
definition: def,
|
|
||||||
contexts,
|
|
||||||
}
|
|
||||||
return generateComponentContextBindings(asset, componentContext)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all component contexts available to a certain component. This handles
|
* Gets all data provider components above a component.
|
||||||
* both global and local bindings, taking into account a component's position
|
|
||||||
* in the component tree.
|
|
||||||
*/
|
*/
|
||||||
export const getComponentContexts = (
|
export const getContextProviderComponents = (
|
||||||
asset,
|
asset,
|
||||||
componentId,
|
componentId,
|
||||||
type,
|
type,
|
||||||
|
@ -213,55 +205,30 @@ export const getComponentContexts = (
|
||||||
if (!asset || !componentId) {
|
if (!asset || !componentId) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
let map = {}
|
|
||||||
|
|
||||||
// Processes all contexts exposed by a component
|
// Get the component tree leading up to this component, ignoring the component
|
||||||
const processContexts = scope => component => {
|
// itself
|
||||||
|
const path = findComponentPath(asset.props, componentId)
|
||||||
|
if (!options?.includeSelf) {
|
||||||
|
path.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by only data provider components
|
||||||
|
return path.filter(component => {
|
||||||
const def = store.actions.components.getDefinition(component._component)
|
const def = store.actions.components.getDefinition(component._component)
|
||||||
if (!def?.context) {
|
if (!def?.context) {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
if (!map[component._id]) {
|
|
||||||
map[component._id] = {
|
// If no type specified, return anything that exposes context
|
||||||
component,
|
if (!type) {
|
||||||
definition: def,
|
return true
|
||||||
contexts: [],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise only match components with the specific context type
|
||||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||||
contexts.forEach(context => {
|
return contexts.find(context => context.type === type) != null
|
||||||
// Ensure type matches
|
})
|
||||||
if (type && context.type !== type) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Ensure scope matches
|
|
||||||
let contextScope = context.scope || "global"
|
|
||||||
if (contextScope !== scope) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Ensure the context is compatible with the component's current settings
|
|
||||||
if (!isContextCompatibleWithComponent(context, component)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
map[component._id].contexts.push(context)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process all global contexts
|
|
||||||
const allComponents = findAllComponents(asset.props)
|
|
||||||
allComponents.forEach(processContexts("global"))
|
|
||||||
|
|
||||||
// Process all local contexts
|
|
||||||
const localComponents = findComponentPath(asset.props, componentId)
|
|
||||||
localComponents.forEach(processContexts("local"))
|
|
||||||
|
|
||||||
// Exclude self if required
|
|
||||||
if (!options?.includeSelf) {
|
|
||||||
delete map[componentId]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only return components which provide at least 1 matching context
|
|
||||||
return Object.values(map).filter(x => x.contexts.length > 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -273,19 +240,20 @@ export const getActionProviders = (
|
||||||
actionType,
|
actionType,
|
||||||
options = { includeSelf: false }
|
options = { includeSelf: false }
|
||||||
) => {
|
) => {
|
||||||
if (!asset) {
|
if (!asset || !componentId) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all components
|
// Get the component tree leading up to this component, ignoring the component
|
||||||
const components = findAllComponents(asset.props)
|
// itself
|
||||||
|
const path = findComponentPath(asset.props, componentId)
|
||||||
|
if (!options?.includeSelf) {
|
||||||
|
path.pop()
|
||||||
|
}
|
||||||
|
|
||||||
// Find matching contexts and generate bindings
|
// Find matching contexts and generate bindings
|
||||||
let providers = []
|
let providers = []
|
||||||
components.forEach(component => {
|
path.forEach(component => {
|
||||||
if (!options?.includeSelf && component._id === componentId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const def = store.actions.components.getDefinition(component._component)
|
const def = store.actions.components.getDefinition(component._component)
|
||||||
const actions = (def?.actions || []).map(action => {
|
const actions = (def?.actions || []).map(action => {
|
||||||
return typeof action === "string" ? { type: action } : action
|
return typeof action === "string" ? { type: action } : action
|
||||||
|
@ -349,135 +317,142 @@ export const getDatasourceForProvider = (asset, component) => {
|
||||||
* Gets all bindable data properties from component data contexts.
|
* Gets all bindable data properties from component data contexts.
|
||||||
*/
|
*/
|
||||||
const getContextBindings = (asset, componentId) => {
|
const getContextBindings = (asset, componentId) => {
|
||||||
// Get all available contexts for this component
|
// Extract any components which provide data contexts
|
||||||
const componentContexts = getComponentContexts(asset, componentId)
|
const dataProviders = getContextProviderComponents(asset, componentId)
|
||||||
|
|
||||||
// Generate bindings for each context
|
// Generate bindings for all matching components
|
||||||
return componentContexts
|
return getProviderContextBindings(asset, dataProviders)
|
||||||
.map(componentContext => {
|
|
||||||
return generateComponentContextBindings(asset, componentContext)
|
|
||||||
})
|
|
||||||
.flat()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a set of bindings for a given component context
|
* Gets the context bindings exposed by a set of data provider components.
|
||||||
*/
|
*/
|
||||||
const generateComponentContextBindings = (asset, componentContext) => {
|
const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
const { component, definition, contexts } = componentContext
|
if (!asset || !dataProviders) {
|
||||||
if (!component || !definition || !contexts?.length) {
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure providers is an array
|
||||||
|
if (!Array.isArray(dataProviders)) {
|
||||||
|
dataProviders = [dataProviders]
|
||||||
|
}
|
||||||
|
|
||||||
// Create bindings for each data provider
|
// Create bindings for each data provider
|
||||||
let bindings = []
|
let bindings = []
|
||||||
contexts.forEach(context => {
|
dataProviders.forEach(component => {
|
||||||
if (!context?.type) {
|
const def = store.actions.components.getDefinition(component._component)
|
||||||
return
|
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||||
}
|
|
||||||
|
|
||||||
let schema
|
// Create bindings for each context block provided by this data provider
|
||||||
let table
|
contexts.forEach(context => {
|
||||||
let readablePrefix
|
if (!context?.type) {
|
||||||
let runtimeSuffix = context.suffix
|
|
||||||
|
|
||||||
if (context.type === "form") {
|
|
||||||
// Forms do not need table schemas
|
|
||||||
// Their schemas are built from their component field names
|
|
||||||
schema = buildFormSchema(component, asset)
|
|
||||||
readablePrefix = "Fields"
|
|
||||||
} else if (context.type === "static") {
|
|
||||||
// Static contexts are fully defined by the components
|
|
||||||
schema = {}
|
|
||||||
const values = context.values || []
|
|
||||||
values.forEach(value => {
|
|
||||||
schema[value.key] = {
|
|
||||||
name: value.label,
|
|
||||||
type: value.type || "string",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if (context.type === "schema") {
|
|
||||||
// Schema contexts are generated dynamically depending on their data
|
|
||||||
const datasource = getDatasourceForProvider(asset, component)
|
|
||||||
if (!datasource) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const info = getSchemaForDatasource(asset, datasource)
|
|
||||||
schema = info.schema
|
|
||||||
table = info.table
|
|
||||||
|
|
||||||
// Determine what to prefix bindings with
|
let schema
|
||||||
if (datasource.type === "jsonarray") {
|
let table
|
||||||
// For JSON arrays, use the array name as the readable prefix
|
let readablePrefix
|
||||||
const split = datasource.label.split(".")
|
let runtimeSuffix = context.suffix
|
||||||
readablePrefix = split[split.length - 1]
|
|
||||||
} else if (datasource.type === "viewV2") {
|
if (context.type === "form") {
|
||||||
// For views, use the view name
|
// Forms do not need table schemas
|
||||||
const view = Object.values(table?.views || {}).find(
|
// Their schemas are built from their component field names
|
||||||
view => view.id === datasource.id
|
schema = buildFormSchema(component, asset)
|
||||||
|
readablePrefix = "Fields"
|
||||||
|
} else if (context.type === "static") {
|
||||||
|
// Static contexts are fully defined by the components
|
||||||
|
schema = {}
|
||||||
|
const values = context.values || []
|
||||||
|
values.forEach(value => {
|
||||||
|
schema[value.key] = {
|
||||||
|
name: value.label,
|
||||||
|
type: value.type || "string",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (context.type === "schema") {
|
||||||
|
// Schema contexts are generated dynamically depending on their data
|
||||||
|
const datasource = getDatasourceForProvider(asset, component)
|
||||||
|
if (!datasource) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const info = getSchemaForDatasource(asset, datasource)
|
||||||
|
schema = info.schema
|
||||||
|
table = info.table
|
||||||
|
|
||||||
|
// Determine what to prefix bindings with
|
||||||
|
if (datasource.type === "jsonarray") {
|
||||||
|
// For JSON arrays, use the array name as the readable prefix
|
||||||
|
const split = datasource.label.split(".")
|
||||||
|
readablePrefix = split[split.length - 1]
|
||||||
|
} else if (datasource.type === "viewV2") {
|
||||||
|
// For views, use the view name
|
||||||
|
const view = Object.values(table?.views || {}).find(
|
||||||
|
view => view.id === datasource.id
|
||||||
|
)
|
||||||
|
readablePrefix = view?.name
|
||||||
|
} else {
|
||||||
|
// Otherwise use the table name
|
||||||
|
readablePrefix = info.table?.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!schema) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = Object.keys(schema).sort()
|
||||||
|
|
||||||
|
// Generate safe unique runtime prefix
|
||||||
|
let providerId = component._id
|
||||||
|
if (runtimeSuffix) {
|
||||||
|
providerId += `-${runtimeSuffix}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filterCategoryByContext(component, context)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeComponentId = makePropSafe(providerId)
|
||||||
|
|
||||||
|
// Create bindable properties for each schema field
|
||||||
|
keys.forEach(key => {
|
||||||
|
const fieldSchema = schema[key]
|
||||||
|
|
||||||
|
// Make safe runtime binding
|
||||||
|
const safeKey = key.split(".").map(makePropSafe).join(".")
|
||||||
|
const runtimeBinding = `${safeComponentId}.${safeKey}`
|
||||||
|
|
||||||
|
// Optionally use a prefix with readable bindings
|
||||||
|
let readableBinding = component._instanceName
|
||||||
|
if (readablePrefix) {
|
||||||
|
readableBinding += `.${readablePrefix}`
|
||||||
|
}
|
||||||
|
readableBinding += `.${fieldSchema.name || key}`
|
||||||
|
|
||||||
|
const bindingCategory = getComponentBindingCategory(
|
||||||
|
component,
|
||||||
|
context,
|
||||||
|
def
|
||||||
)
|
)
|
||||||
readablePrefix = view?.name
|
|
||||||
} else {
|
|
||||||
// Otherwise use the table name
|
|
||||||
readablePrefix = info.table?.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!schema) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = Object.keys(schema).sort()
|
// Create the binding object
|
||||||
|
bindings.push({
|
||||||
// Generate safe unique runtime prefix
|
type: "context",
|
||||||
let providerId = component._id
|
runtimeBinding,
|
||||||
if (runtimeSuffix) {
|
readableBinding,
|
||||||
providerId += `-${runtimeSuffix}`
|
// Field schema and provider are required to construct relationship
|
||||||
}
|
// datasource options, based on bindable properties
|
||||||
const safeComponentId = makePropSafe(providerId)
|
fieldSchema,
|
||||||
|
providerId,
|
||||||
// Create bindable properties for each schema field
|
// Table ID is used by JSON fields to know what table the field is in
|
||||||
keys.forEach(key => {
|
tableId: table?._id,
|
||||||
const fieldSchema = schema[key]
|
component: component._component,
|
||||||
|
category: bindingCategory.category,
|
||||||
// Make safe runtime binding
|
icon: bindingCategory.icon,
|
||||||
const safeKey = key.split(".").map(makePropSafe).join(".")
|
display: {
|
||||||
const runtimeBinding = `${safeComponentId}.${safeKey}`
|
name: fieldSchema.name || key,
|
||||||
|
type: fieldSchema.type,
|
||||||
// Optionally use a prefix with readable bindings
|
},
|
||||||
let readableBinding = component._instanceName
|
})
|
||||||
if (readablePrefix) {
|
|
||||||
readableBinding += `.${readablePrefix}`
|
|
||||||
}
|
|
||||||
readableBinding += `.${fieldSchema.name || key}`
|
|
||||||
|
|
||||||
// Determine which category this binding belongs in
|
|
||||||
const bindingCategory = getComponentBindingCategory(
|
|
||||||
component,
|
|
||||||
context,
|
|
||||||
definition
|
|
||||||
)
|
|
||||||
|
|
||||||
// Temporarily append scope for debugging
|
|
||||||
const scope = `[${(context.scope || "global").toUpperCase()}]`
|
|
||||||
|
|
||||||
// Create the binding object
|
|
||||||
bindings.push({
|
|
||||||
type: "context",
|
|
||||||
runtimeBinding,
|
|
||||||
readableBinding: `${scope} ${readableBinding}`,
|
|
||||||
// Field schema and provider are required to construct relationship
|
|
||||||
// datasource options, based on bindable properties
|
|
||||||
fieldSchema,
|
|
||||||
providerId,
|
|
||||||
// Table ID is used by JSON fields to know what table the field is in
|
|
||||||
tableId: table?._id,
|
|
||||||
component: component._component,
|
|
||||||
category: bindingCategory.category,
|
|
||||||
icon: bindingCategory.icon,
|
|
||||||
display: {
|
|
||||||
name: `${scope} ${fieldSchema.name || key}`,
|
|
||||||
type: fieldSchema.type,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -485,38 +460,25 @@ const generateComponentContextBindings = (asset, componentContext) => {
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Exclude a data context based on the component settings
|
||||||
* Checks if a certain data context is compatible with a certain instance of a
|
const filterCategoryByContext = (component, context) => {
|
||||||
* configured component.
|
const { _component } = component
|
||||||
*/
|
|
||||||
const isContextCompatibleWithComponent = (context, component) => {
|
|
||||||
if (!component) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const { _component, actionType } = component
|
|
||||||
const { type } = context
|
|
||||||
|
|
||||||
// Certain types of form blocks only allow certain contexts
|
|
||||||
if (_component.endsWith("formblock")) {
|
if (_component.endsWith("formblock")) {
|
||||||
if (
|
if (
|
||||||
(actionType === "Create" && type === "schema") ||
|
(component.actionType === "Create" && context.type === "schema") ||
|
||||||
(actionType === "View" && type === "form")
|
(component.actionType === "View" && context.type === "form")
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow the context by default
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich binding category information for certain components
|
// Enrich binding category information for certain components
|
||||||
const getComponentBindingCategory = (component, context, def) => {
|
const getComponentBindingCategory = (component, context, def) => {
|
||||||
// Default category to component name
|
|
||||||
let icon = def.icon
|
let icon = def.icon
|
||||||
let category = component._instanceName
|
let category = component._instanceName
|
||||||
|
|
||||||
// Form block edge case
|
|
||||||
if (component._component.endsWith("formblock")) {
|
if (component._component.endsWith("formblock")) {
|
||||||
if (context.type === "form") {
|
if (context.type === "form") {
|
||||||
category = `${component._instanceName} - Fields`
|
category = `${component._instanceName} - Fields`
|
||||||
|
@ -534,7 +496,7 @@ const getComponentBindingCategory = (component, context, def) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all bindable properties from the logged-in user.
|
* Gets all bindable properties from the logged in user.
|
||||||
*/
|
*/
|
||||||
export const getUserBindings = () => {
|
export const getUserBindings = () => {
|
||||||
let bindings = []
|
let bindings = []
|
||||||
|
@ -604,7 +566,6 @@ const getDeviceBindings = () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all selected rows bindings for tables in the current asset.
|
* Gets all selected rows bindings for tables in the current asset.
|
||||||
* TODO: remove in future because we don't need a separate store for this
|
|
||||||
*/
|
*/
|
||||||
const getSelectedRowsBindings = asset => {
|
const getSelectedRowsBindings = asset => {
|
||||||
let bindings = []
|
let bindings = []
|
||||||
|
@ -647,9 +608,6 @@ const getSelectedRowsBindings = asset => {
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a state binding for a certain key name
|
|
||||||
*/
|
|
||||||
export const makeStateBinding = key => {
|
export const makeStateBinding = key => {
|
||||||
return {
|
return {
|
||||||
type: "context",
|
type: "context",
|
||||||
|
@ -704,9 +662,6 @@ const getUrlBindings = asset => {
|
||||||
return urlParamBindings.concat([queryParamsBinding])
|
return urlParamBindings.concat([queryParamsBinding])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates all bindings for role IDs
|
|
||||||
*/
|
|
||||||
const getRoleBindings = () => {
|
const getRoleBindings = () => {
|
||||||
return (get(rolesStore) || []).map(role => {
|
return (get(rolesStore) || []).map(role => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
import { createHistoryStore } from "builderStore/store/history"
|
import { createHistoryStore } from "builderStore/store/history"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import { getHoverStore } from "./store/hover"
|
||||||
|
|
||||||
export const store = getFrontendStore()
|
export const store = getFrontendStore()
|
||||||
export const automationStore = getAutomationStore()
|
export const automationStore = getAutomationStore()
|
||||||
|
@ -16,6 +17,7 @@ export const themeStore = getThemeStore()
|
||||||
export const temporalStore = getTemporalStore()
|
export const temporalStore = getTemporalStore()
|
||||||
export const userStore = getUserStore()
|
export const userStore = getUserStore()
|
||||||
export const deploymentStore = getDeploymentStore()
|
export const deploymentStore = getDeploymentStore()
|
||||||
|
export const hoverStore = getHoverStore()
|
||||||
|
|
||||||
// Setup history for screens
|
// Setup history for screens
|
||||||
export const screenHistoryStore = createHistoryStore({
|
export const screenHistoryStore = createHistoryStore({
|
||||||
|
|
|
@ -92,9 +92,6 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
// Onboarding
|
// Onboarding
|
||||||
onboarding: false,
|
onboarding: false,
|
||||||
tourNodes: null,
|
tourNodes: null,
|
||||||
|
|
||||||
// UI state
|
|
||||||
hoveredComponentId: null,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFrontendStore = () => {
|
export const getFrontendStore = () => {
|
||||||
|
@ -709,9 +706,10 @@ export const getFrontendStore = () => {
|
||||||
else {
|
else {
|
||||||
if (setting.type === "dataProvider") {
|
if (setting.type === "dataProvider") {
|
||||||
// Validate data provider exists, or else clear it
|
// Validate data provider exists, or else clear it
|
||||||
const providers = findAllMatchingComponents(
|
const treeId = parent?._id || component._id
|
||||||
screen?.props,
|
const path = findComponentPath(screen?.props, treeId)
|
||||||
component => component._component?.endsWith("/dataprovider")
|
const providers = path.filter(component =>
|
||||||
|
component._component?.endsWith("/dataprovider")
|
||||||
)
|
)
|
||||||
// Validate non-empty values
|
// Validate non-empty values
|
||||||
const valid = providers?.some(dp => value.includes?.(dp._id))
|
const valid = providers?.some(dp => value.includes?.(dp._id))
|
||||||
|
@ -733,16 +731,6 @@ export const getFrontendStore = () => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all existing components of this type so that we can give this
|
|
||||||
// component a unique name
|
|
||||||
const screen = get(selectedScreen).props
|
|
||||||
const otherComponents = findAllMatchingComponents(
|
|
||||||
screen,
|
|
||||||
x => x._component === definition.component && x._id !== screen._id
|
|
||||||
)
|
|
||||||
let name = definition.friendlyName || definition.name
|
|
||||||
name = `${name} ${otherComponents.length + 1}`
|
|
||||||
|
|
||||||
// Generate basic component structure
|
// Generate basic component structure
|
||||||
let instance = {
|
let instance = {
|
||||||
_id: Helpers.uuid(),
|
_id: Helpers.uuid(),
|
||||||
|
@ -752,7 +740,7 @@ export const getFrontendStore = () => {
|
||||||
hover: {},
|
hover: {},
|
||||||
active: {},
|
active: {},
|
||||||
},
|
},
|
||||||
_instanceName: name,
|
_instanceName: `New ${definition.friendlyName || definition.name}`,
|
||||||
...presetProps,
|
...presetProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1424,18 +1412,6 @@ export const getFrontendStore = () => {
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
hover: (componentId, notifyClient = true) => {
|
|
||||||
if (componentId === get(store).hoveredComponentId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
store.update(state => {
|
|
||||||
state.hoveredComponentId = componentId
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
if (notifyClient) {
|
|
||||||
store.actions.preview.sendEvent("hover-component", componentId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
links: {
|
links: {
|
||||||
save: async (url, title) => {
|
save: async (url, title) => {
|
||||||
|
|
27
packages/builder/src/builderStore/store/hover.js
Normal file
27
packages/builder/src/builderStore/store/hover.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { get, writable } from "svelte/store"
|
||||||
|
import { store as builder } from "builderStore"
|
||||||
|
|
||||||
|
export const getHoverStore = () => {
|
||||||
|
const initialValue = {
|
||||||
|
componentId: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = writable(initialValue)
|
||||||
|
|
||||||
|
const update = (componentId, notifyClient = true) => {
|
||||||
|
if (componentId === get(store).componentId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
store.update(state => {
|
||||||
|
state.componentId = componentId
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
if (notifyClient) {
|
||||||
|
builder.actions.preview.sendEvent("hover-component", componentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { update },
|
||||||
|
}
|
||||||
|
}
|
|
@ -184,8 +184,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(idx === 0 && automation.trigger?.event === "row:update") ||
|
idx === 0 &&
|
||||||
automation.trigger?.event === "row:save"
|
(automation.trigger?.event === "row:update" ||
|
||||||
|
automation.trigger?.event === "row:save")
|
||||||
) {
|
) {
|
||||||
if (name !== "id" && name !== "revision") return `trigger.row.${name}`
|
if (name !== "id" && name !== "revision") return `trigger.row.${name}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,8 +88,12 @@
|
||||||
hasValidated = false
|
hasValidated = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$: valid =
|
$: valid =
|
||||||
getErrorCount(errors) === 0 && allRequiredAttributesSet(relationshipType)
|
getErrorCount(errors) === 0 &&
|
||||||
|
allRequiredAttributesSet(relationshipType) &&
|
||||||
|
fromId &&
|
||||||
|
toId
|
||||||
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
|
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
|
||||||
$: isManyToOne =
|
$: isManyToOne =
|
||||||
relationshipType === RelationshipType.MANY_TO_ONE ||
|
relationshipType === RelationshipType.MANY_TO_ONE ||
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { getComponentContexts } from "builderStore/dataBinding"
|
import { getContextProviderComponents } from "builderStore/dataBinding"
|
||||||
|
import { store } from "builderStore"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
|
||||||
// Generates bindings for all components that provider "datasource like"
|
// Generates bindings for all components that provider "datasource like"
|
||||||
|
@ -7,49 +8,58 @@ import { capitalise } from "helpers"
|
||||||
// Some examples are saving rows or duplicating rows.
|
// Some examples are saving rows or duplicating rows.
|
||||||
export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
||||||
// Get all form context providers
|
// Get all form context providers
|
||||||
const formComponentContexts = getComponentContexts(
|
const formComponents = getContextProviderComponents(
|
||||||
asset,
|
asset,
|
||||||
componentId,
|
componentId,
|
||||||
"form",
|
"form",
|
||||||
{
|
{ includeSelf: nested }
|
||||||
includeSelf: nested,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get all schema context providers
|
// Get all schema context providers
|
||||||
const schemaComponentContexts = getComponentContexts(
|
const schemaComponents = getContextProviderComponents(
|
||||||
asset,
|
asset,
|
||||||
componentId,
|
componentId,
|
||||||
"schema",
|
"schema",
|
||||||
{
|
{ includeSelf: nested }
|
||||||
includeSelf: nested,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Generate contexts for all form providers
|
||||||
|
const formContexts = formComponents.map(component => ({
|
||||||
|
component,
|
||||||
|
context: extractComponentContext(component, "form"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Generate contexts for all schema providers
|
||||||
|
const schemaContexts = schemaComponents.map(component => ({
|
||||||
|
component,
|
||||||
|
context: extractComponentContext(component, "schema"),
|
||||||
|
}))
|
||||||
|
|
||||||
// Check for duplicate contexts by the same component. In this case, attempt
|
// Check for duplicate contexts by the same component. In this case, attempt
|
||||||
// to label contexts with their suffixes
|
// to label contexts with their suffixes
|
||||||
schemaComponentContexts.forEach(schemaContext => {
|
schemaContexts.forEach(schemaContext => {
|
||||||
// Check if we have a form context for this component
|
// Check if we have a form context for this component
|
||||||
const id = schemaContext.component._id
|
const id = schemaContext.component._id
|
||||||
const existing = formComponentContexts.find(x => x.component._id === id)
|
const existing = formContexts.find(x => x.component._id === id)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
if (existing.contexts[0].suffix) {
|
if (existing.context.suffix) {
|
||||||
const suffix = capitalise(existing.contexts[0].suffix)
|
const suffix = capitalise(existing.context.suffix)
|
||||||
existing.readableSuffix = ` - ${suffix}`
|
existing.readableSuffix = ` - ${suffix}`
|
||||||
}
|
}
|
||||||
if (schemaContext.contexts[0].suffix) {
|
if (schemaContext.context.suffix) {
|
||||||
const suffix = capitalise(schemaContext.contexts[0].suffix)
|
const suffix = capitalise(schemaContext.context.suffix)
|
||||||
schemaContext.readableSuffix = ` - ${suffix}`
|
schemaContext.readableSuffix = ` - ${suffix}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Generate bindings for all contexts
|
// Generate bindings for all contexts
|
||||||
const allContexts = formComponentContexts.concat(schemaComponentContexts)
|
const allContexts = formContexts.concat(schemaContexts)
|
||||||
return allContexts.map(({ component, contexts, readableSuffix }) => {
|
return allContexts.map(({ component, context, readableSuffix }) => {
|
||||||
let readableBinding = component._instanceName
|
let readableBinding = component._instanceName
|
||||||
let runtimeBinding = component._id
|
let runtimeBinding = component._id
|
||||||
if (contexts[0].suffix) {
|
if (context.suffix) {
|
||||||
runtimeBinding += `-${contexts[0].suffix}`
|
runtimeBinding += `-${context.suffix}`
|
||||||
}
|
}
|
||||||
if (readableSuffix) {
|
if (readableSuffix) {
|
||||||
readableBinding += readableSuffix
|
readableBinding += readableSuffix
|
||||||
|
@ -60,3 +70,13 @@ export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gets a context definition of a certain type from a component definition
|
||||||
|
const extractComponentContext = (component, contextType) => {
|
||||||
|
const def = store.actions.components.getDefinition(component?._component)
|
||||||
|
if (!def) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||||
|
return contexts.find(context => context?.type === contextType)
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { getEventContextBindings } from "builderStore/dataBinding"
|
import { getEventContextBindings } from "builderStore/dataBinding"
|
||||||
|
import { cloneDeep, isEqual } from "lodash/fp"
|
||||||
|
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let componentBindings
|
export let componentBindings
|
||||||
|
@ -17,8 +18,13 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let focusItem
|
let focusItem
|
||||||
|
let cachedValue
|
||||||
|
|
||||||
$: buttonList = sanitizeValue(value) || []
|
$: if (!isEqual(value, cachedValue)) {
|
||||||
|
cachedValue = cloneDeep(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: buttonList = sanitizeValue(cachedValue) || []
|
||||||
$: buttonCount = buttonList.length
|
$: buttonCount = buttonList.length
|
||||||
$: eventContextBindings = getEventContextBindings({
|
$: eventContextBindings = getEventContextBindings({
|
||||||
componentInstance,
|
componentInstance,
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select } from "@budibase/bbui"
|
import { Select } from "@budibase/bbui"
|
||||||
import { makePropSafe } from "@budibase/string-templates"
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
import { currentAsset } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { findAllMatchingComponents } from "builderStore/componentUtils"
|
import { findComponentPath } from "builderStore/componentUtils"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
||||||
|
|
||||||
$: providers = findAllMatchingComponents($currentAsset?.props, c =>
|
$: path = findComponentPath($currentAsset?.props, $store.selectedComponentId)
|
||||||
c._component?.endsWith("/dataprovider")
|
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
|
getContextProviderComponents,
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
|
@ -29,7 +30,6 @@
|
||||||
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
||||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { findAllComponents } from "builderStore/componentUtils"
|
|
||||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
import DataSourceCategory from "components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
|
import DataSourceCategory from "components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -75,13 +75,12 @@
|
||||||
...query,
|
...query,
|
||||||
type: "query",
|
type: "query",
|
||||||
}))
|
}))
|
||||||
$: dataProviders = findAllComponents($currentAsset.props)
|
$: contextProviders = getContextProviderComponents(
|
||||||
.filter(component => {
|
$currentAsset,
|
||||||
return (
|
$store.selectedComponentId
|
||||||
component._component?.endsWith("/dataprovider") &&
|
)
|
||||||
component._id !== $store.selectedComponentId
|
$: dataProviders = contextProviders
|
||||||
)
|
.filter(component => component._component?.endsWith("/dataprovider"))
|
||||||
})
|
|
||||||
.map(provider => ({
|
.map(provider => ({
|
||||||
label: provider._instanceName,
|
label: provider._instanceName,
|
||||||
name: provider._instanceName,
|
name: provider._instanceName,
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
export let bindingDrawerLeft
|
export let bindingDrawerLeft
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let customButtonText = null
|
export let customButtonText = null
|
||||||
|
export let compare = (option, value) => option === value
|
||||||
|
|
||||||
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
||||||
name,
|
name,
|
||||||
|
@ -112,7 +113,12 @@
|
||||||
on:blur={changed}
|
on:blur={changed}
|
||||||
/>
|
/>
|
||||||
{#if options}
|
{#if options}
|
||||||
<Select bind:value={field.value} on:change={changed} {options} />
|
<Select
|
||||||
|
bind:value={field.value}
|
||||||
|
{compare}
|
||||||
|
on:change={changed}
|
||||||
|
{options}
|
||||||
|
/>
|
||||||
{:else if bindings && bindings.length}
|
{:else if bindings && bindings.length}
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
{bindings}
|
{bindings}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import KeyValueBuilder from "../KeyValueBuilder.svelte"
|
import KeyValueBuilder from "../KeyValueBuilder.svelte"
|
||||||
import { SchemaTypeOptions } from "constants/backend"
|
import { SchemaTypeOptionsExpanded } from "constants/backend"
|
||||||
|
|
||||||
export let schema
|
export let schema
|
||||||
export let onSchemaChange = () => {}
|
export let onSchemaChange = () => {}
|
||||||
|
@ -24,6 +24,7 @@
|
||||||
object={schema}
|
object={schema}
|
||||||
name="field"
|
name="field"
|
||||||
headings
|
headings
|
||||||
options={SchemaTypeOptions}
|
options={SchemaTypeOptionsExpanded}
|
||||||
|
compare={(option, value) => option.type === value.type}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
PaginationTypes,
|
PaginationTypes,
|
||||||
RawRestBodyTypes,
|
RawRestBodyTypes,
|
||||||
RestBodyTypes as bodyTypes,
|
RestBodyTypes as bodyTypes,
|
||||||
SchemaTypeOptions,
|
SchemaTypeOptionsExpanded,
|
||||||
} from "constants/backend"
|
} from "constants/backend"
|
||||||
import JSONPreview from "components/integration/JSONPreview.svelte"
|
import JSONPreview from "components/integration/JSONPreview.svelte"
|
||||||
import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte"
|
import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte"
|
||||||
|
@ -97,9 +97,7 @@
|
||||||
$: schemaReadOnly = !responseSuccess
|
$: schemaReadOnly = !responseSuccess
|
||||||
$: variablesReadOnly = !responseSuccess
|
$: variablesReadOnly = !responseSuccess
|
||||||
$: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly)
|
$: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly)
|
||||||
$: hasSchema =
|
$: hasSchema = Object.keys(schema || {}).length !== 0
|
||||||
Object.keys(schema || {}).length !== 0 ||
|
|
||||||
Object.keys(query?.schema || {}).length !== 0
|
|
||||||
|
|
||||||
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
|
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
|
||||||
|
|
||||||
|
@ -161,7 +159,7 @@
|
||||||
newQuery.fields.queryString = queryString
|
newQuery.fields.queryString = queryString
|
||||||
newQuery.fields.authConfigId = authConfigId
|
newQuery.fields.authConfigId = authConfigId
|
||||||
newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders)
|
newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders)
|
||||||
newQuery.schema = restUtils.fieldsToSchema(schema)
|
newQuery.schema = schema
|
||||||
|
|
||||||
return newQuery
|
return newQuery
|
||||||
}
|
}
|
||||||
|
@ -231,6 +229,14 @@
|
||||||
notifications.info("Request did not return any data")
|
notifications.info("Request did not return any data")
|
||||||
} else {
|
} else {
|
||||||
response.info = response.info || { code: 200 }
|
response.info = response.info || { code: 200 }
|
||||||
|
// if existing schema, copy over what it is
|
||||||
|
if (schema) {
|
||||||
|
for (let [name, field] of Object.entries(schema)) {
|
||||||
|
if (response.schema[name]) {
|
||||||
|
response.schema[name] = field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
schema = response.schema
|
schema = response.schema
|
||||||
notifications.success("Request sent successfully")
|
notifications.success("Request sent successfully")
|
||||||
}
|
}
|
||||||
|
@ -386,6 +392,7 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
query = getSelectedQuery()
|
query = getSelectedQuery()
|
||||||
|
schema = query.schema
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Clear any unsaved changes to the datasource
|
// Clear any unsaved changes to the datasource
|
||||||
|
@ -416,7 +423,6 @@
|
||||||
query.fields.path = `${datasource.config.url}/${path ? path : ""}`
|
query.fields.path = `${datasource.config.url}/${path ? path : ""}`
|
||||||
}
|
}
|
||||||
url = buildUrl(query.fields.path, breakQs)
|
url = buildUrl(query.fields.path, breakQs)
|
||||||
schema = restUtils.schemaToFields(query.schema)
|
|
||||||
requestBindings = restUtils.queryParametersToKeyValue(query.parameters)
|
requestBindings = restUtils.queryParametersToKeyValue(query.parameters)
|
||||||
authConfigId = getAuthConfigId()
|
authConfigId = getAuthConfigId()
|
||||||
if (!query.fields.disabledHeaders) {
|
if (!query.fields.disabledHeaders) {
|
||||||
|
@ -682,10 +688,11 @@
|
||||||
bind:object={schema}
|
bind:object={schema}
|
||||||
name="schema"
|
name="schema"
|
||||||
headings
|
headings
|
||||||
options={SchemaTypeOptions}
|
options={SchemaTypeOptionsExpanded}
|
||||||
menuItems={schemaMenuItems}
|
menuItems={schemaMenuItems}
|
||||||
showMenu={!schemaReadOnly}
|
showMenu={!schemaReadOnly}
|
||||||
readOnly={schemaReadOnly}
|
readOnly={schemaReadOnly}
|
||||||
|
compare={(option, value) => option.type === value.type}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -271,6 +271,11 @@ export const SchemaTypeOptions = [
|
||||||
{ label: "Datetime", value: "datetime" },
|
{ label: "Datetime", value: "datetime" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const SchemaTypeOptionsExpanded = SchemaTypeOptions.map(el => ({
|
||||||
|
...el,
|
||||||
|
value: { type: el.value },
|
||||||
|
}))
|
||||||
|
|
||||||
export const RawRestBodyTypes = {
|
export const RawRestBodyTypes = {
|
||||||
NONE: "none",
|
NONE: "none",
|
||||||
FORM: "form",
|
FORM: "form",
|
||||||
|
|
|
@ -1,26 +1,6 @@
|
||||||
import { IntegrationTypes } from "constants/backend"
|
import { IntegrationTypes } from "constants/backend"
|
||||||
import { findHBSBlocks } from "@budibase/string-templates"
|
import { findHBSBlocks } from "@budibase/string-templates"
|
||||||
|
|
||||||
export function schemaToFields(schema) {
|
|
||||||
const response = {}
|
|
||||||
if (schema && typeof schema === "object") {
|
|
||||||
for (let [field, value] of Object.entries(schema)) {
|
|
||||||
response[field] = value?.type || "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fieldsToSchema(fields) {
|
|
||||||
const response = {}
|
|
||||||
if (fields && typeof fields === "object") {
|
|
||||||
for (let [name, type] of Object.entries(fields)) {
|
|
||||||
response[name] = { name, type }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
export function breakQueryString(qs) {
|
export function breakQueryString(qs) {
|
||||||
if (!qs) {
|
if (!qs) {
|
||||||
return {}
|
return {}
|
||||||
|
@ -184,10 +164,8 @@ export const parseToCsv = (headers, rows) => {
|
||||||
export default {
|
export default {
|
||||||
breakQueryString,
|
breakQueryString,
|
||||||
buildQueryString,
|
buildQueryString,
|
||||||
fieldsToSchema,
|
|
||||||
flipHeaderState,
|
flipHeaderState,
|
||||||
keyValueToQueryParameters,
|
keyValueToQueryParameters,
|
||||||
parseToCsv,
|
parseToCsv,
|
||||||
queryParametersToKeyValue,
|
queryParametersToKeyValue,
|
||||||
schemaToFields,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,27 @@
|
||||||
import { PlanType } from "@budibase/types"
|
import { PlanType } from "@budibase/types"
|
||||||
|
|
||||||
export function getFormattedPlanName(userPlanType) {
|
export function getFormattedPlanName(userPlanType) {
|
||||||
let planName = "Free"
|
let planName
|
||||||
if (userPlanType === PlanType.PREMIUM_PLUS) {
|
switch (userPlanType) {
|
||||||
planName = "Premium"
|
case PlanType.PRO:
|
||||||
} else if (userPlanType === PlanType.ENTERPRISE_BASIC) {
|
planName = "Pro"
|
||||||
planName = "Enterprise"
|
break
|
||||||
|
case PlanType.TEAM:
|
||||||
|
planName = "Team"
|
||||||
|
break
|
||||||
|
case PlanType.PREMIUM:
|
||||||
|
case PlanType.PREMIUM_PLUS:
|
||||||
|
planName = "Premium"
|
||||||
|
break
|
||||||
|
case PlanType.BUSINESS:
|
||||||
|
planName = "Business"
|
||||||
|
break
|
||||||
|
case PlanType.ENTERPRISE_BASIC:
|
||||||
|
case PlanType.ENTERPRISE:
|
||||||
|
planName = "Enterprise"
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
planName = "Free" // Default to "Free" if the type is not explicitly handled
|
||||||
}
|
}
|
||||||
return `${planName} Plan`
|
return `${planName} Plan`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import { store, selectedScreen, currentAsset } from "builderStore"
|
import { store, selectedScreen, currentAsset, hoverStore } from "builderStore"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import {
|
import {
|
||||||
ProgressCircle,
|
ProgressCircle,
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
} else if (type === "select-component" && data.id) {
|
} else if (type === "select-component" && data.id) {
|
||||||
$store.selectedComponentId = data.id
|
$store.selectedComponentId = data.id
|
||||||
} else if (type === "hover-component") {
|
} else if (type === "hover-component") {
|
||||||
store.actions.components.hover(data.id, false)
|
hoverStore.actions.update(data.id, false)
|
||||||
} else if (type === "update-prop") {
|
} else if (type === "update-prop") {
|
||||||
await store.actions.components.updateSetting(data.prop, data.value)
|
await store.actions.components.updateSetting(data.prop, data.value)
|
||||||
} else if (type === "update-styles") {
|
} else if (type === "update-styles") {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
selectedComponentPath,
|
selectedComponentPath,
|
||||||
selectedComponent,
|
selectedComponent,
|
||||||
selectedScreen,
|
selectedScreen,
|
||||||
|
hoverStore,
|
||||||
} from "builderStore"
|
} from "builderStore"
|
||||||
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
@ -90,7 +91,7 @@
|
||||||
return findComponentPath($selectedComponent, component._id)?.length > 0
|
return findComponentPath($selectedComponent, component._id)?.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const hover = store.actions.components.hover
|
const hover = hoverStore.actions.update
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -111,7 +112,7 @@
|
||||||
on:dragover={dragover(component, index)}
|
on:dragover={dragover(component, index)}
|
||||||
on:iconClick={() => toggleNodeOpen(component._id)}
|
on:iconClick={() => toggleNodeOpen(component._id)}
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}
|
||||||
hovering={$store.hoveredComponentId === component._id}
|
hovering={$hoverStore.componentId === component._id}
|
||||||
on:mouseenter={() => hover(component._id)}
|
on:mouseenter={() => hover(component._id)}
|
||||||
on:mouseleave={() => hover(null)}
|
on:mouseleave={() => hover(null)}
|
||||||
text={getComponentText(component)}
|
text={getComponentText(component)}
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { notifications, Icon, Body } from "@budibase/bbui"
|
import { notifications, Icon, Body } from "@budibase/bbui"
|
||||||
import { isActive, goto } from "@roxi/routify"
|
import { isActive, goto } from "@roxi/routify"
|
||||||
import { store, selectedScreen, userSelectedResourceMap } from "builderStore"
|
import {
|
||||||
|
store,
|
||||||
|
selectedScreen,
|
||||||
|
userSelectedResourceMap,
|
||||||
|
hoverStore,
|
||||||
|
} from "builderStore"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import ComponentTree from "./ComponentTree.svelte"
|
import ComponentTree from "./ComponentTree.svelte"
|
||||||
import { dndStore, DropPosition } from "./dndStore.js"
|
import { dndStore, DropPosition } from "./dndStore.js"
|
||||||
|
@ -36,7 +41,7 @@
|
||||||
scrolling = e.target.scrollTop !== 0
|
scrolling = e.target.scrollTop !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const hover = store.actions.components.hover
|
const hover = hoverStore.actions.update
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="components">
|
<div class="components">
|
||||||
|
@ -60,7 +65,7 @@
|
||||||
icon="WebPage"
|
icon="WebPage"
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}
|
||||||
on:click={() => ($store.selectedComponentId = screenComponentId)}
|
on:click={() => ($store.selectedComponentId = screenComponentId)}
|
||||||
hovering={$store.hoveredComponentId === screenComponentId}
|
hovering={$hoverStore.componentId === screenComponentId}
|
||||||
on:mouseenter={() => hover(screenComponentId)}
|
on:mouseenter={() => hover(screenComponentId)}
|
||||||
on:mouseleave={() => hover(null)}
|
on:mouseleave={() => hover(null)}
|
||||||
id="component-screen"
|
id="component-screen"
|
||||||
|
@ -79,7 +84,7 @@
|
||||||
: "VisibilityOff"}
|
: "VisibilityOff"}
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}
|
||||||
on:click={() => ($store.selectedComponentId = navComponentId)}
|
on:click={() => ($store.selectedComponentId = navComponentId)}
|
||||||
hovering={$store.hoveredComponentId === navComponentId}
|
hovering={$hoverStore.componentId === navComponentId}
|
||||||
on:mouseenter={() => hover(navComponentId)}
|
on:mouseenter={() => hover(navComponentId)}
|
||||||
on:mouseleave={() => hover(null)}
|
on:mouseleave={() => hover(null)}
|
||||||
id="component-nav"
|
id="component-nav"
|
||||||
|
|
|
@ -15,9 +15,9 @@
|
||||||
<Content showMobileNav>
|
<Content showMobileNav>
|
||||||
<SideNav slot="side-nav">
|
<SideNav slot="side-nav">
|
||||||
<SideNavItem
|
<SideNavItem
|
||||||
text="Automation History"
|
text="Automations"
|
||||||
url={$url("./automation-history")}
|
url={$url("./automations")}
|
||||||
active={$isActive("./automation-history")}
|
active={$isActive("./automations")}
|
||||||
/>
|
/>
|
||||||
<SideNavItem
|
<SideNavItem
|
||||||
text="Backups"
|
text="Backups"
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
Body,
|
Body,
|
||||||
Heading,
|
Heading,
|
||||||
Divider,
|
Divider,
|
||||||
|
Toggle,
|
||||||
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
|
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
|
||||||
import StatusRenderer from "./_components/StatusRenderer.svelte"
|
import StatusRenderer from "./_components/StatusRenderer.svelte"
|
||||||
|
@ -16,7 +18,7 @@
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
import { getContext, onDestroy, onMount } from "svelte"
|
import { getContext, onDestroy, onMount } from "svelte"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { auth, licensing, admin } from "stores/portal"
|
import { auth, licensing, admin, apps } from "stores/portal"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
import Portal from "svelte-portal"
|
import Portal from "svelte-portal"
|
||||||
|
|
||||||
|
@ -35,9 +37,13 @@
|
||||||
let timeRange = null
|
let timeRange = null
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
|
$: app = $apps.find(app => app.devId === $store.appId?.includes(app.appId))
|
||||||
$: licensePlan = $auth.user?.license?.plan
|
$: licensePlan = $auth.user?.license?.plan
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
$: fetchLogs(automationId, status, page, timeRange)
|
$: fetchLogs(automationId, status, page, timeRange)
|
||||||
|
$: isCloud = $admin.cloud
|
||||||
|
|
||||||
|
$: chainAutomations = app?.automations?.chainAutomations ?? !isCloud
|
||||||
|
|
||||||
const timeOptions = [
|
const timeOptions = [
|
||||||
{ value: "90-d", label: "Past 90 days" },
|
{ value: "90-d", label: "Past 90 days" },
|
||||||
|
@ -124,6 +130,18 @@
|
||||||
sidePanel.open()
|
sidePanel.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function save({ detail }) {
|
||||||
|
try {
|
||||||
|
await apps.update($store.appId, {
|
||||||
|
automations: {
|
||||||
|
chainAutomations: detail,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error updating automation chaining setting")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await automationStore.actions.fetch()
|
await automationStore.actions.fetch()
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
@ -150,11 +168,30 @@
|
||||||
|
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading>Automation History</Heading>
|
<Heading>Automations</Heading>
|
||||||
<Body>View the automations your app has executed</Body>
|
<Body size="S">See your automation history and edit advanced settings</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="XS">Chain automations</Heading>
|
||||||
|
<Body size="S">Allow automations to trigger from other automations</Body>
|
||||||
|
<div class="setting-spacing">
|
||||||
|
<Toggle
|
||||||
|
text={"Enable chaining"}
|
||||||
|
on:change={e => {
|
||||||
|
save(e)
|
||||||
|
}}
|
||||||
|
value={chainAutomations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="XS">History</Heading>
|
||||||
|
<Body size="S">Free plan stores up to 1 day of automation history</Body>
|
||||||
|
</Layout>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<div class="select">
|
<div class="select">
|
||||||
|
@ -237,6 +274,9 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.setting-spacing {
|
||||||
|
padding-top: var(--spacing-s);
|
||||||
|
}
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
|
|
||||||
$redirect("../settings/automation-history")
|
$redirect("../settings/automations")
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -89,8 +89,8 @@ export function createQueriesStore() {
|
||||||
// Assume all the fields are strings and create a basic schema from the
|
// Assume all the fields are strings and create a basic schema from the
|
||||||
// unique fields returned by the server
|
// unique fields returned by the server
|
||||||
const schema = {}
|
const schema = {}
|
||||||
for (let [field, type] of Object.entries(result.schemaFields)) {
|
for (let [field, metadata] of Object.entries(result.schema)) {
|
||||||
schema[field] = type || "string"
|
schema[field] = metadata || { type: "string" }
|
||||||
}
|
}
|
||||||
return { ...result, schema, rows: result.rows || [] }
|
return { ...result, schema, rows: result.rows || [] }
|
||||||
}
|
}
|
||||||
|
|
|
@ -573,6 +573,7 @@
|
||||||
"description": "A configurable data list that attaches to your backend tables.",
|
"description": "A configurable data list that attaches to your backend tables.",
|
||||||
"icon": "JourneyData",
|
"icon": "JourneyData",
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
|
"requiredAncestors": ["dataprovider"],
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -710,12 +711,10 @@
|
||||||
],
|
],
|
||||||
"context": [
|
"context": [
|
||||||
{
|
{
|
||||||
"type": "schema",
|
"type": "schema"
|
||||||
"scope": "local"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "static",
|
"type": "static",
|
||||||
"scope": "local",
|
|
||||||
"values": [
|
"values": [
|
||||||
{
|
{
|
||||||
"label": "Row index",
|
"label": "Row index",
|
||||||
|
@ -1565,6 +1564,7 @@
|
||||||
"name": "Bar Chart",
|
"name": "Bar Chart",
|
||||||
"description": "Bar chart",
|
"description": "Bar chart",
|
||||||
"icon": "GraphBarVertical",
|
"icon": "GraphBarVertical",
|
||||||
|
"requiredAncestors": ["dataprovider"],
|
||||||
"size": {
|
"size": {
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
|
@ -1727,6 +1727,7 @@
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
},
|
},
|
||||||
|
"requiredAncestors": ["dataprovider"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -1880,6 +1881,7 @@
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
},
|
},
|
||||||
|
"requiredAncestors": ["dataprovider"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -2045,6 +2047,7 @@
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
},
|
},
|
||||||
|
"requiredAncestors": ["dataprovider"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -2174,6 +2177,7 @@
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
},
|
},
|
||||||
|
"requiredAncestors": ["dataprovider"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -2303,6 +2307,7 @@
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
},
|
},
|
||||||
|
"requiredAncestors": ["dataprovider"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -3964,6 +3969,12 @@
|
||||||
"key": "allowManualEntry",
|
"key": "allowManualEntry",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Auto confirm",
|
||||||
|
"key": "autoConfirm",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Play sound on scan",
|
"label": "Play sound on scan",
|
||||||
|
@ -4076,6 +4087,7 @@
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 320
|
"height": 320
|
||||||
},
|
},
|
||||||
|
"requiredAncestors": ["dataprovider"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "dataProvider",
|
"type": "dataProvider",
|
||||||
|
@ -4631,6 +4643,7 @@
|
||||||
"name": "Table",
|
"name": "Table",
|
||||||
"icon": "Table",
|
"icon": "Table",
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
|
"requiredAncestors": ["dataprovider"],
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"showEmptyState": false,
|
"showEmptyState": false,
|
||||||
"size": {
|
"size": {
|
||||||
|
@ -4721,6 +4734,7 @@
|
||||||
"name": "Date Range",
|
"name": "Date Range",
|
||||||
"icon": "Calendar",
|
"icon": "Calendar",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
|
"requiredAncestors": ["dataprovider"],
|
||||||
"hasChildren": false,
|
"hasChildren": false,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 200,
|
"width": 200,
|
||||||
|
@ -4828,6 +4842,7 @@
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"height": 35
|
"height": 35
|
||||||
},
|
},
|
||||||
|
"requiredAncestors": ["dataprovider"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "dataProvider",
|
"type": "dataProvider",
|
||||||
|
@ -5602,38 +5617,7 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"context": {
|
|
||||||
"type": "static",
|
|
||||||
"suffix": "provider",
|
|
||||||
"values": [
|
|
||||||
{
|
|
||||||
"label": "Rows",
|
|
||||||
"key": "rows",
|
|
||||||
"type": "array"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Extra Info",
|
|
||||||
"key": "info",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Rows Length",
|
|
||||||
"key": "rowsLength",
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Schema",
|
|
||||||
"key": "schema",
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Page Number",
|
|
||||||
"key": "pageNumber",
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"cardsblock": {
|
"cardsblock": {
|
||||||
"block": true,
|
"block": true,
|
||||||
|
@ -6036,8 +6020,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "schema",
|
"type": "schema",
|
||||||
"suffix": "repeater",
|
"suffix": "repeater"
|
||||||
"scope": "local"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -6183,10 +6166,6 @@
|
||||||
"type": "form",
|
"type": "form",
|
||||||
"suffix": "form"
|
"suffix": "form"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "schema",
|
|
||||||
"suffix": "repeater"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "static",
|
"type": "static",
|
||||||
"suffix": "form",
|
"suffix": "form",
|
||||||
|
@ -6503,23 +6482,6 @@
|
||||||
"suffix": "repeater"
|
"suffix": "repeater"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"grid": {
|
|
||||||
"name": "Grid",
|
|
||||||
"icon": "ViewGrid",
|
|
||||||
"hasChildren": true,
|
|
||||||
"settings": [
|
|
||||||
{
|
|
||||||
"type": "number",
|
|
||||||
"key": "cols",
|
|
||||||
"label": "Columns"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "number",
|
|
||||||
"key": "rows",
|
|
||||||
"label": "Rows"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"gridblock": {
|
"gridblock": {
|
||||||
"name": "Grid Block",
|
"name": "Grid Block",
|
||||||
"icon": "Table",
|
"icon": "Table",
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext, setContext, onMount } from "svelte"
|
import { getContext, setContext, onMount, onDestroy } from "svelte"
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import {
|
import {
|
||||||
enrichProps,
|
enrichProps,
|
||||||
|
@ -30,15 +30,6 @@
|
||||||
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
||||||
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte"
|
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte"
|
||||||
import { BudibasePrefix } from "../stores/components.js"
|
import { BudibasePrefix } from "../stores/components.js"
|
||||||
import {
|
|
||||||
decodeJSBinding,
|
|
||||||
findHBSBlocks,
|
|
||||||
isJSBinding,
|
|
||||||
} from "@budibase/string-templates"
|
|
||||||
import {
|
|
||||||
getActionContextKey,
|
|
||||||
getActionDependentContextKeys,
|
|
||||||
} from "../utils/buttonActions.js"
|
|
||||||
|
|
||||||
export let instance = {}
|
export let instance = {}
|
||||||
export let isLayout = false
|
export let isLayout = false
|
||||||
|
@ -90,6 +81,7 @@
|
||||||
|
|
||||||
// Keep track of stringified representations of context and instance
|
// Keep track of stringified representations of context and instance
|
||||||
// to avoid enriching bindings as much as possible
|
// to avoid enriching bindings as much as possible
|
||||||
|
let lastContextKey
|
||||||
let lastInstanceKey
|
let lastInstanceKey
|
||||||
|
|
||||||
// Visibility flag used by conditional UI
|
// Visibility flag used by conditional UI
|
||||||
|
@ -106,13 +98,6 @@
|
||||||
// We clear these whenever a new instance is received.
|
// We clear these whenever a new instance is received.
|
||||||
let ephemeralStyles
|
let ephemeralStyles
|
||||||
|
|
||||||
// Single string of all HBS blocks, used to check if we use a certain binding
|
|
||||||
// or not
|
|
||||||
let bindingString = ""
|
|
||||||
|
|
||||||
// List of context keys which we use inside bindings
|
|
||||||
let knownContextKeyMap = {}
|
|
||||||
|
|
||||||
// Set up initial state for each new component instance
|
// Set up initial state for each new component instance
|
||||||
$: initialise(instance)
|
$: initialise(instance)
|
||||||
|
|
||||||
|
@ -170,6 +155,9 @@
|
||||||
hasMissingRequiredSettings)
|
hasMissingRequiredSettings)
|
||||||
$: emptyState = empty && showEmptyState
|
$: emptyState = empty && showEmptyState
|
||||||
|
|
||||||
|
// Enrich component settings
|
||||||
|
$: enrichComponentSettings($context, settingsDefinitionMap)
|
||||||
|
|
||||||
// Evaluate conditional UI settings and store any component setting changes
|
// Evaluate conditional UI settings and store any component setting changes
|
||||||
// which need to be made
|
// which need to be made
|
||||||
$: evaluateConditions(conditions)
|
$: evaluateConditions(conditions)
|
||||||
|
@ -218,7 +206,6 @@
|
||||||
errorState,
|
errorState,
|
||||||
parent: id,
|
parent: id,
|
||||||
ancestors: [...($component?.ancestors ?? []), instance._component],
|
ancestors: [...($component?.ancestors ?? []), instance._component],
|
||||||
path: [...($component?.path ?? []), id],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const initialise = (instance, force = false) => {
|
const initialise = (instance, force = false) => {
|
||||||
|
@ -227,8 +214,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we're processing a new instance
|
// Ensure we're processing a new instance
|
||||||
const stringifiedInstance = JSON.stringify(instance)
|
const instanceKey = Helpers.hashString(JSON.stringify(instance))
|
||||||
const instanceKey = Helpers.hashString(stringifiedInstance)
|
|
||||||
if (instanceKey === lastInstanceKey && !force) {
|
if (instanceKey === lastInstanceKey && !force) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
|
@ -288,54 +274,13 @@
|
||||||
return missing
|
return missing
|
||||||
})
|
})
|
||||||
|
|
||||||
// When considering bindings we can ignore children, so we remove that
|
|
||||||
// before storing the reference stringified version
|
|
||||||
const noChildren = JSON.stringify({ ...instance, _children: null })
|
|
||||||
const bindings = findHBSBlocks(noChildren).map(binding => {
|
|
||||||
let sanitizedBinding = binding.replace(/\\"/g, '"')
|
|
||||||
if (isJSBinding(sanitizedBinding)) {
|
|
||||||
return decodeJSBinding(sanitizedBinding)
|
|
||||||
} else {
|
|
||||||
return sanitizedBinding
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// The known context key map is built up at runtime, as changes to keys are
|
|
||||||
// encountered. We manually seed this to the required action keys as these
|
|
||||||
// are not encountered at runtime and so need computed in advance.
|
|
||||||
knownContextKeyMap = generateActionKeyMap(instance, settingsDefinition)
|
|
||||||
bindingString = bindings.join(" ")
|
|
||||||
|
|
||||||
// Run any migrations
|
// Run any migrations
|
||||||
runMigrations(instance, settingsDefinition)
|
runMigrations(instance, settingsDefinition)
|
||||||
|
|
||||||
// Force an initial enrichment of the new settings
|
// Force an initial enrichment of the new settings
|
||||||
enrichComponentSettings(get(context), settingsDefinitionMap)
|
enrichComponentSettings(get(context), settingsDefinitionMap, {
|
||||||
}
|
force: true,
|
||||||
|
|
||||||
// Extracts a map of all context keys which are required by action settings
|
|
||||||
// to provide the functions to evaluate at runtime. This needs done manually
|
|
||||||
// as the action definitions themselves do not specify bindings for action
|
|
||||||
// keys, meaning we cannot do this while doing the other normal bindings.
|
|
||||||
const generateActionKeyMap = (instance, settingsDefinition) => {
|
|
||||||
let map = {}
|
|
||||||
settingsDefinition.forEach(setting => {
|
|
||||||
if (setting.type === "event") {
|
|
||||||
instance[setting.key]?.forEach(action => {
|
|
||||||
// We depend on the actual action key
|
|
||||||
const actionKey = getActionContextKey(action)
|
|
||||||
if (actionKey) {
|
|
||||||
map[actionKey] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// We also depend on any manually declared context keys
|
|
||||||
getActionDependentContextKeys(action)?.forEach(key => {
|
|
||||||
map[key] = true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return map
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const runMigrations = (instance, settingsDefinition) => {
|
const runMigrations = (instance, settingsDefinition) => {
|
||||||
|
@ -436,7 +381,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enriches any string component props using handlebars
|
// Enriches any string component props using handlebars
|
||||||
const enrichComponentSettings = (context, settingsDefinitionMap) => {
|
const enrichComponentSettings = (
|
||||||
|
context,
|
||||||
|
settingsDefinitionMap,
|
||||||
|
options = { force: false }
|
||||||
|
) => {
|
||||||
|
const contextChanged = context.key !== lastContextKey
|
||||||
|
if (!contextChanged && !options?.force) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastContextKey = context.key
|
||||||
|
|
||||||
// Record the timestamp so we can reference it after enrichment
|
// Record the timestamp so we can reference it after enrichment
|
||||||
latestUpdateTime = Date.now()
|
latestUpdateTime = Date.now()
|
||||||
const enrichmentTime = latestUpdateTime
|
const enrichmentTime = latestUpdateTime
|
||||||
|
@ -551,46 +506,31 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleContextChange = key => {
|
|
||||||
// Check if we already know if this key is used
|
|
||||||
let used = knownContextKeyMap[key]
|
|
||||||
|
|
||||||
// If we don't know, check and cache
|
|
||||||
if (used == null) {
|
|
||||||
used = bindingString.indexOf(`[${key}]`) !== -1
|
|
||||||
knownContextKeyMap[key] = used
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enrich settings if we use this key
|
|
||||||
if (used) {
|
|
||||||
enrichComponentSettings($context, settingsDefinitionMap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register an unregister component instance
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($appStore.isDevApp) {
|
if (
|
||||||
if (!componentStore.actions.isComponentRegistered(id)) {
|
$appStore.isDevApp &&
|
||||||
componentStore.actions.registerInstance(id, {
|
!componentStore.actions.isComponentRegistered(id)
|
||||||
component: instance._component,
|
) {
|
||||||
getSettings: () => cachedSettings,
|
componentStore.actions.registerInstance(id, {
|
||||||
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
|
component: instance._component,
|
||||||
getDataContext: () => get(context),
|
getSettings: () => cachedSettings,
|
||||||
reload: () => initialise(instance, true),
|
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
|
||||||
setEphemeralStyles: styles => (ephemeralStyles = styles),
|
getDataContext: () => get(context),
|
||||||
state: store,
|
reload: () => initialise(instance, true),
|
||||||
})
|
setEphemeralStyles: styles => (ephemeralStyles = styles),
|
||||||
}
|
state: store,
|
||||||
return () => {
|
})
|
||||||
if (componentStore.actions.isComponentRegistered(id)) {
|
|
||||||
componentStore.actions.unregisterInstance(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Observe changes to context
|
onDestroy(() => {
|
||||||
onMount(() => context.actions.observeChanges(handleContextChange))
|
if (
|
||||||
|
$appStore.isDevApp &&
|
||||||
|
componentStore.actions.isComponentRegistered(id)
|
||||||
|
) {
|
||||||
|
componentStore.actions.unregisterInstance(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if constructor && initialSettings && (visible || inSelectedPath) && !builderHidden}
|
{#if constructor && initialSettings && (visible || inSelectedPath) && !builderHidden}
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
datasource: dataSource || {},
|
datasource: dataSource || {},
|
||||||
schema,
|
schema,
|
||||||
rowsLength: $fetch.rows.length,
|
rowsLength: $fetch.rows.length,
|
||||||
pageNumber: $fetch.pageNumber + 1,
|
|
||||||
// Undocumented properties. These aren't supposed to be used in builder
|
// Undocumented properties. These aren't supposed to be used in builder
|
||||||
// bindings, but are used internally by other components
|
// bindings, but are used internally by other components
|
||||||
id: $component?.id,
|
id: $component?.id,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { builderStore, componentStore } = getContext("sdk")
|
const { builderStore, componentStore } = getContext("sdk")
|
||||||
|
@ -9,7 +10,15 @@
|
||||||
|
|
||||||
{#if $builderStore.inBuilder}
|
{#if $builderStore.inBuilder}
|
||||||
<div class="component-placeholder">
|
<div class="component-placeholder">
|
||||||
{$component.name || definition?.name || "Component"}
|
<Icon name="Help" color="var(--spectrum-global-color-blue-600)" />
|
||||||
|
<span
|
||||||
|
class="spectrum-Link"
|
||||||
|
on:click={() => {
|
||||||
|
builderStore.actions.requestAddComponent()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add components inside your {definition?.name || $component.type}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -23,4 +32,14 @@
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Common styles for all error states to use */
|
||||||
|
.component-placeholder :global(mark) {
|
||||||
|
background-color: var(--spectrum-global-color-gray-400);
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.component-placeholder :global(.spectrum-Link) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -19,36 +19,7 @@
|
||||||
export let onRowClick = null
|
export let onRowClick = null
|
||||||
export let buttons = null
|
export let buttons = null
|
||||||
|
|
||||||
const context = getContext("context")
|
// parses columns to fix older formats
|
||||||
const component = getContext("component")
|
|
||||||
const {
|
|
||||||
styleable,
|
|
||||||
API,
|
|
||||||
builderStore,
|
|
||||||
notificationStore,
|
|
||||||
enrichButtonActions,
|
|
||||||
ActionTypes,
|
|
||||||
createContextStore,
|
|
||||||
Provider,
|
|
||||||
} = getContext("sdk")
|
|
||||||
|
|
||||||
let grid
|
|
||||||
|
|
||||||
$: columnWhitelist = parsedColumns
|
|
||||||
?.filter(col => col.active)
|
|
||||||
?.map(col => col.field)
|
|
||||||
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
|
||||||
$: enrichedButtons = enrichButtons(buttons)
|
|
||||||
$: parsedColumns = getParsedColumns(columns)
|
|
||||||
$: actions = [
|
|
||||||
{
|
|
||||||
type: ActionTypes.RefreshDatasource,
|
|
||||||
callback: () => grid?.getContext()?.rows.actions.refreshData(),
|
|
||||||
metadata: { dataSource: table },
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// Parses columns to fix older formats
|
|
||||||
const getParsedColumns = columns => {
|
const getParsedColumns = columns => {
|
||||||
// If the first element has an active key all elements should be in the new format
|
// If the first element has an active key all elements should be in the new format
|
||||||
if (columns?.length && columns[0]?.active !== undefined) {
|
if (columns?.length && columns[0]?.active !== undefined) {
|
||||||
|
@ -62,6 +33,28 @@
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: parsedColumns = getParsedColumns(columns)
|
||||||
|
|
||||||
|
const context = getContext("context")
|
||||||
|
const component = getContext("component")
|
||||||
|
const {
|
||||||
|
styleable,
|
||||||
|
API,
|
||||||
|
builderStore,
|
||||||
|
notificationStore,
|
||||||
|
enrichButtonActions,
|
||||||
|
ActionTypes,
|
||||||
|
createContextStore,
|
||||||
|
} = getContext("sdk")
|
||||||
|
|
||||||
|
let grid
|
||||||
|
|
||||||
|
$: columnWhitelist = parsedColumns
|
||||||
|
?.filter(col => col.active)
|
||||||
|
?.map(col => col.field)
|
||||||
|
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
||||||
|
$: enrichedButtons = enrichButtons(buttons)
|
||||||
|
|
||||||
const getSchemaOverrides = columns => {
|
const getSchemaOverrides = columns => {
|
||||||
let overrides = {}
|
let overrides = {}
|
||||||
columns?.forEach(column => {
|
columns?.forEach(column => {
|
||||||
|
@ -85,6 +78,11 @@
|
||||||
const id = get(component).id
|
const id = get(component).id
|
||||||
const gridContext = createContextStore(context)
|
const gridContext = createContextStore(context)
|
||||||
gridContext.actions.provideData(id, row)
|
gridContext.actions.provideData(id, row)
|
||||||
|
gridContext.actions.provideAction(
|
||||||
|
id,
|
||||||
|
ActionTypes.RefreshDatasource,
|
||||||
|
() => grid?.getContext()?.rows.actions.refreshData()
|
||||||
|
)
|
||||||
const fn = enrichButtonActions(settings.onClick, get(gridContext))
|
const fn = enrichButtonActions(settings.onClick, get(gridContext))
|
||||||
return await fn?.({ row })
|
return await fn?.({ row })
|
||||||
},
|
},
|
||||||
|
@ -96,31 +94,29 @@
|
||||||
use:styleable={$component.styles}
|
use:styleable={$component.styles}
|
||||||
class:in-builder={$builderStore.inBuilder}
|
class:in-builder={$builderStore.inBuilder}
|
||||||
>
|
>
|
||||||
<Provider {actions}>
|
<Grid
|
||||||
<Grid
|
bind:this={grid}
|
||||||
bind:this={grid}
|
datasource={table}
|
||||||
datasource={table}
|
{API}
|
||||||
{API}
|
{stripeRows}
|
||||||
{stripeRows}
|
{initialFilter}
|
||||||
{initialFilter}
|
{initialSortColumn}
|
||||||
{initialSortColumn}
|
{initialSortOrder}
|
||||||
{initialSortOrder}
|
{fixedRowHeight}
|
||||||
{fixedRowHeight}
|
{columnWhitelist}
|
||||||
{columnWhitelist}
|
{schemaOverrides}
|
||||||
{schemaOverrides}
|
canAddRows={allowAddRows}
|
||||||
canAddRows={allowAddRows}
|
canEditRows={allowEditRows}
|
||||||
canEditRows={allowEditRows}
|
canDeleteRows={allowDeleteRows}
|
||||||
canDeleteRows={allowDeleteRows}
|
canEditColumns={false}
|
||||||
canEditColumns={false}
|
canExpandRows={false}
|
||||||
canExpandRows={false}
|
canSaveSchema={false}
|
||||||
canSaveSchema={false}
|
showControls={false}
|
||||||
showControls={false}
|
notifySuccess={notificationStore.actions.success}
|
||||||
notifySuccess={notificationStore.actions.success}
|
notifyError={notificationStore.actions.error}
|
||||||
notifyError={notificationStore.actions.error}
|
buttons={enrichedButtons}
|
||||||
buttons={enrichedButtons}
|
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
/>
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import Placeholder from "./Placeholder.svelte"
|
import Placeholder from "./Placeholder.svelte"
|
||||||
import Container from "./Container.svelte"
|
import Container from "./Container.svelte"
|
||||||
import { ContextScopes } from "constants"
|
|
||||||
|
|
||||||
export let dataProvider
|
export let dataProvider
|
||||||
export let noRowsMessage
|
export let noRowsMessage
|
||||||
|
@ -10,7 +9,6 @@
|
||||||
export let hAlign
|
export let hAlign
|
||||||
export let vAlign
|
export let vAlign
|
||||||
export let gap
|
export let gap
|
||||||
export let scope = ContextScopes.Local
|
|
||||||
|
|
||||||
const { Provider } = getContext("sdk")
|
const { Provider } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
@ -24,7 +22,7 @@
|
||||||
<Placeholder />
|
<Placeholder />
|
||||||
{:else if rows.length > 0}
|
{:else if rows.length > 0}
|
||||||
{#each rows as row, index}
|
{#each rows as row, index}
|
||||||
<Provider data={{ ...row, index }} {scope}>
|
<Provider data={{ ...row, index }}>
|
||||||
<slot />
|
<slot />
|
||||||
</Provider>
|
</Provider>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "components/BlockComponent.svelte"
|
||||||
import { Helpers } from "@budibase/bbui"
|
|
||||||
import { getContext, setContext } from "svelte"
|
import { getContext, setContext } from "svelte"
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "stores"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
@ -42,7 +41,7 @@
|
||||||
let schema
|
let schema
|
||||||
|
|
||||||
$: fetchSchema(dataSource)
|
$: fetchSchema(dataSource)
|
||||||
$: enrichedSteps = enrichSteps(steps, schema, $component.id, $currentStep)
|
$: enrichedSteps = enrichSteps(steps, schema, $component.id)
|
||||||
$: updateCurrentStep(enrichedSteps, $builderStore, $component)
|
$: updateCurrentStep(enrichedSteps, $builderStore, $component)
|
||||||
|
|
||||||
const updateCurrentStep = (steps, builderStore, component) => {
|
const updateCurrentStep = (steps, builderStore, component) => {
|
||||||
|
@ -116,7 +115,6 @@
|
||||||
dataSource,
|
dataSource,
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
_stepId: Helpers.uuid(),
|
|
||||||
fields: getDefaultFields(fields || [], schema),
|
fields: getDefaultFields(fields || [], schema),
|
||||||
title: title ?? defaultProps.title,
|
title: title ?? defaultProps.title,
|
||||||
desc,
|
desc,
|
||||||
|
@ -144,7 +142,7 @@
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#each enrichedSteps as step, stepIdx (step._stepId)}
|
{#each enrichedSteps as step, stepIdx}
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type="formstep"
|
type="formstep"
|
||||||
props={{ step: stepIdx + 1, _instanceName: `Step ${stepIdx + 1}` }}
|
props={{ step: stepIdx + 1, _instanceName: `Step ${stepIdx + 1}` }}
|
||||||
|
@ -188,13 +186,12 @@
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
<BlockComponent type="text" props={{ text: step.desc }} order={1} />
|
<BlockComponent type="text" props={{ text: step.desc }} order={1} />
|
||||||
|
|
||||||
<BlockComponent type="container" order={2}>
|
<BlockComponent type="container" order={2}>
|
||||||
<div
|
<div
|
||||||
class="form-block fields"
|
class="form-block fields"
|
||||||
class:mobile={$context.device.mobile}
|
class:mobile={$context.device.mobile}
|
||||||
>
|
>
|
||||||
{#each step.fields as field, fieldIdx (`${field.field || field.name}_${fieldIdx}`)}
|
{#each step.fields as field, fieldIdx (`${field.field || field.name}_${stepIdx}_${fieldIdx}`)}
|
||||||
{#if getComponentForField(field)}
|
{#if getComponentForField(field)}
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type={getComponentForField(field)}
|
type={getComponentForField(field)}
|
||||||
|
|
|
@ -231,7 +231,6 @@
|
||||||
paginate,
|
paginate,
|
||||||
limit: rowCount,
|
limit: rowCount,
|
||||||
}}
|
}}
|
||||||
context="provider"
|
|
||||||
order={1}
|
order={1}
|
||||||
>
|
>
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
export let noRowsMessage
|
export let noRowsMessage
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { ContextScopes } = getContext("sdk")
|
|
||||||
|
|
||||||
$: providerId = `${$component.id}-provider`
|
$: providerId = `${$component.id}-provider`
|
||||||
$: dataProvider = `{{ literal ${safe(providerId)} }}`
|
$: dataProvider = `{{ literal ${safe(providerId)} }}`
|
||||||
|
@ -56,7 +55,6 @@
|
||||||
noRowsMessage: noRowsMessage || "We couldn't find a row to display",
|
noRowsMessage: noRowsMessage || "We couldn't find a row to display",
|
||||||
direction: "column",
|
direction: "column",
|
||||||
hAlign: "center",
|
hAlign: "center",
|
||||||
scope: ContextScopes.Global,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -14,11 +14,13 @@
|
||||||
export let value
|
export let value
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let allowManualEntry = false
|
export let allowManualEntry = false
|
||||||
|
export let autoConfirm = false
|
||||||
export let scanButtonText = "Scan code"
|
export let scanButtonText = "Scan code"
|
||||||
export let beepOnScan = false
|
export let beepOnScan = false
|
||||||
export let beepFrequency = 2637
|
export let beepFrequency = 2637
|
||||||
export let customFrequency = 1046
|
export let customFrequency = 1046
|
||||||
export let preferredCamera = "environment"
|
export let preferredCamera = "environment"
|
||||||
|
export let validator
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -41,6 +43,9 @@
|
||||||
beep()
|
beep()
|
||||||
}
|
}
|
||||||
dispatch("change", decodedText)
|
dispatch("change", decodedText)
|
||||||
|
if (autoConfirm && !validator?.(decodedText)) {
|
||||||
|
camModal?.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +132,11 @@
|
||||||
<div class="scanner-video-wrapper">
|
<div class="scanner-video-wrapper">
|
||||||
{#if value && !manualMode}
|
{#if value && !manualMode}
|
||||||
<div class="scanner-value field-display">
|
<div class="scanner-value field-display">
|
||||||
<StatusLight positive />
|
{#if validator?.(value)}
|
||||||
|
<StatusLight negative />
|
||||||
|
{:else}
|
||||||
|
<StatusLight positive />
|
||||||
|
{/if}
|
||||||
{value}
|
{value}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -183,11 +192,16 @@
|
||||||
</div>
|
</div>
|
||||||
{#if cameraEnabled === true}
|
{#if cameraEnabled === true}
|
||||||
<div class="code-wrap">
|
<div class="code-wrap">
|
||||||
{#if value}
|
{#if value && !validator?.(value)}
|
||||||
<div class="scanner-value">
|
<div class="scanner-value">
|
||||||
<StatusLight positive />
|
<StatusLight positive />
|
||||||
{value}
|
{value}
|
||||||
</div>
|
</div>
|
||||||
|
{:else if value && validator?.(value)}
|
||||||
|
<div class="scanner-value">
|
||||||
|
<StatusLight negative />
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="scanner-value">
|
<div class="scanner-value">
|
||||||
<StatusLight neutral />
|
<StatusLight neutral />
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
export let defaultValue = ""
|
export let defaultValue = ""
|
||||||
export let onChange
|
export let onChange
|
||||||
export let allowManualEntry
|
export let allowManualEntry
|
||||||
|
export let autoConfirm
|
||||||
export let scanButtonText
|
export let scanButtonText
|
||||||
export let beepOnScan
|
export let beepOnScan
|
||||||
export let beepFrequency
|
export let beepFrequency
|
||||||
|
@ -49,11 +50,13 @@
|
||||||
on:change={handleUpdate}
|
on:change={handleUpdate}
|
||||||
disabled={fieldState.disabled || fieldState.readonly}
|
disabled={fieldState.disabled || fieldState.readonly}
|
||||||
{allowManualEntry}
|
{allowManualEntry}
|
||||||
|
{autoConfirm}
|
||||||
scanButtonText={scanText}
|
scanButtonText={scanText}
|
||||||
{beepOnScan}
|
{beepOnScan}
|
||||||
{beepFrequency}
|
{beepFrequency}
|
||||||
{customFrequency}
|
{customFrequency}
|
||||||
{preferredCamera}
|
{preferredCamera}
|
||||||
|
validator={fieldState.validator}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
export let editAutoColumns = false
|
export let editAutoColumns = false
|
||||||
|
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const component = getContext("component")
|
|
||||||
const { API, fetchDatasourceSchema } = getContext("sdk")
|
const { API, fetchDatasourceSchema } = getContext("sdk")
|
||||||
|
|
||||||
const getInitialFormStep = () => {
|
const getInitialFormStep = () => {
|
||||||
|
@ -39,47 +38,28 @@
|
||||||
|
|
||||||
$: fetchSchema(dataSource)
|
$: fetchSchema(dataSource)
|
||||||
$: schemaKey = generateSchemaKey(schema)
|
$: schemaKey = generateSchemaKey(schema)
|
||||||
$: initialValues = getInitialValues(
|
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
||||||
actionType,
|
|
||||||
dataSource,
|
|
||||||
$component.path,
|
|
||||||
$context
|
|
||||||
)
|
|
||||||
$: resetKey = Helpers.hashString(
|
$: resetKey = Helpers.hashString(
|
||||||
schemaKey + JSON.stringify(initialValues) + disabled + readonly
|
schemaKey + JSON.stringify(initialValues) + disabled + readonly
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns the closes data context which isn't a built in context
|
// Returns the closes data context which isn't a built in context
|
||||||
const getInitialValues = (type, dataSource, path, context) => {
|
const getInitialValues = (type, dataSource, context) => {
|
||||||
// Only inherit values for update forms
|
// Only inherit values for update forms
|
||||||
if (type !== "Update") {
|
if (type !== "Update") {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
// Only inherit values for forms targeting internal tables
|
// Only inherit values for forms targeting internal tables
|
||||||
const dsType = dataSource?.type
|
if (!dataSource?.tableId) {
|
||||||
if (dsType !== "table" && dsType !== "viewV2") {
|
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
// Look up the component tree and find something that is provided by an
|
// Don't inherit values representing built in contexts
|
||||||
// ancestor that matches our datasource. This is for backwards compatibility
|
if (["user", "url"].includes(context.closestComponentId)) {
|
||||||
// as previously we could use the "closest" context.
|
return {}
|
||||||
for (let id of path.reverse().slice(1)) {
|
|
||||||
// Check for matching view datasource
|
|
||||||
if (
|
|
||||||
dataSource.type === "viewV2" &&
|
|
||||||
context[id]?._viewId === dataSource.id
|
|
||||||
) {
|
|
||||||
return context[id]
|
|
||||||
}
|
|
||||||
// Check for matching table datasource
|
|
||||||
if (
|
|
||||||
dataSource.type === "table" &&
|
|
||||||
context[id]?.tableId === dataSource.tableId
|
|
||||||
) {
|
|
||||||
return context[id]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {}
|
// Always inherit the closest datasource
|
||||||
|
const closestContext = context[`${context.closestComponentId}`] || {}
|
||||||
|
return closestContext || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches the form schema from this form's dataSource
|
// Fetches the form schema from this form's dataSource
|
||||||
|
|
|
@ -108,8 +108,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: forceFetchRows(filter)
|
||||||
$: debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
$: debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||||
|
|
||||||
|
const forceFetchRows = async () => {
|
||||||
|
// if the filter has changed, then we need to reset the options, clear the selection, and re-fetch
|
||||||
|
optionsObj = {}
|
||||||
|
fieldApi?.setValue([])
|
||||||
|
selectedValue = []
|
||||||
|
debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||||
|
}
|
||||||
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
||||||
const allRowsFetched =
|
const allRowsFetched =
|
||||||
$fetch.loaded &&
|
$fetch.loaded &&
|
||||||
|
@ -228,7 +236,6 @@
|
||||||
bind:searchTerm
|
bind:searchTerm
|
||||||
loading={$fetch.loading}
|
loading={$fetch.loading}
|
||||||
bind:open
|
bind:open
|
||||||
customPopoverMaxHeight={400}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -1,23 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, setContext, onDestroy } from "svelte"
|
import { getContext, setContext, onDestroy } from "svelte"
|
||||||
import { dataSourceStore, createContextStore } from "stores"
|
import { dataSourceStore, createContextStore } from "stores"
|
||||||
import { ActionTypes, ContextScopes } from "constants"
|
import { ActionTypes } from "constants"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
|
|
||||||
export let data
|
export let data
|
||||||
export let actions
|
export let actions
|
||||||
export let key
|
export let key
|
||||||
export let scope = ContextScopes.Global
|
|
||||||
|
|
||||||
let context = getContext("context")
|
// Clone and create new data context for this component tree
|
||||||
|
const context = getContext("context")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const providerKey = key || $component.id
|
const newContext = createContextStore(context)
|
||||||
|
setContext("context", newContext)
|
||||||
|
|
||||||
// Create a new layer of context if we are only locally scoped
|
const providerKey = key || $component.id
|
||||||
if (scope === ContextScopes.Local) {
|
|
||||||
context = createContextStore(context)
|
|
||||||
setContext("context", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a permanent unique ID for this component and use it to register
|
// Generate a permanent unique ID for this component and use it to register
|
||||||
// any datasource actions
|
// any datasource actions
|
||||||
|
@ -33,7 +30,7 @@
|
||||||
const provideData = newData => {
|
const provideData = newData => {
|
||||||
const dataKey = JSON.stringify(newData)
|
const dataKey = JSON.stringify(newData)
|
||||||
if (dataKey !== lastDataKey) {
|
if (dataKey !== lastDataKey) {
|
||||||
context.actions.provideData(providerKey, newData, scope)
|
newContext.actions.provideData(providerKey, newData)
|
||||||
lastDataKey = dataKey
|
lastDataKey = dataKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +40,7 @@
|
||||||
if (actionsKey !== lastActionsKey) {
|
if (actionsKey !== lastActionsKey) {
|
||||||
lastActionsKey = actionsKey
|
lastActionsKey = actionsKey
|
||||||
newActions?.forEach(({ type, callback, metadata }) => {
|
newActions?.forEach(({ type, callback, metadata }) => {
|
||||||
context.actions.provideAction(providerKey, type, callback, scope)
|
newContext.actions.provideAction(providerKey, type, callback)
|
||||||
|
|
||||||
// Register any "refresh datasource" actions with a singleton store
|
// Register any "refresh datasource" actions with a singleton store
|
||||||
// so we can easily refresh data at all levels for any datasource
|
// so we can easily refresh data at all levels for any datasource
|
||||||
|
|
|
@ -12,10 +12,5 @@ export const ActionTypes = {
|
||||||
ScrollTo: "ScrollTo",
|
ScrollTo: "ScrollTo",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContextScopes = {
|
|
||||||
Local: "local",
|
|
||||||
Global: "global",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DNDPlaceholderID = "dnd-placeholder"
|
export const DNDPlaceholderID = "dnd-placeholder"
|
||||||
export const ScreenslotType = "screenslot"
|
export const ScreenslotType = "screenslot"
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { getAction } from "utils/getAction"
|
||||||
import Provider from "components/context/Provider.svelte"
|
import Provider from "components/context/Provider.svelte"
|
||||||
import Block from "components/Block.svelte"
|
import Block from "components/Block.svelte"
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "components/BlockComponent.svelte"
|
||||||
import { ActionTypes, ContextScopes } from "./constants"
|
import { ActionTypes } from "./constants"
|
||||||
import { fetchDatasourceSchema } from "./utils/schema.js"
|
import { fetchDatasourceSchema } from "./utils/schema.js"
|
||||||
import { getAPIKey } from "./utils/api.js"
|
import { getAPIKey } from "./utils/api.js"
|
||||||
import { enrichButtonActions } from "./utils/buttonActions.js"
|
import { enrichButtonActions } from "./utils/buttonActions.js"
|
||||||
|
@ -54,7 +54,6 @@ export default {
|
||||||
linkable,
|
linkable,
|
||||||
getAction,
|
getAction,
|
||||||
fetchDatasourceSchema,
|
fetchDatasourceSchema,
|
||||||
ContextScopes,
|
|
||||||
getAPIKey,
|
getAPIKey,
|
||||||
enrichButtonActions,
|
enrichButtonActions,
|
||||||
processStringSync,
|
processStringSync,
|
||||||
|
|
|
@ -1,98 +1,59 @@
|
||||||
import { writable, derived } from "svelte/store"
|
import { writable, derived } from "svelte/store"
|
||||||
import { ContextScopes } from "constants"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
|
||||||
export const createContextStore = parentContext => {
|
export const createContextStore = oldContext => {
|
||||||
const context = writable({})
|
const newContext = writable({})
|
||||||
let observers = []
|
const contexts = oldContext ? [oldContext, newContext] : [newContext]
|
||||||
|
|
||||||
// Derive the total context state at this point in the tree
|
|
||||||
const contexts = parentContext ? [parentContext, context] : [context]
|
|
||||||
const totalContext = derived(contexts, $contexts => {
|
const totalContext = derived(contexts, $contexts => {
|
||||||
return $contexts.reduce((total, context) => ({ ...total, ...context }), {})
|
// The key is the serialized representation of context
|
||||||
|
let key = ""
|
||||||
|
for (let i = 0; i < $contexts.length - 1; i++) {
|
||||||
|
key += $contexts[i].key
|
||||||
|
}
|
||||||
|
key = Helpers.hashString(
|
||||||
|
key + JSON.stringify($contexts[$contexts.length - 1])
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reduce global state
|
||||||
|
const reducer = (total, context) => ({ ...total, ...context })
|
||||||
|
const context = $contexts.reduce(reducer, {})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...context,
|
||||||
|
key,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Subscribe to updates in the parent context, so that we can proxy on any
|
// Adds a data context layer to the tree
|
||||||
// change messages to our own subscribers
|
const provideData = (providerId, data) => {
|
||||||
if (parentContext) {
|
|
||||||
parentContext.actions.observeChanges(key => {
|
|
||||||
broadcastChange(key)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide some data in context
|
|
||||||
const provideData = (providerId, data, scope = ContextScopes.Global) => {
|
|
||||||
if (!providerId || data === undefined) {
|
if (!providerId || data === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
newContext.update(state => {
|
||||||
|
state[providerId] = data
|
||||||
|
|
||||||
// Proxy message up the chain if we have a parent and are providing global
|
// Keep track of the closest component ID so we can later hydrate a "data" prop.
|
||||||
// context
|
// This is only required for legacy bindings that used "data" rather than a
|
||||||
if (scope === ContextScopes.Global && parentContext) {
|
// component ID.
|
||||||
parentContext.actions.provideData(providerId, data, scope)
|
state.closestComponentId = providerId
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise this is either the context root, or we're providing a local
|
return state
|
||||||
// context override, so we need to update the local context instead
|
})
|
||||||
else {
|
|
||||||
context.update(state => {
|
|
||||||
state[providerId] = data
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
broadcastChange(providerId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provides some action in context
|
// Adds an action context layer to the tree
|
||||||
const provideAction = (
|
const provideAction = (providerId, actionType, callback) => {
|
||||||
providerId,
|
|
||||||
actionType,
|
|
||||||
callback,
|
|
||||||
scope = ContextScopes.Global
|
|
||||||
) => {
|
|
||||||
if (!providerId || !actionType) {
|
if (!providerId || !actionType) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
newContext.update(state => {
|
||||||
// Proxy message up the chain if we have a parent and are providing global
|
state[`${providerId}_${actionType}`] = callback
|
||||||
// context
|
return state
|
||||||
if (scope === ContextScopes.Global && parentContext) {
|
})
|
||||||
parentContext.actions.provideAction(
|
|
||||||
providerId,
|
|
||||||
actionType,
|
|
||||||
callback,
|
|
||||||
scope
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise this is either the context root, or we're providing a local
|
|
||||||
// context override, so we need to update the local context instead
|
|
||||||
else {
|
|
||||||
const key = `${providerId}_${actionType}`
|
|
||||||
context.update(state => {
|
|
||||||
state[key] = callback
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
broadcastChange(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const observeChanges = callback => {
|
|
||||||
observers.push(callback)
|
|
||||||
return () => {
|
|
||||||
observers = observers.filter(cb => cb !== callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const broadcastChange = key => {
|
|
||||||
observers.forEach(cb => cb(key))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: totalContext.subscribe,
|
subscribe: totalContext.subscribe,
|
||||||
actions: {
|
actions: { provideData, provideAction },
|
||||||
provideData,
|
|
||||||
provideAction,
|
|
||||||
observeChanges,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,54 +17,6 @@ import { ActionTypes } from "constants"
|
||||||
import { enrichDataBindings } from "./enrichDataBinding"
|
import { enrichDataBindings } from "./enrichDataBinding"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
|
||||||
// Default action handler, which extracts an action from context that was
|
|
||||||
// provided by another component and executes it with all action parameters
|
|
||||||
const contextActionHandler = async (action, context) => {
|
|
||||||
const key = getActionContextKey(action)
|
|
||||||
const fn = context[key]
|
|
||||||
if (fn) {
|
|
||||||
return await fn(action.parameters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generates the context key, which is the key that this action depends on in
|
|
||||||
// context to provide the function it will run. This is broken out as a util
|
|
||||||
// because we reuse this inside the core Component.svelte file to determine
|
|
||||||
// what the required action context keys are for all action settings.
|
|
||||||
export const getActionContextKey = action => {
|
|
||||||
const type = action?.["##eventHandlerType"]
|
|
||||||
const key = (componentId, type) => `${componentId}_${type}`
|
|
||||||
switch (type) {
|
|
||||||
case "Scroll To Field":
|
|
||||||
return key(action.parameters.componentId, ActionTypes.ScrollTo)
|
|
||||||
case "Update Field Value":
|
|
||||||
return key(action.parameters.componentId, ActionTypes.UpdateFieldValue)
|
|
||||||
case "Validate Form":
|
|
||||||
return key(action.parameters.componentId, ActionTypes.ValidateForm)
|
|
||||||
case "Refresh Data Provider":
|
|
||||||
return key(action.parameters.componentId, ActionTypes.RefreshDatasource)
|
|
||||||
case "Clear Form":
|
|
||||||
return key(action.parameters.componentId, ActionTypes.ClearForm)
|
|
||||||
case "Change Form Step":
|
|
||||||
return key(action.parameters.componentId, ActionTypes.ChangeFormStep)
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If button actions depend on context, they must declare which keys they need
|
|
||||||
export const getActionDependentContextKeys = action => {
|
|
||||||
const type = action?.["##eventHandlerType"]
|
|
||||||
switch (type) {
|
|
||||||
case "Save Row":
|
|
||||||
case "Duplicate Row":
|
|
||||||
if (action.parameters?.providerId) {
|
|
||||||
return [action.parameters.providerId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveRowHandler = async (action, context) => {
|
const saveRowHandler = async (action, context) => {
|
||||||
const { fields, providerId, tableId, notificationOverride } =
|
const { fields, providerId, tableId, notificationOverride } =
|
||||||
action.parameters
|
action.parameters
|
||||||
|
@ -80,21 +32,20 @@ const saveRowHandler = async (action, context) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tableId) {
|
if (tableId) {
|
||||||
if (tableId.startsWith("view")) {
|
payload.tableId = tableId
|
||||||
payload._viewId = tableId
|
|
||||||
} else {
|
|
||||||
payload.tableId = tableId
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const row = await API.saveRow(payload)
|
const row = await API.saveRow(payload)
|
||||||
|
|
||||||
if (!notificationOverride) {
|
if (!notificationOverride) {
|
||||||
notificationStore.actions.success("Row saved")
|
notificationStore.actions.success("Row saved")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh related datasources
|
// Refresh related datasources
|
||||||
await dataSourceStore.actions.invalidateDataSource(tableId, {
|
await dataSourceStore.actions.invalidateDataSource(tableId, {
|
||||||
invalidateRelationships: true,
|
invalidateRelationships: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
return { row }
|
return { row }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Abort next actions
|
// Abort next actions
|
||||||
|
@ -113,11 +64,7 @@ const duplicateRowHandler = async (action, context) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tableId) {
|
if (tableId) {
|
||||||
if (tableId.startsWith("view")) {
|
payload.tableId = tableId
|
||||||
payload._viewId = tableId
|
|
||||||
} else {
|
|
||||||
payload.tableId = tableId
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
delete payload._id
|
delete payload._id
|
||||||
delete payload._rev
|
delete payload._rev
|
||||||
|
@ -126,10 +73,12 @@ const duplicateRowHandler = async (action, context) => {
|
||||||
if (!notificationOverride) {
|
if (!notificationOverride) {
|
||||||
notificationStore.actions.success("Row saved")
|
notificationStore.actions.success("Row saved")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh related datasources
|
// Refresh related datasources
|
||||||
await dataSourceStore.actions.invalidateDataSource(tableId, {
|
await dataSourceStore.actions.invalidateDataSource(tableId, {
|
||||||
invalidateRelationships: true,
|
invalidateRelationships: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
return { row }
|
return { row }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Abort next actions
|
// Abort next actions
|
||||||
|
@ -241,6 +190,17 @@ const navigationHandler = action => {
|
||||||
routeStore.actions.navigate(url, peek, externalNewTab)
|
routeStore.actions.navigate(url, peek, externalNewTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollHandler = async (action, context) => {
|
||||||
|
return await executeActionHandler(
|
||||||
|
context,
|
||||||
|
action.parameters.componentId,
|
||||||
|
ActionTypes.ScrollTo,
|
||||||
|
{
|
||||||
|
field: action.parameters.field,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const queryExecutionHandler = async action => {
|
const queryExecutionHandler = async action => {
|
||||||
const { datasourceId, queryId, queryParams, notificationOverride } =
|
const { datasourceId, queryId, queryParams, notificationOverride } =
|
||||||
action.parameters
|
action.parameters
|
||||||
|
@ -276,6 +236,47 @@ const queryExecutionHandler = async action => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const executeActionHandler = async (
|
||||||
|
context,
|
||||||
|
componentId,
|
||||||
|
actionType,
|
||||||
|
params
|
||||||
|
) => {
|
||||||
|
const fn = context[`${componentId}_${actionType}`]
|
||||||
|
if (fn) {
|
||||||
|
return await fn(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateFieldValueHandler = async (action, context) => {
|
||||||
|
return await executeActionHandler(
|
||||||
|
context,
|
||||||
|
action.parameters.componentId,
|
||||||
|
ActionTypes.UpdateFieldValue,
|
||||||
|
{
|
||||||
|
type: action.parameters.type,
|
||||||
|
field: action.parameters.field,
|
||||||
|
value: action.parameters.value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateFormHandler = async (action, context) => {
|
||||||
|
return await executeActionHandler(
|
||||||
|
context,
|
||||||
|
action.parameters.componentId,
|
||||||
|
ActionTypes.ValidateForm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshDataProviderHandler = async (action, context) => {
|
||||||
|
return await executeActionHandler(
|
||||||
|
context,
|
||||||
|
action.parameters.componentId,
|
||||||
|
ActionTypes.RefreshDatasource
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const logoutHandler = async action => {
|
const logoutHandler = async action => {
|
||||||
await authStore.actions.logOut()
|
await authStore.actions.logOut()
|
||||||
let redirectUrl = "/builder/auth/login"
|
let redirectUrl = "/builder/auth/login"
|
||||||
|
@ -292,6 +293,23 @@ const logoutHandler = async action => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearFormHandler = async (action, context) => {
|
||||||
|
return await executeActionHandler(
|
||||||
|
context,
|
||||||
|
action.parameters.componentId,
|
||||||
|
ActionTypes.ClearForm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeFormStepHandler = async (action, context) => {
|
||||||
|
return await executeActionHandler(
|
||||||
|
context,
|
||||||
|
action.parameters.componentId,
|
||||||
|
ActionTypes.ChangeFormStep,
|
||||||
|
action.parameters
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const closeScreenModalHandler = action => {
|
const closeScreenModalHandler = action => {
|
||||||
let url
|
let url
|
||||||
if (action?.parameters) {
|
if (action?.parameters) {
|
||||||
|
@ -399,10 +417,16 @@ const handlerMap = {
|
||||||
["Duplicate Row"]: duplicateRowHandler,
|
["Duplicate Row"]: duplicateRowHandler,
|
||||||
["Delete Row"]: deleteRowHandler,
|
["Delete Row"]: deleteRowHandler,
|
||||||
["Navigate To"]: navigationHandler,
|
["Navigate To"]: navigationHandler,
|
||||||
|
["Scroll To Field"]: scrollHandler,
|
||||||
["Execute Query"]: queryExecutionHandler,
|
["Execute Query"]: queryExecutionHandler,
|
||||||
["Trigger Automation"]: triggerAutomationHandler,
|
["Trigger Automation"]: triggerAutomationHandler,
|
||||||
|
["Validate Form"]: validateFormHandler,
|
||||||
|
["Update Field Value"]: updateFieldValueHandler,
|
||||||
|
["Refresh Data Provider"]: refreshDataProviderHandler,
|
||||||
["Log Out"]: logoutHandler,
|
["Log Out"]: logoutHandler,
|
||||||
|
["Clear Form"]: clearFormHandler,
|
||||||
["Close Screen Modal"]: closeScreenModalHandler,
|
["Close Screen Modal"]: closeScreenModalHandler,
|
||||||
|
["Change Form Step"]: changeFormStepHandler,
|
||||||
["Update State"]: updateStateHandler,
|
["Update State"]: updateStateHandler,
|
||||||
["Upload File to S3"]: s3UploadHandler,
|
["Upload File to S3"]: s3UploadHandler,
|
||||||
["Export Data"]: exportDataHandler,
|
["Export Data"]: exportDataHandler,
|
||||||
|
@ -437,12 +461,7 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
return actions
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get handlers for each action. If no bespoke handler is configured, fall
|
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
||||||
// back to simply executing this action from context.
|
|
||||||
const handlers = actions.map(def => {
|
|
||||||
return handlerMap[def["##eventHandlerType"]] || contextActionHandler
|
|
||||||
})
|
|
||||||
|
|
||||||
return async eventContext => {
|
return async eventContext => {
|
||||||
// Button context is built up as actions are executed.
|
// Button context is built up as actions are executed.
|
||||||
// Inherit any previous button context which may have come from actions
|
// Inherit any previous button context which may have come from actions
|
||||||
|
|
|
@ -23,6 +23,16 @@ export const propsAreSame = (a, b) => {
|
||||||
* Data bindings are enriched, and button actions are enriched.
|
* Data bindings are enriched, and button actions are enriched.
|
||||||
*/
|
*/
|
||||||
export const enrichProps = (props, context, settingsDefinitionMap) => {
|
export const enrichProps = (props, context, settingsDefinitionMap) => {
|
||||||
|
// Create context of all bindings and data contexts
|
||||||
|
// Duplicate the closest context as "data" which the builder requires
|
||||||
|
const totalContext = {
|
||||||
|
...context,
|
||||||
|
|
||||||
|
// This is only required for legacy bindings that used "data" rather than a
|
||||||
|
// component ID.
|
||||||
|
data: context[context.closestComponentId],
|
||||||
|
}
|
||||||
|
|
||||||
// We want to exclude any button actions from enrichment at this stage.
|
// We want to exclude any button actions from enrichment at this stage.
|
||||||
// Extract top level button action settings.
|
// Extract top level button action settings.
|
||||||
let normalProps = { ...props }
|
let normalProps = { ...props }
|
||||||
|
@ -39,13 +49,13 @@ export const enrichProps = (props, context, settingsDefinitionMap) => {
|
||||||
let rawConditions = normalProps._conditions
|
let rawConditions = normalProps._conditions
|
||||||
|
|
||||||
// Enrich all props except button actions
|
// Enrich all props except button actions
|
||||||
let enrichedProps = enrichDataBindings(normalProps, context)
|
let enrichedProps = enrichDataBindings(normalProps, totalContext)
|
||||||
|
|
||||||
// Enrich button actions.
|
// Enrich button actions.
|
||||||
// Actions are enriched into a function at this stage, but actual data
|
// Actions are enriched into a function at this stage, but actual data
|
||||||
// binding enrichment is done dynamically at runtime.
|
// binding enrichment is done dynamically at runtime.
|
||||||
Object.keys(actionProps).forEach(prop => {
|
Object.keys(actionProps).forEach(prop => {
|
||||||
enrichedProps[prop] = enrichButtonActions(actionProps[prop], context)
|
enrichedProps[prop] = enrichButtonActions(actionProps[prop], totalContext)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Conditions
|
// Conditions
|
||||||
|
@ -56,7 +66,7 @@ export const enrichProps = (props, context, settingsDefinitionMap) => {
|
||||||
// action
|
// action
|
||||||
condition.settingValue = enrichButtonActions(
|
condition.settingValue = enrichButtonActions(
|
||||||
rawConditions[idx].settingValue,
|
rawConditions[idx].settingValue,
|
||||||
context
|
totalContext
|
||||||
)
|
)
|
||||||
|
|
||||||
// Since we can't compare functions, we need to assume that conditions
|
// Since we can't compare functions, we need to assume that conditions
|
||||||
|
|
|
@ -19,12 +19,11 @@ export const buildRowEndpoints = API => ({
|
||||||
* @param suppressErrors whether or not to suppress error notifications
|
* @param suppressErrors whether or not to suppress error notifications
|
||||||
*/
|
*/
|
||||||
saveRow: async (row, suppressErrors = false) => {
|
saveRow: async (row, suppressErrors = false) => {
|
||||||
const resourceId = row?._viewId || row?.tableId
|
if (!row?.tableId) {
|
||||||
if (!resourceId) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: `/api/${resourceId}/rows`,
|
url: `/api/${row._viewId || row.tableId}/rows`,
|
||||||
body: row,
|
body: row,
|
||||||
suppressErrors,
|
suppressErrors,
|
||||||
})
|
})
|
||||||
|
@ -36,12 +35,11 @@ export const buildRowEndpoints = API => ({
|
||||||
* @param suppressErrors whether or not to suppress error notifications
|
* @param suppressErrors whether or not to suppress error notifications
|
||||||
*/
|
*/
|
||||||
patchRow: async (row, suppressErrors = false) => {
|
patchRow: async (row, suppressErrors = false) => {
|
||||||
const resourceId = row?._viewId || row?.tableId
|
if (!row?.tableId && !row?._viewId) {
|
||||||
if (!resourceId) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return await API.patch({
|
return await API.patch({
|
||||||
url: `/api/${resourceId}/rows`,
|
url: `/api/${row._viewId || row.tableId}/rows`,
|
||||||
body: row,
|
body: row,
|
||||||
suppressErrors,
|
suppressErrors,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit ce7722ed4474718596b465dcfd49bef36cab2e42
|
Subproject commit eb9565f568cfef14b336b14eee753119acfdd43b
|
|
@ -1,15 +1,21 @@
|
||||||
import { generateQueryID } from "../../../db/utils"
|
import { generateQueryID } from "../../../db/utils"
|
||||||
import { BaseQueryVerbs, FieldTypes } from "../../../constants"
|
import { BaseQueryVerbs } from "../../../constants"
|
||||||
import { Thread, ThreadType } from "../../../threads"
|
import { Thread, ThreadType } from "../../../threads"
|
||||||
import { save as saveDatasource } from "../datasource"
|
import { save as saveDatasource } from "../datasource"
|
||||||
import { RestImporter } from "./import"
|
import { RestImporter } from "./import"
|
||||||
import { invalidateDynamicVariables } from "../../../threads/utils"
|
import { invalidateDynamicVariables } from "../../../threads/utils"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import { quotas } from "@budibase/pro"
|
|
||||||
import { events, context, utils, constants } from "@budibase/backend-core"
|
import { events, context, utils, constants } from "@budibase/backend-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { QueryEvent } from "../../../threads/definitions"
|
import { QueryEvent, QueryResponse } from "../../../threads/definitions"
|
||||||
import { ConfigType, Query, UserCtx, SessionCookie } from "@budibase/types"
|
import {
|
||||||
|
ConfigType,
|
||||||
|
Query,
|
||||||
|
UserCtx,
|
||||||
|
SessionCookie,
|
||||||
|
QuerySchema,
|
||||||
|
FieldType,
|
||||||
|
} from "@budibase/types"
|
||||||
import { ValidQueryNameRegex } from "@budibase/shared-core"
|
import { ValidQueryNameRegex } from "@budibase/shared-core"
|
||||||
|
|
||||||
const Runner = new Thread(ThreadType.QUERY, {
|
const Runner = new Thread(ThreadType.QUERY, {
|
||||||
|
@ -162,39 +168,43 @@ export async function preview(ctx: UserCtx) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rows, keys, info, extra } = (await Runner.run(inputs)) as any
|
const { rows, keys, info, extra } = await Runner.run<QueryResponse>(inputs)
|
||||||
const schemaFields: any = {}
|
const previewSchema: Record<string, QuerySchema> = {}
|
||||||
|
const makeQuerySchema = (type: FieldType, name: string): QuerySchema => ({
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
})
|
||||||
if (rows?.length > 0) {
|
if (rows?.length > 0) {
|
||||||
for (let key of [...new Set(keys)] as string[]) {
|
for (let key of [...new Set(keys)] as string[]) {
|
||||||
const field = rows[0][key]
|
const field = rows[0][key]
|
||||||
let type = typeof field,
|
let type = typeof field,
|
||||||
fieldType = FieldTypes.STRING
|
fieldMetadata = makeQuerySchema(FieldType.STRING, key)
|
||||||
if (field)
|
if (field)
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "boolean":
|
case "boolean":
|
||||||
schemaFields[key] = FieldTypes.BOOLEAN
|
fieldMetadata = makeQuerySchema(FieldType.BOOLEAN, key)
|
||||||
break
|
break
|
||||||
case "object":
|
case "object":
|
||||||
if (field instanceof Date) {
|
if (field instanceof Date) {
|
||||||
fieldType = FieldTypes.DATETIME
|
fieldMetadata = makeQuerySchema(FieldType.DATETIME, key)
|
||||||
} else if (Array.isArray(field)) {
|
} else if (Array.isArray(field)) {
|
||||||
fieldType = FieldTypes.ARRAY
|
fieldMetadata = makeQuerySchema(FieldType.ARRAY, key)
|
||||||
} else {
|
} else {
|
||||||
fieldType = FieldTypes.JSON
|
fieldMetadata = makeQuerySchema(FieldType.JSON, key)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case "number":
|
case "number":
|
||||||
fieldType = FieldTypes.NUMBER
|
fieldMetadata = makeQuerySchema(FieldType.NUMBER, key)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
schemaFields[key] = fieldType
|
previewSchema[key] = fieldMetadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if existing schema, update to include any previous schema keys
|
// if existing schema, update to include any previous schema keys
|
||||||
if (existingSchema) {
|
if (existingSchema) {
|
||||||
for (let key of Object.keys(schemaFields)) {
|
for (let key of Object.keys(previewSchema)) {
|
||||||
if (existingSchema[key]?.type) {
|
if (existingSchema[key]) {
|
||||||
schemaFields[key] = existingSchema[key].type
|
previewSchema[key] = existingSchema[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -203,7 +213,7 @@ export async function preview(ctx: UserCtx) {
|
||||||
await events.query.previewed(datasource, query)
|
await events.query.previewed(datasource, query)
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
rows,
|
rows,
|
||||||
schemaFields,
|
schema: previewSchema,
|
||||||
info,
|
info,
|
||||||
extra,
|
extra,
|
||||||
}
|
}
|
||||||
|
@ -257,7 +267,9 @@ async function execute(
|
||||||
schema: query.schema,
|
schema: query.schema,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rows, pagination, extra, info } = (await Runner.run(inputs)) as any
|
const { rows, pagination, extra, info } = await Runner.run<QueryResponse>(
|
||||||
|
inputs
|
||||||
|
)
|
||||||
// remove the raw from execution incase transformer being used to hide data
|
// remove the raw from execution incase transformer being used to hide data
|
||||||
if (extra?.raw) {
|
if (extra?.raw) {
|
||||||
delete extra.raw
|
delete extra.raw
|
||||||
|
|
|
@ -235,9 +235,9 @@ describe("/queries", () => {
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
// these responses come from the mock
|
// these responses come from the mock
|
||||||
expect(res.body.schemaFields).toEqual({
|
expect(res.body.schema).toEqual({
|
||||||
a: "string",
|
a: { type: "string", name: "a" },
|
||||||
b: "number",
|
b: { type: "number", name: "b" },
|
||||||
})
|
})
|
||||||
expect(res.body.rows.length).toEqual(1)
|
expect(res.body.rows.length).toEqual(1)
|
||||||
expect(events.query.previewed).toBeCalledTimes(1)
|
expect(events.query.previewed).toBeCalledTimes(1)
|
||||||
|
@ -300,10 +300,10 @@ describe("/queries", () => {
|
||||||
queryString: "test={{ variable2 }}",
|
queryString: "test={{ variable2 }}",
|
||||||
})
|
})
|
||||||
// these responses come from the mock
|
// these responses come from the mock
|
||||||
expect(res.body.schemaFields).toEqual({
|
expect(res.body.schema).toEqual({
|
||||||
opts: "json",
|
opts: { type: "json", name: "opts" },
|
||||||
url: "string",
|
url: { type: "string", name: "url" },
|
||||||
value: "string",
|
value: { type: "string", name: "value" },
|
||||||
})
|
})
|
||||||
expect(res.body.rows[0].url).toEqual("http://www.google.com?test=1")
|
expect(res.body.rows[0].url).toEqual("http://www.google.com?test=1")
|
||||||
})
|
})
|
||||||
|
@ -314,10 +314,10 @@ describe("/queries", () => {
|
||||||
path: "www.google.com",
|
path: "www.google.com",
|
||||||
queryString: "test={{ variable3 }}",
|
queryString: "test={{ variable3 }}",
|
||||||
})
|
})
|
||||||
expect(res.body.schemaFields).toEqual({
|
expect(res.body.schema).toEqual({
|
||||||
opts: "json",
|
opts: { type: "json", name: "opts" },
|
||||||
url: "string",
|
url: { type: "string", name: "url" },
|
||||||
value: "string",
|
value: { type: "string", name: "value" },
|
||||||
})
|
})
|
||||||
expect(res.body.rows[0].url).toContain("doctype%20html")
|
expect(res.body.rows[0].url).toContain("doctype%20html")
|
||||||
})
|
})
|
||||||
|
@ -337,10 +337,10 @@ describe("/queries", () => {
|
||||||
path: "www.failonce.com",
|
path: "www.failonce.com",
|
||||||
queryString: "test={{ variable3 }}",
|
queryString: "test={{ variable3 }}",
|
||||||
})
|
})
|
||||||
expect(res.body.schemaFields).toEqual({
|
expect(res.body.schema).toEqual({
|
||||||
fails: "number",
|
fails: { type: "number", name: "fails" },
|
||||||
opts: "json",
|
opts: { type: "json", name: "opts" },
|
||||||
url: "string",
|
url: { type: "string", name: "url" },
|
||||||
})
|
})
|
||||||
expect(res.body.rows[0].fails).toEqual(1)
|
expect(res.body.rows[0].fails).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
|
@ -60,6 +60,7 @@ const environment = {
|
||||||
PLUGINS_DIR: process.env.PLUGINS_DIR || "/plugins",
|
PLUGINS_DIR: process.env.PLUGINS_DIR || "/plugins",
|
||||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||||
MAX_IMPORT_SIZE_MB: process.env.MAX_IMPORT_SIZE_MB,
|
MAX_IMPORT_SIZE_MB: process.env.MAX_IMPORT_SIZE_MB,
|
||||||
|
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
|
||||||
// flags
|
// flags
|
||||||
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
|
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
|
||||||
DISABLE_THREADING: process.env.DISABLE_THREADING,
|
DISABLE_THREADING: process.env.DISABLE_THREADING,
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
import { rowEmission, tableEmission } from "./utils"
|
import { rowEmission, tableEmission } from "./utils"
|
||||||
import mainEmitter from "./index"
|
import mainEmitter from "./index"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { Table, Row } from "@budibase/types"
|
import { Table, Row, DocumentType, App } from "@budibase/types"
|
||||||
|
import { context } from "@budibase/backend-core"
|
||||||
|
|
||||||
// max number of automations that can chain on top of each other
|
const MAX_AUTOMATIONS_ALLOWED = 5
|
||||||
// TODO: in future make this configurable at the automation level
|
|
||||||
const MAX_AUTOMATION_CHAIN = env.SELF_HOSTED ? 5 : 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Special emitter which takes the count of automation runs which have occurred and blocks an
|
|
||||||
* automation from running if it has reached the maximum number of chained automations runs.
|
|
||||||
* This essentially "fakes" the normal emitter to add some functionality in-between to stop automations
|
|
||||||
* from getting stuck endlessly chaining.
|
|
||||||
*/
|
|
||||||
class AutomationEmitter {
|
class AutomationEmitter {
|
||||||
chainCount: number
|
chainCount: number
|
||||||
metadata: { automationChainCount: number }
|
metadata: { automationChainCount: number }
|
||||||
|
@ -24,7 +17,23 @@ class AutomationEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emitRow(eventName: string, appId: string, row: Row, table?: Table) {
|
async getMaxAutomationChain() {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
const appMetadata = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
|
let chainAutomations = appMetadata?.automations?.chainAutomations
|
||||||
|
|
||||||
|
if (chainAutomations === true) {
|
||||||
|
return MAX_AUTOMATIONS_ALLOWED
|
||||||
|
} else if (chainAutomations === undefined && env.SELF_HOSTED) {
|
||||||
|
return MAX_AUTOMATIONS_ALLOWED
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async emitRow(eventName: string, appId: string, row: Row, table?: Table) {
|
||||||
|
let MAX_AUTOMATION_CHAIN = await this.getMaxAutomationChain()
|
||||||
|
|
||||||
// don't emit even if we've reached max automation chain
|
// don't emit even if we've reached max automation chain
|
||||||
if (this.chainCount >= MAX_AUTOMATION_CHAIN) {
|
if (this.chainCount >= MAX_AUTOMATION_CHAIN) {
|
||||||
return
|
return
|
||||||
|
@ -39,9 +48,11 @@ class AutomationEmitter {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
emitTable(eventName: string, appId: string, table?: Table) {
|
async emitTable(eventName: string, appId: string, table?: Table) {
|
||||||
|
let MAX_AUTOMATION_CHAIN = await this.getMaxAutomationChain()
|
||||||
|
|
||||||
// don't emit even if we've reached max automation chain
|
// don't emit even if we've reached max automation chain
|
||||||
if (this.chainCount > MAX_AUTOMATION_CHAIN) {
|
if (this.chainCount >= MAX_AUTOMATION_CHAIN) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -376,8 +376,8 @@ export function checkExternalTables(
|
||||||
errors[name] = "Table must have a primary key."
|
errors[name] = "Table must have a primary key."
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemaFields = Object.keys(table.schema)
|
const columnNames = Object.keys(table.schema)
|
||||||
if (schemaFields.find(f => invalidColumns.includes(f))) {
|
if (columnNames.find(f => invalidColumns.includes(f))) {
|
||||||
errors[name] = "Table contains invalid columns."
|
errors[name] = "Table contains invalid columns."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ const checkAuthorized = async (
|
||||||
const isCreatorApi = permType === PermissionType.CREATOR
|
const isCreatorApi = permType === PermissionType.CREATOR
|
||||||
const isBuilderApi = permType === PermissionType.BUILDER
|
const isBuilderApi = permType === PermissionType.BUILDER
|
||||||
const isGlobalBuilder = users.isGlobalBuilder(ctx.user)
|
const isGlobalBuilder = users.isGlobalBuilder(ctx.user)
|
||||||
const isCreator = users.isCreator(ctx.user)
|
const isCreator = await users.isCreator(ctx.user)
|
||||||
const isBuilder = appId
|
const isBuilder = appId
|
||||||
? users.isBuilder(ctx.user, appId)
|
? users.isBuilder(ctx.user, appId)
|
||||||
: users.hasBuilderPermissions(ctx.user)
|
: users.hasBuilderPermissions(ctx.user)
|
||||||
|
|
|
@ -3,6 +3,27 @@ import { processStringSync } from "@budibase/string-templates"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import { getQueryParams, isProdAppID } from "../../../db/utils"
|
import { getQueryParams, isProdAppID } from "../../../db/utils"
|
||||||
import { BaseQueryVerbs } from "../../../constants"
|
import { BaseQueryVerbs } from "../../../constants"
|
||||||
|
import { Query, QuerySchema } from "@budibase/types"
|
||||||
|
|
||||||
|
function updateSchema(query: Query): Query {
|
||||||
|
if (!query.schema) {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
const schema: Record<string, QuerySchema> = {}
|
||||||
|
for (let key of Object.keys(query.schema)) {
|
||||||
|
if (typeof query.schema[key] === "string") {
|
||||||
|
schema[key] = { type: query.schema[key] as string, name: key }
|
||||||
|
} else {
|
||||||
|
schema[key] = query.schema[key] as QuerySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query.schema = schema
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSchemas(queries: Query[]): Query[] {
|
||||||
|
return queries.map(query => updateSchema(query))
|
||||||
|
}
|
||||||
|
|
||||||
// simple function to append "readable" to all read queries
|
// simple function to append "readable" to all read queries
|
||||||
function enrichQueries(input: any) {
|
function enrichQueries(input: any) {
|
||||||
|
@ -25,7 +46,7 @@ export async function find(queryId: string) {
|
||||||
delete query.fields
|
delete query.fields
|
||||||
delete query.parameters
|
delete query.parameters
|
||||||
}
|
}
|
||||||
return query
|
return updateSchema(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch(opts: { enrich: boolean } = { enrich: true }) {
|
export async function fetch(opts: { enrich: boolean } = { enrich: true }) {
|
||||||
|
@ -37,12 +58,11 @@ export async function fetch(opts: { enrich: boolean } = { enrich: true }) {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const queries = body.rows.map((row: any) => row.doc)
|
let queries = body.rows.map((row: any) => row.doc)
|
||||||
if (opts.enrich) {
|
if (opts.enrich) {
|
||||||
return enrichQueries(queries)
|
queries = await enrichQueries(queries)
|
||||||
} else {
|
|
||||||
return queries
|
|
||||||
}
|
}
|
||||||
|
return updateSchemas(queries)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function enrichContext(
|
export async function enrichContext(
|
||||||
|
|
|
@ -84,7 +84,7 @@ describe("syncGlobalUsers", () => {
|
||||||
await syncGlobalUsers()
|
await syncGlobalUsers()
|
||||||
|
|
||||||
const metadata = await rawUserMetadata()
|
const metadata = await rawUserMetadata()
|
||||||
expect(metadata).toHaveLength(3)
|
expect(metadata).toHaveLength(2)
|
||||||
expect(metadata).toContainEqual(
|
expect(metadata).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
_id: db.generateUserMetadataID(user1._id!),
|
_id: db.generateUserMetadataID(user1._id!),
|
||||||
|
@ -121,7 +121,7 @@ describe("syncGlobalUsers", () => {
|
||||||
await syncGlobalUsers()
|
await syncGlobalUsers()
|
||||||
|
|
||||||
const metadata = await rawUserMetadata()
|
const metadata = await rawUserMetadata()
|
||||||
expect(metadata).toHaveLength(0)
|
expect(metadata).toHaveLength(1) //ADMIN user created in test bootstrap still in the application
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -278,6 +278,9 @@ class TestConfiguration {
|
||||||
if (params) {
|
if (params) {
|
||||||
request.params = params
|
request.params = params
|
||||||
}
|
}
|
||||||
|
request.throw = (status: number, message: string) => {
|
||||||
|
throw new Error(`Error ${status} - ${message}`)
|
||||||
|
}
|
||||||
return this.doInContext(appId, async () => {
|
return this.doInContext(appId, async () => {
|
||||||
await controlFunc(request)
|
await controlFunc(request)
|
||||||
return request.body
|
return request.body
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { QuerySchema, Row } from "@budibase/types"
|
||||||
|
|
||||||
export type WorkerCallback = (error: any, response?: any) => void
|
export type WorkerCallback = (error: any, response?: any) => void
|
||||||
|
|
||||||
export interface QueryEvent {
|
export interface QueryEvent {
|
||||||
|
@ -11,7 +13,15 @@ export interface QueryEvent {
|
||||||
queryId: string
|
queryId: string
|
||||||
environmentVariables?: Record<string, string>
|
environmentVariables?: Record<string, string>
|
||||||
ctx?: any
|
ctx?: any
|
||||||
schema?: Record<string, { name?: string; type: string }>
|
schema?: Record<string, QuerySchema | string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryResponse {
|
||||||
|
rows: Row[]
|
||||||
|
keys: string[]
|
||||||
|
info: any
|
||||||
|
extra: any
|
||||||
|
pagination: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryVariable {
|
export interface QueryVariable {
|
||||||
|
|
|
@ -74,7 +74,7 @@ export class Thread {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
run(job: AutomationJob | QueryEvent) {
|
run<T>(job: AutomationJob | QueryEvent): Promise<T> {
|
||||||
const timeout = this.timeoutMs
|
const timeout = this.timeoutMs
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
function fire(worker: any) {
|
function fire(worker: any) {
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { default as threadUtils } from "./utils"
|
import { default as threadUtils } from "./utils"
|
||||||
|
|
||||||
threadUtils.threadSetup()
|
threadUtils.threadSetup()
|
||||||
import { WorkerCallback, QueryEvent, QueryVariable } from "./definitions"
|
import {
|
||||||
|
WorkerCallback,
|
||||||
|
QueryEvent,
|
||||||
|
QueryVariable,
|
||||||
|
QueryResponse,
|
||||||
|
} from "./definitions"
|
||||||
import ScriptRunner from "../utilities/scriptRunner"
|
import ScriptRunner from "../utilities/scriptRunner"
|
||||||
import { getIntegration } from "../integrations"
|
import { getIntegration } from "../integrations"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
@ -9,7 +14,7 @@ import { context, cache, auth } from "@budibase/backend-core"
|
||||||
import { getGlobalIDFromUserMetadataID } from "../db/utils"
|
import { getGlobalIDFromUserMetadataID } from "../db/utils"
|
||||||
import sdk from "../sdk"
|
import sdk from "../sdk"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { SourceName, Query } from "@budibase/types"
|
import { Query } from "@budibase/types"
|
||||||
|
|
||||||
import { isSQL } from "../integrations/utils"
|
import { isSQL } from "../integrations/utils"
|
||||||
import { interpolateSQL } from "../integrations/queries/sql"
|
import { interpolateSQL } from "../integrations/queries/sql"
|
||||||
|
@ -53,7 +58,7 @@ class QueryRunner {
|
||||||
this.hasDynamicVariables = false
|
this.hasDynamicVariables = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(): Promise<any> {
|
async execute(): Promise<QueryResponse> {
|
||||||
let { datasource, fields, queryVerb, transformer, schema } = this
|
let { datasource, fields, queryVerb, transformer, schema } = this
|
||||||
let datasourceClone = cloneDeep(datasource)
|
let datasourceClone = cloneDeep(datasource)
|
||||||
let fieldsClone = cloneDeep(fields)
|
let fieldsClone = cloneDeep(fields)
|
||||||
|
|
|
@ -48,6 +48,9 @@ async function checkResponse(
|
||||||
let error
|
let error
|
||||||
try {
|
try {
|
||||||
error = await response.json()
|
error = await response.json()
|
||||||
|
if (!error.message) {
|
||||||
|
error = JSON.stringify(error)
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = await response.text()
|
error = await response.text()
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ export function hasAppCreatorPermissions(user?: User | ContextUser): boolean {
|
||||||
return _.flow(
|
return _.flow(
|
||||||
_.get("roles"),
|
_.get("roles"),
|
||||||
_.values,
|
_.values,
|
||||||
_.find(x => x === "CREATOR"),
|
_.find(x => ["CREATOR", "ADMIN"].includes(x)),
|
||||||
x => !!x
|
x => !!x
|
||||||
)(user)
|
)(user)
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@
|
||||||
"n"
|
"n"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{ after [1, 2, 3] 1}} -> [3]",
|
"example": "{{ after ['a', 'b', 'c', 'd'] 2}} -> ['c', 'd']",
|
||||||
"description": "<p>Returns all of the items in an array after the specified index. Opposite of <a href=\"#before\">before</a>.</p>\n"
|
"description": "<p>Returns all of the items in an array after the specified index. Opposite of <a href=\"#before\">before</a>.</p>\n"
|
||||||
},
|
},
|
||||||
"arrayify": {
|
"arrayify": {
|
||||||
|
@ -154,7 +154,7 @@
|
||||||
"n"
|
"n"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{ before [1, 2, 3] 2}} -> [1, 2]",
|
"example": "{{ before ['a', 'b', 'c', 'd'] 3}} -> ['a', 'b']",
|
||||||
"description": "<p>Return all of the items in the collection before the specified count. Opposite of <a href=\"#after\">after</a>.</p>\n"
|
"description": "<p>Return all of the items in the collection before the specified count. Opposite of <a href=\"#after\">after</a>.</p>\n"
|
||||||
},
|
},
|
||||||
"eachIndex": {
|
"eachIndex": {
|
||||||
|
@ -182,7 +182,7 @@
|
||||||
"n"
|
"n"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{first [1, 2, 3, 4] 2}} -> [1, 2]",
|
"example": "{{first [1, 2, 3, 4] 2}} -> 1,2",
|
||||||
"description": "<p>Returns the first item, or first <code>n</code> items of an array.</p>\n"
|
"description": "<p>Returns the first item, or first <code>n</code> items of an array.</p>\n"
|
||||||
},
|
},
|
||||||
"forEach": {
|
"forEach": {
|
||||||
|
@ -200,7 +200,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#inArray [1, 2, 3] 2}} 2 exists {{else}} 2 does not exist {{/inArray}} -> 2 exists",
|
"example": "{{#inArray [1, 2, 3] 2}} 2 exists {{else}} 2 does not exist {{/inArray}} -> ' 2 exists '",
|
||||||
"description": "<p>Block helper that renders the block if an array has the given <code>value</code>. Optionally specify an inverse block to render when the array does not have the given value.</p>\n"
|
"description": "<p>Block helper that renders the block if an array has the given <code>value</code>. Optionally specify an inverse block to render when the array does not have the given value.</p>\n"
|
||||||
},
|
},
|
||||||
"isArray": {
|
"isArray": {
|
||||||
|
@ -226,7 +226,7 @@
|
||||||
"separator"
|
"separator"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{join [1, 2, 3]}} -> '1, 2, 3'",
|
"example": "{{join [1, 2, 3]}} -> 1, 2, 3",
|
||||||
"description": "<p>Join all elements of array into a string, optionally using a given separator.</p>\n"
|
"description": "<p>Join all elements of array into a string, optionally using a given separator.</p>\n"
|
||||||
},
|
},
|
||||||
"equalsLength": {
|
"equalsLength": {
|
||||||
|
@ -236,7 +236,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{equalsLength '[1,2,3]' 3}} -> true",
|
"example": "{{equalsLength [1, 2, 3] 3}} -> true",
|
||||||
"description": "<p>Returns true if the the length of the given <code>value</code> is equal to the given <code>length</code>. Can be used as a block or inline helper.</p>\n"
|
"description": "<p>Returns true if the the length of the given <code>value</code> is equal to the given <code>length</code>. Can be used as a block or inline helper.</p>\n"
|
||||||
},
|
},
|
||||||
"last": {
|
"last": {
|
||||||
|
@ -253,7 +253,7 @@
|
||||||
"value"
|
"value"
|
||||||
],
|
],
|
||||||
"numArgs": 1,
|
"numArgs": 1,
|
||||||
"example": "{{length '[1, 2, 3]'}} -> 3",
|
"example": "{{length [1, 2, 3]}} -> 3",
|
||||||
"description": "<p>Returns the length of the given string or array.</p>\n"
|
"description": "<p>Returns the length of the given string or array.</p>\n"
|
||||||
},
|
},
|
||||||
"lengthEqual": {
|
"lengthEqual": {
|
||||||
|
@ -263,7 +263,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{equalsLength '[1,2,3]' 3}} -> true",
|
"example": "{{equalsLength [1, 2, 3] 3}} -> true",
|
||||||
"description": "<p>Returns true if the the length of the given <code>value</code> is equal to the given <code>length</code>. Can be used as a block or inline helper.</p>\n"
|
"description": "<p>Returns true if the the length of the given <code>value</code> is equal to the given <code>length</code>. Can be used as a block or inline helper.</p>\n"
|
||||||
},
|
},
|
||||||
"map": {
|
"map": {
|
||||||
|
@ -299,7 +299,7 @@
|
||||||
"provided"
|
"provided"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#some [1, 'b', 3] isString}} string found {{else}} No string found {{/some}} -> string found",
|
"example": "{{#some [1, \"b\", 3] isString}} string found {{else}} No string found {{/some}} -> ' string found '",
|
||||||
"description": "<p>Block helper that returns the block if the callback returns true for some value in the given array.</p>\n"
|
"description": "<p>Block helper that returns the block if the callback returns true for some value in the given array.</p>\n"
|
||||||
},
|
},
|
||||||
"sort": {
|
"sort": {
|
||||||
|
@ -317,7 +317,7 @@
|
||||||
"props"
|
"props"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{ sortBy [{a: 'zzz'}, {a: 'aaa'}] 'a' }} -> [{'a':'aaa'}, {'a':'zzz'}]",
|
"example": "{{ sortBy [{'a': 'zzz'}, {'a': 'aaa'}] 'a' }} -> [{'a':'aaa'},{'a':'zzz'}]",
|
||||||
"description": "<p>Sort an <code>array</code>. If an array of objects is passed, you may optionally pass a <code>key</code> to sort on as the second argument. You may alternatively pass a sorting function as the second argument.</p>\n"
|
"description": "<p>Sort an <code>array</code>. If an array of objects is passed, you may optionally pass a <code>key</code> to sort on as the second argument. You may alternatively pass a sorting function as the second argument.</p>\n"
|
||||||
},
|
},
|
||||||
"withAfter": {
|
"withAfter": {
|
||||||
|
@ -347,7 +347,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{ withFirst [1, 2, 3] }} {{this}} {{/withFirst}}",
|
"example": "{{#withFirst [1, 2, 3] }}{{this}}{{/withFirst}} -> 1",
|
||||||
"description": "<p>Use the first item in a collection inside a handlebars block expression. Opposite of <a href=\"#withLast\">withLast</a>.</p>\n"
|
"description": "<p>Use the first item in a collection inside a handlebars block expression. Opposite of <a href=\"#withLast\">withLast</a>.</p>\n"
|
||||||
},
|
},
|
||||||
"withGroup": {
|
"withGroup": {
|
||||||
|
@ -357,7 +357,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#withGroup [1, 2, 3, 4] 2}} {{#each this}} {{.}} {{each}} <br> {{/withGroup}} -> 1,2<br> 3,4<br>",
|
"example": "{{#withGroup [1, 2, 3, 4] 2}}{{#each this}}{{.}}{{/each}}<br>{{/withGroup}} -> 12<br>34<br>",
|
||||||
"description": "<p>Block helper that groups array elements by given group <code>size</code>.</p>\n"
|
"description": "<p>Block helper that groups array elements by given group <code>size</code>.</p>\n"
|
||||||
},
|
},
|
||||||
"withLast": {
|
"withLast": {
|
||||||
|
@ -367,7 +367,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#withLast [1, 2, 3, 4]}} {{this}} {{/withLast}} -> 4",
|
"example": "{{#withLast [1, 2, 3, 4]}}{{this}}{{/withLast}} -> 4",
|
||||||
"description": "<p>Use the last item or <code>n</code> items in an array as context inside a block. Opposite of <a href=\"#withFirst\">withFirst</a>.</p>\n"
|
"description": "<p>Use the last item or <code>n</code> items in an array as context inside a block. Opposite of <a href=\"#withFirst\">withFirst</a>.</p>\n"
|
||||||
},
|
},
|
||||||
"withSort": {
|
"withSort": {
|
||||||
|
@ -377,7 +377,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#withSort ['b', 'a', 'c']}} {{this}} {{/withSort}} -> abc",
|
"example": "{{#withSort ['b', 'a', 'c']}}{{this}}{{/withSort}} -> abc",
|
||||||
"description": "<p>Block helper that sorts a collection and exposes the sorted collection as context inside the block.</p>\n"
|
"description": "<p>Block helper that sorts a collection and exposes the sorted collection as context inside the block.</p>\n"
|
||||||
},
|
},
|
||||||
"unique": {
|
"unique": {
|
||||||
|
@ -386,7 +386,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{#each (unique ['a', 'a', 'c', 'b', 'e', 'e']) }} {{.}} {{/each}} -> acbe",
|
"example": "{{#each (unique ['a', 'a', 'c', 'b', 'e', 'e']) }}{{.}}{{/each}} -> acbe",
|
||||||
"description": "<p>Block helper that return an array with all duplicate values removed. Best used along with a <a href=\"#each\">each</a> helper.</p>\n"
|
"description": "<p>Block helper that return an array with all duplicate values removed. Best used along with a <a href=\"#each\">each</a> helper.</p>\n"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -396,7 +396,7 @@
|
||||||
"number"
|
"number"
|
||||||
],
|
],
|
||||||
"numArgs": 1,
|
"numArgs": 1,
|
||||||
"example": "{{ bytes 1386 }} -> 1.4Kb",
|
"example": "{{ bytes 1386 1 }} -> 1.4 kB",
|
||||||
"description": "<p>Format a number to it's equivalent in bytes. If a string is passed, it's length will be formatted and returned. <strong>Examples:</strong> - <code>'foo' => 3 B</code> - <code>13661855 => 13.66 MB</code> - <code>825399 => 825.39 kB</code> - <code>1396 => 1.4 kB</code></p>\n"
|
"description": "<p>Format a number to it's equivalent in bytes. If a string is passed, it's length will be formatted and returned. <strong>Examples:</strong> - <code>'foo' => 3 B</code> - <code>13661855 => 13.66 MB</code> - <code>825399 => 825.39 kB</code> - <code>1396 => 1.4 kB</code></p>\n"
|
||||||
},
|
},
|
||||||
"addCommas": {
|
"addCommas": {
|
||||||
|
@ -430,7 +430,7 @@
|
||||||
"fractionDigits"
|
"fractionDigits"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{ toExponential 10123 2 }} -> 101e+4",
|
"example": "{{ toExponential 10123 2 }} -> 1.01e+4",
|
||||||
"description": "<p>Returns a string representing the given number in exponential notation.</p>\n"
|
"description": "<p>Returns a string representing the given number in exponential notation.</p>\n"
|
||||||
},
|
},
|
||||||
"toFixed": {
|
"toFixed": {
|
||||||
|
@ -472,7 +472,7 @@
|
||||||
"str"
|
"str"
|
||||||
],
|
],
|
||||||
"numArgs": 1,
|
"numArgs": 1,
|
||||||
"example": "{{ encodeURI 'https://myurl?Hello There' }} -> https://myurl?Hello%20There",
|
"example": "{{ encodeURI 'https://myurl?Hello There' }} -> https%3A%2F%2Fmyurl%3FHello%20There",
|
||||||
"description": "<p>Encodes a Uniform Resource Identifier (URI) component by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character.</p>\n"
|
"description": "<p>Encodes a Uniform Resource Identifier (URI) component by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character.</p>\n"
|
||||||
},
|
},
|
||||||
"escape": {
|
"escape": {
|
||||||
|
@ -480,7 +480,7 @@
|
||||||
"str"
|
"str"
|
||||||
],
|
],
|
||||||
"numArgs": 1,
|
"numArgs": 1,
|
||||||
"example": "{{ escape 'https://myurl?Hello+There' }} -> https://myurl?Hello%20There",
|
"example": "{{ escape 'https://myurl?Hello+There' }} -> https%3A%2F%2Fmyurl%3FHello%2BThere",
|
||||||
"description": "<p>Escape the given string by replacing characters with escape sequences. Useful for allowing the string to be used in a URL, etc.</p>\n"
|
"description": "<p>Escape the given string by replacing characters with escape sequences. Useful for allowing the string to be used in a URL, etc.</p>\n"
|
||||||
},
|
},
|
||||||
"decodeURI": {
|
"decodeURI": {
|
||||||
|
@ -488,7 +488,7 @@
|
||||||
"str"
|
"str"
|
||||||
],
|
],
|
||||||
"numArgs": 1,
|
"numArgs": 1,
|
||||||
"example": "{{ decodeURI 'https://myurl?Hello%20There' }} -> https://myurl?=Hello There",
|
"example": "{{ decodeURI 'https://myurl?Hello%20There' }} -> https://myurl?Hello There",
|
||||||
"description": "<p>Decode a Uniform Resource Identifier (URI) component.</p>\n"
|
"description": "<p>Decode a Uniform Resource Identifier (URI) component.</p>\n"
|
||||||
},
|
},
|
||||||
"urlResolve": {
|
"urlResolve": {
|
||||||
|
@ -513,7 +513,7 @@
|
||||||
"url"
|
"url"
|
||||||
],
|
],
|
||||||
"numArgs": 1,
|
"numArgs": 1,
|
||||||
"example": "{{ stripQueryString 'https://myurl/api/test?foo=bar' }} -> 'https://myurl/api/test'",
|
"example": "{{ stripQuerystring 'https://myurl/api/test?foo=bar' }} -> 'https://myurl/api/test'",
|
||||||
"description": "<p>Strip the query string from the given <code>url</code>.</p>\n"
|
"description": "<p>Strip the query string from the given <code>url</code>.</p>\n"
|
||||||
},
|
},
|
||||||
"stripProtocol": {
|
"stripProtocol": {
|
||||||
|
@ -521,7 +521,7 @@
|
||||||
"str"
|
"str"
|
||||||
],
|
],
|
||||||
"numArgs": 1,
|
"numArgs": 1,
|
||||||
"example": "{{ stripProtocol 'https://myurl/api/test' }} -> 'myurl/api/test'",
|
"example": "{{ stripProtocol 'https://myurl/api/test' }} -> '//myurl/api/test'",
|
||||||
"description": "<p>Strip protocol from a <code>url</code>. Useful for displaying media that may have an 'http' protocol on secure connections.</p>\n"
|
"description": "<p>Strip protocol from a <code>url</code>. Useful for displaying media that may have an 'http' protocol on secure connections.</p>\n"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -573,7 +573,7 @@
|
||||||
"string"
|
"string"
|
||||||
],
|
],
|
||||||
"numArgs": 1,
|
"numArgs": 1,
|
||||||
"example": "{{ chop ' ABC '}} -> 'ABC'",
|
"example": "{{ chop ' ABC '}} -> ABC",
|
||||||
"description": "<p>Like trim, but removes both extraneous whitespace <strong>and non-word characters</strong> from the beginning and end of a string.</p>\n"
|
"description": "<p>Like trim, but removes both extraneous whitespace <strong>and non-word characters</strong> from the beginning and end of a string.</p>\n"
|
||||||
},
|
},
|
||||||
"dashcase": {
|
"dashcase": {
|
||||||
|
@ -606,7 +606,7 @@
|
||||||
"length"
|
"length"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{ellipsis 'foo bar baz', 7}} -> foo bar…",
|
"example": "{{ellipsis 'foo bar baz' 7}} -> foo bar…",
|
||||||
"description": "<p>Truncates a string to the specified <code>length</code>, and appends it with an elipsis, <code>…</code>.</p>\n"
|
"description": "<p>Truncates a string to the specified <code>length</code>, and appends it with an elipsis, <code>…</code>.</p>\n"
|
||||||
},
|
},
|
||||||
"hyphenate": {
|
"hyphenate": {
|
||||||
|
@ -675,14 +675,6 @@
|
||||||
"example": "{{prepend 'bar' 'foo-'}} -> foo-bar",
|
"example": "{{prepend 'bar' 'foo-'}} -> foo-bar",
|
||||||
"description": "<p>Prepends the given <code>string</code> with the specified <code>prefix</code>.</p>\n"
|
"description": "<p>Prepends the given <code>string</code> with the specified <code>prefix</code>.</p>\n"
|
||||||
},
|
},
|
||||||
"raw": {
|
|
||||||
"args": [
|
|
||||||
"options"
|
|
||||||
],
|
|
||||||
"numArgs": 1,
|
|
||||||
"example": "{{{{#raw}}}} {{foo}} {{{{/raw}}}} -> {{foo}}",
|
|
||||||
"description": "<p>Render a block without processing mustache templates inside the block.</p>\n"
|
|
||||||
},
|
|
||||||
"remove": {
|
"remove": {
|
||||||
"args": [
|
"args": [
|
||||||
"str",
|
"str",
|
||||||
|
@ -698,7 +690,7 @@
|
||||||
"substring"
|
"substring"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{remove 'a b a b a b' 'a'}} -> b a b a b",
|
"example": "{{removeFirst 'a b a b a b' 'a'}} -> ' b a b a b'",
|
||||||
"description": "<p>Remove the first occurrence of <code>substring</code> from the given <code>str</code>.</p>\n"
|
"description": "<p>Remove the first occurrence of <code>substring</code> from the given <code>str</code>.</p>\n"
|
||||||
},
|
},
|
||||||
"replace": {
|
"replace": {
|
||||||
|
@ -718,7 +710,7 @@
|
||||||
"b"
|
"b"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{replace 'a b a b a b' 'a' 'z'}} -> z b a b a b",
|
"example": "{{replaceFirst 'a b a b a b' 'a' 'z'}} -> z b a b a b",
|
||||||
"description": "<p>Replace the first occurrence of substring <code>a</code> with substring <code>b</code>.</p>\n"
|
"description": "<p>Replace the first occurrence of substring <code>a</code> with substring <code>b</code>.</p>\n"
|
||||||
},
|
},
|
||||||
"sentence": {
|
"sentence": {
|
||||||
|
@ -752,7 +744,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#startsWith 'Goodbye' 'Hello, world!'}} Yep {{else}} Nope {{/startsWith}} -> Nope",
|
"example": "{{#startsWith 'Goodbye' 'Hello, world!'}}Yep{{else}}Nope{{/startsWith}} -> Nope",
|
||||||
"description": "<p>Tests whether a string begins with the given prefix.</p>\n"
|
"description": "<p>Tests whether a string begins with the given prefix.</p>\n"
|
||||||
},
|
},
|
||||||
"titleize": {
|
"titleize": {
|
||||||
|
@ -760,7 +752,7 @@
|
||||||
"str"
|
"str"
|
||||||
],
|
],
|
||||||
"numArgs": 1,
|
"numArgs": 1,
|
||||||
"example": "{{#titleize 'this is title case' }} -> This Is Title Case",
|
"example": "{{titleize 'this is title case' }} -> This Is Title Case",
|
||||||
"description": "<p>Title case the given string.</p>\n"
|
"description": "<p>Title case the given string.</p>\n"
|
||||||
},
|
},
|
||||||
"trim": {
|
"trim": {
|
||||||
|
@ -784,7 +776,7 @@
|
||||||
"string"
|
"string"
|
||||||
],
|
],
|
||||||
"numArgs": 1,
|
"numArgs": 1,
|
||||||
"example": "{{trimRight ' ABC ' }} -> ' ABC '",
|
"example": "{{trimRight ' ABC ' }} -> ' ABC'",
|
||||||
"description": "<p>Removes extraneous whitespace from the end of a string.</p>\n"
|
"description": "<p>Removes extraneous whitespace from the end of a string.</p>\n"
|
||||||
},
|
},
|
||||||
"truncate": {
|
"truncate": {
|
||||||
|
@ -804,7 +796,7 @@
|
||||||
"suffix"
|
"suffix"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{truncateWords 'foo bar baz' 1 }} -> foo",
|
"example": "{{truncateWords 'foo bar baz' 1 }} -> foo…",
|
||||||
"description": "<p>Truncate a string to have the specified number of words. Also see <a href=\"#truncate\">truncate</a>.</p>\n"
|
"description": "<p>Truncate a string to have the specified number of words. Also see <a href=\"#truncate\">truncate</a>.</p>\n"
|
||||||
},
|
},
|
||||||
"upcase": {
|
"upcase": {
|
||||||
|
@ -844,7 +836,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 4,
|
"numArgs": 4,
|
||||||
"example": "{{compare 10 '<' 5 }} -> true",
|
"example": "{{compare 10 '<' 5 }} -> false",
|
||||||
"description": "<p>Render a block when a comparison of the first and third arguments returns true. The second argument is the [arithemetic operator][operators] to use. You may also optionally specify an inverse block to render when falsy.</p>\n"
|
"description": "<p>Render a block when a comparison of the first and third arguments returns true. The second argument is the [arithemetic operator][operators] to use. You may also optionally specify an inverse block to render when falsy.</p>\n"
|
||||||
},
|
},
|
||||||
"contains": {
|
"contains": {
|
||||||
|
@ -874,7 +866,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#eq 3 3}} equal{{else}} not equal{{/eq}} -> equal",
|
"example": "{{#eq 3 3}}equal{{else}}not equal{{/eq}} -> equal",
|
||||||
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=''</code> hash argument for the second value.</p>\n"
|
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=''</code> hash argument for the second value.</p>\n"
|
||||||
},
|
},
|
||||||
"gt": {
|
"gt": {
|
||||||
|
@ -884,7 +876,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#gt 4 3}} greater than{{else}} not greater than{{/gt}} -> greater than",
|
"example": "{{#gt 4 3}} greater than{{else}} not greater than{{/gt}} -> ' greater than'",
|
||||||
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>greater than</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=''</code> hash argument for the second value.</p>\n"
|
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>greater than</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=''</code> hash argument for the second value.</p>\n"
|
||||||
},
|
},
|
||||||
"gte": {
|
"gte": {
|
||||||
|
@ -894,7 +886,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#gte 4 3}} greater than or equal{{else}} not greater than{{/gte}} -> greater than or equal",
|
"example": "{{#gte 4 3}} greater than or equal{{else}} not greater than{{/gte}} -> ' greater than or equal'",
|
||||||
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>greater than or equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=''</code> hash argument for the second value.</p>\n"
|
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>greater than or equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=''</code> hash argument for the second value.</p>\n"
|
||||||
},
|
},
|
||||||
"has": {
|
"has": {
|
||||||
|
@ -904,7 +896,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#has 'foobar' 'foo'}} has it{{else}} doesn't{{/has}} -> has it",
|
"example": "{{#has 'foobar' 'foo'}}has it{{else}}doesn't{{/has}} -> has it",
|
||||||
"description": "<p>Block helper that renders a block if <code>value</code> has <code>pattern</code>. If an inverse block is specified it will be rendered when falsy.</p>\n"
|
"description": "<p>Block helper that renders a block if <code>value</code> has <code>pattern</code>. If an inverse block is specified it will be rendered when falsy.</p>\n"
|
||||||
},
|
},
|
||||||
"isFalsey": {
|
"isFalsey": {
|
||||||
|
@ -931,7 +923,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{#ifEven 2}} even {{else}} odd {{/ifEven}} -> even",
|
"example": "{{#ifEven 2}} even {{else}} odd {{/ifEven}} -> ' even '",
|
||||||
"description": "<p>Return true if the given value is an even number.</p>\n"
|
"description": "<p>Return true if the given value is an even number.</p>\n"
|
||||||
},
|
},
|
||||||
"ifNth": {
|
"ifNth": {
|
||||||
|
@ -941,8 +933,8 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#ifNth 10 2}} remainder {{else}} no remainder {{/ifNth}} -> remainder",
|
"example": "{{#ifNth 2 10}}remainder{{else}}no remainder{{/ifNth}} -> remainder",
|
||||||
"description": "<p>Conditionally renders a block if the remainder is zero when <code>a</code> operand is divided by <code>b</code>. If an inverse block is specified it will be rendered when the remainder is <strong>not zero</strong>.</p>\n"
|
"description": "<p>Conditionally renders a block if the remainder is zero when <code>b</code> operand is divided by <code>a</code>. If an inverse block is specified it will be rendered when the remainder is <strong>not zero</strong>.</p>\n"
|
||||||
},
|
},
|
||||||
"ifOdd": {
|
"ifOdd": {
|
||||||
"args": [
|
"args": [
|
||||||
|
@ -950,7 +942,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{#ifOdd 3}} odd {{else}} even {{/ifOdd}} -> odd",
|
"example": "{{#ifOdd 3}}odd{{else}}even{{/ifOdd}} -> odd",
|
||||||
"description": "<p>Block helper that renders a block if <code>value</code> is <strong>an odd number</strong>. If an inverse block is specified it will be rendered when falsy.</p>\n"
|
"description": "<p>Block helper that renders a block if <code>value</code> is <strong>an odd number</strong>. If an inverse block is specified it will be rendered when falsy.</p>\n"
|
||||||
},
|
},
|
||||||
"is": {
|
"is": {
|
||||||
|
@ -960,7 +952,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#is 3 3}} is {{else}} is not {{/is}} -> is",
|
"example": "{{#is 3 3}} is {{else}} is not {{/is}} -> ' is '",
|
||||||
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. Similar to <a href=\"#eq\">eq</a> but does not do strict equality.</p>\n"
|
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. Similar to <a href=\"#eq\">eq</a> but does not do strict equality.</p>\n"
|
||||||
},
|
},
|
||||||
"isnt": {
|
"isnt": {
|
||||||
|
@ -970,7 +962,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#isnt 3 3}} isnt {{else}} is {{/isnt}} -> is",
|
"example": "{{#isnt 3 3}} isnt {{else}} is {{/isnt}} -> ' is '",
|
||||||
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>not equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. Similar to <a href=\"#unlesseq\">unlessEq</a> but does not use strict equality for comparisons.</p>\n"
|
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>not equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. Similar to <a href=\"#unlesseq\">unlessEq</a> but does not use strict equality for comparisons.</p>\n"
|
||||||
},
|
},
|
||||||
"lt": {
|
"lt": {
|
||||||
|
@ -979,7 +971,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{#lt 2 3}} less than {{else}} more than or equal {{/lt}} -> less than",
|
"example": "{{#lt 2 3}} less than {{else}} more than or equal {{/lt}} -> ' less than '",
|
||||||
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>less than</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=''</code> hash argument for the second value.</p>\n"
|
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>less than</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=''</code> hash argument for the second value.</p>\n"
|
||||||
},
|
},
|
||||||
"lte": {
|
"lte": {
|
||||||
|
@ -989,7 +981,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#lte 2 3}} less than or equal {{else}} more than {{/lte}} -> less than or equal",
|
"example": "{{#lte 2 3}} less than or equal {{else}} more than {{/lte}} -> ' less than or equal '",
|
||||||
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>less than or equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=''</code> hash argument for the second value.</p>\n"
|
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>less than or equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=''</code> hash argument for the second value.</p>\n"
|
||||||
},
|
},
|
||||||
"neither": {
|
"neither": {
|
||||||
|
@ -999,7 +991,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#neither null null}} both falsey {{else}} both not falsey {{/neither}} -> both falsey",
|
"example": "{{#neither null null}}both falsey{{else}}both not falsey{{/neither}} -> both falsey",
|
||||||
"description": "<p>Block helper that renders a block if <strong>neither of</strong> the given values are truthy. If an inverse block is specified it will be rendered when falsy.</p>\n"
|
"description": "<p>Block helper that renders a block if <strong>neither of</strong> the given values are truthy. If an inverse block is specified it will be rendered when falsy.</p>\n"
|
||||||
},
|
},
|
||||||
"not": {
|
"not": {
|
||||||
|
@ -1008,7 +1000,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{#not undefined }} falsey {{else}} not falsey {{/not}} -> falsey",
|
"example": "{{#not undefined }}falsey{{else}}not falsey{{/not}} -> falsey",
|
||||||
"description": "<p>Returns true if <code>val</code> is falsey. Works as a block or inline helper.</p>\n"
|
"description": "<p>Returns true if <code>val</code> is falsey. Works as a block or inline helper.</p>\n"
|
||||||
},
|
},
|
||||||
"or": {
|
"or": {
|
||||||
|
@ -1017,7 +1009,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{#or 1 2 undefined }} at least one truthy {{else}} all falsey {{/or}} -> at least one truthy",
|
"example": "{{#or 1 2 undefined }} at least one truthy {{else}} all falsey {{/or}} -> ' at least one truthy '",
|
||||||
"description": "<p>Block helper that renders a block if <strong>any of</strong> the given values is truthy. If an inverse block is specified it will be rendered when falsy.</p>\n"
|
"description": "<p>Block helper that renders a block if <strong>any of</strong> the given values is truthy. If an inverse block is specified it will be rendered when falsy.</p>\n"
|
||||||
},
|
},
|
||||||
"unlessEq": {
|
"unlessEq": {
|
||||||
|
@ -1027,7 +1019,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#unlessEq 2 1 }} not equal {{else}} equal {{/unlessEq}} -> not equal",
|
"example": "{{#unlessEq 2 1 }} not equal {{else}} equal {{/unlessEq}} -> ' not equal '",
|
||||||
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is equal to <code>b</code></strong>.</p>\n"
|
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is equal to <code>b</code></strong>.</p>\n"
|
||||||
},
|
},
|
||||||
"unlessGt": {
|
"unlessGt": {
|
||||||
|
@ -1037,7 +1029,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#unlessGt 20 1 }} not greater than {{else}} greater than {{/unlessGt}} -> greater than",
|
"example": "{{#unlessGt 20 1 }} not greater than {{else}} greater than {{/unlessGt}} -> ' greater than '",
|
||||||
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is greater than <code>b</code></strong>.</p>\n"
|
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is greater than <code>b</code></strong>.</p>\n"
|
||||||
},
|
},
|
||||||
"unlessLt": {
|
"unlessLt": {
|
||||||
|
@ -1047,7 +1039,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#unlessLt 20 1 }} greater than or equal {{else}} less than {{/unlessLt}} -> greater than or equal",
|
"example": "{{#unlessLt 20 1 }}greater than or equal{{else}}less than{{/unlessLt}} -> greater than or equal",
|
||||||
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is less than <code>b</code></strong>.</p>\n"
|
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is less than <code>b</code></strong>.</p>\n"
|
||||||
},
|
},
|
||||||
"unlessGteq": {
|
"unlessGteq": {
|
||||||
|
@ -1057,7 +1049,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#unlessGteq 20 1 }} less than {{else}} greater than or equal to {{/unlessGteq}} -> greater than or equal to",
|
"example": "{{#unlessGteq 20 1 }} less than {{else}}greater than or equal to{{/unlessGteq}} -> greater than or equal to",
|
||||||
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is greater than or equal to <code>b</code></strong>.</p>\n"
|
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is greater than or equal to <code>b</code></strong>.</p>\n"
|
||||||
},
|
},
|
||||||
"unlessLteq": {
|
"unlessLteq": {
|
||||||
|
@ -1067,7 +1059,7 @@
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"numArgs": 3,
|
"numArgs": 3,
|
||||||
"example": "{{#unlessLteq 20 1 }} greater than {{else}} less than or equal to {{/unlessLteq}} -> greater than",
|
"example": "{{#unlessLteq 20 1 }} greater than {{else}} less than or equal to {{/unlessLteq}} -> ' greater than '",
|
||||||
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is less than or equal to <code>b</code></strong>.</p>\n"
|
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is less than or equal to <code>b</code></strong>.</p>\n"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1204,7 +1196,7 @@
|
||||||
"durationType"
|
"durationType"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{duration timeLeft \"seconds\"}} -> a few seconds",
|
"example": "{{duration 8 \"seconds\"}} -> a few seconds",
|
||||||
"description": "<p>Produce a humanized duration left/until given an amount of time and the type of time measurement.</p>\n"
|
"description": "<p>Produce a humanized duration left/until given an amount of time and the type of time measurement.</p>\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
"manifest": "node ./scripts/gen-collection-info.js"
|
"manifest": "node ./scripts/gen-collection-info.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/handlebars-helpers": "^0.12.0",
|
"@budibase/handlebars-helpers": "^0.13.0",
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
"handlebars": "^4.7.6",
|
"handlebars": "^4.7.6",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
|
|
@ -36,7 +36,7 @@ const ADDED_HELPERS = {
|
||||||
duration: {
|
duration: {
|
||||||
args: ["time", "durationType"],
|
args: ["time", "durationType"],
|
||||||
numArgs: 2,
|
numArgs: 2,
|
||||||
example: '{{duration timeLeft "seconds"}} -> a few seconds',
|
example: '{{duration 8 "seconds"}} -> a few seconds',
|
||||||
description:
|
description:
|
||||||
"Produce a humanized duration left/until given an amount of time and the type of time measurement.",
|
"Produce a humanized duration left/until given an amount of time and the type of time measurement.",
|
||||||
},
|
},
|
||||||
|
@ -118,6 +118,8 @@ function getCommentInfo(file, func) {
|
||||||
return docs
|
return docs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const excludeFunctions = { string: ["raw"] }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This script is very specific to purpose, parsing the handlebars-helpers files to attempt to get information about them.
|
* This script is very specific to purpose, parsing the handlebars-helpers files to attempt to get information about them.
|
||||||
*/
|
*/
|
||||||
|
@ -136,7 +138,8 @@ function run() {
|
||||||
// skip built in functions and ones seen already
|
// skip built in functions and ones seen already
|
||||||
if (
|
if (
|
||||||
HelperFunctionBuiltin.indexOf(name) !== -1 ||
|
HelperFunctionBuiltin.indexOf(name) !== -1 ||
|
||||||
foundNames.indexOf(name) !== -1
|
foundNames.indexOf(name) !== -1 ||
|
||||||
|
excludeFunctions[collection]?.includes(name)
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,10 +61,10 @@ describe("test the array helpers", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should allow use of the before helper", async () => {
|
it("should allow use of the before helper", async () => {
|
||||||
const output = await processString("{{before array 2}}", {
|
const output = await processString("{{before array 3}}", {
|
||||||
array,
|
array,
|
||||||
})
|
})
|
||||||
expect(output).toBe("hi,person,how")
|
expect(output).toBe("hi,person")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should allow use of the filter helper", async () => {
|
it("should allow use of the filter helper", async () => {
|
||||||
|
|
96
packages/string-templates/test/manifest.spec.js
Normal file
96
packages/string-templates/test/manifest.spec.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
jest.mock("@budibase/handlebars-helpers/lib/math", () => {
|
||||||
|
const actual = jest.requireActual("@budibase/handlebars-helpers/lib/math")
|
||||||
|
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
random: () => 10,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
jest.mock("@budibase/handlebars-helpers/lib/uuid", () => {
|
||||||
|
const actual = jest.requireActual("@budibase/handlebars-helpers/lib/uuid")
|
||||||
|
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
uuid: () => "f34ebc66-93bd-4f7c-b79b-92b5569138bc",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fs = require("fs")
|
||||||
|
const { processString } = require("../src/index.cjs")
|
||||||
|
|
||||||
|
const tk = require("timekeeper")
|
||||||
|
tk.freeze("2021-01-21T12:00:00")
|
||||||
|
|
||||||
|
const manifest = JSON.parse(
|
||||||
|
fs.readFileSync(require.resolve("../manifest.json"), "utf8")
|
||||||
|
)
|
||||||
|
|
||||||
|
const collections = Object.keys(manifest)
|
||||||
|
const examples = collections.reduce((acc, collection) => {
|
||||||
|
const functions = Object.keys(manifest[collection]).filter(
|
||||||
|
fnc => manifest[collection][fnc].example
|
||||||
|
)
|
||||||
|
if (functions.length) {
|
||||||
|
acc[collection] = functions
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
function escapeRegExp(string) {
|
||||||
|
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // $& means the whole matched string
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryParseJson(str) {
|
||||||
|
if (typeof str !== "string") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(str.replace(/\'/g, '"'))
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("manifest", () => {
|
||||||
|
describe("examples are valid", () => {
|
||||||
|
describe.each(Object.keys(examples))("%s", collection => {
|
||||||
|
it.each(examples[collection])("%s", async func => {
|
||||||
|
const example = manifest[collection][func].example
|
||||||
|
|
||||||
|
let [hbs, js] = example.split("->").map(x => x.trim())
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
double: i => i * 2,
|
||||||
|
isString: x => typeof x === "string",
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrays = hbs.match(/\[[^/\]]+\]/)
|
||||||
|
arrays?.forEach((arrayString, i) => {
|
||||||
|
hbs = hbs.replace(new RegExp(escapeRegExp(arrayString)), `array${i}`)
|
||||||
|
context[`array${i}`] = JSON.parse(arrayString.replace(/\'/g, '"'))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (js === undefined) {
|
||||||
|
// The function has no return value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await processString(hbs, context)
|
||||||
|
// Trim 's
|
||||||
|
js = js.replace(/^\'|\'$/g, "")
|
||||||
|
if ((parsedExpected = tryParseJson(js))) {
|
||||||
|
if (Array.isArray(parsedExpected)) {
|
||||||
|
if (typeof parsedExpected[0] === "object") {
|
||||||
|
js = JSON.stringify(parsedExpected)
|
||||||
|
} else {
|
||||||
|
js = parsedExpected.join(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = result.replace(/ /g, " ")
|
||||||
|
expect(result).toEqual(js)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -23,6 +23,7 @@ export interface App extends Document {
|
||||||
automationErrors?: AppMetadataErrors
|
automationErrors?: AppMetadataErrors
|
||||||
icon?: AppIcon
|
icon?: AppIcon
|
||||||
features?: AppFeatures
|
features?: AppFeatures
|
||||||
|
automations?: AutomationSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppInstance {
|
export interface AppInstance {
|
||||||
|
@ -68,3 +69,7 @@ export interface AppFeatures {
|
||||||
componentValidation?: boolean
|
componentValidation?: boolean
|
||||||
disableUserMetadata?: boolean
|
disableUserMetadata?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AutomationSettings {
|
||||||
|
chainAutomations?: boolean
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import { Document } from "../document"
|
import { Document } from "../document"
|
||||||
|
|
||||||
|
export interface QuerySchema {
|
||||||
|
name?: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface Query extends Document {
|
export interface Query extends Document {
|
||||||
datasourceId: string
|
datasourceId: string
|
||||||
name: string
|
name: string
|
||||||
parameters: QueryParameter[]
|
parameters: QueryParameter[]
|
||||||
fields: RestQueryFields | any
|
fields: RestQueryFields | any
|
||||||
transformer: string | null
|
transformer: string | null
|
||||||
schema: Record<string, { name?: string; type: string }>
|
schema: Record<string, QuerySchema | string>
|
||||||
readable: boolean
|
readable: boolean
|
||||||
queryVerb: string
|
queryVerb: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,6 +91,9 @@ export async function getSelf(ctx: any) {
|
||||||
id: userId,
|
id: userId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adjust creators quotas (prevents wrong creators count if user has changed the plan)
|
||||||
|
await groups.adjustGroupCreatorsQuotas()
|
||||||
|
|
||||||
// get the main body of the user
|
// get the main body of the user
|
||||||
const user = await userSdk.db.getUser(userId)
|
const user = await userSdk.db.getUser(userId)
|
||||||
ctx.body = await groups.enrichUserRolesFromGroups(user)
|
ctx.body = await groups.enrichUserRolesFromGroups(user)
|
||||||
|
|
|
@ -55,6 +55,7 @@ const environment = {
|
||||||
CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600,
|
CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600,
|
||||||
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
|
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
|
||||||
ENCRYPTED_TEST_PUBLIC_API_KEY: process.env.ENCRYPTED_TEST_PUBLIC_API_KEY,
|
ENCRYPTED_TEST_PUBLIC_API_KEY: process.env.ENCRYPTED_TEST_PUBLIC_API_KEY,
|
||||||
|
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
|
||||||
/**
|
/**
|
||||||
* Mock the email service in use - links to ethereal hosted emails are logged instead.
|
* Mock the email service in use - links to ethereal hosted emails are logged instead.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
yarn build --scope @budibase/server --scope @budibase/worker
|
yarn build --scope @budibase/server --scope @budibase/worker
|
||||||
version=$(./scripts/getCurrentVersion.sh)
|
version=$(./scripts/getCurrentVersion.sh)
|
||||||
docker build -f hosting/single/Dockerfile -t budibase:latest --build-arg BUDIBASE_VERSION=$version .
|
docker build -f hosting/single/Dockerfile -t budibase:latest --build-arg BUDIBASE_VERSION=$version --build-arg TARGETBUILD=single .
|
||||||
|
|
32
yarn.lock
32
yarn.lock
|
@ -2031,10 +2031,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
"@budibase/handlebars-helpers@^0.12.0":
|
"@budibase/handlebars-helpers@^0.13.0":
|
||||||
version "0.12.0"
|
version "0.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.12.0.tgz#dcc4ba8d796a611474e3495b1142c56b470ca67d"
|
resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.13.0.tgz#224333d14e3900b7dacf48286af1e624a9fd62ea"
|
||||||
integrity sha512-JjGboau7KMdrVSO8gGJzgo1ACSeD4BxN46vidIx9hvdrEXy+v1x2bfQZMaq/c7Dv+V1vyq7c006XwxR1bpfARg==
|
integrity sha512-g8+sFrMNxsIDnK+MmdUICTVGr6ReUFtnPp9hJX0VZwz1pN3Ynolpk/Qbu6rEWAvoU1sEqY1mXr9uo/+kEfeGbQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
get-object "^0.2.0"
|
get-object "^0.2.0"
|
||||||
get-value "^3.0.1"
|
get-value "^3.0.1"
|
||||||
|
@ -5557,9 +5557,9 @@
|
||||||
integrity sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg==
|
integrity sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg==
|
||||||
|
|
||||||
"@types/node@>=8.1.0":
|
"@types/node@>=8.1.0":
|
||||||
version "20.11.2"
|
version "20.11.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.2.tgz#39cea3fe02fbbc2f80ed283e94e1d24f2d3856fb"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.6.tgz#6adf4241460e28be53836529c033a41985f85b6e"
|
||||||
integrity sha512-cZShBaVa+UO1LjWWBPmWRR4+/eY/JR/UIEcDlVsw3okjWEu+rB7/mH6X3B/L+qJVHDLjk9QW/y2upp9wp1yDXA==
|
integrity sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~5.26.4"
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
|
@ -9497,9 +9497,9 @@ dotenv@8.6.0, dotenv@^8.2.0:
|
||||||
integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==
|
integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==
|
||||||
|
|
||||||
dotenv@^16.3.1:
|
dotenv@^16.3.1:
|
||||||
version "16.3.1"
|
version "16.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.0.tgz#ac21c3fcaad2e7832a1cd0c0e4e8e52225ecda0e"
|
||||||
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
|
integrity sha512-WvImr5kpN5NGNn7KaDjJnLTh5rDVLZiDf/YLA8T1ZEZEBZNEDOE+mnkS0PVjPax8ZxBP5zC5SLMB3/9VV5de9g==
|
||||||
|
|
||||||
dotenv@~10.0.0:
|
dotenv@~10.0.0:
|
||||||
version "10.0.0"
|
version "10.0.0"
|
||||||
|
@ -17426,11 +17426,12 @@ postgres-interval@^1.1.0:
|
||||||
xtend "^4.0.0"
|
xtend "^4.0.0"
|
||||||
|
|
||||||
posthog-js@^1.13.4:
|
posthog-js@^1.13.4:
|
||||||
version "1.100.0"
|
version "1.101.0"
|
||||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.100.0.tgz#687b9a6e4ed226aa6572f4040b418ea0c8b3d353"
|
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.101.0.tgz#00e0fc6e164addd52b1738f087996bb0d6685943"
|
||||||
integrity sha512-r2XZEiHQ9mBK7D1G9k57I8uYZ2kZTAJ0OCX6K/OOdCWN8jKPhw3h5F9No5weilP6eVAn+hrsy7NvPV7SCX7gMg==
|
integrity sha512-mzwYSSWr9FdEMDeVpc+diLfc85+10r/LgELGtsW/HaYk+0du/GEql6szpqG8YXMMgb2dE4dnj0JICZFIJd7K3w==
|
||||||
dependencies:
|
dependencies:
|
||||||
fflate "^0.4.1"
|
fflate "^0.4.1"
|
||||||
|
preact "^10.19.3"
|
||||||
|
|
||||||
posthog-js@^1.36.0:
|
posthog-js@^1.36.0:
|
||||||
version "1.96.1"
|
version "1.96.1"
|
||||||
|
@ -17676,6 +17677,11 @@ pprof-format@^2.0.7:
|
||||||
resolved "https://registry.yarnpkg.com/pprof-format/-/pprof-format-2.0.7.tgz#526e4361f8b37d16b2ec4bb0696b5292de5046a4"
|
resolved "https://registry.yarnpkg.com/pprof-format/-/pprof-format-2.0.7.tgz#526e4361f8b37d16b2ec4bb0696b5292de5046a4"
|
||||||
integrity sha512-1qWaGAzwMpaXJP9opRa23nPnt2Egi7RMNoNBptEE/XwHbcn4fC2b/4U4bKc5arkGkIh2ZabpF2bEb+c5GNHEKA==
|
integrity sha512-1qWaGAzwMpaXJP9opRa23nPnt2Egi7RMNoNBptEE/XwHbcn4fC2b/4U4bKc5arkGkIh2ZabpF2bEb+c5GNHEKA==
|
||||||
|
|
||||||
|
preact@^10.19.3:
|
||||||
|
version "10.19.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.3.tgz#7a7107ed2598a60676c943709ea3efb8aaafa899"
|
||||||
|
integrity sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==
|
||||||
|
|
||||||
precinct@^8.1.0:
|
precinct@^8.1.0:
|
||||||
version "8.3.1"
|
version "8.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/precinct/-/precinct-8.3.1.tgz#94b99b623df144eed1ce40e0801c86078466f0dc"
|
resolved "https://registry.yarnpkg.com/precinct/-/precinct-8.3.1.tgz#94b99b623df144eed1ce40e0801c86078466f0dc"
|
||||||
|
|
Loading…
Reference in a new issue