Merge remote-tracking branch 'origin/master' into feature/signature-field-and-component

This commit is contained in:
Dean 2024-03-22 15:26:23 +00:00
commit a773c167d5
210 changed files with 2036 additions and 2480 deletions

View File

@ -36,13 +36,14 @@
"files": ["**/*.ts"], "files": ["**/*.ts"],
"excludedFiles": ["qa-core/**"], "excludedFiles": ["qa-core/**"],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended"], "extends": ["eslint:recommended"],
"globals": {
"NodeJS": true
},
"rules": { "rules": {
"no-unused-vars": "off", "no-unused-vars": "off",
"no-inner-declarations": "off", "@typescript-eslint/no-unused-vars": "error",
"no-case-declarations": "off",
"no-undef": "off",
"no-prototype-builtins": "off",
"local-rules/no-budibase-imports": "error" "local-rules/no-budibase-imports": "error"
} }
}, },
@ -50,17 +51,17 @@
"files": ["**/*.spec.ts"], "files": ["**/*.spec.ts"],
"excludedFiles": ["qa-core/**"], "excludedFiles": ["qa-core/**"],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"plugins": ["jest"], "plugins": ["jest", "@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:jest/recommended"], "extends": ["eslint:recommended", "plugin:jest/recommended"],
"env": { "env": {
"jest/globals": true "jest/globals": true
}, },
"globals": {
"NodeJS": true
},
"rules": { "rules": {
"no-unused-vars": "off", "no-unused-vars": "off",
"no-inner-declarations": "off", "@typescript-eslint/no-unused-vars": "error",
"no-case-declarations": "off",
"no-undef": "off",
"no-prototype-builtins": "off",
"local-rules/no-test-com": "error", "local-rules/no-test-com": "error",
"local-rules/email-domain-example-com": "error", "local-rules/email-domain-example-com": "error",
"no-console": "warn", "no-console": "warn",

View File

@ -1,5 +1,5 @@
{ {
"version": "2.22.3", "version": "2.22.11",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

View File

@ -26,6 +26,7 @@
"svelte": "^4.2.10", "svelte": "^4.2.10",
"svelte-eslint-parser": "^0.33.1", "svelte-eslint-parser": "^0.33.1",
"typescript": "5.2.2", "typescript": "5.2.2",
"typescript-eslint": "^7.3.1",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"scripts": { "scripts": {

@ -1 +1 @@
Subproject commit 6465dc9c2a38e1380b32204cad4ae0c1f33e065a Subproject commit f5b467b6b1c55c48847545db41be7b1c035e167a

View File

@ -133,7 +133,7 @@ export async function refreshOAuthToken(
configId?: string configId?: string
): Promise<RefreshResponse> { ): Promise<RefreshResponse> {
switch (providerType) { switch (providerType) {
case SSOProviderType.OIDC: case SSOProviderType.OIDC: {
if (!configId) { if (!configId) {
return { err: { data: "OIDC config id not provided" } } return { err: { data: "OIDC config id not provided" } }
} }
@ -142,12 +142,14 @@ export async function refreshOAuthToken(
return { err: { data: "OIDC configuration not found" } } return { err: { data: "OIDC configuration not found" } }
} }
return refreshOIDCAccessToken(oidcConfig, refreshToken) return refreshOIDCAccessToken(oidcConfig, refreshToken)
case SSOProviderType.GOOGLE: }
case SSOProviderType.GOOGLE: {
let googleConfig = await configs.getGoogleConfig() let googleConfig = await configs.getGoogleConfig()
if (!googleConfig) { if (!googleConfig) {
return { err: { data: "Google configuration not found" } } return { err: { data: "Google configuration not found" } }
} }
return refreshGoogleAccessToken(googleConfig, refreshToken) return refreshGoogleAccessToken(googleConfig, refreshToken)
}
} }
} }

View File

@ -129,7 +129,7 @@ export default class BaseCache {
} }
} }
async bustCache(key: string, opts = { client: null }) { async bustCache(key: string) {
const client = await this.getClient() const client = await this.getClient()
try { try {
await client.delete(generateTenantKey(key)) await client.delete(generateTenantKey(key))

View File

@ -1,5 +1,5 @@
import * as utils from "../utils" import * as utils from "../utils"
import { Duration, DurationType } from "../utils" import { Duration } from "../utils"
import env from "../environment" import env from "../environment"
import { getTenantId } from "../context" import { getTenantId } from "../context"
import * as redis from "../redis/init" import * as redis from "../redis/init"

View File

@ -8,7 +8,7 @@ const DEFAULT_WRITE_RATE_MS = 10000
let CACHE: BaseCache | null = null let CACHE: BaseCache | null = null
interface CacheItem<T extends Document> { interface CacheItem<T extends Document> {
doc: any doc: T
lastWrite: number lastWrite: number
} }

View File

@ -10,10 +10,6 @@ interface SearchResponse<T> {
totalRows: number totalRows: number
} }
interface PaginatedSearchResponse<T> extends SearchResponse<T> {
hasNextPage: boolean
}
export type SearchParams<T> = { export type SearchParams<T> = {
tableId?: string tableId?: string
sort?: string sort?: string

View File

@ -34,12 +34,12 @@ export async function createUserIndex() {
} }
let idxKey = prev != null ? `${prev}.${key}` : key let idxKey = prev != null ? `${prev}.${key}` : key
if (typeof input[key] === "string") { if (typeof input[key] === "string") {
// @ts-expect-error index is available in a CouchDB map function
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
// @ts-ignore
index(idxKey, input[key].toLowerCase(), { facet: true }) index(idxKey, input[key].toLowerCase(), { facet: true })
} else if (typeof input[key] !== "object") { } else if (typeof input[key] !== "object") {
// @ts-expect-error index is available in a CouchDB map function
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
// @ts-ignore
index(idxKey, input[key], { facet: true }) index(idxKey, input[key], { facet: true })
} else { } else {
idx(input[key], idxKey) idx(input[key], idxKey)

View File

@ -17,13 +17,8 @@ export function init(processors: ProcessorMap) {
// if not processing in this instance, kick it off // if not processing in this instance, kick it off
if (!processingPromise) { if (!processingPromise) {
processingPromise = asyncEventQueue.process(async job => { processingPromise = asyncEventQueue.process(async job => {
const { event, identity, properties, timestamp } = job.data const { event, identity, properties } = job.data
await documentProcessor.processEvent( await documentProcessor.processEvent(event, identity, properties)
event,
identity,
properties,
timestamp
)
}) })
} }
} }

View File

@ -1,7 +1,6 @@
import { import {
Event, Event,
Identity, Identity,
Group,
IdentityType, IdentityType,
AuditLogQueueEvent, AuditLogQueueEvent,
AuditLogFn, AuditLogFn,
@ -79,11 +78,11 @@ export default class AuditLogsProcessor implements EventProcessor {
} }
} }
async identify(identity: Identity, timestamp?: string | number) { async identify() {
// no-op // no-op
} }
async identifyGroup(group: Group, timestamp?: string | number) { async identifyGroup() {
// no-op // no-op
} }

View File

@ -8,8 +8,7 @@ export default class LoggingProcessor implements EventProcessor {
async processEvent( async processEvent(
event: Event, event: Event,
identity: Identity, identity: Identity,
properties: any, properties: any
timestamp?: string
): Promise<void> { ): Promise<void> {
if (skipLogging) { if (skipLogging) {
return return
@ -17,14 +16,14 @@ export default class LoggingProcessor implements EventProcessor {
console.log(`[audit] [identityType=${identity.type}] ${event}`, properties) console.log(`[audit] [identityType=${identity.type}] ${event}`, properties)
} }
async identify(identity: Identity, timestamp?: string | number) { async identify(identity: Identity) {
if (skipLogging) { if (skipLogging) {
return return
} }
console.log(`[audit] identified`, identity) console.log(`[audit] identified`, identity)
} }
async identifyGroup(group: Group, timestamp?: string | number) { async identifyGroup(group: Group) {
if (skipLogging) { if (skipLogging) {
return return
} }

View File

@ -14,12 +14,7 @@ export default class DocumentUpdateProcessor implements EventProcessor {
this.processors = processors this.processors = processors
} }
async processEvent( async processEvent(event: Event, identity: Identity, properties: any) {
event: Event,
identity: Identity,
properties: any,
timestamp?: string | number
) {
const tenantId = identity.realTenantId const tenantId = identity.realTenantId
const docId = getDocumentId(event, properties) const docId = getDocumentId(event, properties)
if (!tenantId || !docId) { if (!tenantId || !docId) {

View File

@ -10,6 +10,18 @@ import { formats } from "dd-trace/ext"
import { localFileDestination } from "../system" import { localFileDestination } from "../system"
function isPlainObject(obj: any) {
return typeof obj === "object" && obj !== null && !(obj instanceof Error)
}
function isError(obj: any) {
return obj instanceof Error
}
function isMessage(obj: any) {
return typeof obj === "string"
}
// LOGGER // LOGGER
let pinoInstance: pino.Logger | undefined let pinoInstance: pino.Logger | undefined
@ -71,23 +83,11 @@ if (!env.DISABLE_PINO_LOGGER) {
err?: Error err?: Error
} }
function isPlainObject(obj: any) {
return typeof obj === "object" && obj !== null && !(obj instanceof Error)
}
function isError(obj: any) {
return obj instanceof Error
}
function isMessage(obj: any) {
return typeof obj === "string"
}
/** /**
* Backwards compatibility between console logging statements * Backwards compatibility between console logging statements
* and pino logging requirements. * and pino logging requirements.
*/ */
function getLogParams(args: any[]): [MergingObject, string] { const getLogParams = (args: any[]): [MergingObject, string] => {
let error = undefined let error = undefined
let objects: any[] = [] let objects: any[] = []
let message = "" let message = ""

View File

@ -28,7 +28,7 @@ export const buildMatcherRegex = (
} }
export const matches = (ctx: BBContext, options: RegexMatcher[]) => { export const matches = (ctx: BBContext, options: RegexMatcher[]) => {
return options.find(({ regex, method, route }) => { return options.find(({ regex, method }) => {
const urlMatch = regex.test(ctx.request.url) const urlMatch = regex.test(ctx.request.url)
const methodMatch = const methodMatch =
method === "ALL" method === "ALL"

View File

@ -3,7 +3,7 @@ import { Cookie } from "../../../constants"
import * as configs from "../../../configs" import * as configs from "../../../configs"
import * as cache from "../../../cache" import * as cache from "../../../cache"
import * as utils from "../../../utils" import * as utils from "../../../utils"
import { UserCtx, SSOProfile, DatasourceAuthCookie } from "@budibase/types" import { UserCtx, SSOProfile } from "@budibase/types"
import { ssoSaveUserNoOp } from "../sso/sso" import { ssoSaveUserNoOp } from "../sso/sso"
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy

View File

@ -5,7 +5,6 @@ import * as context from "../../../context"
import fetch from "node-fetch" import fetch from "node-fetch"
import { import {
SaveSSOUserFunction, SaveSSOUserFunction,
SaveUserOpts,
SSOAuthDetails, SSOAuthDetails,
SSOUser, SSOUser,
User, User,
@ -14,10 +13,8 @@ import {
// no-op function for user save // no-op function for user save
// - this allows datasource auth and access token refresh to work correctly // - this allows datasource auth and access token refresh to work correctly
// - prefer no-op over an optional argument to ensure function is provided to login flows // - prefer no-op over an optional argument to ensure function is provided to login flows
export const ssoSaveUserNoOp: SaveSSOUserFunction = ( export const ssoSaveUserNoOp: SaveSSOUserFunction = (user: SSOUser) =>
user: SSOUser, Promise.resolve(user)
opts: SaveUserOpts
) => Promise.resolve(user)
/** /**
* Common authentication logic for third parties. e.g. OAuth, OIDC. * Common authentication logic for third parties. e.g. OAuth, OIDC.

View File

@ -45,10 +45,6 @@ export const runMigration = async (
options: MigrationOptions = {} options: MigrationOptions = {}
) => { ) => {
const migrationType = migration.type const migrationType = migration.type
let tenantId: string | undefined
if (migrationType !== MigrationType.INSTALLATION) {
tenantId = context.getTenantId()
}
const migrationName = migration.name const migrationName = migration.name
const silent = migration.silent const silent = migration.silent

View File

@ -126,7 +126,7 @@ describe("app", () => {
it("gets url with embedded minio", async () => { it("gets url with embedded minio", async () => {
testEnv.withMinio() testEnv.withMinio()
await testEnv.withTenant(tenantId => { await testEnv.withTenant(() => {
const url = getAppFileUrl() const url = getAppFileUrl()
expect(url).toBe( expect(url).toBe(
"/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg" "/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg"
@ -136,7 +136,7 @@ describe("app", () => {
it("gets url with custom S3", async () => { it("gets url with custom S3", async () => {
testEnv.withS3() testEnv.withS3()
await testEnv.withTenant(tenantId => { await testEnv.withTenant(() => {
const url = getAppFileUrl() const url = getAppFileUrl()
expect(url).toBe( expect(url).toBe(
"http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg" "http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg"
@ -146,7 +146,7 @@ describe("app", () => {
it("gets url with cloudfront + s3", async () => { it("gets url with cloudfront + s3", async () => {
testEnv.withCloudfront() testEnv.withCloudfront()
await testEnv.withTenant(tenantId => { await testEnv.withTenant(() => {
const url = getAppFileUrl() const url = getAppFileUrl()
// omit rest of signed params // omit rest of signed params
expect( expect(

View File

@ -3,7 +3,7 @@ import { DBTestConfiguration } from "../../../tests/extra"
import * as tenants from "../tenants" import * as tenants from "../tenants"
describe("tenants", () => { describe("tenants", () => {
const config = new DBTestConfiguration() new DBTestConfiguration()
describe("addTenant", () => { describe("addTenant", () => {
it("concurrently adds multiple tenants safely", async () => { it("concurrently adds multiple tenants safely", async () => {

View File

@ -39,7 +39,7 @@ class InMemoryQueue implements Partial<Queue> {
_opts?: QueueOptions _opts?: QueueOptions
_messages: JobMessage[] _messages: JobMessage[]
_queuedJobIds: Set<string> _queuedJobIds: Set<string>
_emitter: EventEmitter _emitter: NodeJS.EventEmitter
_runCount: number _runCount: number
_addCount: number _addCount: number
@ -166,7 +166,7 @@ class InMemoryQueue implements Partial<Queue> {
return [] return []
} }
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
async removeJobs(pattern: string) { async removeJobs(pattern: string) {
// no-op // no-op
} }

View File

@ -132,7 +132,7 @@ function logging(queue: Queue, jobQueue: JobQueue) {
// A Job is waiting to be processed as soon as a worker is idling. // A Job is waiting to be processed as soon as a worker is idling.
console.info(...getLogParams(eventType, BullEvent.WAITING, { jobId })) console.info(...getLogParams(eventType, BullEvent.WAITING, { jobId }))
}) })
.on(BullEvent.ACTIVE, async (job: Job, jobPromise: any) => { .on(BullEvent.ACTIVE, async (job: Job) => {
// A job has started. You can use `jobPromise.cancel()`` to abort it. // A job has started. You can use `jobPromise.cancel()`` to abort it.
await doInJobContext(job, () => { await doInJobContext(job, () => {
console.info(...getLogParams(eventType, BullEvent.ACTIVE, { job })) console.info(...getLogParams(eventType, BullEvent.ACTIVE, { job }))

View File

@ -40,6 +40,7 @@ export async function shutdown() {
if (inviteClient) await inviteClient.finish() if (inviteClient) await inviteClient.finish()
if (passwordResetClient) await passwordResetClient.finish() if (passwordResetClient) await passwordResetClient.finish()
if (socketClient) await socketClient.finish() if (socketClient) await socketClient.finish()
if (docWritethroughClient) await docWritethroughClient.finish()
} }
process.on("exit", async () => { process.on("exit", async () => {

View File

@ -120,7 +120,7 @@ describe("redis", () => {
await redis.bulkStore(data, ttl) await redis.bulkStore(data, ttl)
for (const [key, value] of Object.entries(data)) { for (const key of Object.keys(data)) {
expect(await redis.get(key)).toBe(null) expect(await redis.get(key)).toBe(null)
} }

View File

@ -45,7 +45,7 @@ describe("Users", () => {
...{ _id: groupId, roles: { app1: "ADMIN" } }, ...{ _id: groupId, roles: { app1: "ADMIN" } },
} }
const users: User[] = [] const users: User[] = []
for (const _ of Array.from({ length: usersInGroup })) { for (let i = 0; i < usersInGroup; i++) {
const userId = `us_${generator.guid()}` const userId = `us_${generator.guid()}`
const user: User = structures.users.user({ const user: User = structures.users.user({
_id: userId, _id: userId,

View File

@ -39,19 +39,23 @@ const handleClick = event => {
return return
} }
if (handler.allowedType && event.type !== handler.allowedType) {
return
}
handler.callback?.(event) handler.callback?.(event)
}) })
} }
document.documentElement.addEventListener("click", handleClick, true) document.documentElement.addEventListener("click", handleClick, true)
document.documentElement.addEventListener("contextmenu", handleClick, true) document.documentElement.addEventListener("mousedown", handleClick, true)
/** /**
* Adds or updates a click handler * Adds or updates a click handler
*/ */
const updateHandler = (id, element, anchor, callback) => { const updateHandler = (id, element, anchor, callback, allowedType) => {
let existingHandler = clickHandlers.find(x => x.id === id) let existingHandler = clickHandlers.find(x => x.id === id)
if (!existingHandler) { if (!existingHandler) {
clickHandlers.push({ id, element, anchor, callback }) clickHandlers.push({ id, element, anchor, callback, allowedType })
} else { } else {
existingHandler.callback = callback existingHandler.callback = callback
} }
@ -75,9 +79,11 @@ const removeHandler = id => {
export default (element, opts) => { export default (element, opts) => {
const id = Math.random() const id = Math.random()
const update = newOpts => { const update = newOpts => {
const callback = newOpts?.callback || newOpts const callback =
newOpts?.callback || (typeof newOpts === "function" ? newOpts : null)
const anchor = newOpts?.anchor || element const anchor = newOpts?.anchor || element
updateHandler(id, element, anchor, callback) const allowedType = newOpts?.allowedType || "click"
updateHandler(id, element, anchor, callback, allowedType)
} }
update(opts) update(opts)
return { return {

View File

@ -42,7 +42,6 @@
.main { .main {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
overflow-x: hidden;
} }
.padding .main { .padding .main {
padding: var(--spacing-xl); padding: var(--spacing-xl);

View File

@ -12,6 +12,7 @@
export let schema export let schema
export let value export let value
export let customRenderers = [] export let customRenderers = []
export let snippets
let renderer let renderer
const typeMap = { const typeMap = {
@ -44,7 +45,7 @@
if (!template) { if (!template) {
return value return value
} }
return processStringSync(template, { value }) return processStringSync(template, { value, snippets })
} }
</script> </script>

View File

@ -42,6 +42,7 @@
export let customPlaceholder = false export let customPlaceholder = false
export let showHeaderBorder = true export let showHeaderBorder = true
export let placeholderText = "No rows found" export let placeholderText = "No rows found"
export let snippets = []
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -425,6 +426,7 @@
<CellRenderer <CellRenderer
{customRenderers} {customRenderers}
{row} {row}
{snippets}
schema={schema[field]} schema={schema[field]}
value={deepGet(row, field)} value={deepGet(row, field)}
on:clickrelationship on:clickrelationship

View File

@ -34,7 +34,12 @@
import { getBindings } from "components/backend/DataTable/formula" import { getBindings } from "components/backend/DataTable/formula"
import JSONSchemaModal from "./JSONSchemaModal.svelte" import JSONSchemaModal from "./JSONSchemaModal.svelte"
import { ValidColumnNameRegex } from "@budibase/shared-core" import { ValidColumnNameRegex } from "@budibase/shared-core"
import { FieldType, FieldSubtype, SourceName } from "@budibase/types" import {
FieldType,
FieldSubtype,
SourceName,
FieldTypeSubtypes,
} from "@budibase/types"
import RelationshipSelector from "components/common/RelationshipSelector.svelte" import RelationshipSelector from "components/common/RelationshipSelector.svelte"
import { RowUtils } from "@budibase/frontend-core" import { RowUtils } from "@budibase/frontend-core"
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte" import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
@ -191,8 +196,10 @@
// don't make field IDs for auto types // don't make field IDs for auto types
if (type === AUTO_TYPE || autocolumn) { if (type === AUTO_TYPE || autocolumn) {
return type.toUpperCase() return type.toUpperCase()
} else { } else if (type === FieldType.BB_REFERENCE) {
return `${type}${subtype || ""}`.toUpperCase() return `${type}${subtype || ""}`.toUpperCase()
} else {
return type.toUpperCase()
} }
} }
@ -706,17 +713,14 @@
/> />
{:else if editableColumn.type === FieldType.ATTACHMENT} {:else if editableColumn.type === FieldType.ATTACHMENT}
<Toggle <Toggle
value={editableColumn.constraints?.length?.maximum !== 1} value={editableColumn.subtype !== FieldTypeSubtypes.ATTACHMENT.SINGLE &&
// Checking config before the subtype was added
editableColumn.constraints?.length?.maximum !== 1}
on:change={e => { on:change={e => {
if (!e.detail) { if (!e.detail) {
editableColumn.constraints ??= { length: {} } editableColumn.subtype = FieldTypeSubtypes.ATTACHMENT.SINGLE
editableColumn.constraints.length ??= {}
editableColumn.constraints.length.maximum = 1
editableColumn.constraints.length.message =
"cannot contain multiple files"
} else { } else {
delete editableColumn.constraints?.length?.maximum delete editableColumn.subtype
delete editableColumn.constraints?.length?.message
} }
}} }}
thin thin

View File

@ -28,7 +28,6 @@
let deleteTableName let deleteTableName
$: externalTable = table?.sourceType === DB_TYPE_EXTERNAL $: externalTable = table?.sourceType === DB_TYPE_EXTERNAL
$: allowDeletion = !externalTable || table?.created
function showDeleteModal() { function showDeleteModal() {
templateScreens = $screenStore.screens.filter( templateScreens = $screenStore.screens.filter(
@ -56,7 +55,7 @@
$goto(`./datasource/${table.datasourceId}`) $goto(`./datasource/${table.datasourceId}`)
} }
} catch (error) { } catch (error) {
notifications.error("Error deleting table") notifications.error(`Error deleting table - ${error.message}`)
} }
} }
@ -86,17 +85,15 @@
} }
</script> </script>
{#if allowDeletion} <ActionMenu>
<ActionMenu> <div slot="control" class="icon">
<div slot="control" class="icon"> <Icon s hoverable name="MoreSmallList" />
<Icon s hoverable name="MoreSmallList" /> </div>
</div> {#if !externalTable}
{#if !externalTable} <MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem> {/if}
{/if} <MenuItem icon="Delete" on:click={showDeleteModal}>Delete</MenuItem>
<MenuItem icon="Delete" on:click={showDeleteModal}>Delete</MenuItem> </ActionMenu>
</ActionMenu>
{/if}
<Modal bind:this={editorModal} on:show={initForm}> <Modal bind:this={editorModal} on:show={initForm}>
<ModalContent <ModalContent

View File

@ -313,7 +313,7 @@ export const bindingsToCompletions = (bindings, mode) => {
...bindingByCategory[catKey].reduce((acc, binding) => { ...bindingByCategory[catKey].reduce((acc, binding) => {
let displayType = binding.fieldSchema?.type || binding.display?.type let displayType = binding.fieldSchema?.type || binding.display?.type
acc.push({ acc.push({
label: binding.display?.name || "NO NAME", label: binding.display?.name || binding.readableBinding || "NO NAME",
info: completion => { info: completion => {
return buildBindingInfoNode(completion, binding) return buildBindingInfoNode(completion, binding)
}, },

View File

@ -8,6 +8,7 @@
export let allowJS = false export let allowJS = false
export let allowHelpers = true export let allowHelpers = true
export let autofocusEditor = false export let autofocusEditor = false
export let context = null
$: enrichedBindings = enrichBindings(bindings) $: enrichedBindings = enrichBindings(bindings)
@ -27,7 +28,7 @@
<BindingPanel <BindingPanel
bindings={enrichedBindings} bindings={enrichedBindings}
context={$previewStore.selectedComponentContext} context={{ ...$previewStore.selectedComponentContext, ...context }}
snippets={$snippets} snippets={$snippets}
{value} {value}
{allowJS} {allowJS}

View File

@ -44,6 +44,7 @@
let appActionPopoverOpen = false let appActionPopoverOpen = false
let appActionPopoverAnchor let appActionPopoverAnchor
let publishing = false let publishing = false
let lastOpened
$: filteredApps = $appsStore.apps.filter(app => app.devId === application) $: filteredApps = $appsStore.apps.filter(app => app.devId === application)
$: selectedApp = filteredApps?.length ? filteredApps[0] : null $: selectedApp = filteredApps?.length ? filteredApps[0] : null
@ -57,7 +58,7 @@
$appStore.version && $appStore.version &&
$appStore.upgradableVersion !== $appStore.version $appStore.upgradableVersion !== $appStore.version
$: canPublish = !publishing && loaded && $sortedScreens.length > 0 $: canPublish = !publishing && loaded && $sortedScreens.length > 0
$: lastDeployed = getLastDeployedString($deploymentStore) $: lastDeployed = getLastDeployedString($deploymentStore, lastOpened)
const initialiseApp = async () => { const initialiseApp = async () => {
const applicationPkg = await API.fetchAppPackage($appStore.devId) const applicationPkg = await API.fetchAppPackage($appStore.devId)
@ -201,6 +202,7 @@
class="app-action-button publish app-action-popover" class="app-action-button publish app-action-popover"
on:click={() => { on:click={() => {
if (!appActionPopoverOpen) { if (!appActionPopoverOpen) {
lastOpened = new Date()
appActionPopover.show() appActionPopover.show()
} else { } else {
appActionPopover.hide() appActionPopover.hide()

View File

@ -7,10 +7,13 @@
Layout, Layout,
Label, Label,
} from "@budibase/bbui" } from "@budibase/bbui"
import { themeStore } from "stores/builder" import { themeStore, previewStore } from "stores/builder"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let column export let column
$: columnValue =
$previewStore.selectedComponentContext?.eventContext?.row?.[column.name]
</script> </script>
<DrawerContent> <DrawerContent>
@ -41,6 +44,9 @@
icon: "TableColumnMerge", icon: "TableColumnMerge",
}, },
]} ]}
context={{
value: columnValue,
}}
/> />
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">
<Label>Background color</Label> <Label>Background color</Label>

View File

@ -129,10 +129,7 @@
filteredUsers = $usersFetch.rows filteredUsers = $usersFetch.rows
.filter(user => user.email !== $auth.user.email) .filter(user => user.email !== $auth.user.email)
.map(user => { .map(user => {
const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder( const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder(user)
user,
prodAppId
)
const isAppBuilder = user.builder?.apps?.includes(prodAppId) const isAppBuilder = user.builder?.apps?.includes(prodAppId)
let role let role
if (isAdminOrGlobalBuilder) { if (isAdminOrGlobalBuilder) {

View File

@ -3,8 +3,6 @@
"name": "Blocks", "name": "Blocks",
"icon": "Article", "icon": "Article",
"children": [ "children": [
"gridblock",
"tableblock",
"cardsblock", "cardsblock",
"repeaterblock", "repeaterblock",
"formblock", "formblock",
@ -16,7 +14,7 @@
{ {
"name": "Layout", "name": "Layout",
"icon": "ClassicGridView", "icon": "ClassicGridView",
"children": ["container", "section", "grid", "sidepanel"] "children": ["container", "section", "sidepanel"]
}, },
{ {
"name": "Data", "name": "Data",
@ -24,7 +22,7 @@
"children": [ "children": [
"dataprovider", "dataprovider",
"repeater", "repeater",
"table", "gridblock",
"spreadsheet", "spreadsheet",
"dynamicfilter", "dynamicfilter",
"daterangepicker" "daterangepicker"

View File

@ -19,7 +19,8 @@
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { TOUR_KEYS } from "components/portal/onboarding/tours.js" import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
import formScreen from "templates/formScreen" import formScreen from "templates/formScreen"
import rowListScreen from "templates/rowListScreen" import gridListScreen from "templates/gridListScreen"
import gridDetailsScreen from "templates/gridDetailsScreen"
let mode let mode
let pendingScreen let pendingScreen
@ -127,7 +128,7 @@
screenAccessRole = Roles.BASIC screenAccessRole = Roles.BASIC
formType = null formType = null
if (mode === "table" || mode === "grid" || mode === "form") { if (mode === "grid" || mode === "gridDetails" || mode === "form") {
datasourceModal.show() datasourceModal.show()
} else if (mode === "blank") { } else if (mode === "blank") {
let templates = getTemplates($tables.list) let templates = getTemplates($tables.list)
@ -153,7 +154,10 @@
// Handler for Datasource Screen Creation // Handler for Datasource Screen Creation
const completeDatasourceScreenCreation = async () => { const completeDatasourceScreenCreation = async () => {
templates = rowListScreen(selectedDatasources, mode) templates =
mode === "grid"
? gridListScreen(selectedDatasources)
: gridDetailsScreen(selectedDatasources)
const screens = templates.map(template => { const screens = templates.map(template => {
let screenTemplate = template.create() let screenTemplate = template.create()

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -2,8 +2,8 @@
import { Body } from "@budibase/bbui" import { Body } from "@budibase/bbui"
import CreationPage from "components/common/CreationPage.svelte" import CreationPage from "components/common/CreationPage.svelte"
import blankImage from "./images/blank.png" import blankImage from "./images/blank.png"
import tableImage from "./images/table.png" import tableInline from "./images/tableInline.png"
import gridImage from "./images/grid.png" import tableDetails from "./images/tableDetails.png"
import formImage from "./images/form.png" import formImage from "./images/form.png"
import CreateScreenModal from "./CreateScreenModal.svelte" import CreateScreenModal from "./CreateScreenModal.svelte"
import { screenStore } from "stores/builder" import { screenStore } from "stores/builder"
@ -38,23 +38,23 @@
</div> </div>
</div> </div>
<div class="card" on:click={() => createScreenModal.show("table")}> <div class="card" on:click={() => createScreenModal.show("grid")}>
<div class="image"> <div class="image">
<img alt="" src={tableImage} /> <img alt="" src={tableInline} />
</div> </div>
<div class="text"> <div class="text">
<Body size="S">Table</Body> <Body size="S">Table with inline editing</Body>
<Body size="XS">View, edit and delete rows on a table</Body> <Body size="XS">View, edit and delete rows inline</Body>
</div> </div>
</div> </div>
<div class="card" on:click={() => createScreenModal.show("grid")}> <div class="card" on:click={() => createScreenModal.show("gridDetails")}>
<div class="image"> <div class="image">
<img alt="" src={gridImage} /> <img alt="" src={tableDetails} />
</div> </div>
<div class="text"> <div class="text">
<Body size="S">Grid</Body> <Body size="S">Table with details panel</Body>
<Body size="XS">View and manipulate rows on a grid</Body> <Body size="XS">Manage your row details in a side panel</Body>
</div> </div>
</div> </div>
@ -113,6 +113,11 @@
width: 100%; width: 100%;
} }
.card .image {
min-height: 130px;
min-width: 235px;
}
.text { .text {
border: 1px solid var(--grey-4); border: 1px solid var(--grey-4);
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;

View File

@ -279,12 +279,10 @@ export class ComponentStore extends BudiStore {
else { else {
if (setting.type === "dataProvider") { if (setting.type === "dataProvider") {
// Validate data provider exists, or else clear it // Validate data provider exists, or else clear it
const treeId = parent?._id || component._id const providers = findAllMatchingComponents(
const path = findComponentPath(screen?.props, treeId) screen?.props,
const providers = path.filter(component => x => x._component === "@budibase/standard-components/dataprovider"
component._component?.endsWith("/dataprovider")
) )
// Validate non-empty values
const valid = providers?.some(dp => value.includes?.(dp._id)) const valid = providers?.some(dp => value.includes?.(dp._id))
if (!valid) { if (!valid) {
if (providers.length) { if (providers.length) {

View File

@ -7,12 +7,25 @@ export const INITIAL_HOVER_STATE = {
} }
export class HoverStore extends BudiStore { export class HoverStore extends BudiStore {
hoverTimeout
constructor() { constructor() {
super({ ...INITIAL_HOVER_STATE }) super({ ...INITIAL_HOVER_STATE })
this.hover = this.hover.bind(this) this.hover = this.hover.bind(this)
} }
hover(componentId, notifyClient = true) { hover(componentId, notifyClient = true) {
clearTimeout(this.hoverTimeout)
if (componentId) {
this.processHover(componentId, notifyClient)
} else {
this.hoverTimeout = setTimeout(() => {
this.processHover(componentId, notifyClient)
}, 10)
}
}
processHover(componentId, notifyClient) {
if (componentId === get(this.store).componentId) { if (componentId === get(this.store).componentId) {
return return
} }

View File

@ -0,0 +1,158 @@
import sanitizeUrl from "helpers/sanitizeUrl"
import { Screen } from "./Screen"
import { Component } from "./Component"
import { generate } from "shortid"
import { makePropSafe as safe } from "@budibase/string-templates"
import { Utils } from "@budibase/frontend-core"
export default function (datasources) {
if (!Array.isArray(datasources)) {
return []
}
return datasources.map(datasource => {
return {
name: `${datasource.label} - List with panel`,
create: () => createScreen(datasource),
id: GRID_DETAILS_TEMPLATE,
resourceId: datasource.resourceId,
}
})
}
export const GRID_DETAILS_TEMPLATE = "GRID_DETAILS_TEMPLATE"
export const gridDetailsUrl = datasource => sanitizeUrl(`/${datasource.label}`)
const createScreen = datasource => {
/*
Create Row
*/
const createRowSidePanel = new Component(
"@budibase/standard-components/sidepanel"
).instanceName("New row side panel")
const buttonGroup = new Component("@budibase/standard-components/buttongroup")
const createButton = new Component("@budibase/standard-components/button")
createButton.customProps({
onClick: [
{
id: 0,
"##eventHandlerType": "Open Side Panel",
parameters: {
id: createRowSidePanel._json._id,
},
},
],
text: "Create row",
type: "cta",
})
buttonGroup.instanceName(`${datasource.label} - Create`).customProps({
hAlign: "right",
buttons: [createButton.json()],
})
const gridHeader = new Component("@budibase/standard-components/container")
.instanceName("Heading container")
.customProps({
direction: "row",
hAlign: "stretch",
})
const heading = new Component("@budibase/standard-components/heading")
.instanceName("Table heading")
.customProps({
text: datasource?.label,
})
gridHeader.addChild(heading)
gridHeader.addChild(buttonGroup)
const createFormBlock = new Component(
"@budibase/standard-components/formblock"
)
createFormBlock.instanceName("Create row form block").customProps({
dataSource: datasource,
labelPosition: "left",
buttonPosition: "top",
actionType: "Create",
title: "Create row",
buttons: Utils.buildFormBlockButtonConfig({
_id: createFormBlock._json._id,
showDeleteButton: false,
showSaveButton: true,
saveButtonLabel: "Save",
actionType: "Create",
dataSource: datasource,
}),
})
createRowSidePanel.addChild(createFormBlock)
/*
Edit Row
*/
const stateKey = `ID_${generate()}`
const detailsSidePanel = new Component(
"@budibase/standard-components/sidepanel"
).instanceName("Edit row side panel")
const editFormBlock = new Component("@budibase/standard-components/formblock")
editFormBlock.instanceName("Edit row form block").customProps({
dataSource: datasource,
labelPosition: "left",
buttonPosition: "top",
actionType: "Update",
title: "Edit",
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
buttons: Utils.buildFormBlockButtonConfig({
_id: editFormBlock._json._id,
showDeleteButton: true,
showSaveButton: true,
saveButtonLabel: "Save",
deleteButtonLabel: "Delete",
actionType: "Update",
dataSource: datasource,
}),
})
detailsSidePanel.addChild(editFormBlock)
const gridBlock = new Component("@budibase/standard-components/gridblock")
gridBlock
.customProps({
table: datasource,
allowAddRows: false,
allowEditRows: false,
allowDeleteRows: false,
onRowClick: [
{
id: 0,
"##eventHandlerType": "Update State",
parameters: {
key: stateKey,
type: "set",
persist: false,
value: `{{ ${safe("eventContext")}.${safe("row")}._id }}`,
},
},
{
id: 1,
"##eventHandlerType": "Open Side Panel",
parameters: {
id: detailsSidePanel._json._id,
},
},
],
})
.instanceName(`${datasource.label} - Table`)
return new Screen()
.route(gridDetailsUrl(datasource))
.instanceName(`${datasource.label} - List and details`)
.addChild(gridHeader)
.addChild(gridBlock)
.addChild(createRowSidePanel)
.addChild(detailsSidePanel)
.json()
}

View File

@ -0,0 +1,41 @@
import sanitizeUrl from "helpers/sanitizeUrl"
import { Screen } from "./Screen"
import { Component } from "./Component"
export default function (datasources) {
if (!Array.isArray(datasources)) {
return []
}
return datasources.map(datasource => {
return {
name: `${datasource.label} - List`,
create: () => createScreen(datasource),
id: GRID_LIST_TEMPLATE,
resourceId: datasource.resourceId,
}
})
}
export const GRID_LIST_TEMPLATE = "GRID_LIST_TEMPLATE"
export const gridListUrl = datasource => sanitizeUrl(`/${datasource.label}`)
const createScreen = datasource => {
const heading = new Component("@budibase/standard-components/heading")
.instanceName("Table heading")
.customProps({
text: datasource?.label,
})
const gridBlock = new Component("@budibase/standard-components/gridblock")
.instanceName(`${datasource.label} - Table`)
.customProps({
table: datasource,
})
return new Screen()
.route(gridListUrl(datasource))
.instanceName(`${datasource.label} - List`)
.addChild(heading)
.addChild(gridBlock)
.json()
}

View File

@ -1,9 +1,11 @@
import rowListScreen from "./rowListScreen" import gridListScreen from "./gridListScreen"
import gridDetailsScreen from "./gridDetailsScreen"
import createFromScratchScreen from "./createFromScratchScreen" import createFromScratchScreen from "./createFromScratchScreen"
import formScreen from "./formScreen" import formScreen from "./formScreen"
const allTemplates = datasources => [ const allTemplates = datasources => [
...rowListScreen(datasources), ...gridListScreen(datasources),
...gridDetailsScreen(datasources),
...formScreen(datasources), ...formScreen(datasources),
] ]

View File

@ -1,63 +0,0 @@
import sanitizeUrl from "helpers/sanitizeUrl"
import { Screen } from "./Screen"
import { Component } from "./Component"
export default function (datasources, mode = "table") {
if (!Array.isArray(datasources)) {
return []
}
return datasources.map(datasource => {
return {
name: `${datasource.label} - List`,
create: () => createScreen(datasource, mode),
id: ROW_LIST_TEMPLATE,
resourceId: datasource.resourceId,
}
})
}
export const ROW_LIST_TEMPLATE = "ROW_LIST_TEMPLATE"
export const rowListUrl = datasource => sanitizeUrl(`/${datasource.label}`)
const generateTableBlock = datasource => {
const tableBlock = new Component("@budibase/standard-components/tableblock")
tableBlock
.customProps({
title: datasource.label,
dataSource: datasource,
sortOrder: "Ascending",
size: "spectrum--medium",
paginate: true,
rowCount: 8,
clickBehaviour: "details",
showTitleButton: true,
titleButtonText: "Create row",
titleButtonClickBehaviour: "new",
sidePanelSaveLabel: "Save",
sidePanelDeleteLabel: "Delete",
})
.instanceName(`${datasource.label} - Table block`)
return tableBlock
}
const generateGridBlock = datasource => {
const gridBlock = new Component("@budibase/standard-components/gridblock")
gridBlock
.customProps({
table: datasource,
})
.instanceName(`${datasource.label} - Grid block`)
return gridBlock
}
const createScreen = (datasource, mode) => {
return new Screen()
.route(rowListUrl(datasource))
.instanceName(`${datasource.label} - List`)
.addChild(
mode === "table"
? generateTableBlock(datasource)
: generateGridBlock(datasource)
)
.json()
}

View File

@ -4722,6 +4722,7 @@
} }
}, },
"table": { "table": {
"deprecated": true,
"name": "Table", "name": "Table",
"icon": "Table", "icon": "Table",
"illegalChildren": ["section"], "illegalChildren": ["section"],
@ -5467,6 +5468,7 @@
] ]
}, },
"tableblock": { "tableblock": {
"deprecated": true,
"block": true, "block": true,
"name": "Table Block", "name": "Table Block",
"icon": "Table", "icon": "Table",
@ -6644,7 +6646,7 @@
] ]
}, },
"gridblock": { "gridblock": {
"name": "Grid Block", "name": "Table",
"icon": "Table", "icon": "Table",
"styles": ["size"], "styles": ["size"],
"size": { "size": {

View File

@ -246,15 +246,18 @@
return return
} }
const cacheId = `${definition.name}${
definition?.deprecated === true ? "_deprecated" : ""
}`
// Get the settings definition for this component, and cache it // Get the settings definition for this component, and cache it
if (SettingsDefinitionCache[definition.name]) { if (SettingsDefinitionCache[cacheId]) {
settingsDefinition = SettingsDefinitionCache[definition.name] settingsDefinition = SettingsDefinitionCache[cacheId]
settingsDefinitionMap = SettingsDefinitionMapCache[definition.name] settingsDefinitionMap = SettingsDefinitionMapCache[cacheId]
} else { } else {
settingsDefinition = getSettingsDefinition(definition) settingsDefinition = getSettingsDefinition(definition)
settingsDefinitionMap = getSettingsDefinitionMap(settingsDefinition) settingsDefinitionMap = getSettingsDefinitionMap(settingsDefinition)
SettingsDefinitionCache[definition.name] = settingsDefinition SettingsDefinitionCache[cacheId] = settingsDefinition
SettingsDefinitionMapCache[definition.name] = settingsDefinitionMap SettingsDefinitionMapCache[cacheId] = settingsDefinitionMap
} }
// Parse the instance settings, and cache them // Parse the instance settings, and cache them

View File

@ -291,7 +291,10 @@
<div <div
id="side-panel-container" id="side-panel-container"
class:open={$sidePanelStore.open} class:open={$sidePanelStore.open}
use:clickOutside={autoCloseSidePanel ? sidePanelStore.actions.close : null} use:clickOutside={{
callback: autoCloseSidePanel ? sidePanelStore.actions.close : null,
allowedType: "mousedown",
}}
class:builder={$builderStore.inBuilder} class:builder={$builderStore.inBuilder}
> >
<div class="side-panel-header"> <div class="side-panel-header">

View File

@ -26,9 +26,12 @@
let schema let schema
$: id = $component.id
$: selected = $component.selected
$: builderStep = $builderStore.metadata?.step
$: fetchSchema(dataSource) $: fetchSchema(dataSource)
$: enrichedSteps = enrichSteps(steps, schema, $component.id, $currentStep) $: enrichedSteps = enrichSteps(steps, schema, id)
$: updateCurrentStep(enrichedSteps, $builderStore, $component) $: updateCurrentStep(enrichedSteps, selected, builderStep)
// Provide additional data context for live binding eval // Provide additional data context for live binding eval
export const getAdditionalDataContext = () => { export const getAdditionalDataContext = () => {
@ -40,30 +43,22 @@
} }
} }
const updateCurrentStep = (steps, builderStore, component) => { const updateCurrentStep = (steps, selected, builderStep) => {
const { componentId, step } = builderStore.metadata || {} // If we aren't selected in the builder then just allowing the normal form
// to take control.
// If we aren't in the builder or aren't selected then don't update the step if (!selected) {
// context at all, allowing the normal form to take control.
if (
!component.selected ||
!builderStore.inBuilder ||
componentId !== component.id
) {
return return
} }
// Ensure we have a valid step selected // Ensure we have a valid step selected
let newStep = Math.min(step || 0, steps.length - 1) let newStep = Math.min(builderStep || 0, steps.length - 1)
// Sanity check
newStep = Math.max(newStep, 0) newStep = Math.max(newStep, 0)
// Add 1 because the form component expects 1 indexed rather than 0 indexed // Add 1 because the form component expects 1 indexed rather than 0 indexed
currentStep.set(newStep + 1) currentStep.set(newStep + 1)
} }
const fetchSchema = async () => { const fetchSchema = async dataSource => {
schema = (await fetchDatasourceSchema(dataSource)) || {} schema = (await fetchDatasourceSchema(dataSource)) || {}
} }

View File

@ -34,6 +34,7 @@
$: formattedFields = convertOldFieldFormat(fields) $: formattedFields = convertOldFieldFormat(fields)
$: fieldsOrDefault = getDefaultFields(formattedFields, schema) $: fieldsOrDefault = getDefaultFields(formattedFields, schema)
$: fetchSchema(dataSource) $: fetchSchema(dataSource)
$: id = $component.id
// We could simply spread $$props into the inner form and append our // We could simply spread $$props into the inner form and append our
// additions, but that would create svelte warnings about unused props and // additions, but that would create svelte warnings about unused props and
// make maintenance in future more confusing as we typically always have a // make maintenance in future more confusing as we typically always have a
@ -53,7 +54,7 @@
buttons: buttons:
buttons || buttons ||
Utils.buildFormBlockButtonConfig({ Utils.buildFormBlockButtonConfig({
_id: $component.id, _id: id,
showDeleteButton, showDeleteButton,
showSaveButton, showSaveButton,
saveButtonLabel, saveButtonLabel,

View File

@ -1,4 +1,3 @@
export { default as tableblock } from "./TableBlock.svelte"
export { default as cardsblock } from "./CardsBlock.svelte" export { default as cardsblock } from "./CardsBlock.svelte"
export { default as repeaterblock } from "./RepeaterBlock.svelte" export { default as repeaterblock } from "./RepeaterBlock.svelte"
export { default as formblock } from "./form/FormBlock.svelte" export { default as formblock } from "./form/FormBlock.svelte"

View File

@ -1,5 +1,6 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { get } from "svelte/store"
import { generate } from "shortid" import { generate } from "shortid"
import Block from "components/Block.svelte" import Block from "components/Block.svelte"
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "components/BlockComponent.svelte"
@ -33,8 +34,9 @@
export let sidePanelDeleteLabel export let sidePanelDeleteLabel
export let notificationOverride export let notificationOverride
const { fetchDatasourceSchema, API } = getContext("sdk") const { fetchDatasourceSchema, API, generateGoldenSample } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
const context = getContext("context")
const stateKey = `ID_${generate()}` const stateKey = `ID_${generate()}`
let formId let formId
@ -48,20 +50,7 @@
let schemaLoaded = false let schemaLoaded = false
$: deleteLabel = setDeleteLabel(sidePanelDeleteLabel, sidePanelShowDelete) $: deleteLabel = setDeleteLabel(sidePanelDeleteLabel, sidePanelShowDelete)
$: id = $component.id
const setDeleteLabel = sidePanelDeleteLabel => {
// Accommodate old config to ensure delete button does not reappear
let labelText = sidePanelShowDelete === false ? "" : sidePanelDeleteLabel
// Empty text is considered hidden.
if (labelText?.trim() === "") {
return ""
}
// Default to "Delete" if the value is unset
return labelText || "Delete"
}
$: isDSPlus = dataSource?.type === "table" || dataSource?.type === "viewV2" $: isDSPlus = dataSource?.type === "table" || dataSource?.type === "viewV2"
$: fetchSchema(dataSource) $: fetchSchema(dataSource)
$: enrichSearchColumns(searchColumns, schema).then( $: enrichSearchColumns(searchColumns, schema).then(
@ -105,6 +94,30 @@
}, },
] ]
// Provide additional data context for live binding eval
export const getAdditionalDataContext = () => {
const rows = get(context)[dataProviderId]?.rows
const goldenRow = generateGoldenSample(rows)
return {
eventContext: {
row: goldenRow,
},
}
}
const setDeleteLabel = sidePanelDeleteLabel => {
// Accommodate old config to ensure delete button does not reappear
let labelText = sidePanelShowDelete === false ? "" : sidePanelDeleteLabel
// Empty text is considered hidden.
if (labelText?.trim() === "") {
return ""
}
// Default to "Delete" if the value is unset
return labelText || "Delete"
}
// Load the datasource schema so we can determine column types // Load the datasource schema so we can determine column types
const fetchSchema = async dataSource => { const fetchSchema = async dataSource => {
if (dataSource?.type === "table") { if (dataSource?.type === "table") {
@ -267,7 +280,7 @@
dataSource, dataSource,
buttonPosition: "top", buttonPosition: "top",
buttons: Utils.buildFormBlockButtonConfig({ buttons: Utils.buildFormBlockButtonConfig({
_id: $component.id + "-form-edit", _id: id + "-form-edit",
showDeleteButton: deleteLabel !== "", showDeleteButton: deleteLabel !== "",
showSaveButton: true, showSaveButton: true,
saveButtonLabel: sidePanelSaveLabel || "Save", saveButtonLabel: sidePanelSaveLabel || "Save",
@ -301,7 +314,7 @@
dataSource, dataSource,
buttonPosition: "top", buttonPosition: "top",
buttons: Utils.buildFormBlockButtonConfig({ buttons: Utils.buildFormBlockButtonConfig({
_id: $component.id + "-form-new", _id: id + "-form-new",
showDeleteButton: false, showDeleteButton: false,
showSaveButton: true, showSaveButton: true,
saveButtonLabel: "Save", saveButtonLabel: "Save",

View File

@ -3,7 +3,7 @@
import { Table } from "@budibase/bbui" import { Table } from "@budibase/bbui"
import SlotRenderer from "./SlotRenderer.svelte" import SlotRenderer from "./SlotRenderer.svelte"
import { canBeSortColumn } from "@budibase/shared-core" import { canBeSortColumn } from "@budibase/shared-core"
import Provider from "../../context/Provider.svelte" import Provider from "components/context/Provider.svelte"
export let dataProvider export let dataProvider
export let columns export let columns
@ -16,8 +16,15 @@
export let noRowsMessage export let noRowsMessage
const component = getContext("component") const component = getContext("component")
const { styleable, getAction, ActionTypes, rowSelectionStore } = const context = getContext("context")
getContext("sdk") const {
styleable,
getAction,
ActionTypes,
rowSelectionStore,
generateGoldenSample,
} = getContext("sdk")
const customColumnKey = `custom-${Math.random()}` const customColumnKey = `custom-${Math.random()}`
const customRenderers = [ const customRenderers = [
{ {
@ -28,6 +35,7 @@
let selectedRows = [] let selectedRows = []
$: snippets = $context.snippets
$: hasChildren = $component.children $: hasChildren = $component.children
$: loading = dataProvider?.loading ?? false $: loading = dataProvider?.loading ?? false
$: data = dataProvider?.rows || [] $: data = dataProvider?.rows || []
@ -61,6 +69,16 @@
selectedRows, selectedRows,
} }
// Provide additional data context for live binding eval
export const getAdditionalDataContext = () => {
const goldenRow = generateGoldenSample(data)
return {
eventContext: {
row: goldenRow,
},
}
}
const getFields = ( const getFields = (
schema, schema,
customColumns, customColumns,
@ -178,6 +196,7 @@
{quiet} {quiet}
{compact} {compact}
{customRenderers} {customRenderers}
{snippets}
allowSelectRows={allowSelectRows && table} allowSelectRows={allowSelectRows && table}
bind:selectedRows bind:selectedRows
allowEditRows={false} allowEditRows={false}

View File

@ -6,36 +6,24 @@ export const getOptions = (
valueColumn, valueColumn,
customOptions customOptions
) => { ) => {
const isArray = fieldSchema?.type === "array"
// Take options from schema // Take options from schema
if (optionsSource == null || optionsSource === "schema") { if (optionsSource == null || optionsSource === "schema") {
return fieldSchema?.constraints?.inclusion ?? [] return fieldSchema?.constraints?.inclusion ?? []
} }
if (optionsSource === "provider" && isArray) {
let optionsSet = {}
dataProvider?.rows?.forEach(row => {
const value = row?.[valueColumn]
if (value != null) {
const label = row[labelColumn] || value
optionsSet[value] = { value, label }
}
})
return Object.values(optionsSet)
}
// Extract options from data provider // Extract options from data provider
if (optionsSource === "provider" && valueColumn) { if (optionsSource === "provider" && valueColumn) {
let optionsSet = {} let valueCache = {}
let options = []
dataProvider?.rows?.forEach(row => { dataProvider?.rows?.forEach(row => {
const value = row?.[valueColumn] const value = row?.[valueColumn]
if (value != null) { if (value != null && !valueCache[value]) {
valueCache[value] = true
const label = row[labelColumn] || value const label = row[labelColumn] || value
optionsSet[value] = { value, label } options.push({ value, label })
} }
}) })
return Object.values(optionsSet) return options
} }
// Extract custom options // Extract custom options

View File

@ -40,11 +40,12 @@ export { default as sidepanel } from "./SidePanel.svelte"
export { default as gridblock } from "./GridBlock.svelte" export { default as gridblock } from "./GridBlock.svelte"
export * from "./charts" export * from "./charts"
export * from "./forms" export * from "./forms"
export * from "./table"
export * from "./blocks" export * from "./blocks"
export * from "./dynamic-filter" export * from "./dynamic-filter"
// Deprecated component left for compatibility in old apps // Deprecated component left for compatibility in old apps
export * from "./deprecated/table"
export { default as tableblock } from "./deprecated/TableBlock.svelte"
export { default as navigation } from "./deprecated/Navigation.svelte" export { default as navigation } from "./deprecated/Navigation.svelte"
export { default as cardhorizontal } from "./deprecated/CardHorizontal.svelte" export { default as cardhorizontal } from "./deprecated/CardHorizontal.svelte"
export { default as stackedlist } from "./deprecated/StackedList.svelte" export { default as stackedlist } from "./deprecated/StackedList.svelte"

View File

@ -345,8 +345,7 @@
<IndicatorSet <IndicatorSet
componentId={$dndParent} componentId={$dndParent}
color="var(--spectrum-global-color-static-green-500)" color="var(--spectrum-global-color-static-green-500)"
zIndex="930" zIndex={920}
transition
prefix="Inside" prefix="Inside"
/> />

View File

@ -1,10 +1,11 @@
<script> <script>
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import IndicatorSet from "./IndicatorSet.svelte" import IndicatorSet from "./IndicatorSet.svelte"
import { builderStore, dndIsDragging, hoverStore } from "stores" import { dndIsDragging, hoverStore, builderStore } from "stores"
$: componentId = $hoverStore.hoveredComponentId $: componentId = $hoverStore.hoveredComponentId
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920 $: selectedComponentId = $builderStore.selectedComponentId
$: selected = componentId === selectedComponentId
const onMouseOver = e => { const onMouseOver = e => {
// Ignore if dragging // Ignore if dragging
@ -45,7 +46,6 @@
<IndicatorSet <IndicatorSet
componentId={$dndIsDragging ? null : componentId} componentId={$dndIsDragging ? null : componentId}
color="var(--spectrum-global-color-static-blue-200)" color="var(--spectrum-global-color-static-blue-200)"
transition zIndex={selected ? 890 : 910}
{zIndex}
allowResizeAnchors allowResizeAnchors
/> />

View File

@ -1,5 +1,4 @@
<script> <script>
import { fade } from "svelte/transition"
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
export let top export let top
@ -11,7 +10,6 @@
export let color export let color
export let zIndex export let zIndex
export let componentId export let componentId
export let transition = false
export let line = false export let line = false
export let alignRight = false export let alignRight = false
export let showResizeAnchors = false export let showResizeAnchors = false
@ -31,10 +29,6 @@
</script> </script>
<div <div
in:fade={{
delay: transition ? 100 : 0,
duration: transition ? 100 : 0,
}}
class="indicator" class="indicator"
class:flipped class:flipped
class:line class:line
@ -127,10 +121,6 @@
font-weight: 600; font-weight: 600;
} }
/* Icon styles */
.label :global(.spectrum-Icon + .text) {
}
/* Anchor */ /* Anchor */
.anchor { .anchor {
--size: 24px; --size: 24px;

View File

@ -4,27 +4,39 @@
import { domDebounce } from "utils/domDebounce" import { domDebounce } from "utils/domDebounce"
import { builderStore } from "stores" import { builderStore } from "stores"
export let componentId export let componentId = null
export let color export let color = null
export let transition export let zIndex = 900
export let zIndex
export let prefix = null export let prefix = null
export let allowResizeAnchors = false export let allowResizeAnchors = false
let indicators = [] const errorColor = "var(--spectrum-global-color-static-red-600)"
const defaultState = () => ({
// Cached props
componentId,
color,
zIndex,
prefix,
allowResizeAnchors,
// Computed state
indicators: [],
text: null,
icon: null,
insideGrid: false,
error: false,
})
let interval let interval
let text let state = defaultState()
let icon let nextState = null
let insideGrid = false
let errorState = false
$: visibleIndicators = indicators.filter(x => x.visible)
$: offset = $builderStore.inBuilder ? 0 : 2
let updating = false let updating = false
let observers = [] let observers = []
let callbackCount = 0 let callbackCount = 0
let nextIndicators = []
$: visibleIndicators = state.indicators.filter(x => x.visible)
$: offset = $builderStore.inBuilder ? 0 : 2
$: $$props, debouncedUpdate()
const checkInsideGrid = id => { const checkInsideGrid = id => {
const component = document.getElementsByClassName(id)[0] const component = document.getElementsByClassName(id)[0]
@ -44,10 +56,10 @@
if (callbackCount >= observers.length) { if (callbackCount >= observers.length) {
return return
} }
nextIndicators[idx].visible = nextState.indicators[idx].visible =
nextIndicators[idx].insideSidePanel || entries[0].isIntersecting nextState.indicators[idx].insideSidePanel || entries[0].isIntersecting
if (++callbackCount === observers.length) { if (++callbackCount === observers.length) {
indicators = nextIndicators state = nextState
updating = false updating = false
} }
} }
@ -59,7 +71,7 @@
// Sanity check // Sanity check
if (!componentId) { if (!componentId) {
indicators = [] state = defaultState()
return return
} }
@ -68,25 +80,25 @@
callbackCount = 0 callbackCount = 0
observers.forEach(o => o.disconnect()) observers.forEach(o => o.disconnect())
observers = [] observers = []
nextIndicators = [] nextState = defaultState()
// Check if we're inside a grid // Check if we're inside a grid
if (allowResizeAnchors) { if (allowResizeAnchors) {
insideGrid = checkInsideGrid(componentId) nextState.insideGrid = checkInsideGrid(componentId)
} }
// Determine next set of indicators // Determine next set of indicators
const parents = document.getElementsByClassName(componentId) const parents = document.getElementsByClassName(componentId)
if (parents.length) { if (parents.length) {
text = parents[0].dataset.name nextState.text = parents[0].dataset.name
if (prefix) { if (nextState.prefix) {
text = `${prefix} ${text}` nextState.text = `${nextState.prefix} ${nextState.text}`
} }
if (parents[0].dataset.icon) { if (parents[0].dataset.icon) {
icon = parents[0].dataset.icon nextState.icon = parents[0].dataset.icon
} }
} }
errorState = parents?.[0]?.classList.contains("error") nextState.error = parents?.[0]?.classList.contains("error")
// Batch reads to minimize reflow // Batch reads to minimize reflow
const scrollX = window.scrollX const scrollX = window.scrollX
@ -102,8 +114,9 @@
// If there aren't any nodes then reset // If there aren't any nodes then reset
if (!children.length) { if (!children.length) {
indicators = [] state = defaultState()
updating = false updating = false
return
} }
const device = document.getElementById("app-root") const device = document.getElementById("app-root")
@ -119,7 +132,7 @@
observers.push(observer) observers.push(observer)
const elBounds = child.getBoundingClientRect() const elBounds = child.getBoundingClientRect()
nextIndicators.push({ nextState.indicators.push({
top: elBounds.top + scrollY - deviceBounds.top - offset, top: elBounds.top + scrollY - deviceBounds.top - offset,
left: elBounds.left + scrollX - deviceBounds.left - offset, left: elBounds.left + scrollX - deviceBounds.left - offset,
width: elBounds.width + 4, width: elBounds.width + 4,
@ -144,20 +157,17 @@
}) })
</script> </script>
{#key componentId} {#each visibleIndicators as indicator, idx}
{#each visibleIndicators as indicator, idx} <Indicator
<Indicator top={indicator.top}
top={indicator.top} left={indicator.left}
left={indicator.left} width={indicator.width}
width={indicator.width} height={indicator.height}
height={indicator.height} text={idx === 0 ? state.text : null}
text={idx === 0 ? text : null} icon={idx === 0 ? state.icon : null}
icon={idx === 0 ? icon : null} showResizeAnchors={state.allowResizeAnchors && state.insideGrid}
showResizeAnchors={allowResizeAnchors && insideGrid} color={state.error ? errorColor : state.color}
color={errorState ? "var(--spectrum-global-color-static-red-600)" : color} componentId={state.componentId}
{componentId} zIndex={state.zIndex}
{transition} />
{zIndex} {/each}
/>
{/each}
{/key}

View File

@ -10,7 +10,6 @@
<IndicatorSet <IndicatorSet
componentId={$builderStore.selectedComponentId} componentId={$builderStore.selectedComponentId}
{color} {color}
zIndex="910" zIndex={900}
transition
allowResizeAnchors allowResizeAnchors
/> />

View File

@ -98,7 +98,7 @@ const loadBudibase = async () => {
context: stringifiedContext, context: stringifiedContext,
}) })
} else if (type === "hover-component") { } else if (type === "hover-component") {
hoverStore.actions.hoverComponent(data) hoverStore.actions.hoverComponent(data, false)
} else if (type === "builder-meta") { } else if (type === "builder-meta") {
builderStore.actions.setMetadata(data) builderStore.actions.setMetadata(data)
} }

View File

@ -34,6 +34,8 @@ import {
LuceneUtils, LuceneUtils,
Constants, Constants,
RowUtils, RowUtils,
memo,
derivedMemo,
} from "@budibase/frontend-core" } from "@budibase/frontend-core"
export default { export default {
@ -71,6 +73,8 @@ export default {
makePropSafe, makePropSafe,
createContextStore, createContextStore,
generateGoldenSample: RowUtils.generateGoldenSample, generateGoldenSample: RowUtils.generateGoldenSample,
memo,
derivedMemo,
// Components // Components
Provider, Provider,

View File

@ -5,13 +5,27 @@ const createHoverStore = () => {
const store = writable({ const store = writable({
hoveredComponentId: null, hoveredComponentId: null,
}) })
let hoverTimeout
const hoverComponent = id => { const hoverComponent = (id, notifyBuilder = true) => {
clearTimeout(hoverTimeout)
if (id) {
processHover(id, notifyBuilder)
} else {
hoverTimeout = setTimeout(() => {
processHover(id, notifyBuilder)
}, 10)
}
}
const processHover = (id, notifyBuilder = true) => {
if (id === get(store).hoveredComponentId) { if (id === get(store).hoveredComponentId) {
return return
} }
store.set({ hoveredComponentId: id }) store.set({ hoveredComponentId: id })
eventStore.actions.dispatchEvent("hover-component", { id }) if (notifyBuilder) {
eventStore.actions.dispatchEvent("hover-component", { id })
}
} }
return { return {

View File

@ -40,16 +40,18 @@
} }
} }
// Handle certain key presses regardless of selection state
if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && $config.canAddRows) {
e.preventDefault()
dispatch("add-row-inline")
return
}
// If nothing selected avoid processing further key presses // If nothing selected avoid processing further key presses
if (!$focusedCellId) { if (!$focusedCellId) {
if (e.key === "Tab" || e.key?.startsWith("Arrow")) { if (e.key === "Tab" || e.key?.startsWith("Arrow")) {
e.preventDefault() e.preventDefault()
focusFirstCell() focusFirstCell()
} else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
if ($config.canAddRows) {
e.preventDefault()
dispatch("add-row-inline")
}
} else if (e.key === "Delete" || e.key === "Backspace") { } else if (e.key === "Delete" || e.key === "Backspace") {
if (Object.keys($selectedRows).length && $config.canDeleteRows) { if (Object.keys($selectedRows).length && $config.canDeleteRows) {
dispatch("request-bulk-delete") dispatch("request-bulk-delete")

@ -1 +1 @@
Subproject commit 7d1b3eaf33e560d19d591813e5bba91d75ef3953 Subproject commit dd748e045ffdbc6662c5d2b76075f01d65a96a2f

View File

@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module FirebaseMock { module FirebaseMock {
const firebase: any = {} const firebase: any = {}

View File

@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module SendgridMock { module SendgridMock {
class Email { class Email {
constructor() { constructor() {

View File

@ -1,8 +1,5 @@
module AirtableMock { class Airtable {
function Airtable() { base = jest.fn()
// @ts-ignore
this.base = jest.fn()
}
module.exports = Airtable
} }
module.exports = Airtable

View File

@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module ArangoMock { module ArangoMock {
const arangodb: any = {} const arangodb: any = {}

View File

@ -1,102 +1,81 @@
import fs from "fs" import fs from "fs"
import { join } from "path" import { join } from "path"
module AwsMock { const response = (body: any, extra?: any) => () => ({
const aws: any = {} promise: () => body,
...extra,
})
const response = (body: any, extra?: any) => () => ({ class DocumentClient {
promise: () => body, put = jest.fn(response({}))
...extra, query = jest.fn(
}) response({
Items: [],
function DocumentClient() { })
// @ts-ignore )
this.put = jest.fn(response({})) scan = jest.fn(
// @ts-ignore response({
this.query = jest.fn( Items: [
response({
Items: [],
})
)
// @ts-ignore
this.scan = jest.fn(
response({
Items: [
{
Name: "test",
},
],
})
)
// @ts-ignore
this.get = jest.fn(response({}))
// @ts-ignore
this.update = jest.fn(response({}))
// @ts-ignore
this.delete = jest.fn(response({}))
}
function S3() {
// @ts-ignore
this.listObjects = jest.fn(
response({
Contents: [],
})
)
// @ts-ignore
this.createBucket = jest.fn(
response({
Contents: {},
})
)
// @ts-ignore
this.deleteObjects = jest.fn(
response({
Contents: {},
})
)
// @ts-ignore
this.getSignedUrl = (operation, params) => {
return `http://example.com/${params.Bucket}/${params.Key}`
}
// @ts-ignore
this.headBucket = jest.fn(
response({
Contents: {},
})
)
// @ts-ignore
this.upload = jest.fn(
response({
Contents: {},
})
)
// @ts-ignore
this.getObject = jest.fn(
response(
{ {
Body: "", Name: "test",
}, },
{ ],
createReadStream: jest })
.fn() )
.mockReturnValue( get = jest.fn(response({}))
fs.createReadStream(join(__dirname, "aws-sdk.ts")) update = jest.fn(response({}))
), delete = jest.fn(response({}))
} }
)
) class S3 {
} listObjects = jest.fn(
response({
aws.DynamoDB = { DocumentClient } Contents: [],
aws.S3 = S3 })
aws.config = { update: jest.fn() } )
createBucket = jest.fn(
module.exports = aws response({
Contents: {},
})
)
deleteObjects = jest.fn(
response({
Contents: {},
})
)
getSignedUrl = jest.fn((operation, params) => {
return `http://example.com/${params.Bucket}/${params.Key}`
})
headBucket = jest.fn(
response({
Contents: {},
})
)
upload = jest.fn(
response({
Contents: {},
})
)
getObject = jest.fn(
response(
{
Body: "",
},
{
createReadStream: jest
.fn()
.mockReturnValue(fs.createReadStream(join(__dirname, "aws-sdk.ts"))),
}
)
)
}
module.exports = {
DynamoDB: {
DocumentClient,
},
S3,
config: {
update: jest.fn(),
},
} }

View File

@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module MongoMock { module MongoMock {
const mongodb: any = {} const mongodb: any = {}

View File

@ -1,24 +0,0 @@
module MsSqlMock {
const mssql: any = {}
mssql.query = jest.fn(() => ({
recordset: [
{
a: "string",
b: 1,
},
],
}))
// mssql.connect = jest.fn(() => ({ recordset: [] }))
mssql.ConnectionPool = jest.fn(() => ({
connect: jest.fn(() => ({
request: jest.fn(() => ({
query: jest.fn(sql => ({ recordset: [sql] })),
})),
})),
}))
module.exports = mssql
}

View File

@ -1,14 +0,0 @@
module MySQLMock {
const mysql: any = {}
const client = {
connect: jest.fn(),
query: jest.fn((query, bindings, fn) => {
fn(null, [])
}),
}
mysql.createConnection = jest.fn(() => client)
module.exports = mysql
}

View File

@ -1,17 +0,0 @@
module MySQLMock {
const mysql: any = {}
const client = {
connect: jest.fn(),
end: jest.fn(),
query: jest.fn(async () => {
return [[]]
}),
}
mysql.createConnection = jest.fn(async () => {
return client
})
module.exports = mysql
}

View File

@ -1,6 +1,7 @@
// @ts-ignore // @ts-ignore
import fs from "fs" import fs from "fs"
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module FetchMock { module FetchMock {
// @ts-ignore // @ts-ignore
const fetch = jest.requireActual("node-fetch") const fetch = jest.requireActual("node-fetch")

View File

@ -1,31 +1,21 @@
module OracleDbMock { const executeMock = jest.fn(() => ({
// mock execute rows: [
const execute = jest.fn(() => ({ {
rows: [ a: "string",
{ b: 1,
a: "string", },
b: 1, ],
}, }))
],
}))
const close = jest.fn() const closeMock = jest.fn()
// mock connection class Connection {
function Connection() {} execute = executeMock
Connection.prototype.execute = execute close = closeMock
Connection.prototype.close = close }
// mock oracledb module.exports = {
const oracleDb: any = {} getConnection: jest.fn(() => new Connection()),
oracleDb.getConnection = jest.fn(() => { executeMock,
// @ts-ignore closeMock,
return new Connection()
})
// expose mocks
oracleDb.executeMock = execute
oracleDb.closeMock = close
module.exports = oracleDb
} }

View File

@ -1,30 +1,25 @@
module PgMock { const query = jest.fn(() => ({
const pg: any = {} rows: [
{
a: "string",
b: 1,
},
],
}))
const query = jest.fn(() => ({ class Client {
rows: [ query = query
{ end = jest.fn(cb => {
a: "string",
b: 1,
},
],
}))
// constructor
function Client() {}
Client.prototype.query = query
Client.prototype.end = jest.fn(cb => {
if (cb) cb() if (cb) cb()
}) })
Client.prototype.connect = jest.fn() connect = jest.fn()
Client.prototype.release = jest.fn() release = jest.fn()
}
const on = jest.fn()
const on = jest.fn()
pg.Client = Client
pg.queryMock = query module.exports = {
pg.on = on Client,
queryMock: query,
module.exports = pg on,
} }

View File

@ -26,7 +26,6 @@ import {
env as envCore, env as envCore,
ErrorCode, ErrorCode,
events, events,
HTTPError,
migrations, migrations,
objectStore, objectStore,
roles, roles,

View File

@ -39,25 +39,28 @@ export async function create(ctx: any) {
let name = "PLUGIN_" + Math.floor(100000 + Math.random() * 900000) let name = "PLUGIN_" + Math.floor(100000 + Math.random() * 900000)
switch (source) { switch (source) {
case PluginSource.NPM: case PluginSource.NPM: {
const { metadata: metadataNpm, directory: directoryNpm } = const { metadata: metadataNpm, directory: directoryNpm } =
await npmUpload(url, name) await npmUpload(url, name)
metadata = metadataNpm metadata = metadataNpm
directory = directoryNpm directory = directoryNpm
break break
case PluginSource.GITHUB: }
case PluginSource.GITHUB: {
const { metadata: metadataGithub, directory: directoryGithub } = const { metadata: metadataGithub, directory: directoryGithub } =
await githubUpload(url, name, githubToken) await githubUpload(url, name, githubToken)
metadata = metadataGithub metadata = metadataGithub
directory = directoryGithub directory = directoryGithub
break break
case PluginSource.URL: }
case PluginSource.URL: {
const headersObj = headers || {} const headersObj = headers || {}
const { metadata: metadataUrl, directory: directoryUrl } = const { metadata: metadataUrl, directory: directoryUrl } =
await urlUpload(url, name, headersObj) await urlUpload(url, name, headersObj)
metadata = metadataUrl metadata = metadataUrl
directory = directoryUrl directory = directoryUrl
break break
}
} }
pluginCore.validate(metadata?.schema) pluginCore.validate(metadata?.schema)

View File

@ -109,13 +109,14 @@ export class OpenAPI2 extends OpenAPISource {
for (let param of allParams) { for (let param of allParams) {
if (parameterNotRef(param)) { if (parameterNotRef(param)) {
switch (param.in) { switch (param.in) {
case "query": case "query": {
let prefix = "" let prefix = ""
if (queryString) { if (queryString) {
prefix = "&" prefix = "&"
} }
queryString = `${queryString}${prefix}${param.name}={{${param.name}}}` queryString = `${queryString}${prefix}${param.name}={{${param.name}}}`
break break
}
case "header": case "header":
headers[param.name] = `{{${param.name}}}` headers[param.name] = `{{${param.name}}}`
break break
@ -125,7 +126,7 @@ export class OpenAPI2 extends OpenAPISource {
case "formData": case "formData":
// future enhancement // future enhancement
break break
case "body": case "body": {
// set the request body to the example provided // set the request body to the example provided
// future enhancement: generate an example from the schema // future enhancement: generate an example from the schema
let bodyParam: OpenAPIV2.InBodyParameterObject = let bodyParam: OpenAPIV2.InBodyParameterObject =
@ -135,6 +136,7 @@ export class OpenAPI2 extends OpenAPISource {
requestBody = schema.example requestBody = schema.example
} }
break break
}
} }
// add the parameter if it can be bound in our config // add the parameter if it can be bound in our config

View File

@ -161,13 +161,14 @@ export class OpenAPI3 extends OpenAPISource {
for (let param of allParams) { for (let param of allParams) {
if (parameterNotRef(param)) { if (parameterNotRef(param)) {
switch (param.in) { switch (param.in) {
case "query": case "query": {
let prefix = "" let prefix = ""
if (queryString) { if (queryString) {
prefix = "&" prefix = "&"
} }
queryString = `${queryString}${prefix}${param.name}={{${param.name}}}` queryString = `${queryString}${prefix}${param.name}={{${param.name}}}`
break break
}
case "header": case "header":
headers[param.name] = `{{${param.name}}}` headers[param.name] = `{{${param.name}}}`
break break

View File

@ -116,7 +116,7 @@ export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) {
target: prodDb.name, target: prodDb.name,
}) })
await replication.replicate({ await replication.replicate({
filter: (doc: any, params: any) => { filter: (doc: any) => {
return doc._id && doc._id.startsWith("role_") return doc._id && doc._id.startsWith("role_")
}, },
}) })

View File

@ -7,13 +7,11 @@ import {
FilterType, FilterType,
IncludeRelationship, IncludeRelationship,
ManyToManyRelationshipFieldMetadata, ManyToManyRelationshipFieldMetadata,
ManyToOneRelationshipFieldMetadata,
OneToManyRelationshipFieldMetadata, OneToManyRelationshipFieldMetadata,
Operation, Operation,
PaginationJson, PaginationJson,
RelationshipFieldMetadata, RelationshipFieldMetadata,
RelationshipsJson, RelationshipsJson,
RelationshipType,
Row, Row,
SearchFilters, SearchFilters,
SortJson, SortJson,
@ -717,7 +715,7 @@ export class ExternalRequest<T extends Operation> {
const rows = related[key]?.rows || [] const rows = related[key]?.rows || []
function relationshipMatchPredicate({ const relationshipMatchPredicate = ({
row, row,
linkPrimary, linkPrimary,
linkSecondary, linkSecondary,
@ -725,7 +723,7 @@ export class ExternalRequest<T extends Operation> {
row: Row row: Row
linkPrimary: string linkPrimary: string
linkSecondary?: string linkSecondary?: string
}) { }) => {
const matchesPrimaryLink = const matchesPrimaryLink =
row[linkPrimary] === relationship.id || row[linkPrimary] === relationship.id ||
row[linkPrimary] === body?.[linkPrimary] row[linkPrimary] === body?.[linkPrimary]

View File

@ -23,6 +23,12 @@ const DISABLED_WRITE_CLIENTS: SqlClient[] = [
SqlClient.ORACLE, SqlClient.ORACLE,
] ]
const DISABLED_OPERATIONS: Operation[] = [
Operation.CREATE_TABLE,
Operation.UPDATE_TABLE,
Operation.DELETE_TABLE,
]
class CharSequence { class CharSequence {
static alphabet = "abcdefghijklmnopqrstuvwxyz" static alphabet = "abcdefghijklmnopqrstuvwxyz"
counters: number[] counters: number[]
@ -59,13 +65,18 @@ export default class AliasTables {
} }
isAliasingEnabled(json: QueryJson, datasource: Datasource) { isAliasingEnabled(json: QueryJson, datasource: Datasource) {
const operation = json.endpoint.operation
const fieldLength = json.resource?.fields?.length const fieldLength = json.resource?.fields?.length
if (!fieldLength || fieldLength <= 0) { if (
!fieldLength ||
fieldLength <= 0 ||
DISABLED_OPERATIONS.includes(operation)
) {
return false return false
} }
try { try {
const sqlClient = getSQLClient(datasource) const sqlClient = getSQLClient(datasource)
const isWrite = WRITE_OPERATIONS.includes(json.endpoint.operation) const isWrite = WRITE_OPERATIONS.includes(operation)
const isDisabledClient = DISABLED_WRITE_CLIENTS.includes(sqlClient) const isDisabledClient = DISABLED_WRITE_CLIENTS.includes(sqlClient)
if (isWrite && isDisabledClient) { if (isWrite && isDisabledClient) {
return false return false

View File

@ -1,4 +1,3 @@
import { quotas } from "@budibase/pro"
import { import {
UserCtx, UserCtx,
ViewV2, ViewV2,

View File

@ -61,9 +61,6 @@ export async function destroy(ctx: UserCtx) {
const tableToDelete: TableRequest = await sdk.tables.getTable( const tableToDelete: TableRequest = await sdk.tables.getTable(
ctx.params.tableId ctx.params.tableId
) )
if (!tableToDelete || !tableToDelete.created) {
ctx.throw(400, "Cannot delete tables which weren't created in Budibase.")
}
const datasourceId = getDatasourceId(tableToDelete) const datasourceId = getDatasourceId(tableToDelete)
try { try {
const { datasource, table } = await sdk.tables.external.destroy( const { datasource, table } = await sdk.tables.external.destroy(

View File

@ -30,6 +30,8 @@ import {
View, View,
RelationshipFieldMetadata, RelationshipFieldMetadata,
FieldType, FieldType,
FieldTypeSubtypes,
AttachmentFieldMetadata,
} from "@budibase/types" } from "@budibase/types"
export async function clearColumns(table: Table, columnNames: string[]) { export async function clearColumns(table: Table, columnNames: string[]) {
@ -88,6 +90,27 @@ export async function checkForColumnUpdates(
// Update views // Update views
await checkForViewUpdates(updatedTable, deletedColumns, columnRename) await checkForViewUpdates(updatedTable, deletedColumns, columnRename)
} }
const changedAttachmentSubtypeColumns = Object.values(
updatedTable.schema
).filter(
(column): column is AttachmentFieldMetadata =>
column.type === FieldType.ATTACHMENT &&
column.subtype !== oldTable?.schema[column.name]?.subtype
)
for (const attachmentColumn of changedAttachmentSubtypeColumns) {
if (attachmentColumn.subtype === FieldTypeSubtypes.ATTACHMENT.SINGLE) {
attachmentColumn.constraints ??= { length: {} }
attachmentColumn.constraints.length ??= {}
attachmentColumn.constraints.length.maximum = 1
attachmentColumn.constraints.length.message =
"cannot contain multiple files"
} else {
delete attachmentColumn.constraints?.length?.maximum
delete attachmentColumn.constraints?.length?.message
}
}
return { rows: updatedRows, table: updatedTable } return { rows: updatedRows, table: updatedTable }
} }

View File

@ -1,6 +1,6 @@
import { generateUserFlagID, InternalTables } from "../../db/utils" import { generateUserFlagID, InternalTables } from "../../db/utils"
import { getFullUser } from "../../utilities/users" import { getFullUser } from "../../utilities/users"
import { cache, context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { import {
ContextUserMetadata, ContextUserMetadata,
Ctx, Ctx,

View File

@ -24,7 +24,7 @@ async function parseSchema(view: CreateViewRequest) {
icon: schemaValue.icon, icon: schemaValue.icon,
} }
Object.entries(fieldSchema) Object.entries(fieldSchema)
.filter(([_, val]) => val === undefined) .filter(([, val]) => val === undefined)
.forEach(([key]) => { .forEach(([key]) => {
delete fieldSchema[key as keyof UIFieldMetadata] delete fieldSchema[key as keyof UIFieldMetadata]
}) })

View File

@ -33,7 +33,6 @@ export { default as staticRoutes } from "./static"
export { default as publicRoutes } from "./public" export { default as publicRoutes } from "./public"
const appBackupRoutes = pro.appBackups const appBackupRoutes = pro.appBackups
const scheduleRoutes = pro.schedules
const environmentVariableRoutes = pro.environmentVariables const environmentVariableRoutes = pro.environmentVariables
export const mainRoutes: Router[] = [ export const mainRoutes: Router[] = [
@ -65,7 +64,6 @@ export const mainRoutes: Router[] = [
pluginRoutes, pluginRoutes,
opsRoutes, opsRoutes,
debugRoutes, debugRoutes,
scheduleRoutes,
environmentVariableRoutes, environmentVariableRoutes,
// these need to be handled last as they still use /api/:tableId // these need to be handled last as they still use /api/:tableId
// this could be breaking as koa may recognise other routes as this // this could be breaking as koa may recognise other routes as this

View File

@ -81,6 +81,7 @@ exports[`/datasources fetch returns all the datasources from the server 1`] = `
{ {
"config": {}, "config": {},
"createdAt": "2020-01-01T00:00:00.000Z", "createdAt": "2020-01-01T00:00:00.000Z",
"isSQL": true,
"name": "Test", "name": "Test",
"source": "POSTGRES", "source": "POSTGRES",
"type": "datasource", "type": "datasource",

View File

@ -16,7 +16,7 @@ describe("/applications/:appId/import", () => {
it("should be able to perform import", async () => { it("should be able to perform import", async () => {
const appId = config.getAppId() const appId = config.getAppId()
const res = await request await request
.post(`/api/applications/${appId}/import`) .post(`/api/applications/${appId}/import`)
.field("encryptionPassword", PASSWORD) .field("encryptionPassword", PASSWORD)
.attach("appExport", path.join(__dirname, "assets", "export.tar.gz")) .attach("appExport", path.join(__dirname, "assets", "export.tar.gz"))

View File

@ -2,7 +2,6 @@ import * as setup from "./utilities"
import { roles, db as dbCore } from "@budibase/backend-core" import { roles, db as dbCore } from "@budibase/backend-core"
describe("/api/applications/:appId/sync", () => { describe("/api/applications/:appId/sync", () => {
let request = setup.getRequest()
let config = setup.getConfig() let config = setup.getConfig()
let app let app

View File

@ -19,6 +19,7 @@ import env from "../../../environment"
import { type App } from "@budibase/types" import { type App } from "@budibase/types"
import tk from "timekeeper" import tk from "timekeeper"
import * as uuid from "uuid" import * as uuid from "uuid"
import { structures } from "@budibase/backend-core/tests"
describe("/applications", () => { describe("/applications", () => {
let config = setup.getConfig() let config = setup.getConfig()
@ -356,7 +357,7 @@ describe("/applications", () => {
it("should reject an unknown app id with a 404", async () => { it("should reject an unknown app id with a 404", async () => {
await config.api.application.duplicateApp( await config.api.application.duplicateApp(
app.appId.slice(0, -1) + "a", structures.db.id(),
{ {
name: "to-dupe 123", name: "to-dupe 123",
url: "/to-dupe-123", url: "/to-dupe-123",
@ -368,7 +369,7 @@ describe("/applications", () => {
}) })
it("should reject with a known name", async () => { it("should reject with a known name", async () => {
const resp = await config.api.application.duplicateApp( await config.api.application.duplicateApp(
app.appId, app.appId,
{ {
name: app.name, name: app.name,
@ -380,7 +381,7 @@ describe("/applications", () => {
}) })
it("should reject with a known url", async () => { it("should reject with a known url", async () => {
const resp = await config.api.application.duplicateApp( await config.api.application.duplicateApp(
app.appId, app.appId,
{ {
name: "this is fine", name: "this is fine",

View File

@ -1,13 +1,5 @@
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") import * as setup from "./utilities"
const setup = require("./utilities") import { checkBuilderEndpoint } from "./utilities/TestFunctions"
import os from "os"
jest.mock("process", () => ({
arch: "arm64",
version: "v14.20.1",
platform: "darwin",
}))
describe("/component", () => { describe("/component", () => {
let request = setup.getRequest() let request = setup.getRequest()
@ -17,21 +9,6 @@ describe("/component", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
os.cpus = () => [
{
model: "test",
speed: 12323,
times: {
user: 0,
nice: 0,
sys: 0,
idle: 0,
irq: 0,
},
},
]
os.uptime = () => 123123123123
os.totalmem = () => 10000000000
}) })
describe("/api/debug", () => { describe("/api/debug", () => {
@ -43,14 +20,16 @@ describe("/component", () => {
.expect(200) .expect(200)
expect(res.body).toEqual({ expect(res.body).toEqual({
budibaseVersion: "0.0.0+jest", budibaseVersion: "0.0.0+jest",
cpuArch: "arm64", cpuArch: expect.any(String),
cpuCores: 1, cpuCores: expect.any(Number),
cpuInfo: "test", cpuInfo: expect.any(String),
hosting: "docker-compose", hosting: "docker-compose",
nodeVersion: "v14.20.1", nodeVersion: expect.stringMatching(/^v\d+\.\d+\.\d+$/),
platform: "darwin", platform: expect.any(String),
totalMemory: "9.313225746154785GB", totalMemory: expect.stringMatching(/^[0-9\\.]+GB$/),
uptime: "1425036 day(s), 3 hour(s), 32 minute(s)", uptime: expect.stringMatching(
/^\d+ day\(s\), \d+ hour\(s\), \d+ minute\(s\)$/
),
}) })
}) })

Some files were not shown because too many files have changed in this diff Show More