1
0
Fork 0
mirror of synced 2024-07-19 13:15:49 +12:00

Merge remote-tracking branch 'origin/master' into feature/screen-deselect

This commit is contained in:
Dean 2024-04-02 09:14:22 +01:00
commit ff5c7ceda8
371 changed files with 4445 additions and 5436 deletions

View file

@ -12,4 +12,5 @@ packages/sdk/sdk
packages/account-portal/packages/server/build
packages/account-portal/packages/ui/.routify
packages/account-portal/packages/ui/build
**/*.ivm.bundle.js
**/*.ivm.bundle.js
packages/server/build/oldClientVersions/**/**

View file

@ -34,18 +34,43 @@
},
{
"files": ["**/*.ts"],
"excludedFiles": ["qa-core/**"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended"],
"globals": {
"NodeJS": true
},
"rules": {
"no-unused-vars": "off",
"no-inner-declarations": "off",
"no-case-declarations": "off",
"no-useless-escape": "off",
"no-undef": "off",
"no-prototype-builtins": "off",
"local-rules/no-budibase-imports": "error",
"@typescript-eslint/no-unused-vars": "error",
"local-rules/no-budibase-imports": "error"
}
},
{
"files": ["**/*.spec.ts"],
"excludedFiles": ["qa-core/**"],
"parser": "@typescript-eslint/parser",
"plugins": ["jest", "@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:jest/recommended"],
"env": {
"jest/globals": true
},
"globals": {
"NodeJS": true
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
"local-rules/no-test-com": "error",
"local-rules/email-domain-example-com": "error"
"local-rules/email-domain-example-com": "error",
"no-console": "warn",
// We have a lot of tests that don't have assertions, they use our test
// API client that does the assertions for them
"jest/expect-expect": "off",
// We do this in some tests where the behaviour of internal tables
// differs to external, but the API is broadly the same
"jest/no-conditional-expect": "off"
}
},
{

View file

@ -66,7 +66,8 @@ jobs:
# Run build all the projects
- name: Build
run: |
yarn build
yarn build:oss
yarn build:account-portal
# Check the types of the projects built via esbuild
- name: Check types
run: |
@ -138,6 +139,8 @@ jobs:
test-server:
runs-on: ubuntu-latest
env:
DEBUG: testcontainers,testcontainers:exec,testcontainers:build,testcontainers:pull
steps:
- name: Checkout repo
uses: actions/checkout@v4
@ -151,7 +154,19 @@ jobs:
with:
node-version: 20.x
cache: yarn
- name: Pull testcontainers images
run: |
docker pull mcr.microsoft.com/mssql/server:2022-latest
docker pull mysql:8.3
docker pull postgres:16.1-bullseye
docker pull mongo:7.0-jammy
docker pull mariadb:lts
docker pull testcontainers/ryuk:0.5.1
docker pull budibase/couchdb
- run: yarn --frozen-lockfile
- name: Test server
run: |
if ${{ env.USE_NX_AFFECTED }}; then
@ -217,27 +232,34 @@ jobs:
echo "pro_commit=$pro_commit"
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
echo "base_commit=$base_commit"
echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT"
base_commit_excluding_merges=$(git log --no-merges -n 1 --format=format:%H $base_commit)
echo "base_commit_excluding_merges=$base_commit_excluding_merges"
echo "base_commit_excluding_merges=$base_commit_excluding_merges" >> "$GITHUB_OUTPUT"
else
echo "Nothing to do - branch to branch merge."
fi
- name: Check submodule merged to base branch
if: ${{ steps.get_pro_commits.outputs.base_commit != '' }}
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const submoduleCommit = '${{ steps.get_pro_commits.outputs.pro_commit }}';
const baseCommit = '${{ steps.get_pro_commits.outputs.base_commit }}';
- name: Check submodule merged and latest on base branch
if: ${{ steps.get_pro_commits.outputs.base_commit_excluding_merges != '' }}
run: |
cd packages/pro
base_commit_excluding_merges='${{ steps.get_pro_commits.outputs.base_commit_excluding_merges }}'
pro_commit='${{ steps.get_pro_commits.outputs.pro_commit }}'
if (submoduleCommit !== baseCommit) {
console.error('Submodule commit does not match the latest commit on the "${{ steps.get_pro_commits.outputs.target_branch }}" branch.');
console.error('Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/master/docs/getting_started.md')
process.exit(1);
} else {
console.log('All good, the submodule had been merged and setup correctly!')
}
any_commit=$(git log --no-merges $base_commit_excluding_merges...$pro_commit)
if [ -n "$any_commit" ]; then
echo $any_commit
echo "An error occurred: <error_message>"
echo 'Submodule commit does not match the latest commit on the "${{ steps.get_pro_commits.outputs.target_branch }}" branch.'
echo 'Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/master/docs/getting_started.md'
exit 1
else
echo 'All good, the submodule had been merged and setup correctly!'
fi
check-accountportal-submodule:
runs-on: ubuntu-latest
@ -250,7 +272,15 @@ jobs:
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0
- name: Check account portal commit
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
src:
- packages/account-portal/**
- if: steps.changes.outputs.src == 'true'
name: Check account portal commit
id: get_accountportal_commits
run: |
cd packages/account-portal

4
.gitignore vendored
View file

@ -5,6 +5,9 @@ packages/server/runtime_apps/
bb-airgapped.tar.gz
*.iml
packages/server/build/oldClientVersions/**/*
packages/builder/src/components/deploy/clientVersions.json
# Logs
logs
*.log
@ -107,3 +110,4 @@ budibase-component
budibase-datasource
*.iml
.nx

8
.vscode/launch.json vendored
View file

@ -1,4 +1,3 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
@ -20,6 +19,13 @@
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
"args": ["${workspaceFolder}/packages/worker/src/index.ts"],
"cwd": "${workspaceFolder}/packages/worker"
},
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:10000",
"webRoot": "${workspaceFolder}"
}
],
"compounds": [

View file

@ -140,7 +140,7 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml
| ingress.className | string | `""` | What ingress class to use. |
| ingress.enabled | bool | `true` | Whether to create an Ingress resource pointing to the Budibase proxy. |
| ingress.hosts | list | `[]` | Standard hosts block for the Ingress resource. Defaults to pointing to the Budibase proxy. |
| nameOverride | string | `""` | Override the name of the deploymen. Defaults to {{ .Chart.Name }}. |
| nameOverride | string | `""` | Override the name of the deployment. Defaults to {{ .Chart.Name }}. |
| service.port | int | `10000` | Port to expose on the service. |
| service.type | string | `"ClusterIP"` | Service type for the service that points to the main Budibase proxy pod. |
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |

View file

@ -1,6 +1,6 @@
# -- Passed to all pods created by this chart. Should not ordinarily need to be changed.
imagePullSecrets: []
# -- Override the name of the deploymen. Defaults to {{ .Chart.Name }}.
# -- Override the name of the deployment. Defaults to {{ .Chart.Name }}.
nameOverride: ""
serviceAccount:

View file

@ -25,11 +25,9 @@ module.exports = {
docs: {
description:
"disallow the use of 'test.com' in strings and replace it with 'example.com'",
category: "Possible Errors",
recommended: false,
},
schema: [], // no options
fixable: "code", // Indicates that this rule supports automatic fixing
schema: [],
fixable: "code",
},
create: function (context) {
return {
@ -58,8 +56,6 @@ module.exports = {
docs: {
description:
"enforce using the example.com domain for generator.email calls",
category: "Possible Errors",
recommended: false,
},
fixable: "code",
schema: [],

25
globalSetup.ts Normal file
View file

@ -0,0 +1,25 @@
import { GenericContainer, Wait } from "testcontainers"
export default async function setup() {
await new GenericContainer("budibase/couchdb")
.withExposedPorts(5984)
.withEnvironment({
COUCHDB_PASSWORD: "budibase",
COUCHDB_USER: "budibase",
})
.withCopyContentToContainer([
{
content: `
[log]
level = warn
`,
target: "/opt/couchdb/etc/local.d/test-couchdb.ini",
},
])
.withWaitStrategy(
Wait.forSuccessfulCommand(
"curl http://budibase:budibase@localhost:5984/_up"
).withStartupTimeout(20000)
)
.start()
}

View file

@ -1,16 +0,0 @@
module.exports = () => {
return {
couchdb: {
image: "budibase/couchdb",
ports: [5984],
env: {
COUCHDB_PASSWORD: "budibase",
COUCHDB_USER: "budibase",
},
wait: {
type: "ports",
timeout: 20000,
}
}
}
}

View file

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

View file

@ -12,6 +12,7 @@
"esbuild-node-externals": "^1.8.0",
"eslint": "^8.52.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-local-rules": "^2.0.0",
"eslint-plugin-svelte": "^2.34.0",
"husky": "^8.0.3",
@ -25,12 +26,16 @@
"svelte": "^4.2.10",
"svelte-eslint-parser": "^0.33.1",
"typescript": "5.2.2",
"typescript-eslint": "^7.3.1",
"yargs": "^17.7.2"
},
"scripts": {
"preinstall": "node scripts/syncProPackage.js",
"get-past-client-version": "node scripts/getPastClientVersion.js",
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
"build": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream",
"build:oss": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --ignore @budibase/account-portal --ignore @budibase/account-portal-server --ignore @budibase/account-portal-ui",
"build:account-portal": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --scope @budibase/account-portal --scope @budibase/account-portal-server --scope @budibase/account-portal-ui",
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
"check:types": "lerna run check:types",
"build:sdk": "lerna run --stream build:sdk",

@ -1 +1 @@
Subproject commit 23a1219732bd778654c0bcc4f49910c511e2d51f
Subproject commit 63ce32bca871f0a752323f5f7ebb5ec16bbbacc3

View file

@ -1,8 +0,0 @@
const { join } = require("path")
require("dotenv").config({
path: join(__dirname, "..", "..", "hosting", ".env"),
})
const jestTestcontainersConfigGenerator = require("../../jestTestcontainersConfigGenerator")
module.exports = jestTestcontainersConfigGenerator()

View file

@ -1,8 +1,8 @@
import { Config } from "@jest/types"
const baseConfig: Config.InitialProjectOptions = {
preset: "@trendyol/jest-testcontainers",
setupFiles: ["./tests/jestEnv.ts"],
globalSetup: "./../../globalSetup.ts",
setupFilesAfterEnv: ["./tests/jestSetup.ts"],
transform: {
"^.+\\.ts?$": "@swc/jest",

View file

@ -60,7 +60,6 @@
"@shopify/jest-koa-mocks": "5.1.1",
"@swc/core": "1.3.71",
"@swc/jest": "0.2.27",
"@trendyol/jest-testcontainers": "^2.1.1",
"@types/chance": "1.1.3",
"@types/cookies": "0.7.8",
"@types/jest": "29.5.5",

View file

@ -4,10 +4,10 @@ set -e
if [[ -n $CI ]]
then
# --runInBand performs better in ci where resources are limited
echo "jest --coverage --runInBand --forceExit"
jest --coverage --runInBand --forceExit
echo "jest --coverage --runInBand --forceExit $@"
jest --coverage --runInBand --forceExit $@
else
# --maxWorkers performs better in development
echo "jest --coverage --detectOpenHandles"
jest --coverage --detectOpenHandles
echo "jest --coverage --forceExit --detectOpenHandles $@"
jest --coverage --forceExit --detectOpenHandles $@
fi

View file

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

View file

@ -8,7 +8,7 @@ describe("platformLogout", () => {
await testEnv.withTenant(async () => {
const ctx = structures.koa.newContext()
await auth.platformLogout({ ctx, userId: "test" })
expect(events.auth.logout).toBeCalledTimes(1)
expect(events.auth.logout).toHaveBeenCalledTimes(1)
})
})
})

View file

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

View file

@ -1,6 +1,6 @@
import { AnyDocument, Database } from "@budibase/types"
import { JobQueue, createQueue } from "../queue"
import { JobQueue, Queue, createQueue } from "../queue"
import * as dbUtils from "../db"
interface ProcessDocMessage {
@ -12,18 +12,26 @@ interface ProcessDocMessage {
const PERSIST_MAX_ATTEMPTS = 100
let processor: DocWritethroughProcessor | undefined
export const docWritethroughProcessorQueue = createQueue<ProcessDocMessage>(
JobQueue.DOC_WRITETHROUGH_QUEUE,
{
jobOptions: {
attempts: PERSIST_MAX_ATTEMPTS,
},
}
)
export class DocWritethroughProcessor {
private static _queue: Queue
public static get queue() {
if (!DocWritethroughProcessor._queue) {
DocWritethroughProcessor._queue = createQueue<ProcessDocMessage>(
JobQueue.DOC_WRITETHROUGH_QUEUE,
{
jobOptions: {
attempts: PERSIST_MAX_ATTEMPTS,
},
}
)
}
return DocWritethroughProcessor._queue
}
class DocWritethroughProcessor {
init() {
docWritethroughProcessorQueue.process(async message => {
DocWritethroughProcessor.queue.process(async message => {
try {
await this.persistToDb(message.data)
} catch (err: any) {
@ -76,7 +84,7 @@ export class DocWritethrough {
}
async patch(data: Record<string, any>) {
await docWritethroughProcessorQueue.add({
await DocWritethroughProcessor.queue.add({
dbName: this.db.name,
docId: this.docId,
data,

View file

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

View file

@ -6,7 +6,7 @@ import { getDB } from "../../db"
import {
DocWritethrough,
docWritethroughProcessorQueue,
DocWritethroughProcessor,
init,
} from "../docWritethrough"
@ -15,7 +15,7 @@ import InMemoryQueue from "../../queue/inMemoryQueue"
const initialTime = Date.now()
async function waitForQueueCompletion() {
const queue: InMemoryQueue = docWritethroughProcessorQueue as never
const queue: InMemoryQueue = DocWritethroughProcessor.queue as never
await queue.waitForCompletion()
}
@ -235,11 +235,11 @@ describe("docWritethrough", () => {
return acc
}, {})
}
const queueMessageSpy = jest.spyOn(docWritethroughProcessorQueue, "add")
const queueMessageSpy = jest.spyOn(DocWritethroughProcessor.queue, "add")
await config.doInTenant(async () => {
let patches = await parallelPatch(5)
expect(queueMessageSpy).toBeCalledTimes(5)
expect(queueMessageSpy).toHaveBeenCalledTimes(5)
await waitForQueueCompletion()
expect(await db.get(documentId)).toEqual(
@ -247,7 +247,7 @@ describe("docWritethrough", () => {
)
patches = { ...patches, ...(await parallelPatch(40)) }
expect(queueMessageSpy).toBeCalledTimes(45)
expect(queueMessageSpy).toHaveBeenCalledTimes(45)
await waitForQueueCompletion()
expect(await db.get(documentId)).toEqual(
@ -255,7 +255,7 @@ describe("docWritethrough", () => {
)
patches = { ...patches, ...(await parallelPatch(10)) }
expect(queueMessageSpy).toBeCalledTimes(55)
expect(queueMessageSpy).toHaveBeenCalledTimes(55)
await waitForQueueCompletion()
expect(await db.get(documentId)).toEqual(
@ -265,6 +265,7 @@ describe("docWritethrough", () => {
})
// This is not yet supported
// eslint-disable-next-line jest/no-disabled-tests
it.skip("patches will execute in order", async () => {
let incrementalValue = 0
const keyToOverride = generator.word()

View file

@ -55,8 +55,8 @@ describe("user cache", () => {
})),
})
expect(UserDB.bulkGet).toBeCalledTimes(1)
expect(UserDB.bulkGet).toBeCalledWith(userIdsToRequest)
expect(UserDB.bulkGet).toHaveBeenCalledTimes(1)
expect(UserDB.bulkGet).toHaveBeenCalledWith(userIdsToRequest)
})
it("on a second all, all of them are retrieved from cache", async () => {
@ -82,7 +82,7 @@ describe("user cache", () => {
),
})
expect(UserDB.bulkGet).toBeCalledTimes(1)
expect(UserDB.bulkGet).toHaveBeenCalledTimes(1)
})
it("when some users are cached, only the missing ones are retrieved from db", async () => {
@ -110,8 +110,8 @@ describe("user cache", () => {
),
})
expect(UserDB.bulkGet).toBeCalledTimes(1)
expect(UserDB.bulkGet).toBeCalledWith([
expect(UserDB.bulkGet).toHaveBeenCalledTimes(1)
expect(UserDB.bulkGet).toHaveBeenCalledWith([
userIdsToRequest[1],
userIdsToRequest[2],
userIdsToRequest[4],

View file

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

View file

@ -246,7 +246,7 @@ describe("context", () => {
context.doInAppMigrationContext(db.generateAppID(), async () => {
await otherContextCall()
})
).rejects.toThrowError(
).rejects.toThrow(
"The context cannot be changed, a migration is currently running"
)
}

View file

@ -27,7 +27,7 @@ class Replication {
return resolve(info)
})
.on("error", function (err) {
throw new Error(`Replication Error: ${err}`)
throw err
})
})
}

View file

@ -10,10 +10,6 @@ interface SearchResponse<T> {
totalRows: number
}
interface PaginatedSearchResponse<T> extends SearchResponse<T> {
hasNextPage: boolean
}
export type SearchParams<T> = {
tableId?: string
sort?: string
@ -247,7 +243,7 @@ export class QueryBuilder<T> {
}
// Escape characters
if (!this.#noEscaping && escape && originalType === "string") {
value = `${value}`.replace(/[ \/#+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
value = `${value}`.replace(/[ /#+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
}
// Wrap in quotes

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { Event } from "@budibase/types"
import { Event, Identity } from "@budibase/types"
import { processors } from "./processors"
import identification from "./identification"
import * as backfill from "./backfill"
@ -7,12 +7,19 @@ import { publishAsyncEvent } from "./asyncEvents"
export const publishEvent = async (
event: Event,
properties: any,
timestamp?: string | number
timestamp?: string | number,
identityOverride?: Identity
) => {
// in future this should use async events via a distributed queue.
const identity = await identification.getCurrentIdentity()
const identity =
identityOverride || (await identification.getCurrentIdentity())
// Backfilling is get from the user cache, but when we override the identity cache is not available. Overrides are
// normally performed in automatic actions or operations in async flows (BPM) where the user session is not available.
const backfilling = identityOverride
? false
: await backfill.isBackfillingEvent(event)
const backfilling = await backfill.isBackfillingEvent(event)
// no backfill - send the event and exit
if (!backfilling) {
// send off async events if required

View file

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

View file

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

View file

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

View file

@ -5,13 +5,19 @@ import {
AccountCreatedEvent,
AccountDeletedEvent,
AccountVerifiedEvent,
Identity,
} from "@budibase/types"
async function created(account: Account) {
async function created(account: Account, identityOverride?: Identity) {
const properties: AccountCreatedEvent = {
tenantId: account.tenantId,
}
await publishEvent(Event.ACCOUNT_CREATED, properties)
await publishEvent(
Event.ACCOUNT_CREATED,
properties,
undefined,
identityOverride
)
}
async function deleted(account: Account) {

View file

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

View file

@ -11,7 +11,6 @@ export const buildMatcherRegex = (
return patterns.map(pattern => {
let route = pattern.route
const method = pattern.method
const strict = pattern.strict ? pattern.strict : false
// if there is a param in the route
// use a wildcard pattern
@ -24,24 +23,17 @@ export const buildMatcherRegex = (
}
}
return { regex: new RegExp(route), method, strict, route }
return { regex: new RegExp(route), method, route }
})
}
export const matches = (ctx: BBContext, options: RegexMatcher[]) => {
return options.find(({ regex, method, strict, route }) => {
let urlMatch
if (strict) {
urlMatch = ctx.request.url === route
} else {
urlMatch = regex.test(ctx.request.url)
}
return options.find(({ regex, method }) => {
const urlMatch = regex.test(ctx.request.url)
const methodMatch =
method === "ALL"
? true
: ctx.request.method.toLowerCase() === method.toLowerCase()
return urlMatch && methodMatch
})
}

View file

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

View file

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

View file

@ -114,11 +114,11 @@ describe("sso", () => {
// tenant id added
ssoUser.tenantId = context.getTenantId()
expect(mockSaveUser).toBeCalledWith(ssoUser, {
expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, {
hashPassword: false,
requirePassword: false,
})
expect(mockDone).toBeCalledWith(null, ssoUser)
expect(mockDone).toHaveBeenCalledWith(null, ssoUser)
})
})
})
@ -159,11 +159,11 @@ describe("sso", () => {
// existing id preserved
ssoUser._id = existingUser._id
expect(mockSaveUser).toBeCalledWith(ssoUser, {
expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, {
hashPassword: false,
requirePassword: false,
})
expect(mockDone).toBeCalledWith(null, ssoUser)
expect(mockDone).toHaveBeenCalledWith(null, ssoUser)
})
})
@ -187,11 +187,11 @@ describe("sso", () => {
// existing id preserved
ssoUser._id = existingUser._id
expect(mockSaveUser).toBeCalledWith(ssoUser, {
expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, {
hashPassword: false,
requirePassword: false,
})
expect(mockDone).toBeCalledWith(null, ssoUser)
expect(mockDone).toHaveBeenCalledWith(null, ssoUser)
})
})
})

View file

@ -24,13 +24,13 @@ function buildUserCtx(user: ContextUser) {
}
function passed(throwFn: jest.Func, nextFn: jest.Func) {
expect(throwFn).not.toBeCalled()
expect(nextFn).toBeCalled()
expect(throwFn).not.toHaveBeenCalled()
expect(nextFn).toHaveBeenCalled()
}
function threw(throwFn: jest.Func) {
// cant check next, the throw function doesn't actually throw - so it still continues
expect(throwFn).toBeCalled()
expect(throwFn).toHaveBeenCalled()
}
describe("adminOnly middleware", () => {

View file

@ -34,23 +34,6 @@ describe("matchers", () => {
expect(!!matchers.matches(ctx, built)).toBe(true)
})
it("doesn't wildcard path with strict", () => {
const pattern = [
{
route: "/api/tests",
method: "POST",
strict: true,
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests/id/something/else"
ctx.request.method = "POST"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(false)
})
it("matches with param", () => {
const pattern = [
{
@ -67,23 +50,6 @@ describe("matchers", () => {
expect(!!matchers.matches(ctx, built)).toBe(true)
})
// TODO: Support the below behaviour
// Strict does not work when a param is present
// it("matches with param with strict", () => {
// const pattern = [{
// route: "/api/tests/:testId",
// method: "GET",
// strict: true
// }]
// const ctx = structures.koa.newContext()
// ctx.request.url = "/api/tests/id"
// ctx.request.method = "GET"
//
// const built = matchers.buildMatcherRegex(pattern)
//
// expect(!!matchers.matches(ctx, built)).toBe(true)
// })
it("doesn't match by path", () => {
const pattern = [
{

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@ export async function lookupTenantId(userId: string) {
return user.tenantId
}
async function getUserDoc(emailOrId: string): Promise<PlatformUser> {
export async function getUserDoc(emailOrId: string): Promise<PlatformUser> {
const db = getPlatformDB()
return db.get(emailOrId)
}
@ -79,6 +79,17 @@ async function addUserDoc(emailOrId: string, newDocFn: () => PlatformUser) {
}
}
export async function addSsoUser(
ssoId: string,
email: string,
userId: string,
tenantId: string
) {
return addUserDoc(ssoId, () =>
newUserSsoIdDoc(ssoId, email, userId, tenantId)
)
}
export async function addUser(
tenantId: string,
userId: string,
@ -91,9 +102,7 @@ export async function addUser(
]
if (ssoId) {
promises.push(
addUserDoc(ssoId, () => newUserSsoIdDoc(ssoId, email, userId, tenantId))
)
promises.push(addSsoUser(ssoId, email, userId, tenantId))
}
await Promise.all(promises)

View file

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

View file

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

View file

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

View file

@ -120,7 +120,7 @@ describe("redis", () => {
await redis.bulkStore(data, ttl)
for (const [key, value] of Object.entries(data)) {
for (const key of Object.keys(data)) {
expect(await redis.get(key)).toBe(null)
}
@ -147,17 +147,6 @@ describe("redis", () => {
expect(results).toEqual([1, 2, 3, 4, 5])
})
it("can increment on a new key", async () => {
const key1 = structures.uuid()
const key2 = structures.uuid()
const result1 = await redis.increment(key1)
expect(result1).toBe(1)
const result2 = await redis.increment(key2)
expect(result2).toBe(1)
})
it("can increment multiple times in parallel", async () => {
const key = structures.uuid()
const results = await Promise.all(
@ -184,7 +173,7 @@ describe("redis", () => {
const key = structures.uuid()
await redis.store(key, value)
await expect(redis.increment(key)).rejects.toThrowError(
await expect(redis.increment(key)).rejects.toThrow(
"ERR value is not an integer or out of range"
)
})

View file

@ -96,8 +96,8 @@ describe("redlockImpl", () => {
task: mockTask,
executionTimeMs: lockTtl * 2,
})
).rejects.toThrowError(
`Unable to fully release the lock on resource \"lock:${config.tenantId}_persist_writethrough\".`
).rejects.toThrow(
`Unable to fully release the lock on resource "lock:${config.tenantId}_persist_writethrough".`
)
}
)

View file

@ -158,8 +158,8 @@ describe("getTenantIDFromCtx", () => {
],
}
expect(getTenantIDFromCtx(ctx, mockOpts)).toBeUndefined()
expect(ctx.throw).toBeCalledTimes(1)
expect(ctx.throw).toBeCalledWith(403, "Tenant id not set")
expect(ctx.throw).toHaveBeenCalledTimes(1)
expect(ctx.throw).toHaveBeenCalledWith(403, "Tenant id not set")
})
it("returns undefined if allowNoTenant is true", () => {

View file

@ -500,13 +500,13 @@ export class UserDB {
static async createAdminUser(
email: string,
password: string,
tenantId: string,
password?: string,
opts?: CreateAdminUserOpts
) {
const user: User = {
email: email,
password: password,
password,
createdAt: Date.now(),
roles: {},
builder: {

View file

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

View file

@ -3,7 +3,7 @@ import { generator } from "./generator"
export function userGroup(): UserGroup {
return {
name: generator.word(),
name: generator.guid(),
icon: generator.word(),
color: generator.word(),
}

View file

@ -1,80 +1,58 @@
import { DatabaseImpl } from "../../../src/db"
import { execSync } from "child_process"
let dockerPsResult: string | undefined
function formatDockerPsResult(serverName: string, port: number) {
const lines = dockerPsResult?.split("\n")
let first = true
if (!lines) {
return null
}
for (let line of lines) {
if (first) {
first = false
continue
}
let toLookFor = serverName.split("-service")[0]
if (!line.includes(toLookFor)) {
continue
}
const regex = new RegExp(`0.0.0.0:([0-9]*)->${port}`, "g")
const found = line.match(regex)
if (found) {
return found[0].split(":")[1].split("->")[0]
}
}
return null
interface ContainerInfo {
Command: string
CreatedAt: string
ID: string
Image: string
Labels: string
LocalVolumes: string
Mounts: string
Names: string
Networks: string
Ports: string
RunningFor: string
Size: string
State: string
Status: string
}
function getTestContainerSettings(
serverName: string,
key: string
): string | null {
const entry = Object.entries(global).find(
([k]) =>
k.includes(`${serverName.toUpperCase()}`) &&
k.includes(`${key.toUpperCase()}`)
)
if (!entry) {
return null
}
return entry[1]
function getTestcontainers(): ContainerInfo[] {
return execSync("docker ps --format json")
.toString()
.split("\n")
.filter(x => x.length > 0)
.map(x => JSON.parse(x) as ContainerInfo)
.filter(x => x.Labels.includes("org.testcontainers=true"))
}
function getContainerInfo(containerName: string, port: number) {
let assignedPort = getTestContainerSettings(
containerName.toUpperCase(),
`PORT_${port}`
)
if (!dockerPsResult) {
try {
const outputBuffer = execSync("docker ps")
dockerPsResult = outputBuffer.toString("utf8")
} catch (err) {
//no-op
}
}
const possiblePort = formatDockerPsResult(containerName, port)
if (possiblePort) {
assignedPort = possiblePort
}
const host = getTestContainerSettings(containerName.toUpperCase(), "IP")
return {
port: assignedPort,
host,
url: host && assignedPort && `http://${host}:${assignedPort}`,
}
function getContainerByImage(image: string) {
return getTestcontainers().find(x => x.Image.startsWith(image))
}
function getCouchConfig() {
return getContainerInfo("couchdb", 5984)
function getExposedPort(container: ContainerInfo, port: number) {
const match = container.Ports.match(new RegExp(`0.0.0.0:(\\d+)->${port}/tcp`))
if (!match) {
return undefined
}
return parseInt(match[1])
}
export function setupEnv(...envs: any[]) {
const couch = getCouchConfig()
const couch = getContainerByImage("budibase/couchdb")
if (!couch) {
throw new Error("CouchDB container not found")
}
const couchPort = getExposedPort(couch, 5984)
if (!couchPort) {
throw new Error("CouchDB port not found")
}
const configs = [
{ key: "COUCH_DB_PORT", value: couch.port },
{ key: "COUCH_DB_URL", value: couch.url },
{ key: "COUCH_DB_PORT", value: `${couchPort}` },
{ key: "COUCH_DB_URL", value: `http://localhost:${couchPort}` },
]
for (const config of configs.filter(x => !!x.value)) {
@ -82,4 +60,7 @@ export function setupEnv(...envs: any[]) {
env._set(config.key, config.value)
}
}
// @ts-expect-error
DatabaseImpl.nano = undefined
}

View file

@ -4,3 +4,7 @@ process.env.NODE_ENV = "jest"
process.env.MOCK_REDIS = "1"
process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error"
process.env.REDIS_PASSWORD = "budibase"
process.env.COUCH_DB_PASSWORD = "budibase"
process.env.COUCH_DB_USER = "budibase"
process.env.API_ENCRYPTION_KEY = "testsecret"
process.env.JWT_SECRET = "testsecret"

View file

@ -12,6 +12,13 @@ export default {
format: "esm",
file: "dist/bbui.es.js",
},
onwarn(warning, warn) {
// suppress eval warnings
if (warning.code === "EVAL") {
return
}
warn(warning)
},
plugins: [
resolve(),
commonjs(),

View file

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

View file

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

View file

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

View file

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

View file

@ -1,15 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}

View file

@ -1,8 +0,0 @@
{
"javascript.format.enable": false,
"svelte.plugin.svelte.format.enable": false,
"html.format.enable": false,
"json.format.enable": false,
"editor.trimAutoWhitespace": false,
"sass.format.deleteWhitespace": false
}

View file

@ -191,8 +191,10 @@
// don't make field IDs for auto types
if (type === AUTO_TYPE || autocolumn) {
return type.toUpperCase()
} else {
} else if (type === FieldType.BB_REFERENCE) {
return `${type}${subtype || ""}`.toUpperCase()
} else {
return type.toUpperCase()
}
}
@ -703,24 +705,6 @@
thin
text="Allow multiple users"
/>
{:else if editableColumn.type === FieldType.ATTACHMENT}
<Toggle
value={editableColumn.constraints?.length?.maximum !== 1}
on:change={e => {
if (!e.detail) {
editableColumn.constraints ??= { length: {} }
editableColumn.constraints.length ??= {}
editableColumn.constraints.length.maximum = 1
editableColumn.constraints.length.message =
"cannot contain multiple files"
} else {
delete editableColumn.constraints?.length?.maximum
delete editableColumn.constraints?.length?.message
}
}}
thin
text="Allow multiple"
/>
{/if}
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
<Select

View file

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

View file

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

View file

@ -371,6 +371,7 @@
<style>
.binding-panel {
height: 100%;
overflow: hidden;
}
.binding-panel,
.tabs {

View file

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

View file

@ -32,10 +32,14 @@
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
import { goto } from "@roxi/routify"
import { onMount } from "svelte"
import PosthogClient from "../../analytics/PosthogClient"
export let application
export let loaded
const posthog = new PosthogClient(process.env.POSTHOG_TOKEN)
let unpublishModal
let updateAppModal
let revertModal
@ -44,6 +48,8 @@
let appActionPopoverOpen = false
let appActionPopoverAnchor
let publishing = false
let showNpsSurvey = false
let lastOpened
$: filteredApps = $appsStore.apps.filter(app => app.devId === application)
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
@ -57,7 +63,7 @@
$appStore.version &&
$appStore.upgradableVersion !== $appStore.version
$: canPublish = !publishing && loaded && $sortedScreens.length > 0
$: lastDeployed = getLastDeployedString($deploymentStore)
$: lastDeployed = getLastDeployedString($deploymentStore, lastOpened)
const initialiseApp = async () => {
const applicationPkg = await API.fetchAppPackage($appStore.devId)
@ -97,6 +103,7 @@
type: "success",
icon: "GlobeCheck",
})
showNpsSurvey = true
await completePublish()
} catch (error) {
console.error(error)
@ -147,6 +154,10 @@
notifications.error("Error refreshing app")
}
}
onMount(() => {
posthog.init()
})
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
@ -201,6 +212,7 @@
class="app-action-button publish app-action-popover"
on:click={() => {
if (!appActionPopoverOpen) {
lastOpened = new Date()
appActionPopover.show()
} else {
appActionPopover.hide()
@ -343,6 +355,10 @@
<RevertModal bind:this={revertModal} />
<VersionModal hideIcon bind:this={versionModal} />
{#if showNpsSurvey}
<div class="nps-survey" />
{/if}
<style>
.app-action-popover-content {
padding: var(--spacing-xl);

View file

@ -0,0 +1,33 @@
<script>
import { API } from "api"
import clientVersions from "./clientVersions.json"
import { appStore } from "stores/builder"
import { Select } from "@budibase/bbui"
export let revertableVersion
$: appId = $appStore.appId
const handleChange = e => {
const value = e.detail
if (value == null) return
API.setRevertableVersion(appId, value)
}
</script>
<div class="select">
<Select
autoWidth
value={revertableVersion}
options={clientVersions}
on:change={handleChange}
footer={"Older versions of the Budibase client can be acquired using `yarn get-past-client-version x.x.x`. This toggle is only available in dev mode."}
/>
</div>
<style>
.select {
width: 120px;
display: inline-block;
}
</style>

View file

@ -1,4 +1,5 @@
<script>
import { admin } from "stores/portal"
import {
Modal,
notifications,
@ -9,6 +10,7 @@
} from "@budibase/bbui"
import { appStore, initialise } from "stores/builder"
import { API } from "api"
import RevertModalVersionSelect from "./RevertModalVersionSelect.svelte"
export function show() {
updateModal.show()
@ -28,7 +30,9 @@
$appStore.upgradableVersion &&
$appStore.version &&
$appStore.upgradableVersion !== $appStore.version
$: revertAvailable = $appStore.revertableVersion != null
$: revertAvailable =
$appStore.revertableVersion != null ||
($admin.isDev && $appStore.version === "0.0.0")
const refreshAppPackage = async () => {
try {
@ -62,7 +66,9 @@
// Don't wait for the async refresh, since this causes modal flashing
refreshAppPackage()
notifications.success(
`App reverted successfully to version ${$appStore.revertableVersion}`
$appStore.revertableVersion
? `App reverted successfully to version ${$appStore.revertableVersion}`
: "App reverted successfully"
)
} catch (err) {
notifications.error(`Error reverting app: ${err}`)
@ -103,7 +109,13 @@
{#if revertAvailable}
<Body size="S">
You can revert this app to version
<b>{$appStore.revertableVersion}</b>
{#if $admin.isDev}
<RevertModalVersionSelect
revertableVersion={$appStore.revertableVersion}
/>
{:else}
<b>{$appStore.revertableVersion}</b>
{/if}
if you're experiencing issues with the current version.
</Body>
{/if}

View file

@ -0,0 +1 @@
[]

View file

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

View file

@ -279,3 +279,11 @@ export const buildContextTreeLookupMap = rootComponent => {
})
return map
}
// Get a flat list of ids for all descendants of a component
export const getChildIdsForComponent = component => {
return [
component._id,
...(component?._children ?? []).map(getChildIdsForComponent).flat(1),
]
}

View file

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

View file

@ -24,6 +24,13 @@
navigationStore,
} from "stores/builder"
import { DefaultAppTheme } from "constants"
import BarButtonList from "/src/components/design/settings/controls/BarButtonList.svelte"
$: alignmentOptions = [
{ value: "Left", barIcon: "TextAlignLeft" },
{ value: "Center", barIcon: "TextAlignCenter" },
{ value: "Right", barIcon: "TextAlignRight" },
]
$: screenRouteOptions = $screenStore.screens
.map(screen => screen.routing?.route)
@ -46,6 +53,10 @@
notifications.error("Error updating navigation settings")
}
}
const updateTextAlign = textAlignValue => {
navigationStore.syncAppNavigation({ textAlign: textAlignValue })
}
</script>
<Panel
@ -133,6 +144,15 @@
on:change={e => update("title", e.detail)}
updateOnChange={false}
/>
<div class="label">
<Label size="M">Text align</Label>
</div>
<BarButtonList
options={alignmentOptions}
value={$navigationStore.textAlign}
onChange={updateTextAlign}
/>
{/if}
<div class="label">
<Label>Background</Label>

View file

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

View file

@ -10,20 +10,15 @@
navigationStore,
selectedScreen,
hoverStore,
componentTreeNodesStore,
snippets,
} from "stores/builder"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import {
ProgressCircle,
Layout,
Heading,
Body,
Icon,
notifications,
} from "@budibase/bbui"
import { Layout, Heading, Body, Icon, notifications } from "@budibase/bbui"
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
import { findComponent, findComponentPath } from "helpers/components"
import { isActive, goto } from "@roxi/routify"
import { ClientAppSkeleton } from "@budibase/frontend-core"
let iframe
let layout
@ -132,6 +127,7 @@
error = event.error || "An unknown error occurred"
} else if (type === "select-component" && "id" in data) {
componentStore.select(data.id)
componentTreeNodesStore.makeNodeVisible(data.id)
} else if (type === "hover-component") {
hoverStore.hover(data.id, false)
} else if (type === "update-prop") {
@ -252,8 +248,16 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="component-container">
{#if loading}
<div class="center">
<ProgressCircle />
<div
class={`loading ${$themeStore.baseTheme} ${$themeStore.theme}`}
class:tablet={$previewStore.previewDevice === "tablet"}
class:mobile={$previewStore.previewDevice === "mobile"}
>
<ClientAppSkeleton
sideNav={$navigationStore?.navigation === "Left"}
hideFooter
hideDevTools
/>
</div>
{:else if error}
<div class="center error">
@ -270,8 +274,6 @@
bind:this={iframe}
src="/app/preview"
class:hidden={loading || error}
class:tablet={$previewStore.previewDevice === "tablet"}
class:mobile={$previewStore.previewDevice === "mobile"}
/>
<div
class="add-component"
@ -291,6 +293,25 @@
/>
<style>
.loading {
position: absolute;
container-type: inline-size;
width: 100%;
height: 100%;
border: 2px solid transparent;
box-sizing: border-box;
}
.loading.tablet {
width: calc(1024px + 6px);
max-height: calc(768px + 6px);
}
.loading.mobile {
width: calc(390px + 6px);
max-height: calc(844px + 6px);
}
.component-container {
grid-row-start: middle;
grid-column-start: middle;

View file

@ -4,12 +4,12 @@
selectedScreen,
componentStore,
selectedComponent,
componentTreeNodesStore,
} from "stores/builder"
import { findComponent } from "helpers/components"
import { findComponent, getChildIdsForComponent } from "helpers/components"
import { goto, isActive } from "@roxi/routify"
import { notifications } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
let confirmDeleteDialog
let confirmEjectDialog
@ -63,38 +63,25 @@
componentStore.selectNext()
},
["ArrowRight"]: component => {
componentTreeNodesStore.expandNode(component._id)
componentTreeNodesStore.expandNodes([component._id])
},
["ArrowLeft"]: component => {
componentTreeNodesStore.collapseNode(component._id)
// Select the collapsing root component to ensure the currently selected component is not
// hidden in a collapsed node
componentStore.select(component._id)
componentTreeNodesStore.collapseNodes([component._id])
},
["Ctrl+ArrowRight"]: component => {
componentTreeNodesStore.expandNode(component._id)
const expandChildren = component => {
const children = component._children ?? []
children.forEach(child => {
componentTreeNodesStore.expandNode(child._id)
expandChildren(child)
})
}
expandChildren(component)
const childIds = getChildIdsForComponent(component)
componentTreeNodesStore.expandNodes(childIds)
},
["Ctrl+ArrowLeft"]: component => {
componentTreeNodesStore.collapseNode(component._id)
// Select the collapsing root component to ensure the currently selected component is not
// hidden in a collapsed node
componentStore.select(component._id)
const collapseChildren = component => {
const children = component._children ?? []
children.forEach(child => {
componentTreeNodesStore.collapseNode(child._id)
collapseChildren(child)
})
}
collapseChildren(component)
const childIds = getChildIdsForComponent(component)
componentTreeNodesStore.collapseNodes(childIds)
},
["Escape"]: () => {
if ($isActive(`./:componentId/new`)) {

View file

@ -7,8 +7,8 @@
componentStore,
userSelectedResourceMap,
selectedComponent,
selectedComponentPath,
hoverStore,
componentTreeNodesStore,
} from "stores/builder"
import {
findComponentPath,
@ -17,7 +17,6 @@
} from "helpers/components"
import { get } from "svelte/store"
import { dndStore } from "./dndStore"
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
export let components = []
export let level = 0
@ -64,14 +63,11 @@
}
}
const isOpen = (component, selectedComponentPath, openNodes) => {
const isOpen = component => {
if (!component?._children?.length) {
return false
}
if (selectedComponentPath.slice(0, -1).includes(component._id)) {
return true
}
return openNodes[`nodeOpen-${component._id}`]
return componentTreeNodesStore.isNodeExpanded(component._id)
}
const isChildOfSelectedComponent = component => {
@ -83,6 +79,11 @@
return findComponentPath($selectedComponent, component._id)?.length > 0
}
const handleIconClick = componentId => {
componentStore.select(componentId)
componentTreeNodesStore.toggleNode(componentId)
}
const hover = hoverStore.hover
</script>
@ -90,7 +91,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<ul>
{#each filteredComponents || [] as component, index (component._id)}
{@const opened = isOpen(component, $selectedComponentPath, openNodes)}
{@const opened = isOpen(component, openNodes)}
<li
on:click|stopPropagation={() => {
componentStore.select(component._id)
@ -104,7 +105,7 @@
on:dragend={dndStore.actions.reset}
on:dragstart={() => dndStore.actions.dragstart(component)}
on:dragover={dragover(component, index)}
on:iconClick={() => componentTreeNodesStore.toggleNode(component._id)}
on:iconClick={() => handleIconClick(component._id)}
on:drop={onDrop}
hovering={$hoverStore.componentId === component._id}
on:mouseenter={() => hover(component._id)}

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

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

View file

@ -1,6 +1,12 @@
<script>
import { onMount, onDestroy } from "svelte"
import { params, goto } from "@roxi/routify"
import { auth, sideBarCollapsed, enrichedApps } from "stores/portal"
import {
licensing,
auth,
sideBarCollapsed,
enrichedApps,
} from "stores/portal"
import AppRowContext from "components/start/AppRowContext.svelte"
import FavouriteAppButton from "../FavouriteAppButton.svelte"
import {
@ -14,12 +20,17 @@
import { sdk } from "@budibase/shared-core"
import { API } from "api"
import ErrorSVG from "./ErrorSVG.svelte"
import { getBaseTheme, ClientAppSkeleton } from "@budibase/frontend-core"
$: app = $enrichedApps.find(app => app.appId === $params.appId)
$: iframeUrl = getIframeURL(app)
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
let loading = true
const getIframeURL = app => {
loading = true
if (app.status === "published") {
return `/app${app.url}`
}
@ -37,6 +48,20 @@
}
$: fetchScreens(app?.devId)
const receiveMessage = async message => {
if (message.data.type === "docLoaded") {
loading = false
}
}
onMount(() => {
window.addEventListener("message", receiveMessage)
})
onDestroy(() => {
window.removeEventListener("message", receiveMessage)
})
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
@ -108,7 +133,26 @@
</Body>
</div>
{:else}
<iframe src={iframeUrl} title={app.name} />
<div
class:hide={!loading || !app?.features?.skeletonLoader}
class="loading"
>
<div
class={`loadingThemeWrapper ${getBaseTheme(app.theme)} ${app.theme}`}
>
<ClientAppSkeleton
noAnimation
hideDevTools={app?.status === "published"}
sideNav={app?.navigation.navigation === "Left"}
hideFooter={$licensing.brandingEnabled}
/>
</div>
</div>
<iframe
class:hide={loading && app?.features?.skeletonLoader}
src={iframeUrl}
title={app.name}
/>
{/if}
</div>
@ -139,6 +183,23 @@
flex: 0 0 50px;
}
.loading {
height: 100%;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: var(--spacing-s);
overflow: hidden;
}
.loadingThemeWrapper {
height: 100%;
container-type: inline-size;
}
.hide {
visibility: hidden;
height: 0;
border: none;
}
iframe {
flex: 1 1 auto;
border-radius: var(--spacing-s);

View file

@ -0,0 +1,67 @@
import { get } from "svelte/store"
import { createSessionStorageStore } from "@budibase/frontend-core"
import { selectedScreen as selectedScreenStore } from "./screens"
import { findComponentPath } from "helpers/components"
const baseStore = createSessionStorageStore("openNodes", {})
const toggleNode = componentId => {
baseStore.update(openNodes => {
openNodes[`nodeOpen-${componentId}`] = !openNodes[`nodeOpen-${componentId}`]
return openNodes
})
}
const expandNodes = componentIds => {
baseStore.update(openNodes => {
const newNodes = Object.fromEntries(
componentIds.map(id => [`nodeOpen-${id}`, true])
)
return { ...openNodes, ...newNodes }
})
}
const collapseNodes = componentIds => {
baseStore.update(openNodes => {
const newNodes = Object.fromEntries(
componentIds.map(id => [`nodeOpen-${id}`, false])
)
return { ...openNodes, ...newNodes }
})
}
// Will ensure all parents of a node are expanded so that it is visible in the tree
const makeNodeVisible = componentId => {
const selectedScreen = get(selectedScreenStore)
const path = findComponentPath(selectedScreen.props, componentId)
const componentIds = path.map(component => component._id)
baseStore.update(openNodes => {
const newNodes = Object.fromEntries(
componentIds.map(id => [`nodeOpen-${id}`, true])
)
return { ...openNodes, ...newNodes }
})
}
const isNodeExpanded = componentId => {
const openNodes = get(baseStore)
return !!openNodes[`nodeOpen-${componentId}`]
}
const store = {
subscribe: baseStore.subscribe,
toggleNode,
expandNodes,
makeNodeVisible,
collapseNodes,
isNodeExpanded,
}
export default store

View file

@ -19,6 +19,7 @@ import {
appStore,
previewStore,
tables,
componentTreeNodesStore,
} from "stores/builder/index"
import { buildFormSchema, getSchemaForDatasource } from "dataBinding"
import {
@ -29,7 +30,6 @@ import {
} from "constants/backend"
import BudiStore from "../BudiStore"
import { Utils } from "@budibase/frontend-core"
import componentTreeNodesStore from "stores/portal/componentTreeNodesStore"
export const INITIAL_COMPONENTS_STATE = {
components: {},
@ -279,12 +279,10 @@ export class ComponentStore extends BudiStore {
else {
if (setting.type === "dataProvider") {
// Validate data provider exists, or else clear it
const treeId = parent?._id || component._id
const path = findComponentPath(screen?.props, treeId)
const providers = path.filter(component =>
component._component?.endsWith("/dataprovider")
const providers = findAllMatchingComponents(
screen?.props,
x => x._component === "@budibase/standard-components/dataprovider"
)
// Validate non-empty values
const valid = providers?.some(dp => value.includes?.(dp._id))
if (!valid) {
if (providers.length) {
@ -653,8 +651,11 @@ export class ComponentStore extends BudiStore {
this.update(state => {
state.selectedScreenId = targetScreenId
state.selectedComponentId = newComponentId
return state
})
componentTreeNodesStore.makeNodeVisible(newComponentId)
}
getPrevious() {
@ -663,7 +664,6 @@ export class ComponentStore extends BudiStore {
const screen = get(selectedScreen)
const parent = findComponentParent(screen.props, componentId)
const index = parent?._children.findIndex(x => x._id === componentId)
const componentTreeNodes = get(componentTreeNodesStore)
// Check for screen and navigation component edge cases
const screenComponentId = `${screen._id}-screen`
@ -680,16 +680,16 @@ export class ComponentStore extends BudiStore {
// If we have siblings above us, choose the sibling or a descendant
if (index > 0) {
// If sibling before us accepts children, select a descendant
// If sibling before us accepts children, and is not collapsed, select a descendant
const previousSibling = parent._children[index - 1]
if (
previousSibling._children?.length &&
componentTreeNodes[`nodeOpen-${previousSibling._id}`]
componentTreeNodesStore.isNodeExpanded(previousSibling._id)
) {
let target = previousSibling
while (
target._children?.length &&
componentTreeNodes[`nodeOpen-${target._id}`]
componentTreeNodesStore.isNodeExpanded(target._id)
) {
target = target._children[target._children.length - 1]
}
@ -711,7 +711,6 @@ export class ComponentStore extends BudiStore {
const screen = get(selectedScreen)
const parent = findComponentParent(screen.props, componentId)
const index = parent?._children.findIndex(x => x._id === componentId)
const componentTreeNodes = get(componentTreeNodesStore)
// Check for screen and navigation component edge cases
const screenComponentId = `${screen._id}-screen`
@ -720,11 +719,11 @@ export class ComponentStore extends BudiStore {
return navComponentId
}
// If we have children, select first child
// If we have children, select first child, and the node is not collapsed
if (
component._children?.length &&
(state.selectedComponentId === navComponentId ||
componentTreeNodes[`nodeOpen-${component._id}`])
componentTreeNodesStore.isNodeExpanded(component._id))
) {
return component._children[0]._id
} else if (!parent) {
@ -803,7 +802,10 @@ export class ComponentStore extends BudiStore {
// sibling
const previousSibling = parent._children[index - 1]
const definition = this.getDefinition(previousSibling._component)
if (definition.hasChildren) {
if (
definition.hasChildren &&
componentTreeNodesStore.isNodeExpanded(previousSibling._id)
) {
previousSibling._children.push(originalComponent)
}
@ -852,10 +854,13 @@ export class ComponentStore extends BudiStore {
// Move below the next sibling if we are not the last sibling
if (index < parent._children.length) {
// If the next sibling has children, become the first child
// If the next sibling has children, and is not collapsed, become the first child
const nextSibling = parent._children[index]
const definition = this.getDefinition(nextSibling._component)
if (definition.hasChildren) {
if (
definition.hasChildren &&
componentTreeNodesStore.isNodeExpanded(nextSibling._id)
) {
nextSibling._children.splice(0, 0, originalComponent)
}
@ -1151,13 +1156,3 @@ export const selectedComponent = derived(
return clone
}
)
export const selectedComponentPath = derived(
[componentStore, selectedScreen],
([$store, $selectedScreen]) => {
return findComponentPath(
$selectedScreen?.props,
$store.selectedComponentId
).map(component => component._id)
}
)

View file

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

View file

@ -1,10 +1,6 @@
import { layoutStore } from "./layouts.js"
import { appStore } from "./app.js"
import {
componentStore,
selectedComponent,
selectedComponentPath,
} from "./components"
import { componentStore, selectedComponent } from "./components"
import { navigationStore } from "./navigation.js"
import { themeStore } from "./theme.js"
import { screenStore, selectedScreen, sortedScreens } from "./screens.js"
@ -31,8 +27,10 @@ import { integrations } from "./integrations"
import { sortedIntegrations } from "./sortedIntegrations"
import { queries } from "./queries"
import { flags } from "./flags"
import componentTreeNodesStore from "./componentTreeNodes"
export {
componentTreeNodesStore,
layoutStore,
appStore,
componentStore,
@ -51,7 +49,6 @@ export {
isOnlyUser,
deploymentStore,
selectedComponent,
selectedComponentPath,
tables,
views,
viewsV2,

View file

@ -11,6 +11,7 @@ export const INITIAL_NAVIGATION_STATE = {
hideLogo: null,
logoUrl: null,
hideTitle: null,
textAlign: "Left",
navBackground: null,
navWidth: null,
navTextColor: null,

View file

@ -1,5 +1,6 @@
import { writable, get } from "svelte/store"
import { API } from "api"
import { getBaseTheme } from "@budibase/frontend-core"
const INITIAL_THEMES_STATE = {
theme: "",
@ -12,11 +13,15 @@ export const themes = () => {
})
const syncAppTheme = app => {
store.update(state => ({
...state,
theme: app.theme || "spectrum--light",
customTheme: app.customTheme,
}))
store.update(state => {
const theme = app.theme || "spectrum--light"
return {
...state,
theme,
baseTheme: getBaseTheme(theme),
customTheme: app.customTheme,
}
})
}
const save = async (theme, appId) => {

View file

@ -1,36 +0,0 @@
import { createSessionStorageStore } from "@budibase/frontend-core"
const baseStore = createSessionStorageStore("openNodes", {})
const toggleNode = componentId => {
baseStore.update(openNodes => {
openNodes[`nodeOpen-${componentId}`] = !openNodes[`nodeOpen-${componentId}`]
return openNodes
})
}
const expandNode = componentId => {
baseStore.update(openNodes => {
openNodes[`nodeOpen-${componentId}`] = true
return openNodes
})
}
const collapseNode = componentId => {
baseStore.update(openNodes => {
openNodes[`nodeOpen-${componentId}`] = false
return openNodes
})
}
const store = {
subscribe: baseStore.subscribe,
toggleNode,
expandNode,
collapseNode,
}
export default store

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,16 @@
"composite": true,
"declaration": true,
"sourceMap": true,
"baseUrl": "."
"baseUrl": ".",
"paths": {
"assets/*": ["./assets/*"],
"@budibase/*": [
"../*/src/index.ts",
"../*/src/index.js",
"../*",
"../../node_modules/@budibase/*"
],
"*": ["./src/*"]
}
}
}

View file

@ -11,6 +11,7 @@
"types": ["node", "jest"],
"outDir": "dist",
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@budibase/types": ["../types/src"],
"@budibase/backend-core": ["../backend-core/src"],

View file

@ -10,7 +10,8 @@
"rowSelection": true,
"continueIfAction": true,
"showNotificationAction": true,
"sidePanel": true
"sidePanel": true,
"skeletonLoader": true
},
"layout": {
"name": "Layout",
@ -4673,6 +4674,7 @@
}
},
"table": {
"deprecated": true,
"name": "Table",
"icon": "Table",
"illegalChildren": ["section"],
@ -5418,6 +5420,7 @@
]
},
"tableblock": {
"deprecated": true,
"block": true,
"name": "Table Block",
"icon": "Table",
@ -6595,7 +6598,7 @@
]
},
"gridblock": {
"name": "Grid Block",
"name": "Table",
"icon": "Table",
"styles": ["size"],
"size": {

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