Merge remote-tracking branch 'origin/develop' into feat/relationship-configuration
This commit is contained in:
commit
66e67ffa44
|
@ -55,7 +55,7 @@ http {
|
||||||
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
|
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
|
||||||
set $csp_object "object-src 'none'";
|
set $csp_object "object-src 'none'";
|
||||||
set $csp_base_uri "base-uri 'self'";
|
set $csp_base_uri "base-uri 'self'";
|
||||||
set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com";
|
set $csp_connect "connect-src 'self' https://*.budibase.app https://*.budibase.qa https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com";
|
||||||
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
|
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
|
||||||
set $csp_frame "frame-src 'self' https:";
|
set $csp_frame "frame-src 'self' https:";
|
||||||
set $csp_img "img-src http: https: data: blob:";
|
set $csp_img "img-src http: https: data: blob:";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.10.9-alpha.4",
|
"version": "2.10.12-alpha.7",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { User } from "@budibase/types"
|
||||||
|
import { generator, structures } from "../../../tests"
|
||||||
|
import { DBTestConfiguration } from "../../../tests/extra"
|
||||||
|
import { getUsers } from "../user"
|
||||||
|
import { getGlobalDB } from "../../context"
|
||||||
|
import _ from "lodash"
|
||||||
|
|
||||||
|
import * as redis from "../../redis/init"
|
||||||
|
import { UserDB } from "../../users"
|
||||||
|
|
||||||
|
const config = new DBTestConfiguration()
|
||||||
|
|
||||||
|
describe("user cache", () => {
|
||||||
|
describe("getUsers", () => {
|
||||||
|
const users: User[] = []
|
||||||
|
beforeAll(async () => {
|
||||||
|
const userCount = 10
|
||||||
|
const userIds = generator.arrayOf(() => generator.guid(), {
|
||||||
|
min: userCount,
|
||||||
|
max: userCount,
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
for (const userId of userIds) {
|
||||||
|
const user = structures.users.user({ _id: userId })
|
||||||
|
await db.put(user)
|
||||||
|
users.push(user)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
const redisClient = await redis.getUserClient()
|
||||||
|
await redisClient.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("when no user is in cache, all of them are retrieved from db", async () => {
|
||||||
|
const usersToRequest = _.sampleSize(users, 5)
|
||||||
|
|
||||||
|
const userIdsToRequest = usersToRequest.map(x => x._id!)
|
||||||
|
|
||||||
|
jest.spyOn(UserDB, "bulkGet")
|
||||||
|
|
||||||
|
const results = await config.doInTenant(() => getUsers(userIdsToRequest))
|
||||||
|
|
||||||
|
expect(results.users).toHaveLength(5)
|
||||||
|
expect(results).toEqual({
|
||||||
|
users: usersToRequest.map(u => ({
|
||||||
|
...u,
|
||||||
|
budibaseAccess: true,
|
||||||
|
_rev: expect.any(String),
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(UserDB.bulkGet).toBeCalledTimes(1)
|
||||||
|
expect(UserDB.bulkGet).toBeCalledWith(userIdsToRequest)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("on a second all, all of them are retrieved from cache", async () => {
|
||||||
|
const usersToRequest = _.sampleSize(users, 5)
|
||||||
|
|
||||||
|
const userIdsToRequest = usersToRequest.map(x => x._id!)
|
||||||
|
|
||||||
|
jest.spyOn(UserDB, "bulkGet")
|
||||||
|
|
||||||
|
await config.doInTenant(() => getUsers(userIdsToRequest))
|
||||||
|
const resultsFromCache = await config.doInTenant(() =>
|
||||||
|
getUsers(userIdsToRequest)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(resultsFromCache.users).toHaveLength(5)
|
||||||
|
expect(resultsFromCache).toEqual({
|
||||||
|
users: expect.arrayContaining(
|
||||||
|
usersToRequest.map(u => ({
|
||||||
|
...u,
|
||||||
|
budibaseAccess: true,
|
||||||
|
_rev: expect.any(String),
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(UserDB.bulkGet).toBeCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("when some users are cached, only the missing ones are retrieved from db", async () => {
|
||||||
|
const usersToRequest = _.sampleSize(users, 5)
|
||||||
|
|
||||||
|
const userIdsToRequest = usersToRequest.map(x => x._id!)
|
||||||
|
|
||||||
|
jest.spyOn(UserDB, "bulkGet")
|
||||||
|
|
||||||
|
await config.doInTenant(() =>
|
||||||
|
getUsers([userIdsToRequest[0], userIdsToRequest[3]])
|
||||||
|
)
|
||||||
|
;(UserDB.bulkGet as jest.Mock).mockClear()
|
||||||
|
|
||||||
|
const results = await config.doInTenant(() => getUsers(userIdsToRequest))
|
||||||
|
|
||||||
|
expect(results.users).toHaveLength(5)
|
||||||
|
expect(results).toEqual({
|
||||||
|
users: expect.arrayContaining(
|
||||||
|
usersToRequest.map(u => ({
|
||||||
|
...u,
|
||||||
|
budibaseAccess: true,
|
||||||
|
_rev: expect.any(String),
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(UserDB.bulkGet).toBeCalledTimes(1)
|
||||||
|
expect(UserDB.bulkGet).toBeCalledWith([
|
||||||
|
userIdsToRequest[1],
|
||||||
|
userIdsToRequest[2],
|
||||||
|
userIdsToRequest[4],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("requesting existing and unexisting ids will return found ones", async () => {
|
||||||
|
const usersToRequest = _.sampleSize(users, 3)
|
||||||
|
const missingIds = [generator.guid(), generator.guid()]
|
||||||
|
|
||||||
|
const userIdsToRequest = _.shuffle([
|
||||||
|
...missingIds,
|
||||||
|
...usersToRequest.map(x => x._id!),
|
||||||
|
])
|
||||||
|
|
||||||
|
const results = await config.doInTenant(() => getUsers(userIdsToRequest))
|
||||||
|
|
||||||
|
expect(results.users).toHaveLength(3)
|
||||||
|
expect(results).toEqual({
|
||||||
|
users: expect.arrayContaining(
|
||||||
|
usersToRequest.map(u => ({
|
||||||
|
...u,
|
||||||
|
budibaseAccess: true,
|
||||||
|
_rev: expect.any(String),
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
notFoundIds: expect.arrayContaining(missingIds),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -6,6 +6,7 @@ import env from "../environment"
|
||||||
import * as accounts from "../accounts"
|
import * as accounts from "../accounts"
|
||||||
import { UserDB } from "../users"
|
import { UserDB } from "../users"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
import { User } from "@budibase/types"
|
||||||
|
|
||||||
const EXPIRY_SECONDS = 3600
|
const EXPIRY_SECONDS = 3600
|
||||||
|
|
||||||
|
@ -27,6 +28,35 @@ async function populateFromDB(userId: string, tenantId: string) {
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function populateUsersFromDB(
|
||||||
|
userIds: string[]
|
||||||
|
): Promise<{ users: User[]; notFoundIds?: string[] }> {
|
||||||
|
const getUsersResponse = await UserDB.bulkGet(userIds)
|
||||||
|
|
||||||
|
// Handle missed user ids
|
||||||
|
const notFoundIds = userIds.filter((uid, i) => !getUsersResponse[i])
|
||||||
|
|
||||||
|
const users = getUsersResponse.filter(x => x)
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
users.map(async (user: any) => {
|
||||||
|
user.budibaseAccess = true
|
||||||
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||||
|
const account = await accounts.getAccount(user.email)
|
||||||
|
if (account) {
|
||||||
|
user.account = account
|
||||||
|
user.accountPortalAccess = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (notFoundIds.length) {
|
||||||
|
return { users, notFoundIds }
|
||||||
|
}
|
||||||
|
return { users }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the requested user by id.
|
* Get the requested user by id.
|
||||||
* Use redis cache to first read the user.
|
* Use redis cache to first read the user.
|
||||||
|
@ -77,6 +107,36 @@ export async function getUser(
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the requested users by id.
|
||||||
|
* Use redis cache to first read the users.
|
||||||
|
* If not present fallback to loading the users directly and re-caching.
|
||||||
|
* @param {*} userIds the ids of the user to get
|
||||||
|
* @param {*} tenantId the tenant of the users to get
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getUsers(
|
||||||
|
userIds: string[]
|
||||||
|
): Promise<{ users: User[]; notFoundIds?: string[] }> {
|
||||||
|
const client = await redis.getUserClient()
|
||||||
|
// try cache
|
||||||
|
let usersFromCache = await client.bulkGet(userIds)
|
||||||
|
const missingUsersFromCache = userIds.filter(uid => !usersFromCache[uid])
|
||||||
|
const users = Object.values(usersFromCache)
|
||||||
|
let notFoundIds
|
||||||
|
|
||||||
|
if (missingUsersFromCache.length) {
|
||||||
|
const usersFromDb = await populateUsersFromDB(missingUsersFromCache)
|
||||||
|
|
||||||
|
notFoundIds = usersFromDb.notFoundIds
|
||||||
|
for (const userToCache of usersFromDb.users) {
|
||||||
|
await client.store(userToCache._id!, userToCache, EXPIRY_SECONDS)
|
||||||
|
}
|
||||||
|
users.push(...usersFromDb.users)
|
||||||
|
}
|
||||||
|
return { users, notFoundIds: notFoundIds }
|
||||||
|
}
|
||||||
|
|
||||||
export async function invalidateUser(userId: string) {
|
export async function invalidateUser(userId: string) {
|
||||||
const client = await redis.getUserClient()
|
const client = await redis.getUserClient()
|
||||||
await client.delete(userId)
|
await client.delete(userId)
|
||||||
|
|
|
@ -102,6 +102,7 @@ describe("sso", () => {
|
||||||
|
|
||||||
// modified external id to match user format
|
// modified external id to match user format
|
||||||
ssoUser._id = "us_" + details.userId
|
ssoUser._id = "us_" + details.userId
|
||||||
|
delete ssoUser.userId
|
||||||
|
|
||||||
// new sso user won't have a password
|
// new sso user won't have a password
|
||||||
delete ssoUser.password
|
delete ssoUser.password
|
||||||
|
|
|
@ -250,7 +250,7 @@ class RedisWrapper {
|
||||||
const prefixedKeys = keys.map(key => addDbPrefix(db, key))
|
const prefixedKeys = keys.map(key => addDbPrefix(db, key))
|
||||||
let response = await this.getClient().mget(prefixedKeys)
|
let response = await this.getClient().mget(prefixedKeys)
|
||||||
if (Array.isArray(response)) {
|
if (Array.isArray(response)) {
|
||||||
let final: any = {}
|
let final: Record<string, any> = {}
|
||||||
let count = 0
|
let count = 0
|
||||||
for (let result of response) {
|
for (let result of response) {
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { User } from "@budibase/types"
|
|
||||||
import { generator } from "./generator"
|
|
||||||
import { uuid } from "./common"
|
|
||||||
|
|
||||||
export const newEmail = () => {
|
|
||||||
return `${uuid()}@test.com`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const user = (userProps?: any): User => {
|
|
||||||
return {
|
|
||||||
email: newEmail(),
|
|
||||||
password: "test",
|
|
||||||
roles: { app_test: "admin" },
|
|
||||||
firstName: generator.first(),
|
|
||||||
lastName: generator.last(),
|
|
||||||
pictureUrl: "http://test.com",
|
|
||||||
...userProps,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,8 +13,7 @@ import {
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { generator } from "./generator"
|
import { generator } from "./generator"
|
||||||
import { email, uuid } from "./common"
|
import { email, uuid } from "./common"
|
||||||
import * as shared from "./shared"
|
import * as users from "./users"
|
||||||
import { user } from "./shared"
|
|
||||||
import sample from "lodash/sample"
|
import sample from "lodash/sample"
|
||||||
|
|
||||||
export function OAuth(): OAuth2 {
|
export function OAuth(): OAuth2 {
|
||||||
|
@ -26,7 +25,7 @@ export function OAuth(): OAuth2 {
|
||||||
|
|
||||||
export function authDetails(userDoc?: User): SSOAuthDetails {
|
export function authDetails(userDoc?: User): SSOAuthDetails {
|
||||||
if (!userDoc) {
|
if (!userDoc) {
|
||||||
userDoc = user()
|
userDoc = users.user()
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = userDoc._id || uuid()
|
const userId = userDoc._id || uuid()
|
||||||
|
@ -52,7 +51,7 @@ export function providerType(): SSOProviderType {
|
||||||
|
|
||||||
export function ssoProfile(user?: User): SSOProfile {
|
export function ssoProfile(user?: User): SSOProfile {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = shared.user()
|
user = users.user()
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: user._id!,
|
id: user._id!,
|
||||||
|
|
|
@ -4,11 +4,33 @@ import {
|
||||||
BuilderUser,
|
BuilderUser,
|
||||||
SSOAuthDetails,
|
SSOAuthDetails,
|
||||||
SSOUser,
|
SSOUser,
|
||||||
|
User,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { user } from "./shared"
|
|
||||||
import { authDetails } from "./sso"
|
import { authDetails } from "./sso"
|
||||||
|
import { uuid } from "./common"
|
||||||
|
import { generator } from "./generator"
|
||||||
|
import { tenant } from "."
|
||||||
|
import { generateGlobalUserID } from "../../../../src/docIds"
|
||||||
|
|
||||||
export { user, newEmail } from "./shared"
|
export const newEmail = () => {
|
||||||
|
return `${uuid()}@test.com`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
|
||||||
|
const userId = userProps?._id || generateGlobalUserID()
|
||||||
|
return {
|
||||||
|
_id: userId,
|
||||||
|
userId,
|
||||||
|
email: newEmail(),
|
||||||
|
password: "test",
|
||||||
|
roles: { app_test: "admin" },
|
||||||
|
firstName: generator.first(),
|
||||||
|
lastName: generator.last(),
|
||||||
|
pictureUrl: "http://test.com",
|
||||||
|
tenantId: tenant.id(),
|
||||||
|
...userProps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const adminUser = (userProps?: any): AdminUser => {
|
export const adminUser = (userProps?: any): AdminUser => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -83,8 +83,7 @@
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
return value.split(",").map(x => x.trim())
|
||||||
return [value]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "json") {
|
if (type === "json") {
|
||||||
|
@ -146,7 +145,7 @@
|
||||||
placeholder={placeholders[schema.type]}
|
placeholder={placeholders[schema.type]}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
value={Array.isArray(value[field])
|
value={Array.isArray(value[field])
|
||||||
? value[field].join(" ")
|
? value[field].join(",")
|
||||||
: value[field]}
|
: value[field]}
|
||||||
on:change={e => onChange(e, field, schema.type)}
|
on:change={e => onChange(e, field, schema.type)}
|
||||||
label={field}
|
label={field}
|
||||||
|
|
|
@ -55,9 +55,14 @@
|
||||||
bind:value={value[field]}
|
bind:value={value[field]}
|
||||||
label={field}
|
label={field}
|
||||||
options={schema.constraints.inclusion}
|
options={schema.constraints.inclusion}
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === "longform"}
|
{:else if schema.type === "longform"}
|
||||||
<TextArea label={field} bind:value={value[field]} />
|
<TextArea
|
||||||
|
label={field}
|
||||||
|
bind:value={value[field]}
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
/>
|
||||||
{:else if schema.type === "json"}
|
{:else if schema.type === "json"}
|
||||||
<span>
|
<span>
|
||||||
<Label>{field}</Label>
|
<Label>{field}</Label>
|
||||||
|
@ -73,7 +78,11 @@
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
{:else if schema.type === "link"}
|
{:else if schema.type === "link"}
|
||||||
<LinkedRowSelector bind:linkedRows={value[field]} {schema} />
|
<LinkedRowSelector
|
||||||
|
bind:linkedRows={value[field]}
|
||||||
|
{schema}
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
/>
|
||||||
{:else if schema.type === "string" || schema.type === "number"}
|
{:else if schema.type === "string" || schema.type === "number"}
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
||||||
|
|
|
@ -235,7 +235,7 @@
|
||||||
const baseExtensions = buildBaseExtensions()
|
const baseExtensions = buildBaseExtensions()
|
||||||
|
|
||||||
editor = new EditorView({
|
editor = new EditorView({
|
||||||
doc: value,
|
doc: value?.toString(),
|
||||||
extensions: buildExtensions(baseExtensions),
|
extensions: buildExtensions(baseExtensions),
|
||||||
parent: textarea,
|
parent: textarea,
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,10 +3,13 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { Select, Label, Multiselect } from "@budibase/bbui"
|
import { Select, Label, Multiselect } from "@budibase/bbui"
|
||||||
import { capitalise } from "../../helpers"
|
import { capitalise } from "../../helpers"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let schema
|
export let schema
|
||||||
export let linkedRows = []
|
export let linkedRows = []
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let rows = []
|
let rows = []
|
||||||
let linkedIds = (Array.isArray(linkedRows) ? linkedRows : [])?.map(
|
let linkedIds = (Array.isArray(linkedRows) ? linkedRows : [])?.map(
|
||||||
row => row?._id || row
|
row => row?._id || row
|
||||||
|
@ -44,7 +47,10 @@
|
||||||
options={rows}
|
options={rows}
|
||||||
getOptionLabel={getPrettyName}
|
getOptionLabel={getPrettyName}
|
||||||
getOptionValue={row => row._id}
|
getOptionValue={row => row._id}
|
||||||
on:change={e => (linkedIds = e.detail ? [e.detail] : [])}
|
on:change={e => {
|
||||||
|
linkedIds = e.detail ? [e.detail] : []
|
||||||
|
dispatch("change", linkedIds)
|
||||||
|
}}
|
||||||
{label}
|
{label}
|
||||||
sort
|
sort
|
||||||
/>
|
/>
|
||||||
|
@ -56,5 +62,6 @@
|
||||||
getOptionLabel={getPrettyName}
|
getOptionLabel={getPrettyName}
|
||||||
getOptionValue={row => row._id}
|
getOptionValue={row => row._id}
|
||||||
sort
|
sort
|
||||||
|
on:change={() => dispatch("change", linkedIds)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -38,14 +38,12 @@
|
||||||
hoverable
|
hoverable
|
||||||
on:click={store.undo}
|
on:click={store.undo}
|
||||||
disabled={!$store.canUndo}
|
disabled={!$store.canUndo}
|
||||||
tooltip="Undo latest change"
|
|
||||||
/>
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
name="Redo"
|
name="Redo"
|
||||||
hoverable
|
hoverable
|
||||||
on:click={store.redo}
|
on:click={store.redo}
|
||||||
disabled={!$store.canRedo}
|
disabled={!$store.canRedo}
|
||||||
tooltip="Redo latest undo"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
<script>
|
||||||
|
import { notifications, Button, Icon, Body } from "@budibase/bbui"
|
||||||
|
import { admin, auth } from "stores/portal"
|
||||||
|
|
||||||
|
$: user = $auth.user
|
||||||
|
let loading = false
|
||||||
|
let complete = false
|
||||||
|
|
||||||
|
const resetPassword = async () => {
|
||||||
|
if (loading || complete) return
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`${$admin.accountPortalUrl}/api/auth/reset`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: user.email }),
|
||||||
|
})
|
||||||
|
|
||||||
|
complete = true
|
||||||
|
} catch (e) {
|
||||||
|
notifications.error("There was an issue sending your validation email.")
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if user?.account?.verified === false}
|
||||||
|
<section class="banner">
|
||||||
|
<div class="icon">
|
||||||
|
<Icon name="Info" />
|
||||||
|
</div>
|
||||||
|
<div class="copy">
|
||||||
|
<Body size="S">
|
||||||
|
Please verify your account. We've sent the verification link to <span
|
||||||
|
class="email">{user.email}</span
|
||||||
|
></Body
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="button" class:disabled={loading || complete}>
|
||||||
|
<Button on:click={resetPassword}
|
||||||
|
>{complete ? "Email sent" : "Resend email"}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.banner {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--grey-2);
|
||||||
|
height: 48px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 15px;
|
||||||
|
color: var(--ink);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy :global(p) {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button :global(button) {
|
||||||
|
color: var(--ink);
|
||||||
|
background-color: var(--grey-3);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button :global(button):hover {
|
||||||
|
background-color: var(--grey-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled :global(button) {
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--ink);
|
||||||
|
background-color: var(--grey-4);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -224,10 +224,10 @@
|
||||||
<span
|
<span
|
||||||
class="app-link"
|
class="app-link"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
appActionPopover.hide()
|
||||||
if (isPublished) {
|
if (isPublished) {
|
||||||
viewApp()
|
viewApp()
|
||||||
} else {
|
} else {
|
||||||
appActionPopover.hide()
|
|
||||||
updateAppModal.show()
|
updateAppModal.show()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import ButtonActionDrawer from "./ButtonActionDrawer.svelte"
|
import ButtonActionDrawer from "./ButtonActionDrawer.svelte"
|
||||||
import { automationStore } from "builderStore"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -24,47 +23,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveEventData = async () => {
|
const saveEventData = async () => {
|
||||||
// any automations that need created from event triggers
|
|
||||||
const automationsToCreate = tmpValue.filter(
|
|
||||||
action => action["##eventHandlerType"] === "Trigger Automation"
|
|
||||||
)
|
|
||||||
for (let action of automationsToCreate) {
|
|
||||||
await createAutomation(action.parameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch("change", tmpValue)
|
dispatch("change", tmpValue)
|
||||||
notifications.success("Component actions saved.")
|
notifications.success("Component actions saved.")
|
||||||
drawer.hide()
|
drawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
// called by the parent modal when actions are saved
|
|
||||||
const createAutomation = async parameters => {
|
|
||||||
if (parameters.automationId || !parameters.newAutomationName) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
let trigger = automationStore.actions.constructBlock(
|
|
||||||
"TRIGGER",
|
|
||||||
"APP",
|
|
||||||
$automationStore.blockDefinitions.TRIGGER.APP
|
|
||||||
)
|
|
||||||
trigger.inputs = {
|
|
||||||
fields: Object.keys(parameters.fields ?? {}).reduce((fields, key) => {
|
|
||||||
fields[key] = "string"
|
|
||||||
return fields
|
|
||||||
}, {}),
|
|
||||||
}
|
|
||||||
const automation = await automationStore.actions.create(
|
|
||||||
parameters.newAutomationName,
|
|
||||||
trigger
|
|
||||||
)
|
|
||||||
parameters.automationId = automation._id
|
|
||||||
delete parameters.newAutomationName
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error creating automation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: actionCount = value?.length
|
$: actionCount = value?.length
|
||||||
$: actionText = `${actionCount || "No"} action${
|
$: actionText = `${actionCount || "No"} action${
|
||||||
actionCount !== 1 ? "s" : ""
|
actionCount !== 1 ? "s" : ""
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { Label, Combobox, Select } from "@budibase/bbui"
|
import { Label, Combobox, Select } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
getActionProviderComponents,
|
getActionProviderComponents,
|
||||||
|
@ -10,12 +9,6 @@
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (!parameters.type) {
|
|
||||||
parameters.type = "top"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
$: formComponent = findComponent($currentAsset.props, parameters.componentId)
|
$: formComponent = findComponent($currentAsset.props, parameters.componentId)
|
||||||
$: formSchema = buildFormSchema(formComponent)
|
$: formSchema = buildFormSchema(formComponent)
|
||||||
$: fieldOptions = Object.keys(formSchema || {})
|
$: fieldOptions = Object.keys(formSchema || {})
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import { admin, auth, licensing } from "stores/portal"
|
import { admin, auth, licensing } from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { CookieUtils, Constants } from "@budibase/frontend-core"
|
import { CookieUtils, Constants } from "@budibase/frontend-core"
|
||||||
import { banner, BANNER_TYPES } from "@budibase/bbui"
|
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import Branding from "./Branding.svelte"
|
import Branding from "./Branding.svelte"
|
||||||
|
|
||||||
|
@ -17,32 +16,6 @@
|
||||||
$: user = $auth.user
|
$: user = $auth.user
|
||||||
|
|
||||||
$: useAccountPortal = cloud && !$admin.disableAccountPortal
|
$: useAccountPortal = cloud && !$admin.disableAccountPortal
|
||||||
let showVerificationPrompt = false
|
|
||||||
|
|
||||||
const checkVerification = user => {
|
|
||||||
if (!showVerificationPrompt && user?.account?.verified === false) {
|
|
||||||
showVerificationPrompt = true
|
|
||||||
banner.queue([
|
|
||||||
{
|
|
||||||
message: `Please verify your account. We've sent the verification link to ${user.email}`,
|
|
||||||
type: BANNER_TYPES.NEUTRAL,
|
|
||||||
showCloseButton: false,
|
|
||||||
extraButtonAction: () => {
|
|
||||||
fetch(`${$admin.accountPortalUrl}/api/auth/reset`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email: user.email }),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
extraButtonText: "Resend email",
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: checkVerification(user)
|
|
||||||
|
|
||||||
const validateTenantId = async () => {
|
const validateTenantId = async () => {
|
||||||
const host = window.location.host
|
const host = window.location.host
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
import VerificationPromptBanner from "components/common/VerificationPromptBanner.svelte"
|
||||||
import CommandPalette from "components/commandPalette/CommandPalette.svelte"
|
import CommandPalette from "components/commandPalette/CommandPalette.svelte"
|
||||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||||
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
|
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
|
||||||
|
@ -136,6 +137,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="root" class:blur={$store.showPreview}>
|
<div class="root" class:blur={$store.showPreview}>
|
||||||
|
<VerificationPromptBanner />
|
||||||
<div class="top-nav">
|
<div class="top-nav">
|
||||||
{#if $store.initialised}
|
{#if $store.initialised}
|
||||||
<div class="topleftnav">
|
<div class="topleftnav">
|
||||||
|
|
|
@ -61,10 +61,12 @@
|
||||||
key: "_css",
|
key: "_css",
|
||||||
type: "text",
|
type: "text",
|
||||||
})
|
})
|
||||||
$: settingOptions = settings.map(setting => ({
|
$: settingOptions = settings
|
||||||
label: makeLabel(setting),
|
.filter(setting => setting.supportsConditions !== false)
|
||||||
value: setting.key,
|
.map(setting => ({
|
||||||
}))
|
label: makeLabel(setting),
|
||||||
|
value: setting.key,
|
||||||
|
}))
|
||||||
$: conditions.forEach(link => {
|
$: conditions.forEach(link => {
|
||||||
if (!link.id) {
|
if (!link.id) {
|
||||||
link.id = generate()
|
link.id = generate()
|
||||||
|
|
|
@ -36,10 +36,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetailSummary name={"Conditions"} collapsible={false}>
|
<DetailSummary name={"Conditions"} collapsible={false}>
|
||||||
<div class="conditionCount">{conditionText}</div>
|
<ActionButton on:click={openDrawer}>{conditionText}</ActionButton>
|
||||||
<div>
|
|
||||||
<ActionButton on:click={openDrawer}>Configure conditions</ActionButton>
|
|
||||||
</div>
|
|
||||||
</DetailSummary>
|
</DetailSummary>
|
||||||
<Drawer bind:this={drawer} title="Conditions">
|
<Drawer bind:this={drawer} title="Conditions">
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
|
@ -48,10 +45,3 @@
|
||||||
<Button cta slot="buttons" on:click={() => save()}>Save</Button>
|
<Button cta slot="buttons" on:click={() => save()}>Save</Button>
|
||||||
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} {bindings} />
|
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} {bindings} />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<style>
|
|
||||||
.conditionCount {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: -5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import Logo from "./_components/Logo.svelte"
|
import Logo from "./_components/Logo.svelte"
|
||||||
import UserDropdown from "./_components/UserDropdown.svelte"
|
import UserDropdown from "./_components/UserDropdown.svelte"
|
||||||
import HelpMenu from "components/common/HelpMenu.svelte"
|
import HelpMenu from "components/common/HelpMenu.svelte"
|
||||||
|
import VerificationPromptBanner from "components/common/VerificationPromptBanner.svelte"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
@ -55,6 +56,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<HelpMenu />
|
<HelpMenu />
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<VerificationPromptBanner />
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<div class="branding">
|
<div class="branding">
|
||||||
<Logo />
|
<Logo />
|
||||||
|
|
|
@ -2539,7 +2539,8 @@
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue",
|
||||||
|
"supportsConditions": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "event",
|
"type": "event",
|
||||||
|
@ -2629,7 +2630,8 @@
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue",
|
||||||
|
"supportsConditions": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "event",
|
"type": "event",
|
||||||
|
@ -2685,7 +2687,8 @@
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue",
|
||||||
|
"supportsConditions": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "event",
|
"type": "event",
|
||||||
|
@ -2736,7 +2739,8 @@
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue",
|
||||||
|
"supportsConditions": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "event",
|
"type": "event",
|
||||||
|
@ -2841,7 +2845,8 @@
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue",
|
||||||
|
"supportsConditions": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
@ -2960,7 +2965,8 @@
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue",
|
||||||
|
"supportsConditions": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "event",
|
"type": "event",
|
||||||
|
@ -3143,7 +3149,8 @@
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue",
|
||||||
|
"supportsConditions": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "event",
|
"type": "event",
|
||||||
|
@ -3200,7 +3207,8 @@
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue",
|
||||||
|
"supportsConditions": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "event",
|
"type": "event",
|
||||||
|
@ -3301,7 +3309,8 @@
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue",
|
||||||
|
"supportsConditions": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "event",
|
"type": "event",
|
||||||
|
@ -3355,7 +3364,8 @@
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue",
|
||||||
|
"supportsConditions": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
@ -3622,7 +3632,8 @@
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue",
|
||||||
|
"supportsConditions": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "event",
|
"type": "event",
|
||||||
|
@ -3689,7 +3700,8 @@
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Default value",
|
"label": "Default value",
|
||||||
"key": "defaultValue"
|
"key": "defaultValue",
|
||||||
|
"supportsConditions": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "event",
|
"type": "event",
|
||||||
|
|
|
@ -230,10 +230,16 @@
|
||||||
// We want to validate every field (even if validation fails early) to
|
// We want to validate every field (even if validation fails early) to
|
||||||
// ensure that all fields are populated with errors if invalid
|
// ensure that all fields are populated with errors if invalid
|
||||||
let valid = true
|
let valid = true
|
||||||
|
let hasScrolled = false
|
||||||
stepFields.forEach(field => {
|
stepFields.forEach(field => {
|
||||||
const fieldValid = get(field).fieldApi.validate()
|
const fieldValid = get(field).fieldApi.validate()
|
||||||
valid = valid && fieldValid
|
valid = valid && fieldValid
|
||||||
|
if (!valid && !hasScrolled) {
|
||||||
|
handleScrollToField({ field: get(field) })
|
||||||
|
hasScrolled = true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return valid
|
return valid
|
||||||
},
|
},
|
||||||
reset: () => {
|
reset: () => {
|
||||||
|
@ -409,10 +415,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScrollToField = ({ field }) => {
|
const handleScrollToField = ({ field }) => {
|
||||||
const fieldId = get(getField(field)).fieldState.fieldId
|
if (!field.fieldState) {
|
||||||
|
field = get(getField(field))
|
||||||
|
}
|
||||||
|
const fieldId = field.fieldState.fieldId
|
||||||
|
const fieldElement = document.getElementById(fieldId)
|
||||||
|
fieldElement.focus({ preventScroll: true })
|
||||||
const label = document.querySelector(`label[for="${fieldId}"]`)
|
const label = document.querySelector(`label[for="${fieldId}"]`)
|
||||||
document.getElementById(fieldId).focus({ preventScroll: true })
|
label.style.scrollMargin = "100px"
|
||||||
label.scrollIntoView({ behavior: "smooth" })
|
label.scrollIntoView({ behavior: "smooth", block: "nearest" })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action context to pass to children
|
// Action context to pass to children
|
||||||
|
|
|
@ -17,6 +17,13 @@
|
||||||
let fieldApi
|
let fieldApi
|
||||||
let localFiles = []
|
let localFiles = []
|
||||||
|
|
||||||
|
$: {
|
||||||
|
// If the field state is reset, clear the local files
|
||||||
|
if (!fieldState?.value?.length) {
|
||||||
|
localFiles = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { API, notificationStore, uploadStore } = getContext("sdk")
|
const { API, notificationStore, uploadStore } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ export async function handleRequest(
|
||||||
) {
|
) {
|
||||||
// make sure the filters are cleaned up, no empty strings for equals, fuzzy or string
|
// make sure the filters are cleaned up, no empty strings for equals, fuzzy or string
|
||||||
if (opts && opts.filters) {
|
if (opts && opts.filters) {
|
||||||
opts.filters = utils.removeEmptyFilters(opts.filters)
|
opts.filters = sdk.rows.removeEmptyFilters(opts.filters)
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!dataFilters.hasFilters(opts?.filters) &&
|
!dataFilters.hasFilters(opts?.filters) &&
|
||||||
|
|
|
@ -72,6 +72,11 @@ export const save = async (ctx: UserCtx<Row, Row>) => {
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
const body = ctx.request.body
|
const body = ctx.request.body
|
||||||
|
|
||||||
|
// user metadata doesn't exist yet - don't allow creation
|
||||||
|
if (utils.isUserMetadataTable(tableId) && !body._rev) {
|
||||||
|
ctx.throw(400, "Cannot create new user entry.")
|
||||||
|
}
|
||||||
|
|
||||||
// if it has an ID already then its a patch
|
// if it has an ID already then its a patch
|
||||||
if (body && body._id) {
|
if (body && body._id) {
|
||||||
return patch(ctx as UserCtx<PatchRowRequest, PatchRowResponse>)
|
return patch(ctx as UserCtx<PatchRowRequest, PatchRowResponse>)
|
||||||
|
|
|
@ -175,3 +175,7 @@ export function removeEmptyFilters(filters: SearchFilters) {
|
||||||
}
|
}
|
||||||
return filters
|
return filters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isUserMetadataTable(tableId: string) {
|
||||||
|
return tableId === InternalTables.USER_METADATA
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { databaseTestProviders } from "../../../integrations/tests/utils"
|
||||||
import tk from "timekeeper"
|
import tk from "timekeeper"
|
||||||
import { outputProcessing } from "../../../utilities/rowProcessor"
|
import { outputProcessing } from "../../../utilities/rowProcessor"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import { context, roles, tenancy } from "@budibase/backend-core"
|
import { context, InternalTable, roles, tenancy } from "@budibase/backend-core"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import {
|
import {
|
||||||
FieldType,
|
FieldType,
|
||||||
|
@ -1415,6 +1415,23 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
isInternal &&
|
||||||
|
it("doesn't allow creating in user table", async () => {
|
||||||
|
const userTableId = InternalTable.USER_METADATA
|
||||||
|
const response = await config.api.row.save(
|
||||||
|
userTableId,
|
||||||
|
{
|
||||||
|
tableId: userTableId,
|
||||||
|
firstName: "Joe",
|
||||||
|
lastName: "Joe",
|
||||||
|
email: "joe@joe.com",
|
||||||
|
roles: {},
|
||||||
|
},
|
||||||
|
{ expectStatus: 400 }
|
||||||
|
)
|
||||||
|
expect(response.message).toBe("Cannot create new user entry.")
|
||||||
|
})
|
||||||
|
|
||||||
describe("permissions", () => {
|
describe("permissions", () => {
|
||||||
let viewId: string
|
let viewId: string
|
||||||
let tableId: string
|
let tableId: string
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { Knex, knex } from "knex"
|
import { Knex, knex } from "knex"
|
||||||
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
|
import { QueryOptions } from "../../definitions/datasource"
|
||||||
|
import { isIsoDateString, SqlClient } from "../utils"
|
||||||
|
import SqlTableQueryBuilder from "./sqlTable"
|
||||||
import {
|
import {
|
||||||
Operation,
|
Operation,
|
||||||
QueryJson,
|
QueryJson,
|
||||||
|
@ -6,11 +10,8 @@ import {
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { db as dbCore } from "@budibase/backend-core"
|
|
||||||
import { QueryOptions } from "../../definitions/datasource"
|
|
||||||
import { isIsoDateString, SqlClient } from "../utils"
|
|
||||||
import SqlTableQueryBuilder from "./sqlTable"
|
|
||||||
import environment from "../../environment"
|
import environment from "../../environment"
|
||||||
|
import { isValidFilter } from "../utils"
|
||||||
|
|
||||||
const envLimit = environment.SQL_MAX_ROWS
|
const envLimit = environment.SQL_MAX_ROWS
|
||||||
? parseInt(environment.SQL_MAX_ROWS)
|
? parseInt(environment.SQL_MAX_ROWS)
|
||||||
|
@ -261,15 +262,17 @@ class InternalBuilder {
|
||||||
if (isEmptyObject(value.high)) {
|
if (isEmptyObject(value.high)) {
|
||||||
value.high = ""
|
value.high = ""
|
||||||
}
|
}
|
||||||
if (value.low && value.high) {
|
const lowValid = isValidFilter(value.low),
|
||||||
|
highValid = isValidFilter(value.high)
|
||||||
|
if (lowValid && highValid) {
|
||||||
// Use a between operator if we have 2 valid range values
|
// Use a between operator if we have 2 valid range values
|
||||||
const fnc = allOr ? "orWhereBetween" : "whereBetween"
|
const fnc = allOr ? "orWhereBetween" : "whereBetween"
|
||||||
query = query[fnc](key, [value.low, value.high])
|
query = query[fnc](key, [value.low, value.high])
|
||||||
} else if (value.low) {
|
} else if (lowValid) {
|
||||||
// Use just a single greater than operator if we only have a low
|
// Use just a single greater than operator if we only have a low
|
||||||
const fnc = allOr ? "orWhere" : "where"
|
const fnc = allOr ? "orWhere" : "where"
|
||||||
query = query[fnc](key, ">", value.low)
|
query = query[fnc](key, ">", value.low)
|
||||||
} else if (value.high) {
|
} else if (highValid) {
|
||||||
// Use just a single less than operator if we only have a high
|
// Use just a single less than operator if we only have a high
|
||||||
const fnc = allOr ? "orWhere" : "where"
|
const fnc = allOr ? "orWhere" : "where"
|
||||||
query = query[fnc](key, "<", value.high)
|
query = query[fnc](key, "<", value.high)
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { SourceName, SqlQuery, Datasource, Table } from "@budibase/types"
|
import { SqlQuery, Table, SearchFilters } from "@budibase/types"
|
||||||
import { DocumentType, SEPARATOR } from "../db/utils"
|
import { DocumentType, SEPARATOR } from "../db/utils"
|
||||||
import { FieldTypes, BuildSchemaErrors, InvalidColumns } from "../constants"
|
import {
|
||||||
|
FieldTypes,
|
||||||
|
BuildSchemaErrors,
|
||||||
|
InvalidColumns,
|
||||||
|
NoEmptyFilterStrings,
|
||||||
|
} from "../constants"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
|
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
|
||||||
|
@ -343,3 +348,36 @@ export function getPrimaryDisplay(testValue: unknown): string | undefined {
|
||||||
}
|
}
|
||||||
return testValue as string
|
return testValue as string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isValidFilter(value: any) {
|
||||||
|
return value != null && value !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't do a pure falsy check, as 0 is included
|
||||||
|
// https://github.com/Budibase/budibase/issues/10118
|
||||||
|
export function removeEmptyFilters(filters: SearchFilters) {
|
||||||
|
for (let filterField of NoEmptyFilterStrings) {
|
||||||
|
if (!filters[filterField]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let filterType of Object.keys(filters)) {
|
||||||
|
if (filterType !== filterField) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// don't know which one we're checking, type could be anything
|
||||||
|
const value = filters[filterType] as unknown
|
||||||
|
if (typeof value === "object") {
|
||||||
|
for (let [key, value] of Object.entries(
|
||||||
|
filters[filterType] as object
|
||||||
|
)) {
|
||||||
|
if (value == null || value === "") {
|
||||||
|
// @ts-ignore
|
||||||
|
delete filters[filterField][key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { AppStatus } from "../../../db/utils"
|
import { AppStatus } from "../../../db/utils"
|
||||||
import { App, ContextUser } from "@budibase/types"
|
import { App, ContextUser, User } from "@budibase/types"
|
||||||
import { getLocksById } from "../../../utilities/redis"
|
import { getLocksById } from "../../../utilities/redis"
|
||||||
import { enrichApps } from "../../users/sessions"
|
import { enrichApps } from "../../users/sessions"
|
||||||
import { checkAppMetadata } from "../../../automations/logging"
|
import { checkAppMetadata } from "../../../automations/logging"
|
||||||
import { db as dbCore, users } from "@budibase/backend-core"
|
import { db as dbCore, users } from "@budibase/backend-core"
|
||||||
|
import { groups } from "@budibase/pro"
|
||||||
|
|
||||||
export function filterAppList(user: ContextUser, apps: App[]) {
|
export function filterAppList(user: User, apps: App[]) {
|
||||||
let appList: string[] = []
|
let appList: string[] = []
|
||||||
const roleApps = Object.keys(user.roles || {})
|
const roleApps = Object.keys(user.roles)
|
||||||
if (users.hasAppBuilderPermissions(user)) {
|
if (users.hasAppBuilderPermissions(user)) {
|
||||||
appList = user.builder?.apps || []
|
appList = user.builder?.apps || []
|
||||||
appList = appList.concat(roleApps)
|
appList = appList.concat(roleApps)
|
||||||
|
@ -23,7 +24,12 @@ export async function fetch(status: AppStatus, user: ContextUser) {
|
||||||
const dev = status === AppStatus.DEV
|
const dev = status === AppStatus.DEV
|
||||||
const all = status === AppStatus.ALL
|
const all = status === AppStatus.ALL
|
||||||
let apps = (await dbCore.getAllApps({ dev, all })) as App[]
|
let apps = (await dbCore.getAllApps({ dev, all })) as App[]
|
||||||
apps = filterAppList(user, apps)
|
|
||||||
|
const enrichedUser = await groups.enrichUserRolesFromGroups({
|
||||||
|
...user,
|
||||||
|
roles: user.roles || {},
|
||||||
|
})
|
||||||
|
apps = filterAppList(enrichedUser, apps)
|
||||||
|
|
||||||
const appIds = apps
|
const appIds = apps
|
||||||
.filter((app: any) => app.status === "development")
|
.filter((app: any) => app.status === "development")
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { isExternalTable } from "../../../integrations/utils"
|
||||||
import * as internal from "./search/internal"
|
import * as internal from "./search/internal"
|
||||||
import * as external from "./search/external"
|
import * as external from "./search/external"
|
||||||
import { Format } from "../../../api/controllers/view/exporters"
|
import { Format } from "../../../api/controllers/view/exporters"
|
||||||
|
export { isValidFilter, removeEmptyFilters } from "../../../integrations/utils"
|
||||||
|
|
||||||
export interface ViewParams {
|
export interface ViewParams {
|
||||||
calculation: string
|
calculation: string
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as utils from "../utils"
|
import * as search from "../../app/rows/search"
|
||||||
|
|
||||||
describe("removeEmptyFilters", () => {
|
describe("removeEmptyFilters", () => {
|
||||||
it("0 should not be removed", () => {
|
it("0 should not be removed", () => {
|
||||||
const filters = utils.removeEmptyFilters({
|
const filters = search.removeEmptyFilters({
|
||||||
equal: {
|
equal: {
|
||||||
column: 0,
|
column: 0,
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,7 @@ describe("removeEmptyFilters", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("empty string should be removed", () => {
|
it("empty string should be removed", () => {
|
||||||
const filters = utils.removeEmptyFilters({
|
const filters = search.removeEmptyFilters({
|
||||||
equal: {
|
equal: {
|
||||||
column: "",
|
column: "",
|
||||||
},
|
},
|
Loading…
Reference in New Issue