Merge branch 'master' into type-portal-oidc-store
This commit is contained in:
commit
df18dcfeff
|
@ -19,5 +19,8 @@ jobs:
|
||||||
cache: yarn
|
cache: yarn
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Install OpenAPI pkg
|
||||||
|
run: yarn global add openapi
|
||||||
|
|
||||||
- name: update specs
|
- name: update specs
|
||||||
run: cd packages/server && yarn specs && openapi specs/openapi.yaml --key=${{ secrets.README_API_KEY }} --id=6728a74f5918b50036c61841
|
run: cd packages/server && yarn specs && openapi specs/openapi.yaml --key=${{ secrets.README_API_KEY }} --id=6728a74f5918b50036c61841
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bulma": "^0.9.3",
|
"bulma": "^0.9.3",
|
||||||
"next": "14.2.15",
|
"next": "14.2.21",
|
||||||
"node-fetch": "^3.2.10",
|
"node-fetch": "^3.2.10",
|
||||||
"sass": "^1.52.3",
|
"sass": "^1.52.3",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
|
|
|
@ -46,10 +46,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
||||||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
||||||
|
|
||||||
"@next/env@14.2.15":
|
"@next/env@14.2.21":
|
||||||
version "14.2.15"
|
version "14.2.21"
|
||||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.15.tgz#06d984e37e670d93ddd6790af1844aeb935f332f"
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.21.tgz#09ff0813d29c596397e141205d4f5fd5c236bdd0"
|
||||||
integrity sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==
|
integrity sha512-lXcwcJd5oR01tggjWJ6SrNNYFGuOOMB9c251wUNkjCpkoXOPkDeF/15c3mnVlBqrW4JJXb2kVxDFhC4GduJt2A==
|
||||||
|
|
||||||
"@next/eslint-plugin-next@12.1.0":
|
"@next/eslint-plugin-next@12.1.0":
|
||||||
version "12.1.0"
|
version "12.1.0"
|
||||||
|
@ -58,50 +58,50 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
glob "7.1.7"
|
glob "7.1.7"
|
||||||
|
|
||||||
"@next/swc-darwin-arm64@14.2.15":
|
"@next/swc-darwin-arm64@14.2.21":
|
||||||
version "14.2.15"
|
version "14.2.21"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz#6386d585f39a1c490c60b72b1f76612ba4434347"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.21.tgz#32a31992aace1440981df9cf7cb3af7845d94fec"
|
||||||
integrity sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==
|
integrity sha512-HwEjcKsXtvszXz5q5Z7wCtrHeTTDSTgAbocz45PHMUjU3fBYInfvhR+ZhavDRUYLonm53aHZbB09QtJVJj8T7g==
|
||||||
|
|
||||||
"@next/swc-darwin-x64@14.2.15":
|
"@next/swc-darwin-x64@14.2.21":
|
||||||
version "14.2.15"
|
version "14.2.21"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz#b7baeedc6a28f7545ad2bc55adbab25f7b45cb89"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.21.tgz#5ab4b3f6685b6b52f810d0f5cf6e471480ddffdb"
|
||||||
integrity sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==
|
integrity sha512-TSAA2ROgNzm4FhKbTbyJOBrsREOMVdDIltZ6aZiKvCi/v0UwFmwigBGeqXDA97TFMpR3LNNpw52CbVelkoQBxA==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu@14.2.15":
|
"@next/swc-linux-arm64-gnu@14.2.21":
|
||||||
version "14.2.15"
|
version "14.2.21"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz#fa13c59d3222f70fb4cb3544ac750db2c6e34d02"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.21.tgz#8a0e1fa887aef19ca218af2af515d0a5ee67ba3f"
|
||||||
integrity sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==
|
integrity sha512-0Dqjn0pEUz3JG+AImpnMMW/m8hRtl1GQCNbO66V1yp6RswSTiKmnHf3pTX6xMdJYSemf3O4Q9ykiL0jymu0TuA==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl@14.2.15":
|
"@next/swc-linux-arm64-musl@14.2.21":
|
||||||
version "14.2.15"
|
version "14.2.21"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz#30e45b71831d9a6d6d18d7ac7d611a8d646a17f9"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.21.tgz#ddad844406b42fa8965fe11250abc85c1fe0fd05"
|
||||||
integrity sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==
|
integrity sha512-Ggfw5qnMXldscVntwnjfaQs5GbBbjioV4B4loP+bjqNEb42fzZlAaK+ldL0jm2CTJga9LynBMhekNfV8W4+HBw==
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu@14.2.15":
|
"@next/swc-linux-x64-gnu@14.2.21":
|
||||||
version "14.2.15"
|
version "14.2.21"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz#5065db17fc86f935ad117483f21f812dc1b39254"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.21.tgz#db55fd666f9ba27718f65caa54b622a912cdd16b"
|
||||||
integrity sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==
|
integrity sha512-uokj0lubN1WoSa5KKdThVPRffGyiWlm/vCc/cMkWOQHw69Qt0X1o3b2PyLLx8ANqlefILZh1EdfLRz9gVpG6tg==
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl@14.2.15":
|
"@next/swc-linux-x64-musl@14.2.21":
|
||||||
version "14.2.15"
|
version "14.2.21"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz#3c4a4568d8be7373a820f7576cf33388b5dab47e"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.21.tgz#dddb850353624efcd58c4c4e30ad8a1aab379642"
|
||||||
integrity sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==
|
integrity sha512-iAEBPzWNbciah4+0yI4s7Pce6BIoxTQ0AGCkxn/UBuzJFkYyJt71MadYQkjPqCQCJAFQ26sYh7MOKdU+VQFgPg==
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc@14.2.15":
|
"@next/swc-win32-arm64-msvc@14.2.21":
|
||||||
version "14.2.15"
|
version "14.2.21"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz#fb812cc4ca0042868e32a6a021da91943bb08b98"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.21.tgz#290012ee57b196d3d2d04853e6bf0179cae9fbaf"
|
||||||
integrity sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==
|
integrity sha512-plykgB3vL2hB4Z32W3ktsfqyuyGAPxqwiyrAi2Mr8LlEUhNn9VgkiAl5hODSBpzIfWweX3er1f5uNpGDygfQVQ==
|
||||||
|
|
||||||
"@next/swc-win32-ia32-msvc@14.2.15":
|
"@next/swc-win32-ia32-msvc@14.2.21":
|
||||||
version "14.2.15"
|
version "14.2.21"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz#ec26e6169354f8ced240c1427be7fd485c5df898"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.21.tgz#c959135a78cab18cca588d11d1e33bcf199590d4"
|
||||||
integrity sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==
|
integrity sha512-w5bacz4Vxqrh06BjWgua3Yf7EMDb8iMcVhNrNx8KnJXt8t+Uu0Zg4JHLDL/T7DkTCEEfKXO/Er1fcfWxn2xfPA==
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc@14.2.15":
|
"@next/swc-win32-x64-msvc@14.2.21":
|
||||||
version "14.2.15"
|
version "14.2.21"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz#18d68697002b282006771f8d92d79ade9efd35c4"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.21.tgz#21ff892286555b90538a7d1b505ea21a005d6ead"
|
||||||
integrity sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==
|
integrity sha512-sT6+llIkzpsexGYZq8cjjthRyRGe5cJVhqh12FmlbxHqna6zsDDK8UNaV7g41T6atFHCJUPeLb3uyAwrBwy0NA==
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
|
@ -1253,12 +1253,12 @@ natural-compare@^1.4.0:
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||||
|
|
||||||
next@14.2.15:
|
next@14.2.21:
|
||||||
version "14.2.15"
|
version "14.2.21"
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-14.2.15.tgz#348e5603e22649775d19c785c09a89c9acb5189a"
|
resolved "https://registry.yarnpkg.com/next/-/next-14.2.21.tgz#f6da9e2abba1a0e4ca7a5273825daf06632554ba"
|
||||||
integrity sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==
|
integrity sha512-rZmLwucLHr3/zfDMYbJXbw0ZeoBpirxkXuvsJbk7UPorvPYZhP7vq7aHbKnU7dQNCYIimRrbB2pp3xmf+wsYUg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@next/env" "14.2.15"
|
"@next/env" "14.2.21"
|
||||||
"@swc/helpers" "0.5.5"
|
"@swc/helpers" "0.5.5"
|
||||||
busboy "1.6.0"
|
busboy "1.6.0"
|
||||||
caniuse-lite "^1.0.30001579"
|
caniuse-lite "^1.0.30001579"
|
||||||
|
@ -1266,15 +1266,15 @@ next@14.2.15:
|
||||||
postcss "8.4.31"
|
postcss "8.4.31"
|
||||||
styled-jsx "5.1.1"
|
styled-jsx "5.1.1"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@next/swc-darwin-arm64" "14.2.15"
|
"@next/swc-darwin-arm64" "14.2.21"
|
||||||
"@next/swc-darwin-x64" "14.2.15"
|
"@next/swc-darwin-x64" "14.2.21"
|
||||||
"@next/swc-linux-arm64-gnu" "14.2.15"
|
"@next/swc-linux-arm64-gnu" "14.2.21"
|
||||||
"@next/swc-linux-arm64-musl" "14.2.15"
|
"@next/swc-linux-arm64-musl" "14.2.21"
|
||||||
"@next/swc-linux-x64-gnu" "14.2.15"
|
"@next/swc-linux-x64-gnu" "14.2.21"
|
||||||
"@next/swc-linux-x64-musl" "14.2.15"
|
"@next/swc-linux-x64-musl" "14.2.21"
|
||||||
"@next/swc-win32-arm64-msvc" "14.2.15"
|
"@next/swc-win32-arm64-msvc" "14.2.21"
|
||||||
"@next/swc-win32-ia32-msvc" "14.2.15"
|
"@next/swc-win32-ia32-msvc" "14.2.21"
|
||||||
"@next/swc-win32-x64-msvc" "14.2.15"
|
"@next/swc-win32-x64-msvc" "14.2.21"
|
||||||
|
|
||||||
node-domexception@^1.0.0:
|
node-domexception@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "3.2.29",
|
"version": "3.2.37",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"concurrency": 20,
|
"concurrency": 20,
|
||||||
"command": {
|
"command": {
|
||||||
|
|
|
@ -385,17 +385,17 @@ export function getCurrentContext(): ContextMap | undefined {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFeatureFlags<T extends Record<string, any>>(
|
export function getFeatureFlags(
|
||||||
key: string
|
key: string
|
||||||
): T | undefined {
|
): Record<string, boolean> | undefined {
|
||||||
const context = getCurrentContext()
|
const context = getCurrentContext()
|
||||||
if (!context) {
|
if (!context) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return context.featureFlagCache?.[key] as T
|
return context.featureFlagCache?.[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setFeatureFlags(key: string, value: Record<string, any>) {
|
export function setFeatureFlags(key: string, value: Record<string, boolean>) {
|
||||||
const context = getCurrentContext()
|
const context = getCurrentContext()
|
||||||
if (!context) {
|
if (!context) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -20,7 +20,7 @@ export type ContextMap = {
|
||||||
clients: Record<string, GoogleSpreadsheet>
|
clients: Record<string, GoogleSpreadsheet>
|
||||||
}
|
}
|
||||||
featureFlagCache?: {
|
featureFlagCache?: {
|
||||||
[key: string]: Record<string, any>
|
[key: string]: Record<string, boolean>
|
||||||
}
|
}
|
||||||
viewToTableCache?: Record<string, Table>
|
viewToTableCache?: Record<string, Table>
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,10 @@ import env from "../environment"
|
||||||
import * as crypto from "crypto"
|
import * as crypto from "crypto"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
import { PostHog, PostHogOptions } from "posthog-node"
|
import { PostHog, PostHogOptions } from "posthog-node"
|
||||||
import { FeatureFlag } from "@budibase/types"
|
|
||||||
import tracer from "dd-trace"
|
import tracer from "dd-trace"
|
||||||
import { Duration } from "../utils"
|
import { Duration } from "../utils"
|
||||||
|
import { cloneDeep } from "lodash"
|
||||||
|
import { FeatureFlagDefaults } from "@budibase/types"
|
||||||
|
|
||||||
let posthog: PostHog | undefined
|
let posthog: PostHog | undefined
|
||||||
export function init(opts?: PostHogOptions) {
|
export function init(opts?: PostHogOptions) {
|
||||||
|
@ -30,74 +31,6 @@ export function shutdown() {
|
||||||
posthog?.shutdown()
|
posthog?.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class Flag<T> {
|
|
||||||
static boolean(defaultValue: boolean): Flag<boolean> {
|
|
||||||
return new BooleanFlag(defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
static string(defaultValue: string): Flag<string> {
|
|
||||||
return new StringFlag(defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
static number(defaultValue: number): Flag<number> {
|
|
||||||
return new NumberFlag(defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected constructor(public defaultValue: T) {}
|
|
||||||
|
|
||||||
abstract parse(value: any): T
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnwrapFlag<F> = F extends Flag<infer U> ? U : never
|
|
||||||
|
|
||||||
export type FlagValues<T> = {
|
|
||||||
[K in keyof T]: UnwrapFlag<T[K]>
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeysOfType<T, U> = {
|
|
||||||
[K in keyof T]: T[K] extends Flag<U> ? K : never
|
|
||||||
}[keyof T]
|
|
||||||
|
|
||||||
class BooleanFlag extends Flag<boolean> {
|
|
||||||
parse(value: any) {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return ["true", "t", "1"].includes(value.toLowerCase())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`could not parse value "${value}" as boolean`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StringFlag extends Flag<string> {
|
|
||||||
parse(value: any) {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
throw new Error(`could not parse value "${value}" as string`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NumberFlag extends Flag<number> {
|
|
||||||
parse(value: any) {
|
|
||||||
if (typeof value === "number") {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "string") {
|
|
||||||
const parsed = parseFloat(value)
|
|
||||||
if (!isNaN(parsed)) {
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`could not parse value "${value}" as number`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EnvFlagEntry {
|
export interface EnvFlagEntry {
|
||||||
tenantId: string
|
tenantId: string
|
||||||
key: string
|
key: string
|
||||||
|
@ -120,7 +53,7 @@ export function parseEnvFlags(flags: string): EnvFlagEntry[] {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
export class FlagSet<T extends { [name: string]: boolean }> {
|
||||||
// This is used to safely cache flags sets in the current request context.
|
// This is used to safely cache flags sets in the current request context.
|
||||||
// Because multiple sets could theoretically exist, we don't want the cache of
|
// Because multiple sets could theoretically exist, we don't want the cache of
|
||||||
// one to leak into another.
|
// one to leak into another.
|
||||||
|
@ -130,34 +63,25 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||||
this.setId = crypto.randomUUID()
|
this.setId = crypto.randomUUID()
|
||||||
}
|
}
|
||||||
|
|
||||||
defaults(): FlagValues<T> {
|
defaults(): T {
|
||||||
return Object.keys(this.flagSchema).reduce((acc, key) => {
|
return cloneDeep(this.flagSchema)
|
||||||
const typedKey = key as keyof T
|
|
||||||
acc[typedKey] = this.flagSchema[key].defaultValue
|
|
||||||
return acc
|
|
||||||
}, {} as FlagValues<T>)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isFlagName(name: string | number | symbol): name is keyof T {
|
isFlagName(name: string | number | symbol): name is keyof T {
|
||||||
return this.flagSchema[name as keyof T] !== undefined
|
return this.flagSchema[name as keyof T] !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async get<K extends keyof T>(key: K): Promise<FlagValues<T>[K]> {
|
async isEnabled<K extends keyof T>(key: K): Promise<T[K]> {
|
||||||
const flags = await this.fetch()
|
const flags = await this.fetch()
|
||||||
return flags[key]
|
return flags[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
async isEnabled<K extends KeysOfType<T, boolean>>(key: K): Promise<boolean> {
|
async fetch(): Promise<T> {
|
||||||
const flags = await this.fetch()
|
|
||||||
return flags[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetch(): Promise<FlagValues<T>> {
|
|
||||||
return await tracer.trace("features.fetch", async span => {
|
return await tracer.trace("features.fetch", async span => {
|
||||||
const cachedFlags = context.getFeatureFlags<FlagValues<T>>(this.setId)
|
const cachedFlags = context.getFeatureFlags(this.setId)
|
||||||
if (cachedFlags) {
|
if (cachedFlags) {
|
||||||
span?.addTags({ fromCache: true })
|
span?.addTags({ fromCache: true })
|
||||||
return cachedFlags
|
return cachedFlags as T
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags: Record<string, any> = {}
|
const tags: Record<string, any> = {}
|
||||||
|
@ -189,7 +113,7 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||||
|
|
||||||
// @ts-expect-error - TS does not like you writing into a generic type,
|
// @ts-expect-error - TS does not like you writing into a generic type,
|
||||||
// but we know that it's okay in this case because it's just an object.
|
// but we know that it's okay in this case because it's just an object.
|
||||||
flagValues[key as keyof FlagValues] = value
|
flagValues[key as keyof T] = value
|
||||||
tags[`flags.${key}.source`] = "environment"
|
tags[`flags.${key}.source`] = "environment"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,11 +141,11 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||||
tags[`readFromPostHog`] = true
|
tags[`readFromPostHog`] = true
|
||||||
|
|
||||||
const personProperties: Record<string, string> = { tenantId }
|
const personProperties: Record<string, string> = { tenantId }
|
||||||
const posthogFlags = await posthog.getAllFlagsAndPayloads(userId, {
|
const posthogFlags = await posthog.getAllFlags(userId, {
|
||||||
personProperties,
|
personProperties,
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const [name, value] of Object.entries(posthogFlags.featureFlags)) {
|
for (const [name, value] of Object.entries(posthogFlags)) {
|
||||||
if (!this.isFlagName(name)) {
|
if (!this.isFlagName(name)) {
|
||||||
// We don't want an unexpected PostHog flag to break the app, so we
|
// We don't want an unexpected PostHog flag to break the app, so we
|
||||||
// just log it and continue.
|
// just log it and continue.
|
||||||
|
@ -229,19 +153,20 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof value !== "boolean") {
|
||||||
|
console.warn(`Invalid value for posthog flag "${name}": ${value}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (flagValues[name] === true || specificallySetFalse.has(name)) {
|
if (flagValues[name] === true || specificallySetFalse.has(name)) {
|
||||||
// If the flag is already set to through environment variables, we
|
// If the flag is already set to through environment variables, we
|
||||||
// don't want to override it back to false here.
|
// don't want to override it back to false here.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = posthogFlags.featureFlagPayloads?.[name]
|
|
||||||
const flag = this.flagSchema[name]
|
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error - TS does not like you writing into a generic
|
// @ts-expect-error - TS does not like you writing into a generic type.
|
||||||
// type, but we know that it's okay in this case because it's just
|
flagValues[name] = value
|
||||||
// an object.
|
|
||||||
flagValues[name] = flag.parse(payload || value)
|
|
||||||
tags[`flags.${name}.source`] = "posthog"
|
tags[`flags.${name}.source`] = "posthog"
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// We don't want an invalid PostHog flag to break the app, so we just
|
// We don't want an invalid PostHog flag to break the app, so we just
|
||||||
|
@ -262,18 +187,12 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is the primary source of truth for feature flags. If you want to add a
|
export const flags = new FlagSet(FeatureFlagDefaults)
|
||||||
// new flag, add it here and use the `fetch` and `get` functions to access it.
|
|
||||||
// All of the machinery in this file is to make sure that flags have their
|
|
||||||
// default values set correctly and their types flow through the system.
|
|
||||||
const flagsConfig: Record<FeatureFlag, Flag<any>> = {
|
|
||||||
[FeatureFlag.DEFAULT_VALUES]: Flag.boolean(true),
|
|
||||||
[FeatureFlag.AUTOMATION_BRANCHING]: Flag.boolean(true),
|
|
||||||
[FeatureFlag.AI_CUSTOM_CONFIGS]: Flag.boolean(true),
|
|
||||||
[FeatureFlag.BUDIBASE_AI]: Flag.boolean(true),
|
|
||||||
[FeatureFlag.USE_ZOD_VALIDATOR]: Flag.boolean(env.isDev()),
|
|
||||||
}
|
|
||||||
export const flags = new FlagSet(flagsConfig)
|
|
||||||
|
|
||||||
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T
|
export async function isEnabled(flag: keyof typeof FeatureFlagDefaults) {
|
||||||
export type FeatureFlags = UnwrapPromise<ReturnType<typeof flags.fetch>>
|
return await flags.isEnabled(flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function all() {
|
||||||
|
return await flags.fetch()
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { IdentityContext, IdentityType } from "@budibase/types"
|
import { IdentityContext, IdentityType } from "@budibase/types"
|
||||||
import { Flag, FlagSet, FlagValues, init, shutdown } from "../"
|
import { FlagSet, init, shutdown } from "../"
|
||||||
import * as context from "../../context"
|
import * as context from "../../context"
|
||||||
import environment, { withEnv } from "../../environment"
|
import environment, { withEnv } from "../../environment"
|
||||||
import nodeFetch from "node-fetch"
|
import nodeFetch from "node-fetch"
|
||||||
|
@ -7,10 +7,8 @@ import nock from "nock"
|
||||||
import * as crypto from "crypto"
|
import * as crypto from "crypto"
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
TEST_BOOLEAN: Flag.boolean(false),
|
TEST_BOOLEAN: false,
|
||||||
TEST_STRING: Flag.string("default value"),
|
TEST_BOOLEAN_DEFAULT_TRUE: true,
|
||||||
TEST_NUMBER: Flag.number(0),
|
|
||||||
TEST_BOOLEAN_DEFAULT_TRUE: Flag.boolean(true),
|
|
||||||
}
|
}
|
||||||
const flags = new FlagSet(schema)
|
const flags = new FlagSet(schema)
|
||||||
|
|
||||||
|
@ -19,7 +17,7 @@ interface TestCase {
|
||||||
identity?: Partial<IdentityContext>
|
identity?: Partial<IdentityContext>
|
||||||
environmentFlags?: string
|
environmentFlags?: string
|
||||||
posthogFlags?: PostHogFlags
|
posthogFlags?: PostHogFlags
|
||||||
expected?: Partial<FlagValues<typeof schema>>
|
expected?: Partial<typeof schema>
|
||||||
errorMessage?: string | RegExp
|
errorMessage?: string | RegExp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,22 +81,6 @@ describe("feature flags", () => {
|
||||||
},
|
},
|
||||||
expected: { TEST_BOOLEAN: true },
|
expected: { TEST_BOOLEAN: true },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
it: "should be able to read string flags from PostHog",
|
|
||||||
posthogFlags: {
|
|
||||||
featureFlags: { TEST_STRING: true },
|
|
||||||
featureFlagPayloads: { TEST_STRING: "test" },
|
|
||||||
},
|
|
||||||
expected: { TEST_STRING: "test" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
it: "should be able to read numeric flags from PostHog",
|
|
||||||
posthogFlags: {
|
|
||||||
featureFlags: { TEST_NUMBER: true },
|
|
||||||
featureFlagPayloads: { TEST_NUMBER: "123" },
|
|
||||||
},
|
|
||||||
expected: { TEST_NUMBER: 123 },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
it: "should not be able to override a negative environment flag from PostHog",
|
it: "should not be able to override a negative environment flag from PostHog",
|
||||||
environmentFlags: "default:!TEST_BOOLEAN",
|
environmentFlags: "default:!TEST_BOOLEAN",
|
||||||
|
@ -177,7 +159,7 @@ describe("feature flags", () => {
|
||||||
expect(values).toMatchObject(expected)
|
expect(values).toMatchObject(expected)
|
||||||
|
|
||||||
for (const [key, expectedValue] of Object.entries(expected)) {
|
for (const [key, expectedValue] of Object.entries(expected)) {
|
||||||
const value = await flags.get(key as keyof typeof schema)
|
const value = await flags.isEnabled(key as keyof typeof schema)
|
||||||
expect(value).toBe(expectedValue)
|
expect(value).toBe(expectedValue)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { FeatureFlags, parseEnvFlags } from ".."
|
import { FeatureFlags } from "@budibase/types"
|
||||||
import { setEnv } from "../../environment"
|
import { setEnv } from "../../environment"
|
||||||
|
import { parseEnvFlags } from "../features"
|
||||||
|
|
||||||
function getCurrentFlags(): Record<string, Record<string, boolean>> {
|
function getCurrentFlags(): Record<string, Record<string, boolean>> {
|
||||||
const result: Record<string, Record<string, boolean>> = {}
|
const result: Record<string, Record<string, boolean>> = {}
|
||||||
|
|
|
@ -87,6 +87,7 @@
|
||||||
let popover
|
let popover
|
||||||
let user, tenantOwner
|
let user, tenantOwner
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
let userFieldsToUpdate = {}
|
||||||
|
|
||||||
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
|
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
|
||||||
|
|
||||||
|
@ -164,40 +165,45 @@
|
||||||
return label
|
return label
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUserFirstName(evt) {
|
async function saveUser() {
|
||||||
try {
|
try {
|
||||||
await users.save({ ...user, firstName: evt.target.value })
|
await users.save({ ...user, ...userFieldsToUpdate })
|
||||||
|
userFieldsToUpdate = {}
|
||||||
await fetchUser()
|
await fetchUser()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error updating user")
|
notifications.error("Error updating user")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateUserFirstName(evt) {
|
||||||
|
userFieldsToUpdate.firstName = evt.target.value
|
||||||
|
}
|
||||||
|
|
||||||
async function updateUserLastName(evt) {
|
async function updateUserLastName(evt) {
|
||||||
try {
|
userFieldsToUpdate.lastName = evt.target.value
|
||||||
await users.save({ ...user, lastName: evt.target.value })
|
|
||||||
await fetchUser()
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error updating user")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUserRole({ detail }) {
|
async function updateUserRole({ detail }) {
|
||||||
|
let flags = {}
|
||||||
if (detail === Constants.BudibaseRoles.Developer) {
|
if (detail === Constants.BudibaseRoles.Developer) {
|
||||||
toggleFlags({ admin: { global: false }, builder: { global: true } })
|
flags = { admin: { global: false }, builder: { global: true } }
|
||||||
} else if (detail === Constants.BudibaseRoles.Admin) {
|
} else if (detail === Constants.BudibaseRoles.Admin) {
|
||||||
toggleFlags({ admin: { global: true }, builder: { global: true } })
|
flags = { admin: { global: true }, builder: { global: true } }
|
||||||
} else if (detail === Constants.BudibaseRoles.AppUser) {
|
} else if (detail === Constants.BudibaseRoles.AppUser) {
|
||||||
toggleFlags({ admin: { global: false }, builder: { global: false } })
|
flags = { admin: { global: false }, builder: { global: false } }
|
||||||
} else if (detail === Constants.BudibaseRoles.Creator) {
|
} else if (detail === Constants.BudibaseRoles.Creator) {
|
||||||
toggleFlags({
|
flags = {
|
||||||
admin: { global: false },
|
admin: { global: false },
|
||||||
builder: {
|
builder: {
|
||||||
global: false,
|
global: false,
|
||||||
creator: true,
|
creator: true,
|
||||||
apps: user?.builder?.apps || [],
|
apps: user?.builder?.apps || [],
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
userFieldsToUpdate = {
|
||||||
|
...userFieldsToUpdate,
|
||||||
|
...flags,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,15 +215,6 @@
|
||||||
tenantOwner = await users.getAccountHolder()
|
tenantOwner = await users.getAccountHolder()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleFlags(detail) {
|
|
||||||
try {
|
|
||||||
await users.save({ ...user, ...detail })
|
|
||||||
await fetchUser()
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error updating user")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addGroup = async groupId => {
|
const addGroup = async groupId => {
|
||||||
await groups.addUser(groupId, userId)
|
await groups.addUser(groupId, userId)
|
||||||
await fetchUser()
|
await fetchUser()
|
||||||
|
@ -296,7 +293,7 @@
|
||||||
<Input
|
<Input
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={user?.firstName}
|
value={user?.firstName}
|
||||||
on:blur={updateUserFirstName}
|
on:input={updateUserFirstName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
@ -304,7 +301,7 @@
|
||||||
<Input
|
<Input
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={user?.lastName}
|
value={user?.lastName}
|
||||||
on:blur={updateUserLastName}
|
on:input={updateUserLastName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- don't let a user remove the privileges that let them be here -->
|
<!-- don't let a user remove the privileges that let them be here -->
|
||||||
|
@ -325,6 +322,13 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
disabled={Object.keys(userFieldsToUpdate).length === 0}
|
||||||
|
on:click={saveUser}>Save</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if $licensing.groupsEnabled}
|
{#if $licensing.groupsEnabled}
|
||||||
<!-- User groups -->
|
<!-- User groups -->
|
||||||
|
|
|
@ -291,8 +291,8 @@ const automationActions = (store: AutomationStore) => ({
|
||||||
let result: (AutomationStep | AutomationTrigger)[] = []
|
let result: (AutomationStep | AutomationTrigger)[] = []
|
||||||
pathWay.forEach(path => {
|
pathWay.forEach(path => {
|
||||||
const { stepIdx, branchIdx } = path
|
const { stepIdx, branchIdx } = path
|
||||||
let last = result ? result[result.length - 1] : []
|
let last = result.length ? result[result.length - 1] : []
|
||||||
if (!result) {
|
if (!result.length) {
|
||||||
// Preceeding steps.
|
// Preceeding steps.
|
||||||
result = steps.slice(0, stepIdx + 1)
|
result = steps.slice(0, stepIdx + 1)
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { writable } from "svelte/store"
|
|
||||||
import { API } from "@/api"
|
|
||||||
|
|
||||||
export function createFlagsStore() {
|
|
||||||
const { subscribe, set } = writable({})
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
fetch: async () => {
|
|
||||||
const flags = await API.getFlags()
|
|
||||||
set(flags)
|
|
||||||
},
|
|
||||||
updateFlag: async (flag, value) => {
|
|
||||||
await API.updateFlag(flag, value)
|
|
||||||
await actions.fetch()
|
|
||||||
},
|
|
||||||
toggleUiFeature: async feature => {
|
|
||||||
await API.toggleUiFeature(feature)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe,
|
|
||||||
...actions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flags = createFlagsStore()
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { API } from "@/api"
|
||||||
|
import { GetUserFlagsResponse } from "@budibase/types"
|
||||||
|
import { BudiStore } from "../BudiStore"
|
||||||
|
|
||||||
|
interface FlagsState {
|
||||||
|
flags: GetUserFlagsResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL_FLAGS_STATE: FlagsState = {
|
||||||
|
flags: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FlagsStore extends BudiStore<FlagsState> {
|
||||||
|
constructor() {
|
||||||
|
super(INITIAL_FLAGS_STATE)
|
||||||
|
|
||||||
|
this.fetch = this.fetch.bind(this)
|
||||||
|
this.updateFlag = this.updateFlag.bind(this)
|
||||||
|
this.toggleUiFeature = this.toggleUiFeature.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const flags = await API.getFlags()
|
||||||
|
this.update(state => ({
|
||||||
|
...state,
|
||||||
|
flags,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFlag(flag: string, value: any) {
|
||||||
|
await API.updateFlag(flag, value)
|
||||||
|
await this.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleUiFeature(feature: string) {
|
||||||
|
await API.toggleUiFeature(feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const flags = new FlagsStore()
|
|
@ -2,19 +2,24 @@ import { get } from "svelte/store"
|
||||||
import { BudiStore } from "../BudiStore"
|
import { BudiStore } from "../BudiStore"
|
||||||
import { previewStore } from "@/stores/builder"
|
import { previewStore } from "@/stores/builder"
|
||||||
|
|
||||||
|
interface BuilderHoverStore {
|
||||||
|
hoverTimeout?: NodeJS.Timeout
|
||||||
|
componentId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export const INITIAL_HOVER_STATE = {
|
export const INITIAL_HOVER_STATE = {
|
||||||
componentId: null,
|
componentId: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HoverStore extends BudiStore {
|
export class HoverStore extends BudiStore<BuilderHoverStore> {
|
||||||
hoverTimeout
|
hoverTimeout?: NodeJS.Timeout
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ ...INITIAL_HOVER_STATE })
|
super({ ...INITIAL_HOVER_STATE })
|
||||||
this.hover = this.hover.bind(this)
|
this.hover = this.hover.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
hover(componentId, notifyClient = true) {
|
hover(componentId: string, notifyClient = true) {
|
||||||
clearTimeout(this.hoverTimeout)
|
clearTimeout(this.hoverTimeout)
|
||||||
if (componentId) {
|
if (componentId) {
|
||||||
this.processHover(componentId, notifyClient)
|
this.processHover(componentId, notifyClient)
|
||||||
|
@ -25,7 +30,7 @@ export class HoverStore extends BudiStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processHover(componentId, notifyClient) {
|
processHover(componentId: string, notifyClient?: boolean) {
|
||||||
if (componentId === get(this.store).componentId) {
|
if (componentId === get(this.store).componentId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
|
@ -2,13 +2,19 @@ import { derived, get } from "svelte/store"
|
||||||
import { componentStore } from "@/stores/builder"
|
import { componentStore } from "@/stores/builder"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { BudiStore } from "../BudiStore"
|
import { BudiStore } from "../BudiStore"
|
||||||
|
import { Layout } from "@budibase/types"
|
||||||
|
|
||||||
export const INITIAL_LAYOUT_STATE = {
|
interface LayoutState {
|
||||||
|
layouts: Layout[]
|
||||||
|
selectedLayoutId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const INITIAL_LAYOUT_STATE: LayoutState = {
|
||||||
layouts: [],
|
layouts: [],
|
||||||
selectedLayoutId: null,
|
selectedLayoutId: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LayoutStore extends BudiStore {
|
export class LayoutStore extends BudiStore<LayoutState> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(INITIAL_LAYOUT_STATE)
|
super(INITIAL_LAYOUT_STATE)
|
||||||
|
|
||||||
|
@ -22,14 +28,14 @@ export class LayoutStore extends BudiStore {
|
||||||
this.store.set({ ...INITIAL_LAYOUT_STATE })
|
this.store.set({ ...INITIAL_LAYOUT_STATE })
|
||||||
}
|
}
|
||||||
|
|
||||||
syncAppLayouts(pkg) {
|
syncAppLayouts(pkg: { layouts: Layout[] }) {
|
||||||
this.update(state => ({
|
this.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
layouts: [...pkg.layouts],
|
layouts: [...pkg.layouts],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
select(layoutId) {
|
select(layoutId: string) {
|
||||||
// Check this layout exists
|
// Check this layout exists
|
||||||
const state = get(this.store)
|
const state = get(this.store)
|
||||||
const componentState = get(componentStore)
|
const componentState = get(componentStore)
|
||||||
|
@ -48,15 +54,15 @@ export class LayoutStore extends BudiStore {
|
||||||
|
|
||||||
// Select new layout
|
// Select new layout
|
||||||
this.update(state => {
|
this.update(state => {
|
||||||
state.selectedLayoutId = layout._id
|
state.selectedLayoutId = layout._id!
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
||||||
componentStore.select(layout.props?._id)
|
componentStore.select(layout.props?._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteLayout(layout) {
|
async deleteLayout(layout: Layout) {
|
||||||
if (!layout?._id) {
|
if (!layout?._id || !layout?._rev) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await API.deleteLayout(layout._id, layout._rev)
|
await API.deleteLayout(layout._id, layout._rev)
|
|
@ -2,27 +2,22 @@ import { get } from "svelte/store"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { appStore } from "@/stores/builder"
|
import { appStore } from "@/stores/builder"
|
||||||
import { BudiStore } from "../BudiStore"
|
import { BudiStore } from "../BudiStore"
|
||||||
|
import { AppNavigation, AppNavigationLink, UIObject } from "@budibase/types"
|
||||||
|
|
||||||
|
interface BuilderNavigationStore extends AppNavigation {}
|
||||||
|
|
||||||
export const INITIAL_NAVIGATION_STATE = {
|
export const INITIAL_NAVIGATION_STATE = {
|
||||||
navigation: "Top",
|
navigation: "Top",
|
||||||
links: [],
|
links: [],
|
||||||
title: null,
|
|
||||||
sticky: null,
|
|
||||||
hideLogo: null,
|
|
||||||
logoUrl: null,
|
|
||||||
hideTitle: null,
|
|
||||||
textAlign: "Left",
|
textAlign: "Left",
|
||||||
navBackground: null,
|
|
||||||
navWidth: null,
|
|
||||||
navTextColor: null,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NavigationStore extends BudiStore {
|
export class NavigationStore extends BudiStore<BuilderNavigationStore> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(INITIAL_NAVIGATION_STATE)
|
super(INITIAL_NAVIGATION_STATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
syncAppNavigation(nav) {
|
syncAppNavigation(nav: AppNavigation) {
|
||||||
this.update(state => ({
|
this.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
...nav,
|
...nav,
|
||||||
|
@ -33,15 +28,17 @@ export class NavigationStore extends BudiStore {
|
||||||
this.store.set({ ...INITIAL_NAVIGATION_STATE })
|
this.store.set({ ...INITIAL_NAVIGATION_STATE })
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(navigation) {
|
async save(navigation: AppNavigation) {
|
||||||
const appId = get(appStore).appId
|
const appId = get(appStore).appId
|
||||||
const app = await API.saveAppMetadata(appId, { navigation })
|
const app = await API.saveAppMetadata(appId, { navigation })
|
||||||
this.syncAppNavigation(app.navigation)
|
if (app.navigation) {
|
||||||
|
this.syncAppNavigation(app.navigation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveLink(url, title, roleId) {
|
async saveLink(url: string, title: string, roleId: string) {
|
||||||
const navigation = get(this.store)
|
const navigation = get(this.store)
|
||||||
let links = [...(navigation?.links ?? [])]
|
let links: AppNavigationLink[] = [...(navigation?.links ?? [])]
|
||||||
|
|
||||||
// Skip if we have an identical link
|
// Skip if we have an identical link
|
||||||
if (links.find(link => link.url === url && link.text === title)) {
|
if (links.find(link => link.url === url && link.text === title)) {
|
||||||
|
@ -60,7 +57,7 @@ export class NavigationStore extends BudiStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteLink(urls) {
|
async deleteLink(urls: string[] | string) {
|
||||||
const navigation = get(this.store)
|
const navigation = get(this.store)
|
||||||
let links = navigation?.links
|
let links = navigation?.links
|
||||||
if (!links?.length) {
|
if (!links?.length) {
|
||||||
|
@ -86,7 +83,7 @@ export class NavigationStore extends BudiStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
syncMetadata(metadata) {
|
syncMetadata(metadata: UIObject) {
|
||||||
const { navigation } = metadata
|
const { navigation } = metadata
|
||||||
this.syncAppNavigation(navigation)
|
this.syncAppNavigation(navigation)
|
||||||
}
|
}
|
|
@ -1,80 +0,0 @@
|
||||||
import { writable, get } from "svelte/store"
|
|
||||||
|
|
||||||
const INITIAL_PREVIEW_STATE = {
|
|
||||||
previewDevice: "desktop",
|
|
||||||
previewEventHandler: null,
|
|
||||||
showPreview: false,
|
|
||||||
selectedComponentContext: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createPreviewStore = () => {
|
|
||||||
const store = writable({
|
|
||||||
...INITIAL_PREVIEW_STATE,
|
|
||||||
})
|
|
||||||
|
|
||||||
const setDevice = device => {
|
|
||||||
store.update(state => {
|
|
||||||
state.previewDevice = device
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Potential evt names "eject-block", "dragging-new-component"
|
|
||||||
const sendEvent = (name, payload) => {
|
|
||||||
const { previewEventHandler } = get(store)
|
|
||||||
previewEventHandler?.(name, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
const registerEventHandler = handler => {
|
|
||||||
store.update(state => {
|
|
||||||
state.previewEventHandler = handler
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const startDrag = component => {
|
|
||||||
sendEvent("dragging-new-component", {
|
|
||||||
dragging: true,
|
|
||||||
component,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopDrag = () => {
|
|
||||||
sendEvent("dragging-new-component", {
|
|
||||||
dragging: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//load preview?
|
|
||||||
const showPreview = isVisible => {
|
|
||||||
store.update(state => {
|
|
||||||
state.showPreview = isVisible
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const setSelectedComponentContext = context => {
|
|
||||||
store.update(state => {
|
|
||||||
state.selectedComponentContext = context
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestComponentContext = () => {
|
|
||||||
sendEvent("request-context")
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe: store.subscribe,
|
|
||||||
setDevice,
|
|
||||||
sendEvent, //may not be required
|
|
||||||
registerEventHandler,
|
|
||||||
startDrag,
|
|
||||||
stopDrag,
|
|
||||||
showPreview,
|
|
||||||
setSelectedComponentContext,
|
|
||||||
requestComponentContext,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const previewStore = createPreviewStore()
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { BudiStore } from "../BudiStore"
|
||||||
|
|
||||||
|
type PreviewDevice = "desktop" | "tablet" | "mobile"
|
||||||
|
type PreviewEventHandler = (name: string, payload?: any) => void
|
||||||
|
type ComponentContext = Record<string, any>
|
||||||
|
|
||||||
|
interface PreviewState {
|
||||||
|
previewDevice: PreviewDevice
|
||||||
|
previewEventHandler: PreviewEventHandler | null
|
||||||
|
showPreview: boolean
|
||||||
|
selectedComponentContext: ComponentContext | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL_PREVIEW_STATE: PreviewState = {
|
||||||
|
previewDevice: "desktop",
|
||||||
|
previewEventHandler: null,
|
||||||
|
showPreview: false,
|
||||||
|
selectedComponentContext: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PreviewStore extends BudiStore<PreviewState> {
|
||||||
|
constructor() {
|
||||||
|
super(INITIAL_PREVIEW_STATE)
|
||||||
|
|
||||||
|
this.setDevice = this.setDevice.bind(this)
|
||||||
|
this.sendEvent = this.sendEvent.bind(this)
|
||||||
|
this.registerEventHandler = this.registerEventHandler.bind(this)
|
||||||
|
this.startDrag = this.startDrag.bind(this)
|
||||||
|
this.stopDrag = this.stopDrag.bind(this)
|
||||||
|
this.showPreview = this.showPreview.bind(this)
|
||||||
|
this.setSelectedComponentContext =
|
||||||
|
this.setSelectedComponentContext.bind(this)
|
||||||
|
this.requestComponentContext = this.requestComponentContext.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
setDevice(device: PreviewDevice) {
|
||||||
|
this.update(state => ({
|
||||||
|
...state,
|
||||||
|
previewDevice: device,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Potential evt names "eject-block", "dragging-new-component"
|
||||||
|
sendEvent(name: string, payload?: any) {
|
||||||
|
const { previewEventHandler } = get(this.store)
|
||||||
|
previewEventHandler?.(name, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEventHandler(handler: PreviewEventHandler) {
|
||||||
|
this.update(state => ({
|
||||||
|
...state,
|
||||||
|
previewEventHandler: handler,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
startDrag(component: any) {
|
||||||
|
this.sendEvent("dragging-new-component", {
|
||||||
|
dragging: true,
|
||||||
|
component,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stopDrag() {
|
||||||
|
this.sendEvent("dragging-new-component", {
|
||||||
|
dragging: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//load preview?
|
||||||
|
showPreview(isVisible: boolean) {
|
||||||
|
this.update(state => ({
|
||||||
|
...state,
|
||||||
|
showPreview: isVisible,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedComponentContext(context: ComponentContext) {
|
||||||
|
this.update(state => ({
|
||||||
|
...state,
|
||||||
|
selectedComponentContext: context,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
requestComponentContext() {
|
||||||
|
this.sendEvent("request-context")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const previewStore = new PreviewStore()
|
|
@ -1,13 +1,16 @@
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
import { appsStore } from "@/stores/portal/apps"
|
import { appsStore } from "@/stores/portal/apps"
|
||||||
import { deploymentStore } from "./deployments"
|
import { deploymentStore } from "./deployments"
|
||||||
import { derived } from "svelte/store"
|
import { derived, type Readable } from "svelte/store"
|
||||||
|
import { DeploymentProgressResponse, DeploymentStatus } from "@budibase/types"
|
||||||
|
|
||||||
export const appPublished = derived(
|
export const appPublished: Readable<boolean> = derived(
|
||||||
[appStore, appsStore, deploymentStore],
|
[appStore, appsStore, deploymentStore],
|
||||||
([$appStore, $appsStore, $deploymentStore]) => {
|
([$appStore, $appsStore, $deploymentStore]) => {
|
||||||
const app = $appsStore.apps.find(app => app.devId === $appStore.appId)
|
const app = $appsStore.apps.find(app => app.devId === $appStore.appId)
|
||||||
const deployments = $deploymentStore.filter(x => x.status === "SUCCESS")
|
const deployments = $deploymentStore.filter(
|
||||||
|
(x: DeploymentProgressResponse) => x.status === DeploymentStatus.SUCCESS
|
||||||
|
)
|
||||||
return app?.status === "published" && deployments.length > 0
|
return app?.status === "published" && deployments.length > 0
|
||||||
}
|
}
|
||||||
)
|
)
|
|
@ -1,88 +0,0 @@
|
||||||
import { derived, writable, get } from "svelte/store"
|
|
||||||
import { API } from "@/api"
|
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
|
||||||
|
|
||||||
export function createRolesStore() {
|
|
||||||
const store = writable([])
|
|
||||||
const enriched = derived(store, $store => {
|
|
||||||
return $store.map(role => ({
|
|
||||||
...role,
|
|
||||||
|
|
||||||
// Ensure we have new metadata for all roles
|
|
||||||
uiMetadata: {
|
|
||||||
displayName: role.uiMetadata?.displayName || role.name,
|
|
||||||
color:
|
|
||||||
role.uiMetadata?.color || "var(--spectrum-global-color-magenta-400)",
|
|
||||||
description: role.uiMetadata?.description || "Custom role",
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
function setRoles(roles) {
|
|
||||||
store.set(
|
|
||||||
roles.sort((a, b) => {
|
|
||||||
const priorityA = RoleUtils.getRolePriority(a._id)
|
|
||||||
const priorityB = RoleUtils.getRolePriority(b._id)
|
|
||||||
if (priorityA !== priorityB) {
|
|
||||||
return priorityA > priorityB ? -1 : 1
|
|
||||||
}
|
|
||||||
const nameA = a.uiMetadata?.displayName || a.name
|
|
||||||
const nameB = b.uiMetadata?.displayName || b.name
|
|
||||||
return nameA < nameB ? -1 : 1
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
fetch: async () => {
|
|
||||||
const roles = await API.getRoles()
|
|
||||||
setRoles(roles)
|
|
||||||
},
|
|
||||||
fetchByAppId: async appId => {
|
|
||||||
const { roles } = await API.getRolesForApp(appId)
|
|
||||||
setRoles(roles)
|
|
||||||
},
|
|
||||||
delete: async role => {
|
|
||||||
await API.deleteRole(role._id, role._rev)
|
|
||||||
await actions.fetch()
|
|
||||||
},
|
|
||||||
save: async role => {
|
|
||||||
const savedRole = await API.saveRole(role)
|
|
||||||
await actions.fetch()
|
|
||||||
return savedRole
|
|
||||||
},
|
|
||||||
replace: (roleId, role) => {
|
|
||||||
// Handles external updates of roles
|
|
||||||
if (!roleId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle deletion
|
|
||||||
if (!role) {
|
|
||||||
store.update(state => state.filter(x => x._id !== roleId))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new role
|
|
||||||
const index = get(store).findIndex(x => x._id === role._id)
|
|
||||||
if (index === -1) {
|
|
||||||
store.update(state => [...state, role])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update existing role
|
|
||||||
else if (role) {
|
|
||||||
store.update(state => {
|
|
||||||
state[index] = role
|
|
||||||
return [...state]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe: enriched.subscribe,
|
|
||||||
...actions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const roles = createRolesStore()
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { derived, get, type Writable } from "svelte/store"
|
||||||
|
import { API } from "@/api"
|
||||||
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
import { DerivedBudiStore } from "../BudiStore"
|
||||||
|
import { Role } from "@budibase/types"
|
||||||
|
|
||||||
|
export class RoleStore extends DerivedBudiStore<Role[], Role[]> {
|
||||||
|
constructor() {
|
||||||
|
const makeDerivedStore = (store: Writable<Role[]>) =>
|
||||||
|
derived(store, $store => {
|
||||||
|
return $store.map((role: Role) => ({
|
||||||
|
...role,
|
||||||
|
// Ensure we have new metadata for all roles
|
||||||
|
uiMetadata: {
|
||||||
|
displayName: role.uiMetadata?.displayName || role.name,
|
||||||
|
color:
|
||||||
|
role.uiMetadata?.color ||
|
||||||
|
"var(--spectrum-global-color-magenta-400)",
|
||||||
|
description: role.uiMetadata?.description || "Custom role",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
super([], makeDerivedStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
setRoles = (roles: Role[]) => {
|
||||||
|
this.set(
|
||||||
|
roles.sort((a, b) => {
|
||||||
|
// Ensure we have valid IDs for priority comparison
|
||||||
|
const priorityA = RoleUtils.getRolePriority(a._id)
|
||||||
|
const priorityB = RoleUtils.getRolePriority(b._id)
|
||||||
|
if (priorityA !== priorityB) {
|
||||||
|
return priorityA > priorityB ? -1 : 1
|
||||||
|
}
|
||||||
|
const nameA = a.uiMetadata?.displayName || a.name
|
||||||
|
const nameB = b.uiMetadata?.displayName || b.name
|
||||||
|
return nameA < nameB ? -1 : 1
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch = async () => {
|
||||||
|
const roles = await API.getRoles()
|
||||||
|
this.setRoles(roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchByAppId = async (appId: string) => {
|
||||||
|
const { roles } = await API.getRolesForApp(appId)
|
||||||
|
this.setRoles(roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = async (role: Role) => {
|
||||||
|
if (!role._id || !role._rev) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await API.deleteRole(role._id, role._rev)
|
||||||
|
await this.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
save = async (role: Role) => {
|
||||||
|
const savedRole = await API.saveRole(role)
|
||||||
|
await this.fetch()
|
||||||
|
return savedRole
|
||||||
|
}
|
||||||
|
|
||||||
|
replace = (roleId: string, role?: Role) => {
|
||||||
|
// Handles external updates of roles
|
||||||
|
if (!roleId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle deletion
|
||||||
|
if (!role) {
|
||||||
|
this.update(state => state.filter(x => x._id !== roleId))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new role
|
||||||
|
const index = get(this).findIndex(x => x._id === role._id)
|
||||||
|
if (index === -1) {
|
||||||
|
this.update(state => [...state, role])
|
||||||
|
}
|
||||||
|
// Update existing role
|
||||||
|
else if (role) {
|
||||||
|
this.update(state => {
|
||||||
|
state[index] = role
|
||||||
|
return [...state]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const roles = new RoleStore()
|
|
@ -6,18 +6,29 @@ import { automationStore } from "./automations"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { getSequentialName } from "@/helpers/duplicate"
|
import { getSequentialName } from "@/helpers/duplicate"
|
||||||
|
|
||||||
const initialState = {}
|
interface RowAction {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
tableId: string
|
||||||
|
allowedSources?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export class RowActionStore extends BudiStore {
|
interface RowActionState {
|
||||||
|
[tableId: string]: RowAction[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: RowActionState = {}
|
||||||
|
|
||||||
|
export class RowActionStore extends BudiStore<RowActionState> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(initialState)
|
super(initialState)
|
||||||
}
|
}
|
||||||
|
|
||||||
reset = () => {
|
reset = () => {
|
||||||
this.store.set(initialState)
|
this.set(initialState)
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshRowActions = async sourceId => {
|
refreshRowActions = async (sourceId: string) => {
|
||||||
if (!sourceId) {
|
if (!sourceId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -34,26 +45,30 @@ export class RowActionStore extends BudiStore {
|
||||||
|
|
||||||
// Fetch row actions for this table
|
// Fetch row actions for this table
|
||||||
const res = await API.rowActions.fetch(tableId)
|
const res = await API.rowActions.fetch(tableId)
|
||||||
const actions = Object.values(res || {})
|
const actions = Object.values(res || {}) as RowAction[]
|
||||||
this.update(state => ({
|
this.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
[tableId]: actions,
|
[tableId]: actions,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
createRowAction = async (tableId, viewId, name) => {
|
createRowAction = async (tableId: string, viewId?: string, name?: string) => {
|
||||||
if (!tableId) {
|
if (!tableId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a unique name for this action
|
// Get a unique name for this action
|
||||||
if (!name) {
|
if (!name) {
|
||||||
const existingRowActions = get(this.store)[tableId] || []
|
const existingRowActions = get(this)[tableId] || []
|
||||||
name = getSequentialName(existingRowActions, "New row action ", {
|
name = getSequentialName(existingRowActions, "New row action ", {
|
||||||
getName: x => x.name,
|
getName: x => x.name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
throw new Error("Failed to generate a unique name for the row action")
|
||||||
|
}
|
||||||
|
|
||||||
// Create the action
|
// Create the action
|
||||||
const res = await API.rowActions.create(tableId, name)
|
const res = await API.rowActions.create(tableId, name)
|
||||||
|
|
||||||
|
@ -73,41 +88,35 @@ export class RowActionStore extends BudiStore {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
enableView = async (tableId, rowActionId, viewId) => {
|
enableView = async (tableId: string, rowActionId: string, viewId: string) => {
|
||||||
await API.rowActions.enableView(tableId, rowActionId, viewId)
|
await API.rowActions.enableView(tableId, rowActionId, viewId)
|
||||||
await this.refreshRowActions(tableId)
|
await this.refreshRowActions(tableId)
|
||||||
}
|
}
|
||||||
|
|
||||||
disableView = async (tableId, rowActionId, viewId) => {
|
disableView = async (
|
||||||
|
tableId: string,
|
||||||
|
rowActionId: string,
|
||||||
|
viewId: string
|
||||||
|
) => {
|
||||||
await API.rowActions.disableView(tableId, rowActionId, viewId)
|
await API.rowActions.disableView(tableId, rowActionId, viewId)
|
||||||
await this.refreshRowActions(tableId)
|
await this.refreshRowActions(tableId)
|
||||||
}
|
}
|
||||||
|
|
||||||
rename = async (tableId, rowActionId, name) => {
|
delete = async (tableId: string, rowActionId: string) => {
|
||||||
await API.rowActions.update({
|
|
||||||
tableId,
|
|
||||||
rowActionId,
|
|
||||||
name,
|
|
||||||
})
|
|
||||||
await this.refreshRowActions(tableId)
|
|
||||||
automationStore.actions.fetch()
|
|
||||||
}
|
|
||||||
|
|
||||||
delete = async (tableId, rowActionId) => {
|
|
||||||
await API.rowActions.delete(tableId, rowActionId)
|
await API.rowActions.delete(tableId, rowActionId)
|
||||||
await this.refreshRowActions(tableId)
|
await this.refreshRowActions(tableId)
|
||||||
// We don't need to refresh automations as we can only delete row actions
|
// We don't need to refresh automations as we can only delete row actions
|
||||||
// from the automations store, so we already handle the state update there
|
// from the automations store, so we already handle the state update there
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger = async (sourceId, rowActionId, rowId) => {
|
trigger = async (sourceId: string, rowActionId: string, rowId: string) => {
|
||||||
await API.rowActions.trigger(sourceId, rowActionId, rowId)
|
await API.rowActions.trigger(sourceId, rowActionId, rowId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = new RowActionStore()
|
const store = new RowActionStore()
|
||||||
const derivedStore = derived(store, $store => {
|
const derivedStore = derived<RowActionStore, RowActionState>(store, $store => {
|
||||||
let map = {}
|
const map: RowActionState = {}
|
||||||
|
|
||||||
// Generate an entry for every view as well
|
// Generate an entry for every view as well
|
||||||
Object.keys($store || {}).forEach(tableId => {
|
Object.keys($store || {}).forEach(tableId => {
|
||||||
|
@ -115,7 +124,7 @@ const derivedStore = derived(store, $store => {
|
||||||
map[tableId] = $store[tableId]
|
map[tableId] = $store[tableId]
|
||||||
for (let action of $store[tableId]) {
|
for (let action of $store[tableId]) {
|
||||||
const otherSources = (action.allowedSources || []).filter(
|
const otherSources = (action.allowedSources || []).filter(
|
||||||
sourceId => sourceId !== tableId
|
(sourceId: string) => sourceId !== tableId
|
||||||
)
|
)
|
||||||
for (let source of otherSources) {
|
for (let source of otherSources) {
|
||||||
map[source] ??= []
|
map[source] ??= []
|
|
@ -1,35 +0,0 @@
|
||||||
import { writable, get } from "svelte/store"
|
|
||||||
import { API } from "@/api"
|
|
||||||
import { appStore } from "./app"
|
|
||||||
|
|
||||||
const createsnippets = () => {
|
|
||||||
const store = writable([])
|
|
||||||
|
|
||||||
const syncMetadata = metadata => {
|
|
||||||
store.set(metadata?.snippets || [])
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveSnippet = async updatedSnippet => {
|
|
||||||
const snippets = [
|
|
||||||
...get(store).filter(snippet => snippet.name !== updatedSnippet.name),
|
|
||||||
updatedSnippet,
|
|
||||||
]
|
|
||||||
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
|
|
||||||
syncMetadata(app)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteSnippet = async snippetName => {
|
|
||||||
const snippets = get(store).filter(snippet => snippet.name !== snippetName)
|
|
||||||
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
|
|
||||||
syncMetadata(app)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...store,
|
|
||||||
syncMetadata,
|
|
||||||
saveSnippet,
|
|
||||||
deleteSnippet,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const snippets = createsnippets()
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { API } from "@/api"
|
||||||
|
import { appStore } from "./app"
|
||||||
|
import { BudiStore } from "../BudiStore"
|
||||||
|
import { Snippet, UpdateAppResponse } from "@budibase/types"
|
||||||
|
|
||||||
|
export class SnippetStore extends BudiStore<Snippet[]> {
|
||||||
|
constructor() {
|
||||||
|
super([])
|
||||||
|
}
|
||||||
|
|
||||||
|
syncMetadata = (metadata: UpdateAppResponse) => {
|
||||||
|
this.set(metadata?.snippets || [])
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSnippet = async (updatedSnippet: Snippet) => {
|
||||||
|
const snippets = [
|
||||||
|
...get(this).filter(snippet => snippet.name !== updatedSnippet.name),
|
||||||
|
updatedSnippet,
|
||||||
|
]
|
||||||
|
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
|
||||||
|
this.syncMetadata(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSnippet = async (snippetName: string) => {
|
||||||
|
const snippets = get(this).filter(snippet => snippet.name !== snippetName)
|
||||||
|
const app = await API.saveAppMetadata(get(appStore).appId, { snippets })
|
||||||
|
this.syncMetadata(app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const snippets = new SnippetStore()
|
|
@ -1,58 +0,0 @@
|
||||||
import { writable, get } from "svelte/store"
|
|
||||||
import { API } from "@/api"
|
|
||||||
import { ensureValidTheme, DefaultAppTheme } from "@budibase/shared-core"
|
|
||||||
|
|
||||||
export const createThemeStore = () => {
|
|
||||||
const store = writable({
|
|
||||||
theme: DefaultAppTheme,
|
|
||||||
customTheme: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
const syncAppTheme = app => {
|
|
||||||
store.update(state => {
|
|
||||||
const theme = ensureValidTheme(app.theme, DefaultAppTheme)
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
theme,
|
|
||||||
customTheme: app.customTheme,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = async (theme, appId) => {
|
|
||||||
const app = await API.saveAppMetadata(appId, { theme })
|
|
||||||
store.update(state => {
|
|
||||||
state.theme = app.theme
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveCustom = async (theme, appId) => {
|
|
||||||
const updated = { ...get(store).customTheme, ...theme }
|
|
||||||
const app = await API.saveAppMetadata(appId, { customTheme: updated })
|
|
||||||
store.update(state => {
|
|
||||||
state.customTheme = app.customTheme
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncMetadata = metadata => {
|
|
||||||
const { theme, customTheme } = metadata
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
theme: ensureValidTheme(theme, DefaultAppTheme),
|
|
||||||
customTheme,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe: store.subscribe,
|
|
||||||
update: store.update,
|
|
||||||
syncMetadata,
|
|
||||||
syncAppTheme,
|
|
||||||
save,
|
|
||||||
saveCustom,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const themeStore = createThemeStore()
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { API } from "@/api"
|
||||||
|
import { BudiStore } from "../BudiStore"
|
||||||
|
import { ensureValidTheme, DefaultAppTheme } from "@budibase/shared-core"
|
||||||
|
import { App, UpdateAppResponse, Theme, AppCustomTheme } from "@budibase/types"
|
||||||
|
|
||||||
|
interface ThemeState {
|
||||||
|
theme: Theme
|
||||||
|
customTheme: AppCustomTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ThemeStore extends BudiStore<ThemeState> {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
theme: DefaultAppTheme,
|
||||||
|
customTheme: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
syncAppTheme = (app: App) => {
|
||||||
|
this.update(state => {
|
||||||
|
const theme = ensureValidTheme(app.theme, DefaultAppTheme)
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
theme,
|
||||||
|
customTheme: app.customTheme || {},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
save = async (theme: Theme, appId: string) => {
|
||||||
|
const app = await API.saveAppMetadata(appId, { theme })
|
||||||
|
this.update(state => ({
|
||||||
|
...state,
|
||||||
|
theme: ensureValidTheme(app.theme, DefaultAppTheme),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCustom = async (theme: Partial<AppCustomTheme>, appId: string) => {
|
||||||
|
const updated = { ...get(this).customTheme, ...theme }
|
||||||
|
const app = await API.saveAppMetadata(appId, { customTheme: updated })
|
||||||
|
this.update(state => ({
|
||||||
|
...state,
|
||||||
|
customTheme: app.customTheme || {},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
syncMetadata = (metadata: UpdateAppResponse) => {
|
||||||
|
const { theme, customTheme } = metadata
|
||||||
|
this.update(state => ({
|
||||||
|
...state,
|
||||||
|
theme: ensureValidTheme(theme, DefaultAppTheme),
|
||||||
|
customTheme: customTheme || {},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const themeStore = new ThemeStore()
|
|
@ -1,62 +0,0 @@
|
||||||
import { writable, get, derived } from "svelte/store"
|
|
||||||
|
|
||||||
export const createUserStore = () => {
|
|
||||||
const store = writable([])
|
|
||||||
|
|
||||||
const init = users => {
|
|
||||||
store.set(users)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateUser = user => {
|
|
||||||
const $users = get(store)
|
|
||||||
if (!$users.some(x => x.sessionId === user.sessionId)) {
|
|
||||||
store.set([...$users, user])
|
|
||||||
} else {
|
|
||||||
store.update(state => {
|
|
||||||
const index = state.findIndex(x => x.sessionId === user.sessionId)
|
|
||||||
state[index] = user
|
|
||||||
return state.slice()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeUser = sessionId => {
|
|
||||||
store.update(state => {
|
|
||||||
return state.filter(x => x.sessionId !== sessionId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
store.set([])
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...store,
|
|
||||||
actions: {
|
|
||||||
init,
|
|
||||||
updateUser,
|
|
||||||
removeUser,
|
|
||||||
reset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const userStore = createUserStore()
|
|
||||||
|
|
||||||
export const userSelectedResourceMap = derived(userStore, $userStore => {
|
|
||||||
let map = {}
|
|
||||||
$userStore.forEach(user => {
|
|
||||||
const resource = user.builderMetadata?.selectedResourceId
|
|
||||||
if (resource) {
|
|
||||||
if (!map[resource]) {
|
|
||||||
map[resource] = []
|
|
||||||
}
|
|
||||||
map[resource].push(user)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return map
|
|
||||||
})
|
|
||||||
|
|
||||||
export const isOnlyUser = derived(userStore, $userStore => {
|
|
||||||
return $userStore.length < 2
|
|
||||||
})
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { get, derived } from "svelte/store"
|
||||||
|
import { BudiStore } from "../BudiStore"
|
||||||
|
import { UIUser } from "@budibase/types"
|
||||||
|
|
||||||
|
export class UserStore extends BudiStore<UIUser[]> {
|
||||||
|
constructor() {
|
||||||
|
super([])
|
||||||
|
}
|
||||||
|
|
||||||
|
init(users: UIUser[]) {
|
||||||
|
this.set(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser(user: UIUser) {
|
||||||
|
const $users = get(this)
|
||||||
|
if (!$users.some(x => x.sessionId === user.sessionId)) {
|
||||||
|
this.set([...$users, user])
|
||||||
|
} else {
|
||||||
|
this.update(state => {
|
||||||
|
const index = state.findIndex(x => x.sessionId === user.sessionId)
|
||||||
|
state[index] = user
|
||||||
|
return state.slice()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUser(sessionId: string) {
|
||||||
|
this.update(state => {
|
||||||
|
return state.filter(x => x.sessionId !== sessionId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.set([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userStore = new UserStore()
|
||||||
|
|
||||||
|
export const userSelectedResourceMap = derived(
|
||||||
|
userStore,
|
||||||
|
($userStore): Record<string, UIUser[]> => {
|
||||||
|
let map: Record<string, UIUser[]> = {}
|
||||||
|
$userStore.forEach(user => {
|
||||||
|
const resource = user.builderMetadata?.selectedResourceId
|
||||||
|
if (resource) {
|
||||||
|
if (!map[resource]) {
|
||||||
|
map[resource] = []
|
||||||
|
}
|
||||||
|
map[resource].push(user)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const isOnlyUser = derived(userStore, $userStore => {
|
||||||
|
return $userStore.length < 2
|
||||||
|
})
|
|
@ -1,95 +0,0 @@
|
||||||
import { createWebsocket } from "@budibase/frontend-core"
|
|
||||||
import {
|
|
||||||
automationStore,
|
|
||||||
userStore,
|
|
||||||
appStore,
|
|
||||||
themeStore,
|
|
||||||
navigationStore,
|
|
||||||
deploymentStore,
|
|
||||||
snippets,
|
|
||||||
datasources,
|
|
||||||
tables,
|
|
||||||
roles,
|
|
||||||
} from "@/stores/builder"
|
|
||||||
import { get } from "svelte/store"
|
|
||||||
import { auth, appsStore } from "@/stores/portal"
|
|
||||||
import { screenStore } from "./screens"
|
|
||||||
import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core"
|
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export const createBuilderWebsocket = appId => {
|
|
||||||
const socket = createWebsocket("/socket/builder")
|
|
||||||
|
|
||||||
// Built-in events
|
|
||||||
socket.on("connect", () => {
|
|
||||||
socket.emit(BuilderSocketEvent.SelectApp, { appId }, ({ users }) => {
|
|
||||||
userStore.actions.init(users)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
socket.on("connect_error", err => {
|
|
||||||
console.error("Failed to connect to builder websocket:", err.message)
|
|
||||||
})
|
|
||||||
socket.on("disconnect", () => {
|
|
||||||
userStore.actions.reset()
|
|
||||||
})
|
|
||||||
|
|
||||||
// User events
|
|
||||||
socket.onOther(SocketEvent.UserUpdate, ({ user }) => {
|
|
||||||
userStore.actions.updateUser(user)
|
|
||||||
})
|
|
||||||
socket.onOther(SocketEvent.UserDisconnect, ({ sessionId }) => {
|
|
||||||
userStore.actions.removeUser(sessionId)
|
|
||||||
})
|
|
||||||
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
|
|
||||||
if (userId === get(auth)?.user?._id) {
|
|
||||||
appStore.update(state => ({
|
|
||||||
...state,
|
|
||||||
hasLock: true,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Data section events
|
|
||||||
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
|
|
||||||
tables.replaceTable(id, table)
|
|
||||||
})
|
|
||||||
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
|
|
||||||
datasources.replaceDatasource(id, datasource)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Role events
|
|
||||||
socket.onOther(BuilderSocketEvent.RoleChange, ({ id, role }) => {
|
|
||||||
roles.replace(id, role)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Design section events
|
|
||||||
socket.onOther(BuilderSocketEvent.ScreenChange, ({ id, screen }) => {
|
|
||||||
screenStore.replace(id, screen)
|
|
||||||
})
|
|
||||||
|
|
||||||
// App events
|
|
||||||
socket.onOther(BuilderSocketEvent.AppMetadataChange, ({ metadata }) => {
|
|
||||||
appStore.syncMetadata(metadata)
|
|
||||||
themeStore.syncMetadata(metadata)
|
|
||||||
navigationStore.syncMetadata(metadata)
|
|
||||||
snippets.syncMetadata(metadata)
|
|
||||||
})
|
|
||||||
socket.onOther(
|
|
||||||
BuilderSocketEvent.AppPublishChange,
|
|
||||||
async ({ user, published }) => {
|
|
||||||
await appsStore.load()
|
|
||||||
if (published) {
|
|
||||||
await deploymentStore.load()
|
|
||||||
}
|
|
||||||
const verb = published ? "published" : "unpublished"
|
|
||||||
notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Automation events
|
|
||||||
socket.onOther(BuilderSocketEvent.AutomationChange, ({ id, automation }) => {
|
|
||||||
automationStore.actions.replace(id, automation)
|
|
||||||
})
|
|
||||||
|
|
||||||
return socket
|
|
||||||
}
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { createWebsocket } from "@budibase/frontend-core"
|
||||||
|
import {
|
||||||
|
automationStore,
|
||||||
|
userStore,
|
||||||
|
appStore,
|
||||||
|
themeStore,
|
||||||
|
navigationStore,
|
||||||
|
deploymentStore,
|
||||||
|
snippets,
|
||||||
|
datasources,
|
||||||
|
tables,
|
||||||
|
roles,
|
||||||
|
} from "@/stores/builder"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { auth, appsStore } from "@/stores/portal"
|
||||||
|
import { screenStore } from "./screens"
|
||||||
|
import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import { Automation, Datasource, Role, Table, UIUser } from "@budibase/types"
|
||||||
|
|
||||||
|
export const createBuilderWebsocket = (appId: string) => {
|
||||||
|
const socket = createWebsocket("/socket/builder")
|
||||||
|
|
||||||
|
// Built-in events
|
||||||
|
socket.on("connect", () => {
|
||||||
|
socket.emit(
|
||||||
|
BuilderSocketEvent.SelectApp,
|
||||||
|
{ appId },
|
||||||
|
({ users }: { users: UIUser[] }) => {
|
||||||
|
userStore.init(users)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
socket.on("connect_error", err => {
|
||||||
|
console.error("Failed to connect to builder websocket:", err.message)
|
||||||
|
})
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
userStore.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
// User events
|
||||||
|
socket.onOther(SocketEvent.UserUpdate, ({ user }: { user: UIUser }) => {
|
||||||
|
userStore.updateUser(user)
|
||||||
|
})
|
||||||
|
socket.onOther(
|
||||||
|
SocketEvent.UserDisconnect,
|
||||||
|
({ sessionId }: { sessionId: string }) => {
|
||||||
|
userStore.removeUser(sessionId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
socket.onOther(
|
||||||
|
BuilderSocketEvent.LockTransfer,
|
||||||
|
({ userId }: { userId: string }) => {
|
||||||
|
if (userId === get(auth)?.user?._id) {
|
||||||
|
appStore.update(state => ({
|
||||||
|
...state,
|
||||||
|
hasLock: true,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Data section events
|
||||||
|
socket.onOther(
|
||||||
|
BuilderSocketEvent.TableChange,
|
||||||
|
({ id, table }: { id: string; table: Table }) => {
|
||||||
|
tables.replaceTable(id, table)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
socket.onOther(
|
||||||
|
BuilderSocketEvent.DatasourceChange,
|
||||||
|
({ id, datasource }: { id: string; datasource: Datasource }) => {
|
||||||
|
datasources.replaceDatasource(id, datasource)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Role events
|
||||||
|
socket.onOther(
|
||||||
|
BuilderSocketEvent.RoleChange,
|
||||||
|
({ id, role }: { id: string; role: Role }) => {
|
||||||
|
roles.replace(id, role)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Design section events
|
||||||
|
socket.onOther(
|
||||||
|
BuilderSocketEvent.ScreenChange,
|
||||||
|
({ id, screen }: { id: string; screen: Screen }) => {
|
||||||
|
screenStore.replace(id, screen)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// App events
|
||||||
|
socket.onOther(
|
||||||
|
BuilderSocketEvent.AppMetadataChange,
|
||||||
|
({ metadata }: { metadata: any }) => {
|
||||||
|
appStore.syncMetadata(metadata)
|
||||||
|
themeStore.syncMetadata(metadata)
|
||||||
|
navigationStore.syncMetadata(metadata)
|
||||||
|
snippets.syncMetadata(metadata)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
socket.onOther(
|
||||||
|
BuilderSocketEvent.AppPublishChange,
|
||||||
|
async ({ user, published }: { user: UIUser; published: boolean }) => {
|
||||||
|
await appsStore.load()
|
||||||
|
if (published) {
|
||||||
|
await deploymentStore.load()
|
||||||
|
}
|
||||||
|
const verb = published ? "published" : "unpublished"
|
||||||
|
notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Automation events
|
||||||
|
socket.onOther(
|
||||||
|
BuilderSocketEvent.AutomationChange,
|
||||||
|
({ id, automation }: { id: string; automation: Automation }) => {
|
||||||
|
automationStore.actions.replace(id, automation)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return socket
|
||||||
|
}
|
|
@ -1,19 +1,8 @@
|
||||||
import { derived, Readable } from "svelte/store"
|
import { derived, Readable } from "svelte/store"
|
||||||
import { auth } from "@/stores/portal"
|
import { auth } from "@/stores/portal"
|
||||||
|
import { FeatureFlags, FeatureFlagDefaults } from "@budibase/types"
|
||||||
|
|
||||||
export const INITIAL_FEATUREFLAG_STATE = {
|
export const featureFlags: Readable<FeatureFlags> = derived(auth, $auth => ({
|
||||||
SQS: false,
|
...FeatureFlagDefaults,
|
||||||
DEFAULT_VALUES: false,
|
...($auth?.user?.flags || {}),
|
||||||
BUDIBASE_AI: false,
|
}))
|
||||||
AI_CUSTOM_CONFIGS: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const featureFlags: Readable<Record<string, any>> = derived(
|
|
||||||
[auth],
|
|
||||||
([$auth]) => {
|
|
||||||
return {
|
|
||||||
...INITIAL_FEATUREFLAG_STATE,
|
|
||||||
...($auth?.user?.flags || {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
|
@ -47,9 +47,12 @@ class GroupStore extends BudiStore<UserGroup[]> {
|
||||||
|
|
||||||
async delete(group: UserGroup) {
|
async delete(group: UserGroup) {
|
||||||
await API.deleteGroup(group._id!, group._rev!)
|
await API.deleteGroup(group._id!, group._rev!)
|
||||||
this.update(state => {
|
this.update(groups => {
|
||||||
state = state.filter(state => state._id !== group._id)
|
const index = groups.findIndex(g => g._id === group._id)
|
||||||
return state
|
if (index !== -1) {
|
||||||
|
groups.splice(index, 1)
|
||||||
|
}
|
||||||
|
return groups
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -227,7 +227,7 @@ class LicensingStore extends BudiStore<LicensingState> {
|
||||||
MonthlyQuotaName.AUTOMATIONS,
|
MonthlyQuotaName.AUTOMATIONS,
|
||||||
].reduce((acc: MonthlyMetrics, key) => {
|
].reduce((acc: MonthlyMetrics, key) => {
|
||||||
const limit = license.quotas.usage.monthly[key].value
|
const limit = license.quotas.usage.monthly[key].value
|
||||||
const used = (usage.monthly.current?.[key] || 0 / limit) * 100
|
const used = ((usage.monthly.current?.[key] || 0) / limit) * 100
|
||||||
acc[key] = limit > -1 ? Math.floor(used) : -1
|
acc[key] = limit > -1 ? Math.floor(used) : -1
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
@ -236,7 +236,7 @@ class LicensingStore extends BudiStore<LicensingState> {
|
||||||
const staticMetrics = [StaticQuotaName.APPS, StaticQuotaName.ROWS].reduce(
|
const staticMetrics = [StaticQuotaName.APPS, StaticQuotaName.ROWS].reduce(
|
||||||
(acc: StaticMetrics, key) => {
|
(acc: StaticMetrics, key) => {
|
||||||
const limit = license.quotas.usage.static[key].value
|
const limit = license.quotas.usage.static[key].value
|
||||||
const used = (usage.usageQuota[key] || 0 / limit) * 100
|
const used = ((usage.usageQuota[key] || 0) / limit) * 100
|
||||||
acc[key] = limit > -1 ? Math.floor(used) : -1
|
acc[key] = limit > -1 ? Math.floor(used) : -1
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "*",
|
"@budibase/backend-core": "*",
|
||||||
|
"@budibase/pouchdb-replication-stream": "1.2.11",
|
||||||
"@budibase/string-templates": "*",
|
"@budibase/string-templates": "*",
|
||||||
"@budibase/types": "*",
|
"@budibase/types": "*",
|
||||||
"chalk": "4.1.0",
|
"chalk": "4.1.0",
|
||||||
|
@ -28,9 +29,9 @@
|
||||||
"inquirer": "8.0.0",
|
"inquirer": "8.0.0",
|
||||||
"lookpath": "1.1.0",
|
"lookpath": "1.1.0",
|
||||||
"node-fetch": "2.6.7",
|
"node-fetch": "2.6.7",
|
||||||
|
"open": "8.4.2",
|
||||||
"posthog-node": "4.0.1",
|
"posthog-node": "4.0.1",
|
||||||
"pouchdb": "7.3.0",
|
"pouchdb": "7.3.0",
|
||||||
"@budibase/pouchdb-replication-stream": "1.2.11",
|
|
||||||
"randomstring": "1.1.5",
|
"randomstring": "1.1.5",
|
||||||
"tar": "6.2.1",
|
"tar": "6.2.1",
|
||||||
"yaml": "^2.1.1"
|
"yaml": "^2.1.1"
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { info, success } from "../utils"
|
||||||
import * as makeFiles from "./makeFiles"
|
import * as makeFiles from "./makeFiles"
|
||||||
import compose from "docker-compose"
|
import compose from "docker-compose"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
|
import { confirmation } from "../questions"
|
||||||
|
const open = require("open")
|
||||||
|
|
||||||
export async function start() {
|
export async function start() {
|
||||||
await checkDockerConfigured()
|
await checkDockerConfigured()
|
||||||
|
@ -22,6 +24,9 @@ export async function start() {
|
||||||
// need to log as it makes it more clear
|
// need to log as it makes it more clear
|
||||||
await compose.upAll({ cwd: "./", log: true })
|
await compose.upAll({ cwd: "./", log: true })
|
||||||
})
|
})
|
||||||
|
if (await confirmation(`Do you wish to open http://localhost:${port} ?`)) {
|
||||||
|
await open(`http://localhost:${port}`)
|
||||||
|
}
|
||||||
console.log(
|
console.log(
|
||||||
success(
|
success(
|
||||||
`Services started, please go to http://localhost:${port} for next steps.`
|
`Services started, please go to http://localhost:${port} for next steps.`
|
||||||
|
|
|
@ -66,10 +66,9 @@ export const patchAPI = API => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fetchRelationshipData = API.fetchRelationshipData
|
const fetchRelationshipData = API.fetchRelationshipData
|
||||||
API.fetchRelationshipData = async params => {
|
API.fetchRelationshipData = async (sourceId, rowId, fieldName) => {
|
||||||
const tableId = params?.tableId
|
const rows = await fetchRelationshipData(sourceId, rowId, fieldName)
|
||||||
const rows = await fetchRelationshipData(params)
|
return await enrichRows(rows, sourceId)
|
||||||
return await enrichRows(rows, tableId)
|
|
||||||
}
|
}
|
||||||
const fetchTableData = API.fetchTableData
|
const fetchTableData = API.fetchTableData
|
||||||
API.fetchTableData = async tableId => {
|
API.fetchTableData = async tableId => {
|
||||||
|
@ -85,9 +84,9 @@ export const patchAPI = API => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fetchViewData = API.fetchViewData
|
const fetchViewData = API.fetchViewData
|
||||||
API.fetchViewData = async params => {
|
API.fetchViewData = async (viewName, params) => {
|
||||||
const tableId = params?.tableId
|
const tableId = params?.tableId
|
||||||
const rows = await fetchViewData(params)
|
const rows = await fetchViewData(viewName, params)
|
||||||
return await enrichRows(rows, tableId)
|
return await enrichRows(rows, tableId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -216,11 +216,11 @@ const deleteRowHandler = async action => {
|
||||||
const triggerAutomationHandler = async action => {
|
const triggerAutomationHandler = async action => {
|
||||||
const { fields, notificationOverride, timeout } = action.parameters
|
const { fields, notificationOverride, timeout } = action.parameters
|
||||||
try {
|
try {
|
||||||
const result = await API.triggerAutomation({
|
const result = await API.triggerAutomation(
|
||||||
automationId: action.parameters.automationId,
|
action.parameters.automationId,
|
||||||
fields,
|
fields,
|
||||||
timeout,
|
timeout
|
||||||
})
|
)
|
||||||
|
|
||||||
// Value will exist if automation is synchronous, so return it.
|
// Value will exist if automation is synchronous, so return it.
|
||||||
if (result.value) {
|
if (result.value) {
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
: RelationshipType.MANY_TO_MANY,
|
: RelationshipType.MANY_TO_MANY,
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchFunction(searchParams) {
|
async function searchFunction(_tableId, searchParams) {
|
||||||
if (
|
if (
|
||||||
subtype !== BBReferenceFieldSubType.USER &&
|
subtype !== BBReferenceFieldSubType.USER &&
|
||||||
subtype !== BBReferenceFieldSubType.USERS
|
subtype !== BBReferenceFieldSubType.USERS
|
||||||
|
|
|
@ -8,9 +8,15 @@ export default class ViewFetch extends DataFetch {
|
||||||
async getData() {
|
async getData() {
|
||||||
const { datasource } = this.options
|
const { datasource } = this.options
|
||||||
try {
|
try {
|
||||||
const res = await this.API.fetchViewData(datasource.name)
|
const res = await this.API.fetchViewData(datasource.name, {
|
||||||
|
calculation: datasource.calculation,
|
||||||
|
field: datasource.field,
|
||||||
|
groupBy: datasource.groupBy,
|
||||||
|
tableId: datasource.tableId,
|
||||||
|
})
|
||||||
return { rows: res || [] }
|
return { rows: res || [] }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
return { rows: [] }
|
return { rows: [] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
import { io } from "socket.io-client"
|
import { io, Socket } from "socket.io-client"
|
||||||
import { SocketEvent, SocketSessionTTL } from "@budibase/shared-core"
|
import { SocketEvent, SocketSessionTTL } from "@budibase/shared-core"
|
||||||
import { APISessionID } from "../api"
|
import { APISessionID } from "../api"
|
||||||
|
|
||||||
const DefaultOptions = {
|
const DefaultOptions = {
|
||||||
heartbeat: true,
|
heartbeat: true,
|
||||||
}
|
}
|
||||||
|
export interface ExtendedSocket extends Socket {
|
||||||
|
onOther: (event: string, callback: (data: any) => void) => void
|
||||||
|
}
|
||||||
|
|
||||||
export const createWebsocket = (path, options = DefaultOptions) => {
|
export const createWebsocket = (
|
||||||
|
path: string,
|
||||||
|
options = DefaultOptions
|
||||||
|
): ExtendedSocket => {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
throw "A websocket path must be provided"
|
throw "A websocket path must be provided"
|
||||||
}
|
}
|
||||||
|
@ -32,10 +38,10 @@ export const createWebsocket = (path, options = DefaultOptions) => {
|
||||||
// Disable polling and rely on websocket only, as HTTP transport
|
// Disable polling and rely on websocket only, as HTTP transport
|
||||||
// will only work with sticky sessions which we don't have
|
// will only work with sticky sessions which we don't have
|
||||||
transports: ["websocket"],
|
transports: ["websocket"],
|
||||||
})
|
}) as ExtendedSocket
|
||||||
|
|
||||||
// Set up a heartbeat that's half of the session TTL
|
// Set up a heartbeat that's half of the session TTL
|
||||||
let interval
|
let interval: NodeJS.Timeout | undefined
|
||||||
if (heartbeat) {
|
if (heartbeat) {
|
||||||
interval = setInterval(() => {
|
interval = setInterval(() => {
|
||||||
socket.emit(SocketEvent.Heartbeat)
|
socket.emit(SocketEvent.Heartbeat)
|
||||||
|
@ -48,7 +54,7 @@ export const createWebsocket = (path, options = DefaultOptions) => {
|
||||||
|
|
||||||
// Helper utility to ignore events that were triggered due to API
|
// Helper utility to ignore events that were triggered due to API
|
||||||
// changes made by us
|
// changes made by us
|
||||||
socket.onOther = (event, callback) => {
|
socket.onOther = (event: string, callback: (data: any) => void) => {
|
||||||
socket.on(event, data => {
|
socket.on(event, data => {
|
||||||
if (data?.apiSessionId !== APISessionID) {
|
if (data?.apiSessionId !== APISessionID) {
|
||||||
callback(data)
|
callback(data)
|
|
@ -1 +1 @@
|
||||||
Subproject commit ae786121d923449b0ad5fcbd123d0a9fec28f65e
|
Subproject commit 32d84f109d4edc526145472a7446327312151442
|
|
@ -163,9 +163,9 @@ export async function finaliseRow(
|
||||||
contextRows: [enrichedRow],
|
contextRows: [enrichedRow],
|
||||||
})
|
})
|
||||||
const aiEnabled =
|
const aiEnabled =
|
||||||
((await features.flags.isEnabled(FeatureFlag.BUDIBASE_AI)) &&
|
((await features.isEnabled(FeatureFlag.BUDIBASE_AI)) &&
|
||||||
(await pro.features.isBudibaseAIEnabled())) ||
|
(await pro.features.isBudibaseAIEnabled())) ||
|
||||||
((await features.flags.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) &&
|
((await features.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) &&
|
||||||
(await pro.features.isAICustomConfigsEnabled()))
|
(await pro.features.isAICustomConfigsEnabled()))
|
||||||
if (aiEnabled) {
|
if (aiEnabled) {
|
||||||
row = await processAIColumns(table, row, {
|
row = await processAIColumns(table, row, {
|
||||||
|
|
|
@ -105,13 +105,13 @@ if (env.SELF_HOSTED) {
|
||||||
export async function getActionDefinitions(): Promise<
|
export async function getActionDefinitions(): Promise<
|
||||||
Record<keyof typeof AutomationActionStepId, AutomationStepDefinition>
|
Record<keyof typeof AutomationActionStepId, AutomationStepDefinition>
|
||||||
> {
|
> {
|
||||||
if (await features.flags.isEnabled(FeatureFlag.AUTOMATION_BRANCHING)) {
|
if (await features.isEnabled(FeatureFlag.AUTOMATION_BRANCHING)) {
|
||||||
BUILTIN_ACTION_DEFINITIONS["BRANCH"] = branch.definition
|
BUILTIN_ACTION_DEFINITIONS["BRANCH"] = branch.definition
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
env.SELF_HOSTED ||
|
env.SELF_HOSTED ||
|
||||||
(await features.flags.isEnabled(FeatureFlag.BUDIBASE_AI)) ||
|
(await features.isEnabled(FeatureFlag.BUDIBASE_AI)) ||
|
||||||
(await features.flags.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS))
|
(await features.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS))
|
||||||
) {
|
) {
|
||||||
BUILTIN_ACTION_DEFINITIONS["OPENAI"] = openai.definition
|
BUILTIN_ACTION_DEFINITIONS["OPENAI"] = openai.definition
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,10 +100,10 @@ export async function run({
|
||||||
try {
|
try {
|
||||||
let response
|
let response
|
||||||
const customConfigsEnabled =
|
const customConfigsEnabled =
|
||||||
(await features.flags.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) &&
|
(await features.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) &&
|
||||||
(await pro.features.isAICustomConfigsEnabled())
|
(await pro.features.isAICustomConfigsEnabled())
|
||||||
const budibaseAIEnabled =
|
const budibaseAIEnabled =
|
||||||
(await features.flags.isEnabled(FeatureFlag.BUDIBASE_AI)) &&
|
(await features.isEnabled(FeatureFlag.BUDIBASE_AI)) &&
|
||||||
(await pro.features.isBudibaseAIEnabled())
|
(await pro.features.isBudibaseAIEnabled())
|
||||||
|
|
||||||
let llmWrapper
|
let llmWrapper
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { fromZodError } from "zod-validation-error"
|
||||||
function validate(schema: AnyZodObject, property: "body" | "params") {
|
function validate(schema: AnyZodObject, property: "body" | "params") {
|
||||||
// Return a Koa middleware function
|
// Return a Koa middleware function
|
||||||
return async (ctx: Ctx, next: any) => {
|
return async (ctx: Ctx, next: any) => {
|
||||||
if (!(await features.flags.isEnabled(FeatureFlag.USE_ZOD_VALIDATOR))) {
|
if (!(await features.isEnabled(FeatureFlag.USE_ZOD_VALIDATOR))) {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -174,7 +174,9 @@ class QueryRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
// needs to an array for next step
|
// needs to an array for next step
|
||||||
if (!Array.isArray(rows)) {
|
if (rows === null) {
|
||||||
|
rows = []
|
||||||
|
} else if (!Array.isArray(rows)) {
|
||||||
rows = [rows]
|
rows = [rows]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { License } from "../../../sdk"
|
import { License } from "../../../sdk"
|
||||||
import { Account, DevInfo, User } from "../../../documents"
|
import { Account, DevInfo, User } from "../../../documents"
|
||||||
|
import { FeatureFlags } from "@budibase/types"
|
||||||
|
|
||||||
export interface GenerateAPIKeyRequest {
|
export interface GenerateAPIKeyRequest {
|
||||||
userId?: string
|
userId?: string
|
||||||
|
@ -9,7 +10,7 @@ export interface GenerateAPIKeyResponse extends DevInfo {}
|
||||||
export interface FetchAPIKeyResponse extends DevInfo {}
|
export interface FetchAPIKeyResponse extends DevInfo {}
|
||||||
|
|
||||||
export interface GetGlobalSelfResponse extends User {
|
export interface GetGlobalSelfResponse extends User {
|
||||||
flags?: Record<string, any>
|
flags?: FeatureFlags
|
||||||
account?: Account
|
account?: Account
|
||||||
license: License
|
license: License
|
||||||
budibaseAccess: boolean
|
budibaseAccess: boolean
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { User, Document, Plugin, Snippet } from "../"
|
import { User, Document, Plugin, Snippet, Theme } from "../"
|
||||||
import { SocketSession } from "../../sdk"
|
import { SocketSession } from "../../sdk"
|
||||||
|
|
||||||
export type AppMetadataErrors = { [key: string]: string[] }
|
export type AppMetadataErrors = { [key: string]: string[] }
|
||||||
|
@ -14,7 +14,7 @@ export interface App extends Document {
|
||||||
instance: AppInstance
|
instance: AppInstance
|
||||||
tenantId: string
|
tenantId: string
|
||||||
status: string
|
status: string
|
||||||
theme?: string
|
theme?: Theme
|
||||||
customTheme?: AppCustomTheme
|
customTheme?: AppCustomTheme
|
||||||
revertableVersion?: string
|
revertableVersion?: string
|
||||||
lockedBy?: User
|
lockedBy?: User
|
||||||
|
@ -37,8 +37,8 @@ export interface AppInstance {
|
||||||
|
|
||||||
export interface AppNavigation {
|
export interface AppNavigation {
|
||||||
navigation: string
|
navigation: string
|
||||||
title: string
|
title?: string
|
||||||
navWidth: string
|
navWidth?: string
|
||||||
sticky?: boolean
|
sticky?: boolean
|
||||||
hideLogo?: boolean
|
hideLogo?: boolean
|
||||||
logoUrl?: string
|
logoUrl?: string
|
||||||
|
@ -46,6 +46,7 @@ export interface AppNavigation {
|
||||||
navBackground?: string
|
navBackground?: string
|
||||||
navTextColor?: string
|
navTextColor?: string
|
||||||
links?: AppNavigationLink[]
|
links?: AppNavigationLink[]
|
||||||
|
textAlign?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppNavigationLink {
|
export interface AppNavigationLink {
|
||||||
|
@ -53,6 +54,8 @@ export interface AppNavigationLink {
|
||||||
url: string
|
url: string
|
||||||
id?: string
|
id?: string
|
||||||
roleId?: string
|
roleId?: string
|
||||||
|
type?: string
|
||||||
|
subLinks?: AppNavigationLink[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppCustomTheme {
|
export interface AppCustomTheme {
|
||||||
|
|
|
@ -6,6 +6,12 @@ export enum FeatureFlag {
|
||||||
USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR",
|
USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TenantFeatureFlags {
|
export const FeatureFlagDefaults = {
|
||||||
[key: string]: FeatureFlag[]
|
[FeatureFlag.DEFAULT_VALUES]: true,
|
||||||
|
[FeatureFlag.AUTOMATION_BRANCHING]: true,
|
||||||
|
[FeatureFlag.AI_CUSTOM_CONFIGS]: true,
|
||||||
|
[FeatureFlag.BUDIBASE_AI]: true,
|
||||||
|
[FeatureFlag.USE_ZOD_VALIDATOR]: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FeatureFlags = typeof FeatureFlagDefaults
|
||||||
|
|
|
@ -3,4 +3,5 @@ import { User } from "@budibase/types"
|
||||||
export interface UIUser extends User {
|
export interface UIUser extends User {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
gridMetadata?: { focusedCellId?: string }
|
gridMetadata?: { focusedCellId?: string }
|
||||||
|
builderMetadata?: { selectedResourceId?: string }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
export * from "./integration"
|
export * from "./integration"
|
||||||
|
export * from "./misc"
|
||||||
export * from "./automations"
|
export * from "./automations"
|
||||||
export * from "./grid"
|
export * from "./grid"
|
||||||
|
export * from "./preview"
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
// type purely to capture structures that the type is unknown, but maybe known later
|
||||||
|
export type UIObject = Record<string, any>
|
|
@ -0,0 +1 @@
|
||||||
|
export type PreviewDevice = "desktop" | "tablet" | "mobile"
|
|
@ -72,12 +72,14 @@ export const save = async (ctx: UserCtx<User, SaveUserResponse>) => {
|
||||||
const requestUser = ctx.request.body
|
const requestUser = ctx.request.body
|
||||||
|
|
||||||
// Do not allow the account holder role to be changed
|
// Do not allow the account holder role to be changed
|
||||||
const accountMetadata = await users.getExistingAccounts([requestUser.email])
|
if (
|
||||||
if (accountMetadata?.length > 0) {
|
requestUser.admin?.global !== true ||
|
||||||
if (
|
requestUser.builder?.global !== true
|
||||||
requestUser.admin?.global !== true ||
|
) {
|
||||||
requestUser.builder?.global !== true
|
const accountMetadata = await users.getExistingAccounts([
|
||||||
) {
|
requestUser.email,
|
||||||
|
])
|
||||||
|
if (accountMetadata?.length > 0) {
|
||||||
throw Error("Cannot set role of account holder")
|
throw Error("Cannot set role of account holder")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -441,7 +443,6 @@ export const checkInvite = async (ctx: UserCtx<void, CheckInviteResponse>) => {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Error getting invite from code", e)
|
console.warn("Error getting invite from code", e)
|
||||||
ctx.throw(400, "There was a problem with the invite")
|
ctx.throw(400, "There was a problem with the invite")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
email: invite.email,
|
email: invite.email,
|
||||||
|
@ -472,7 +473,6 @@ export const updateInvite = async (
|
||||||
invite = await cache.invite.getCode(code)
|
invite = await cache.invite.getCode(code)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.throw(400, "There was a problem with the invite")
|
ctx.throw(400, "There was a problem with the invite")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let updated = {
|
let updated = {
|
||||||
|
|
24
yarn.lock
24
yarn.lock
|
@ -2131,9 +2131,9 @@
|
||||||
through2 "^2.0.0"
|
through2 "^2.0.0"
|
||||||
|
|
||||||
"@budibase/pro@npm:@budibase/pro@latest":
|
"@budibase/pro@npm:@budibase/pro@latest":
|
||||||
version "3.2.28"
|
version "3.2.32"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.28.tgz#59b5b37225715bb8fbf5b1c5c989140b10b58710"
|
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.32.tgz#f6abcd5a5524e7f33d958acb6e610e29995427bb"
|
||||||
integrity sha512-eDPeZpYFRZYQhCulcQAUwFoPk68c8+K9mIsB6QD3oMHmHTDA1P2ZcXvLNqDTIqHB94DqnWinqDf4hTuGYApgPA==
|
integrity sha512-bF0pd17IjYugjll2yKYmb0RM+tfKZcCmRBc4XG2NZ4f/I47QaOovm9RqSw6tfqCFuzRewxR3SWmtmSseUc/e0w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@anthropic-ai/sdk" "^0.27.3"
|
"@anthropic-ai/sdk" "^0.27.3"
|
||||||
"@budibase/backend-core" "*"
|
"@budibase/backend-core" "*"
|
||||||
|
@ -15600,15 +15600,7 @@ only@~0.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
|
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
|
||||||
integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==
|
integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==
|
||||||
|
|
||||||
open@^7.3.1:
|
open@8.4.2, open@^8.0.0, open@^8.4.0, open@~8.4.0:
|
||||||
version "7.4.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
|
|
||||||
integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
|
|
||||||
dependencies:
|
|
||||||
is-docker "^2.0.0"
|
|
||||||
is-wsl "^2.1.1"
|
|
||||||
|
|
||||||
open@^8.0.0, open@^8.4.0, open@~8.4.0:
|
|
||||||
version "8.4.2"
|
version "8.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9"
|
resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9"
|
||||||
integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==
|
integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==
|
||||||
|
@ -15617,6 +15609,14 @@ open@^8.0.0, open@^8.4.0, open@~8.4.0:
|
||||||
is-docker "^2.1.1"
|
is-docker "^2.1.1"
|
||||||
is-wsl "^2.2.0"
|
is-wsl "^2.2.0"
|
||||||
|
|
||||||
|
open@^7.3.1:
|
||||||
|
version "7.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
|
||||||
|
integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
|
||||||
|
dependencies:
|
||||||
|
is-docker "^2.0.0"
|
||||||
|
is-wsl "^2.1.1"
|
||||||
|
|
||||||
openai@4.59.0:
|
openai@4.59.0:
|
||||||
version "4.59.0"
|
version "4.59.0"
|
||||||
resolved "https://registry.yarnpkg.com/openai/-/openai-4.59.0.tgz#3961d11a9afb5920e1bd475948a87969e244fc08"
|
resolved "https://registry.yarnpkg.com/openai/-/openai-4.59.0.tgz#3961d11a9afb5920e1bd475948a87969e244fc08"
|
||||||
|
|
Loading…
Reference in New Issue