1
0
Fork 0
mirror of synced 2024-09-10 06:26:02 +12:00

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

View file

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

View file

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

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

View file

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

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()
try {
await client.delete(generateTenantKey(key))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,7 +28,7 @@ export const buildMatcherRegex = (
}
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 methodMatch =
method === "ALL"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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.
console.info(...getLogParams(eventType, BullEvent.WAITING, { jobId }))
})
.on(BullEvent.ACTIVE, async (job: Job, jobPromise: any) => {
.on(BullEvent.ACTIVE, async (job: Job) => {
// A job has started. You can use `jobPromise.cancel()`` to abort it.
await doInJobContext(job, () => {
console.info(...getLogParams(eventType, BullEvent.ACTIVE, { job }))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

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

View file

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

View file

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

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 formScreen from "./formScreen"
const allTemplates = datasources => [
...rowListScreen(datasources),
...gridListScreen(datasources),
...gridDetailsScreen(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": {
"deprecated": true,
"name": "Table",
"icon": "Table",
"illegalChildren": ["section"],
@ -5467,6 +5468,7 @@
]
},
"tableblock": {
"deprecated": true,
"block": true,
"name": "Table Block",
"icon": "Table",
@ -6644,7 +6646,7 @@
]
},
"gridblock": {
"name": "Grid Block",
"name": "Table",
"icon": "Table",
"styles": ["size"],
"size": {

View file

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

View file

@ -291,7 +291,10 @@
<div
id="side-panel-container"
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}
>
<div class="side-panel-header">

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
<script>
import { getContext } from "svelte"
import { get } from "svelte/store"
import { generate } from "shortid"
import Block from "components/Block.svelte"
import BlockComponent from "components/BlockComponent.svelte"
@ -33,8 +34,9 @@
export let sidePanelDeleteLabel
export let notificationOverride
const { fetchDatasourceSchema, API } = getContext("sdk")
const { fetchDatasourceSchema, API, generateGoldenSample } = getContext("sdk")
const component = getContext("component")
const context = getContext("context")
const stateKey = `ID_${generate()}`
let formId
@ -48,20 +50,7 @@
let schemaLoaded = false
$: deleteLabel = setDeleteLabel(sidePanelDeleteLabel, sidePanelShowDelete)
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"
}
$: id = $component.id
$: isDSPlus = dataSource?.type === "table" || dataSource?.type === "viewV2"
$: fetchSchema(dataSource)
$: 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
const fetchSchema = async dataSource => {
if (dataSource?.type === "table") {
@ -267,7 +280,7 @@
dataSource,
buttonPosition: "top",
buttons: Utils.buildFormBlockButtonConfig({
_id: $component.id + "-form-edit",
_id: id + "-form-edit",
showDeleteButton: deleteLabel !== "",
showSaveButton: true,
saveButtonLabel: sidePanelSaveLabel || "Save",
@ -301,7 +314,7 @@
dataSource,
buttonPosition: "top",
buttons: Utils.buildFormBlockButtonConfig({
_id: $component.id + "-form-new",
_id: id + "-form-new",
showDeleteButton: false,
showSaveButton: true,
saveButtonLabel: "Save",

View file

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

View file

@ -6,36 +6,24 @@ export const getOptions = (
valueColumn,
customOptions
) => {
const isArray = fieldSchema?.type === "array"
// Take options from schema
if (optionsSource == null || optionsSource === "schema") {
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
if (optionsSource === "provider" && valueColumn) {
let optionsSet = {}
let valueCache = {}
let options = []
dataProvider?.rows?.forEach(row => {
const value = row?.[valueColumn]
if (value != null) {
if (value != null && !valueCache[value]) {
valueCache[value] = true
const label = row[labelColumn] || value
optionsSet[value] = { value, label }
options.push({ value, label })
}
})
return Object.values(optionsSet)
return 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 * from "./charts"
export * from "./forms"
export * from "./table"
export * from "./blocks"
export * from "./dynamic-filter"
// 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 cardhorizontal } from "./deprecated/CardHorizontal.svelte"
export { default as stackedlist } from "./deprecated/StackedList.svelte"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,13 +5,27 @@ const createHoverStore = () => {
const store = writable({
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) {
return
}
store.set({ hoveredComponentId: id })
eventStore.actions.dispatchEvent("hover-component", { id })
if (notifyBuilder) {
eventStore.actions.dispatchEvent("hover-component", { id })
}
}
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 (!$focusedCellId) {
if (e.key === "Tab" || e.key?.startsWith("Arrow")) {
e.preventDefault()
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") {
if (Object.keys($selectedRows).length && $config.canDeleteRows) {
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 {
const firebase: any = {}

View file

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

View file

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

View file

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

View file

@ -1,102 +1,81 @@
import fs from "fs"
import { join } from "path"
module AwsMock {
const aws: any = {}
const response = (body: any, extra?: any) => () => ({
promise: () => body,
...extra,
})
const response = (body: any, extra?: any) => () => ({
promise: () => body,
...extra,
})
function DocumentClient() {
// @ts-ignore
this.put = jest.fn(response({}))
// @ts-ignore
this.query = jest.fn(
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(
class DocumentClient {
put = jest.fn(response({}))
query = jest.fn(
response({
Items: [],
})
)
scan = jest.fn(
response({
Items: [
{
Body: "",
Name: "test",
},
{
createReadStream: jest
.fn()
.mockReturnValue(
fs.createReadStream(join(__dirname, "aws-sdk.ts"))
),
}
)
)
}
aws.DynamoDB = { DocumentClient }
aws.S3 = S3
aws.config = { update: jest.fn() }
module.exports = aws
],
})
)
get = jest.fn(response({}))
update = jest.fn(response({}))
delete = jest.fn(response({}))
}
class S3 {
listObjects = jest.fn(
response({
Contents: [],
})
)
createBucket = jest.fn(
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 {
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
import fs from "fs"
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module FetchMock {
// @ts-ignore
const fetch = jest.requireActual("node-fetch")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,6 +30,8 @@ import {
View,
RelationshipFieldMetadata,
FieldType,
FieldTypeSubtypes,
AttachmentFieldMetadata,
} from "@budibase/types"
export async function clearColumns(table: Table, columnNames: string[]) {
@ -88,6 +90,27 @@ export async function checkForColumnUpdates(
// Update views
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 }
}

View file

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

View file

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

View file

@ -33,7 +33,6 @@ export { default as staticRoutes } from "./static"
export { default as publicRoutes } from "./public"
const appBackupRoutes = pro.appBackups
const scheduleRoutes = pro.schedules
const environmentVariableRoutes = pro.environmentVariables
export const mainRoutes: Router[] = [
@ -65,7 +64,6 @@ export const mainRoutes: Router[] = [
pluginRoutes,
opsRoutes,
debugRoutes,
scheduleRoutes,
environmentVariableRoutes,
// these need to be handled last as they still use /api/:tableId
// 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": {},
"createdAt": "2020-01-01T00:00:00.000Z",
"isSQL": true,
"name": "Test",
"source": "POSTGRES",
"type": "datasource",

View file

@ -16,7 +16,7 @@ describe("/applications/:appId/import", () => {
it("should be able to perform import", async () => {
const appId = config.getAppId()
const res = await request
await request
.post(`/api/applications/${appId}/import`)
.field("encryptionPassword", PASSWORD)
.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"
describe("/api/applications/:appId/sync", () => {
let request = setup.getRequest()
let config = setup.getConfig()
let app

View file

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

View file

@ -1,13 +1,5 @@
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const setup = require("./utilities")
import os from "os"
jest.mock("process", () => ({
arch: "arm64",
version: "v14.20.1",
platform: "darwin",
}))
import * as setup from "./utilities"
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
describe("/component", () => {
let request = setup.getRequest()
@ -17,21 +9,6 @@ describe("/component", () => {
beforeAll(async () => {
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", () => {
@ -43,14 +20,16 @@ describe("/component", () => {
.expect(200)
expect(res.body).toEqual({
budibaseVersion: "0.0.0+jest",
cpuArch: "arm64",
cpuCores: 1,
cpuInfo: "test",
cpuArch: expect.any(String),
cpuCores: expect.any(Number),
cpuInfo: expect.any(String),
hosting: "docker-compose",
nodeVersion: "v14.20.1",
platform: "darwin",
totalMemory: "9.313225746154785GB",
uptime: "1425036 day(s), 3 hour(s), 32 minute(s)",
nodeVersion: expect.stringMatching(/^v\d+\.\d+\.\d+$/),
platform: expect.any(String),
totalMemory: expect.stringMatching(/^[0-9\\.]+GB$/),
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