Merge branch 'master' of github.com:Budibase/budibase into feature/sql-query-aliasing
This commit is contained in:
commit
fcf8cf61da
|
@ -33,13 +33,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -50,14 +50,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -80,7 +80,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
@ -92,14 +92,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -116,14 +116,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -140,14 +140,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -165,14 +165,14 @@ jobs:
|
|||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -189,13 +189,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
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')
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
@ -249,7 +249,7 @@ jobs:
|
|||
|
||||
- name: Check submodule merged to base branch
|
||||
if: ${{ steps.get_pro_commits.outputs.base_commit != '' }}
|
||||
uses: actions/github-script@v4
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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')
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
@ -299,7 +299,7 @@ jobs:
|
|||
|
||||
- name: Check submodule merged to base branch
|
||||
if: ${{ steps.get_accountportal_commits.outputs.base_commit != '' }}
|
||||
uses: actions/github-script@v4
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
github.event.label.name == 'feature-branch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }}
|
||||
|
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
contains(github.event.pull_request.labels.*.name, 'feature-branch')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
||||
|
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
run: |
|
||||
echo "Ref is not master, you must run this job from master."
|
||||
exit 1
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
@ -53,7 +53,7 @@ jobs:
|
|||
needs: [tag-release]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.15.5",
|
||||
"version": "2.15.7",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 05c90ce55144e260da6688335c16783eab79bf96
|
||||
Subproject commit 64290ce8957d093bc997190402922df10d092953
|
|
@ -179,6 +179,7 @@ const environment = {
|
|||
...getPackageJsonFields(),
|
||||
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
||||
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
||||
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
|
||||
_set(key: any, value: any) {
|
||||
process.env[key] = value
|
||||
// @ts-ignore
|
||||
|
|
|
@ -2,6 +2,7 @@ export * as configs from "./configs"
|
|||
export * as events from "./events"
|
||||
export * as migrations from "./migrations"
|
||||
export * as users from "./users"
|
||||
export * as userUtils from "./users/utils"
|
||||
export * as roles from "./security/roles"
|
||||
export * as permissions from "./security/permissions"
|
||||
export * as accounts from "./accounts"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
const redis = require("../redis/init")
|
||||
const { v4: uuidv4 } = require("uuid")
|
||||
const { logWarn } = require("../logging")
|
||||
|
||||
import * as redis from "../redis/init"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { logWarn } from "../logging"
|
||||
import env from "../environment"
|
||||
import { Duration } from "../utils"
|
||||
import {
|
||||
Session,
|
||||
ScannedSession,
|
||||
|
@ -10,8 +10,10 @@ import {
|
|||
CreateSession,
|
||||
} from "@budibase/types"
|
||||
|
||||
// a week in seconds
|
||||
const EXPIRY_SECONDS = 86400 * 7
|
||||
// a week expiry is the default
|
||||
const EXPIRY_SECONDS = env.SESSION_EXPIRY_SECONDS
|
||||
? parseInt(env.SESSION_EXPIRY_SECONDS)
|
||||
: Duration.fromDays(7).toSeconds()
|
||||
|
||||
function makeSessionID(userId: string, sessionId: string) {
|
||||
return `${userId}/${sessionId}`
|
||||
|
|
|
@ -251,7 +251,8 @@ export class UserDB {
|
|||
}
|
||||
|
||||
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 () => {
|
||||
await validateUniqueUser(email, tenantId)
|
||||
|
||||
|
@ -335,7 +336,7 @@ export class UserDB {
|
|||
}
|
||||
newUser.userGroups = groups || []
|
||||
newUsers.push(newUser)
|
||||
if (isCreator(newUser)) {
|
||||
if (await isCreator(newUser)) {
|
||||
newCreators.push(newUser)
|
||||
}
|
||||
}
|
||||
|
@ -432,12 +433,16 @@ export class UserDB {
|
|||
_deleted: true,
|
||||
}))
|
||||
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) {
|
||||
await bulkDeleteProcessing(user)
|
||||
}
|
||||
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDelete.length)
|
||||
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount)
|
||||
|
||||
// Build Response
|
||||
// index users by id
|
||||
|
@ -486,7 +491,7 @@ export class UserDB {
|
|||
|
||||
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 eventHelpers.handleDeleteEvents(dbUser)
|
||||
await cache.user.invalidateUser(userId)
|
||||
|
|
|
@ -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
|
||||
async function iterate(startPage?: string) {
|
||||
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) {
|
||||
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 env from "../environment"
|
||||
import { getPlatformUser } from "./lookup"
|
||||
|
@ -6,17 +6,48 @@ import { EmailUnavailableError } from "../errors"
|
|||
import { getTenantId } from "../context"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
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
|
||||
export const isBuilder = sdk.users.isBuilder
|
||||
export const isAdmin = sdk.users.isAdmin
|
||||
export const isCreator = sdk.users.isCreator
|
||||
export const isGlobalBuilder = sdk.users.isGlobalBuilder
|
||||
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
|
||||
export const hasAdminPermissions = sdk.users.hasAdminPermissions
|
||||
export const hasBuilderPermissions = sdk.users.hasBuilderPermissions
|
||||
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) {
|
||||
// check budibase users in other tenants
|
||||
if (env.MULTI_TENANCY) {
|
||||
|
|
|
@ -18,7 +18,6 @@ export default function positionDropdown(element, opts) {
|
|||
useAnchorWidth,
|
||||
offset = 5,
|
||||
customUpdate,
|
||||
offsetBelow,
|
||||
} = opts
|
||||
if (!anchor) {
|
||||
return
|
||||
|
@ -48,7 +47,7 @@ export default function positionDropdown(element, opts) {
|
|||
styles.top = anchorBounds.top - elementBounds.height - offset
|
||||
styles.maxHeight = maxHeight || 240
|
||||
} else {
|
||||
styles.top = anchorBounds.bottom + (offsetBelow || offset)
|
||||
styles.top = anchorBounds.bottom + offset
|
||||
styles.maxHeight =
|
||||
maxHeight || window.innerHeight - anchorBounds.bottom - 20
|
||||
}
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
export let autoWidth = false
|
||||
export let searchTerm = null
|
||||
export let customPopoverHeight
|
||||
export let customPopoverOffsetBelow
|
||||
export let customPopoverMaxHeight
|
||||
export let open = false
|
||||
export let loading
|
||||
|
||||
|
@ -98,7 +96,5 @@
|
|||
{sort}
|
||||
{autoWidth}
|
||||
{customPopoverHeight}
|
||||
{customPopoverOffsetBelow}
|
||||
{customPopoverMaxHeight}
|
||||
{loading}
|
||||
/>
|
||||
|
|
|
@ -37,8 +37,6 @@
|
|||
export let sort = false
|
||||
export let searchTerm = null
|
||||
export let customPopoverHeight
|
||||
export let customPopoverOffsetBelow
|
||||
export let customPopoverMaxHeight
|
||||
export let align = "left"
|
||||
export let footer = null
|
||||
export let customAnchor = null
|
||||
|
@ -156,9 +154,7 @@
|
|||
on:close={() => (open = false)}
|
||||
useAnchorWidth={!autoWidth}
|
||||
maxWidth={autoWidth ? 400 : null}
|
||||
maxHeight={customPopoverMaxHeight}
|
||||
customHeight={customPopoverHeight}
|
||||
offsetBelow={customPopoverOffsetBelow}
|
||||
>
|
||||
<div
|
||||
class="popover-content"
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export let getOptionIcon = () => null
|
||||
export let getOptionColour = () => null
|
||||
export let getOptionSubtitle = () => null
|
||||
export let compare = null
|
||||
export let useOptionIconImage = false
|
||||
export let isOptionEnabled
|
||||
export let readonly = false
|
||||
|
@ -23,8 +24,6 @@
|
|||
export let footer = null
|
||||
export let open = false
|
||||
export let tag = null
|
||||
export let customPopoverOffsetBelow
|
||||
export let customPopoverMaxHeight
|
||||
export let searchTerm = null
|
||||
export let loading
|
||||
|
||||
|
@ -34,13 +33,19 @@
|
|||
$: fieldIcon = getFieldAttribute(getOptionIcon, 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) => {
|
||||
// Wait for options to load if there is a value but no options
|
||||
if (!options?.length) {
|
||||
return ""
|
||||
}
|
||||
const index = options.findIndex(
|
||||
(option, idx) => getOptionValue(option, idx) === value
|
||||
const index = options.findIndex((option, idx) =>
|
||||
compareOptionAndValue(getOptionValue(option, idx), value)
|
||||
)
|
||||
return index !== -1 ? getAttribute(options[index], index) : null
|
||||
}
|
||||
|
@ -90,11 +95,9 @@
|
|||
{autocomplete}
|
||||
{sort}
|
||||
{tag}
|
||||
{customPopoverOffsetBelow}
|
||||
{customPopoverMaxHeight}
|
||||
isPlaceholder={value == null || value === ""}
|
||||
placeholderOption={placeholder === false ? null : placeholder}
|
||||
isOptionSelected={option => option === value}
|
||||
isOptionSelected={option => compareOptionAndValue(option, value)}
|
||||
onSelectOption={selectOption}
|
||||
{loading}
|
||||
/>
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
export let footer = null
|
||||
export let tag = null
|
||||
export let helpText = null
|
||||
export let compare
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
value = e.detail
|
||||
|
@ -65,6 +66,7 @@
|
|||
{autocomplete}
|
||||
{customPopoverHeight}
|
||||
{tag}
|
||||
{compare}
|
||||
on:change={onChange}
|
||||
on:click
|
||||
/>
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
export let useAnchorWidth = false
|
||||
export let dismissible = true
|
||||
export let offset = 5
|
||||
export let offsetBelow
|
||||
export let customHeight
|
||||
export let animate = true
|
||||
export let customZindex
|
||||
|
@ -89,7 +88,6 @@
|
|||
maxWidth,
|
||||
useAnchorWidth,
|
||||
offset,
|
||||
offsetBelow,
|
||||
customUpdate: handlePostionUpdate,
|
||||
}}
|
||||
use:clickOutside={{
|
||||
|
|
|
@ -92,7 +92,14 @@ export const findAllMatchingComponents = (rootComponent, selector) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* Finds the closes parent component which matches certain criteria
|
||||
* Recurses through the component tree and finds all components.
|
||||
*/
|
||||
export const findAllComponents = rootComponent => {
|
||||
return findAllMatchingComponents(rootComponent, () => true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the closest parent component which matches certain criteria
|
||||
*/
|
||||
export const findClosestMatchingComponent = (
|
||||
rootComponent,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import { get } from "svelte/store"
|
||||
import {
|
||||
findAllComponents,
|
||||
findAllMatchingComponents,
|
||||
findComponent,
|
||||
findComponentPath,
|
||||
|
@ -102,6 +103,9 @@ export const getAuthBindings = () => {
|
|||
return bindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all bindings for environment variables
|
||||
*/
|
||||
export const getEnvironmentBindings = () => {
|
||||
let envVars = get(environment).variables
|
||||
return envVars.map(variable => {
|
||||
|
@ -130,26 +134,22 @@ export const toBindingsArray = (valueMap, prefix, category) => {
|
|||
if (!binding) {
|
||||
return acc
|
||||
}
|
||||
|
||||
let config = {
|
||||
type: "context",
|
||||
runtimeBinding: binding,
|
||||
readableBinding: `${prefix}.${binding}`,
|
||||
icon: "Brackets",
|
||||
}
|
||||
|
||||
if (category) {
|
||||
config.category = category
|
||||
}
|
||||
|
||||
acc.push(config)
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility - coverting a map of readable bindings to runtime
|
||||
* Utility to covert a map of readable bindings to runtime
|
||||
*/
|
||||
export const readableToRuntimeMap = (bindings, ctx) => {
|
||||
if (!bindings || !ctx) {
|
||||
|
@ -162,7 +162,7 @@ export const readableToRuntimeMap = (bindings, ctx) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* Utility - coverting a map of runtime bindings to readable
|
||||
* Utility to covert a map of runtime bindings to readable bindings
|
||||
*/
|
||||
export const runtimeToReadableMap = (bindings, ctx) => {
|
||||
if (!bindings || !ctx) {
|
||||
|
@ -188,15 +188,23 @@ export const getComponentBindableProperties = (asset, componentId) => {
|
|||
if (!def?.context) {
|
||||
return []
|
||||
}
|
||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||
|
||||
// Get the bindings for the component
|
||||
return getProviderContextBindings(asset, component)
|
||||
const componentContext = {
|
||||
component,
|
||||
definition: def,
|
||||
contexts,
|
||||
}
|
||||
return generateComponentContextBindings(asset, componentContext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all data provider components above a component.
|
||||
* Gets all component contexts available to a certain component. This handles
|
||||
* both global and local bindings, taking into account a component's position
|
||||
* in the component tree.
|
||||
*/
|
||||
export const getContextProviderComponents = (
|
||||
export const getComponentContexts = (
|
||||
asset,
|
||||
componentId,
|
||||
type,
|
||||
|
@ -205,30 +213,55 @@ export const getContextProviderComponents = (
|
|||
if (!asset || !componentId) {
|
||||
return []
|
||||
}
|
||||
let map = {}
|
||||
|
||||
// Get the component tree leading up to this component, ignoring the component
|
||||
// itself
|
||||
const path = findComponentPath(asset.props, componentId)
|
||||
if (!options?.includeSelf) {
|
||||
path.pop()
|
||||
}
|
||||
|
||||
// Filter by only data provider components
|
||||
return path.filter(component => {
|
||||
// Processes all contexts exposed by a component
|
||||
const processContexts = scope => component => {
|
||||
const def = store.actions.components.getDefinition(component._component)
|
||||
if (!def?.context) {
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
// If no type specified, return anything that exposes context
|
||||
if (!type) {
|
||||
return true
|
||||
if (!map[component._id]) {
|
||||
map[component._id] = {
|
||||
component,
|
||||
definition: def,
|
||||
contexts: [],
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise only match components with the specific context type
|
||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||
return contexts.find(context => context.type === type) != null
|
||||
})
|
||||
contexts.forEach(context => {
|
||||
// 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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -240,20 +273,19 @@ export const getActionProviders = (
|
|||
actionType,
|
||||
options = { includeSelf: false }
|
||||
) => {
|
||||
if (!asset || !componentId) {
|
||||
if (!asset) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get the component tree leading up to this component, ignoring the component
|
||||
// itself
|
||||
const path = findComponentPath(asset.props, componentId)
|
||||
if (!options?.includeSelf) {
|
||||
path.pop()
|
||||
}
|
||||
// Get all components
|
||||
const components = findAllComponents(asset.props)
|
||||
|
||||
// Find matching contexts and generate bindings
|
||||
let providers = []
|
||||
path.forEach(component => {
|
||||
components.forEach(component => {
|
||||
if (!options?.includeSelf && component._id === componentId) {
|
||||
return
|
||||
}
|
||||
const def = store.actions.components.getDefinition(component._component)
|
||||
const actions = (def?.actions || []).map(action => {
|
||||
return typeof action === "string" ? { type: action } : action
|
||||
|
@ -317,142 +349,132 @@ export const getDatasourceForProvider = (asset, component) => {
|
|||
* Gets all bindable data properties from component data contexts.
|
||||
*/
|
||||
const getContextBindings = (asset, componentId) => {
|
||||
// Extract any components which provide data contexts
|
||||
const dataProviders = getContextProviderComponents(asset, componentId)
|
||||
// Get all available contexts for this component
|
||||
const componentContexts = getComponentContexts(asset, componentId)
|
||||
|
||||
// Generate bindings for all matching components
|
||||
return getProviderContextBindings(asset, dataProviders)
|
||||
// Generate bindings for each context
|
||||
return componentContexts
|
||||
.map(componentContext => {
|
||||
return generateComponentContextBindings(asset, componentContext)
|
||||
})
|
||||
.flat()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the context bindings exposed by a set of data provider components.
|
||||
* Generates a set of bindings for a given component context
|
||||
*/
|
||||
const getProviderContextBindings = (asset, dataProviders) => {
|
||||
if (!asset || !dataProviders) {
|
||||
const generateComponentContextBindings = (asset, componentContext) => {
|
||||
console.log("Hello ")
|
||||
const { component, definition, contexts } = componentContext
|
||||
if (!component || !definition || !contexts?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Ensure providers is an array
|
||||
if (!Array.isArray(dataProviders)) {
|
||||
dataProviders = [dataProviders]
|
||||
}
|
||||
|
||||
// Create bindings for each data provider
|
||||
let bindings = []
|
||||
dataProviders.forEach(component => {
|
||||
const def = store.actions.components.getDefinition(component._component)
|
||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||
contexts.forEach(context => {
|
||||
if (!context?.type) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create bindings for each context block provided by this data provider
|
||||
contexts.forEach(context => {
|
||||
if (!context?.type) {
|
||||
let schema
|
||||
let table
|
||||
let readablePrefix
|
||||
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
|
||||
}
|
||||
const info = getSchemaForDatasource(asset, datasource)
|
||||
schema = info.schema
|
||||
table = info.table
|
||||
|
||||
let schema
|
||||
let table
|
||||
let readablePrefix
|
||||
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
|
||||
}
|
||||
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
|
||||
// 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
|
||||
}
|
||||
|
||||
// Create the binding object
|
||||
bindings.push({
|
||||
type: "context",
|
||||
runtimeBinding,
|
||||
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: fieldSchema.name || key,
|
||||
type: fieldSchema.type,
|
||||
},
|
||||
})
|
||||
const keys = Object.keys(schema).sort()
|
||||
|
||||
// Generate safe unique runtime prefix
|
||||
let providerId = component._id
|
||||
if (runtimeSuffix) {
|
||||
providerId += `-${runtimeSuffix}`
|
||||
}
|
||||
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}`
|
||||
|
||||
// Determine which category this binding belongs in
|
||||
const bindingCategory = getComponentBindingCategory(
|
||||
component,
|
||||
context,
|
||||
definition
|
||||
)
|
||||
// Create the binding object
|
||||
bindings.push({
|
||||
type: "context",
|
||||
runtimeBinding,
|
||||
readableBinding: `${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: `${fieldSchema.name || key}`,
|
||||
type: fieldSchema.type,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -460,25 +482,38 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
|||
return bindings
|
||||
}
|
||||
|
||||
// Exclude a data context based on the component settings
|
||||
const filterCategoryByContext = (component, context) => {
|
||||
const { _component } = component
|
||||
/**
|
||||
* Checks if a certain data context is compatible with a certain instance of a
|
||||
* configured 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.actionType === "Create" && context.type === "schema") ||
|
||||
(component.actionType === "View" && context.type === "form")
|
||||
(actionType === "Create" && type === "schema") ||
|
||||
(actionType === "View" && type === "form")
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Allow the context by default
|
||||
return true
|
||||
}
|
||||
|
||||
// Enrich binding category information for certain components
|
||||
const getComponentBindingCategory = (component, context, def) => {
|
||||
// Default category to component name
|
||||
let icon = def.icon
|
||||
let category = component._instanceName
|
||||
|
||||
// Form block edge case
|
||||
if (component._component.endsWith("formblock")) {
|
||||
if (context.type === "form") {
|
||||
category = `${component._instanceName} - Fields`
|
||||
|
@ -496,7 +531,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 = () => {
|
||||
let bindings = []
|
||||
|
@ -566,6 +601,7 @@ const getDeviceBindings = () => {
|
|||
|
||||
/**
|
||||
* 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 => {
|
||||
let bindings = []
|
||||
|
@ -608,6 +644,9 @@ const getSelectedRowsBindings = asset => {
|
|||
return bindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a state binding for a certain key name
|
||||
*/
|
||||
export const makeStateBinding = key => {
|
||||
return {
|
||||
type: "context",
|
||||
|
@ -662,6 +701,9 @@ const getUrlBindings = asset => {
|
|||
return urlParamBindings.concat([queryParamsBinding])
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates all bindings for role IDs
|
||||
*/
|
||||
const getRoleBindings = () => {
|
||||
return (get(rolesStore) || []).map(role => {
|
||||
return {
|
||||
|
@ -1035,11 +1077,48 @@ export const getAllStateVariables = () => {
|
|||
getAllAssets().forEach(asset => {
|
||||
findAllMatchingComponents(asset.props, component => {
|
||||
const settings = getComponentSettings(component._component)
|
||||
settings
|
||||
.filter(setting => setting.type === "event")
|
||||
.forEach(setting => {
|
||||
eventSettings.push(component[setting.key])
|
||||
})
|
||||
|
||||
const parseEventSettings = (settings, comp) => {
|
||||
settings
|
||||
.filter(setting => setting.type === "event")
|
||||
.forEach(setting => {
|
||||
eventSettings.push(comp[setting.key])
|
||||
})
|
||||
}
|
||||
|
||||
const parseComponentSettings = (settings, component) => {
|
||||
// Parse the nested button configurations
|
||||
settings
|
||||
.filter(setting => setting.type === "buttonConfiguration")
|
||||
.forEach(setting => {
|
||||
const buttonConfig = component[setting.key]
|
||||
|
||||
if (Array.isArray(buttonConfig)) {
|
||||
buttonConfig.forEach(button => {
|
||||
const nestedSettings = getComponentSettings(button._component)
|
||||
parseEventSettings(nestedSettings, button)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
parseEventSettings(settings, component)
|
||||
}
|
||||
|
||||
// Parse the base component settings
|
||||
parseComponentSettings(settings, component)
|
||||
|
||||
// Parse step configuration
|
||||
const stepSetting = settings.find(
|
||||
setting => setting.type === "stepConfiguration"
|
||||
)
|
||||
const steps = stepSetting ? component[stepSetting.key] : []
|
||||
const stepDefinition = getComponentSettings(
|
||||
"@budibase/standard-components/multistepformblockstep"
|
||||
)
|
||||
|
||||
steps.forEach(step => {
|
||||
parseComponentSettings(stepDefinition, step)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { findComponent, findComponentPath } from "./componentUtils"
|
|||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { createHistoryStore } from "builderStore/store/history"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { getHoverStore } from "./store/hover"
|
||||
|
||||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
|
@ -16,6 +17,7 @@ export const themeStore = getThemeStore()
|
|||
export const temporalStore = getTemporalStore()
|
||||
export const userStore = getUserStore()
|
||||
export const deploymentStore = getDeploymentStore()
|
||||
export const hoverStore = getHoverStore()
|
||||
|
||||
// Setup history for screens
|
||||
export const screenHistoryStore = createHistoryStore({
|
||||
|
|
|
@ -92,9 +92,6 @@ const INITIAL_FRONTEND_STATE = {
|
|||
// Onboarding
|
||||
onboarding: false,
|
||||
tourNodes: null,
|
||||
|
||||
// UI state
|
||||
hoveredComponentId: null,
|
||||
}
|
||||
|
||||
export const getFrontendStore = () => {
|
||||
|
@ -709,10 +706,9 @@ export const getFrontendStore = () => {
|
|||
else {
|
||||
if (setting.type === "dataProvider") {
|
||||
// Validate data provider exists, or else clear it
|
||||
const treeId = parent?._id || component._id
|
||||
const path = findComponentPath(screen?.props, treeId)
|
||||
const providers = path.filter(component =>
|
||||
component._component?.endsWith("/dataprovider")
|
||||
const providers = findAllMatchingComponents(
|
||||
screen?.props,
|
||||
component => component._component?.endsWith("/dataprovider")
|
||||
)
|
||||
// Validate non-empty values
|
||||
const valid = providers?.some(dp => value.includes?.(dp._id))
|
||||
|
@ -734,6 +730,16 @@ export const getFrontendStore = () => {
|
|||
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
|
||||
let instance = {
|
||||
_id: Helpers.uuid(),
|
||||
|
@ -743,7 +749,7 @@ export const getFrontendStore = () => {
|
|||
hover: {},
|
||||
active: {},
|
||||
},
|
||||
_instanceName: `New ${definition.friendlyName || definition.name}`,
|
||||
_instanceName: name,
|
||||
...presetProps,
|
||||
}
|
||||
|
||||
|
@ -1415,18 +1421,6 @@ export const getFrontendStore = () => {
|
|||
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: {
|
||||
save: async (url, title) => {
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
}
|
|
@ -88,8 +88,12 @@
|
|||
hasValidated = false
|
||||
})
|
||||
}
|
||||
|
||||
$: valid =
|
||||
getErrorCount(errors) === 0 && allRequiredAttributesSet(relationshipType)
|
||||
getErrorCount(errors) === 0 &&
|
||||
allRequiredAttributesSet(relationshipType) &&
|
||||
fromId &&
|
||||
toId
|
||||
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
|
||||
$: isManyToOne =
|
||||
relationshipType === RelationshipType.MANY_TO_ONE ||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { getContextProviderComponents } from "builderStore/dataBinding"
|
||||
import { store } from "builderStore"
|
||||
import { getComponentContexts } from "builderStore/dataBinding"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
// Generates bindings for all components that provider "datasource like"
|
||||
|
@ -8,58 +7,49 @@ import { capitalise } from "helpers"
|
|||
// Some examples are saving rows or duplicating rows.
|
||||
export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
||||
// Get all form context providers
|
||||
const formComponents = getContextProviderComponents(
|
||||
const formComponentContexts = getComponentContexts(
|
||||
asset,
|
||||
componentId,
|
||||
"form",
|
||||
{ includeSelf: nested }
|
||||
{
|
||||
includeSelf: nested,
|
||||
}
|
||||
)
|
||||
|
||||
// Get all schema context providers
|
||||
const schemaComponents = getContextProviderComponents(
|
||||
const schemaComponentContexts = getComponentContexts(
|
||||
asset,
|
||||
componentId,
|
||||
"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
|
||||
// to label contexts with their suffixes
|
||||
schemaContexts.forEach(schemaContext => {
|
||||
schemaComponentContexts.forEach(schemaContext => {
|
||||
// Check if we have a form context for this component
|
||||
const id = schemaContext.component._id
|
||||
const existing = formContexts.find(x => x.component._id === id)
|
||||
const existing = formComponentContexts.find(x => x.component._id === id)
|
||||
if (existing) {
|
||||
if (existing.context.suffix) {
|
||||
const suffix = capitalise(existing.context.suffix)
|
||||
if (existing.contexts[0].suffix) {
|
||||
const suffix = capitalise(existing.contexts[0].suffix)
|
||||
existing.readableSuffix = ` - ${suffix}`
|
||||
}
|
||||
if (schemaContext.context.suffix) {
|
||||
const suffix = capitalise(schemaContext.context.suffix)
|
||||
if (schemaContext.contexts[0].suffix) {
|
||||
const suffix = capitalise(schemaContext.contexts[0].suffix)
|
||||
schemaContext.readableSuffix = ` - ${suffix}`
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Generate bindings for all contexts
|
||||
const allContexts = formContexts.concat(schemaContexts)
|
||||
return allContexts.map(({ component, context, readableSuffix }) => {
|
||||
const allContexts = formComponentContexts.concat(schemaComponentContexts)
|
||||
return allContexts.map(({ component, contexts, readableSuffix }) => {
|
||||
let readableBinding = component._instanceName
|
||||
let runtimeBinding = component._id
|
||||
if (context.suffix) {
|
||||
runtimeBinding += `-${context.suffix}`
|
||||
if (contexts[0].suffix) {
|
||||
runtimeBinding += `-${contexts[0].suffix}`
|
||||
}
|
||||
if (readableSuffix) {
|
||||
readableBinding += readableSuffix
|
||||
|
@ -70,13 +60,3 @@ 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 { Helpers } from "@budibase/bbui"
|
||||
import { getEventContextBindings } from "builderStore/dataBinding"
|
||||
import { cloneDeep, isEqual } from "lodash/fp"
|
||||
|
||||
export let componentInstance
|
||||
export let componentBindings
|
||||
|
@ -17,8 +18,13 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
|
||||
let focusItem
|
||||
let cachedValue
|
||||
|
||||
$: buttonList = sanitizeValue(value) || []
|
||||
$: if (!isEqual(value, cachedValue)) {
|
||||
cachedValue = cloneDeep(value)
|
||||
}
|
||||
|
||||
$: buttonList = sanitizeValue(cachedValue) || []
|
||||
$: buttonCount = buttonList.length
|
||||
$: eventContextBindings = getEventContextBindings({
|
||||
componentInstance,
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { makePropSafe } from "@budibase/string-templates"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
import { findComponentPath } from "builderStore/componentUtils"
|
||||
import { currentAsset } from "builderStore"
|
||||
import { findAllMatchingComponents } from "builderStore/componentUtils"
|
||||
|
||||
export let value
|
||||
|
||||
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
||||
|
||||
$: path = findComponentPath($currentAsset?.props, $store.selectedComponentId)
|
||||
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
|
||||
$: providers = findAllMatchingComponents($currentAsset?.props, c =>
|
||||
c._component?.endsWith("/dataprovider")
|
||||
)
|
||||
</script>
|
||||
|
||||
<Select
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import {
|
||||
getContextProviderComponents,
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
|
@ -30,6 +29,7 @@
|
|||
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import { findAllComponents } from "builderStore/componentUtils"
|
||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||
import DataSourceCategory from "components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
|
||||
import { API } from "api"
|
||||
|
@ -75,12 +75,13 @@
|
|||
...query,
|
||||
type: "query",
|
||||
}))
|
||||
$: contextProviders = getContextProviderComponents(
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: dataProviders = contextProviders
|
||||
.filter(component => component._component?.endsWith("/dataprovider"))
|
||||
$: dataProviders = findAllComponents($currentAsset.props)
|
||||
.filter(component => {
|
||||
return (
|
||||
component._component?.endsWith("/dataprovider") &&
|
||||
component._id !== $store.selectedComponentId
|
||||
)
|
||||
})
|
||||
.map(provider => ({
|
||||
label: provider._instanceName,
|
||||
name: provider._instanceName,
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
export let bindingDrawerLeft
|
||||
export let allowHelpers = true
|
||||
export let customButtonText = null
|
||||
export let compare = (option, value) => option === value
|
||||
|
||||
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
||||
name,
|
||||
|
@ -112,7 +113,12 @@
|
|||
on:blur={changed}
|
||||
/>
|
||||
{#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}
|
||||
<DrawerBindableInput
|
||||
{bindings}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import KeyValueBuilder from "../KeyValueBuilder.svelte"
|
||||
import { SchemaTypeOptions } from "constants/backend"
|
||||
import { SchemaTypeOptionsExpanded } from "constants/backend"
|
||||
|
||||
export let schema
|
||||
export let onSchemaChange = () => {}
|
||||
|
@ -24,6 +24,7 @@
|
|||
object={schema}
|
||||
name="field"
|
||||
headings
|
||||
options={SchemaTypeOptions}
|
||||
options={SchemaTypeOptionsExpanded}
|
||||
compare={(option, value) => option.type === value.type}
|
||||
/>
|
||||
{/key}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
PaginationTypes,
|
||||
RawRestBodyTypes,
|
||||
RestBodyTypes as bodyTypes,
|
||||
SchemaTypeOptions,
|
||||
SchemaTypeOptionsExpanded,
|
||||
} from "constants/backend"
|
||||
import JSONPreview from "components/integration/JSONPreview.svelte"
|
||||
import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte"
|
||||
|
@ -97,9 +97,7 @@
|
|||
$: schemaReadOnly = !responseSuccess
|
||||
$: variablesReadOnly = !responseSuccess
|
||||
$: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly)
|
||||
$: hasSchema =
|
||||
Object.keys(schema || {}).length !== 0 ||
|
||||
Object.keys(query?.schema || {}).length !== 0
|
||||
$: hasSchema = Object.keys(schema || {}).length !== 0
|
||||
|
||||
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
|
||||
|
||||
|
@ -161,7 +159,7 @@
|
|||
newQuery.fields.queryString = queryString
|
||||
newQuery.fields.authConfigId = authConfigId
|
||||
newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders)
|
||||
newQuery.schema = restUtils.fieldsToSchema(schema)
|
||||
newQuery.schema = schema
|
||||
|
||||
return newQuery
|
||||
}
|
||||
|
@ -231,6 +229,14 @@
|
|||
notifications.info("Request did not return any data")
|
||||
} else {
|
||||
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
|
||||
notifications.success("Request sent successfully")
|
||||
}
|
||||
|
@ -386,6 +392,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
query = getSelectedQuery()
|
||||
schema = query.schema
|
||||
|
||||
try {
|
||||
// Clear any unsaved changes to the datasource
|
||||
|
@ -416,7 +423,6 @@
|
|||
query.fields.path = `${datasource.config.url}/${path ? path : ""}`
|
||||
}
|
||||
url = buildUrl(query.fields.path, breakQs)
|
||||
schema = restUtils.schemaToFields(query.schema)
|
||||
requestBindings = restUtils.queryParametersToKeyValue(query.parameters)
|
||||
authConfigId = getAuthConfigId()
|
||||
if (!query.fields.disabledHeaders) {
|
||||
|
@ -682,10 +688,11 @@
|
|||
bind:object={schema}
|
||||
name="schema"
|
||||
headings
|
||||
options={SchemaTypeOptions}
|
||||
options={SchemaTypeOptionsExpanded}
|
||||
menuItems={schemaMenuItems}
|
||||
showMenu={!schemaReadOnly}
|
||||
readOnly={schemaReadOnly}
|
||||
compare={(option, value) => option.type === value.type}
|
||||
/>
|
||||
</Tab>
|
||||
{/if}
|
||||
|
|
|
@ -271,6 +271,11 @@ export const SchemaTypeOptions = [
|
|||
{ label: "Datetime", value: "datetime" },
|
||||
]
|
||||
|
||||
export const SchemaTypeOptionsExpanded = SchemaTypeOptions.map(el => ({
|
||||
...el,
|
||||
value: { type: el.value },
|
||||
}))
|
||||
|
||||
export const RawRestBodyTypes = {
|
||||
NONE: "none",
|
||||
FORM: "form",
|
||||
|
|
|
@ -1,26 +1,6 @@
|
|||
import { IntegrationTypes } from "constants/backend"
|
||||
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) {
|
||||
if (!qs) {
|
||||
return {}
|
||||
|
@ -184,10 +164,8 @@ export const parseToCsv = (headers, rows) => {
|
|||
export default {
|
||||
breakQueryString,
|
||||
buildQueryString,
|
||||
fieldsToSchema,
|
||||
flipHeaderState,
|
||||
keyValueToQueryParameters,
|
||||
parseToCsv,
|
||||
queryParametersToKeyValue,
|
||||
schemaToFields,
|
||||
}
|
||||
|
|
|
@ -392,6 +392,10 @@
|
|||
}
|
||||
|
||||
const openInviteFlow = () => {
|
||||
// prevent email from getting overwritten if changes are made
|
||||
if (!email) {
|
||||
email = query
|
||||
}
|
||||
$licensing.userLimitReached
|
||||
? userLimitReachedModal.show()
|
||||
: (invitingFlow = true)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { get } from "svelte/store"
|
||||
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 {
|
||||
ProgressCircle,
|
||||
|
@ -118,7 +118,7 @@
|
|||
} else if (type === "select-component" && data.id) {
|
||||
$store.selectedComponentId = data.id
|
||||
} else if (type === "hover-component") {
|
||||
store.actions.components.hover(data.id, false)
|
||||
hoverStore.actions.update(data.id, false)
|
||||
} else if (type === "update-prop") {
|
||||
await store.actions.components.updateSetting(data.prop, data.value)
|
||||
} else if (type === "update-styles") {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
selectedComponentPath,
|
||||
selectedComponent,
|
||||
selectedScreen,
|
||||
hoverStore,
|
||||
} from "builderStore"
|
||||
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
|
@ -90,7 +91,7 @@
|
|||
return findComponentPath($selectedComponent, component._id)?.length > 0
|
||||
}
|
||||
|
||||
const hover = store.actions.components.hover
|
||||
const hover = hoverStore.actions.update
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
|
@ -111,7 +112,7 @@
|
|||
on:dragover={dragover(component, index)}
|
||||
on:iconClick={() => toggleNodeOpen(component._id)}
|
||||
on:drop={onDrop}
|
||||
hovering={$store.hoveredComponentId === component._id}
|
||||
hovering={$hoverStore.componentId === component._id}
|
||||
on:mouseenter={() => hover(component._id)}
|
||||
on:mouseleave={() => hover(null)}
|
||||
text={getComponentText(component)}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
<script>
|
||||
import { notifications, Icon, Body } from "@budibase/bbui"
|
||||
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 ComponentTree from "./ComponentTree.svelte"
|
||||
import { dndStore, DropPosition } from "./dndStore.js"
|
||||
|
@ -36,7 +41,7 @@
|
|||
scrolling = e.target.scrollTop !== 0
|
||||
}
|
||||
|
||||
const hover = store.actions.components.hover
|
||||
const hover = hoverStore.actions.update
|
||||
</script>
|
||||
|
||||
<div class="components">
|
||||
|
@ -60,7 +65,7 @@
|
|||
icon="WebPage"
|
||||
on:drop={onDrop}
|
||||
on:click={() => ($store.selectedComponentId = screenComponentId)}
|
||||
hovering={$store.hoveredComponentId === screenComponentId}
|
||||
hovering={$hoverStore.componentId === screenComponentId}
|
||||
on:mouseenter={() => hover(screenComponentId)}
|
||||
on:mouseleave={() => hover(null)}
|
||||
id="component-screen"
|
||||
|
@ -79,7 +84,7 @@
|
|||
: "VisibilityOff"}
|
||||
on:drop={onDrop}
|
||||
on:click={() => ($store.selectedComponentId = navComponentId)}
|
||||
hovering={$store.hoveredComponentId === navComponentId}
|
||||
hovering={$hoverStore.componentId === navComponentId}
|
||||
on:mouseenter={() => hover(navComponentId)}
|
||||
on:mouseleave={() => hover(null)}
|
||||
id="component-nav"
|
||||
|
|
|
@ -89,8 +89,8 @@ export function createQueriesStore() {
|
|||
// Assume all the fields are strings and create a basic schema from the
|
||||
// unique fields returned by the server
|
||||
const schema = {}
|
||||
for (let [field, type] of Object.entries(result.schemaFields)) {
|
||||
schema[field] = type || "string"
|
||||
for (let [field, metadata] of Object.entries(result.schema)) {
|
||||
schema[field] = metadata || { type: "string" }
|
||||
}
|
||||
return { ...result, schema, rows: result.rows || [] }
|
||||
}
|
||||
|
|
|
@ -573,7 +573,6 @@
|
|||
"description": "A configurable data list that attaches to your backend tables.",
|
||||
"icon": "JourneyData",
|
||||
"illegalChildren": ["section"],
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"hasChildren": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
|
@ -711,10 +710,12 @@
|
|||
],
|
||||
"context": [
|
||||
{
|
||||
"type": "schema"
|
||||
"type": "schema",
|
||||
"scope": "local"
|
||||
},
|
||||
{
|
||||
"type": "static",
|
||||
"scope": "local",
|
||||
"values": [
|
||||
{
|
||||
"label": "Row index",
|
||||
|
@ -1564,7 +1565,6 @@
|
|||
"name": "Bar Chart",
|
||||
"description": "Bar chart",
|
||||
"icon": "GraphBarVertical",
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
|
@ -1727,7 +1727,6 @@
|
|||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -1881,7 +1880,6 @@
|
|||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -2047,7 +2045,6 @@
|
|||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -2177,7 +2174,6 @@
|
|||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -2307,7 +2303,6 @@
|
|||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -3969,6 +3964,12 @@
|
|||
"key": "allowManualEntry",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Auto confirm",
|
||||
"key": "autoConfirm",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Play sound on scan",
|
||||
|
@ -4081,7 +4082,6 @@
|
|||
"width": 400,
|
||||
"height": 320
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
|
@ -4637,7 +4637,6 @@
|
|||
"name": "Table",
|
||||
"icon": "Table",
|
||||
"illegalChildren": ["section"],
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"hasChildren": true,
|
||||
"showEmptyState": false,
|
||||
"size": {
|
||||
|
@ -4728,7 +4727,6 @@
|
|||
"name": "Date Range",
|
||||
"icon": "Calendar",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"hasChildren": false,
|
||||
"size": {
|
||||
"width": 200,
|
||||
|
@ -4836,7 +4834,6 @@
|
|||
"width": 100,
|
||||
"height": 35
|
||||
},
|
||||
"requiredAncestors": ["dataprovider"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
|
@ -5611,7 +5608,38 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"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": {
|
||||
"block": true,
|
||||
|
@ -5790,7 +5818,8 @@
|
|||
],
|
||||
"context": {
|
||||
"type": "schema",
|
||||
"suffix": "repeater"
|
||||
"suffix": "repeater",
|
||||
"scope": "local"
|
||||
}
|
||||
},
|
||||
"repeaterblock": {
|
||||
|
@ -6014,7 +6043,8 @@
|
|||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"suffix": "repeater"
|
||||
"suffix": "repeater",
|
||||
"scope": "local"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -6160,6 +6190,10 @@
|
|||
"type": "form",
|
||||
"suffix": "form"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"suffix": "repeater"
|
||||
},
|
||||
{
|
||||
"type": "static",
|
||||
"suffix": "form",
|
||||
|
@ -6473,9 +6507,27 @@
|
|||
],
|
||||
"context": {
|
||||
"type": "schema",
|
||||
"suffix": "repeater"
|
||||
"suffix": "repeater",
|
||||
"scope": "local"
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
"name": "Grid",
|
||||
"icon": "ViewGrid",
|
||||
"hasChildren": true,
|
||||
"settings": [
|
||||
{
|
||||
"type": "number",
|
||||
"key": "cols",
|
||||
"label": "Columns"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "rows",
|
||||
"label": "Rows"
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridblock": {
|
||||
"name": "Grid Block",
|
||||
"icon": "Table",
|
||||
|
@ -6619,7 +6671,8 @@
|
|||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "schema"
|
||||
"type": "schema",
|
||||
"scope": "local"
|
||||
},
|
||||
"actions": ["RefreshDatasource"]
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</script>
|
||||
|
||||
<script>
|
||||
import { getContext, setContext, onMount, onDestroy } from "svelte"
|
||||
import { getContext, setContext, onMount } from "svelte"
|
||||
import { writable, get } from "svelte/store"
|
||||
import {
|
||||
enrichProps,
|
||||
|
@ -30,6 +30,15 @@
|
|||
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
||||
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte"
|
||||
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 isLayout = false
|
||||
|
@ -81,7 +90,6 @@
|
|||
|
||||
// Keep track of stringified representations of context and instance
|
||||
// to avoid enriching bindings as much as possible
|
||||
let lastContextKey
|
||||
let lastInstanceKey
|
||||
|
||||
// Visibility flag used by conditional UI
|
||||
|
@ -98,6 +106,13 @@
|
|||
// We clear these whenever a new instance is received.
|
||||
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
|
||||
$: initialise(instance)
|
||||
|
||||
|
@ -155,9 +170,6 @@
|
|||
hasMissingRequiredSettings)
|
||||
$: emptyState = empty && showEmptyState
|
||||
|
||||
// Enrich component settings
|
||||
$: enrichComponentSettings($context, settingsDefinitionMap)
|
||||
|
||||
// Evaluate conditional UI settings and store any component setting changes
|
||||
// which need to be made
|
||||
$: evaluateConditions(conditions)
|
||||
|
@ -206,6 +218,7 @@
|
|||
errorState,
|
||||
parent: id,
|
||||
ancestors: [...($component?.ancestors ?? []), instance._component],
|
||||
path: [...($component?.path ?? []), id],
|
||||
})
|
||||
|
||||
const initialise = (instance, force = false) => {
|
||||
|
@ -214,7 +227,8 @@
|
|||
}
|
||||
|
||||
// Ensure we're processing a new instance
|
||||
const instanceKey = Helpers.hashString(JSON.stringify(instance))
|
||||
const stringifiedInstance = JSON.stringify(instance)
|
||||
const instanceKey = Helpers.hashString(stringifiedInstance)
|
||||
if (instanceKey === lastInstanceKey && !force) {
|
||||
return
|
||||
} else {
|
||||
|
@ -274,13 +288,54 @@
|
|||
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
|
||||
runMigrations(instance, settingsDefinition)
|
||||
|
||||
// Force an initial enrichment of the new settings
|
||||
enrichComponentSettings(get(context), settingsDefinitionMap, {
|
||||
force: true,
|
||||
enrichComponentSettings(get(context), settingsDefinitionMap)
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
|
@ -381,17 +436,7 @@
|
|||
}
|
||||
|
||||
// Enriches any string component props using handlebars
|
||||
const enrichComponentSettings = (
|
||||
context,
|
||||
settingsDefinitionMap,
|
||||
options = { force: false }
|
||||
) => {
|
||||
const contextChanged = context.key !== lastContextKey
|
||||
if (!contextChanged && !options?.force) {
|
||||
return
|
||||
}
|
||||
lastContextKey = context.key
|
||||
|
||||
const enrichComponentSettings = (context, settingsDefinitionMap) => {
|
||||
// Record the timestamp so we can reference it after enrichment
|
||||
latestUpdateTime = Date.now()
|
||||
const enrichmentTime = latestUpdateTime
|
||||
|
@ -506,31 +551,46 @@
|
|||
})
|
||||
}
|
||||
|
||||
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(() => {
|
||||
if (
|
||||
$appStore.isDevApp &&
|
||||
!componentStore.actions.isComponentRegistered(id)
|
||||
) {
|
||||
componentStore.actions.registerInstance(id, {
|
||||
component: instance._component,
|
||||
getSettings: () => cachedSettings,
|
||||
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
|
||||
getDataContext: () => get(context),
|
||||
reload: () => initialise(instance, true),
|
||||
setEphemeralStyles: styles => (ephemeralStyles = styles),
|
||||
state: store,
|
||||
})
|
||||
if ($appStore.isDevApp) {
|
||||
if (!componentStore.actions.isComponentRegistered(id)) {
|
||||
componentStore.actions.registerInstance(id, {
|
||||
component: instance._component,
|
||||
getSettings: () => cachedSettings,
|
||||
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
|
||||
getDataContext: () => get(context),
|
||||
reload: () => initialise(instance, true),
|
||||
setEphemeralStyles: styles => (ephemeralStyles = styles),
|
||||
state: store,
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
if (componentStore.actions.isComponentRegistered(id)) {
|
||||
componentStore.actions.unregisterInstance(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (
|
||||
$appStore.isDevApp &&
|
||||
componentStore.actions.isComponentRegistered(id)
|
||||
) {
|
||||
componentStore.actions.unregisterInstance(id)
|
||||
}
|
||||
})
|
||||
// Observe changes to context
|
||||
onMount(() => context.actions.observeChanges(handleContextChange))
|
||||
</script>
|
||||
|
||||
{#if constructor && initialSettings && (visible || inSelectedPath) && !builderHidden}
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
datasource: dataSource || {},
|
||||
schema,
|
||||
rowsLength: $fetch.rows.length,
|
||||
|
||||
pageNumber: $fetch.pageNumber + 1,
|
||||
// Undocumented properties. These aren't supposed to be used in builder
|
||||
// bindings, but are used internally by other components
|
||||
id: $component?.id,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
|
||||
const component = getContext("component")
|
||||
const { builderStore, componentStore } = getContext("sdk")
|
||||
|
@ -10,15 +9,7 @@
|
|||
|
||||
{#if $builderStore.inBuilder}
|
||||
<div class="component-placeholder">
|
||||
<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>
|
||||
{$component.name || definition?.name || "Component"}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -32,14 +23,4 @@
|
|||
font-size: var(--font-size-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>
|
||||
|
|
|
@ -19,7 +19,36 @@
|
|||
export let onRowClick = null
|
||||
export let buttons = null
|
||||
|
||||
// parses columns to fix older formats
|
||||
const context = getContext("context")
|
||||
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 => {
|
||||
// If the first element has an active key all elements should be in the new format
|
||||
if (columns?.length && columns[0]?.active !== undefined) {
|
||||
|
@ -33,28 +62,6 @@
|
|||
}))
|
||||
}
|
||||
|
||||
$: 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 => {
|
||||
let overrides = {}
|
||||
columns?.forEach(column => {
|
||||
|
@ -78,11 +85,6 @@
|
|||
const id = get(component).id
|
||||
const gridContext = createContextStore(context)
|
||||
gridContext.actions.provideData(id, row)
|
||||
gridContext.actions.provideAction(
|
||||
id,
|
||||
ActionTypes.RefreshDatasource,
|
||||
() => grid?.getContext()?.rows.actions.refreshData()
|
||||
)
|
||||
const fn = enrichButtonActions(settings.onClick, get(gridContext))
|
||||
return await fn?.({ row })
|
||||
},
|
||||
|
@ -94,29 +96,31 @@
|
|||
use:styleable={$component.styles}
|
||||
class:in-builder={$builderStore.inBuilder}
|
||||
>
|
||||
<Grid
|
||||
bind:this={grid}
|
||||
datasource={table}
|
||||
{API}
|
||||
{stripeRows}
|
||||
{initialFilter}
|
||||
{initialSortColumn}
|
||||
{initialSortOrder}
|
||||
{fixedRowHeight}
|
||||
{columnWhitelist}
|
||||
{schemaOverrides}
|
||||
canAddRows={allowAddRows}
|
||||
canEditRows={allowEditRows}
|
||||
canDeleteRows={allowDeleteRows}
|
||||
canEditColumns={false}
|
||||
canExpandRows={false}
|
||||
canSaveSchema={false}
|
||||
showControls={false}
|
||||
notifySuccess={notificationStore.actions.success}
|
||||
notifyError={notificationStore.actions.error}
|
||||
buttons={enrichedButtons}
|
||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||
/>
|
||||
<Provider {actions}>
|
||||
<Grid
|
||||
bind:this={grid}
|
||||
datasource={table}
|
||||
{API}
|
||||
{stripeRows}
|
||||
{initialFilter}
|
||||
{initialSortColumn}
|
||||
{initialSortOrder}
|
||||
{fixedRowHeight}
|
||||
{columnWhitelist}
|
||||
{schemaOverrides}
|
||||
canAddRows={allowAddRows}
|
||||
canEditRows={allowEditRows}
|
||||
canDeleteRows={allowDeleteRows}
|
||||
canEditColumns={false}
|
||||
canExpandRows={false}
|
||||
canSaveSchema={false}
|
||||
showControls={false}
|
||||
notifySuccess={notificationStore.actions.success}
|
||||
notifyError={notificationStore.actions.error}
|
||||
buttons={enrichedButtons}
|
||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||
/>
|
||||
</Provider>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import Placeholder from "./Placeholder.svelte"
|
||||
import Container from "./Container.svelte"
|
||||
import { ContextScopes } from "constants"
|
||||
|
||||
export let dataProvider
|
||||
export let noRowsMessage
|
||||
|
@ -9,6 +10,7 @@
|
|||
export let hAlign
|
||||
export let vAlign
|
||||
export let gap
|
||||
export let scope = ContextScopes.Local
|
||||
|
||||
const { Provider } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
@ -22,7 +24,7 @@
|
|||
<Placeholder />
|
||||
{:else if rows.length > 0}
|
||||
{#each rows as row, index}
|
||||
<Provider data={{ ...row, index }}>
|
||||
<Provider data={{ ...row, index }} {scope}>
|
||||
<slot />
|
||||
</Provider>
|
||||
{/each}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import BlockComponent from "components/BlockComponent.svelte"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { getContext, setContext } from "svelte"
|
||||
import { builderStore } from "stores"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
|
@ -41,7 +42,7 @@
|
|||
let schema
|
||||
|
||||
$: fetchSchema(dataSource)
|
||||
$: enrichedSteps = enrichSteps(steps, schema, $component.id)
|
||||
$: enrichedSteps = enrichSteps(steps, schema, $component.id, $currentStep)
|
||||
$: updateCurrentStep(enrichedSteps, $builderStore, $component)
|
||||
|
||||
const updateCurrentStep = (steps, builderStore, component) => {
|
||||
|
@ -115,6 +116,7 @@
|
|||
dataSource,
|
||||
})
|
||||
return {
|
||||
_stepId: Helpers.uuid(),
|
||||
fields: getDefaultFields(fields || [], schema),
|
||||
title: title ?? defaultProps.title,
|
||||
desc,
|
||||
|
@ -142,7 +144,7 @@
|
|||
},
|
||||
}}
|
||||
>
|
||||
{#each enrichedSteps as step, stepIdx}
|
||||
{#each enrichedSteps as step, stepIdx (step._stepId)}
|
||||
<BlockComponent
|
||||
type="formstep"
|
||||
props={{ step: stepIdx + 1, _instanceName: `Step ${stepIdx + 1}` }}
|
||||
|
@ -186,12 +188,13 @@
|
|||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
<BlockComponent type="text" props={{ text: step.desc }} order={1} />
|
||||
|
||||
<BlockComponent type="container" order={2}>
|
||||
<div
|
||||
class="form-block fields"
|
||||
class:mobile={$context.device.mobile}
|
||||
>
|
||||
{#each step.fields as field, fieldIdx (`${field.field || field.name}_${stepIdx}_${fieldIdx}`)}
|
||||
{#each step.fields as field, fieldIdx (`${field.field || field.name}_${fieldIdx}`)}
|
||||
{#if getComponentForField(field)}
|
||||
<BlockComponent
|
||||
type={getComponentForField(field)}
|
||||
|
|
|
@ -231,6 +231,7 @@
|
|||
paginate,
|
||||
limit: rowCount,
|
||||
}}
|
||||
context="provider"
|
||||
order={1}
|
||||
>
|
||||
<BlockComponent
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
export let noRowsMessage
|
||||
|
||||
const component = getContext("component")
|
||||
const { ContextScopes } = getContext("sdk")
|
||||
|
||||
$: providerId = `${$component.id}-provider`
|
||||
$: dataProvider = `{{ literal ${safe(providerId)} }}`
|
||||
|
@ -55,6 +56,7 @@
|
|||
noRowsMessage: noRowsMessage || "We couldn't find a row to display",
|
||||
direction: "column",
|
||||
hAlign: "center",
|
||||
scope: ContextScopes.Global,
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
|
|
|
@ -14,11 +14,13 @@
|
|||
export let value
|
||||
export let disabled = false
|
||||
export let allowManualEntry = false
|
||||
export let autoConfirm = false
|
||||
export let scanButtonText = "Scan code"
|
||||
export let beepOnScan = false
|
||||
export let beepFrequency = 2637
|
||||
export let customFrequency = 1046
|
||||
export let preferredCamera = "environment"
|
||||
export let validator
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -41,6 +43,9 @@
|
|||
beep()
|
||||
}
|
||||
dispatch("change", decodedText)
|
||||
if (autoConfirm && !validator?.(decodedText)) {
|
||||
camModal?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,7 +132,11 @@
|
|||
<div class="scanner-video-wrapper">
|
||||
{#if value && !manualMode}
|
||||
<div class="scanner-value field-display">
|
||||
<StatusLight positive />
|
||||
{#if validator?.(value)}
|
||||
<StatusLight negative />
|
||||
{:else}
|
||||
<StatusLight positive />
|
||||
{/if}
|
||||
{value}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -183,11 +192,16 @@
|
|||
</div>
|
||||
{#if cameraEnabled === true}
|
||||
<div class="code-wrap">
|
||||
{#if value}
|
||||
{#if value && !validator?.(value)}
|
||||
<div class="scanner-value">
|
||||
<StatusLight positive />
|
||||
{value}
|
||||
</div>
|
||||
{:else if value && validator?.(value)}
|
||||
<div class="scanner-value">
|
||||
<StatusLight negative />
|
||||
{value}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="scanner-value">
|
||||
<StatusLight neutral />
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
export let defaultValue = ""
|
||||
export let onChange
|
||||
export let allowManualEntry
|
||||
export let autoConfirm
|
||||
export let scanButtonText
|
||||
export let beepOnScan
|
||||
export let beepFrequency
|
||||
|
@ -49,11 +50,13 @@
|
|||
on:change={handleUpdate}
|
||||
disabled={fieldState.disabled || fieldState.readonly}
|
||||
{allowManualEntry}
|
||||
{autoConfirm}
|
||||
scanButtonText={scanText}
|
||||
{beepOnScan}
|
||||
{beepFrequency}
|
||||
{customFrequency}
|
||||
{preferredCamera}
|
||||
validator={fieldState.validator}
|
||||
/>
|
||||
{/if}
|
||||
</Field>
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
export let editAutoColumns = false
|
||||
|
||||
const context = getContext("context")
|
||||
const component = getContext("component")
|
||||
const { API, fetchDatasourceSchema } = getContext("sdk")
|
||||
|
||||
const getInitialFormStep = () => {
|
||||
|
@ -38,28 +39,47 @@
|
|||
|
||||
$: fetchSchema(dataSource)
|
||||
$: schemaKey = generateSchemaKey(schema)
|
||||
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
||||
$: initialValues = getInitialValues(
|
||||
actionType,
|
||||
dataSource,
|
||||
$component.path,
|
||||
$context
|
||||
)
|
||||
$: resetKey = Helpers.hashString(
|
||||
schemaKey + JSON.stringify(initialValues) + disabled + readonly
|
||||
)
|
||||
|
||||
// Returns the closes data context which isn't a built in context
|
||||
const getInitialValues = (type, dataSource, context) => {
|
||||
const getInitialValues = (type, dataSource, path, context) => {
|
||||
// Only inherit values for update forms
|
||||
if (type !== "Update") {
|
||||
return {}
|
||||
}
|
||||
// Only inherit values for forms targeting internal tables
|
||||
if (!dataSource?.tableId) {
|
||||
const dsType = dataSource?.type
|
||||
if (dsType !== "table" && dsType !== "viewV2") {
|
||||
return {}
|
||||
}
|
||||
// Don't inherit values representing built in contexts
|
||||
if (["user", "url"].includes(context.closestComponentId)) {
|
||||
return {}
|
||||
// Look up the component tree and find something that is provided by an
|
||||
// ancestor that matches our datasource. This is for backwards compatibility
|
||||
// as previously we could use the "closest" context.
|
||||
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]
|
||||
}
|
||||
}
|
||||
// Always inherit the closest datasource
|
||||
const closestContext = context[`${context.closestComponentId}`] || {}
|
||||
return closestContext || {}
|
||||
return {}
|
||||
}
|
||||
|
||||
// Fetches the form schema from this form's dataSource
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
const forceFetchRows = async () => {
|
||||
// if the filter has changed, then we need to reset the options, clear the selection, and re-fetch
|
||||
optionsObj = {}
|
||||
fieldApi.setValue([])
|
||||
fieldApi?.setValue([])
|
||||
selectedValue = []
|
||||
debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||
}
|
||||
|
@ -236,7 +236,6 @@
|
|||
bind:searchTerm
|
||||
loading={$fetch.loading}
|
||||
bind:open
|
||||
customPopoverMaxHeight={400}
|
||||
/>
|
||||
{/if}
|
||||
</Field>
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
<script>
|
||||
import { getContext, setContext, onDestroy } from "svelte"
|
||||
import { dataSourceStore, createContextStore } from "stores"
|
||||
import { ActionTypes } from "constants"
|
||||
import { ActionTypes, ContextScopes } from "constants"
|
||||
import { generate } from "shortid"
|
||||
|
||||
export let data
|
||||
export let actions
|
||||
export let key
|
||||
export let scope = ContextScopes.Global
|
||||
|
||||
// Clone and create new data context for this component tree
|
||||
const context = getContext("context")
|
||||
let context = getContext("context")
|
||||
const component = getContext("component")
|
||||
const newContext = createContextStore(context)
|
||||
setContext("context", newContext)
|
||||
|
||||
const providerKey = key || $component.id
|
||||
|
||||
// Create a new layer of context if we are only locally scoped
|
||||
if (scope === ContextScopes.Local) {
|
||||
context = createContextStore(context)
|
||||
setContext("context", context)
|
||||
}
|
||||
|
||||
// Generate a permanent unique ID for this component and use it to register
|
||||
// any datasource actions
|
||||
const instanceId = generate()
|
||||
|
@ -30,7 +33,7 @@
|
|||
const provideData = newData => {
|
||||
const dataKey = JSON.stringify(newData)
|
||||
if (dataKey !== lastDataKey) {
|
||||
newContext.actions.provideData(providerKey, newData)
|
||||
context.actions.provideData(providerKey, newData, scope)
|
||||
lastDataKey = dataKey
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +43,7 @@
|
|||
if (actionsKey !== lastActionsKey) {
|
||||
lastActionsKey = actionsKey
|
||||
newActions?.forEach(({ type, callback, metadata }) => {
|
||||
newContext.actions.provideAction(providerKey, type, callback)
|
||||
context.actions.provideAction(providerKey, type, callback, scope)
|
||||
|
||||
// Register any "refresh datasource" actions with a singleton store
|
||||
// so we can easily refresh data at all levels for any datasource
|
||||
|
|
|
@ -12,5 +12,10 @@ export const ActionTypes = {
|
|||
ScrollTo: "ScrollTo",
|
||||
}
|
||||
|
||||
export const ContextScopes = {
|
||||
Local: "local",
|
||||
Global: "global",
|
||||
}
|
||||
|
||||
export const DNDPlaceholderID = "dnd-placeholder"
|
||||
export const ScreenslotType = "screenslot"
|
||||
|
|
|
@ -23,7 +23,7 @@ import { getAction } from "utils/getAction"
|
|||
import Provider from "components/context/Provider.svelte"
|
||||
import Block from "components/Block.svelte"
|
||||
import BlockComponent from "components/BlockComponent.svelte"
|
||||
import { ActionTypes } from "./constants"
|
||||
import { ActionTypes, ContextScopes } from "./constants"
|
||||
import { fetchDatasourceSchema } from "./utils/schema.js"
|
||||
import { getAPIKey } from "./utils/api.js"
|
||||
import { enrichButtonActions } from "./utils/buttonActions.js"
|
||||
|
@ -54,6 +54,7 @@ export default {
|
|||
linkable,
|
||||
getAction,
|
||||
fetchDatasourceSchema,
|
||||
ContextScopes,
|
||||
getAPIKey,
|
||||
enrichButtonActions,
|
||||
processStringSync,
|
||||
|
|
|
@ -1,59 +1,98 @@
|
|||
import { writable, derived } from "svelte/store"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { ContextScopes } from "constants"
|
||||
|
||||
export const createContextStore = oldContext => {
|
||||
const newContext = writable({})
|
||||
const contexts = oldContext ? [oldContext, newContext] : [newContext]
|
||||
export const createContextStore = parentContext => {
|
||||
const context = writable({})
|
||||
let observers = []
|
||||
|
||||
// Derive the total context state at this point in the tree
|
||||
const contexts = parentContext ? [parentContext, context] : [context]
|
||||
const totalContext = derived(contexts, $contexts => {
|
||||
// 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,
|
||||
}
|
||||
return $contexts.reduce((total, context) => ({ ...total, ...context }), {})
|
||||
})
|
||||
|
||||
// Adds a data context layer to the tree
|
||||
const provideData = (providerId, data) => {
|
||||
if (!providerId || data === undefined) {
|
||||
return
|
||||
}
|
||||
newContext.update(state => {
|
||||
state[providerId] = data
|
||||
|
||||
// Keep track of the closest component ID so we can later hydrate a "data" prop.
|
||||
// This is only required for legacy bindings that used "data" rather than a
|
||||
// component ID.
|
||||
state.closestComponentId = providerId
|
||||
|
||||
return state
|
||||
// Subscribe to updates in the parent context, so that we can proxy on any
|
||||
// change messages to our own subscribers
|
||||
if (parentContext) {
|
||||
parentContext.actions.observeChanges(key => {
|
||||
broadcastChange(key)
|
||||
})
|
||||
}
|
||||
|
||||
// Adds an action context layer to the tree
|
||||
const provideAction = (providerId, actionType, callback) => {
|
||||
// Provide some data in context
|
||||
const provideData = (providerId, data, scope = ContextScopes.Global) => {
|
||||
if (!providerId || data === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// Proxy message up the chain if we have a parent and are providing global
|
||||
// context
|
||||
if (scope === ContextScopes.Global && parentContext) {
|
||||
parentContext.actions.provideData(providerId, data, 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 {
|
||||
context.update(state => {
|
||||
state[providerId] = data
|
||||
return state
|
||||
})
|
||||
broadcastChange(providerId)
|
||||
}
|
||||
}
|
||||
|
||||
// Provides some action in context
|
||||
const provideAction = (
|
||||
providerId,
|
||||
actionType,
|
||||
callback,
|
||||
scope = ContextScopes.Global
|
||||
) => {
|
||||
if (!providerId || !actionType) {
|
||||
return
|
||||
}
|
||||
newContext.update(state => {
|
||||
state[`${providerId}_${actionType}`] = callback
|
||||
return state
|
||||
})
|
||||
|
||||
// Proxy message up the chain if we have a parent and are providing global
|
||||
// context
|
||||
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 {
|
||||
subscribe: totalContext.subscribe,
|
||||
actions: { provideData, provideAction },
|
||||
actions: {
|
||||
provideData,
|
||||
provideAction,
|
||||
observeChanges,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,54 @@ import { ActionTypes } from "constants"
|
|||
import { enrichDataBindings } from "./enrichDataBinding"
|
||||
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 { fields, providerId, tableId, notificationOverride } =
|
||||
action.parameters
|
||||
|
@ -32,20 +80,21 @@ const saveRowHandler = async (action, context) => {
|
|||
}
|
||||
}
|
||||
if (tableId) {
|
||||
payload.tableId = tableId
|
||||
if (tableId.startsWith("view")) {
|
||||
payload._viewId = tableId
|
||||
} else {
|
||||
payload.tableId = tableId
|
||||
}
|
||||
}
|
||||
try {
|
||||
const row = await API.saveRow(payload)
|
||||
|
||||
if (!notificationOverride) {
|
||||
notificationStore.actions.success("Row saved")
|
||||
}
|
||||
|
||||
// Refresh related datasources
|
||||
await dataSourceStore.actions.invalidateDataSource(tableId, {
|
||||
invalidateRelationships: true,
|
||||
})
|
||||
|
||||
return { row }
|
||||
} catch (error) {
|
||||
// Abort next actions
|
||||
|
@ -64,7 +113,11 @@ const duplicateRowHandler = async (action, context) => {
|
|||
}
|
||||
}
|
||||
if (tableId) {
|
||||
payload.tableId = tableId
|
||||
if (tableId.startsWith("view")) {
|
||||
payload._viewId = tableId
|
||||
} else {
|
||||
payload.tableId = tableId
|
||||
}
|
||||
}
|
||||
delete payload._id
|
||||
delete payload._rev
|
||||
|
@ -73,12 +126,10 @@ const duplicateRowHandler = async (action, context) => {
|
|||
if (!notificationOverride) {
|
||||
notificationStore.actions.success("Row saved")
|
||||
}
|
||||
|
||||
// Refresh related datasources
|
||||
await dataSourceStore.actions.invalidateDataSource(tableId, {
|
||||
invalidateRelationships: true,
|
||||
})
|
||||
|
||||
return { row }
|
||||
} catch (error) {
|
||||
// Abort next actions
|
||||
|
@ -190,17 +241,6 @@ const navigationHandler = action => {
|
|||
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 { datasourceId, queryId, queryParams, notificationOverride } =
|
||||
action.parameters
|
||||
|
@ -236,47 +276,6 @@ 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 => {
|
||||
await authStore.actions.logOut()
|
||||
let redirectUrl = "/builder/auth/login"
|
||||
|
@ -293,23 +292,6 @@ 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 => {
|
||||
let url
|
||||
if (action?.parameters) {
|
||||
|
@ -417,16 +399,10 @@ const handlerMap = {
|
|||
["Duplicate Row"]: duplicateRowHandler,
|
||||
["Delete Row"]: deleteRowHandler,
|
||||
["Navigate To"]: navigationHandler,
|
||||
["Scroll To Field"]: scrollHandler,
|
||||
["Execute Query"]: queryExecutionHandler,
|
||||
["Trigger Automation"]: triggerAutomationHandler,
|
||||
["Validate Form"]: validateFormHandler,
|
||||
["Update Field Value"]: updateFieldValueHandler,
|
||||
["Refresh Data Provider"]: refreshDataProviderHandler,
|
||||
["Log Out"]: logoutHandler,
|
||||
["Clear Form"]: clearFormHandler,
|
||||
["Close Screen Modal"]: closeScreenModalHandler,
|
||||
["Change Form Step"]: changeFormStepHandler,
|
||||
["Update State"]: updateStateHandler,
|
||||
["Upload File to S3"]: s3UploadHandler,
|
||||
["Export Data"]: exportDataHandler,
|
||||
|
@ -461,7 +437,12 @@ export const enrichButtonActions = (actions, context) => {
|
|||
return actions
|
||||
}
|
||||
|
||||
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
||||
// Get handlers for each action. If no bespoke handler is configured, fall
|
||||
// back to simply executing this action from context.
|
||||
const handlers = actions.map(def => {
|
||||
return handlerMap[def["##eventHandlerType"]] || contextActionHandler
|
||||
})
|
||||
|
||||
return async eventContext => {
|
||||
// Button context is built up as actions are executed.
|
||||
// Inherit any previous button context which may have come from actions
|
||||
|
|
|
@ -23,16 +23,6 @@ export const propsAreSame = (a, b) => {
|
|||
* Data bindings are enriched, and button actions are enriched.
|
||||
*/
|
||||
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.
|
||||
// Extract top level button action settings.
|
||||
let normalProps = { ...props }
|
||||
|
@ -49,13 +39,13 @@ export const enrichProps = (props, context, settingsDefinitionMap) => {
|
|||
let rawConditions = normalProps._conditions
|
||||
|
||||
// Enrich all props except button actions
|
||||
let enrichedProps = enrichDataBindings(normalProps, totalContext)
|
||||
let enrichedProps = enrichDataBindings(normalProps, context)
|
||||
|
||||
// Enrich button actions.
|
||||
// Actions are enriched into a function at this stage, but actual data
|
||||
// binding enrichment is done dynamically at runtime.
|
||||
Object.keys(actionProps).forEach(prop => {
|
||||
enrichedProps[prop] = enrichButtonActions(actionProps[prop], totalContext)
|
||||
enrichedProps[prop] = enrichButtonActions(actionProps[prop], context)
|
||||
})
|
||||
|
||||
// Conditions
|
||||
|
@ -66,7 +56,7 @@ export const enrichProps = (props, context, settingsDefinitionMap) => {
|
|||
// action
|
||||
condition.settingValue = enrichButtonActions(
|
||||
rawConditions[idx].settingValue,
|
||||
totalContext
|
||||
context
|
||||
)
|
||||
|
||||
// Since we can't compare functions, we need to assume that conditions
|
||||
|
|
|
@ -19,11 +19,12 @@ export const buildRowEndpoints = API => ({
|
|||
* @param suppressErrors whether or not to suppress error notifications
|
||||
*/
|
||||
saveRow: async (row, suppressErrors = false) => {
|
||||
if (!row?.tableId) {
|
||||
const resourceId = row?._viewId || row?.tableId
|
||||
if (!resourceId) {
|
||||
return
|
||||
}
|
||||
return await API.post({
|
||||
url: `/api/${row._viewId || row.tableId}/rows`,
|
||||
url: `/api/${resourceId}/rows`,
|
||||
body: row,
|
||||
suppressErrors,
|
||||
})
|
||||
|
@ -35,11 +36,12 @@ export const buildRowEndpoints = API => ({
|
|||
* @param suppressErrors whether or not to suppress error notifications
|
||||
*/
|
||||
patchRow: async (row, suppressErrors = false) => {
|
||||
if (!row?.tableId && !row?._viewId) {
|
||||
const resourceId = row?._viewId || row?.tableId
|
||||
if (!resourceId) {
|
||||
return
|
||||
}
|
||||
return await API.patch({
|
||||
url: `/api/${row._viewId || row.tableId}/rows`,
|
||||
url: `/api/${resourceId}/rows`,
|
||||
body: row,
|
||||
suppressErrors,
|
||||
})
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
import { getColumnIcon } from "../lib/utils"
|
||||
import MigrationModal from "../controls/MigrationModal.svelte"
|
||||
import { debounce } from "../../../utils/utils"
|
||||
import { FieldType, FormulaTypes } from "@budibase/types"
|
||||
import { FieldType, FormulaType } from "@budibase/types"
|
||||
import { TableNames } from "../../../constants"
|
||||
|
||||
export let column
|
||||
|
@ -96,7 +96,7 @@
|
|||
const { type, formulaType } = col.schema
|
||||
return (
|
||||
searchableTypes.includes(type) ||
|
||||
(type === FieldType.FORMULA && formulaType === FormulaTypes.STATIC)
|
||||
(type === FieldType.FORMULA && formulaType === FormulaType.STATIC)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit ce7722ed4474718596b465dcfd49bef36cab2e42
|
||||
Subproject commit eb9565f568cfef14b336b14eee753119acfdd43b
|
|
@ -119,8 +119,8 @@
|
|||
"@types/google-spreadsheet": "3.1.5",
|
||||
"@types/jest": "29.5.5",
|
||||
"@types/koa": "2.13.4",
|
||||
"@types/koa__router": "8.0.8",
|
||||
"@types/koa-send": "^4.1.6",
|
||||
"@types/koa__router": "8.0.8",
|
||||
"@types/lodash": "4.14.200",
|
||||
"@types/mssql": "9.1.4",
|
||||
"@types/node-fetch": "2.6.4",
|
||||
|
@ -142,6 +142,7 @@
|
|||
"rimraf": "3.0.2",
|
||||
"supertest": "6.3.3",
|
||||
"swagger-jsdoc": "6.1.0",
|
||||
"testcontainers": "10.6.0",
|
||||
"timekeeper": "2.2.0",
|
||||
"ts-node": "10.8.1",
|
||||
"tsconfig-paths": "4.0.0",
|
||||
|
|
|
@ -2,7 +2,7 @@ version: "3.8"
|
|||
services:
|
||||
db:
|
||||
container_name: postgres
|
||||
image: postgres:15-bullseye
|
||||
image: postgres:16.1-bullseye
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FieldTypes, RelationshipType, FormulaTypes } from "../../src/constants"
|
||||
import { FieldType, FormulaType, RelationshipType } from "@budibase/types"
|
||||
import { object } from "./utils"
|
||||
import Resource from "./utils/Resource"
|
||||
|
||||
|
@ -27,7 +27,7 @@ const table = {
|
|||
const baseColumnDef = {
|
||||
type: {
|
||||
type: "string",
|
||||
enum: Object.values(FieldTypes),
|
||||
enum: Object.values(FieldType),
|
||||
description:
|
||||
"Defines the type of the column, most explain themselves, a link column is a relationship.",
|
||||
},
|
||||
|
@ -81,7 +81,7 @@ const tableSchema = {
|
|||
...baseColumnDef,
|
||||
type: {
|
||||
type: "string",
|
||||
enum: [FieldTypes.LINK],
|
||||
enum: [FieldType.LINK],
|
||||
description: "A relationship column.",
|
||||
},
|
||||
fieldName: {
|
||||
|
@ -128,7 +128,7 @@ const tableSchema = {
|
|||
...baseColumnDef,
|
||||
type: {
|
||||
type: "string",
|
||||
enum: [FieldTypes.FORMULA],
|
||||
enum: [FieldType.FORMULA],
|
||||
description: "A formula column.",
|
||||
},
|
||||
formula: {
|
||||
|
@ -138,7 +138,7 @@ const tableSchema = {
|
|||
},
|
||||
formulaType: {
|
||||
type: "string",
|
||||
enum: Object.values(FormulaTypes),
|
||||
enum: Object.values(FormulaType),
|
||||
description:
|
||||
"Defines whether this is a static or dynamic formula.",
|
||||
},
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
import { generateQueryID } from "../../../db/utils"
|
||||
import { BaseQueryVerbs, FieldTypes } from "../../../constants"
|
||||
import { BaseQueryVerbs } from "../../../constants"
|
||||
import { Thread, ThreadType } from "../../../threads"
|
||||
import { save as saveDatasource } from "../datasource"
|
||||
import { RestImporter } from "./import"
|
||||
import { invalidateDynamicVariables } from "../../../threads/utils"
|
||||
import env from "../../../environment"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import { events, context, utils, constants } from "@budibase/backend-core"
|
||||
import sdk from "../../../sdk"
|
||||
import { QueryEvent } from "../../../threads/definitions"
|
||||
import { ConfigType, Query, UserCtx, SessionCookie } from "@budibase/types"
|
||||
import { QueryEvent, QueryResponse } from "../../../threads/definitions"
|
||||
import {
|
||||
ConfigType,
|
||||
Query,
|
||||
UserCtx,
|
||||
SessionCookie,
|
||||
QuerySchema,
|
||||
FieldType,
|
||||
} from "@budibase/types"
|
||||
import { ValidQueryNameRegex } from "@budibase/shared-core"
|
||||
|
||||
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 schemaFields: any = {}
|
||||
const { rows, keys, info, extra } = await Runner.run<QueryResponse>(inputs)
|
||||
const previewSchema: Record<string, QuerySchema> = {}
|
||||
const makeQuerySchema = (type: FieldType, name: string): QuerySchema => ({
|
||||
type,
|
||||
name,
|
||||
})
|
||||
if (rows?.length > 0) {
|
||||
for (let key of [...new Set(keys)] as string[]) {
|
||||
const field = rows[0][key]
|
||||
let type = typeof field,
|
||||
fieldType = FieldTypes.STRING
|
||||
fieldMetadata = makeQuerySchema(FieldType.STRING, key)
|
||||
if (field)
|
||||
switch (type) {
|
||||
case "boolean":
|
||||
schemaFields[key] = FieldTypes.BOOLEAN
|
||||
fieldMetadata = makeQuerySchema(FieldType.BOOLEAN, key)
|
||||
break
|
||||
case "object":
|
||||
if (field instanceof Date) {
|
||||
fieldType = FieldTypes.DATETIME
|
||||
fieldMetadata = makeQuerySchema(FieldType.DATETIME, key)
|
||||
} else if (Array.isArray(field)) {
|
||||
fieldType = FieldTypes.ARRAY
|
||||
fieldMetadata = makeQuerySchema(FieldType.ARRAY, key)
|
||||
} else {
|
||||
fieldType = FieldTypes.JSON
|
||||
fieldMetadata = makeQuerySchema(FieldType.JSON, key)
|
||||
}
|
||||
break
|
||||
case "number":
|
||||
fieldType = FieldTypes.NUMBER
|
||||
fieldMetadata = makeQuerySchema(FieldType.NUMBER, key)
|
||||
break
|
||||
}
|
||||
schemaFields[key] = fieldType
|
||||
previewSchema[key] = fieldMetadata
|
||||
}
|
||||
}
|
||||
// if existing schema, update to include any previous schema keys
|
||||
if (existingSchema) {
|
||||
for (let key of Object.keys(schemaFields)) {
|
||||
if (existingSchema[key]?.type) {
|
||||
schemaFields[key] = existingSchema[key].type
|
||||
for (let key of Object.keys(previewSchema)) {
|
||||
if (existingSchema[key]) {
|
||||
previewSchema[key] = existingSchema[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -203,7 +213,7 @@ export async function preview(ctx: UserCtx) {
|
|||
await events.query.previewed(datasource, query)
|
||||
ctx.body = {
|
||||
rows,
|
||||
schemaFields,
|
||||
schema: previewSchema,
|
||||
info,
|
||||
extra,
|
||||
}
|
||||
|
@ -257,7 +267,9 @@ async function execute(
|
|||
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
|
||||
if (extra?.raw) {
|
||||
delete extra.raw
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AutoFieldSubType,
|
||||
AutoReason,
|
||||
Datasource,
|
||||
FieldSchema,
|
||||
|
@ -27,7 +28,6 @@ import {
|
|||
isSQL,
|
||||
} from "../../../integrations/utils"
|
||||
import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils"
|
||||
import { AutoFieldSubTypes, FieldTypes } from "../../../constants"
|
||||
import { processObjectSync } from "@budibase/string-templates"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { processDates, processFormulas } from "../../../utilities/rowProcessor"
|
||||
|
@ -112,10 +112,10 @@ function buildFilters(
|
|||
*/
|
||||
function cleanupConfig(config: RunConfig, table: Table): RunConfig {
|
||||
const primaryOptions = [
|
||||
FieldTypes.STRING,
|
||||
FieldTypes.LONGFORM,
|
||||
FieldTypes.OPTIONS,
|
||||
FieldTypes.NUMBER,
|
||||
FieldType.STRING,
|
||||
FieldType.LONGFORM,
|
||||
FieldType.OPTIONS,
|
||||
FieldType.NUMBER,
|
||||
]
|
||||
// filter out fields which cannot be keys
|
||||
const fieldNames = Object.entries(table.schema)
|
||||
|
@ -242,10 +242,7 @@ function basicProcessing({
|
|||
|
||||
function fixArrayTypes(row: Row, table: Table) {
|
||||
for (let [fieldName, schema] of Object.entries(table.schema)) {
|
||||
if (
|
||||
schema.type === FieldTypes.ARRAY &&
|
||||
typeof row[fieldName] === "string"
|
||||
) {
|
||||
if (schema.type === FieldType.ARRAY && typeof row[fieldName] === "string") {
|
||||
try {
|
||||
row[fieldName] = JSON.parse(row[fieldName])
|
||||
} catch (err) {
|
||||
|
@ -275,8 +272,8 @@ function isEditableColumn(column: FieldSchema) {
|
|||
const isExternalAutoColumn =
|
||||
column.autocolumn &&
|
||||
column.autoReason !== AutoReason.FOREIGN_KEY &&
|
||||
column.subtype !== AutoFieldSubTypes.AUTO_ID
|
||||
const isFormula = column.type === FieldTypes.FORMULA
|
||||
column.subtype !== AutoFieldSubType.AUTO_ID
|
||||
const isFormula = column.type === FieldType.FORMULA
|
||||
return !(isExternalAutoColumn || isFormula)
|
||||
}
|
||||
|
||||
|
@ -323,11 +320,11 @@ export class ExternalRequest<T extends Operation> {
|
|||
continue
|
||||
}
|
||||
// parse floats/numbers
|
||||
if (field.type === FieldTypes.NUMBER && !isNaN(parseFloat(row[key]))) {
|
||||
if (field.type === FieldType.NUMBER && !isNaN(parseFloat(row[key]))) {
|
||||
newRow[key] = parseFloat(row[key])
|
||||
}
|
||||
// if its not a link then just copy it over
|
||||
if (field.type !== FieldTypes.LINK) {
|
||||
if (field.type !== FieldType.LINK) {
|
||||
newRow[key] = row[key]
|
||||
continue
|
||||
}
|
||||
|
@ -533,7 +530,7 @@ export class ExternalRequest<T extends Operation> {
|
|||
buildRelationships(table: Table): RelationshipsJson[] {
|
||||
const relationships = []
|
||||
for (let [fieldName, field] of Object.entries(table.schema)) {
|
||||
if (field.type !== FieldTypes.LINK) {
|
||||
if (field.type !== FieldType.LINK) {
|
||||
continue
|
||||
}
|
||||
const { tableName: linkTableName } = breakExternalTableId(field.tableId)
|
||||
|
@ -587,7 +584,7 @@ export class ExternalRequest<T extends Operation> {
|
|||
// we need this to work out if any relationships need removed
|
||||
for (const field of Object.values(table.schema)) {
|
||||
if (
|
||||
field.type !== FieldTypes.LINK ||
|
||||
field.type !== FieldType.LINK ||
|
||||
!field.fieldName ||
|
||||
isOneSide(field)
|
||||
) {
|
||||
|
@ -731,15 +728,15 @@ export class ExternalRequest<T extends Operation> {
|
|||
return Object.entries(table.schema)
|
||||
.filter(
|
||||
column =>
|
||||
column[1].type !== FieldTypes.LINK &&
|
||||
column[1].type !== FieldTypes.FORMULA &&
|
||||
column[1].type !== FieldType.LINK &&
|
||||
column[1].type !== FieldType.FORMULA &&
|
||||
!existing.find((field: string) => field === column[0])
|
||||
)
|
||||
.map(column => `${table.name}.${column[0]}`)
|
||||
}
|
||||
let fields = extractRealFields(table)
|
||||
for (let field of Object.values(table.schema)) {
|
||||
if (field.type !== FieldTypes.LINK || !includeRelations) {
|
||||
if (field.type !== FieldType.LINK || !includeRelations) {
|
||||
continue
|
||||
}
|
||||
const { tableName: linkTableName } = breakExternalTableId(field.tableId)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { FieldTypes } from "../../../constants"
|
||||
import {
|
||||
breakExternalTableId,
|
||||
breakRowIdField,
|
||||
|
@ -9,6 +8,7 @@ import {
|
|||
RunConfig,
|
||||
} from "./ExternalRequest"
|
||||
import {
|
||||
FieldType,
|
||||
Datasource,
|
||||
IncludeRelationship,
|
||||
Operation,
|
||||
|
@ -154,7 +154,7 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
|
|||
// for a single row, there is probably a better way to do this with some smart multi-layer joins
|
||||
for (let [fieldName, field] of Object.entries(table.schema)) {
|
||||
if (
|
||||
field.type !== FieldTypes.LINK ||
|
||||
field.type !== FieldType.LINK ||
|
||||
!row[fieldName] ||
|
||||
row[fieldName].length === 0
|
||||
) {
|
||||
|
|
|
@ -6,12 +6,12 @@ import {
|
|||
inputProcessing,
|
||||
outputProcessing,
|
||||
} from "../../../utilities/rowProcessor"
|
||||
import { FieldTypes } from "../../../constants"
|
||||
import * as utils from "./utils"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import { finaliseRow, updateRelatedFormula } from "./staticFormula"
|
||||
import {
|
||||
FieldType,
|
||||
LinkDocumentValue,
|
||||
PatchRowRequest,
|
||||
PatchRowResponse,
|
||||
|
@ -225,7 +225,7 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
|
|||
// insert the link rows in the correct place throughout the main row
|
||||
for (let fieldName of Object.keys(table.schema)) {
|
||||
let field = table.schema[fieldName]
|
||||
if (field.type === FieldTypes.LINK) {
|
||||
if (field.type === FieldType.LINK) {
|
||||
// find the links that pertain to this field
|
||||
const links = linkVals.filter(link => link.fieldName === fieldName)
|
||||
// find the rows that the links state are linked to this field
|
||||
|
|
|
@ -4,9 +4,15 @@ import {
|
|||
processAutoColumn,
|
||||
processFormulas,
|
||||
} from "../../../utilities/rowProcessor"
|
||||
import { FieldTypes, FormulaTypes } from "../../../constants"
|
||||
import { context, locks } from "@budibase/backend-core"
|
||||
import { Table, Row, LockType, LockName } from "@budibase/types"
|
||||
import {
|
||||
Table,
|
||||
Row,
|
||||
LockType,
|
||||
LockName,
|
||||
FormulaType,
|
||||
FieldType,
|
||||
} from "@budibase/types"
|
||||
import * as linkRows from "../../../db/linkedRows"
|
||||
import sdk from "../../../sdk"
|
||||
import isEqual from "lodash/isEqual"
|
||||
|
@ -35,7 +41,7 @@ export async function updateRelatedFormula(
|
|||
let relatedRows: Record<string, Row[]> = {}
|
||||
for (let [key, field] of Object.entries(enrichedRow)) {
|
||||
const columnDefinition = table.schema[key]
|
||||
if (columnDefinition && columnDefinition.type === FieldTypes.LINK) {
|
||||
if (columnDefinition && columnDefinition.type === FieldType.LINK) {
|
||||
const relatedTableId = columnDefinition.tableId!
|
||||
if (!relatedRows[relatedTableId]) {
|
||||
relatedRows[relatedTableId] = []
|
||||
|
@ -63,8 +69,8 @@ export async function updateRelatedFormula(
|
|||
for (let column of Object.values(relatedTable!.schema)) {
|
||||
// needs updated in related rows
|
||||
if (
|
||||
column.type === FieldTypes.FORMULA &&
|
||||
column.formulaType === FormulaTypes.STATIC
|
||||
column.type === FieldType.FORMULA &&
|
||||
column.formulaType === FormulaType.STATIC
|
||||
) {
|
||||
// re-enrich rows for all the related, don't update the related formula for them
|
||||
promises = promises.concat(
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { FormulaTypes } from "../../../constants"
|
||||
import { clearColumns } from "./utils"
|
||||
import { doesContainStrings } from "@budibase/string-templates"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
@ -7,6 +6,7 @@ import uniq from "lodash/uniq"
|
|||
import { updateAllFormulasInTable } from "../row/staticFormula"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import {
|
||||
FormulaType,
|
||||
FieldSchema,
|
||||
FieldType,
|
||||
FormulaFieldMetadata,
|
||||
|
@ -17,10 +17,10 @@ import { isRelationshipColumn } from "../../../db/utils"
|
|||
|
||||
function isStaticFormula(
|
||||
column: FieldSchema
|
||||
): column is FormulaFieldMetadata & { formulaType: FormulaTypes.STATIC } {
|
||||
): column is FormulaFieldMetadata & { formulaType: FormulaType.STATIC } {
|
||||
return (
|
||||
column.type === FieldType.FORMULA &&
|
||||
column.formulaType === FormulaTypes.STATIC
|
||||
column.formulaType === FormulaType.STATIC
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { FieldType } from "@budibase/types"
|
||||
import { AutoFieldSubTypes } from "../../../../constants"
|
||||
import { AutoFieldSubType, FieldType } from "@budibase/types"
|
||||
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
|
||||
import { importToRows } from "../utils"
|
||||
|
||||
|
@ -22,7 +21,7 @@ describe("utils", () => {
|
|||
autoId: {
|
||||
name: "autoId",
|
||||
type: FieldType.NUMBER,
|
||||
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
type: FieldType.NUMBER,
|
||||
|
@ -69,7 +68,7 @@ describe("utils", () => {
|
|||
autoId: {
|
||||
name: "autoId",
|
||||
type: FieldType.NUMBER,
|
||||
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
type: FieldType.NUMBER,
|
||||
|
|
|
@ -2,8 +2,6 @@ import { parse, isSchema, isRows } from "../../../utilities/schema"
|
|||
import { getRowParams, generateRowID, InternalTables } from "../../../db/utils"
|
||||
import isEqual from "lodash/isEqual"
|
||||
import {
|
||||
AutoFieldSubTypes,
|
||||
FieldTypes,
|
||||
GOOGLE_SHEETS_PRIMARY_KEY,
|
||||
USERS_TABLE_SCHEMA,
|
||||
SwitchableTypes,
|
||||
|
@ -19,6 +17,7 @@ import { cloneDeep } from "lodash/fp"
|
|||
import { quotas } from "@budibase/pro"
|
||||
import { events, context } from "@budibase/backend-core"
|
||||
import {
|
||||
AutoFieldSubType,
|
||||
ContextUser,
|
||||
Datasource,
|
||||
Row,
|
||||
|
@ -106,7 +105,7 @@ export function makeSureTableUpToDate(table: Table, tableToSave: Table) {
|
|||
for ([field, column] of Object.entries(table.schema)) {
|
||||
if (
|
||||
column.autocolumn &&
|
||||
column.subtype === AutoFieldSubTypes.AUTO_ID &&
|
||||
column.subtype === AutoFieldSubType.AUTO_ID &&
|
||||
tableToSave.schema[field]
|
||||
) {
|
||||
const tableCol = tableToSave.schema[field] as NumberFieldMetadata
|
||||
|
@ -144,8 +143,8 @@ export async function importToRows(
|
|||
? row[fieldName]
|
||||
: [row[fieldName]]
|
||||
if (
|
||||
(schema.type === FieldTypes.OPTIONS ||
|
||||
schema.type === FieldTypes.ARRAY) &&
|
||||
(schema.type === FieldType.OPTIONS ||
|
||||
schema.type === FieldType.ARRAY) &&
|
||||
row[fieldName]
|
||||
) {
|
||||
let merged = [...schema.constraints!.inclusion!, ...rowVal]
|
||||
|
@ -403,7 +402,7 @@ export async function checkForViewUpdates(
|
|||
)
|
||||
const newViewTemplate = viewTemplate(
|
||||
viewMetadata,
|
||||
groupByField?.type === FieldTypes.ARRAY
|
||||
groupByField?.type === FieldType.ARRAY
|
||||
)
|
||||
const viewName = view.name!
|
||||
await saveView(null, viewName, newViewTemplate)
|
||||
|
@ -434,7 +433,7 @@ export function generateJunctionTableName(
|
|||
|
||||
export function foreignKeyStructure(keyName: string, meta?: any) {
|
||||
const structure: any = {
|
||||
type: FieldTypes.NUMBER,
|
||||
type: FieldType.NUMBER,
|
||||
constraints: {},
|
||||
name: keyName,
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import { fetchView } from "../row"
|
|||
import { context, events } from "@budibase/backend-core"
|
||||
import { DocumentType } from "../../../db/utils"
|
||||
import sdk from "../../../sdk"
|
||||
import { FieldTypes } from "../../../constants"
|
||||
import {
|
||||
FieldType,
|
||||
Ctx,
|
||||
Row,
|
||||
Table,
|
||||
|
@ -37,7 +37,7 @@ export async function save(ctx: Ctx) {
|
|||
(field: any) => field.name == viewToSave.groupBy
|
||||
)
|
||||
|
||||
const view = viewTemplate(viewToSave, groupByField?.type === FieldTypes.ARRAY)
|
||||
const view = viewTemplate(viewToSave, groupByField?.type === FieldType.ARRAY)
|
||||
const viewName = viewToSave.name
|
||||
|
||||
if (!viewName) {
|
||||
|
|
|
@ -235,9 +235,9 @@ describe("/queries", () => {
|
|||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
// these responses come from the mock
|
||||
expect(res.body.schemaFields).toEqual({
|
||||
a: "string",
|
||||
b: "number",
|
||||
expect(res.body.schema).toEqual({
|
||||
a: { type: "string", name: "a" },
|
||||
b: { type: "number", name: "b" },
|
||||
})
|
||||
expect(res.body.rows.length).toEqual(1)
|
||||
expect(events.query.previewed).toBeCalledTimes(1)
|
||||
|
@ -300,10 +300,10 @@ describe("/queries", () => {
|
|||
queryString: "test={{ variable2 }}",
|
||||
})
|
||||
// these responses come from the mock
|
||||
expect(res.body.schemaFields).toEqual({
|
||||
opts: "json",
|
||||
url: "string",
|
||||
value: "string",
|
||||
expect(res.body.schema).toEqual({
|
||||
opts: { type: "json", name: "opts" },
|
||||
url: { type: "string", name: "url" },
|
||||
value: { type: "string", name: "value" },
|
||||
})
|
||||
expect(res.body.rows[0].url).toEqual("http://www.google.com?test=1")
|
||||
})
|
||||
|
@ -314,10 +314,10 @@ describe("/queries", () => {
|
|||
path: "www.google.com",
|
||||
queryString: "test={{ variable3 }}",
|
||||
})
|
||||
expect(res.body.schemaFields).toEqual({
|
||||
opts: "json",
|
||||
url: "string",
|
||||
value: "string",
|
||||
expect(res.body.schema).toEqual({
|
||||
opts: { type: "json", name: "opts" },
|
||||
url: { type: "string", name: "url" },
|
||||
value: { type: "string", name: "value" },
|
||||
})
|
||||
expect(res.body.rows[0].url).toContain("doctype%20html")
|
||||
})
|
||||
|
@ -337,10 +337,10 @@ describe("/queries", () => {
|
|||
path: "www.failonce.com",
|
||||
queryString: "test={{ variable3 }}",
|
||||
})
|
||||
expect(res.body.schemaFields).toEqual({
|
||||
fails: "number",
|
||||
opts: "json",
|
||||
url: "string",
|
||||
expect(res.body.schema).toEqual({
|
||||
fails: { type: "number", name: "fails" },
|
||||
opts: { type: "json", name: "opts" },
|
||||
url: { type: "string", name: "url" },
|
||||
})
|
||||
expect(res.body.rows[0].fails).toEqual(1)
|
||||
})
|
||||
|
|
|
@ -6,11 +6,11 @@ import * as setup from "./utilities"
|
|||
import { context, InternalTable, roles, tenancy } from "@budibase/backend-core"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import {
|
||||
AutoFieldSubTypes,
|
||||
AutoFieldSubType,
|
||||
FieldSchema,
|
||||
FieldType,
|
||||
FieldTypeSubtypes,
|
||||
FormulaTypes,
|
||||
FormulaType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
MonthlyQuotaName,
|
||||
PermissionLevel,
|
||||
|
@ -192,7 +192,7 @@ describe.each([
|
|||
"Row ID": {
|
||||
name: "Row ID",
|
||||
type: FieldType.NUMBER,
|
||||
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
icon: "ri-magic-line",
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
|
@ -2032,7 +2032,7 @@ describe.each([
|
|||
name: "formula",
|
||||
type: FieldType.FORMULA,
|
||||
formula: "{{ links.0.name }}",
|
||||
formulaType: FormulaTypes.DYNAMIC,
|
||||
formulaType: FormulaType.DYNAMIC,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -2086,7 +2086,7 @@ describe.each([
|
|||
name: "formula",
|
||||
type: FieldType.FORMULA,
|
||||
formula: `{{ js "${js}"}}`,
|
||||
formulaType: FormulaTypes.DYNAMIC,
|
||||
formulaType: FormulaType.DYNAMIC,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -2129,7 +2129,7 @@ describe.each([
|
|||
name: "formula",
|
||||
type: FieldType.FORMULA,
|
||||
formula: `{{ js "${js}"}}`,
|
||||
formulaType: FormulaTypes.DYNAMIC,
|
||||
formulaType: FormulaType.DYNAMIC,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { context, events } from "@budibase/backend-core"
|
||||
import {
|
||||
AutoFieldSubTypes,
|
||||
AutoFieldSubType,
|
||||
FieldSubtype,
|
||||
FieldType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
|
@ -205,7 +205,7 @@ describe("/tables", () => {
|
|||
autoId: {
|
||||
name: "id",
|
||||
type: FieldType.NUMBER,
|
||||
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
type: "number",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as rowController from "../../api/controllers/row"
|
||||
import * as tableController from "../../api/controllers/table"
|
||||
import { FieldTypes } from "../../constants"
|
||||
import { buildCtx } from "./utils"
|
||||
import * as automationUtils from "../automationUtils"
|
||||
import {
|
||||
FieldType,
|
||||
AutomationActionStepId,
|
||||
AutomationCustomIOType,
|
||||
AutomationFeature,
|
||||
|
@ -115,7 +115,7 @@ function typeCoercion(filters: SearchFilters, table: Table) {
|
|||
if (!column || typeof value !== "string") {
|
||||
continue
|
||||
}
|
||||
if (column.type === FieldTypes.NUMBER) {
|
||||
if (column.type === FieldType.NUMBER) {
|
||||
if (key === "oneOf") {
|
||||
searchParam[property] = value
|
||||
.split(",")
|
||||
|
@ -148,11 +148,11 @@ export async function run({ inputs, appId }: AutomationStepInput) {
|
|||
}
|
||||
}
|
||||
const table = await getTable(appId, tableId)
|
||||
let sortType = FieldTypes.STRING
|
||||
let sortType = FieldType.STRING
|
||||
if (table && table.schema && table.schema[sortColumn] && sortColumn) {
|
||||
const fieldType = table.schema[sortColumn].type
|
||||
sortType =
|
||||
fieldType === FieldTypes.NUMBER ? FieldTypes.NUMBER : FieldTypes.STRING
|
||||
fieldType === FieldType.NUMBER ? FieldType.NUMBER : FieldType.STRING
|
||||
}
|
||||
const ctx: any = buildCtx(appId, null, {
|
||||
params: {
|
||||
|
|
|
@ -1,18 +1,11 @@
|
|||
import { constants, objectStore, roles } from "@budibase/backend-core"
|
||||
import {
|
||||
FieldType as FieldTypes,
|
||||
FieldType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
Table,
|
||||
TableSourceType,
|
||||
} from "@budibase/types"
|
||||
|
||||
export {
|
||||
FieldType as FieldTypes,
|
||||
RelationshipType,
|
||||
AutoFieldSubTypes,
|
||||
FormulaTypes,
|
||||
} from "@budibase/types"
|
||||
|
||||
export enum FilterTypes {
|
||||
STRING = "string",
|
||||
FUZZY = "fuzzy",
|
||||
|
@ -36,14 +29,14 @@ export const NoEmptyFilterStrings = [
|
|||
]
|
||||
|
||||
export const CanSwitchTypes = [
|
||||
[FieldTypes.JSON, FieldTypes.ARRAY],
|
||||
[FieldType.JSON, FieldType.ARRAY],
|
||||
[
|
||||
FieldTypes.STRING,
|
||||
FieldTypes.OPTIONS,
|
||||
FieldTypes.LONGFORM,
|
||||
FieldTypes.BARCODEQR,
|
||||
FieldType.STRING,
|
||||
FieldType.OPTIONS,
|
||||
FieldType.LONGFORM,
|
||||
FieldType.BARCODEQR,
|
||||
],
|
||||
[FieldTypes.BOOLEAN, FieldTypes.NUMBER],
|
||||
[FieldType.BOOLEAN, FieldType.NUMBER],
|
||||
]
|
||||
|
||||
export const SwitchableTypes = CanSwitchTypes.reduce((prev, current) =>
|
||||
|
@ -86,9 +79,9 @@ export const USERS_TABLE_SCHEMA: Table = {
|
|||
// TODO: ADMIN PANEL - when implemented this doesn't need to be carried out
|
||||
schema: {
|
||||
email: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
email: true,
|
||||
length: {
|
||||
maximum: "",
|
||||
|
@ -99,34 +92,34 @@ export const USERS_TABLE_SCHEMA: Table = {
|
|||
},
|
||||
firstName: {
|
||||
name: "firstName",
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
lastName: {
|
||||
name: "lastName",
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
roleId: {
|
||||
name: "roleId",
|
||||
type: FieldTypes.OPTIONS,
|
||||
type: FieldType.OPTIONS,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
presence: false,
|
||||
inclusion: Object.values(roles.BUILTIN_ROLE_IDS),
|
||||
},
|
||||
},
|
||||
status: {
|
||||
name: "status",
|
||||
type: FieldTypes.OPTIONS,
|
||||
type: FieldType.OPTIONS,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
presence: false,
|
||||
inclusion: Object.values(constants.UserStatus),
|
||||
},
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import {
|
||||
AutoFieldSubTypes,
|
||||
FieldTypes,
|
||||
DEFAULT_BB_DATASOURCE_ID,
|
||||
DEFAULT_INVENTORY_TABLE_ID,
|
||||
DEFAULT_EMPLOYEE_TABLE_ID,
|
||||
|
@ -16,6 +14,7 @@ import { jobsImport } from "./jobsImport"
|
|||
import { expensesImport } from "./expensesImport"
|
||||
import { db as dbCore } from "@budibase/backend-core"
|
||||
import {
|
||||
AutoFieldSubType,
|
||||
FieldType,
|
||||
RelationshipType,
|
||||
Row,
|
||||
|
@ -40,7 +39,7 @@ function syncLastIds(table: Table, rowCount: number) {
|
|||
if (
|
||||
entry.autocolumn &&
|
||||
entry.type === FieldType.NUMBER &&
|
||||
entry.subtype == AutoFieldSubTypes.AUTO_ID
|
||||
entry.subtype == AutoFieldSubType.AUTO_ID
|
||||
) {
|
||||
entry.lastID = rowCount
|
||||
}
|
||||
|
@ -58,12 +57,12 @@ async function tableImport(table: Table, data: Row[]) {
|
|||
const AUTO_COLUMNS: TableSchema = {
|
||||
"Created At": {
|
||||
name: "Created At",
|
||||
type: FieldTypes.DATETIME,
|
||||
subtype: AutoFieldSubTypes.CREATED_AT,
|
||||
type: FieldType.DATETIME,
|
||||
subtype: AutoFieldSubType.CREATED_AT,
|
||||
icon: "ri-magic-line",
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
datetime: {
|
||||
|
@ -74,12 +73,12 @@ const AUTO_COLUMNS: TableSchema = {
|
|||
},
|
||||
"Updated At": {
|
||||
name: "Updated At",
|
||||
type: FieldTypes.DATETIME,
|
||||
subtype: AutoFieldSubTypes.UPDATED_AT,
|
||||
type: FieldType.DATETIME,
|
||||
subtype: AutoFieldSubType.UPDATED_AT,
|
||||
icon: "ri-magic-line",
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
datetime: {
|
||||
|
@ -101,12 +100,12 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
|
|||
schema: {
|
||||
"Item ID": {
|
||||
name: "Item ID",
|
||||
type: FieldTypes.NUMBER,
|
||||
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||
type: FieldType.NUMBER,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
icon: "ri-magic-line",
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
type: FieldTypes.NUMBER,
|
||||
type: FieldType.NUMBER,
|
||||
presence: false,
|
||||
numericality: {
|
||||
greaterThanOrEqualTo: "",
|
||||
|
@ -115,9 +114,9 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
|
|||
},
|
||||
},
|
||||
"Item Name": {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {
|
||||
maximum: null,
|
||||
},
|
||||
|
@ -128,9 +127,9 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
|
|||
name: "Item Name",
|
||||
},
|
||||
"Item Tags": {
|
||||
type: FieldTypes.ARRAY,
|
||||
type: FieldType.ARRAY,
|
||||
constraints: {
|
||||
type: FieldTypes.ARRAY,
|
||||
type: FieldType.ARRAY,
|
||||
presence: {
|
||||
allowEmpty: false,
|
||||
},
|
||||
|
@ -140,9 +139,9 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
|
|||
sortable: false,
|
||||
},
|
||||
Notes: {
|
||||
type: FieldTypes.LONGFORM,
|
||||
type: FieldType.LONGFORM,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
},
|
||||
|
@ -150,9 +149,9 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
|
|||
useRichText: null,
|
||||
},
|
||||
Status: {
|
||||
type: FieldTypes.ARRAY,
|
||||
type: FieldType.ARRAY,
|
||||
constraints: {
|
||||
type: FieldTypes.ARRAY,
|
||||
type: FieldType.ARRAY,
|
||||
presence: {
|
||||
allowEmpty: false,
|
||||
},
|
||||
|
@ -162,18 +161,18 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
|
|||
sortable: false,
|
||||
},
|
||||
SKU: {
|
||||
type: FieldTypes.BARCODEQR,
|
||||
type: FieldType.BARCODEQR,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
},
|
||||
name: "SKU",
|
||||
},
|
||||
"Purchase Date": {
|
||||
type: FieldTypes.DATETIME,
|
||||
type: FieldType.DATETIME,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
datetime: {
|
||||
|
@ -185,9 +184,9 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
|
|||
ignoreTimezones: true,
|
||||
},
|
||||
"Purchase Price": {
|
||||
type: FieldTypes.NUMBER,
|
||||
type: FieldType.NUMBER,
|
||||
constraints: {
|
||||
type: FieldTypes.NUMBER,
|
||||
type: FieldType.NUMBER,
|
||||
presence: false,
|
||||
numericality: {
|
||||
greaterThanOrEqualTo: null,
|
||||
|
@ -211,75 +210,75 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
|
|||
schema: {
|
||||
"First Name": {
|
||||
name: "First Name",
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
"Last Name": {
|
||||
name: "Last Name",
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
Email: {
|
||||
name: "Email",
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
Address: {
|
||||
name: "Address",
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
City: {
|
||||
name: "City",
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
Postcode: {
|
||||
name: "Postcode",
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
Phone: {
|
||||
name: "Phone",
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
"EMPLOYEE ID": {
|
||||
name: "EMPLOYEE ID",
|
||||
type: FieldTypes.NUMBER,
|
||||
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||
type: FieldType.NUMBER,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
icon: "ri-magic-line",
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
type: FieldTypes.NUMBER,
|
||||
type: FieldType.NUMBER,
|
||||
presence: false,
|
||||
numericality: {
|
||||
greaterThanOrEqualTo: "",
|
||||
|
@ -288,9 +287,9 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
|
|||
},
|
||||
},
|
||||
"Employee Level": {
|
||||
type: FieldTypes.ARRAY,
|
||||
type: FieldType.ARRAY,
|
||||
constraints: {
|
||||
type: FieldTypes.ARRAY,
|
||||
type: FieldType.ARRAY,
|
||||
presence: false,
|
||||
inclusion: ["Manager", "Junior", "Senior", "Apprentice", "Contractor"],
|
||||
},
|
||||
|
@ -298,18 +297,18 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
|
|||
sortable: false,
|
||||
},
|
||||
"Badge Photo": {
|
||||
type: FieldTypes.ATTACHMENT,
|
||||
type: FieldType.ATTACHMENT,
|
||||
constraints: {
|
||||
type: FieldTypes.ARRAY,
|
||||
type: FieldType.ARRAY,
|
||||
presence: false,
|
||||
},
|
||||
name: "Badge Photo",
|
||||
sortable: false,
|
||||
},
|
||||
Jobs: {
|
||||
type: FieldTypes.LINK,
|
||||
type: FieldType.LINK,
|
||||
constraints: {
|
||||
type: FieldTypes.ARRAY,
|
||||
type: FieldType.ARRAY,
|
||||
presence: false,
|
||||
},
|
||||
fieldName: "Assigned",
|
||||
|
@ -318,9 +317,9 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
|
|||
tableId: DEFAULT_JOBS_TABLE_ID,
|
||||
},
|
||||
"Start Date": {
|
||||
type: FieldTypes.DATETIME,
|
||||
type: FieldType.DATETIME,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
datetime: {
|
||||
|
@ -332,9 +331,9 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
|
|||
ignoreTimezones: true,
|
||||
},
|
||||
"End Date": {
|
||||
type: FieldTypes.DATETIME,
|
||||
type: FieldType.DATETIME,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
datetime: {
|
||||
|
@ -359,12 +358,12 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
|
|||
schema: {
|
||||
"Job ID": {
|
||||
name: "Job ID",
|
||||
type: FieldTypes.NUMBER,
|
||||
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||
type: FieldType.NUMBER,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
icon: "ri-magic-line",
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
type: FieldTypes.NUMBER,
|
||||
type: FieldType.NUMBER,
|
||||
presence: false,
|
||||
numericality: {
|
||||
greaterThanOrEqualTo: "",
|
||||
|
@ -373,9 +372,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
|
|||
},
|
||||
},
|
||||
"Quote Date": {
|
||||
type: FieldTypes.DATETIME,
|
||||
type: FieldType.DATETIME,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: {
|
||||
allowEmpty: false,
|
||||
|
@ -389,9 +388,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
|
|||
ignoreTimezones: true,
|
||||
},
|
||||
"Quote Price": {
|
||||
type: FieldTypes.NUMBER,
|
||||
type: FieldType.NUMBER,
|
||||
constraints: {
|
||||
type: FieldTypes.NUMBER,
|
||||
type: FieldType.NUMBER,
|
||||
presence: {
|
||||
allowEmpty: false,
|
||||
},
|
||||
|
@ -403,9 +402,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
|
|||
name: "Quote Price",
|
||||
},
|
||||
"Works Start": {
|
||||
type: FieldTypes.DATETIME,
|
||||
type: FieldType.DATETIME,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
datetime: {
|
||||
|
@ -417,9 +416,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
|
|||
ignoreTimezones: true,
|
||||
},
|
||||
Address: {
|
||||
type: FieldTypes.LONGFORM,
|
||||
type: FieldType.LONGFORM,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
},
|
||||
|
@ -427,9 +426,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
|
|||
useRichText: null,
|
||||
},
|
||||
"Customer Name": {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {
|
||||
maximum: null,
|
||||
},
|
||||
|
@ -438,9 +437,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
|
|||
name: "Customer Name",
|
||||
},
|
||||
Notes: {
|
||||
type: FieldTypes.LONGFORM,
|
||||
type: FieldType.LONGFORM,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
},
|
||||
|
@ -448,9 +447,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
|
|||
useRichText: null,
|
||||
},
|
||||
"Customer Phone": {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {
|
||||
maximum: null,
|
||||
},
|
||||
|
@ -459,9 +458,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
|
|||
name: "Customer Phone",
|
||||
},
|
||||
"Customer Email": {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {
|
||||
maximum: null,
|
||||
},
|
||||
|
@ -471,14 +470,14 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
|
|||
},
|
||||
Assigned: {
|
||||
name: "Assigned",
|
||||
type: FieldTypes.LINK,
|
||||
type: FieldType.LINK,
|
||||
tableId: DEFAULT_EMPLOYEE_TABLE_ID,
|
||||
fieldName: "Jobs",
|
||||
relationshipType: RelationshipType.MANY_TO_MANY,
|
||||
// sortable: true,
|
||||
},
|
||||
"Works End": {
|
||||
type: FieldTypes.DATETIME,
|
||||
type: FieldType.DATETIME,
|
||||
constraints: {
|
||||
type: "string",
|
||||
length: {},
|
||||
|
@ -492,7 +491,7 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
|
|||
ignoreTimezones: true,
|
||||
},
|
||||
"Updated Price": {
|
||||
type: FieldTypes.NUMBER,
|
||||
type: FieldType.NUMBER,
|
||||
constraints: {
|
||||
type: "number",
|
||||
presence: false,
|
||||
|
@ -518,12 +517,12 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
|
|||
schema: {
|
||||
"Expense ID": {
|
||||
name: "Expense ID",
|
||||
type: FieldTypes.NUMBER,
|
||||
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||
type: FieldType.NUMBER,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
icon: "ri-magic-line",
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
type: FieldTypes.NUMBER,
|
||||
type: FieldType.NUMBER,
|
||||
presence: false,
|
||||
numericality: {
|
||||
greaterThanOrEqualTo: "",
|
||||
|
@ -532,9 +531,9 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
|
|||
},
|
||||
},
|
||||
"Expense Tags": {
|
||||
type: FieldTypes.ARRAY,
|
||||
type: FieldType.ARRAY,
|
||||
constraints: {
|
||||
type: FieldTypes.ARRAY,
|
||||
type: FieldType.ARRAY,
|
||||
presence: {
|
||||
allowEmpty: false,
|
||||
},
|
||||
|
@ -554,9 +553,9 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
|
|||
sortable: false,
|
||||
},
|
||||
Cost: {
|
||||
type: FieldTypes.NUMBER,
|
||||
type: FieldType.NUMBER,
|
||||
constraints: {
|
||||
type: FieldTypes.NUMBER,
|
||||
type: FieldType.NUMBER,
|
||||
presence: {
|
||||
allowEmpty: false,
|
||||
},
|
||||
|
@ -568,9 +567,9 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
|
|||
name: "Cost",
|
||||
},
|
||||
Notes: {
|
||||
type: FieldTypes.LONGFORM,
|
||||
type: FieldType.LONGFORM,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
},
|
||||
|
@ -578,9 +577,9 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
|
|||
useRichText: null,
|
||||
},
|
||||
"Payment Due": {
|
||||
type: FieldTypes.DATETIME,
|
||||
type: FieldType.DATETIME,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
datetime: {
|
||||
|
@ -592,9 +591,9 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
|
|||
ignoreTimezones: true,
|
||||
},
|
||||
"Date Paid": {
|
||||
type: FieldTypes.DATETIME,
|
||||
type: FieldType.DATETIME,
|
||||
constraints: {
|
||||
type: FieldTypes.STRING,
|
||||
type: FieldType.STRING,
|
||||
length: {},
|
||||
presence: false,
|
||||
datetime: {
|
||||
|
@ -606,9 +605,9 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
|
|||
ignoreTimezones: true,
|
||||
},
|
||||
Attachment: {
|
||||
type: FieldTypes.ATTACHMENT,
|
||||
type: FieldType.ATTACHMENT,
|
||||
constraints: {
|
||||
type: FieldTypes.ARRAY,
|
||||
type: FieldType.ARRAY,
|
||||
presence: false,
|
||||
},
|
||||
name: "Attachment",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { IncludeDocs, getLinkDocuments } from "./linkUtils"
|
||||
import { InternalTables, getUserMetadataParams } from "../utils"
|
||||
import { FieldTypes } from "../../constants"
|
||||
import { context, logging } from "@budibase/backend-core"
|
||||
import LinkDocument from "./LinkDocument"
|
||||
import {
|
||||
|
@ -62,7 +61,7 @@ class LinkController {
|
|||
}
|
||||
for (let fieldName of Object.keys(table.schema)) {
|
||||
const { type } = table.schema[fieldName]
|
||||
if (type === FieldTypes.LINK) {
|
||||
if (type === FieldType.LINK) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -96,7 +95,7 @@ class LinkController {
|
|||
validateTable(table: Table) {
|
||||
const usedAlready = []
|
||||
for (let schema of Object.values(table.schema)) {
|
||||
if (schema.type !== FieldTypes.LINK) {
|
||||
if (schema.type !== FieldType.LINK) {
|
||||
continue
|
||||
}
|
||||
const unique = schema.tableId! + schema?.fieldName
|
||||
|
@ -172,7 +171,7 @@ class LinkController {
|
|||
// get the links this row wants to make
|
||||
const rowField = row[fieldName]
|
||||
const field = table.schema[fieldName]
|
||||
if (field.type === FieldTypes.LINK && rowField != null) {
|
||||
if (field.type === FieldType.LINK && rowField != null) {
|
||||
// check which links actual pertain to the update in this row
|
||||
const thisFieldLinkDocs = linkDocs.filter(
|
||||
linkDoc =>
|
||||
|
@ -353,7 +352,7 @@ class LinkController {
|
|||
const schema = table.schema
|
||||
for (let fieldName of Object.keys(schema)) {
|
||||
const field = schema[fieldName]
|
||||
if (field.type === FieldTypes.LINK && field.fieldName) {
|
||||
if (field.type === FieldType.LINK && field.fieldName) {
|
||||
// handle this in a separate try catch, want
|
||||
// the put to bubble up as an error, if can't update
|
||||
// table for some reason
|
||||
|
@ -366,7 +365,7 @@ class LinkController {
|
|||
}
|
||||
const fields = this.handleRelationshipType(field, {
|
||||
name: field.fieldName,
|
||||
type: FieldTypes.LINK,
|
||||
type: FieldType.LINK,
|
||||
// these are the props of the table that initiated the link
|
||||
tableId: table._id!,
|
||||
fieldName: fieldName,
|
||||
|
@ -413,10 +412,7 @@ class LinkController {
|
|||
for (let fieldName of Object.keys(oldTable?.schema || {})) {
|
||||
const field = oldTable?.schema[fieldName] as FieldSchema
|
||||
// this field has been removed from the table schema
|
||||
if (
|
||||
field.type === FieldTypes.LINK &&
|
||||
newTable.schema[fieldName] == null
|
||||
) {
|
||||
if (field.type === FieldType.LINK && newTable.schema[fieldName] == null) {
|
||||
await this.removeFieldFromTable(fieldName)
|
||||
}
|
||||
}
|
||||
|
@ -437,7 +433,7 @@ class LinkController {
|
|||
for (let fieldName of Object.keys(schema)) {
|
||||
const field = schema[fieldName]
|
||||
try {
|
||||
if (field.type === FieldTypes.LINK && field.fieldName) {
|
||||
if (field.type === FieldType.LINK && field.fieldName) {
|
||||
const linkedTable = await this._db.get<Table>(field.tableId)
|
||||
delete linkedTable.schema[field.fieldName]
|
||||
field.tableRev = (await this._db.put(linkedTable)).rev
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { generateLinkID } from "../utils"
|
||||
import { FieldTypes } from "../../constants"
|
||||
import { LinkDocument } from "@budibase/types"
|
||||
import { FieldType, LinkDocument } from "@budibase/types"
|
||||
|
||||
/**
|
||||
* Creates a new link document structure which can be put to the database. It is important to
|
||||
|
@ -43,7 +42,7 @@ class LinkDocumentImpl implements LinkDocument {
|
|||
fieldName1,
|
||||
fieldName2
|
||||
)
|
||||
this.type = FieldTypes.LINK
|
||||
this.type = FieldType.LINK
|
||||
this.doc1 = {
|
||||
tableId: tableId1,
|
||||
fieldName: fieldName1,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { ViewName, getQueryIndex, isRelationshipColumn } from "../utils"
|
||||
import { FieldTypes } from "../../constants"
|
||||
import { createLinkView } from "../views/staticViews"
|
||||
import { context, logging } from "@budibase/backend-core"
|
||||
import {
|
||||
FieldType,
|
||||
DatabaseQueryOpts,
|
||||
LinkDocument,
|
||||
LinkDocumentValue,
|
||||
|
@ -131,11 +131,11 @@ export async function getLinkedTable(id: string, tables: Table[]) {
|
|||
export function getRelatedTableForField(table: Table, fieldName: string) {
|
||||
// look to see if its on the table, straight in the schema
|
||||
const field = table.schema[fieldName]
|
||||
if (field?.type === FieldTypes.LINK) {
|
||||
if (field?.type === FieldType.LINK) {
|
||||
return field.tableId
|
||||
}
|
||||
for (let column of Object.values(table.schema)) {
|
||||
if (column.type === FieldTypes.LINK && column.fieldName === fieldName) {
|
||||
if (column.type === FieldType.LINK && column.fieldName === fieldName) {
|
||||
return column.tableId
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,57 @@
|
|||
const TestConfig = require("../../tests/utilities/TestConfiguration")
|
||||
const {
|
||||
basicRow,
|
||||
import TestConfig from "../../tests/utilities/TestConfiguration"
|
||||
import {
|
||||
basicLinkedRow,
|
||||
basicRow,
|
||||
basicTable,
|
||||
} = require("../../tests/utilities/structures")
|
||||
const LinkController = require("../linkedRows/LinkController").default
|
||||
const { context } = require("@budibase/backend-core")
|
||||
const { RelationshipType } = require("../../constants")
|
||||
const { cloneDeep } = require("lodash/fp")
|
||||
} from "../../tests/utilities/structures"
|
||||
import LinkController from "../linkedRows/LinkController"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import {
|
||||
FieldType,
|
||||
ManyToManyRelationshipFieldMetadata,
|
||||
ManyToOneRelationshipFieldMetadata,
|
||||
OneToManyRelationshipFieldMetadata,
|
||||
RelationshipFieldMetadata,
|
||||
RelationshipType,
|
||||
Row,
|
||||
Table,
|
||||
} from "@budibase/types"
|
||||
import { cloneDeep } from "lodash"
|
||||
|
||||
const baseColumn = {
|
||||
type: FieldType.LINK,
|
||||
fieldName: "",
|
||||
tableId: "",
|
||||
name: "",
|
||||
}
|
||||
|
||||
function mockManyToManyColumn(): ManyToManyRelationshipFieldMetadata {
|
||||
return <ManyToManyRelationshipFieldMetadata>{
|
||||
...baseColumn,
|
||||
through: "",
|
||||
throughFrom: "",
|
||||
throughTo: "",
|
||||
relationshipType: RelationshipType.MANY_TO_MANY,
|
||||
}
|
||||
}
|
||||
|
||||
function mockManyToOneColumn(): ManyToOneRelationshipFieldMetadata {
|
||||
return <ManyToOneRelationshipFieldMetadata>{
|
||||
...baseColumn,
|
||||
relationshipType: RelationshipType.MANY_TO_ONE,
|
||||
}
|
||||
}
|
||||
|
||||
function mockOneToManyColumn(): OneToManyRelationshipFieldMetadata {
|
||||
return <OneToManyRelationshipFieldMetadata>{
|
||||
...baseColumn,
|
||||
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||
}
|
||||
}
|
||||
|
||||
describe("test the link controller", () => {
|
||||
let config = new TestConfig()
|
||||
let table1, table2, appId
|
||||
let table1: Table, table2: Table, appId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
const app = await config.init()
|
||||
|
@ -30,9 +70,18 @@ describe("test the link controller", () => {
|
|||
|
||||
afterAll(config.end)
|
||||
|
||||
async function createLinkController(table, row = null, oldTable = null) {
|
||||
async function createLinkController(
|
||||
table: Table,
|
||||
row?: Row,
|
||||
oldTable?: Table
|
||||
) {
|
||||
return context.doInAppContext(appId, () => {
|
||||
const linkConfig = {
|
||||
const linkConfig: {
|
||||
tableId?: string
|
||||
table: Table
|
||||
row?: Row
|
||||
oldTable?: Table
|
||||
} = {
|
||||
tableId: table._id,
|
||||
table,
|
||||
}
|
||||
|
@ -47,11 +96,11 @@ describe("test the link controller", () => {
|
|||
}
|
||||
|
||||
async function createLinkedRow(linkField = "link", t1 = table1, t2 = table2) {
|
||||
const row = await config.createRow(basicRow(t2._id))
|
||||
const row = await config.createRow(basicRow(t2._id!))
|
||||
const { _id } = await config.createRow(
|
||||
basicLinkedRow(t1._id, row._id, linkField)
|
||||
basicLinkedRow(t1._id!, row._id!, linkField)
|
||||
)
|
||||
return config.getRow(t1._id, _id)
|
||||
return config.getRow(t1._id!, _id!)
|
||||
}
|
||||
|
||||
it("should be able to confirm if two table schemas are equal", async () => {
|
||||
|
@ -71,6 +120,7 @@ describe("test the link controller", () => {
|
|||
it("should be able to check the relationship types across two fields", async () => {
|
||||
const controller = await createLinkController(table1)
|
||||
// empty case
|
||||
//@ts-ignore
|
||||
let output = controller.handleRelationshipType({}, {})
|
||||
expect(output.linkedField.relationshipType).toEqual(
|
||||
RelationshipType.MANY_TO_MANY
|
||||
|
@ -79,8 +129,8 @@ describe("test the link controller", () => {
|
|||
RelationshipType.MANY_TO_MANY
|
||||
)
|
||||
output = controller.handleRelationshipType(
|
||||
{ relationshipType: RelationshipType.MANY_TO_MANY },
|
||||
{}
|
||||
mockManyToManyColumn(),
|
||||
{} as any
|
||||
)
|
||||
expect(output.linkedField.relationshipType).toEqual(
|
||||
RelationshipType.MANY_TO_MANY
|
||||
|
@ -88,20 +138,14 @@ describe("test the link controller", () => {
|
|||
expect(output.linkerField.relationshipType).toEqual(
|
||||
RelationshipType.MANY_TO_MANY
|
||||
)
|
||||
output = controller.handleRelationshipType(
|
||||
{ relationshipType: RelationshipType.MANY_TO_ONE },
|
||||
{}
|
||||
)
|
||||
output = controller.handleRelationshipType(mockManyToOneColumn(), {} as any)
|
||||
expect(output.linkedField.relationshipType).toEqual(
|
||||
RelationshipType.ONE_TO_MANY
|
||||
)
|
||||
expect(output.linkerField.relationshipType).toEqual(
|
||||
RelationshipType.MANY_TO_ONE
|
||||
)
|
||||
output = controller.handleRelationshipType(
|
||||
{ relationshipType: RelationshipType.ONE_TO_MANY },
|
||||
{}
|
||||
)
|
||||
output = controller.handleRelationshipType(mockOneToManyColumn(), {} as any)
|
||||
expect(output.linkedField.relationshipType).toEqual(
|
||||
RelationshipType.MANY_TO_ONE
|
||||
)
|
||||
|
@ -115,16 +159,16 @@ describe("test the link controller", () => {
|
|||
const controller = await createLinkController(table1, row)
|
||||
await context.doInAppContext(appId, async () => {
|
||||
// get initial count
|
||||
const beforeLinks = await controller.getRowLinkDocs(row._id)
|
||||
const beforeLinks = await controller.getRowLinkDocs(row._id!)
|
||||
await controller.rowDeleted()
|
||||
let afterLinks = await controller.getRowLinkDocs(row._id)
|
||||
let afterLinks = await controller.getRowLinkDocs(row._id!)
|
||||
expect(beforeLinks.length).toEqual(1)
|
||||
expect(afterLinks.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
it("shouldn't throw an error when deleting a row with no links", async () => {
|
||||
const row = await config.createRow(basicRow(table1._id))
|
||||
const row = await config.createRow(basicRow(table1._id!))
|
||||
const controller = await createLinkController(table1, row)
|
||||
await context.doInAppContext(appId, async () => {
|
||||
let error
|
||||
|
@ -142,12 +186,13 @@ describe("test the link controller", () => {
|
|||
const copyTable = {
|
||||
...table1,
|
||||
}
|
||||
//@ts-ignore
|
||||
copyTable.schema.otherTableLink = {
|
||||
type: "link",
|
||||
type: FieldType.LINK,
|
||||
fieldName: "link",
|
||||
tableId: table2._id,
|
||||
tableId: table2._id!,
|
||||
}
|
||||
let error
|
||||
let error: any
|
||||
try {
|
||||
controller.validateTable(copyTable)
|
||||
} catch (err) {
|
||||
|
@ -166,7 +211,7 @@ describe("test the link controller", () => {
|
|||
const controller = await createLinkController(table1, row)
|
||||
await context.doInAppContext(appId, async () => {
|
||||
await controller.rowSaved()
|
||||
let links = await controller.getRowLinkDocs(row._id)
|
||||
let links = await controller.getRowLinkDocs(row._id!)
|
||||
expect(links.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
@ -186,7 +231,7 @@ describe("test the link controller", () => {
|
|||
it("should be able to remove a linked field from a table", async () => {
|
||||
await createLinkedRow()
|
||||
await createLinkedRow("link2")
|
||||
const controller = await createLinkController(table1, null, table1)
|
||||
const controller = await createLinkController(table1, undefined, table1)
|
||||
await context.doInAppContext(appId, async () => {
|
||||
let before = await controller.getTableLinkDocs()
|
||||
await controller.removeFieldFromTable("link")
|
||||
|
@ -199,7 +244,8 @@ describe("test the link controller", () => {
|
|||
|
||||
it("should throw an error when overwriting a link column", async () => {
|
||||
const update = cloneDeep(table1)
|
||||
update.schema.link.relationshipType = RelationshipType.MANY_TO_ONE
|
||||
const linkSchema = update.schema.link as ManyToOneRelationshipFieldMetadata
|
||||
linkSchema.relationshipType = RelationshipType.MANY_TO_ONE
|
||||
let error
|
||||
try {
|
||||
const controller = await createLinkController(update)
|
||||
|
@ -215,7 +261,7 @@ describe("test the link controller", () => {
|
|||
await createLinkedRow()
|
||||
const newTable = cloneDeep(table1)
|
||||
delete newTable.schema.link
|
||||
const controller = await createLinkController(newTable, null, table1)
|
||||
const controller = await createLinkController(newTable, undefined, table1)
|
||||
await context.doInAppContext(appId, async () => {
|
||||
await controller.tableUpdated()
|
||||
const links = await controller.getTableLinkDocs()
|
||||
|
@ -235,7 +281,7 @@ describe("test the link controller", () => {
|
|||
let error
|
||||
try {
|
||||
// create another row to initiate the error
|
||||
await config.createRow(basicLinkedRow(row.tableId, row.link[0]))
|
||||
await config.createRow(basicLinkedRow(row.tableId!, row.link[0]))
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
|
@ -245,7 +291,7 @@ describe("test the link controller", () => {
|
|||
it("should not error if a link being created doesn't exist", async () => {
|
||||
let error
|
||||
try {
|
||||
await config.createRow(basicLinkedRow(table1._id, "invalid"))
|
||||
await config.createRow(basicLinkedRow(table1._id!, "invalid"))
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
|
@ -255,10 +301,11 @@ describe("test the link controller", () => {
|
|||
it("make sure auto column goes onto other row too", async () => {
|
||||
const table = await config.createTable()
|
||||
const tableCfg = basicTable()
|
||||
//@ts-ignore
|
||||
tableCfg.schema.link = {
|
||||
type: "link",
|
||||
type: FieldType.LINK,
|
||||
fieldName: "link",
|
||||
tableId: table._id,
|
||||
tableId: table._id!,
|
||||
name: "link",
|
||||
autocolumn: true,
|
||||
}
|
||||
|
@ -269,10 +316,11 @@ describe("test the link controller", () => {
|
|||
|
||||
it("should be able to link to self", async () => {
|
||||
const table = await config.createTable()
|
||||
//@ts-ignore
|
||||
table.schema.link = {
|
||||
type: "link",
|
||||
type: FieldType.LINK,
|
||||
fieldName: "link",
|
||||
tableId: table._id,
|
||||
tableId: table._id!,
|
||||
name: "link",
|
||||
autocolumn: true,
|
||||
}
|
||||
|
@ -282,8 +330,9 @@ describe("test the link controller", () => {
|
|||
it("should be able to remove a linked field from a table, even if the linked table does not exist", async () => {
|
||||
await createLinkedRow()
|
||||
await createLinkedRow("link2")
|
||||
table1.schema["link"].tableId = "not_found"
|
||||
const controller = await createLinkController(table1, null, table1)
|
||||
const linkSchema = table1.schema["link"] as RelationshipFieldMetadata
|
||||
linkSchema.tableId = "not_found"
|
||||
const controller = await createLinkController(table1, undefined, table1)
|
||||
await context.doInAppContext(appId, async () => {
|
||||
let before = await controller.getTableLinkDocs()
|
||||
await controller.removeFieldFromTable("link")
|
|
@ -1,14 +1,15 @@
|
|||
const TestConfig = require("../../tests/utilities/TestConfiguration")
|
||||
const { basicTable } = require("../../tests/utilities/structures")
|
||||
const linkUtils = require("../linkedRows/linkUtils")
|
||||
const { context } = require("@budibase/backend-core")
|
||||
import TestConfig from "../../tests/utilities/TestConfiguration"
|
||||
import { basicTable } from "../../tests/utilities/structures"
|
||||
import * as linkUtils from "../linkedRows/linkUtils"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import { FieldType, RelationshipType, Table } from "@budibase/types"
|
||||
|
||||
describe("test link functionality", () => {
|
||||
const config = new TestConfig()
|
||||
let appId
|
||||
let appId: string
|
||||
|
||||
describe("getLinkedTable", () => {
|
||||
let table
|
||||
let table: Table
|
||||
beforeAll(async () => {
|
||||
const app = await config.init()
|
||||
appId = app.appId
|
||||
|
@ -17,15 +18,15 @@ describe("test link functionality", () => {
|
|||
|
||||
it("should be able to retrieve a linked table from a list", async () => {
|
||||
await context.doInAppContext(appId, async () => {
|
||||
const retrieved = await linkUtils.getLinkedTable(table._id, [table])
|
||||
const retrieved = await linkUtils.getLinkedTable(table._id!, [table])
|
||||
expect(retrieved._id).toBe(table._id)
|
||||
})
|
||||
})
|
||||
|
||||
it("should be able to retrieve a table from DB and update list", async () => {
|
||||
const tables = []
|
||||
const tables: Table[] = []
|
||||
await context.doInAppContext(appId, async () => {
|
||||
const retrieved = await linkUtils.getLinkedTable(table._id, tables)
|
||||
const retrieved = await linkUtils.getLinkedTable(table._id!, tables)
|
||||
expect(retrieved._id).toBe(table._id)
|
||||
expect(tables[0]).toBeDefined()
|
||||
})
|
||||
|
@ -35,9 +36,11 @@ describe("test link functionality", () => {
|
|||
describe("getRelatedTableForField", () => {
|
||||
let link = basicTable()
|
||||
link.schema.link = {
|
||||
name: "link",
|
||||
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||
fieldName: "otherLink",
|
||||
tableId: "tableID",
|
||||
type: "link",
|
||||
type: FieldType.LINK,
|
||||
}
|
||||
|
||||
it("should get the field from the table directly", () => {
|
|
@ -1,6 +1,7 @@
|
|||
import newid from "./newid"
|
||||
import { db as dbCore } from "@budibase/backend-core"
|
||||
import {
|
||||
FieldType,
|
||||
DocumentType,
|
||||
FieldSchema,
|
||||
RelationshipFieldMetadata,
|
||||
|
@ -8,7 +9,6 @@ import {
|
|||
INTERNAL_TABLE_SOURCE_ID,
|
||||
DatabaseQueryOpts,
|
||||
} from "@budibase/types"
|
||||
import { FieldTypes } from "../constants"
|
||||
|
||||
export { DocumentType, VirtualDocumentType } from "@budibase/types"
|
||||
|
||||
|
@ -315,5 +315,5 @@ export function extractViewInfoFromID(viewId: string) {
|
|||
export function isRelationshipColumn(
|
||||
column: FieldSchema
|
||||
): column is RelationshipFieldMetadata {
|
||||
return column.type === FieldTypes.LINK
|
||||
return column.type === FieldType.LINK
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ const environment = {
|
|||
PLUGINS_DIR: process.env.PLUGINS_DIR || "/plugins",
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
MAX_IMPORT_SIZE_MB: process.env.MAX_IMPORT_SIZE_MB,
|
||||
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
|
||||
// flags
|
||||
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
|
||||
DISABLE_THREADING: process.env.DISABLE_THREADING,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Knex, knex } from "knex"
|
||||
import {
|
||||
RelationshipType,
|
||||
FieldSubtype,
|
||||
NumberFieldMetadata,
|
||||
Operation,
|
||||
|
@ -11,7 +12,6 @@ import {
|
|||
import { breakExternalTableId } from "../utils"
|
||||
import SchemaBuilder = Knex.SchemaBuilder
|
||||
import CreateTableBuilder = Knex.CreateTableBuilder
|
||||
import { RelationshipType } from "../../constants"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
|
||||
function isIgnoredType(type: FieldType) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
FieldType,
|
||||
DatasourceFieldType,
|
||||
Integration,
|
||||
Operation,
|
||||
|
@ -21,7 +22,6 @@ import {
|
|||
SqlClient,
|
||||
} from "./utils"
|
||||
import Sql from "./base/sql"
|
||||
import { FieldTypes } from "../constants"
|
||||
import {
|
||||
BindParameters,
|
||||
Connection,
|
||||
|
@ -302,7 +302,7 @@ class OracleIntegration extends Sql implements DatasourcePlus {
|
|||
})
|
||||
|
||||
if (this.isBooleanType(oracleColumn)) {
|
||||
fieldSchema.type = FieldTypes.BOOLEAN
|
||||
fieldSchema.type = FieldType.BOOLEAN
|
||||
}
|
||||
|
||||
table.schema[columnName] = fieldSchema
|
||||
|
|
|
@ -1,27 +1,23 @@
|
|||
import { Datasource, SourceName } from "@budibase/types"
|
||||
import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
|
||||
import env from "../../../environment"
|
||||
|
||||
let container: StartedTestContainer | undefined
|
||||
|
||||
const isMac = process.platform === "darwin"
|
||||
|
||||
export async function getDsConfig(): Promise<Datasource> {
|
||||
try {
|
||||
if (!container) {
|
||||
// postgres 15-bullseye safer bet on Linux
|
||||
const version = isMac ? undefined : "15-bullseye"
|
||||
container = await new GenericContainer("postgres", version)
|
||||
container = await new GenericContainer("postgres:16.1-bullseye")
|
||||
.withExposedPorts(5432)
|
||||
.withEnv("POSTGRES_PASSWORD", "password")
|
||||
.withEnvironment({ POSTGRES_PASSWORD: "password" })
|
||||
.withWaitStrategy(
|
||||
Wait.forLogMessage(
|
||||
"PostgreSQL init process complete; ready for start up."
|
||||
"database system is ready to accept connections",
|
||||
2
|
||||
)
|
||||
)
|
||||
.start()
|
||||
}
|
||||
const host = container.getContainerIpAddress()
|
||||
const host = container.getHost()
|
||||
const port = container.getMappedPort(5432)
|
||||
|
||||
return {
|
||||
|
|
|
@ -376,8 +376,8 @@ export function checkExternalTables(
|
|||
errors[name] = "Table must have a primary key."
|
||||
}
|
||||
|
||||
const schemaFields = Object.keys(table.schema)
|
||||
if (schemaFields.find(f => invalidColumns.includes(f))) {
|
||||
const columnNames = Object.keys(table.schema)
|
||||
if (columnNames.find(f => invalidColumns.includes(f))) {
|
||||
errors[name] = "Table contains invalid columns."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ const checkAuthorized = async (
|
|||
const isCreatorApi = permType === PermissionType.CREATOR
|
||||
const isBuilderApi = permType === PermissionType.BUILDER
|
||||
const isGlobalBuilder = users.isGlobalBuilder(ctx.user)
|
||||
const isCreator = users.isCreator(ctx.user)
|
||||
const isCreator = await users.isCreator(ctx.user)
|
||||
const isBuilder = appId
|
||||
? users.isBuilder(ctx.user, appId)
|
||||
: users.hasBuilderPermissions(ctx.user)
|
||||
|
|
|
@ -3,6 +3,27 @@ import { processStringSync } from "@budibase/string-templates"
|
|||
import { context } from "@budibase/backend-core"
|
||||
import { getQueryParams, isProdAppID } from "../../../db/utils"
|
||||
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
|
||||
function enrichQueries(input: any) {
|
||||
|
@ -25,7 +46,7 @@ export async function find(queryId: string) {
|
|||
delete query.fields
|
||||
delete query.parameters
|
||||
}
|
||||
return query
|
||||
return updateSchema(query)
|
||||
}
|
||||
|
||||
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) {
|
||||
return enrichQueries(queries)
|
||||
} else {
|
||||
return queries
|
||||
queries = await enrichQueries(queries)
|
||||
}
|
||||
return updateSchemas(queries)
|
||||
}
|
||||
|
||||
export async function enrichContext(
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { CouchFindOptions, Table, Row } from "@budibase/types"
|
||||
import { FieldType, CouchFindOptions, Table, Row } from "@budibase/types"
|
||||
import { db as dbCore } from "@budibase/backend-core"
|
||||
import { DocumentType, SEPARATOR } from "../../../db/utils"
|
||||
import { FieldTypes } from "../../../constants"
|
||||
|
||||
// default limit - seems to work well for performance
|
||||
export const FIND_LIMIT = 25
|
||||
|
@ -31,7 +30,7 @@ export async function getRowsWithAttachments(appId: string, table: Table) {
|
|||
const db = dbCore.getDB(appId)
|
||||
const attachmentCols: string[] = []
|
||||
for (let [key, column] of Object.entries(table.schema)) {
|
||||
if (column.type === FieldTypes.ATTACHMENT) {
|
||||
if (column.type === FieldType.ATTACHMENT) {
|
||||
attachmentCols.push(key)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
expectAnyExternalColsAttributes,
|
||||
generator,
|
||||
} from "@budibase/backend-core/tests"
|
||||
import datasource from "../../../../../api/routes/datasource"
|
||||
|
||||
jest.unmock("mysql2/promise")
|
||||
|
||||
|
@ -30,13 +29,15 @@ describe.skip("external", () => {
|
|||
beforeAll(async () => {
|
||||
const container = await new GenericContainer("mysql")
|
||||
.withExposedPorts(3306)
|
||||
.withEnv("MYSQL_ROOT_PASSWORD", "admin")
|
||||
.withEnv("MYSQL_DATABASE", "db")
|
||||
.withEnv("MYSQL_USER", "user")
|
||||
.withEnv("MYSQL_PASSWORD", "password")
|
||||
.withEnvironment({
|
||||
MYSQL_ROOT_PASSWORD: "admin",
|
||||
MYSQL_DATABASE: "db",
|
||||
MYSQL_USER: "user",
|
||||
MYSQL_PASSWORD: "password",
|
||||
})
|
||||
.start()
|
||||
|
||||
const host = container.getContainerIpAddress()
|
||||
const host = container.getHost()
|
||||
const port = container.getMappedPort(3306)
|
||||
|
||||
await config.init()
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
TableSourceType,
|
||||
FieldType,
|
||||
Table,
|
||||
AutoFieldSubTypes,
|
||||
AutoFieldSubType,
|
||||
} from "@budibase/types"
|
||||
|
||||
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
|
||||
|
@ -117,7 +117,7 @@ describe("sdk >> rows >> internal", () => {
|
|||
id: {
|
||||
name: "id",
|
||||
type: FieldType.AUTO,
|
||||
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
autocolumn: true,
|
||||
lastID: 0,
|
||||
},
|
||||
|
@ -181,7 +181,7 @@ describe("sdk >> rows >> internal", () => {
|
|||
id: {
|
||||
name: "id",
|
||||
type: FieldType.AUTO,
|
||||
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||
subtype: AutoFieldSubType.AUTO_ID,
|
||||
autocolumn: true,
|
||||
lastID: 0,
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import cloneDeep from "lodash/cloneDeep"
|
||||
import validateJs from "validate.js"
|
||||
import { QueryJson, Row, Table, TableSchema } from "@budibase/types"
|
||||
import { FieldTypes } from "../../../constants"
|
||||
import { FieldType, QueryJson, Row, Table, TableSchema } from "@budibase/types"
|
||||
import { makeExternalQuery } from "../../../integrations/base/query"
|
||||
import { Format } from "../../../api/controllers/view/exporters"
|
||||
import sdk from "../.."
|
||||
|
@ -22,7 +21,7 @@ export function cleanExportRows(
|
|||
let cleanRows = [...rows]
|
||||
|
||||
const relationships = Object.entries(schema)
|
||||
.filter((entry: any[]) => entry[1].type === FieldTypes.LINK)
|
||||
.filter((entry: any[]) => entry[1].type === FieldType.LINK)
|
||||
.map(entry => entry[0])
|
||||
|
||||
relationships.forEach(column => {
|
||||
|
@ -88,17 +87,17 @@ export async function validate({
|
|||
continue
|
||||
}
|
||||
// formulas shouldn't validated, data will be deleted anyway
|
||||
if (type === FieldTypes.FORMULA || column.autocolumn) {
|
||||
if (type === FieldType.FORMULA || column.autocolumn) {
|
||||
continue
|
||||
}
|
||||
// special case for options, need to always allow unselected (empty)
|
||||
if (type === FieldTypes.OPTIONS && constraints?.inclusion) {
|
||||
if (type === FieldType.OPTIONS && constraints?.inclusion) {
|
||||
constraints.inclusion.push(null as any, "")
|
||||
}
|
||||
let res
|
||||
|
||||
// Validate.js doesn't seem to handle array
|
||||
if (type === FieldTypes.ARRAY && row[fieldName]) {
|
||||
if (type === FieldType.ARRAY && row[fieldName]) {
|
||||
if (row[fieldName].length) {
|
||||
if (!Array.isArray(row[fieldName])) {
|
||||
row[fieldName] = row[fieldName].split(",")
|
||||
|
@ -116,13 +115,13 @@ export async function validate({
|
|||
errors[fieldName] = [`${fieldName} is required`]
|
||||
}
|
||||
} else if (
|
||||
(type === FieldTypes.ATTACHMENT || type === FieldTypes.JSON) &&
|
||||
(type === FieldType.ATTACHMENT || type === FieldType.JSON) &&
|
||||
typeof row[fieldName] === "string"
|
||||
) {
|
||||
// this should only happen if there is an error
|
||||
try {
|
||||
const json = JSON.parse(row[fieldName])
|
||||
if (type === FieldTypes.ATTACHMENT) {
|
||||
if (type === FieldType.ATTACHMENT) {
|
||||
if (Array.isArray(json)) {
|
||||
row[fieldName] = json
|
||||
} else {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
FieldType,
|
||||
Operation,
|
||||
RelationshipType,
|
||||
RenameColumn,
|
||||
|
@ -14,7 +15,6 @@ import {
|
|||
setStaticSchemas,
|
||||
} from "../../../../api/controllers/table/utils"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { FieldTypes } from "../../../../constants"
|
||||
import { makeTableRequest } from "../../../../api/controllers/table/ExternalRequest"
|
||||
import {
|
||||
isRelationshipSetup,
|
||||
|
@ -78,7 +78,7 @@ export async function save(
|
|||
|
||||
// check if relations need setup
|
||||
for (let schema of Object.values(tableToSave.schema)) {
|
||||
if (schema.type !== FieldTypes.LINK || isRelationshipSetup(schema)) {
|
||||
if (schema.type !== FieldType.LINK || isRelationshipSetup(schema)) {
|
||||
continue
|
||||
}
|
||||
const schemaTableId = schema.tableId
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
Table,
|
||||
TableSourceType,
|
||||
} from "@budibase/types"
|
||||
import { FieldTypes } from "../../../../constants"
|
||||
import {
|
||||
foreignKeyStructure,
|
||||
generateForeignKey,
|
||||
|
@ -27,7 +26,7 @@ export function cleanupRelationships(
|
|||
// clean up relationships in couch table schemas
|
||||
for (let [key, schema] of Object.entries(tableToIterate.schema)) {
|
||||
if (
|
||||
schema.type === FieldTypes.LINK &&
|
||||
schema.type === FieldType.LINK &&
|
||||
(!oldTable || table.schema[key] == null)
|
||||
) {
|
||||
const schemaTableId = schema.tableId
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
FieldType,
|
||||
RenameColumn,
|
||||
Table,
|
||||
ViewStatisticsSchema,
|
||||
|
@ -10,7 +11,6 @@ import {
|
|||
hasTypeChanged,
|
||||
TableSaveFunctions,
|
||||
} from "../../../../api/controllers/table/utils"
|
||||
import { FieldTypes } from "../../../../constants"
|
||||
import { EventType, updateLinks } from "../../../../db/linkedRows"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import isEqual from "lodash/isEqual"
|
||||
|
@ -63,7 +63,7 @@ export async function save(
|
|||
}
|
||||
|
||||
// rename row fields when table column is renamed
|
||||
if (renaming && table.schema[renaming.updated].type === FieldTypes.LINK) {
|
||||
if (renaming && table.schema[renaming.updated].type === FieldType.LINK) {
|
||||
throw new Error("Cannot rename a linked column.")
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ describe("syncGlobalUsers", () => {
|
|||
await syncGlobalUsers()
|
||||
|
||||
const metadata = await rawUserMetadata()
|
||||
expect(metadata).toHaveLength(3)
|
||||
expect(metadata).toHaveLength(2)
|
||||
expect(metadata).toContainEqual(
|
||||
expect.objectContaining({
|
||||
_id: db.generateUserMetadataID(user1._id!),
|
||||
|
@ -121,7 +121,7 @@ describe("syncGlobalUsers", () => {
|
|||
await syncGlobalUsers()
|
||||
|
||||
const metadata = await rawUserMetadata()
|
||||
expect(metadata).toHaveLength(0)
|
||||
expect(metadata).toHaveLength(1) //ADMIN user created in test bootstrap still in the application
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue