Merge remote-tracking branch 'origin/master' into feature/signature-field-and-component
This commit is contained in:
commit
a773c167d5
|
@ -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",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.22.3",
|
"version": "2.22.11",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 = ""
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }))
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
},
|
},
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 |
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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)) || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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",
|
|
@ -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}
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}
|
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
<IndicatorSet
|
<IndicatorSet
|
||||||
componentId={$builderStore.selectedComponentId}
|
componentId={$builderStore.selectedComponentId}
|
||||||
{color}
|
{color}
|
||||||
zIndex="910"
|
zIndex={900}
|
||||||
transition
|
|
||||||
allowResizeAnchors
|
allowResizeAnchors
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
|
@ -1,3 +1,4 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
module FirebaseMock {
|
module FirebaseMock {
|
||||||
const firebase: any = {}
|
const firebase: any = {}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
module SendgridMock {
|
module SendgridMock {
|
||||||
class Email {
|
class Email {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
module ArangoMock {
|
module ArangoMock {
|
||||||
const arangodb: any = {}
|
const arangodb: any = {}
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
module MongoMock {
|
module MongoMock {
|
||||||
const mongodb: any = {}
|
const mongodb: any = {}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ import {
|
||||||
env as envCore,
|
env as envCore,
|
||||||
ErrorCode,
|
ErrorCode,
|
||||||
events,
|
events,
|
||||||
HTTPError,
|
|
||||||
migrations,
|
migrations,
|
||||||
objectStore,
|
objectStore,
|
||||||
roles,
|
roles,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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_")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { quotas } from "@budibase/pro"
|
|
||||||
import {
|
import {
|
||||||
UserCtx,
|
UserCtx,
|
||||||
ViewV2,
|
ViewV2,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue