merge
This commit is contained in:
commit
3e348908f5
|
@ -79,7 +79,11 @@ spec:
|
||||||
- name: MINIO_URL
|
- name: MINIO_URL
|
||||||
value: {{ .Values.services.objectStore.url }}
|
value: {{ .Values.services.objectStore.url }}
|
||||||
- name: PLUGIN_BUCKET_NAME
|
- name: PLUGIN_BUCKET_NAME
|
||||||
value: {{ .Values.services.objectStore.pluginBucketName | default "plugins" | quote }}
|
value: {{ .Values.services.objectStore.pluginBucketName | quote }}
|
||||||
|
- name: APPS_BUCKET_NAME
|
||||||
|
value: {{ .Values.services.objectStore.appsBucketName | quote }}
|
||||||
|
- name: GLOBAL_CLOUD_BUCKET_NAME
|
||||||
|
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
||||||
- name: PORT
|
- name: PORT
|
||||||
value: {{ .Values.services.apps.port | quote }}
|
value: {{ .Values.services.apps.port | quote }}
|
||||||
{{ if .Values.services.worker.publicApiRateLimitPerSecond }}
|
{{ if .Values.services.worker.publicApiRateLimitPerSecond }}
|
||||||
|
|
|
@ -78,7 +78,11 @@ spec:
|
||||||
- name: MINIO_URL
|
- name: MINIO_URL
|
||||||
value: {{ .Values.services.objectStore.url }}
|
value: {{ .Values.services.objectStore.url }}
|
||||||
- name: PLUGIN_BUCKET_NAME
|
- name: PLUGIN_BUCKET_NAME
|
||||||
value: {{ .Values.services.objectStore.pluginBucketName | default "plugins" | quote }}
|
value: {{ .Values.services.objectStore.pluginBucketName | quote }}
|
||||||
|
- name: APPS_BUCKET_NAME
|
||||||
|
value: {{ .Values.services.objectStore.appsBucketName | quote }}
|
||||||
|
- name: GLOBAL_CLOUD_BUCKET_NAME
|
||||||
|
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
||||||
- name: PORT
|
- name: PORT
|
||||||
value: {{ .Values.services.worker.port | quote }}
|
value: {{ .Values.services.worker.port | quote }}
|
||||||
- name: MULTI_TENANCY
|
- name: MULTI_TENANCY
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# vim:sw=4:ts=4:et
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ME=$(basename $0)
|
||||||
|
NGINX_CONF_FILE="/etc/nginx/nginx.conf"
|
||||||
|
DEFAULT_CONF_FILE="/etc/nginx/conf.d/default.conf"
|
||||||
|
|
||||||
|
# check if we have ipv6 available
|
||||||
|
if [ ! -f "/proc/net/if_inet6" ]; then
|
||||||
|
# ipv6 not available so delete lines from nginx conf
|
||||||
|
if [ -f "$NGINX_CONF_FILE" ]; then
|
||||||
|
sed -i '/listen \[::\]/d' $NGINX_CONF_FILE
|
||||||
|
fi
|
||||||
|
if [ -f "$DEFAULT_CONF_FILE" ]; then
|
||||||
|
sed -i '/listen \[::\]/d' $DEFAULT_CONF_FILE
|
||||||
|
fi
|
||||||
|
echo "$ME: info: ipv6 not available so delete lines from nginx conf"
|
||||||
|
else
|
||||||
|
echo "$ME: info: ipv6 is available so no need to delete lines from nginx conf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
|
@ -5,7 +5,12 @@ FROM nginx:latest
|
||||||
# override the output dir to output directly to /etc/nginx instead of /etc/nginx/conf.d
|
# override the output dir to output directly to /etc/nginx instead of /etc/nginx/conf.d
|
||||||
ENV NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx
|
ENV NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx
|
||||||
COPY .generated-nginx.prod.conf /etc/nginx/templates/nginx.conf.template
|
COPY .generated-nginx.prod.conf /etc/nginx/templates/nginx.conf.template
|
||||||
COPY 10-listen-on-ipv6-by-default.sh /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
|
|
||||||
|
# IPv6 removal needs to happen after envsubst
|
||||||
|
RUN rm -rf /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
|
||||||
|
COPY 80-listen-on-ipv6-by-default.sh /docker-entrypoint.d/80-listen-on-ipv6-by-default.sh
|
||||||
|
RUN chmod +x /docker-entrypoint.d/80-listen-on-ipv6-by-default.sh
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
COPY error.html /usr/share/nginx/html/error.html
|
COPY error.html /usr/share/nginx/html/error.html
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME
|
||||||
[[ -z "${WORKER_URL}" ]] && export WORKER_URL=http://localhost:4002
|
[[ -z "${WORKER_URL}" ]] && export WORKER_URL=http://localhost:4002
|
||||||
[[ -z "${APPS_URL}" ]] && export APPS_URL=http://localhost:4001
|
[[ -z "${APPS_URL}" ]] && export APPS_URL=http://localhost:4001
|
||||||
# export CUSTOM_DOMAIN=budi001.custom.com
|
# export CUSTOM_DOMAIN=budi001.custom.com
|
||||||
|
|
||||||
# Azure App Service customisations
|
# Azure App Service customisations
|
||||||
if [[ "${TARGETBUILD}" = "aas" ]]; then
|
if [[ "${TARGETBUILD}" = "aas" ]]; then
|
||||||
DATA_DIR=/home
|
DATA_DIR=/home
|
||||||
|
@ -27,6 +28,13 @@ else
|
||||||
DATA_DIR=${DATA_DIR:-/data}
|
DATA_DIR=${DATA_DIR:-/data}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Mount NFS or GCP Filestore if env vars exist for it
|
||||||
|
if [[ -z ${FILESHARE_IP} && -z ${FILESHARE_NAME} ]]; then
|
||||||
|
echo "Mount file share ${FILESHARE_IP}:/${FILESHARE_NAME} to ${DATA_DIR}"
|
||||||
|
mount -o nolock ${FILESHARE_IP}:/${FILESHARE_NAME} ${DATA_DIR}
|
||||||
|
echo "Mounting completed."
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -f "${DATA_DIR}/.env" ]; then
|
if [ -f "${DATA_DIR}/.env" ]; then
|
||||||
# Read in the .env file and export the variables
|
# Read in the .env file and export the variables
|
||||||
for LINE in $(cat ${DATA_DIR}/.env); do export $LINE; done
|
for LINE in $(cat ${DATA_DIR}/.env); do export $LINE; done
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.0.30-alpha.13",
|
"version": "2.0.34-alpha.3",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "2.0.30-alpha.13",
|
"version": "2.0.34-alpha.3",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
@ -20,12 +20,13 @@
|
||||||
"test:watch": "jest --watchAll"
|
"test:watch": "jest --watchAll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/types": "2.0.30-alpha.13",
|
"@budibase/types": "2.0.34-alpha.3",
|
||||||
"@shopify/jest-koa-mocks": "5.0.1",
|
"@shopify/jest-koa-mocks": "5.0.1",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-sdk": "2.1030.0",
|
"aws-sdk": "2.1030.0",
|
||||||
"bcrypt": "5.0.1",
|
"bcrypt": "5.0.1",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
|
"bull": "4.10.1",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"emitter-listener": "1.1.2",
|
"emitter-listener": "1.1.2",
|
||||||
"ioredis": "4.28.0",
|
"ioredis": "4.28.0",
|
||||||
|
@ -63,6 +64,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chance": "1.1.3",
|
"@types/chance": "1.1.3",
|
||||||
|
"@types/ioredis": "4.28.0",
|
||||||
"@types/jest": "27.5.1",
|
"@types/jest": "27.5.1",
|
||||||
"@types/koa": "2.0.52",
|
"@types/koa": "2.0.52",
|
||||||
"@types/lodash": "4.14.180",
|
"@types/lodash": "4.14.180",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import BaseCache from "./base"
|
import BaseCache from "./base"
|
||||||
import { getWritethroughClient } from "../redis/init"
|
import { getWritethroughClient } from "../redis/init"
|
||||||
import { logWarn } from "../logging"
|
import { logWarn } from "../logging"
|
||||||
|
import PouchDB from "pouchdb"
|
||||||
|
|
||||||
const DEFAULT_WRITE_RATE_MS = 10000
|
const DEFAULT_WRITE_RATE_MS = 10000
|
||||||
let CACHE: BaseCache | null = null
|
let CACHE: BaseCache | null = null
|
||||||
|
|
|
@ -53,6 +53,9 @@ export const getTenantIDFromAppID = (appId: string) => {
|
||||||
if (!appId) {
|
if (!appId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
if (!isMultiTenant()) {
|
||||||
|
return DEFAULT_TENANT_ID
|
||||||
|
}
|
||||||
const split = appId.split(SEPARATOR)
|
const split = appId.split(SEPARATOR)
|
||||||
const hasDev = split[1] === DocumentType.DEV
|
const hasDev = split[1] === DocumentType.DEV
|
||||||
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
|
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
|
||||||
|
|
|
@ -21,6 +21,7 @@ export enum ViewName {
|
||||||
ACCOUNT_BY_EMAIL = "account_by_email",
|
ACCOUNT_BY_EMAIL = "account_by_email",
|
||||||
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
|
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
|
||||||
USER_BY_GROUP = "by_group_user",
|
USER_BY_GROUP = "by_group_user",
|
||||||
|
APP_BACKUP_BY_TRIGGER = "by_trigger",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeprecatedViews = {
|
export const DeprecatedViews = {
|
||||||
|
@ -30,6 +31,10 @@ export const DeprecatedViews = {
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum InternalTable {
|
||||||
|
USER_METADATA = "ta_users",
|
||||||
|
}
|
||||||
|
|
||||||
export enum DocumentType {
|
export enum DocumentType {
|
||||||
USER = "us",
|
USER = "us",
|
||||||
GROUP = "gr",
|
GROUP = "gr",
|
||||||
|
@ -46,9 +51,23 @@ export enum DocumentType {
|
||||||
AUTOMATION_LOG = "log_au",
|
AUTOMATION_LOG = "log_au",
|
||||||
ACCOUNT_METADATA = "acc_metadata",
|
ACCOUNT_METADATA = "acc_metadata",
|
||||||
PLUGIN = "plg",
|
PLUGIN = "plg",
|
||||||
TABLE = "ta",
|
|
||||||
DATASOURCE = "datasource",
|
DATASOURCE = "datasource",
|
||||||
DATASOURCE_PLUS = "datasource_plus",
|
DATASOURCE_PLUS = "datasource_plus",
|
||||||
|
APP_BACKUP = "backup",
|
||||||
|
TABLE = "ta",
|
||||||
|
ROW = "ro",
|
||||||
|
AUTOMATION = "au",
|
||||||
|
LINK = "li",
|
||||||
|
WEBHOOK = "wh",
|
||||||
|
INSTANCE = "inst",
|
||||||
|
LAYOUT = "layout",
|
||||||
|
SCREEN = "screen",
|
||||||
|
QUERY = "query",
|
||||||
|
DEPLOYMENTS = "deployments",
|
||||||
|
METADATA = "metadata",
|
||||||
|
MEM_VIEW = "view",
|
||||||
|
USER_FLAG = "flag",
|
||||||
|
AUTOMATION_METADATA = "meta_au",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StaticDatabases = {
|
export const StaticDatabases = {
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
const pouch = require("./pouch")
|
|
||||||
const env = require("../environment")
|
|
||||||
|
|
||||||
const openDbs = []
|
|
||||||
let PouchDB
|
|
||||||
let initialised = false
|
|
||||||
const dbList = new Set()
|
|
||||||
|
|
||||||
if (env.MEMORY_LEAK_CHECK) {
|
|
||||||
setInterval(() => {
|
|
||||||
console.log("--- OPEN DBS ---")
|
|
||||||
console.log(openDbs)
|
|
||||||
}, 5000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const put =
|
|
||||||
dbPut =>
|
|
||||||
async (doc, options = {}) => {
|
|
||||||
if (!doc.createdAt) {
|
|
||||||
doc.createdAt = new Date().toISOString()
|
|
||||||
}
|
|
||||||
doc.updatedAt = new Date().toISOString()
|
|
||||||
return dbPut(doc, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkInitialised = () => {
|
|
||||||
if (!initialised) {
|
|
||||||
throw new Error("init has not been called")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.init = opts => {
|
|
||||||
PouchDB = pouch.getPouch(opts)
|
|
||||||
initialised = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: THIS IS A DANGEROUS FUNCTION - USE WITH CAUTION
|
|
||||||
// this function is prone to leaks, should only be used
|
|
||||||
// in situations that using the function doWithDB does not work
|
|
||||||
exports.dangerousGetDB = (dbName, opts) => {
|
|
||||||
checkInitialised()
|
|
||||||
if (env.isTest()) {
|
|
||||||
dbList.add(dbName)
|
|
||||||
}
|
|
||||||
const db = new PouchDB(dbName, opts)
|
|
||||||
if (env.MEMORY_LEAK_CHECK) {
|
|
||||||
openDbs.push(db.name)
|
|
||||||
}
|
|
||||||
const dbPut = db.put
|
|
||||||
db.put = put(dbPut)
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
// use this function if you have called dangerousGetDB - close
|
|
||||||
// the databases you've opened once finished
|
|
||||||
exports.closeDB = async db => {
|
|
||||||
if (!db || env.isTest()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (env.MEMORY_LEAK_CHECK) {
|
|
||||||
openDbs.splice(openDbs.indexOf(db.name), 1)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// specifically await so that if there is an error, it can be ignored
|
|
||||||
return await db.close()
|
|
||||||
} catch (err) {
|
|
||||||
// ignore error, already closed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we have to use a callback for this so that we can close
|
|
||||||
// the DB when we're done, without this manual requests would
|
|
||||||
// need to close the database when done with it to avoid memory leaks
|
|
||||||
exports.doWithDB = async (dbName, cb, opts = {}) => {
|
|
||||||
const db = exports.dangerousGetDB(dbName, opts)
|
|
||||||
// need this to be async so that we can correctly close DB after all
|
|
||||||
// async operations have been completed
|
|
||||||
try {
|
|
||||||
return await cb(db)
|
|
||||||
} finally {
|
|
||||||
await exports.closeDB(db)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.allDbs = () => {
|
|
||||||
if (!env.isTest()) {
|
|
||||||
throw new Error("Cannot be used outside test environment.")
|
|
||||||
}
|
|
||||||
checkInitialised()
|
|
||||||
return [...dbList]
|
|
||||||
}
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
import * as pouch from "./pouch"
|
||||||
|
import env from "../environment"
|
||||||
|
import { checkSlashesInUrl } from "../helpers"
|
||||||
|
import fetch from "node-fetch"
|
||||||
|
import { PouchOptions, CouchFindOptions } from "@budibase/types"
|
||||||
|
import PouchDB from "pouchdb"
|
||||||
|
|
||||||
|
const openDbs: string[] = []
|
||||||
|
let Pouch: any
|
||||||
|
let initialised = false
|
||||||
|
const dbList = new Set()
|
||||||
|
|
||||||
|
if (env.MEMORY_LEAK_CHECK) {
|
||||||
|
setInterval(() => {
|
||||||
|
console.log("--- OPEN DBS ---")
|
||||||
|
console.log(openDbs)
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const put =
|
||||||
|
(dbPut: any) =>
|
||||||
|
async (doc: any, options = {}) => {
|
||||||
|
if (!doc.createdAt) {
|
||||||
|
doc.createdAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
doc.updatedAt = new Date().toISOString()
|
||||||
|
return dbPut(doc, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkInitialised = () => {
|
||||||
|
if (!initialised) {
|
||||||
|
throw new Error("init has not been called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function init(opts?: PouchOptions) {
|
||||||
|
Pouch = pouch.getPouch(opts)
|
||||||
|
initialised = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: THIS IS A DANGEROUS FUNCTION - USE WITH CAUTION
|
||||||
|
// this function is prone to leaks, should only be used
|
||||||
|
// in situations that using the function doWithDB does not work
|
||||||
|
export function dangerousGetDB(dbName: string, opts?: any): PouchDB.Database {
|
||||||
|
checkInitialised()
|
||||||
|
if (env.isTest()) {
|
||||||
|
dbList.add(dbName)
|
||||||
|
}
|
||||||
|
const db = new Pouch(dbName, opts)
|
||||||
|
if (env.MEMORY_LEAK_CHECK) {
|
||||||
|
openDbs.push(db.name)
|
||||||
|
}
|
||||||
|
const dbPut = db.put
|
||||||
|
db.put = put(dbPut)
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// use this function if you have called dangerousGetDB - close
|
||||||
|
// the databases you've opened once finished
|
||||||
|
export async function closeDB(db: PouchDB.Database) {
|
||||||
|
if (!db || env.isTest()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (env.MEMORY_LEAK_CHECK) {
|
||||||
|
openDbs.splice(openDbs.indexOf(db.name), 1)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// specifically await so that if there is an error, it can be ignored
|
||||||
|
return await db.close()
|
||||||
|
} catch (err) {
|
||||||
|
// ignore error, already closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have to use a callback for this so that we can close
|
||||||
|
// the DB when we're done, without this manual requests would
|
||||||
|
// need to close the database when done with it to avoid memory leaks
|
||||||
|
export async function doWithDB(dbName: string, cb: any, opts = {}) {
|
||||||
|
const db = dangerousGetDB(dbName, opts)
|
||||||
|
// need this to be async so that we can correctly close DB after all
|
||||||
|
// async operations have been completed
|
||||||
|
try {
|
||||||
|
return await cb(db)
|
||||||
|
} finally {
|
||||||
|
await closeDB(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function allDbs() {
|
||||||
|
if (!env.isTest()) {
|
||||||
|
throw new Error("Cannot be used outside test environment.")
|
||||||
|
}
|
||||||
|
checkInitialised()
|
||||||
|
return [...dbList]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function directCouchQuery(
|
||||||
|
path: string,
|
||||||
|
method: string = "GET",
|
||||||
|
body?: any
|
||||||
|
) {
|
||||||
|
let { url, cookie } = pouch.getCouchInfo()
|
||||||
|
const couchUrl = `${url}/${path}`
|
||||||
|
const params: any = {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
Authorization: cookie,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (body && method !== "GET") {
|
||||||
|
params.body = JSON.stringify(body)
|
||||||
|
params.headers["Content-Type"] = "application/json"
|
||||||
|
}
|
||||||
|
const response = await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params)
|
||||||
|
if (response.status < 300) {
|
||||||
|
return await response.json()
|
||||||
|
} else {
|
||||||
|
throw "Cannot connect to CouchDB instance"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function directCouchAllDbs(queryString?: string) {
|
||||||
|
let couchPath = "/_all_dbs"
|
||||||
|
if (queryString) {
|
||||||
|
couchPath += `?${queryString}`
|
||||||
|
}
|
||||||
|
return await directCouchQuery(couchPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function directCouchFind(dbName: string, opts: CouchFindOptions) {
|
||||||
|
const json = await directCouchQuery(`${dbName}/_find`, "POST", opts)
|
||||||
|
return { rows: json.docs, bookmark: json.bookmark }
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
const PouchDB = require("pouchdb")
|
import PouchDB from "pouchdb"
|
||||||
const env = require("../environment")
|
import env from "../environment"
|
||||||
|
|
||||||
exports.getUrlInfo = (url = env.COUCH_DB_URL) => {
|
export const getUrlInfo = (url = env.COUCH_DB_URL) => {
|
||||||
let cleanUrl, username, password, host
|
let cleanUrl, username, password, host
|
||||||
if (url) {
|
if (url) {
|
||||||
// Ensure the URL starts with a protocol
|
// Ensure the URL starts with a protocol
|
||||||
|
@ -44,8 +44,8 @@ exports.getUrlInfo = (url = env.COUCH_DB_URL) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getCouchInfo = () => {
|
export const getCouchInfo = () => {
|
||||||
const urlInfo = exports.getUrlInfo()
|
const urlInfo = getUrlInfo()
|
||||||
let username
|
let username
|
||||||
let password
|
let password
|
||||||
if (env.COUCH_DB_USERNAME) {
|
if (env.COUCH_DB_USERNAME) {
|
||||||
|
@ -82,11 +82,11 @@ exports.getCouchInfo = () => {
|
||||||
* This should be rarely used outside of the main application config.
|
* This should be rarely used outside of the main application config.
|
||||||
* Exposed for exceptional cases such as in-memory views.
|
* Exposed for exceptional cases such as in-memory views.
|
||||||
*/
|
*/
|
||||||
exports.getPouch = (opts = {}) => {
|
export const getPouch = (opts: any = {}) => {
|
||||||
let { url, cookie } = exports.getCouchInfo()
|
let { url, cookie } = getCouchInfo()
|
||||||
let POUCH_DB_DEFAULTS = {
|
let POUCH_DB_DEFAULTS = {
|
||||||
prefix: url,
|
prefix: url,
|
||||||
fetch: (url, opts) => {
|
fetch: (url: string, opts: any) => {
|
||||||
// use a specific authorization cookie - be very explicit about how we authenticate
|
// use a specific authorization cookie - be very explicit about how we authenticate
|
||||||
opts.headers.set("Authorization", cookie)
|
opts.headers.set("Authorization", cookie)
|
||||||
return PouchDB.fetch(url, opts)
|
return PouchDB.fetch(url, opts)
|
||||||
|
@ -98,6 +98,7 @@ exports.getPouch = (opts = {}) => {
|
||||||
PouchDB.plugin(inMemory)
|
PouchDB.plugin(inMemory)
|
||||||
POUCH_DB_DEFAULTS = {
|
POUCH_DB_DEFAULTS = {
|
||||||
prefix: undefined,
|
prefix: undefined,
|
||||||
|
// @ts-ignore
|
||||||
adapter: "memory",
|
adapter: "memory",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,6 +106,7 @@ exports.getPouch = (opts = {}) => {
|
||||||
if (opts.onDisk) {
|
if (opts.onDisk) {
|
||||||
POUCH_DB_DEFAULTS = {
|
POUCH_DB_DEFAULTS = {
|
||||||
prefix: undefined,
|
prefix: undefined,
|
||||||
|
// @ts-ignore
|
||||||
adapter: "leveldb",
|
adapter: "leveldb",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,6 +114,7 @@ exports.getPouch = (opts = {}) => {
|
||||||
if (opts.replication) {
|
if (opts.replication) {
|
||||||
const replicationStream = require("pouchdb-replication-stream")
|
const replicationStream = require("pouchdb-replication-stream")
|
||||||
PouchDB.plugin(replicationStream.plugin)
|
PouchDB.plugin(replicationStream.plugin)
|
||||||
|
// @ts-ignore
|
||||||
PouchDB.adapter("writableStream", replicationStream.adapters.writableStream)
|
PouchDB.adapter("writableStream", replicationStream.adapters.writableStream)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import { newid } from "../hashing"
|
import { newid } from "../hashing"
|
||||||
import { DEFAULT_TENANT_ID, Configs } from "../constants"
|
import { DEFAULT_TENANT_ID, Configs } from "../constants"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { SEPARATOR, DocumentType, UNICODE_MAX, ViewName } from "./constants"
|
import {
|
||||||
|
SEPARATOR,
|
||||||
|
DocumentType,
|
||||||
|
UNICODE_MAX,
|
||||||
|
ViewName,
|
||||||
|
InternalTable,
|
||||||
|
} from "./constants"
|
||||||
import { getTenantId, getGlobalDB } from "../context"
|
import { getTenantId, getGlobalDB } from "../context"
|
||||||
import { getGlobalDBName } from "./tenancy"
|
import { getGlobalDBName } from "./tenancy"
|
||||||
import fetch from "node-fetch"
|
import { doWithDB, allDbs, directCouchAllDbs } from "./index"
|
||||||
import { doWithDB, allDbs } from "./index"
|
|
||||||
import { getCouchInfo } from "./pouch"
|
|
||||||
import { getAppMetadata } from "../cache/appMetadata"
|
import { getAppMetadata } from "../cache/appMetadata"
|
||||||
import { checkSlashesInUrl } from "../helpers"
|
|
||||||
import { isDevApp, isDevAppID, getProdAppID } from "./conversions"
|
import { isDevApp, isDevAppID, getProdAppID } from "./conversions"
|
||||||
import { APP_PREFIX } from "./constants"
|
import { APP_PREFIX } from "./constants"
|
||||||
import * as events from "../events"
|
import * as events from "../events"
|
||||||
|
@ -43,8 +46,8 @@ export const generateAppID = (tenantId = null) => {
|
||||||
* @returns {object} Parameters which can then be used with an allDocs request.
|
* @returns {object} Parameters which can then be used with an allDocs request.
|
||||||
*/
|
*/
|
||||||
export function getDocParams(
|
export function getDocParams(
|
||||||
docType: any,
|
docType: string,
|
||||||
docId: any = null,
|
docId?: string | null,
|
||||||
otherProps: any = {}
|
otherProps: any = {}
|
||||||
) {
|
) {
|
||||||
if (docId == null) {
|
if (docId == null) {
|
||||||
|
@ -57,6 +60,28 @@ export function getDocParams(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the DB allDocs/query params for retrieving a row.
|
||||||
|
* @param {string|null} tableId The table in which the rows have been stored.
|
||||||
|
* @param {string|null} rowId The ID of the row which is being specifically queried for. This can be
|
||||||
|
* left null to get all the rows in the table.
|
||||||
|
* @param {object} otherProps Any other properties to add to the request.
|
||||||
|
* @returns {object} Parameters which can then be used with an allDocs request.
|
||||||
|
*/
|
||||||
|
export function getRowParams(
|
||||||
|
tableId?: string | null,
|
||||||
|
rowId?: string | null,
|
||||||
|
otherProps = {}
|
||||||
|
) {
|
||||||
|
if (tableId == null) {
|
||||||
|
return getDocParams(DocumentType.ROW, null, otherProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
const endOfKey = rowId == null ? `${tableId}${SEPARATOR}` : rowId
|
||||||
|
|
||||||
|
return getDocParams(DocumentType.ROW, endOfKey, otherProps)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the correct index for a view based on default design DB.
|
* Retrieve the correct index for a view based on default design DB.
|
||||||
*/
|
*/
|
||||||
|
@ -64,6 +89,17 @@ export function getQueryIndex(viewName: ViewName) {
|
||||||
return `database/${viewName}`
|
return `database/${viewName}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a new row ID for the specified table.
|
||||||
|
* @param {string} tableId The table which the row is being created for.
|
||||||
|
* @param {string|null} id If an ID is to be used then the UUID can be substituted for this.
|
||||||
|
* @returns {string} The new ID which a row doc can be stored under.
|
||||||
|
*/
|
||||||
|
export function generateRowID(tableId: string, id?: string) {
|
||||||
|
id = id || newid()
|
||||||
|
return `${DocumentType.ROW}${SEPARATOR}${tableId}${SEPARATOR}${id}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a given ID is that of a table.
|
* Check if a given ID is that of a table.
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
|
@ -131,6 +167,33 @@ export function getGlobalUserParams(globalId: any, otherProps: any = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets parameters for retrieving users, this is a utility function for the getDocParams function.
|
||||||
|
*/
|
||||||
|
export function getUserMetadataParams(userId?: string, otherProps = {}) {
|
||||||
|
return getRowParams(InternalTable.USER_METADATA, userId, otherProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new user ID based on the passed in global ID.
|
||||||
|
* @param {string} globalId The ID of the global user.
|
||||||
|
* @returns {string} The new user ID which the user doc can be stored under.
|
||||||
|
*/
|
||||||
|
export function generateUserMetadataID(globalId: string) {
|
||||||
|
return generateRowID(InternalTable.USER_METADATA, globalId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Breaks up the ID to get the global ID.
|
||||||
|
*/
|
||||||
|
export function getGlobalIDFromUserMetadataID(id: string) {
|
||||||
|
const prefix = `${DocumentType.ROW}${SEPARATOR}${InternalTable.USER_METADATA}${SEPARATOR}`
|
||||||
|
if (!id || !id.includes(prefix)) {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
return id.split(prefix)[1]
|
||||||
|
}
|
||||||
|
|
||||||
export function getUsersByAppParams(appId: any, otherProps: any = {}) {
|
export function getUsersByAppParams(appId: any, otherProps: any = {}) {
|
||||||
const prodAppId = getProdAppID(appId)
|
const prodAppId = getProdAppID(appId)
|
||||||
return {
|
return {
|
||||||
|
@ -191,9 +254,9 @@ export function getRoleParams(roleId = null, otherProps = {}) {
|
||||||
return getDocParams(DocumentType.ROLE, roleId, otherProps)
|
return getDocParams(DocumentType.ROLE, roleId, otherProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStartEndKeyURL(base: any, baseKey: any, tenantId = null) {
|
export function getStartEndKeyURL(baseKey: any, tenantId = null) {
|
||||||
const tenancy = tenantId ? `${SEPARATOR}${tenantId}` : ""
|
const tenancy = tenantId ? `${SEPARATOR}${tenantId}` : ""
|
||||||
return `${base}?startkey="${baseKey}${tenancy}"&endkey="${baseKey}${tenancy}${UNICODE_MAX}"`
|
return `startkey="${baseKey}${tenancy}"&endkey="${baseKey}${tenancy}${UNICODE_MAX}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -209,22 +272,10 @@ export async function getAllDbs(opts = { efficient: false }) {
|
||||||
return allDbs()
|
return allDbs()
|
||||||
}
|
}
|
||||||
let dbs: any[] = []
|
let dbs: any[] = []
|
||||||
let { url, cookie } = getCouchInfo()
|
async function addDbs(queryString?: string) {
|
||||||
async function addDbs(couchUrl: string) {
|
const json = await directCouchAllDbs(queryString)
|
||||||
const response = await fetch(checkSlashesInUrl(encodeURI(couchUrl)), {
|
dbs = dbs.concat(json)
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: cookie,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (response.status === 200) {
|
|
||||||
let json = await response.json()
|
|
||||||
dbs = dbs.concat(json)
|
|
||||||
} else {
|
|
||||||
throw "Cannot connect to CouchDB instance"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let couchUrl = `${url}/_all_dbs`
|
|
||||||
let tenantId = getTenantId()
|
let tenantId = getTenantId()
|
||||||
if (!env.MULTI_TENANCY || (!efficient && tenantId === DEFAULT_TENANT_ID)) {
|
if (!env.MULTI_TENANCY || (!efficient && tenantId === DEFAULT_TENANT_ID)) {
|
||||||
// just get all DBs when:
|
// just get all DBs when:
|
||||||
|
@ -232,12 +283,12 @@ export async function getAllDbs(opts = { efficient: false }) {
|
||||||
// - default tenant
|
// - default tenant
|
||||||
// - apps dbs don't contain tenant id
|
// - apps dbs don't contain tenant id
|
||||||
// - non-default tenant dbs are filtered out application side in getAllApps
|
// - non-default tenant dbs are filtered out application side in getAllApps
|
||||||
await addDbs(couchUrl)
|
await addDbs()
|
||||||
} else {
|
} else {
|
||||||
// get prod apps
|
// get prod apps
|
||||||
await addDbs(getStartEndKeyURL(couchUrl, DocumentType.APP, tenantId))
|
await addDbs(getStartEndKeyURL(DocumentType.APP, tenantId))
|
||||||
// get dev apps
|
// get dev apps
|
||||||
await addDbs(getStartEndKeyURL(couchUrl, DocumentType.APP_DEV, tenantId))
|
await addDbs(getStartEndKeyURL(DocumentType.APP_DEV, tenantId))
|
||||||
// add global db name
|
// add global db name
|
||||||
dbs.push(getGlobalDBName(tenantId))
|
dbs.push(getGlobalDBName(tenantId))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { AppBackup, AppBackupRestoreEvent, Event } from "@budibase/types"
|
||||||
|
import { publishEvent } from "../events"
|
||||||
|
|
||||||
|
export async function appBackupRestored(backup: AppBackup) {
|
||||||
|
const properties: AppBackupRestoreEvent = {
|
||||||
|
appId: backup.appId,
|
||||||
|
backupName: backup.name!,
|
||||||
|
backupCreatedAt: backup.timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
await publishEvent(Event.APP_BACKUP_RESTORED, properties)
|
||||||
|
}
|
|
@ -19,3 +19,4 @@ export * as installation from "./installation"
|
||||||
export * as backfill from "./backfill"
|
export * as backfill from "./backfill"
|
||||||
export * as group from "./group"
|
export * as group from "./group"
|
||||||
export * as plugin from "./plugin"
|
export * as plugin from "./plugin"
|
||||||
|
export * as backup from "./backup"
|
||||||
|
|
|
@ -19,6 +19,7 @@ import pino from "./pino"
|
||||||
import * as middleware from "./middleware"
|
import * as middleware from "./middleware"
|
||||||
import plugins from "./plugin"
|
import plugins from "./plugin"
|
||||||
import encryption from "./security/encryption"
|
import encryption from "./security/encryption"
|
||||||
|
import * as queue from "./queue"
|
||||||
|
|
||||||
// mimic the outer package exports
|
// mimic the outer package exports
|
||||||
import * as db from "./pkg/db"
|
import * as db from "./pkg/db"
|
||||||
|
@ -63,6 +64,7 @@ const core = {
|
||||||
...errorClasses,
|
...errorClasses,
|
||||||
middleware,
|
middleware,
|
||||||
encryption,
|
encryption,
|
||||||
|
queue,
|
||||||
}
|
}
|
||||||
|
|
||||||
export = core
|
export = core
|
||||||
|
|
|
@ -18,11 +18,16 @@ const STATE = {
|
||||||
bucketCreationPromises: {},
|
bucketCreationPromises: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ListParams = {
|
||||||
|
ContinuationToken?: string
|
||||||
|
}
|
||||||
|
|
||||||
const CONTENT_TYPE_MAP: any = {
|
const CONTENT_TYPE_MAP: any = {
|
||||||
html: "text/html",
|
html: "text/html",
|
||||||
css: "text/css",
|
css: "text/css",
|
||||||
js: "application/javascript",
|
js: "application/javascript",
|
||||||
json: "application/json",
|
json: "application/json",
|
||||||
|
gz: "application/gzip",
|
||||||
}
|
}
|
||||||
const STRING_CONTENT_TYPES = [
|
const STRING_CONTENT_TYPES = [
|
||||||
CONTENT_TYPE_MAP.html,
|
CONTENT_TYPE_MAP.html,
|
||||||
|
@ -32,16 +37,16 @@ const STRING_CONTENT_TYPES = [
|
||||||
]
|
]
|
||||||
|
|
||||||
// does normal sanitization and then swaps dev apps to apps
|
// does normal sanitization and then swaps dev apps to apps
|
||||||
export function sanitizeKey(input: any) {
|
export function sanitizeKey(input: string) {
|
||||||
return sanitize(sanitizeBucket(input)).replace(/\\/g, "/")
|
return sanitize(sanitizeBucket(input)).replace(/\\/g, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// simply handles the dev app to app conversion
|
// simply handles the dev app to app conversion
|
||||||
export function sanitizeBucket(input: any) {
|
export function sanitizeBucket(input: string) {
|
||||||
return input.replace(new RegExp(APP_DEV_PREFIX, "g"), APP_PREFIX)
|
return input.replace(new RegExp(APP_DEV_PREFIX, "g"), APP_PREFIX)
|
||||||
}
|
}
|
||||||
|
|
||||||
function publicPolicy(bucketName: any) {
|
function publicPolicy(bucketName: string) {
|
||||||
return {
|
return {
|
||||||
Version: "2012-10-17",
|
Version: "2012-10-17",
|
||||||
Statement: [
|
Statement: [
|
||||||
|
@ -69,7 +74,7 @@ const PUBLIC_BUCKETS = [
|
||||||
* @return {Object} an S3 object store object, check S3 Nodejs SDK for usage.
|
* @return {Object} an S3 object store object, check S3 Nodejs SDK for usage.
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export const ObjectStore = (bucket: any) => {
|
export const ObjectStore = (bucket: string) => {
|
||||||
const config: any = {
|
const config: any = {
|
||||||
s3ForcePathStyle: true,
|
s3ForcePathStyle: true,
|
||||||
signatureVersion: "v4",
|
signatureVersion: "v4",
|
||||||
|
@ -93,7 +98,7 @@ export const ObjectStore = (bucket: any) => {
|
||||||
* Given an object store and a bucket name this will make sure the bucket exists,
|
* Given an object store and a bucket name this will make sure the bucket exists,
|
||||||
* if it does not exist then it will create it.
|
* if it does not exist then it will create it.
|
||||||
*/
|
*/
|
||||||
export const makeSureBucketExists = async (client: any, bucketName: any) => {
|
export const makeSureBucketExists = async (client: any, bucketName: string) => {
|
||||||
bucketName = sanitizeBucket(bucketName)
|
bucketName = sanitizeBucket(bucketName)
|
||||||
try {
|
try {
|
||||||
await client
|
await client
|
||||||
|
@ -145,7 +150,7 @@ export const upload = async ({
|
||||||
type,
|
type,
|
||||||
metadata,
|
metadata,
|
||||||
}: any) => {
|
}: any) => {
|
||||||
const extension = [...filename.split(".")].pop()
|
const extension = filename.split(".").pop()
|
||||||
const fileBytes = fs.readFileSync(path)
|
const fileBytes = fs.readFileSync(path)
|
||||||
|
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
|
@ -168,8 +173,8 @@ export const upload = async ({
|
||||||
* through to the object store.
|
* through to the object store.
|
||||||
*/
|
*/
|
||||||
export const streamUpload = async (
|
export const streamUpload = async (
|
||||||
bucketName: any,
|
bucketName: string,
|
||||||
filename: any,
|
filename: string,
|
||||||
stream: any,
|
stream: any,
|
||||||
extra = {}
|
extra = {}
|
||||||
) => {
|
) => {
|
||||||
|
@ -202,7 +207,7 @@ export const streamUpload = async (
|
||||||
* retrieves the contents of a file from the object store, if it is a known content type it
|
* retrieves the contents of a file from the object store, if it is a known content type it
|
||||||
* will be converted, otherwise it will be returned as a buffer stream.
|
* will be converted, otherwise it will be returned as a buffer stream.
|
||||||
*/
|
*/
|
||||||
export const retrieve = async (bucketName: any, filepath: any) => {
|
export const retrieve = async (bucketName: string, filepath: string) => {
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: sanitizeBucket(bucketName),
|
Bucket: sanitizeBucket(bucketName),
|
||||||
|
@ -217,10 +222,38 @@ export const retrieve = async (bucketName: any, filepath: any) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const listAllObjects = async (bucketName: string, path: string) => {
|
||||||
|
const objectStore = ObjectStore(bucketName)
|
||||||
|
const list = (params: ListParams = {}) => {
|
||||||
|
return objectStore
|
||||||
|
.listObjectsV2({
|
||||||
|
...params,
|
||||||
|
Bucket: sanitizeBucket(bucketName),
|
||||||
|
Prefix: sanitizeKey(path),
|
||||||
|
})
|
||||||
|
.promise()
|
||||||
|
}
|
||||||
|
let isTruncated = false,
|
||||||
|
token,
|
||||||
|
objects: AWS.S3.Types.Object[] = []
|
||||||
|
do {
|
||||||
|
let params: ListParams = {}
|
||||||
|
if (token) {
|
||||||
|
params.ContinuationToken = token
|
||||||
|
}
|
||||||
|
const response = await list(params)
|
||||||
|
if (response.Contents) {
|
||||||
|
objects = objects.concat(response.Contents)
|
||||||
|
}
|
||||||
|
isTruncated = !!response.IsTruncated
|
||||||
|
} while (isTruncated)
|
||||||
|
return objects
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as retrieval function but puts to a temporary file.
|
* Same as retrieval function but puts to a temporary file.
|
||||||
*/
|
*/
|
||||||
export const retrieveToTmp = async (bucketName: any, filepath: any) => {
|
export const retrieveToTmp = async (bucketName: string, filepath: string) => {
|
||||||
bucketName = sanitizeBucket(bucketName)
|
bucketName = sanitizeBucket(bucketName)
|
||||||
filepath = sanitizeKey(filepath)
|
filepath = sanitizeKey(filepath)
|
||||||
const data = await retrieve(bucketName, filepath)
|
const data = await retrieve(bucketName, filepath)
|
||||||
|
@ -229,10 +262,31 @@ export const retrieveToTmp = async (bucketName: any, filepath: any) => {
|
||||||
return outputPath
|
return outputPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const retrieveDirectory = async (bucketName: string, path: string) => {
|
||||||
|
let writePath = join(budibaseTempDir(), v4())
|
||||||
|
fs.mkdirSync(writePath)
|
||||||
|
const objects = await listAllObjects(bucketName, path)
|
||||||
|
let fullObjects = await Promise.all(
|
||||||
|
objects.map(obj => retrieve(bucketName, obj.Key!))
|
||||||
|
)
|
||||||
|
let count = 0
|
||||||
|
for (let obj of objects) {
|
||||||
|
const filename = obj.Key!
|
||||||
|
const data = fullObjects[count++]
|
||||||
|
const possiblePath = filename.split("/")
|
||||||
|
if (possiblePath.length > 1) {
|
||||||
|
const dirs = possiblePath.slice(0, possiblePath.length - 1)
|
||||||
|
fs.mkdirSync(join(writePath, ...dirs), { recursive: true })
|
||||||
|
}
|
||||||
|
fs.writeFileSync(join(writePath, ...possiblePath), data)
|
||||||
|
}
|
||||||
|
return writePath
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a single file.
|
* Delete a single file.
|
||||||
*/
|
*/
|
||||||
export const deleteFile = async (bucketName: any, filepath: any) => {
|
export const deleteFile = async (bucketName: string, filepath: string) => {
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
await makeSureBucketExists(objectStore, bucketName)
|
await makeSureBucketExists(objectStore, bucketName)
|
||||||
const params = {
|
const params = {
|
||||||
|
@ -242,7 +296,7 @@ export const deleteFile = async (bucketName: any, filepath: any) => {
|
||||||
return objectStore.deleteObject(params)
|
return objectStore.deleteObject(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteFiles = async (bucketName: any, filepaths: any) => {
|
export const deleteFiles = async (bucketName: string, filepaths: string[]) => {
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
await makeSureBucketExists(objectStore, bucketName)
|
await makeSureBucketExists(objectStore, bucketName)
|
||||||
const params = {
|
const params = {
|
||||||
|
@ -258,8 +312,8 @@ export const deleteFiles = async (bucketName: any, filepaths: any) => {
|
||||||
* Delete a path, including everything within.
|
* Delete a path, including everything within.
|
||||||
*/
|
*/
|
||||||
export const deleteFolder = async (
|
export const deleteFolder = async (
|
||||||
bucketName: any,
|
bucketName: string,
|
||||||
folder: any
|
folder: string
|
||||||
): Promise<any> => {
|
): Promise<any> => {
|
||||||
bucketName = sanitizeBucket(bucketName)
|
bucketName = sanitizeBucket(bucketName)
|
||||||
folder = sanitizeKey(folder)
|
folder = sanitizeKey(folder)
|
||||||
|
@ -292,9 +346,9 @@ export const deleteFolder = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
export const uploadDirectory = async (
|
export const uploadDirectory = async (
|
||||||
bucketName: any,
|
bucketName: string,
|
||||||
localPath: any,
|
localPath: string,
|
||||||
bucketPath: any
|
bucketPath: string
|
||||||
) => {
|
) => {
|
||||||
bucketName = sanitizeBucket(bucketName)
|
bucketName = sanitizeBucket(bucketName)
|
||||||
let uploads = []
|
let uploads = []
|
||||||
|
@ -326,7 +380,11 @@ exports.downloadTarballDirect = async (
|
||||||
await streamPipeline(response.body, zlib.Unzip(), tar.extract(path))
|
await streamPipeline(response.body, zlib.Unzip(), tar.extract(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const downloadTarball = async (url: any, bucketName: any, path: any) => {
|
export const downloadTarball = async (
|
||||||
|
url: string,
|
||||||
|
bucketName: string,
|
||||||
|
path: string
|
||||||
|
) => {
|
||||||
bucketName = sanitizeBucket(bucketName)
|
bucketName = sanitizeBucket(bucketName)
|
||||||
path = sanitizeKey(path)
|
path = sanitizeKey(path)
|
||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const { join } = require("path")
|
const { join } = require("path")
|
||||||
const { tmpdir } = require("os")
|
const { tmpdir } = require("os")
|
||||||
|
const fs = require("fs")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
/****************************************************
|
/****************************************************
|
||||||
|
@ -16,6 +17,11 @@ exports.ObjectStoreBuckets = {
|
||||||
PLUGINS: env.PLUGIN_BUCKET_NAME,
|
PLUGINS: env.PLUGIN_BUCKET_NAME,
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.budibaseTempDir = function () {
|
const bbTmp = join(tmpdir(), ".budibase")
|
||||||
return join(tmpdir(), ".budibase")
|
if (!fs.existsSync(bbTmp)) {
|
||||||
|
fs.mkdirSync(bbTmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.budibaseTempDir = function () {
|
||||||
|
return bbTmp
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum JobQueue {
|
||||||
|
AUTOMATION = "automationQueue",
|
||||||
|
APP_BACKUP = "appBackupQueue",
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
import events from "events"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bull works with a Job wrapper around all messages that contains a lot more information about
|
||||||
|
* the state of the message, this object constructor implements the same schema of Bull jobs
|
||||||
|
* for the sake of maintaining API consistency.
|
||||||
|
* @param {string} queue The name of the queue which the message will be carried on.
|
||||||
|
* @param {object} message The JSON message which will be passed back to the consumer.
|
||||||
|
* @returns {Object} A new job which can now be put onto the queue, this is mostly an
|
||||||
|
* internal structure so that an in memory queue can be easily swapped for a Bull queue.
|
||||||
|
*/
|
||||||
|
function newJob(queue: string, message: any) {
|
||||||
|
return {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
queue: queue,
|
||||||
|
data: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is designed to replicate Bull (https://github.com/OptimalBits/bull) in memory as a sort of mock.
|
||||||
|
* It is relatively simple, using an event emitter internally to register when messages are available
|
||||||
|
* to the consumers - in can support many inputs and many consumers.
|
||||||
|
*/
|
||||||
|
class InMemoryQueue {
|
||||||
|
_name: string
|
||||||
|
_opts?: any
|
||||||
|
_messages: any[]
|
||||||
|
_emitter: EventEmitter
|
||||||
|
/**
|
||||||
|
* The constructor the queue, exactly the same as that of Bulls.
|
||||||
|
* @param {string} name The name of the queue which is being configured.
|
||||||
|
* @param {object|null} opts This is not used by the in memory queue as there is no real use
|
||||||
|
* case when in memory, but is the same API as Bull
|
||||||
|
*/
|
||||||
|
constructor(name: string, opts = null) {
|
||||||
|
this._name = name
|
||||||
|
this._opts = opts
|
||||||
|
this._messages = []
|
||||||
|
this._emitter = new events.EventEmitter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same callback API as Bull, each callback passed to this will consume messages as they are
|
||||||
|
* available. Please note this is a queue service, not a notification service, so each
|
||||||
|
* consumer will receive different messages.
|
||||||
|
* @param {function<object>} func The callback function which will return a "Job", the same
|
||||||
|
* as the Bull API, within this job the property "data" contains the JSON message. Please
|
||||||
|
* note this is incredibly limited compared to Bull as in reality the Job would contain
|
||||||
|
* a lot more information about the queue and current status of Bull cluster.
|
||||||
|
*/
|
||||||
|
process(func: any) {
|
||||||
|
this._emitter.on("message", async () => {
|
||||||
|
if (this._messages.length <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let msg = this._messages.shift()
|
||||||
|
let resp = func(msg)
|
||||||
|
if (resp.then != null) {
|
||||||
|
await resp
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// simply puts a message to the queue and emits to the queue for processing
|
||||||
|
/**
|
||||||
|
* Simple function to replicate the add message functionality of Bull, putting
|
||||||
|
* a new message on the queue. This then emits an event which will be used to
|
||||||
|
* return the message to a consumer (if one is attached).
|
||||||
|
* @param {object} msg A message to be transported over the queue, this should be
|
||||||
|
* a JSON message as this is required by Bull.
|
||||||
|
* @param {boolean} repeat serves no purpose for the import queue.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
add(msg: any, repeat: boolean) {
|
||||||
|
if (typeof msg !== "object") {
|
||||||
|
throw "Queue only supports carrying JSON."
|
||||||
|
}
|
||||||
|
this._messages.push(newJob(this._name, msg))
|
||||||
|
this._emitter.emit("message")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* replicating the close function from bull, which waits for jobs to finish.
|
||||||
|
*/
|
||||||
|
async close() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This removes a cron which has been implemented, this is part of Bull API.
|
||||||
|
* @param {string} cronJobId The cron which is to be removed.
|
||||||
|
*/
|
||||||
|
removeRepeatableByKey(cronJobId: string) {
|
||||||
|
// TODO: implement for testing
|
||||||
|
console.log(cronJobId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implemented for tests
|
||||||
|
*/
|
||||||
|
getRepeatableJobs() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
removeJobs(pattern: string) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implemented for tests
|
||||||
|
*/
|
||||||
|
async clean() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
async getJob() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
on() {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export = InMemoryQueue
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./queue"
|
||||||
|
export * from "./constants"
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { Job, JobId, Queue } from "bull"
|
||||||
|
import { JobQueue } from "./constants"
|
||||||
|
|
||||||
|
export type StalledFn = (job: Job) => Promise<void>
|
||||||
|
|
||||||
|
export function addListeners(
|
||||||
|
queue: Queue,
|
||||||
|
jobQueue: JobQueue,
|
||||||
|
removeStalledCb?: StalledFn
|
||||||
|
) {
|
||||||
|
logging(queue, jobQueue)
|
||||||
|
if (removeStalledCb) {
|
||||||
|
handleStalled(queue, removeStalledCb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStalled(queue: Queue, removeStalledCb?: StalledFn) {
|
||||||
|
queue.on("stalled", async (job: Job) => {
|
||||||
|
if (removeStalledCb) {
|
||||||
|
await removeStalledCb(job)
|
||||||
|
} else if (job.opts.repeat) {
|
||||||
|
const jobId = job.id
|
||||||
|
const repeatJobs = await queue.getRepeatableJobs()
|
||||||
|
for (let repeatJob of repeatJobs) {
|
||||||
|
if (repeatJob.id === jobId) {
|
||||||
|
await queue.removeRepeatableByKey(repeatJob.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`jobId=${jobId} disabled`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function logging(queue: Queue, jobQueue: JobQueue) {
|
||||||
|
let eventType: string
|
||||||
|
switch (jobQueue) {
|
||||||
|
case JobQueue.AUTOMATION:
|
||||||
|
eventType = "automation-event"
|
||||||
|
break
|
||||||
|
case JobQueue.APP_BACKUP:
|
||||||
|
eventType = "app-backup-event"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (process.env.NODE_DEBUG?.includes("bull")) {
|
||||||
|
queue
|
||||||
|
.on("error", (error: any) => {
|
||||||
|
// An error occurred.
|
||||||
|
console.error(`${eventType}=error error=${JSON.stringify(error)}`)
|
||||||
|
})
|
||||||
|
.on("waiting", (jobId: JobId) => {
|
||||||
|
// A Job is waiting to be processed as soon as a worker is idling.
|
||||||
|
console.log(`${eventType}=waiting jobId=${jobId}`)
|
||||||
|
})
|
||||||
|
.on("active", (job: Job, jobPromise: any) => {
|
||||||
|
// A job has started. You can use `jobPromise.cancel()`` to abort it.
|
||||||
|
console.log(`${eventType}=active jobId=${job.id}`)
|
||||||
|
})
|
||||||
|
.on("stalled", (job: Job) => {
|
||||||
|
// A job has been marked as stalled. This is useful for debugging job
|
||||||
|
// workers that crash or pause the event loop.
|
||||||
|
console.error(
|
||||||
|
`${eventType}=stalled jobId=${job.id} job=${JSON.stringify(job)}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on("progress", (job: Job, progress: any) => {
|
||||||
|
// A job's progress was updated!
|
||||||
|
console.log(
|
||||||
|
`${eventType}=progress jobId=${job.id} progress=${progress}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on("completed", (job: Job, result) => {
|
||||||
|
// A job successfully completed with a `result`.
|
||||||
|
console.log(`${eventType}=completed jobId=${job.id} result=${result}`)
|
||||||
|
})
|
||||||
|
.on("failed", (job, err: any) => {
|
||||||
|
// A job failed with reason `err`!
|
||||||
|
console.log(`${eventType}=failed jobId=${job.id} error=${err}`)
|
||||||
|
})
|
||||||
|
.on("paused", () => {
|
||||||
|
// The queue has been paused.
|
||||||
|
console.log(`${eventType}=paused`)
|
||||||
|
})
|
||||||
|
.on("resumed", (job: Job) => {
|
||||||
|
// The queue has been resumed.
|
||||||
|
console.log(`${eventType}=paused jobId=${job.id}`)
|
||||||
|
})
|
||||||
|
.on("cleaned", (jobs: Job[], type: string) => {
|
||||||
|
// Old jobs have been cleaned from the queue. `jobs` is an array of cleaned
|
||||||
|
// jobs, and `type` is the type of jobs cleaned.
|
||||||
|
console.log(`${eventType}=cleaned length=${jobs.length} type=${type}`)
|
||||||
|
})
|
||||||
|
.on("drained", () => {
|
||||||
|
// Emitted every time the queue has processed all the waiting jobs (even if there can be some delayed jobs not yet processed)
|
||||||
|
console.log(`${eventType}=drained`)
|
||||||
|
})
|
||||||
|
.on("removed", (job: Job) => {
|
||||||
|
// A job successfully removed.
|
||||||
|
console.log(`${eventType}=removed jobId=${job.id}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import env from "../environment"
|
||||||
|
import { getRedisOptions } from "../redis/utils"
|
||||||
|
import { JobQueue } from "./constants"
|
||||||
|
import InMemoryQueue from "./inMemoryQueue"
|
||||||
|
import BullQueue from "bull"
|
||||||
|
import { addListeners, StalledFn } from "./listeners"
|
||||||
|
const { opts: redisOpts, redisProtocolUrl } = getRedisOptions()
|
||||||
|
|
||||||
|
const CLEANUP_PERIOD_MS = 60 * 1000
|
||||||
|
let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = []
|
||||||
|
let cleanupInterval: NodeJS.Timeout
|
||||||
|
|
||||||
|
async function cleanup() {
|
||||||
|
for (let queue of QUEUES) {
|
||||||
|
await queue.clean(CLEANUP_PERIOD_MS, "completed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createQueue<T>(
|
||||||
|
jobQueue: JobQueue,
|
||||||
|
opts: { removeStalledCb?: StalledFn } = {}
|
||||||
|
): BullQueue.Queue<T> {
|
||||||
|
const queueConfig: any = redisProtocolUrl || { redis: redisOpts }
|
||||||
|
let queue: any
|
||||||
|
if (!env.isTest()) {
|
||||||
|
queue = new BullQueue(jobQueue, queueConfig)
|
||||||
|
} else {
|
||||||
|
queue = new InMemoryQueue(jobQueue, queueConfig)
|
||||||
|
}
|
||||||
|
addListeners(queue, jobQueue, opts?.removeStalledCb)
|
||||||
|
QUEUES.push(queue)
|
||||||
|
if (!cleanupInterval) {
|
||||||
|
cleanupInterval = setInterval(cleanup, CLEANUP_PERIOD_MS)
|
||||||
|
// fire off an initial cleanup
|
||||||
|
cleanup().catch(err => {
|
||||||
|
console.error(`Unable to cleanup automation queue initially - ${err}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return queue
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.shutdown = async () => {
|
||||||
|
if (QUEUES.length) {
|
||||||
|
clearInterval(cleanupInterval)
|
||||||
|
for (let queue of QUEUES) {
|
||||||
|
await queue.close()
|
||||||
|
}
|
||||||
|
QUEUES = []
|
||||||
|
}
|
||||||
|
console.log("Queues shutdown")
|
||||||
|
}
|
|
@ -55,7 +55,12 @@ export const doWithLock = async (opts: LockOptions, task: any) => {
|
||||||
let lock
|
let lock
|
||||||
try {
|
try {
|
||||||
// aquire lock
|
// aquire lock
|
||||||
let name: string = `${tenancy.getTenantId()}_${opts.name}`
|
let name: string
|
||||||
|
if (opts.systemLock) {
|
||||||
|
name = opts.name
|
||||||
|
} else {
|
||||||
|
name = `${tenancy.getTenantId()}_${opts.name}`
|
||||||
|
}
|
||||||
if (opts.nameSuffix) {
|
if (opts.nameSuffix) {
|
||||||
name = name + `_${opts.nameSuffix}`
|
name = name + `_${opts.nameSuffix}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -543,6 +543,36 @@
|
||||||
semver "^7.3.5"
|
semver "^7.3.5"
|
||||||
tar "^6.1.11"
|
tar "^6.1.11"
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-darwin-arm64@2.1.2":
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-2.1.2.tgz#9571b87be3a3f2c46de05585470bc4f3af2f6f00"
|
||||||
|
integrity sha512-TyVLn3S/+ikMDsh0gbKv2YydKClN8HaJDDpONlaZR+LVJmsxLFUgA+O7zu59h9+f9gX1aj/ahw9wqa6rosmrYQ==
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-darwin-x64@2.1.2":
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-2.1.2.tgz#bfbc6936ede2955218f5621a675679a5fe8e6f4c"
|
||||||
|
integrity sha512-YPXtcVkhmVNoMGlqp81ZHW4dMxK09msWgnxtsDpSiZwTzUBG2N+No2bsr7WMtBKCVJMSD6mbAl7YhKUqkp/Few==
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-arm64@2.1.2":
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-2.1.2.tgz#22555e28382af2922e7450634c8a2f240bb9eb82"
|
||||||
|
integrity sha512-vHZ2JiOWF2+DN9lzltGbhtQNzDo8fKFGrf37UJrgqxU0yvtERrzUugnfnX1wmVfFhSsF8OxrfqiNOUc5hko1Zg==
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-arm@2.1.2":
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-2.1.2.tgz#ffb6ae1beea7ac572b6be6bf2a8e8162ebdd8be7"
|
||||||
|
integrity sha512-42R4MAFeIeNn+L98qwxAt360bwzX2Kf0ZQkBBucJ2Ircza3asoY4CDbgiu9VWklq8gWJVSJSJBwDI+c/THiWkA==
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-x64@2.1.2":
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-2.1.2.tgz#7caf62eebbfb1345de40f75e89666b3d4194755f"
|
||||||
|
integrity sha512-RjRoRxg7Q3kPAdUSC5EUUPlwfMkIVhmaRTIe+cqHbKrGZ4M6TyCA/b5qMaukQ/1CHWrqYY2FbKOAU8Hg0pQFzg==
|
||||||
|
|
||||||
|
"@msgpackr-extract/msgpackr-extract-win32-x64@2.1.2":
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.1.2.tgz#f2d8b9ddd8d191205ed26ce54aba3dfc5ae3e7c9"
|
||||||
|
integrity sha512-rIZVR48zA8hGkHIK7ED6+ZiXsjRCcAVBJbm8o89OKAMTmEAQ2QvoOxoiu3w2isAaWwzgtQIOFIqHwvZDyLKCvw==
|
||||||
|
|
||||||
"@shopify/jest-koa-mocks@5.0.1":
|
"@shopify/jest-koa-mocks@5.0.1":
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@shopify/jest-koa-mocks/-/jest-koa-mocks-5.0.1.tgz#fba490b6b7985fbb571eb9974897d396a3642e94"
|
resolved "https://registry.yarnpkg.com/@shopify/jest-koa-mocks/-/jest-koa-mocks-5.0.1.tgz#fba490b6b7985fbb571eb9974897d396a3642e94"
|
||||||
|
@ -733,6 +763,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1"
|
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1"
|
||||||
integrity sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==
|
integrity sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==
|
||||||
|
|
||||||
|
"@types/ioredis@4.28.0":
|
||||||
|
version "4.28.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.0.tgz#609b2ea0d91231df2dd7f67dd77436bc72584911"
|
||||||
|
integrity sha512-HSA/JQivJgV0e+353gvgu6WVoWvGRe0HyHOnAN2AvbVIhUlJBhNnnkP8gEEokrDWrxywrBkwo8NuDZ6TVPL9XA==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
|
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
|
||||||
|
@ -1497,6 +1534,21 @@ buffer@^5.5.0, buffer@^5.6.0:
|
||||||
base64-js "^1.3.1"
|
base64-js "^1.3.1"
|
||||||
ieee754 "^1.1.13"
|
ieee754 "^1.1.13"
|
||||||
|
|
||||||
|
bull@4.10.1:
|
||||||
|
version "4.10.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/bull/-/bull-4.10.1.tgz#f14974b6089358b62b495a2cbf838aadc098e43f"
|
||||||
|
integrity sha512-Fp21tRPb2EaZPVfmM+ONZKVz2RA+to+zGgaTLyCKt3JMSU8OOBqK8143OQrnGuGpsyE5G+9FevFAGhdZZfQP2g==
|
||||||
|
dependencies:
|
||||||
|
cron-parser "^4.2.1"
|
||||||
|
debuglog "^1.0.0"
|
||||||
|
get-port "^5.1.1"
|
||||||
|
ioredis "^4.28.5"
|
||||||
|
lodash "^4.17.21"
|
||||||
|
msgpackr "^1.5.2"
|
||||||
|
p-timeout "^3.2.0"
|
||||||
|
semver "^7.3.2"
|
||||||
|
uuid "^8.3.0"
|
||||||
|
|
||||||
cache-content-type@^1.0.0:
|
cache-content-type@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c"
|
resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c"
|
||||||
|
@ -1764,6 +1816,13 @@ core-util-is@~1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||||
|
|
||||||
|
cron-parser@^4.2.1:
|
||||||
|
version "4.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.6.0.tgz#404c3fdbff10ae80eef6b709555d577ef2fd2e0d"
|
||||||
|
integrity sha512-guZNLMGUgg6z4+eGhmHGw7ft+v6OQeuHzd1gcLxCo9Yg/qoxmG3nindp2/uwGCLizEisf2H0ptqeVXeoCpP6FA==
|
||||||
|
dependencies:
|
||||||
|
luxon "^3.0.1"
|
||||||
|
|
||||||
cross-spawn@^7.0.3:
|
cross-spawn@^7.0.3:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||||
|
@ -1837,6 +1896,11 @@ debug@~3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "2.0.0"
|
ms "2.0.0"
|
||||||
|
|
||||||
|
debuglog@^1.0.0:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
|
||||||
|
integrity sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==
|
||||||
|
|
||||||
decimal.js@^10.2.1:
|
decimal.js@^10.2.1:
|
||||||
version "10.3.1"
|
version "10.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
|
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
|
||||||
|
@ -2318,6 +2382,11 @@ get-package-type@^0.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
|
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
|
||||||
integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
|
integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
|
||||||
|
|
||||||
|
get-port@^5.1.1:
|
||||||
|
version "5.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
|
||||||
|
integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
|
||||||
|
|
||||||
get-stream@^4.1.0:
|
get-stream@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
|
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
|
||||||
|
@ -2652,6 +2721,23 @@ ioredis@4.28.0:
|
||||||
redis-parser "^3.0.0"
|
redis-parser "^3.0.0"
|
||||||
standard-as-callback "^2.1.0"
|
standard-as-callback "^2.1.0"
|
||||||
|
|
||||||
|
ioredis@^4.28.5:
|
||||||
|
version "4.28.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.5.tgz#5c149e6a8d76a7f8fa8a504ffc85b7d5b6797f9f"
|
||||||
|
integrity sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==
|
||||||
|
dependencies:
|
||||||
|
cluster-key-slot "^1.1.0"
|
||||||
|
debug "^4.3.1"
|
||||||
|
denque "^1.1.0"
|
||||||
|
lodash.defaults "^4.2.0"
|
||||||
|
lodash.flatten "^4.4.0"
|
||||||
|
lodash.isarguments "^3.1.0"
|
||||||
|
p-map "^2.1.0"
|
||||||
|
redis-commands "1.7.0"
|
||||||
|
redis-errors "^1.2.0"
|
||||||
|
redis-parser "^3.0.0"
|
||||||
|
standard-as-callback "^2.1.0"
|
||||||
|
|
||||||
is-arrayish@^0.2.1:
|
is-arrayish@^0.2.1:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||||
|
@ -3725,6 +3811,11 @@ ltgt@2.2.1, ltgt@^2.1.2, ltgt@~2.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5"
|
resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5"
|
||||||
integrity sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==
|
integrity sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==
|
||||||
|
|
||||||
|
luxon@^3.0.1:
|
||||||
|
version "3.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.0.4.tgz#d179e4e9f05e092241e7044f64aaa54796b03929"
|
||||||
|
integrity sha512-aV48rGUwP/Vydn8HT+5cdr26YYQiUZ42NM6ToMoaGKwYfWbfLeRkEu1wXWMHBZT6+KyLfcbbtVcoQFCbbPjKlw==
|
||||||
|
|
||||||
make-dir@^3.0.0, make-dir@^3.1.0:
|
make-dir@^3.0.0, make-dir@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
|
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
|
||||||
|
@ -3872,6 +3963,27 @@ ms@^2.1.1, ms@^2.1.3:
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||||
|
|
||||||
|
msgpackr-extract@^2.1.2:
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-2.1.2.tgz#56272030f3e163e1b51964ef8b1cd5e7240c03ed"
|
||||||
|
integrity sha512-cmrmERQFb19NX2JABOGtrKdHMyI6RUyceaPBQ2iRz9GnDkjBWFjNJC0jyyoOfZl2U/LZE3tQCCQc4dlRyA8mcA==
|
||||||
|
dependencies:
|
||||||
|
node-gyp-build-optional-packages "5.0.3"
|
||||||
|
optionalDependencies:
|
||||||
|
"@msgpackr-extract/msgpackr-extract-darwin-arm64" "2.1.2"
|
||||||
|
"@msgpackr-extract/msgpackr-extract-darwin-x64" "2.1.2"
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-arm" "2.1.2"
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-arm64" "2.1.2"
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-x64" "2.1.2"
|
||||||
|
"@msgpackr-extract/msgpackr-extract-win32-x64" "2.1.2"
|
||||||
|
|
||||||
|
msgpackr@^1.5.2:
|
||||||
|
version "1.7.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.7.2.tgz#68d6debf5999d6b61abb6e7046a689991ebf7261"
|
||||||
|
integrity sha512-mWScyHTtG6TjivXX9vfIy2nBtRupaiAj0HQ2mtmpmYujAmqZmaaEVPaSZ1NKLMvicaMLFzEaMk0ManxMRg8rMQ==
|
||||||
|
optionalDependencies:
|
||||||
|
msgpackr-extract "^2.1.2"
|
||||||
|
|
||||||
napi-macros@~2.0.0:
|
napi-macros@~2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
|
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
|
||||||
|
@ -3919,6 +4031,11 @@ node-forge@^0.7.1:
|
||||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
|
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
|
||||||
integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
|
integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
|
||||||
|
|
||||||
|
node-gyp-build-optional-packages@5.0.3:
|
||||||
|
version "5.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17"
|
||||||
|
integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==
|
||||||
|
|
||||||
node-gyp-build@~4.1.0:
|
node-gyp-build@~4.1.0:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb"
|
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb"
|
||||||
|
@ -4075,6 +4192,11 @@ p-cancelable@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
|
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
|
||||||
integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
|
integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
|
||||||
|
|
||||||
|
p-finally@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
|
||||||
|
integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
|
||||||
|
|
||||||
p-limit@^2.2.0:
|
p-limit@^2.2.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
|
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
|
||||||
|
@ -4094,6 +4216,13 @@ p-map@^2.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
|
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
|
||||||
integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
|
integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
|
||||||
|
|
||||||
|
p-timeout@^3.2.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
|
||||||
|
integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
|
||||||
|
dependencies:
|
||||||
|
p-finally "^1.0.0"
|
||||||
|
|
||||||
p-try@^2.0.0:
|
p-try@^2.0.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
||||||
|
@ -5360,7 +5489,7 @@ uuid@8.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
|
||||||
integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
|
integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
|
||||||
|
|
||||||
uuid@8.3.2, uuid@^8.3.2:
|
uuid@8.3.2, uuid@^8.3.0, uuid@^8.3.2:
|
||||||
version "8.3.2"
|
version "8.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "2.0.30-alpha.13",
|
"version": "2.0.34-alpha.3",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||||
"@budibase/string-templates": "2.0.30-alpha.13",
|
"@budibase/string-templates": "2.0.34-alpha.3",
|
||||||
"@spectrum-css/actionbutton": "^1.0.1",
|
"@spectrum-css/actionbutton": "^1.0.1",
|
||||||
"@spectrum-css/actiongroup": "^1.0.1",
|
"@spectrum-css/actiongroup": "^1.0.1",
|
||||||
"@spectrum-css/avatar": "^3.0.2",
|
"@spectrum-css/avatar": "^3.0.2",
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
export let timeOnly = false
|
export let timeOnly = false
|
||||||
export let ignoreTimezones = false
|
export let ignoreTimezones = false
|
||||||
export let time24hr = false
|
export let time24hr = false
|
||||||
|
export let range = false
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const flatpickrId = `${uuid()}-wrapper`
|
const flatpickrId = `${uuid()}-wrapper`
|
||||||
let open = false
|
let open = false
|
||||||
|
@ -41,6 +41,7 @@
|
||||||
time_24hr: time24hr || false,
|
time_24hr: time24hr || false,
|
||||||
altFormat: timeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
altFormat: timeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
||||||
wrap: true,
|
wrap: true,
|
||||||
|
mode: range ? "range" : null,
|
||||||
appendTo,
|
appendTo,
|
||||||
disableMobile: "true",
|
disableMobile: "true",
|
||||||
onReady: () => {
|
onReady: () => {
|
||||||
|
@ -64,7 +65,6 @@
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
newValue = newValue.toISOString()
|
newValue = newValue.toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If time only set date component to 2000-01-01
|
// If time only set date component to 2000-01-01
|
||||||
if (timeOnly) {
|
if (timeOnly) {
|
||||||
// Classic flackpickr causing issues.
|
// Classic flackpickr causing issues.
|
||||||
|
@ -95,7 +95,11 @@
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch("change", newValue)
|
if (range) {
|
||||||
|
dispatch("change", event.detail)
|
||||||
|
} else {
|
||||||
|
dispatch("change", newValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearDateOnBackspace = event => {
|
const clearDateOnBackspace = event => {
|
||||||
|
@ -160,7 +164,7 @@
|
||||||
{#key redrawOptions}
|
{#key redrawOptions}
|
||||||
<Flatpickr
|
<Flatpickr
|
||||||
bind:flatpickr
|
bind:flatpickr
|
||||||
value={parseDate(value)}
|
value={range ? value : parseDate(value)}
|
||||||
on:open={onOpen}
|
on:open={onOpen}
|
||||||
on:close={onClose}
|
on:close={onClose}
|
||||||
options={flatpickrOptions}
|
options={flatpickrOptions}
|
||||||
|
|
|
@ -14,11 +14,17 @@
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
export let appendTo = undefined
|
export let appendTo = undefined
|
||||||
export let ignoreTimezones = false
|
export let ignoreTimezones = false
|
||||||
|
export let range = false
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
value = e.detail
|
if (range) {
|
||||||
|
// Flatpickr cant take two dates and work out what to display, needs to be provided a string.
|
||||||
|
// Like - "Date1 to Date2". Hence passing in that specifically from the array
|
||||||
|
value = e?.detail[1]
|
||||||
|
} else {
|
||||||
|
value = e.detail
|
||||||
|
}
|
||||||
dispatch("change", e.detail)
|
dispatch("change", e.detail)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -34,6 +40,7 @@
|
||||||
{time24hr}
|
{time24hr}
|
||||||
{appendTo}
|
{appendTo}
|
||||||
{ignoreTimezones}
|
{ignoreTimezones}
|
||||||
|
{range}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
{schema}
|
{schema}
|
||||||
value={cellValue}
|
value={cellValue}
|
||||||
on:clickrelationship
|
on:clickrelationship
|
||||||
|
on:buttonclick
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</svelte:component>
|
</svelte:component>
|
||||||
|
|
|
@ -387,6 +387,7 @@
|
||||||
schema={schema[field]}
|
schema={schema[field]}
|
||||||
value={deepGet(row, field)}
|
value={deepGet(row, field)}
|
||||||
on:clickrelationship
|
on:clickrelationship
|
||||||
|
on:buttonclick
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</CellRenderer>
|
</CellRenderer>
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 314 KiB |
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "2.0.30-alpha.13",
|
"version": "2.0.34-alpha.3",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -71,10 +71,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.0.30-alpha.13",
|
"@budibase/bbui": "2.0.34-alpha.3",
|
||||||
"@budibase/client": "2.0.30-alpha.13",
|
"@budibase/client": "2.0.34-alpha.3",
|
||||||
"@budibase/frontend-core": "2.0.30-alpha.13",
|
"@budibase/frontend-core": "2.0.34-alpha.3",
|
||||||
"@budibase/string-templates": "2.0.30-alpha.13",
|
"@budibase/string-templates": "2.0.34-alpha.3",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
|
@ -182,7 +182,70 @@ export const getFrontendStore = () => {
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
validate: screen => {
|
||||||
|
// Recursive function to find any illegal children in component trees
|
||||||
|
const findIllegalChild = (
|
||||||
|
component,
|
||||||
|
illegalChildren = [],
|
||||||
|
legalDirectChildren = []
|
||||||
|
) => {
|
||||||
|
const type = component._component
|
||||||
|
if (illegalChildren.includes(type)) {
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
legalDirectChildren.length &&
|
||||||
|
!legalDirectChildren.includes(type)
|
||||||
|
) {
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
if (!component?._children?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = store.actions.components.getDefinition(
|
||||||
|
component._component
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reset whitelist for direct children
|
||||||
|
legalDirectChildren = []
|
||||||
|
if (definition?.legalDirectChildren?.length) {
|
||||||
|
legalDirectChildren = definition.legalDirectChildren.map(x => {
|
||||||
|
return `@budibase/standard-components/${x}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append blacklisted components and remove duplicates
|
||||||
|
if (definition?.illegalChildren?.length) {
|
||||||
|
const blacklist = definition.illegalChildren.map(x => {
|
||||||
|
return `@budibase/standard-components/${x}`
|
||||||
|
})
|
||||||
|
illegalChildren = [...new Set([...illegalChildren, ...blacklist])]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse on all children
|
||||||
|
for (let child of component._children) {
|
||||||
|
const illegalChild = findIllegalChild(
|
||||||
|
child,
|
||||||
|
illegalChildren,
|
||||||
|
legalDirectChildren
|
||||||
|
)
|
||||||
|
if (illegalChild) {
|
||||||
|
return illegalChild
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the entire tree and throw an error if an illegal child is
|
||||||
|
// found anywhere
|
||||||
|
const illegalChild = findIllegalChild(screen.props)
|
||||||
|
if (illegalChild) {
|
||||||
|
const def = store.actions.components.getDefinition(illegalChild)
|
||||||
|
throw `You can't place a ${def.name} here`
|
||||||
|
}
|
||||||
|
},
|
||||||
save: async screen => {
|
save: async screen => {
|
||||||
|
store.actions.screens.validate(screen)
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
const creatingNewScreen = screen._id === undefined
|
const creatingNewScreen = screen._id === undefined
|
||||||
const savedScreen = await API.saveScreen(screen)
|
const savedScreen = await API.saveScreen(screen)
|
||||||
|
@ -445,7 +508,11 @@ export const getFrontendStore = () => {
|
||||||
return {
|
return {
|
||||||
_id: Helpers.uuid(),
|
_id: Helpers.uuid(),
|
||||||
_component: definition.component,
|
_component: definition.component,
|
||||||
_styles: { normal: {}, hover: {}, active: {} },
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
},
|
||||||
_instanceName: `New ${definition.friendlyName || definition.name}`,
|
_instanceName: `New ${definition.friendlyName || definition.name}`,
|
||||||
...cloneDeep(props),
|
...cloneDeep(props),
|
||||||
...extras,
|
...extras,
|
||||||
|
@ -533,12 +600,11 @@ export const getFrontendStore = () => {
|
||||||
},
|
},
|
||||||
patch: async (patchFn, componentId, screenId) => {
|
patch: async (patchFn, componentId, screenId) => {
|
||||||
// Use selected component by default
|
// Use selected component by default
|
||||||
if (!componentId && !screenId) {
|
if (!componentId || !screenId) {
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
componentId = state.selectedComponentId
|
componentId = componentId || state.selectedComponentId
|
||||||
screenId = state.selectedScreenId
|
screenId = screenId || state.selectedScreenId
|
||||||
}
|
}
|
||||||
// Invalid if only a screen or component ID provided
|
|
||||||
if (!componentId || !screenId || !patchFn) {
|
if (!componentId || !screenId || !patchFn) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -601,16 +667,14 @@ export const getFrontendStore = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Select the parent if cutting
|
// Select the parent if cutting
|
||||||
if (cut) {
|
if (cut && selectParent) {
|
||||||
const screen = get(selectedScreen)
|
const screen = get(selectedScreen)
|
||||||
const parent = findComponentParent(screen?.props, component._id)
|
const parent = findComponentParent(screen?.props, component._id)
|
||||||
if (parent) {
|
if (parent) {
|
||||||
if (selectParent) {
|
store.update(state => {
|
||||||
store.update(state => {
|
state.selectedComponentId = parent._id
|
||||||
state.selectedComponentId = parent._id
|
return state
|
||||||
return state
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -621,16 +685,24 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
let newComponentId
|
let newComponentId
|
||||||
|
|
||||||
|
// Remove copied component if cutting, regardless if pasting works
|
||||||
|
let componentToPaste = cloneDeep(state.componentToPaste)
|
||||||
|
if (componentToPaste.isCut) {
|
||||||
|
store.update(state => {
|
||||||
|
delete state.componentToPaste
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Patch screen
|
// Patch screen
|
||||||
const patch = screen => {
|
const patch = screen => {
|
||||||
// Get up to date ref to target
|
// Get up to date ref to target
|
||||||
targetComponent = findComponent(screen.props, targetComponent._id)
|
targetComponent = findComponent(screen.props, targetComponent._id)
|
||||||
if (!targetComponent) {
|
if (!targetComponent) {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
const cut = state.componentToPaste.isCut
|
const cut = componentToPaste.isCut
|
||||||
const originalId = state.componentToPaste._id
|
const originalId = componentToPaste._id
|
||||||
let componentToPaste = cloneDeep(state.componentToPaste)
|
|
||||||
delete componentToPaste.isCut
|
delete componentToPaste.isCut
|
||||||
|
|
||||||
// Make new component unique if copying
|
// Make new component unique if copying
|
||||||
|
@ -685,11 +757,8 @@ export const getFrontendStore = () => {
|
||||||
const targetScreenId = targetScreen?._id || state.selectedScreenId
|
const targetScreenId = targetScreen?._id || state.selectedScreenId
|
||||||
await store.actions.screens.patch(patch, targetScreenId)
|
await store.actions.screens.patch(patch, targetScreenId)
|
||||||
|
|
||||||
|
// Select the new component
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
// Remove copied component if cutting
|
|
||||||
if (state.componentToPaste.isCut) {
|
|
||||||
delete state.componentToPaste
|
|
||||||
}
|
|
||||||
state.selectedScreenId = targetScreenId
|
state.selectedScreenId = targetScreenId
|
||||||
state.selectedComponentId = newComponentId
|
state.selectedComponentId = newComponentId
|
||||||
return state
|
return state
|
||||||
|
@ -893,6 +962,15 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
updateStyles: async (styles, id) => {
|
||||||
|
const patchFn = component => {
|
||||||
|
component._styles.normal = {
|
||||||
|
...component._styles.normal,
|
||||||
|
...styles,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await store.actions.components.patch(patchFn, id)
|
||||||
|
},
|
||||||
updateCustomStyle: async style => {
|
updateCustomStyle: async style => {
|
||||||
await store.actions.components.patch(component => {
|
await store.actions.components.patch(component => {
|
||||||
component._styles.custom = style
|
component._styles.custom = style
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||||
import CreateEditUser from "./modals/CreateEditUser.svelte"
|
import CreateEditUser from "./modals/CreateEditUser.svelte"
|
||||||
import CreateEditColumn from "./modals/CreateEditColumn.svelte"
|
import CreateEditColumn from "./modals/CreateEditColumn.svelte"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
import {
|
import {
|
||||||
TableNames,
|
TableNames,
|
||||||
UNEDITABLE_USER_FIELDS,
|
UNEDITABLE_USER_FIELDS,
|
||||||
|
@ -110,7 +111,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const editColumn = field => {
|
const editColumn = field => {
|
||||||
editableColumn = schema?.[field]
|
editableColumn = cloneDeep(schema?.[field])
|
||||||
if (editableColumn) {
|
if (editableColumn) {
|
||||||
editColumnModal.show()
|
editColumnModal.show()
|
||||||
}
|
}
|
||||||
|
|
|
@ -488,7 +488,7 @@
|
||||||
]}
|
]}
|
||||||
getOptionLabel={option => option.label}
|
getOptionLabel={option => option.label}
|
||||||
getOptionValue={option => option.value}
|
getOptionValue={option => option.value}
|
||||||
tooltip="Dynamic formula are calculated when retrieved, but cannot be filtered,
|
tooltip="Dynamic formula are calculated when retrieved, but cannot be filtered or sorted by,
|
||||||
while static formula are calculated when the row is saved."
|
while static formula are calculated when the row is saved."
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -10,10 +10,14 @@
|
||||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import { IntegrationTypes } from "constants/backend"
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let datasource
|
export let datasource
|
||||||
export let schema
|
export let schema
|
||||||
export let creating
|
export let creating
|
||||||
|
const validation = createValidationStore()
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
function filter([key, value]) {
|
function filter([key, value]) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
@ -31,6 +35,17 @@
|
||||||
.filter(el => filter(el))
|
.filter(el => filter(el))
|
||||||
.map(([key]) => key)
|
.map(([key]) => key)
|
||||||
|
|
||||||
|
// setup the validation for each required field
|
||||||
|
$: configKeys.forEach(key => {
|
||||||
|
if (schema[key].required) {
|
||||||
|
validation.addValidatorType(key, schema[key].type, schema[key].required)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// run the validation whenever the config changes
|
||||||
|
$: validation.check(config)
|
||||||
|
// dispatch the validation result
|
||||||
|
$: dispatch("valid", $validation.valid)
|
||||||
|
|
||||||
let addButton
|
let addButton
|
||||||
|
|
||||||
function getDisplayName(key) {
|
function getDisplayName(key) {
|
||||||
|
@ -79,6 +94,7 @@
|
||||||
type={schema[configKey].type}
|
type={schema[configKey].type}
|
||||||
on:change
|
on:change
|
||||||
bind:value={config[configKey]}
|
bind:value={config[configKey]}
|
||||||
|
error={$validation.errors[configKey]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -88,6 +104,7 @@
|
||||||
type={schema[configKey].type}
|
type={schema[configKey].type}
|
||||||
on:change
|
on:change
|
||||||
bind:value={config[configKey]}
|
bind:value={config[configKey]}
|
||||||
|
error={$validation.errors[configKey]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
// kill the reference so the input isn't saved
|
// kill the reference so the input isn't saved
|
||||||
let datasource = cloneDeep(integration)
|
let datasource = cloneDeep(integration)
|
||||||
let skipFetch = false
|
let skipFetch = false
|
||||||
|
let isValid = false
|
||||||
|
|
||||||
$: name =
|
$: name =
|
||||||
IntegrationNames[datasource.type] || datasource.name || datasource.type
|
IntegrationNames[datasource.type] || datasource.name || datasource.type
|
||||||
|
@ -53,6 +54,7 @@
|
||||||
return true
|
return true
|
||||||
}}
|
}}
|
||||||
size="L"
|
size="L"
|
||||||
|
disabled={!isValid}
|
||||||
>
|
>
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<Body size="XS"
|
<Body size="XS"
|
||||||
|
@ -63,5 +65,6 @@
|
||||||
schema={datasource.schema}
|
schema={datasource.schema}
|
||||||
bind:datasource
|
bind:datasource
|
||||||
creating={true}
|
creating={true}
|
||||||
|
on:valid={e => (isValid = e.detail)}
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import TableSelect from "./controls/TableSelect.svelte"
|
||||||
import ColorPicker from "./controls/ColorPicker.svelte"
|
import ColorPicker from "./controls/ColorPicker.svelte"
|
||||||
import { IconSelect } from "./controls/IconSelect"
|
import { IconSelect } from "./controls/IconSelect"
|
||||||
import FieldSelect from "./controls/FieldSelect.svelte"
|
import FieldSelect from "./controls/FieldSelect.svelte"
|
||||||
|
import SortableFieldSelect from "./controls/SortableFieldSelect.svelte"
|
||||||
import MultiFieldSelect from "./controls/MultiFieldSelect.svelte"
|
import MultiFieldSelect from "./controls/MultiFieldSelect.svelte"
|
||||||
import SearchFieldSelect from "./controls/SearchFieldSelect.svelte"
|
import SearchFieldSelect from "./controls/SearchFieldSelect.svelte"
|
||||||
import SchemaSelect from "./controls/SchemaSelect.svelte"
|
import SchemaSelect from "./controls/SchemaSelect.svelte"
|
||||||
|
@ -41,6 +42,7 @@ const componentMap = {
|
||||||
filter: FilterEditor,
|
filter: FilterEditor,
|
||||||
url: URLSelect,
|
url: URLSelect,
|
||||||
columns: ColumnEditor,
|
columns: ColumnEditor,
|
||||||
|
"field/sortable": SortableFieldSelect,
|
||||||
"field/string": FormFieldSelect,
|
"field/string": FormFieldSelect,
|
||||||
"field/number": FormFieldSelect,
|
"field/number": FormFieldSelect,
|
||||||
"field/options": FormFieldSelect,
|
"field/options": FormFieldSelect,
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script>
|
||||||
|
import { Select } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
getDatasourceForProvider,
|
||||||
|
getSchemaForDatasource,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
import { currentAsset } from "builderStore"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { UNSORTABLE_TYPES } from "constants"
|
||||||
|
|
||||||
|
export let componentInstance = {}
|
||||||
|
export let value = ""
|
||||||
|
export let placeholder
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
|
$: schema = getSchemaForDatasource($currentAsset, datasource).schema
|
||||||
|
$: options = getSortableFields(schema)
|
||||||
|
$: boundValue = getValidValue(value, options)
|
||||||
|
|
||||||
|
const getSortableFields = schema => {
|
||||||
|
return Object.entries(schema || {})
|
||||||
|
.filter(entry => !UNSORTABLE_TYPES.includes(entry[1].type))
|
||||||
|
.map(entry => entry[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
const getValidValue = (value, options) => {
|
||||||
|
// Reset value if there aren't any options
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset value if the value isn't found in the options
|
||||||
|
if (options.indexOf(value) === -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = value => {
|
||||||
|
boundValue = getValidValue(value.detail, options)
|
||||||
|
dispatch("change", boundValue)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Select {placeholder} value={boundValue} on:change={onChange} {options} />
|
|
@ -0,0 +1,114 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
ActionMenu,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Modal,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import CreateRestoreModal from "./CreateRestoreModal.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let row
|
||||||
|
|
||||||
|
let deleteDialog
|
||||||
|
let restoreDialog
|
||||||
|
let updateDialog
|
||||||
|
let name
|
||||||
|
let restoreBackupModal
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const onClickRestore = name => {
|
||||||
|
dispatch("buttonclick", {
|
||||||
|
type: "backupRestore",
|
||||||
|
name,
|
||||||
|
backupId: row._id,
|
||||||
|
restoreBackupName: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickDelete = () => {
|
||||||
|
dispatch("buttonclick", {
|
||||||
|
type: "backupDelete",
|
||||||
|
backupId: row._id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickUpdate = () => {
|
||||||
|
dispatch("buttonclick", {
|
||||||
|
type: "backupUpdate",
|
||||||
|
backupId: row._id,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadExport() {
|
||||||
|
window.open(`/api/apps/${row.appId}/backups/${row._id}/file`, "_blank")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="cell">
|
||||||
|
<ActionMenu align="right">
|
||||||
|
<div slot="control">
|
||||||
|
<Icon size="M" hoverable name="MoreSmallList" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if row.type !== "restore"}
|
||||||
|
<MenuItem on:click={restoreDialog.show} icon="Revert">Restore</MenuItem>
|
||||||
|
<MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem>
|
||||||
|
<MenuItem on:click={downloadExport} icon="Download">Download</MenuItem>
|
||||||
|
{/if}
|
||||||
|
<MenuItem on:click={updateDialog.show} icon="Edit">Update</MenuItem>
|
||||||
|
</ActionMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal bind:this={restoreBackupModal}>
|
||||||
|
<CreateRestoreModal confirm={name => onClickRestore(name)} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={deleteDialog}
|
||||||
|
okText="Delete Backup"
|
||||||
|
onOk={onClickDelete}
|
||||||
|
title="Confirm Deletion"
|
||||||
|
>
|
||||||
|
Are you sure you wish to delete the backup
|
||||||
|
<i>{row.name}?</i>
|
||||||
|
This action cannot be undone.
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={restoreDialog}
|
||||||
|
okText="Continue"
|
||||||
|
onOk={restoreBackupModal?.show}
|
||||||
|
title="Confirm restore"
|
||||||
|
warning={false}
|
||||||
|
>
|
||||||
|
<Heading size="S">{row.name || "Backup"}</Heading>
|
||||||
|
<Body size="S">{new Date(row.timestamp).toLocaleString()}</Body>
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={updateDialog}
|
||||||
|
disabled={!name}
|
||||||
|
okText="Confirm"
|
||||||
|
onOk={onClickUpdate}
|
||||||
|
title="Update Backup"
|
||||||
|
warning={false}
|
||||||
|
>
|
||||||
|
<Input onlabel="Backup name" placeholder={row.name} bind:value={name} />
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script>
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let row
|
||||||
|
|
||||||
|
$: automations = row?.automations
|
||||||
|
$: datasources = row?.datasources
|
||||||
|
$: screens = row?.screens
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="cell">
|
||||||
|
{#if automations != null && screens != null && datasources != null}
|
||||||
|
<div class="item">
|
||||||
|
<Icon name="Data" />
|
||||||
|
<div>{datasources || 0}</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<Icon name="WebPage" />
|
||||||
|
<div>{screens || 0}</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<Icon name="JourneyVoyager" />
|
||||||
|
<div>{automations || 0}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: calc(var(--spacing-xl) * 2);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,345 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
ActionButton,
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Divider,
|
||||||
|
Layout,
|
||||||
|
Modal,
|
||||||
|
notifications,
|
||||||
|
Pagination,
|
||||||
|
Select,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Tags,
|
||||||
|
Tag,
|
||||||
|
Table,
|
||||||
|
Page,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { backups, licensing, auth, admin } from "stores/portal"
|
||||||
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
|
import AppSizeRenderer from "./AppSizeRenderer.svelte"
|
||||||
|
import CreateBackupModal from "./CreateBackupModal.svelte"
|
||||||
|
import ActionsRenderer from "./ActionsRenderer.svelte"
|
||||||
|
import DateRenderer from "./DateRenderer.svelte"
|
||||||
|
import UserRenderer from "./UserRenderer.svelte"
|
||||||
|
import StatusRenderer from "./StatusRenderer.svelte"
|
||||||
|
import TypeRenderer from "./TypeRenderer.svelte"
|
||||||
|
import BackupsDefault from "assets/backups-default.png"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
export let app
|
||||||
|
|
||||||
|
let backupData = null
|
||||||
|
let modal
|
||||||
|
let pageInfo = createPaginationStore()
|
||||||
|
let filterOpt = null
|
||||||
|
let startDate = null
|
||||||
|
let endDate = null
|
||||||
|
let filters = getFilters()
|
||||||
|
let loaded = false
|
||||||
|
$: page = $pageInfo.page
|
||||||
|
$: fetchBackups(filterOpt, page, startDate, endDate)
|
||||||
|
|
||||||
|
function getFilters() {
|
||||||
|
const options = []
|
||||||
|
let types = ["backup"]
|
||||||
|
let triggers = ["manual", "publish", "scheduled", "restoring"]
|
||||||
|
for (let type of types) {
|
||||||
|
for (let trigger of triggers) {
|
||||||
|
let label = `${trigger} ${type}`
|
||||||
|
label = label.charAt(0).toUpperCase() + label?.slice(1)
|
||||||
|
options.push({ label, value: { type, trigger } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options.push({
|
||||||
|
label: `Manual restore`,
|
||||||
|
value: { type: "restore", trigger: "manual" },
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: {
|
||||||
|
displayName: "Type",
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
displayName: "Date",
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
displayName: "Name",
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
appSize: {
|
||||||
|
displayName: "App size",
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
createdBy: {
|
||||||
|
displayName: "User",
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
displayName: "Status",
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
displayName: null,
|
||||||
|
width: "5%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const customRenderers = [
|
||||||
|
{ column: "appSize", component: AppSizeRenderer },
|
||||||
|
{ column: "actions", component: ActionsRenderer },
|
||||||
|
{ column: "createdAt", component: DateRenderer },
|
||||||
|
{ column: "createdBy", component: UserRenderer },
|
||||||
|
{ column: "status", component: StatusRenderer },
|
||||||
|
{ column: "type", component: TypeRenderer },
|
||||||
|
]
|
||||||
|
|
||||||
|
function flattenBackups(backups) {
|
||||||
|
return backups.map(backup => {
|
||||||
|
return {
|
||||||
|
...backup,
|
||||||
|
...backup?.contents,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchBackups(filters, page, startDate, endDate) {
|
||||||
|
const response = await backups.searchBackups({
|
||||||
|
appId: app.instance._id,
|
||||||
|
...filters,
|
||||||
|
page,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
})
|
||||||
|
pageInfo.fetched(response.hasNextPage, response.nextPage)
|
||||||
|
|
||||||
|
// flatten so we have an easier structure to use for the table schema
|
||||||
|
backupData = flattenBackups(response.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createManualBackup(name) {
|
||||||
|
try {
|
||||||
|
let response = await backups.createManualBackup({
|
||||||
|
appId: app.instance._id,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
await fetchBackups(filterOpt, page)
|
||||||
|
notifications.success(response.message)
|
||||||
|
} catch {
|
||||||
|
notifications.error("Unable to create backup")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleButtonClick({ detail }) {
|
||||||
|
if (detail.type === "backupDelete") {
|
||||||
|
await backups.deleteBackup({
|
||||||
|
appId: app.instance._id,
|
||||||
|
backupId: detail.backupId,
|
||||||
|
})
|
||||||
|
await fetchBackups(filterOpt, page)
|
||||||
|
} else if (detail.type === "backupRestore") {
|
||||||
|
await backups.restoreBackup({
|
||||||
|
appId: app.instance._id,
|
||||||
|
backupId: detail.backupId,
|
||||||
|
name: detail.restoreBackupName,
|
||||||
|
})
|
||||||
|
await fetchBackups(filterOpt, page)
|
||||||
|
} else if (detail.type === "backupUpdate") {
|
||||||
|
await backups.updateBackup({
|
||||||
|
appId: app.instance._id,
|
||||||
|
backupId: detail.backupId,
|
||||||
|
name: detail.name,
|
||||||
|
})
|
||||||
|
await fetchBackups(filterOpt, page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
fetchBackups(filterOpt, page, startDate, endDate)
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
{#if !$licensing.backupsEnabled}
|
||||||
|
<Page wide={false}>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<div class="title">
|
||||||
|
<Heading size="M">Backups</Heading>
|
||||||
|
<Tags>
|
||||||
|
<Tag icon="LockClosed">Pro plan</Tag>
|
||||||
|
</Tags>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Body>
|
||||||
|
Back up your apps and restore them to their previous state.
|
||||||
|
{#if !$auth.accountPortalAccess && !$licensing.groupsEnabled && $admin.cloud}
|
||||||
|
Contact your account holder to upgrade your plan.
|
||||||
|
{/if}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<div class="pro-buttons">
|
||||||
|
{#if $auth.accountPortalAccess}
|
||||||
|
<Button
|
||||||
|
newStyles
|
||||||
|
primary
|
||||||
|
disabled={!$auth.accountPortalAccess && $admin.cloud}
|
||||||
|
on:click={$licensing.goToUpgradePage()}
|
||||||
|
>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<!--Show the view plans button-->
|
||||||
|
<Button
|
||||||
|
newStyles
|
||||||
|
secondary
|
||||||
|
on:click={() => {
|
||||||
|
window.open("https://budibase.com/pricing/", "_blank")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View plans
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
{:else if backupData?.length === 0 && !loaded && !filterOpt && !startDate}
|
||||||
|
<Page wide={false}>
|
||||||
|
<div class="align">
|
||||||
|
<img
|
||||||
|
width="220px"
|
||||||
|
height="130px"
|
||||||
|
src={BackupsDefault}
|
||||||
|
alt="BackupsDefault"
|
||||||
|
/>
|
||||||
|
<Layout gap="S">
|
||||||
|
<Heading>You have no backups yet</Heading>
|
||||||
|
<div class="opacity">
|
||||||
|
<Body size="S">You can manually backup your app any time</Body>
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
<Button on:click={modal.show} cta>Create Backup</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
{:else if loaded}
|
||||||
|
<Layout noPadding gap="M" alignContent="start">
|
||||||
|
<div class="search">
|
||||||
|
<div class="select">
|
||||||
|
<Select
|
||||||
|
placeholder="All"
|
||||||
|
label="Type"
|
||||||
|
options={filters}
|
||||||
|
getOptionValue={filter => filter.value}
|
||||||
|
getOptionLabel={filter => filter.label}
|
||||||
|
bind:value={filterOpt}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<DatePicker
|
||||||
|
range={true}
|
||||||
|
label={"Filter Range"}
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail[0].length > 1) {
|
||||||
|
startDate = e.detail[0][0].toISOString()
|
||||||
|
endDate = e.detail[0][1].toISOString()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="split-buttons">
|
||||||
|
<ActionButton on:click={modal.show} icon="SaveAsFloppy"
|
||||||
|
>Create new backup</ActionButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
{schema}
|
||||||
|
disableSorting
|
||||||
|
allowSelectRows={false}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
data={backupData}
|
||||||
|
{customRenderers}
|
||||||
|
placeholderText="No backups found"
|
||||||
|
border={false}
|
||||||
|
on:buttonclick={handleButtonClick}
|
||||||
|
/>
|
||||||
|
<div class="pagination">
|
||||||
|
<Pagination
|
||||||
|
page={$pageInfo.pageNumber}
|
||||||
|
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
|
||||||
|
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
|
||||||
|
goToPrevPage={pageInfo.prevPage}
|
||||||
|
goToNextPage={pageInfo.nextPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<CreateBackupModal {createManualBackup} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
height: 100%;
|
||||||
|
padding: var(--spectrum-alias-grid-gutter-medium)
|
||||||
|
var(--spectrum-alias-grid-gutter-large);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
width: 100%;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
flex-basis: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 1;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.align {
|
||||||
|
margin-top: 5%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pro-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script>
|
||||||
|
import { ModalContent, Input } from "@budibase/bbui"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
|
export let createManualBackup
|
||||||
|
|
||||||
|
let templateName = $auth.user.firstName
|
||||||
|
? `${$auth.user.firstName}'s Backup`
|
||||||
|
: "New Backup"
|
||||||
|
let name = templateName
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
onConfirm={() => createManualBackup(name)}
|
||||||
|
title="Create new backup"
|
||||||
|
diabled={!name}
|
||||||
|
confirmText="Create"
|
||||||
|
><Input label="Backup name" bind:value={name} /></ModalContent
|
||||||
|
>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script>
|
||||||
|
import { ModalContent, Input, Body } from "@budibase/bbui"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
|
export let confirm
|
||||||
|
|
||||||
|
let templateName = $auth.user.firstName
|
||||||
|
? `${$auth.user.firstName}'s Backup`
|
||||||
|
: "Restore Backup"
|
||||||
|
let name = templateName
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
onConfirm={() => confirm(name)}
|
||||||
|
title="Back up your current version"
|
||||||
|
confirmText="Confirm Restore"
|
||||||
|
disabled={!name}
|
||||||
|
>
|
||||||
|
<Body size="S"
|
||||||
|
>Create a backup of your current app to allow you to roll back after
|
||||||
|
restoring this backup</Body
|
||||||
|
>
|
||||||
|
<Input label="Backup name" bind:value={name} />
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script>
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="cell">
|
||||||
|
{#if value != null}
|
||||||
|
<Icon name="Data" />
|
||||||
|
<div>{value || 0}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script>
|
||||||
|
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime"
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
export let value
|
||||||
|
$: timeSince = dayjs(value).fromNow()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="cell">
|
||||||
|
{timeSince} - <DateTimeRenderer {value} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script>
|
||||||
|
import { Badge } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let value = "started"
|
||||||
|
$: status = value[0].toUpperCase() + value?.slice(1)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
grey={value === "started" || value === "pending"}
|
||||||
|
green={value === "complete"}
|
||||||
|
red={value === "failed"}
|
||||||
|
size="S"
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</Badge>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script>
|
||||||
|
export let row
|
||||||
|
|
||||||
|
$: baseTrig = row?.trigger || "manual"
|
||||||
|
$: type = row?.type || "backup"
|
||||||
|
$: trigger = baseTrig.charAt(0).toUpperCase() + baseTrig.slice(1)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="cell">
|
||||||
|
{trigger}
|
||||||
|
{type}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script>
|
||||||
|
export let value
|
||||||
|
|
||||||
|
let firstName = value?.firstName
|
||||||
|
let lastName = value?.lastName || ""
|
||||||
|
|
||||||
|
$: username =
|
||||||
|
firstName && lastName ? `${firstName} ${lastName}` : value?.email
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="cell">
|
||||||
|
{#if value != null}
|
||||||
|
<div>{username}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -17,7 +17,12 @@ export const convertCamel = str => {
|
||||||
|
|
||||||
export const pipe = (arg, funcs) => flow(funcs)(arg)
|
export const pipe = (arg, funcs) => flow(funcs)(arg)
|
||||||
|
|
||||||
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
|
export const capitalise = s => {
|
||||||
|
if (!s) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s.substring(0, 1).toUpperCase() + s.substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
|
export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import { object } from "yup"
|
import { object, string, number } from "yup"
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
|
@ -20,6 +20,32 @@ export const createValidationStore = () => {
|
||||||
validator[propertyName] = propertyValidator
|
validator[propertyName] = propertyValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addValidatorType = (propertyName, type, required) => {
|
||||||
|
if (!type || !propertyName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let propertyValidator
|
||||||
|
switch (type) {
|
||||||
|
case "number":
|
||||||
|
propertyValidator = number().transform(value =>
|
||||||
|
isNaN(value) ? undefined : value
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case "email":
|
||||||
|
propertyValidator = string().email()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
propertyValidator = string()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required) {
|
||||||
|
propertyValidator = propertyValidator.required()
|
||||||
|
}
|
||||||
|
|
||||||
|
validator[propertyName] = propertyValidator
|
||||||
|
}
|
||||||
|
|
||||||
const check = async values => {
|
const check = async values => {
|
||||||
const obj = object().shape(validator)
|
const obj = object().shape(validator)
|
||||||
// clear the previous errors
|
// clear the previous errors
|
||||||
|
@ -62,5 +88,6 @@ export const createValidationStore = () => {
|
||||||
set: validation.set,
|
set: validation.set,
|
||||||
check,
|
check,
|
||||||
addValidator,
|
addValidator,
|
||||||
|
addValidatorType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,18 +113,23 @@
|
||||||
>
|
>
|
||||||
Access
|
Access
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{#if isPublished}
|
<MenuItem
|
||||||
<MenuItem
|
on:click={() =>
|
||||||
on:click={() =>
|
$goto(
|
||||||
$goto(
|
`../../portal/overview/${application}?tab=${encodeURIComponent(
|
||||||
`../../portal/overview/${application}?tab=${encodeURIComponent(
|
"Automation History"
|
||||||
"Automation History"
|
)}`
|
||||||
)}`
|
)}
|
||||||
)}
|
>
|
||||||
>
|
Automation history
|
||||||
Automation history
|
</MenuItem>
|
||||||
</MenuItem>
|
<MenuItem
|
||||||
{/if}
|
on:click={() =>
|
||||||
|
$goto(`../../portal/overview/${application}?tab=Backups`)}
|
||||||
|
>
|
||||||
|
Backups
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
$goto(`../../portal/overview/${application}?tab=Settings`)}
|
$goto(`../../portal/overview/${application}?tab=Settings`)}
|
||||||
|
|
|
@ -23,7 +23,8 @@
|
||||||
|
|
||||||
let importQueriesModal
|
let importQueriesModal
|
||||||
|
|
||||||
let changed
|
let changed,
|
||||||
|
isValid = true
|
||||||
let integration, baseDatasource, datasource
|
let integration, baseDatasource, datasource
|
||||||
let queryList
|
let queryList
|
||||||
const querySchema = {
|
const querySchema = {
|
||||||
|
@ -101,12 +102,15 @@
|
||||||
<Divider />
|
<Divider />
|
||||||
<div class="config-header">
|
<div class="config-header">
|
||||||
<Heading size="S">Configuration</Heading>
|
<Heading size="S">Configuration</Heading>
|
||||||
<Button disabled={!changed} cta on:click={saveDatasource}>Save</Button>
|
<Button disabled={!changed || !isValid} cta on:click={saveDatasource}
|
||||||
|
>Save</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<IntegrationConfigForm
|
<IntegrationConfigForm
|
||||||
on:change={hasChanged}
|
on:change={hasChanged}
|
||||||
schema={integration.datasource}
|
schema={integration.datasource}
|
||||||
bind:datasource
|
bind:datasource
|
||||||
|
on:valid={e => (isValid = e.detail)}
|
||||||
/>
|
/>
|
||||||
{#if datasource.plus}
|
{#if datasource.plus}
|
||||||
<PlusConfigForm bind:datasource save={saveDatasource} />
|
<PlusConfigForm bind:datasource save={saveDatasource} />
|
||||||
|
|
|
@ -86,7 +86,11 @@
|
||||||
: [],
|
: [],
|
||||||
isBudibaseEvent: true,
|
isBudibaseEvent: true,
|
||||||
usedPlugins: $store.usedPlugins,
|
usedPlugins: $store.usedPlugins,
|
||||||
location: window.location,
|
location: {
|
||||||
|
protocol: window.location.protocol,
|
||||||
|
hostname: window.location.hostname,
|
||||||
|
port: window.location.port,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the preview when required
|
// Refresh the preview when required
|
||||||
|
@ -99,7 +103,7 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register handler to send custom to the preview
|
// Register handler to send custom to the preview
|
||||||
$: store.actions.preview.registerEventHandler((name, payload) => {
|
$: sendPreviewEvent = (name, payload) => {
|
||||||
iframe?.contentWindow.postMessage(
|
iframe?.contentWindow.postMessage(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
name,
|
name,
|
||||||
|
@ -108,120 +112,116 @@
|
||||||
runtimeEvent: true,
|
runtimeEvent: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
$: store.actions.preview.registerEventHandler(sendPreviewEvent)
|
||||||
|
|
||||||
// Update the iframe with the builder info to render the correct preview
|
// Update the iframe with the builder info to render the correct preview
|
||||||
const refreshContent = message => {
|
const refreshContent = message => {
|
||||||
iframe?.contentWindow.postMessage(message)
|
iframe?.contentWindow.postMessage(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
const receiveMessage = message => {
|
const receiveMessage = async message => {
|
||||||
const handlers = {
|
if (!message?.data?.type) {
|
||||||
[MessageTypes.READY]: () => {
|
|
||||||
// Initialise the app when mounted
|
|
||||||
if ($store.clientFeatures.messagePassing) {
|
|
||||||
if (!loading) return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display preview immediately if the intelligent loading feature
|
|
||||||
// is not supported
|
|
||||||
if (!$store.clientFeatures.intelligentLoading) {
|
|
||||||
loading = false
|
|
||||||
}
|
|
||||||
refreshContent(json)
|
|
||||||
},
|
|
||||||
[MessageTypes.ERROR]: event => {
|
|
||||||
// Catch any app errors
|
|
||||||
loading = false
|
|
||||||
error = event.error || "An unknown error occurred"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageHandler = handlers[message.data.type] || handleBudibaseEvent
|
|
||||||
messageHandler(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBudibaseEvent = async event => {
|
|
||||||
const { type, data } = event.data || event.detail
|
|
||||||
if (!type) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Await the event handler
|
||||||
try {
|
try {
|
||||||
if (type === "select-component" && data.id) {
|
await handleBudibaseEvent(message)
|
||||||
$store.selectedComponentId = data.id
|
|
||||||
if (!$isActive("./components")) {
|
|
||||||
$goto("./components")
|
|
||||||
}
|
|
||||||
} else if (type === "update-prop") {
|
|
||||||
await store.actions.components.updateSetting(data.prop, data.value)
|
|
||||||
} else if (type === "delete-component" && data.id) {
|
|
||||||
// Legacy type, can be deleted in future
|
|
||||||
confirmDeleteComponent(data.id)
|
|
||||||
} else if (type === "key-down") {
|
|
||||||
const { key, ctrlKey } = data
|
|
||||||
document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey }))
|
|
||||||
} else if (type === "duplicate-component" && data.id) {
|
|
||||||
const rootComponent = get(currentAsset).props
|
|
||||||
const component = findComponent(rootComponent, data.id)
|
|
||||||
store.actions.components.copy(component)
|
|
||||||
await store.actions.components.paste(component)
|
|
||||||
} else if (type === "preview-loaded") {
|
|
||||||
// Wait for this event to show the client library if intelligent
|
|
||||||
// loading is supported
|
|
||||||
loading = false
|
|
||||||
} else if (type === "move-component") {
|
|
||||||
const { componentId, destinationComponentId } = data
|
|
||||||
const rootComponent = get(currentAsset).props
|
|
||||||
|
|
||||||
// Get source and destination components
|
|
||||||
const source = findComponent(rootComponent, componentId)
|
|
||||||
const destination = findComponent(rootComponent, destinationComponentId)
|
|
||||||
|
|
||||||
// Stop if the target is a child of source
|
|
||||||
const path = findComponentPath(source, destinationComponentId)
|
|
||||||
const ids = path.map(component => component._id)
|
|
||||||
if (ids.includes(data.destinationComponentId)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cut and paste the component to the new destination
|
|
||||||
if (source && destination) {
|
|
||||||
store.actions.components.copy(source, true)
|
|
||||||
await store.actions.components.paste(destination, data.mode)
|
|
||||||
}
|
|
||||||
} else if (type === "click-nav") {
|
|
||||||
if (!$isActive("./navigation")) {
|
|
||||||
$goto("./navigation")
|
|
||||||
}
|
|
||||||
} else if (type === "request-add-component") {
|
|
||||||
toggleAddComponent()
|
|
||||||
} else if (type === "highlight-setting") {
|
|
||||||
store.actions.settings.highlight(data.setting)
|
|
||||||
|
|
||||||
// Also scroll setting into view
|
|
||||||
const selector = `[data-cy="${data.setting}-prop-control"`
|
|
||||||
const element = document.querySelector(selector)?.parentElement
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({
|
|
||||||
behavior: "smooth",
|
|
||||||
block: "center",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if (type === "eject-block") {
|
|
||||||
const { id, definition } = data
|
|
||||||
await store.actions.components.handleEjectBlock(id, definition)
|
|
||||||
} else if (type === "reload-plugin") {
|
|
||||||
await store.actions.components.refreshDefinitions()
|
|
||||||
} else if (type === "drop-new-component") {
|
|
||||||
const { component, parent, index } = data
|
|
||||||
await store.actions.components.create(component, null, parent, index)
|
|
||||||
} else {
|
|
||||||
console.warn(`Client sent unknown event type: ${type}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error)
|
notifications.error(error || "Error handling event from app preview")
|
||||||
notifications.error("Error handling event from app preview")
|
}
|
||||||
|
|
||||||
|
// Reply that the event has been completed
|
||||||
|
if (message.data?.id) {
|
||||||
|
sendPreviewEvent("event-completed", message.data?.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBudibaseEvent = async event => {
|
||||||
|
const { type, data } = event.data
|
||||||
|
if (type === MessageTypes.READY) {
|
||||||
|
// Initialise the app when mounted
|
||||||
|
if (!loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshContent(json)
|
||||||
|
} else if (type === MessageTypes.ERROR) {
|
||||||
|
// Catch any app errors
|
||||||
|
loading = false
|
||||||
|
error = event.error || "An unknown error occurred"
|
||||||
|
} else if (type === "select-component" && data.id) {
|
||||||
|
$store.selectedComponentId = data.id
|
||||||
|
if (!$isActive("./components")) {
|
||||||
|
$goto("./components")
|
||||||
|
}
|
||||||
|
} else if (type === "update-prop") {
|
||||||
|
await store.actions.components.updateSetting(data.prop, data.value)
|
||||||
|
} else if (type === "update-styles") {
|
||||||
|
await store.actions.components.updateStyles(data.styles, data.id)
|
||||||
|
} else if (type === "delete-component" && data.id) {
|
||||||
|
// Legacy type, can be deleted in future
|
||||||
|
confirmDeleteComponent(data.id)
|
||||||
|
} else if (type === "key-down") {
|
||||||
|
const { key, ctrlKey } = data
|
||||||
|
document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey }))
|
||||||
|
} else if (type === "duplicate-component" && data.id) {
|
||||||
|
const rootComponent = get(currentAsset).props
|
||||||
|
const component = findComponent(rootComponent, data.id)
|
||||||
|
store.actions.components.copy(component)
|
||||||
|
await store.actions.components.paste(component)
|
||||||
|
} else if (type === "preview-loaded") {
|
||||||
|
// Wait for this event to show the client library if intelligent
|
||||||
|
// loading is supported
|
||||||
|
loading = false
|
||||||
|
} else if (type === "move-component") {
|
||||||
|
const { componentId, destinationComponentId } = data
|
||||||
|
const rootComponent = get(currentAsset).props
|
||||||
|
|
||||||
|
// Get source and destination components
|
||||||
|
const source = findComponent(rootComponent, componentId)
|
||||||
|
const destination = findComponent(rootComponent, destinationComponentId)
|
||||||
|
|
||||||
|
// Stop if the target is a child of source
|
||||||
|
const path = findComponentPath(source, destinationComponentId)
|
||||||
|
const ids = path.map(component => component._id)
|
||||||
|
if (ids.includes(data.destinationComponentId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cut and paste the component to the new destination
|
||||||
|
if (source && destination) {
|
||||||
|
store.actions.components.copy(source, true, false)
|
||||||
|
await store.actions.components.paste(destination, data.mode)
|
||||||
|
}
|
||||||
|
} else if (type === "click-nav") {
|
||||||
|
if (!$isActive("./navigation")) {
|
||||||
|
$goto("./navigation")
|
||||||
|
}
|
||||||
|
} else if (type === "request-add-component") {
|
||||||
|
toggleAddComponent()
|
||||||
|
} else if (type === "highlight-setting") {
|
||||||
|
store.actions.settings.highlight(data.setting)
|
||||||
|
|
||||||
|
// Also scroll setting into view
|
||||||
|
const selector = `[data-cy="${data.setting}-prop-control"`
|
||||||
|
const element = document.querySelector(selector)?.parentElement
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (type === "eject-block") {
|
||||||
|
const { id, definition } = data
|
||||||
|
await store.actions.components.handleEjectBlock(id, definition)
|
||||||
|
} else if (type === "reload-plugin") {
|
||||||
|
await store.actions.components.refreshDefinitions()
|
||||||
|
} else if (type === "drop-new-component") {
|
||||||
|
const { component, parent, index } = data
|
||||||
|
await store.actions.components.create(component, null, parent, index)
|
||||||
|
} else {
|
||||||
|
console.warn(`Client sent unknown event type: ${type}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,42 +254,10 @@
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener("message", receiveMessage)
|
window.addEventListener("message", receiveMessage)
|
||||||
if (!$store.clientFeatures.messagePassing) {
|
|
||||||
// Legacy - remove in later versions of BB
|
|
||||||
iframe.contentWindow.addEventListener(
|
|
||||||
"ready",
|
|
||||||
() => {
|
|
||||||
receiveMessage({ data: { type: MessageTypes.READY } })
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
)
|
|
||||||
iframe.contentWindow.addEventListener(
|
|
||||||
"error",
|
|
||||||
event => {
|
|
||||||
receiveMessage({
|
|
||||||
data: { type: MessageTypes.ERROR, error: event.detail },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
)
|
|
||||||
// Add listener for events sent by client library in preview
|
|
||||||
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Remove all iframe event listeners on component destroy
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
window.removeEventListener("message", receiveMessage)
|
window.removeEventListener("message", receiveMessage)
|
||||||
|
|
||||||
if (iframe.contentWindow) {
|
|
||||||
if (!$store.clientFeatures.messagePassing) {
|
|
||||||
// Legacy - remove in later versions of BB
|
|
||||||
iframe.contentWindow.removeEventListener(
|
|
||||||
"bb-event",
|
|
||||||
handleBudibaseEvent
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -80,10 +80,9 @@
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
}
|
}
|
||||||
return handler(component)
|
return await handler(component)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
notifications.error(error || "Error handling key press")
|
||||||
notifications.error("Error handling key press")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,34 +70,12 @@
|
||||||
closedNodes = closedNodes
|
closedNodes = closedNodes
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDrop = async (e, component) => {
|
const onDrop = async e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
try {
|
try {
|
||||||
const compDef = store.actions.components.getDefinition(
|
|
||||||
$dndStore.source?._component
|
|
||||||
)
|
|
||||||
if (!compDef) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const compTypeName = compDef.name.toLowerCase()
|
|
||||||
const path = findComponentPath(currentScreen.props, component._id)
|
|
||||||
|
|
||||||
for (let pathComp of path) {
|
|
||||||
const pathCompDef = store.actions.components.getDefinition(
|
|
||||||
pathComp?._component
|
|
||||||
)
|
|
||||||
if (pathCompDef?.illegalChildren?.indexOf(compTypeName) > -1) {
|
|
||||||
notifications.warning(
|
|
||||||
`${compDef.name} cannot be a child of ${pathCompDef.name} (${pathComp._instanceName})`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await dndStore.actions.drop()
|
await dndStore.actions.drop()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
notifications.error(error || "Error saving component")
|
||||||
notifications.error("Error saving component")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,9 +115,7 @@
|
||||||
on:dragstart={() => dndStore.actions.dragstart(component)}
|
on:dragstart={() => dndStore.actions.dragstart(component)}
|
||||||
on:dragover={dragover(component, index)}
|
on:dragover={dragover(component, index)}
|
||||||
on:iconClick={() => toggleNodeOpen(component._id)}
|
on:iconClick={() => toggleNodeOpen(component._id)}
|
||||||
on:drop={e => {
|
on:drop={onDrop}
|
||||||
onDrop(e, component)
|
|
||||||
}}
|
|
||||||
text={getComponentText(component)}
|
text={getComponentText(component)}
|
||||||
icon={getComponentIcon(component)}
|
icon={getComponentIcon(component)}
|
||||||
withArrow={componentHasChildren(component)}
|
withArrow={componentHasChildren(component)}
|
||||||
|
|
|
@ -29,6 +29,10 @@
|
||||||
|
|
||||||
// Filter out settings which shouldn't be rendered
|
// Filter out settings which shouldn't be rendered
|
||||||
sections.forEach(section => {
|
sections.forEach(section => {
|
||||||
|
section.visible = shouldDisplay(instance, section)
|
||||||
|
if (!section.visible) {
|
||||||
|
return
|
||||||
|
}
|
||||||
section.settings.forEach(setting => {
|
section.settings.forEach(setting => {
|
||||||
setting.visible = canRenderControl(instance, setting, isScreen)
|
setting.visible = canRenderControl(instance, setting, isScreen)
|
||||||
})
|
})
|
||||||
|
@ -46,17 +50,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const canRenderControl = (instance, setting, isScreen) => {
|
const shouldDisplay = (instance, setting) => {
|
||||||
// Prevent rendering on click setting for screens
|
|
||||||
if (setting?.type === "event" && isScreen) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const control = getComponentForSetting(setting)
|
|
||||||
if (!control) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse dependant settings
|
// Parse dependant settings
|
||||||
if (setting.dependsOn) {
|
if (setting.dependsOn) {
|
||||||
let dependantSetting = setting.dependsOn
|
let dependantSetting = setting.dependsOn
|
||||||
|
@ -93,6 +87,19 @@
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canRenderControl = (instance, setting, isScreen) => {
|
||||||
|
// Prevent rendering on click setting for screens
|
||||||
|
if (setting?.type === "event" && isScreen) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const control = getComponentForSetting(setting)
|
||||||
|
if (!control) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldDisplay(instance, setting)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each sections as section, idx (section.name)}
|
{#each sections as section, idx (section.name)}
|
||||||
|
|
|
@ -11,9 +11,10 @@
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import structure from "./componentStructure.json"
|
import structure from "./componentStructure.json"
|
||||||
import { store, selectedComponent } from "builderStore"
|
import { store, selectedComponent, selectedScreen } from "builderStore"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
|
import { findComponentPath } from "builderStore/componentUtils"
|
||||||
|
|
||||||
let section = "components"
|
let section = "components"
|
||||||
let searchString
|
let searchString
|
||||||
|
@ -21,8 +22,10 @@
|
||||||
let selectedIndex
|
let selectedIndex
|
||||||
let componentList = []
|
let componentList = []
|
||||||
|
|
||||||
$: currentDefinition = store.actions.components.getDefinition(
|
$: allowedComponents = getAllowedComponents(
|
||||||
$selectedComponent?._component
|
$store.components,
|
||||||
|
$selectedScreen,
|
||||||
|
$selectedComponent
|
||||||
)
|
)
|
||||||
$: enrichedStructure = enrichStructure(
|
$: enrichedStructure = enrichStructure(
|
||||||
structure,
|
structure,
|
||||||
|
@ -31,13 +34,50 @@
|
||||||
)
|
)
|
||||||
$: filteredStructure = filterStructure(
|
$: filteredStructure = filterStructure(
|
||||||
enrichedStructure,
|
enrichedStructure,
|
||||||
section,
|
allowedComponents,
|
||||||
currentDefinition,
|
|
||||||
searchString
|
searchString
|
||||||
)
|
)
|
||||||
$: blocks = enrichedStructure.find(x => x.name === "Blocks").children
|
$: blocks = enrichedStructure.find(x => x.name === "Blocks").children
|
||||||
$: orderMap = createComponentOrderMap(componentList)
|
$: orderMap = createComponentOrderMap(componentList)
|
||||||
|
|
||||||
|
const getAllowedComponents = (allComponents, screen, component) => {
|
||||||
|
const path = findComponentPath(screen?.props, component?._id)
|
||||||
|
if (!path?.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get initial set of allowed components
|
||||||
|
let allowedComponents = []
|
||||||
|
const definition = store.actions.components.getDefinition(
|
||||||
|
component?._component
|
||||||
|
)
|
||||||
|
if (definition.legalDirectChildren?.length) {
|
||||||
|
allowedComponents = definition.legalDirectChildren.map(x => {
|
||||||
|
return `@budibase/standard-components/${x}`
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
allowedComponents = Object.keys(allComponents)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build up list of illegal children from ancestors
|
||||||
|
let illegalChildren = definition.illegalChildren || []
|
||||||
|
path.forEach(ancestor => {
|
||||||
|
const def = store.actions.components.getDefinition(ancestor._component)
|
||||||
|
const blacklist = def?.illegalChildren?.map(x => {
|
||||||
|
return `@budibase/standard-components/${x}`
|
||||||
|
})
|
||||||
|
illegalChildren = [...illegalChildren, ...(blacklist || [])]
|
||||||
|
})
|
||||||
|
illegalChildren = [...new Set(illegalChildren)]
|
||||||
|
|
||||||
|
// Filter out illegal children from allowed components
|
||||||
|
allowedComponents = allowedComponents.filter(x => {
|
||||||
|
return !illegalChildren.includes(x)
|
||||||
|
})
|
||||||
|
|
||||||
|
return allowedComponents
|
||||||
|
}
|
||||||
|
|
||||||
// Creates a simple lookup map from an array, so we can find the selected
|
// Creates a simple lookup map from an array, so we can find the selected
|
||||||
// component much faster
|
// component much faster
|
||||||
const createComponentOrderMap = list => {
|
const createComponentOrderMap = list => {
|
||||||
|
@ -90,7 +130,7 @@
|
||||||
return enrichedStructure
|
return enrichedStructure
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterStructure = (structure, section, currentDefinition, search) => {
|
const filterStructure = (structure, allowedComponents, search) => {
|
||||||
selectedIndex = search ? 0 : null
|
selectedIndex = search ? 0 : null
|
||||||
componentList = []
|
componentList = []
|
||||||
if (!structure?.length) {
|
if (!structure?.length) {
|
||||||
|
@ -114,7 +154,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the component is allowed as a child
|
// Check if the component is allowed as a child
|
||||||
return !currentDefinition?.illegalChildren?.includes(name)
|
return allowedComponents.includes(child.component)
|
||||||
})
|
})
|
||||||
if (matchedChildren.length) {
|
if (matchedChildren.length) {
|
||||||
filteredStructure.push({
|
filteredStructure.push({
|
||||||
|
@ -138,7 +178,7 @@
|
||||||
await store.actions.components.create(component)
|
await store.actions.components.create(component)
|
||||||
$goto("../")
|
$goto("../")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error creating component")
|
notifications.error(error || "Error creating component")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
"tableblock",
|
"tableblock",
|
||||||
"cardsblock",
|
"cardsblock",
|
||||||
"repeaterblock",
|
"repeaterblock",
|
||||||
"formblock"
|
"formblock",
|
||||||
|
"chartblock"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -14,7 +15,8 @@
|
||||||
"icon": "ClassicGridView",
|
"icon": "ClassicGridView",
|
||||||
"children": [
|
"children": [
|
||||||
"container",
|
"container",
|
||||||
"section"
|
"section",
|
||||||
|
"grid"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { apps, auth, admin, templates, licensing } from "stores/portal"
|
import { apps, auth, admin, templates, licensing } from "stores/portal"
|
||||||
import download from "downloadjs"
|
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import AppRow from "components/start/AppRow.svelte"
|
import AppRow from "components/start/AppRow.svelte"
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
|
@ -140,7 +139,7 @@
|
||||||
|
|
||||||
const initiateAppsExport = () => {
|
const initiateAppsExport = () => {
|
||||||
try {
|
try {
|
||||||
download(`/api/cloud/export`)
|
window.location = `/api/cloud/export`
|
||||||
notifications.success("Apps exported successfully")
|
notifications.success("Apps exported successfully")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error exporting apps: ${err}`)
|
notifications.error(`Error exporting apps: ${err}`)
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
Add plugin
|
Add plugin
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{#if filteredPlugins?.length}
|
{#if $plugins?.length}
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<div class="select">
|
<div class="select">
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
||||||
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||||
import { onDestroy, onMount } from "svelte"
|
import { onDestroy, onMount } from "svelte"
|
||||||
|
import BackupsTab from "components/portal/overview/backups/BackupsTab.svelte"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
|
||||||
|
@ -318,16 +319,12 @@
|
||||||
<Tab title="Access">
|
<Tab title="Access">
|
||||||
<AccessTab app={selectedApp} />
|
<AccessTab app={selectedApp} />
|
||||||
</Tab>
|
</Tab>
|
||||||
{#if isPublished}
|
<Tab title="Automation History">
|
||||||
<Tab title="Automation History">
|
<HistoryTab app={selectedApp} />
|
||||||
<HistoryTab app={selectedApp} />
|
</Tab>
|
||||||
</Tab>
|
<Tab title="Backups">
|
||||||
{/if}
|
<BackupsTab app={selectedApp} />
|
||||||
{#if false}
|
</Tab>
|
||||||
<Tab title="Backups">
|
|
||||||
<div class="container">Backups contents</div>
|
|
||||||
</Tab>
|
|
||||||
{/if}
|
|
||||||
<Tab title="Settings">
|
<Tab title="Settings">
|
||||||
<SettingsTab app={selectedApp} />
|
<SettingsTab app={selectedApp} />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
|
@ -2,6 +2,9 @@ import { writable } from "svelte/store"
|
||||||
import { AppStatus } from "../../constants"
|
import { AppStatus } from "../../constants"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
|
// properties that should always come from the dev app, not the deployed
|
||||||
|
const DEV_PROPS = ["updatedBy", "updatedAt"]
|
||||||
|
|
||||||
const extractAppId = id => {
|
const extractAppId = id => {
|
||||||
const split = id?.split("_") || []
|
const split = id?.split("_") || []
|
||||||
return split.length ? split[split.length - 1] : null
|
return split.length ? split[split.length - 1] : null
|
||||||
|
@ -57,9 +60,19 @@ export function createAppStore() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let devProps = {}
|
||||||
|
if (appMap[id]) {
|
||||||
|
const entries = Object.entries(appMap[id]).filter(
|
||||||
|
([key]) => DEV_PROPS.indexOf(key) !== -1
|
||||||
|
)
|
||||||
|
entries.forEach(entry => {
|
||||||
|
devProps[entry[0]] = entry[1]
|
||||||
|
})
|
||||||
|
}
|
||||||
appMap[id] = {
|
appMap[id] = {
|
||||||
...appMap[id],
|
...appMap[id],
|
||||||
...app,
|
...app,
|
||||||
|
...devProps,
|
||||||
prodId: app.appId,
|
prodId: app.appId,
|
||||||
prodRev: app._rev,
|
prodRev: app._rev,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { API } from "api"
|
||||||
|
|
||||||
|
export function createBackupsStore() {
|
||||||
|
const store = writable({})
|
||||||
|
|
||||||
|
function selectBackup(backupId) {
|
||||||
|
store.update(state => {
|
||||||
|
state.selectedBackup = backupId
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchBackups({
|
||||||
|
appId,
|
||||||
|
trigger,
|
||||||
|
type,
|
||||||
|
page,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
}) {
|
||||||
|
return API.searchBackups({ appId, trigger, type, page, startDate, endDate })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreBackup({ appId, backupId, name }) {
|
||||||
|
return API.restoreBackup({ appId, backupId, name })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBackup({ appId, backupId }) {
|
||||||
|
return API.deleteBackup({ appId, backupId })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createManualBackup(appId, name) {
|
||||||
|
return API.createManualBackup(appId, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateBackup({ appId, backupId, name }) {
|
||||||
|
return API.updateBackup({ appId, backupId, name })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createManualBackup,
|
||||||
|
searchBackups,
|
||||||
|
selectBackup,
|
||||||
|
deleteBackup,
|
||||||
|
restoreBackup,
|
||||||
|
updateBackup,
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const backups = createBackupsStore()
|
|
@ -9,3 +9,4 @@ export { templates } from "./templates"
|
||||||
export { licensing } from "./licensing"
|
export { licensing } from "./licensing"
|
||||||
export { groups } from "./groups"
|
export { groups } from "./groups"
|
||||||
export { plugins } from "./plugins"
|
export { plugins } from "./plugins"
|
||||||
|
export { backups } from "./backups"
|
||||||
|
|
|
@ -14,6 +14,7 @@ export const createLicensingStore = () => {
|
||||||
isFreePlan: true,
|
isFreePlan: true,
|
||||||
// features
|
// features
|
||||||
groupsEnabled: false,
|
groupsEnabled: false,
|
||||||
|
backupsEnabled: false,
|
||||||
// the currently used quotas from the db
|
// the currently used quotas from the db
|
||||||
quotaUsage: undefined,
|
quotaUsage: undefined,
|
||||||
// derived quota metrics for percentages used
|
// derived quota metrics for percentages used
|
||||||
|
@ -56,12 +57,17 @@ export const createLicensingStore = () => {
|
||||||
const groupsEnabled = license.features.includes(
|
const groupsEnabled = license.features.includes(
|
||||||
Constants.Features.USER_GROUPS
|
Constants.Features.USER_GROUPS
|
||||||
)
|
)
|
||||||
|
const backupsEnabled = license.features.includes(
|
||||||
|
Constants.Features.BACKUPS
|
||||||
|
)
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
license,
|
license,
|
||||||
isFreePlan,
|
isFreePlan,
|
||||||
groupsEnabled,
|
groupsEnabled,
|
||||||
|
backupsEnabled,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "2.0.30-alpha.13",
|
"version": "2.0.34-alpha.3",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -26,9 +26,9 @@
|
||||||
"outputPath": "build"
|
"outputPath": "build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "2.0.30-alpha.13",
|
"@budibase/backend-core": "2.0.34-alpha.3",
|
||||||
"@budibase/string-templates": "2.0.30-alpha.13",
|
"@budibase/string-templates": "2.0.34-alpha.3",
|
||||||
"@budibase/types": "2.0.30-alpha.13",
|
"@budibase/types": "2.0.34-alpha.3",
|
||||||
"axios": "0.21.2",
|
"axios": "0.21.2",
|
||||||
"chalk": "4.1.0",
|
"chalk": "4.1.0",
|
||||||
"cli-progress": "3.11.2",
|
"cli-progress": "3.11.2",
|
||||||
|
|
|
@ -87,7 +87,7 @@
|
||||||
"showSettingsBar": true,
|
"showSettingsBar": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 100
|
"height": 200
|
||||||
},
|
},
|
||||||
"styles": [
|
"styles": [
|
||||||
"padding",
|
"padding",
|
||||||
|
@ -3654,7 +3654,7 @@
|
||||||
"key": "filter"
|
"key": "filter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "field",
|
"type": "field/sortable",
|
||||||
"label": "Sort Column",
|
"label": "Sort Column",
|
||||||
"key": "sortColumn"
|
"key": "sortColumn"
|
||||||
},
|
},
|
||||||
|
@ -3972,6 +3972,477 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"chartblock": {
|
||||||
|
"block": true,
|
||||||
|
"name": "Chart block",
|
||||||
|
"icon": "GraphPie",
|
||||||
|
"hasChildren": false,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Chart Type",
|
||||||
|
"key": "chartType",
|
||||||
|
"required": true,
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Pie",
|
||||||
|
"value": "pie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Bar",
|
||||||
|
"value": "bar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Line",
|
||||||
|
"value": "line"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Donut",
|
||||||
|
"value": "donut"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Candlestick",
|
||||||
|
"value": "candlestick"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Area",
|
||||||
|
"value": "area"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dataSource",
|
||||||
|
"label": "Data",
|
||||||
|
"key": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Title",
|
||||||
|
"key": "chartTitle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "filter",
|
||||||
|
"label": "Filtering",
|
||||||
|
"key": "filter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Sort Column",
|
||||||
|
"key": "sortColumn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Sort Order",
|
||||||
|
"key": "sortOrder",
|
||||||
|
"options": [
|
||||||
|
"Ascending",
|
||||||
|
"Descending"
|
||||||
|
],
|
||||||
|
"defaultValue": "Ascending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Limit",
|
||||||
|
"key": "limit",
|
||||||
|
"defaultValue": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Width",
|
||||||
|
"key": "width"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Height",
|
||||||
|
"key": "height",
|
||||||
|
"defaultValue": "400"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Colors",
|
||||||
|
"key": "palette",
|
||||||
|
"defaultValue": "Palette 1",
|
||||||
|
"options": [
|
||||||
|
"Custom",
|
||||||
|
"Palette 1",
|
||||||
|
"Palette 2",
|
||||||
|
"Palette 3",
|
||||||
|
"Palette 4",
|
||||||
|
"Palette 5",
|
||||||
|
"Palette 6",
|
||||||
|
"Palette 7",
|
||||||
|
"Palette 8",
|
||||||
|
"Palette 9",
|
||||||
|
"Palette 10"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "color",
|
||||||
|
"label": "C1",
|
||||||
|
"key": "c1",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "palette",
|
||||||
|
"value": "Custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "color",
|
||||||
|
"label": "C2",
|
||||||
|
"key": "c2",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "palette",
|
||||||
|
"value": "Custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "color",
|
||||||
|
"label": "C3",
|
||||||
|
"key": "c3",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "palette",
|
||||||
|
"value": "Custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "color",
|
||||||
|
"label": "C4",
|
||||||
|
"key": "c4",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "palette",
|
||||||
|
"value": "Custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "color",
|
||||||
|
"label": "C5",
|
||||||
|
"key": "c5",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "palette",
|
||||||
|
"value": "Custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Data Labels",
|
||||||
|
"key": "dataLabels",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Legend",
|
||||||
|
"key": "legend",
|
||||||
|
"defaultValue": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Animate",
|
||||||
|
"key": "animate",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": true,
|
||||||
|
"name": "Pie Chart",
|
||||||
|
"icon": "GraphPie",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "chartType",
|
||||||
|
"value": "pie"
|
||||||
|
},
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Label Col.",
|
||||||
|
"key": "labelColumn",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Data Col.",
|
||||||
|
"key": "valueColumn",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": true,
|
||||||
|
"name": "Donut Chart",
|
||||||
|
"icon": "GraphDonut",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "chartType",
|
||||||
|
"value": "donut"
|
||||||
|
},
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Label Col.",
|
||||||
|
"key": "labelColumn",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Data Col.",
|
||||||
|
"key": "valueColumn",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": true,
|
||||||
|
"name": "Bar Chart",
|
||||||
|
"icon": "GraphBarVertical",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "chartType",
|
||||||
|
"value": "bar"
|
||||||
|
},
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Label Col.",
|
||||||
|
"key": "labelColumn",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "multifield",
|
||||||
|
"label": "Data Cols.",
|
||||||
|
"key": "valueColumns",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Format",
|
||||||
|
"key": "yAxisUnits",
|
||||||
|
"options": [
|
||||||
|
"Default",
|
||||||
|
"Thousands",
|
||||||
|
"Millions"
|
||||||
|
],
|
||||||
|
"defaultValue": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Y Axis Label",
|
||||||
|
"key": "yAxisLabel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "X Axis Label",
|
||||||
|
"key": "xAxisLabel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Stacked",
|
||||||
|
"key": "stacked",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Horizontal",
|
||||||
|
"key": "horizontal",
|
||||||
|
"defaultValue": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": true,
|
||||||
|
"name": "Line Chart",
|
||||||
|
"icon": "GraphTrend",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "chartType",
|
||||||
|
"value": "line"
|
||||||
|
},
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Label Col.",
|
||||||
|
"key": "labelColumn",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "multifield",
|
||||||
|
"label": "Data Cols.",
|
||||||
|
"key": "valueColumns",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Format",
|
||||||
|
"key": "yAxisUnits",
|
||||||
|
"options": [
|
||||||
|
"Default",
|
||||||
|
"Thousands",
|
||||||
|
"Millions"
|
||||||
|
],
|
||||||
|
"defaultValue": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Y Axis Label",
|
||||||
|
"key": "yAxisLabel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "X Axis Label",
|
||||||
|
"key": "xAxisLabel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Curve",
|
||||||
|
"key": "curve",
|
||||||
|
"options": [
|
||||||
|
"Smooth",
|
||||||
|
"Straight",
|
||||||
|
"Stepline"
|
||||||
|
],
|
||||||
|
"defaultValue": "Smooth"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": true,
|
||||||
|
"name": "Area Chart",
|
||||||
|
"icon": "GraphAreaStacked",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "chartType",
|
||||||
|
"value": "area"
|
||||||
|
},
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Label Col.",
|
||||||
|
"key": "labelColumn",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "multifield",
|
||||||
|
"label": "Data Cols.",
|
||||||
|
"key": "valueColumns",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Format",
|
||||||
|
"key": "yAxisUnits",
|
||||||
|
"options": [
|
||||||
|
"Default",
|
||||||
|
"Thousands",
|
||||||
|
"Millions"
|
||||||
|
],
|
||||||
|
"defaultValue": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Y Axis Label",
|
||||||
|
"key": "yAxisLabel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "X Axis Label",
|
||||||
|
"key": "xAxisLabel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Curve",
|
||||||
|
"key": "curve",
|
||||||
|
"options": [
|
||||||
|
"Smooth",
|
||||||
|
"Straight",
|
||||||
|
"Stepline"
|
||||||
|
],
|
||||||
|
"defaultValue": "Smooth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Stacked",
|
||||||
|
"key": "stacked",
|
||||||
|
"defaultValue": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Gradient",
|
||||||
|
"key": "gradient",
|
||||||
|
"defaultValue": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": true,
|
||||||
|
"name": "Candlestick Chart",
|
||||||
|
"icon": "GraphBarVerticalStacked",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "chartType",
|
||||||
|
"value": "candlestick"
|
||||||
|
},
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Date Col.",
|
||||||
|
"key": "dateColumn",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Open Col.",
|
||||||
|
"key": "openColumn",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Close Col.",
|
||||||
|
"key": "closeColumn",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "High Col.",
|
||||||
|
"key": "highColumn",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Low Col.",
|
||||||
|
"key": "lowColumn",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Format",
|
||||||
|
"key": "yAxisUnits",
|
||||||
|
"options": [
|
||||||
|
"Default",
|
||||||
|
"Thousands",
|
||||||
|
"Millions"
|
||||||
|
],
|
||||||
|
"defaultValue": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Y Axis Label",
|
||||||
|
"key": "yAxisLabel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "X Axis Label",
|
||||||
|
"key": "xAxisLabel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"tableblock": {
|
"tableblock": {
|
||||||
"block": true,
|
"block": true,
|
||||||
"name": "Table block",
|
"name": "Table block",
|
||||||
|
@ -4008,7 +4479,7 @@
|
||||||
"key": "filter"
|
"key": "filter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "field",
|
"type": "field/sortable",
|
||||||
"label": "Sort Column",
|
"label": "Sort Column",
|
||||||
"key": "sortColumn"
|
"key": "sortColumn"
|
||||||
},
|
},
|
||||||
|
@ -4177,7 +4648,7 @@
|
||||||
"key": "filter"
|
"key": "filter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "field",
|
"type": "field/sortable",
|
||||||
"label": "Sort Column",
|
"label": "Sort Column",
|
||||||
"key": "sortColumn"
|
"key": "sortColumn"
|
||||||
},
|
},
|
||||||
|
@ -4344,7 +4815,7 @@
|
||||||
"key": "filter"
|
"key": "filter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "field",
|
"type": "field/sortable",
|
||||||
"label": "Sort Column",
|
"label": "Sort Column",
|
||||||
"key": "sortColumn"
|
"key": "sortColumn"
|
||||||
},
|
},
|
||||||
|
@ -4566,6 +5037,45 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"grid": {
|
||||||
|
"name": "Grid (Beta)",
|
||||||
|
"icon": "ViewGrid",
|
||||||
|
"hasChildren": true,
|
||||||
|
"styles": [
|
||||||
|
"size"
|
||||||
|
],
|
||||||
|
"illegalChildren": ["section", "grid"],
|
||||||
|
"legalDirectChildren": [
|
||||||
|
"container",
|
||||||
|
"tableblock",
|
||||||
|
"cardsblock",
|
||||||
|
"repeaterblock",
|
||||||
|
"formblock"
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"width": 800,
|
||||||
|
"height": 400
|
||||||
|
},
|
||||||
|
"showEmptyState": false,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Rows",
|
||||||
|
"key": "rows",
|
||||||
|
"defaultValue": 12,
|
||||||
|
"min": 1,
|
||||||
|
"max": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Columns",
|
||||||
|
"key": "cols",
|
||||||
|
"defaultValue": 12,
|
||||||
|
"min": 1,
|
||||||
|
"max": 32
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"formblock": {
|
"formblock": {
|
||||||
"name": "Form Block",
|
"name": "Form Block",
|
||||||
"icon": "Form",
|
"icon": "Form",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "2.0.30-alpha.13",
|
"version": "2.0.34-alpha.3",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.0.30-alpha.13",
|
"@budibase/bbui": "2.0.34-alpha.3",
|
||||||
"@budibase/frontend-core": "2.0.30-alpha.13",
|
"@budibase/frontend-core": "2.0.34-alpha.3",
|
||||||
"@budibase/string-templates": "2.0.30-alpha.13",
|
"@budibase/string-templates": "2.0.34-alpha.3",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
||||||
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
||||||
import DNDHandler from "components/preview/DNDHandler.svelte"
|
import DNDHandler from "components/preview/DNDHandler.svelte"
|
||||||
|
import GridDNDHandler from "components/preview/GridDNDHandler.svelte"
|
||||||
import KeyboardManager from "components/preview/KeyboardManager.svelte"
|
import KeyboardManager from "components/preview/KeyboardManager.svelte"
|
||||||
import DevToolsHeader from "components/devtools/DevToolsHeader.svelte"
|
import DevToolsHeader from "components/devtools/DevToolsHeader.svelte"
|
||||||
import DevTools from "components/devtools/DevTools.svelte"
|
import DevTools from "components/devtools/DevTools.svelte"
|
||||||
|
@ -196,6 +197,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if $builderStore.inBuilder}
|
{#if $builderStore.inBuilder}
|
||||||
<DNDHandler />
|
<DNDHandler />
|
||||||
|
<GridDNDHandler />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</QueryParamsProvider>
|
</QueryParamsProvider>
|
||||||
|
|
|
@ -21,8 +21,8 @@
|
||||||
devToolsStore,
|
devToolsStore,
|
||||||
componentStore,
|
componentStore,
|
||||||
appStore,
|
appStore,
|
||||||
dndIsDragging,
|
|
||||||
dndComponentPath,
|
dndComponentPath,
|
||||||
|
dndIsDragging,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { getActiveConditions, reduceConditionActions } from "utils/conditions"
|
import { getActiveConditions, reduceConditionActions } from "utils/conditions"
|
||||||
|
@ -90,6 +90,10 @@
|
||||||
let settingsDefinitionMap
|
let settingsDefinitionMap
|
||||||
let missingRequiredSettings = false
|
let missingRequiredSettings = false
|
||||||
|
|
||||||
|
// Temporary styles which can be added in the app preview for things like DND.
|
||||||
|
// We clear these whenever a new instance is received.
|
||||||
|
let ephemeralStyles
|
||||||
|
|
||||||
// Set up initial state for each new component instance
|
// Set up initial state for each new component instance
|
||||||
$: initialise(instance)
|
$: initialise(instance)
|
||||||
|
|
||||||
|
@ -171,6 +175,10 @@
|
||||||
children: children.length,
|
children: children.length,
|
||||||
styles: {
|
styles: {
|
||||||
...instance._styles,
|
...instance._styles,
|
||||||
|
normal: {
|
||||||
|
...instance._styles?.normal,
|
||||||
|
...ephemeralStyles,
|
||||||
|
},
|
||||||
custom: customCSS,
|
custom: customCSS,
|
||||||
id,
|
id,
|
||||||
empty: emptyState,
|
empty: emptyState,
|
||||||
|
@ -449,6 +457,7 @@
|
||||||
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
|
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
|
||||||
getDataContext: () => get(context),
|
getDataContext: () => get(context),
|
||||||
reload: () => initialise(instance, true),
|
reload: () => initialise(instance, true),
|
||||||
|
setEphemeralStyles: styles => (ephemeralStyles = styles),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -506,8 +515,8 @@
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
.component.pad :global(> *) {
|
.component.pad :global(> *) {
|
||||||
padding: var(--spacing-l) !important;
|
padding: var(--spacing-m) !important;
|
||||||
gap: var(--spacing-l) !important;
|
gap: var(--spacing-m) !important;
|
||||||
border: 2px dashed var(--spectrum-global-color-gray-400) !important;
|
border: 2px dashed var(--spectrum-global-color-gray-400) !important;
|
||||||
border-radius: 4px !important;
|
border-radius: 4px !important;
|
||||||
transition: padding 260ms ease-out, border 260ms ease-out;
|
transition: padding 260ms ease-out, border 260ms ease-out;
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const component = getContext("component")
|
||||||
|
const { styleable, builderStore } = getContext("sdk")
|
||||||
|
|
||||||
|
export let cols = 12
|
||||||
|
export let rows = 12
|
||||||
|
|
||||||
|
// Deliberately non-reactive as we want this fixed whenever the grid renders
|
||||||
|
const defaultColSpan = Math.ceil((cols + 1) / 2)
|
||||||
|
const defaultRowSpan = Math.ceil((rows + 1) / 2)
|
||||||
|
|
||||||
|
$: coords = generateCoords(rows, cols)
|
||||||
|
|
||||||
|
const generateCoords = (rows, cols) => {
|
||||||
|
let grid = []
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
for (let col = 0; col < cols; col++) {
|
||||||
|
grid.push({ row, col })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="grid"
|
||||||
|
use:styleable={{
|
||||||
|
...$component.styles,
|
||||||
|
normal: {
|
||||||
|
...$component.styles?.normal,
|
||||||
|
"--cols": cols,
|
||||||
|
"--rows": rows,
|
||||||
|
"--default-col-span": defaultColSpan,
|
||||||
|
"--default-row-span": defaultRowSpan,
|
||||||
|
gap: "0 !important",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
data-rows={rows}
|
||||||
|
data-cols={cols}
|
||||||
|
>
|
||||||
|
{#if $builderStore.inBuilder}
|
||||||
|
<div class="underlay">
|
||||||
|
{#each coords as coord}
|
||||||
|
<div class="placeholder" />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/*
|
||||||
|
Ensure all children of containers which are top level children of
|
||||||
|
grids do not overflow
|
||||||
|
*/
|
||||||
|
:global(.grid > .component > .valid-container > .component > *) {
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure all top level children have some grid styles set */
|
||||||
|
:global(.grid > .component > *) {
|
||||||
|
overflow: hidden;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
grid-column-start: 1;
|
||||||
|
grid-column-end: var(--default-col-span);
|
||||||
|
grid-row-start: 1;
|
||||||
|
grid-row-end: var(--default-row-span);
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
position: relative;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
.grid,
|
||||||
|
.underlay {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(var(--rows), 1fr);
|
||||||
|
grid-template-columns: repeat(var(--cols), 1fr);
|
||||||
|
}
|
||||||
|
.underlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
grid-gap: 2px;
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
border: 2px solid var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
|
.underlay {
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
.placeholder {
|
||||||
|
background-color: var(--spectrum-global-color-gray-100);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,99 @@
|
||||||
|
<script>
|
||||||
|
import Block from "components/Block.svelte"
|
||||||
|
import BlockComponent from "components/BlockComponent.svelte"
|
||||||
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
|
|
||||||
|
// Datasource
|
||||||
|
export let filter
|
||||||
|
export let sortColumn
|
||||||
|
export let sortOrder
|
||||||
|
export let limit
|
||||||
|
|
||||||
|
// Block
|
||||||
|
export let chartTitle
|
||||||
|
export let chartType
|
||||||
|
export let dataSource
|
||||||
|
export let palette
|
||||||
|
export let c1, c2, c3, c4, c5
|
||||||
|
export let labelColumn
|
||||||
|
export let legend
|
||||||
|
export let animate
|
||||||
|
export let dataLabels
|
||||||
|
export let height
|
||||||
|
export let width
|
||||||
|
|
||||||
|
// Pie/Donut
|
||||||
|
export let valueColumn
|
||||||
|
|
||||||
|
// Bar
|
||||||
|
export let stacked
|
||||||
|
export let horizontal
|
||||||
|
|
||||||
|
// Bar/Line/Area
|
||||||
|
export let valueColumns
|
||||||
|
export let yAxisUnits
|
||||||
|
export let yAxisLabel
|
||||||
|
export let xAxisLabel
|
||||||
|
export let curve
|
||||||
|
|
||||||
|
// Area
|
||||||
|
export let gradient
|
||||||
|
|
||||||
|
// Candlestick
|
||||||
|
export let closeColumn
|
||||||
|
export let openColumn
|
||||||
|
export let highColumn
|
||||||
|
export let lowColumn
|
||||||
|
export let dateColumn
|
||||||
|
|
||||||
|
let dataProviderId
|
||||||
|
|
||||||
|
$: colors = c1 && c2 && c3 && c4 && c5 ? [c1, c2, c3, c4, c5] : null
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Block>
|
||||||
|
<BlockComponent
|
||||||
|
type="dataprovider"
|
||||||
|
context="provider"
|
||||||
|
bind:id={dataProviderId}
|
||||||
|
props={{
|
||||||
|
dataSource,
|
||||||
|
filter,
|
||||||
|
sortColumn,
|
||||||
|
sortOrder,
|
||||||
|
limit,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if dataProviderId && chartType}
|
||||||
|
<BlockComponent
|
||||||
|
type={chartType}
|
||||||
|
props={{
|
||||||
|
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
title: chartTitle,
|
||||||
|
labelColumn,
|
||||||
|
valueColumn,
|
||||||
|
valueColumns,
|
||||||
|
palette,
|
||||||
|
dataLabels,
|
||||||
|
legend,
|
||||||
|
animate,
|
||||||
|
...colors,
|
||||||
|
yAxisUnits,
|
||||||
|
yAxisLabel,
|
||||||
|
xAxisLabel,
|
||||||
|
stacked,
|
||||||
|
horizontal,
|
||||||
|
curve,
|
||||||
|
gradient, //issue?
|
||||||
|
closeColumn,
|
||||||
|
openColumn,
|
||||||
|
highColumn,
|
||||||
|
lowColumn,
|
||||||
|
dateColumn,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</BlockComponent>
|
||||||
|
</Block>
|
|
@ -2,3 +2,4 @@ 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 "./FormBlock.svelte"
|
export { default as formblock } from "./FormBlock.svelte"
|
||||||
|
export { default as chartblock } from "./ChartBlock.svelte"
|
||||||
|
|
|
@ -24,8 +24,11 @@
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
div :global(.apexcharts-yaxis-label),
|
div :global(.apexcharts-text.apexcharts-xaxis-title-text),
|
||||||
div :global(.apexcharts-xaxis-label) {
|
div :global(.apexcharts-text.apexcharts-yaxis-title-text),
|
||||||
|
div :global(.apexcharts-text.apexcharts-xaxis-label),
|
||||||
|
div :global(.apexcharts-text.apexcharts-yaxis-label),
|
||||||
|
div :global(.apexcharts-title-text) {
|
||||||
fill: var(--spectrum-global-color-gray-600);
|
fill: var(--spectrum-global-color-gray-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -184,6 +184,9 @@ export class ApexOptionsBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
palette(palette) {
|
palette(palette) {
|
||||||
|
if (!palette) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
return this.setOption(
|
return this.setOption(
|
||||||
["theme", "palette"],
|
["theme", "palette"],
|
||||||
palette.toLowerCase().replace(/[\W]/g, "")
|
palette.toLowerCase().replace(/[\W]/g, "")
|
||||||
|
|
|
@ -34,6 +34,7 @@ export { default as spectrumcard } from "./SpectrumCard.svelte"
|
||||||
export { default as tag } from "./Tag.svelte"
|
export { default as tag } from "./Tag.svelte"
|
||||||
export { default as markdownviewer } from "./MarkdownViewer.svelte"
|
export { default as markdownviewer } from "./MarkdownViewer.svelte"
|
||||||
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
||||||
|
export { default as grid } from "./Grid.svelte"
|
||||||
export * from "./charts"
|
export * from "./charts"
|
||||||
export * from "./forms"
|
export * from "./forms"
|
||||||
export * from "./table"
|
export * from "./table"
|
||||||
|
|
|
@ -22,6 +22,18 @@
|
||||||
$: target = $dndStore.target
|
$: target = $dndStore.target
|
||||||
$: drop = $dndStore.drop
|
$: drop = $dndStore.drop
|
||||||
|
|
||||||
|
// Local flag for whether we are awaiting an async drop event
|
||||||
|
let dropping = false
|
||||||
|
|
||||||
|
// Util to check if a DND event originates from a grid (or inside a grid).
|
||||||
|
// This is important as we do not handle grid DND in this handler.
|
||||||
|
const isGridEvent = e => {
|
||||||
|
return e.target
|
||||||
|
?.closest?.(".component")
|
||||||
|
?.parentNode?.closest?.(".component")
|
||||||
|
?.childNodes[0]?.classList.contains("grid")
|
||||||
|
}
|
||||||
|
|
||||||
// Util to get the inner DOM node by a component ID
|
// Util to get the inner DOM node by a component ID
|
||||||
const getDOMNode = id => {
|
const getDOMNode = id => {
|
||||||
const component = document.getElementsByClassName(id)[0]
|
const component = document.getElementsByClassName(id)[0]
|
||||||
|
@ -41,6 +53,10 @@
|
||||||
|
|
||||||
// Callback when drag stops (whether dropped or not)
|
// Callback when drag stops (whether dropped or not)
|
||||||
const stopDragging = () => {
|
const stopDragging = () => {
|
||||||
|
if (dropping) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Reset listener
|
// Reset listener
|
||||||
if (source?.id) {
|
if (source?.id) {
|
||||||
const component = document.getElementsByClassName(source?.id)[0]
|
const component = document.getElementsByClassName(source?.id)[0]
|
||||||
|
@ -55,6 +71,9 @@
|
||||||
|
|
||||||
// Callback when initially starting a drag on a draggable component
|
// Callback when initially starting a drag on a draggable component
|
||||||
const onDragStart = e => {
|
const onDragStart = e => {
|
||||||
|
if (isGridEvent(e)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const component = e.target.closest(".component")
|
const component = e.target.closest(".component")
|
||||||
if (!component?.classList.contains("draggable")) {
|
if (!component?.classList.contains("draggable")) {
|
||||||
return
|
return
|
||||||
|
@ -99,9 +118,9 @@
|
||||||
|
|
||||||
// Core logic for handling drop events and determining where to render the
|
// Core logic for handling drop events and determining where to render the
|
||||||
// drop target placeholder
|
// drop target placeholder
|
||||||
const processEvent = (mouseX, mouseY) => {
|
const processEvent = Utils.throttle((mouseX, mouseY) => {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return null
|
return
|
||||||
}
|
}
|
||||||
let { id, parent, node, acceptsChildren, empty } = target
|
let { id, parent, node, acceptsChildren, empty } = target
|
||||||
|
|
||||||
|
@ -201,15 +220,15 @@
|
||||||
parent: id,
|
parent: id,
|
||||||
index: idx,
|
index: idx,
|
||||||
})
|
})
|
||||||
}
|
}, ThrottleRate)
|
||||||
const throttledProcessEvent = Utils.throttle(processEvent, ThrottleRate)
|
|
||||||
|
|
||||||
const handleEvent = e => {
|
const handleEvent = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
throttledProcessEvent(e.clientX, e.clientY)
|
e.stopPropagation()
|
||||||
|
processEvent(e.clientX, e.clientY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback when on top of a component
|
// Callback when on top of a component.
|
||||||
const onDragOver = e => {
|
const onDragOver = e => {
|
||||||
if (!source || !target) {
|
if (!source || !target) {
|
||||||
return
|
return
|
||||||
|
@ -241,18 +260,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback when dropping a drag on top of some component
|
// Callback when dropping a drag on top of some component
|
||||||
const onDrop = () => {
|
const onDrop = async () => {
|
||||||
if (!source || !drop?.parent || drop?.index == null) {
|
if (!source || !drop?.parent || drop?.index == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're adding a new component rather than moving one
|
// Check if we're adding a new component rather than moving one
|
||||||
if (source.newComponentType) {
|
if (source.newComponentType) {
|
||||||
builderStore.actions.dropNewComponent(
|
dropping = true
|
||||||
|
await builderStore.actions.dropNewComponent(
|
||||||
source.newComponentType,
|
source.newComponentType,
|
||||||
drop.parent,
|
drop.parent,
|
||||||
drop.index
|
drop.index
|
||||||
)
|
)
|
||||||
|
dropping = false
|
||||||
|
stopDragging()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,11 +311,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (legacyDropTarget && legacyDropMode) {
|
if (legacyDropTarget && legacyDropMode) {
|
||||||
builderStore.actions.moveComponent(
|
dropping = true
|
||||||
|
await builderStore.actions.moveComponent(
|
||||||
source.id,
|
source.id,
|
||||||
legacyDropTarget,
|
legacyDropTarget,
|
||||||
legacyDropMode
|
legacyDropMode
|
||||||
)
|
)
|
||||||
|
dropping = false
|
||||||
|
stopDragging()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
<script>
|
|
||||||
import { dndBounds } from "stores"
|
|
||||||
import { DNDPlaceholderID } from "constants"
|
|
||||||
|
|
||||||
$: style = getStyle($dndBounds)
|
|
||||||
|
|
||||||
const getStyle = bounds => {
|
|
||||||
if (!bounds) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return `--height: ${bounds.height}px; --width: ${bounds.width}px;`
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if style}
|
|
||||||
<div class="wrapper">
|
|
||||||
<div class="placeholder" id={DNDPlaceholderID} {style} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.wrapper {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.placeholder {
|
|
||||||
display: block;
|
|
||||||
height: var(--height);
|
|
||||||
width: var(--width);
|
|
||||||
max-height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -6,7 +6,8 @@
|
||||||
let left, top, height, width
|
let left, top, height, width
|
||||||
|
|
||||||
const updatePosition = () => {
|
const updatePosition = () => {
|
||||||
const node = document.getElementById(DNDPlaceholderID)
|
const node =
|
||||||
|
document.getElementsByClassName(DNDPlaceholderID)[0]?.childNodes[0]
|
||||||
if (!node) {
|
if (!node) {
|
||||||
height = 0
|
height = 0
|
||||||
width = 0
|
width = 0
|
||||||
|
|
|
@ -0,0 +1,227 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
import { builderStore, componentStore } from "stores"
|
||||||
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
let dragInfo
|
||||||
|
let gridStyles
|
||||||
|
let id
|
||||||
|
|
||||||
|
// Some memoisation of primitive types for performance
|
||||||
|
$: jsonStyles = JSON.stringify(gridStyles)
|
||||||
|
$: id = dragInfo?.id || id
|
||||||
|
|
||||||
|
// Set ephemeral grid styles on the dragged component
|
||||||
|
$: componentStore.actions.getComponentInstance(id)?.setEphemeralStyles({
|
||||||
|
...gridStyles,
|
||||||
|
...(gridStyles ? { "z-index": 999 } : null),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Util to check if a DND event originates from a grid (or inside a grid).
|
||||||
|
// This is important as we do not handle grid DND in this handler.
|
||||||
|
const isGridEvent = e => {
|
||||||
|
return (
|
||||||
|
e.target
|
||||||
|
.closest?.(".component")
|
||||||
|
?.parentNode.closest(".component")
|
||||||
|
?.childNodes[0].classList.contains("grid") ||
|
||||||
|
e.target.classList.contains("anchor")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Util to get the inner DOM node by a component ID
|
||||||
|
const getDOMNode = id => {
|
||||||
|
const component = document.getElementsByClassName(id)[0]
|
||||||
|
return [...component.children][0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const processEvent = Utils.throttle((mouseX, mouseY) => {
|
||||||
|
if (!dragInfo?.grid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mode, side, gridId, grid } = dragInfo
|
||||||
|
const {
|
||||||
|
startX,
|
||||||
|
startY,
|
||||||
|
rowStart,
|
||||||
|
rowEnd,
|
||||||
|
colStart,
|
||||||
|
colEnd,
|
||||||
|
rowDeltaMin,
|
||||||
|
rowDeltaMax,
|
||||||
|
colDeltaMin,
|
||||||
|
colDeltaMax,
|
||||||
|
} = grid
|
||||||
|
|
||||||
|
const domGrid = getDOMNode(gridId)
|
||||||
|
const cols = parseInt(domGrid.dataset.cols)
|
||||||
|
const rows = parseInt(domGrid.dataset.rows)
|
||||||
|
const { width, height } = domGrid.getBoundingClientRect()
|
||||||
|
|
||||||
|
const colWidth = width / cols
|
||||||
|
const diffX = mouseX - startX
|
||||||
|
let deltaX = Math.round(diffX / colWidth)
|
||||||
|
const rowHeight = height / rows
|
||||||
|
const diffY = mouseY - startY
|
||||||
|
let deltaY = Math.round(diffY / rowHeight)
|
||||||
|
|
||||||
|
if (mode === "move") {
|
||||||
|
deltaY = Math.min(Math.max(deltaY, rowDeltaMin), rowDeltaMax)
|
||||||
|
deltaX = Math.min(Math.max(deltaX, colDeltaMin), colDeltaMax)
|
||||||
|
const newStyles = {
|
||||||
|
"grid-row-start": rowStart + deltaY,
|
||||||
|
"grid-row-end": rowEnd + deltaY,
|
||||||
|
"grid-column-start": colStart + deltaX,
|
||||||
|
"grid-column-end": colEnd + deltaX,
|
||||||
|
}
|
||||||
|
if (JSON.stringify(newStyles) !== jsonStyles) {
|
||||||
|
gridStyles = newStyles
|
||||||
|
}
|
||||||
|
} else if (mode === "resize") {
|
||||||
|
let newStyles = {}
|
||||||
|
if (side === "right") {
|
||||||
|
newStyles["grid-column-end"] = Math.max(colEnd + deltaX, colStart + 1)
|
||||||
|
} else if (side === "left") {
|
||||||
|
newStyles["grid-column-start"] = Math.min(colStart + deltaX, colEnd - 1)
|
||||||
|
} else if (side === "top") {
|
||||||
|
newStyles["grid-row-start"] = Math.min(rowStart + deltaY, rowEnd - 1)
|
||||||
|
} else if (side === "bottom") {
|
||||||
|
newStyles["grid-row-end"] = Math.max(rowEnd + deltaY, rowStart + 1)
|
||||||
|
} else if (side === "bottom-right") {
|
||||||
|
newStyles["grid-column-end"] = Math.max(colEnd + deltaX, colStart + 1)
|
||||||
|
newStyles["grid-row-end"] = Math.max(rowEnd + deltaY, rowStart + 1)
|
||||||
|
} else if (side === "bottom-left") {
|
||||||
|
newStyles["grid-column-start"] = Math.min(colStart + deltaX, colEnd - 1)
|
||||||
|
newStyles["grid-row-end"] = Math.max(rowEnd + deltaY, rowStart + 1)
|
||||||
|
} else if (side === "top-right") {
|
||||||
|
newStyles["grid-column-end"] = Math.max(colEnd + deltaX, colStart + 1)
|
||||||
|
newStyles["grid-row-start"] = Math.min(rowStart + deltaY, rowEnd - 1)
|
||||||
|
} else if (side === "top-left") {
|
||||||
|
newStyles["grid-column-start"] = Math.min(colStart + deltaX, colEnd - 1)
|
||||||
|
newStyles["grid-row-start"] = Math.min(rowStart + deltaY, rowEnd - 1)
|
||||||
|
}
|
||||||
|
if (JSON.stringify(newStyles) !== jsonStyles) {
|
||||||
|
gridStyles = newStyles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
const handleEvent = e => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
processEvent(e.clientX, e.clientY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when initially starting a drag on a draggable component
|
||||||
|
const onDragStart = e => {
|
||||||
|
if (!isGridEvent(e)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide drag ghost image
|
||||||
|
e.dataTransfer.setDragImage(new Image(), 0, 0)
|
||||||
|
|
||||||
|
// Extract state
|
||||||
|
let mode, id, side
|
||||||
|
if (e.target.classList.contains("anchor")) {
|
||||||
|
// Handle resize
|
||||||
|
mode = "resize"
|
||||||
|
id = e.target.dataset.id
|
||||||
|
side = e.target.dataset.side
|
||||||
|
} else {
|
||||||
|
// Handle move
|
||||||
|
mode = "move"
|
||||||
|
const component = e.target.closest(".component")
|
||||||
|
id = component.dataset.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find grid parent
|
||||||
|
const domComponent = getDOMNode(id)
|
||||||
|
const gridId = domComponent?.closest(".grid")?.parentNode.dataset.id
|
||||||
|
if (!gridId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
dragInfo = {
|
||||||
|
domTarget: e.target,
|
||||||
|
id,
|
||||||
|
gridId,
|
||||||
|
mode,
|
||||||
|
side,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event handler to clear all drag state when dragging ends
|
||||||
|
dragInfo.domTarget.addEventListener("dragend", stopDragging)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when entering a potential drop target
|
||||||
|
const onDragEnter = e => {
|
||||||
|
// Skip if we aren't validly dragging currently
|
||||||
|
if (!dragInfo || dragInfo.grid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const domGrid = getDOMNode(dragInfo.gridId)
|
||||||
|
const gridCols = parseInt(domGrid.dataset.cols)
|
||||||
|
const gridRows = parseInt(domGrid.dataset.rows)
|
||||||
|
const domNode = getDOMNode(dragInfo.id)
|
||||||
|
const styles = window.getComputedStyle(domNode)
|
||||||
|
if (domGrid) {
|
||||||
|
const minMax = (value, min, max) => Math.min(max, Math.max(min, value))
|
||||||
|
const getStyle = x => parseInt(styles?.[x] || "0")
|
||||||
|
const getColStyle = x => minMax(getStyle(x), 1, gridCols + 1)
|
||||||
|
const getRowStyle = x => minMax(getStyle(x), 1, gridRows + 1)
|
||||||
|
dragInfo.grid = {
|
||||||
|
startX: e.clientX,
|
||||||
|
startY: e.clientY,
|
||||||
|
rowStart: getRowStyle("grid-row-start"),
|
||||||
|
rowEnd: getRowStyle("grid-row-end"),
|
||||||
|
colStart: getColStyle("grid-column-start"),
|
||||||
|
colEnd: getColStyle("grid-column-end"),
|
||||||
|
rowDeltaMin: 1 - getRowStyle("grid-row-start"),
|
||||||
|
rowDeltaMax: gridRows + 1 - getRowStyle("grid-row-end"),
|
||||||
|
colDeltaMin: 1 - getColStyle("grid-column-start"),
|
||||||
|
colDeltaMax: gridCols + 1 - getColStyle("grid-column-end"),
|
||||||
|
}
|
||||||
|
handleEvent(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragOver = e => {
|
||||||
|
if (!dragInfo?.grid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleEvent(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when drag stops (whether dropped or not)
|
||||||
|
const stopDragging = async () => {
|
||||||
|
// Save changes
|
||||||
|
if (gridStyles) {
|
||||||
|
await builderStore.actions.updateStyles(gridStyles, dragInfo.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset listener
|
||||||
|
if (dragInfo?.domTarget) {
|
||||||
|
dragInfo.domTarget.removeEventListener("dragend", stopDragging)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
dragInfo = null
|
||||||
|
gridStyles = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener("dragstart", onDragStart, false)
|
||||||
|
document.addEventListener("dragenter", onDragEnter, false)
|
||||||
|
document.addEventListener("dragover", onDragOver, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
document.removeEventListener("dragstart", onDragStart, false)
|
||||||
|
document.removeEventListener("dragenter", onDragEnter, false)
|
||||||
|
document.removeEventListener("dragover", onDragOver, false)
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -4,11 +4,25 @@
|
||||||
import { builderStore, dndIsDragging } from "stores"
|
import { builderStore, dndIsDragging } from "stores"
|
||||||
|
|
||||||
let componentId
|
let componentId
|
||||||
|
|
||||||
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
||||||
|
|
||||||
const onMouseOver = e => {
|
const onMouseOver = e => {
|
||||||
const element = e.target.closest(".interactive.component")
|
// Ignore if dragging
|
||||||
const newId = element?.dataset?.id
|
if (e.buttons > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let newId
|
||||||
|
if (e.target.classList.contains("anchor")) {
|
||||||
|
// Handle resize anchors
|
||||||
|
newId = e.target.dataset.id
|
||||||
|
} else {
|
||||||
|
// Handle normal components
|
||||||
|
const element = e.target.closest(".interactive.component")
|
||||||
|
newId = element?.dataset?.id
|
||||||
|
}
|
||||||
|
|
||||||
if (newId !== componentId) {
|
if (newId !== componentId) {
|
||||||
componentId = newId
|
componentId = newId
|
||||||
}
|
}
|
||||||
|
@ -34,4 +48,5 @@
|
||||||
color="var(--spectrum-global-color-static-blue-200)"
|
color="var(--spectrum-global-color-static-blue-200)"
|
||||||
transition
|
transition
|
||||||
{zIndex}
|
{zIndex}
|
||||||
|
allowResizeAnchors
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,9 +10,22 @@
|
||||||
export let icon
|
export let icon
|
||||||
export let color
|
export let color
|
||||||
export let zIndex
|
export let zIndex
|
||||||
|
export let componentId
|
||||||
export let transition = false
|
export let transition = false
|
||||||
export let line = false
|
export let line = false
|
||||||
export let alignRight = false
|
export let alignRight = false
|
||||||
|
export let showResizeAnchors = false
|
||||||
|
|
||||||
|
const AnchorSides = [
|
||||||
|
"right",
|
||||||
|
"left",
|
||||||
|
"top",
|
||||||
|
"bottom",
|
||||||
|
"bottom-right",
|
||||||
|
"bottom-left",
|
||||||
|
"top-right",
|
||||||
|
"top-left",
|
||||||
|
]
|
||||||
|
|
||||||
$: flipped = top < 24
|
$: flipped = top < 24
|
||||||
</script>
|
</script>
|
||||||
|
@ -40,6 +53,18 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if showResizeAnchors}
|
||||||
|
{#each AnchorSides as side}
|
||||||
|
<div
|
||||||
|
draggable="true"
|
||||||
|
class="anchor {side}"
|
||||||
|
data-side={side}
|
||||||
|
data-id={componentId}
|
||||||
|
>
|
||||||
|
<div class="anchor-inner" />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -105,4 +130,64 @@
|
||||||
/* Icon styles */
|
/* Icon styles */
|
||||||
.label :global(.spectrum-Icon + .text) {
|
.label :global(.spectrum-Icon + .text) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Anchor */
|
||||||
|
.anchor {
|
||||||
|
--size: 24px;
|
||||||
|
position: absolute;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
pointer-events: all;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.anchor-inner {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: white;
|
||||||
|
border: 2px solid var(--color);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.anchor.right {
|
||||||
|
right: calc(var(--size) / -2 - 1px);
|
||||||
|
top: calc(50% - var(--size) / 2);
|
||||||
|
cursor: e-resize;
|
||||||
|
}
|
||||||
|
.anchor.left {
|
||||||
|
left: calc(var(--size) / -2 - 1px);
|
||||||
|
top: calc(50% - var(--size) / 2);
|
||||||
|
cursor: w-resize;
|
||||||
|
}
|
||||||
|
.anchor.bottom {
|
||||||
|
left: calc(50% - var(--size) / 2 + 1px);
|
||||||
|
bottom: calc(var(--size) / -2 - 1px);
|
||||||
|
cursor: s-resize;
|
||||||
|
}
|
||||||
|
.anchor.top {
|
||||||
|
left: calc(50% - var(--size) / 2 + 1px);
|
||||||
|
top: calc(var(--size) / -2 - 1px);
|
||||||
|
cursor: n-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anchor.bottom-right {
|
||||||
|
right: calc(var(--size) / -2 - 1px);
|
||||||
|
bottom: calc(var(--size) / -2 - 1px);
|
||||||
|
cursor: se-resize;
|
||||||
|
}
|
||||||
|
.anchor.bottom-left {
|
||||||
|
left: calc(var(--size) / -2 - 1px);
|
||||||
|
bottom: calc(var(--size) / -2 - 1px);
|
||||||
|
cursor: sw-resize;
|
||||||
|
}
|
||||||
|
.anchor.top-right {
|
||||||
|
right: calc(var(--size) / -2 - 1px);
|
||||||
|
top: calc(var(--size) / -2 - 1px);
|
||||||
|
cursor: ne-resize;
|
||||||
|
}
|
||||||
|
.anchor.top-left {
|
||||||
|
left: calc(var(--size) / -2 - 1px);
|
||||||
|
top: calc(var(--size) / -2 - 1px);
|
||||||
|
cursor: nw-resize;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -9,11 +9,13 @@
|
||||||
export let transition
|
export let transition
|
||||||
export let zIndex
|
export let zIndex
|
||||||
export let prefix = null
|
export let prefix = null
|
||||||
|
export let allowResizeAnchors = false
|
||||||
|
|
||||||
let indicators = []
|
let indicators = []
|
||||||
let interval
|
let interval
|
||||||
let text
|
let text
|
||||||
let icon
|
let icon
|
||||||
|
let insideGrid = false
|
||||||
|
|
||||||
$: visibleIndicators = indicators.filter(x => x.visible)
|
$: visibleIndicators = indicators.filter(x => x.visible)
|
||||||
$: offset = $builderStore.inBuilder ? 0 : 2
|
$: offset = $builderStore.inBuilder ? 0 : 2
|
||||||
|
@ -23,6 +25,20 @@
|
||||||
let callbackCount = 0
|
let callbackCount = 0
|
||||||
let nextIndicators = []
|
let nextIndicators = []
|
||||||
|
|
||||||
|
const checkInsideGrid = id => {
|
||||||
|
const component = document.getElementsByClassName(id)[0]
|
||||||
|
const domNode = component?.children[0]
|
||||||
|
|
||||||
|
// Ignore grid itself
|
||||||
|
if (domNode?.classList.contains("grid")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return component?.parentNode
|
||||||
|
?.closest?.(".component")
|
||||||
|
?.childNodes[0]?.classList.contains("grid")
|
||||||
|
}
|
||||||
|
|
||||||
const createIntersectionCallback = idx => entries => {
|
const createIntersectionCallback = idx => entries => {
|
||||||
if (callbackCount >= observers.length) {
|
if (callbackCount >= observers.length) {
|
||||||
return
|
return
|
||||||
|
@ -52,6 +68,11 @@
|
||||||
observers = []
|
observers = []
|
||||||
nextIndicators = []
|
nextIndicators = []
|
||||||
|
|
||||||
|
// Check if we're inside a grid
|
||||||
|
if (allowResizeAnchors) {
|
||||||
|
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) {
|
||||||
|
@ -127,6 +148,8 @@
|
||||||
height={indicator.height}
|
height={indicator.height}
|
||||||
text={idx === 0 ? text : null}
|
text={idx === 0 ? text : null}
|
||||||
icon={idx === 0 ? icon : null}
|
icon={idx === 0 ? icon : null}
|
||||||
|
showResizeAnchors={allowResizeAnchors && insideGrid}
|
||||||
|
{componentId}
|
||||||
{transition}
|
{transition}
|
||||||
{zIndex}
|
{zIndex}
|
||||||
{color}
|
{color}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { builderStore, dndIsDragging } from "stores"
|
import { builderStore } from "stores"
|
||||||
import IndicatorSet from "./IndicatorSet.svelte"
|
import IndicatorSet from "./IndicatorSet.svelte"
|
||||||
|
|
||||||
$: color = $builderStore.editMode
|
$: color = $builderStore.editMode
|
||||||
|
@ -8,8 +8,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IndicatorSet
|
<IndicatorSet
|
||||||
componentId={$dndIsDragging ? null : $builderStore.selectedComponentId}
|
componentId={$builderStore.selectedComponentId}
|
||||||
{color}
|
{color}
|
||||||
zIndex="910"
|
zIndex="910"
|
||||||
transition
|
transition
|
||||||
|
allowResizeAnchors
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -17,6 +17,11 @@
|
||||||
|
|
||||||
$: definition = $componentStore.selectedComponentDefinition
|
$: definition = $componentStore.selectedComponentDefinition
|
||||||
$: showBar = definition?.showSettingsBar && !$dndIsDragging
|
$: showBar = definition?.showSettingsBar && !$dndIsDragging
|
||||||
|
$: {
|
||||||
|
if (!showBar) {
|
||||||
|
measured = false
|
||||||
|
}
|
||||||
|
}
|
||||||
$: settings = getBarSettings(definition)
|
$: settings = getBarSettings(definition)
|
||||||
|
|
||||||
const getBarSettings = definition => {
|
const getBarSettings = definition => {
|
||||||
|
|
|
@ -32,5 +32,4 @@ export const ActionTypes = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DNDPlaceholderID = "dnd-placeholder"
|
export const DNDPlaceholderID = "dnd-placeholder"
|
||||||
export const DNDPlaceholderType = "dnd-placeholder"
|
|
||||||
export const ScreenslotType = "screenslot"
|
export const ScreenslotType = "screenslot"
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
componentStore,
|
componentStore,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
dndStore,
|
dndStore,
|
||||||
|
eventStore,
|
||||||
} from "./stores"
|
} from "./stores"
|
||||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
|
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
@ -46,7 +47,9 @@ const loadBudibase = async () => {
|
||||||
appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])
|
appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])
|
||||||
|
|
||||||
// Fetch environment info
|
// Fetch environment info
|
||||||
await environmentStore.actions.fetchEnvironment()
|
if (!get(environmentStore)?.loaded) {
|
||||||
|
await environmentStore.actions.fetchEnvironment()
|
||||||
|
}
|
||||||
|
|
||||||
// Enable dev tools or not. We need to be using a dev app and not inside
|
// Enable dev tools or not. We need to be using a dev app and not inside
|
||||||
// the builder preview to enable them.
|
// the builder preview to enable them.
|
||||||
|
@ -54,15 +57,17 @@ const loadBudibase = async () => {
|
||||||
devToolsStore.actions.setEnabled(enableDevTools)
|
devToolsStore.actions.setEnabled(enableDevTools)
|
||||||
|
|
||||||
// Register handler for runtime events from the builder
|
// Register handler for runtime events from the builder
|
||||||
window.handleBuilderRuntimeEvent = (name, payload) => {
|
window.handleBuilderRuntimeEvent = (type, data) => {
|
||||||
if (!window["##BUDIBASE_IN_BUILDER##"]) {
|
if (!window["##BUDIBASE_IN_BUILDER##"]) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (name === "eject-block") {
|
if (type === "event-completed") {
|
||||||
const block = blockStore.actions.getBlock(payload)
|
eventStore.actions.resolveEvent(data)
|
||||||
|
} else if (type === "eject-block") {
|
||||||
|
const block = blockStore.actions.getBlock(data)
|
||||||
block?.eject()
|
block?.eject()
|
||||||
} else if (name === "dragging-new-component") {
|
} else if (type === "dragging-new-component") {
|
||||||
const { dragging, component } = payload
|
const { dragging, component } = data
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
const definition =
|
const definition =
|
||||||
componentStore.actions.getComponentDefinition(component)
|
componentStore.actions.getComponentDefinition(component)
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { devToolsStore } from "./devTools.js"
|
import { devToolsStore } from "./devTools.js"
|
||||||
|
import { eventStore } from "./events.js"
|
||||||
const dispatchEvent = (type, data = {}) => {
|
|
||||||
window.parent.postMessage({ type, data })
|
|
||||||
}
|
|
||||||
|
|
||||||
const createBuilderStore = () => {
|
const createBuilderStore = () => {
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
@ -19,6 +16,7 @@ const createBuilderStore = () => {
|
||||||
navigation: null,
|
navigation: null,
|
||||||
hiddenComponentIds: [],
|
hiddenComponentIds: [],
|
||||||
usedPlugins: null,
|
usedPlugins: null,
|
||||||
|
eventResolvers: {},
|
||||||
|
|
||||||
// Legacy - allow the builder to specify a layout
|
// Legacy - allow the builder to specify a layout
|
||||||
layout: null,
|
layout: null,
|
||||||
|
@ -35,22 +33,25 @@ const createBuilderStore = () => {
|
||||||
selectedComponentId: id,
|
selectedComponentId: id,
|
||||||
}))
|
}))
|
||||||
devToolsStore.actions.setAllowSelection(false)
|
devToolsStore.actions.setAllowSelection(false)
|
||||||
dispatchEvent("select-component", { id })
|
eventStore.actions.dispatchEvent("select-component", { id })
|
||||||
},
|
},
|
||||||
updateProp: (prop, value) => {
|
updateProp: (prop, value) => {
|
||||||
dispatchEvent("update-prop", { prop, value })
|
eventStore.actions.dispatchEvent("update-prop", { prop, value })
|
||||||
|
},
|
||||||
|
updateStyles: async (styles, id) => {
|
||||||
|
await eventStore.actions.dispatchEvent("update-styles", { styles, id })
|
||||||
},
|
},
|
||||||
keyDown: (key, ctrlKey) => {
|
keyDown: (key, ctrlKey) => {
|
||||||
dispatchEvent("key-down", { key, ctrlKey })
|
eventStore.actions.dispatchEvent("key-down", { key, ctrlKey })
|
||||||
},
|
},
|
||||||
duplicateComponent: id => {
|
duplicateComponent: id => {
|
||||||
dispatchEvent("duplicate-component", { id })
|
eventStore.actions.dispatchEvent("duplicate-component", { id })
|
||||||
},
|
},
|
||||||
deleteComponent: id => {
|
deleteComponent: id => {
|
||||||
dispatchEvent("delete-component", { id })
|
eventStore.actions.dispatchEvent("delete-component", { id })
|
||||||
},
|
},
|
||||||
notifyLoaded: () => {
|
notifyLoaded: () => {
|
||||||
dispatchEvent("preview-loaded")
|
eventStore.actions.dispatchEvent("preview-loaded")
|
||||||
},
|
},
|
||||||
analyticsPing: async () => {
|
analyticsPing: async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -59,15 +60,15 @@ const createBuilderStore = () => {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
moveComponent: (componentId, destinationComponentId, mode) => {
|
moveComponent: async (componentId, destinationComponentId, mode) => {
|
||||||
dispatchEvent("move-component", {
|
await eventStore.actions.dispatchEvent("move-component", {
|
||||||
componentId,
|
componentId,
|
||||||
destinationComponentId,
|
destinationComponentId,
|
||||||
mode,
|
mode,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
dropNewComponent: (component, parent, index) => {
|
dropNewComponent: (component, parent, index) => {
|
||||||
dispatchEvent("drop-new-component", {
|
eventStore.actions.dispatchEvent("drop-new-component", {
|
||||||
component,
|
component,
|
||||||
parent,
|
parent,
|
||||||
index,
|
index,
|
||||||
|
@ -80,16 +81,16 @@ const createBuilderStore = () => {
|
||||||
store.update(state => ({ ...state, editMode: enabled }))
|
store.update(state => ({ ...state, editMode: enabled }))
|
||||||
},
|
},
|
||||||
clickNav: () => {
|
clickNav: () => {
|
||||||
dispatchEvent("click-nav")
|
eventStore.actions.dispatchEvent("click-nav")
|
||||||
},
|
},
|
||||||
requestAddComponent: () => {
|
requestAddComponent: () => {
|
||||||
dispatchEvent("request-add-component")
|
eventStore.actions.dispatchEvent("request-add-component")
|
||||||
},
|
},
|
||||||
highlightSetting: setting => {
|
highlightSetting: setting => {
|
||||||
dispatchEvent("highlight-setting", { setting })
|
eventStore.actions.dispatchEvent("highlight-setting", { setting })
|
||||||
},
|
},
|
||||||
ejectBlock: (id, definition) => {
|
ejectBlock: (id, definition) => {
|
||||||
dispatchEvent("eject-block", { id, definition })
|
eventStore.actions.dispatchEvent("eject-block", { id, definition })
|
||||||
},
|
},
|
||||||
updateUsedPlugin: (name, hash) => {
|
updateUsedPlugin: (name, hash) => {
|
||||||
// Check if we used this plugin
|
// Check if we used this plugin
|
||||||
|
@ -106,7 +107,7 @@ const createBuilderStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the builder so we can reload component definitions
|
// Notify the builder so we can reload component definitions
|
||||||
dispatchEvent("reload-plugin")
|
eventStore.actions.dispatchEvent("reload-plugin")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -5,9 +5,8 @@ import { devToolsStore } from "./devTools"
|
||||||
import { screenStore } from "./screens"
|
import { screenStore } from "./screens"
|
||||||
import { builderStore } from "./builder"
|
import { builderStore } from "./builder"
|
||||||
import Router from "../components/Router.svelte"
|
import Router from "../components/Router.svelte"
|
||||||
import DNDPlaceholder from "../components/preview/DNDPlaceholder.svelte"
|
|
||||||
import * as AppComponents from "../components/app/index.js"
|
import * as AppComponents from "../components/app/index.js"
|
||||||
import { DNDPlaceholderType, ScreenslotType } from "../constants.js"
|
import { ScreenslotType } from "../constants.js"
|
||||||
|
|
||||||
const budibasePrefix = "@budibase/standard-components/"
|
const budibasePrefix = "@budibase/standard-components/"
|
||||||
|
|
||||||
|
@ -49,6 +48,9 @@ const createComponentStore = () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const registerInstance = (id, instance) => {
|
const registerInstance = (id, instance) => {
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
// If this is a custom component, flag it so we can reload this component
|
// If this is a custom component, flag it so we can reload this component
|
||||||
// later if required
|
// later if required
|
||||||
|
@ -68,6 +70,9 @@ const createComponentStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const unregisterInstance = id => {
|
const unregisterInstance = id => {
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
// Remove from custom component map if required
|
// Remove from custom component map if required
|
||||||
const component = state.mountedComponents[id]?.instance?.component
|
const component = state.mountedComponents[id]?.instance?.component
|
||||||
|
@ -103,8 +108,6 @@ const createComponentStore = () => {
|
||||||
// Screenslot is an edge case
|
// Screenslot is an edge case
|
||||||
if (type === ScreenslotType) {
|
if (type === ScreenslotType) {
|
||||||
type = `${budibasePrefix}${type}`
|
type = `${budibasePrefix}${type}`
|
||||||
} else if (type === DNDPlaceholderType) {
|
|
||||||
return {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle built-in components
|
// Handle built-in components
|
||||||
|
@ -124,8 +127,6 @@ const createComponentStore = () => {
|
||||||
}
|
}
|
||||||
if (type === ScreenslotType) {
|
if (type === ScreenslotType) {
|
||||||
return Router
|
return Router
|
||||||
} else if (type === DNDPlaceholderType) {
|
|
||||||
return DNDPlaceholder
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle budibase components
|
// Handle budibase components
|
||||||
|
@ -140,6 +141,13 @@ const createComponentStore = () => {
|
||||||
return customComponentManifest?.[type]?.Component
|
return customComponentManifest?.[type]?.Component
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getComponentInstance = id => {
|
||||||
|
if (!id) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return get(store).mountedComponents[id]
|
||||||
|
}
|
||||||
|
|
||||||
const registerCustomComponent = ({ Component, schema, version }) => {
|
const registerCustomComponent = ({ Component, schema, version }) => {
|
||||||
if (!Component || !schema?.schema?.name || !version) {
|
if (!Component || !schema?.schema?.name || !version) {
|
||||||
return
|
return
|
||||||
|
@ -171,6 +179,7 @@ const createComponentStore = () => {
|
||||||
getComponentById,
|
getComponentById,
|
||||||
getComponentDefinition,
|
getComponentDefinition,
|
||||||
getComponentConstructor,
|
getComponentConstructor,
|
||||||
|
getComponentInstance,
|
||||||
registerCustomComponent,
|
registerCustomComponent,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { writable, derived } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
import { computed } from "../utils/computed.js"
|
||||||
|
|
||||||
const createDndStore = () => {
|
const createDndStore = () => {
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
@ -77,14 +78,11 @@ export const dndStore = createDndStore()
|
||||||
// performance by deriving any state that needs to be externally observed.
|
// performance by deriving any state that needs to be externally observed.
|
||||||
// By doing this and using primitives, we can avoid invalidating other stores
|
// By doing this and using primitives, we can avoid invalidating other stores
|
||||||
// or components which depend on DND state unless values actually change.
|
// or components which depend on DND state unless values actually change.
|
||||||
export const dndIsDragging = derived(dndStore, $dndStore => !!$dndStore.source)
|
export const dndParent = computed(dndStore, x => x.drop?.parent)
|
||||||
export const dndParent = derived(dndStore, $dndStore => $dndStore.drop?.parent)
|
export const dndIndex = computed(dndStore, x => x.drop?.index)
|
||||||
export const dndIndex = derived(dndStore, $dndStore => $dndStore.drop?.index)
|
export const dndBounds = computed(dndStore, x => x.source?.bounds)
|
||||||
export const dndBounds = derived(
|
export const dndIsDragging = computed(dndStore, x => !!x.source)
|
||||||
|
export const dndIsNewComponent = computed(
|
||||||
dndStore,
|
dndStore,
|
||||||
$dndStore => $dndStore.source?.bounds
|
x => x.source?.newComponentType != null
|
||||||
)
|
|
||||||
export const dndIsNewComponent = derived(
|
|
||||||
dndStore,
|
|
||||||
$dndStore => $dndStore.source?.newComponentType != null
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { API } from "api"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
loaded: false,
|
||||||
cloud: false,
|
cloud: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ const createEnvironmentStore = () => {
|
||||||
store.set({
|
store.set({
|
||||||
...initialState,
|
...initialState,
|
||||||
...environment,
|
...environment,
|
||||||
|
loaded: true,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
store.set(initialState)
|
store.set(initialState)
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
|
||||||
|
const createEventStore = () => {
|
||||||
|
const initialState = {
|
||||||
|
eventResolvers: {},
|
||||||
|
}
|
||||||
|
const store = writable(initialState)
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
dispatchEvent: (type, data) => {
|
||||||
|
const id = Math.random()
|
||||||
|
return new Promise(resolve => {
|
||||||
|
window.parent.postMessage({ type, data, id })
|
||||||
|
store.update(state => {
|
||||||
|
state.eventResolvers[id] = resolve
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resolveEvent: data => {
|
||||||
|
get(store).eventResolvers[data]?.()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventStore = createEventStore()
|
|
@ -15,6 +15,7 @@ export { uploadStore } from "./uploads.js"
|
||||||
export { rowSelectionStore } from "./rowSelection.js"
|
export { rowSelectionStore } from "./rowSelection.js"
|
||||||
export { blockStore } from "./blocks.js"
|
export { blockStore } from "./blocks.js"
|
||||||
export { environmentStore } from "./environment"
|
export { environmentStore } from "./environment"
|
||||||
|
export { eventStore } from "./events.js"
|
||||||
export {
|
export {
|
||||||
dndStore,
|
dndStore,
|
||||||
dndIndex,
|
dndIndex,
|
||||||
|
|
|
@ -2,11 +2,11 @@ import { derived } from "svelte/store"
|
||||||
import { routeStore } from "./routes"
|
import { routeStore } from "./routes"
|
||||||
import { builderStore } from "./builder"
|
import { builderStore } from "./builder"
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
import { dndIndex, dndParent, dndIsNewComponent } from "./dnd.js"
|
import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
import { findComponentById, findComponentParent } from "../utils/components.js"
|
import { findComponentById, findComponentParent } from "../utils/components.js"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { DNDPlaceholderID, DNDPlaceholderType } from "constants"
|
import { DNDPlaceholderID } from "constants"
|
||||||
|
|
||||||
const createScreenStore = () => {
|
const createScreenStore = () => {
|
||||||
const store = derived(
|
const store = derived(
|
||||||
|
@ -17,6 +17,7 @@ const createScreenStore = () => {
|
||||||
dndParent,
|
dndParent,
|
||||||
dndIndex,
|
dndIndex,
|
||||||
dndIsNewComponent,
|
dndIsNewComponent,
|
||||||
|
dndBounds,
|
||||||
],
|
],
|
||||||
([
|
([
|
||||||
$appStore,
|
$appStore,
|
||||||
|
@ -25,6 +26,7 @@ const createScreenStore = () => {
|
||||||
$dndParent,
|
$dndParent,
|
||||||
$dndIndex,
|
$dndIndex,
|
||||||
$dndIsNewComponent,
|
$dndIsNewComponent,
|
||||||
|
$dndBounds,
|
||||||
]) => {
|
]) => {
|
||||||
let activeLayout, activeScreen
|
let activeLayout, activeScreen
|
||||||
let screens
|
let screens
|
||||||
|
@ -62,32 +64,43 @@ const createScreenStore = () => {
|
||||||
|
|
||||||
// Insert DND placeholder if required
|
// Insert DND placeholder if required
|
||||||
if (activeScreen && $dndParent && $dndIndex != null) {
|
if (activeScreen && $dndParent && $dndIndex != null) {
|
||||||
|
const { selectedComponentId } = $builderStore
|
||||||
|
|
||||||
|
// Extract and save the selected component as we need a reference to it
|
||||||
|
// later, and we may be removing it
|
||||||
|
let selectedParent = findComponentParent(
|
||||||
|
activeScreen.props,
|
||||||
|
selectedComponentId
|
||||||
|
)
|
||||||
|
|
||||||
// Remove selected component from tree if we are moving an existing
|
// Remove selected component from tree if we are moving an existing
|
||||||
// component
|
// component
|
||||||
const { selectedComponentId } = $builderStore
|
if (!$dndIsNewComponent && selectedParent) {
|
||||||
if (!$dndIsNewComponent) {
|
selectedParent._children = selectedParent._children?.filter(
|
||||||
let selectedParent = findComponentParent(
|
x => x._id !== selectedComponentId
|
||||||
activeScreen.props,
|
|
||||||
selectedComponentId
|
|
||||||
)
|
)
|
||||||
if (selectedParent) {
|
|
||||||
selectedParent._children = selectedParent._children?.filter(
|
|
||||||
x => x._id !== selectedComponentId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert placeholder component
|
// Insert placeholder component
|
||||||
const placeholder = {
|
const componentToInsert = {
|
||||||
_component: DNDPlaceholderID,
|
_component: "@budibase/standard-components/container",
|
||||||
_id: DNDPlaceholderType,
|
_id: DNDPlaceholderID,
|
||||||
|
_styles: {
|
||||||
|
normal: {
|
||||||
|
width: `${$dndBounds?.width || 400}px`,
|
||||||
|
height: `${$dndBounds?.height || 200}px`,
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
static: true,
|
static: true,
|
||||||
}
|
}
|
||||||
let parent = findComponentById(activeScreen.props, $dndParent)
|
let parent = findComponentById(activeScreen.props, $dndParent)
|
||||||
if (!parent._children?.length) {
|
if (parent) {
|
||||||
parent._children = [placeholder]
|
if (!parent._children?.length) {
|
||||||
} else {
|
parent._children = [componentToInsert]
|
||||||
parent._children.splice($dndIndex, 0, placeholder)
|
} else {
|
||||||
|
parent._children.splice($dndIndex, 0, componentToInsert)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension of Svelte's built in "derived" stores, which the addition of deep
|
||||||
|
* comparison of non-primitives. Falls back to using shallow comparison for
|
||||||
|
* primitive types to avoid performance penalties.
|
||||||
|
* Useful for instances where a deep comparison is cheaper than an additional
|
||||||
|
* store invalidation.
|
||||||
|
* @param store the store to observer
|
||||||
|
* @param deriveValue the derivation function
|
||||||
|
* @returns {Writable<*>} a derived svelte store containing just the derived value
|
||||||
|
*/
|
||||||
|
export const computed = (store, deriveValue) => {
|
||||||
|
const initialValue = deriveValue(store)
|
||||||
|
const computedStore = writable(initialValue)
|
||||||
|
let lastKey = getKey(initialValue)
|
||||||
|
|
||||||
|
store.subscribe(state => {
|
||||||
|
const value = deriveValue(state)
|
||||||
|
const key = getKey(value)
|
||||||
|
if (key !== lastKey) {
|
||||||
|
lastKey = key
|
||||||
|
computedStore.set(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return computedStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to serialise any value into a primitive which can be cheaply
|
||||||
|
// and shallowly compared
|
||||||
|
const getKey = value => {
|
||||||
|
if (value == null || typeof value !== "object") {
|
||||||
|
return value
|
||||||
|
} else {
|
||||||
|
return JSON.stringify(value)
|
||||||
|
}
|
||||||
|
}
|
|
@ -671,9 +671,15 @@ has@^1.0.3:
|
||||||
function-bind "^1.1.1"
|
function-bind "^1.1.1"
|
||||||
|
|
||||||
html5-qrcode@^2.2.1:
|
html5-qrcode@^2.2.1:
|
||||||
|
<<<<<<< HEAD
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/html5-qrcode/-/html5-qrcode-2.2.4.tgz#99e4b36fbd8fbc4956036cf3f21ea3e98c3463d1"
|
resolved "https://registry.yarnpkg.com/html5-qrcode/-/html5-qrcode-2.2.4.tgz#99e4b36fbd8fbc4956036cf3f21ea3e98c3463d1"
|
||||||
integrity sha512-X8wVVsHpNb35tl7KcoCGAboc6Nep2VyT3CIMjFvrfWrHbHTC0yYTjE+DhO/VcswY2MfHy1uB7b1G9+L13gM6dQ==
|
integrity sha512-X8wVVsHpNb35tl7KcoCGAboc6Nep2VyT3CIMjFvrfWrHbHTC0yYTjE+DhO/VcswY2MfHy1uB7b1G9+L13gM6dQ==
|
||||||
|
=======
|
||||||
|
version "2.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/html5-qrcode/-/html5-qrcode-2.2.3.tgz#5acb826860365e7c7ab91e1e14528ea16a502e8a"
|
||||||
|
integrity sha512-9CtEz5FVT56T76entiQxyrASzBWl8Rm30NHiQH8T163Eml5LS14BoZlYel9igxbikOt7O8KhvrT3awN1Y2HMqw==
|
||||||
|
>>>>>>> 50f0a0509d79dcee7a0f12608054279d65662b10
|
||||||
|
|
||||||
htmlparser2@^6.0.0:
|
htmlparser2@^6.0.0:
|
||||||
version "6.1.0"
|
version "6.1.0"
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/frontend-core",
|
"name": "@budibase/frontend-core",
|
||||||
"version": "2.0.30-alpha.13",
|
"version": "2.0.34-alpha.3",
|
||||||
"description": "Budibase frontend core libraries used in builder and client",
|
"description": "Budibase frontend core libraries used in builder and client",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.0.30-alpha.13",
|
"@budibase/bbui": "2.0.34-alpha.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"svelte": "^3.46.2"
|
"svelte": "^3.46.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
export const buildBackupsEndpoints = API => ({
|
||||||
|
/**
|
||||||
|
* Gets a list of users in the current tenant.
|
||||||
|
*/
|
||||||
|
searchBackups: async ({ appId, trigger, type, page, startDate, endDate }) => {
|
||||||
|
const opts = {}
|
||||||
|
if (page) {
|
||||||
|
opts.page = page
|
||||||
|
}
|
||||||
|
if (trigger && type) {
|
||||||
|
opts.trigger = trigger.toLowerCase()
|
||||||
|
opts.type = type.toLowerCase()
|
||||||
|
}
|
||||||
|
if (startDate && endDate) {
|
||||||
|
opts.startDate = startDate
|
||||||
|
opts.endDate = endDate
|
||||||
|
}
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/apps/${appId}/backups/search`,
|
||||||
|
body: opts,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
createManualBackup: async ({ appId, name }) => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/apps/${appId}/backups`,
|
||||||
|
body: { name },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteBackup: async ({ appId, backupId }) => {
|
||||||
|
return await API.delete({
|
||||||
|
url: `/api/apps/${appId}/backups/${backupId}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBackup: async ({ appId, backupId, name }) => {
|
||||||
|
return await API.patch({
|
||||||
|
url: `/api/apps/${appId}/backups/${backupId}`,
|
||||||
|
body: { name },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
restoreBackup: async ({ appId, backupId, name }) => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/apps/${appId}/backups/${backupId}/import`,
|
||||||
|
body: { name },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue