Merge branch 'master' into screen-store-ts-conversion
This commit is contained in:
commit
9d49d685a3
|
@ -8,41 +8,15 @@ jobs:
|
|||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
days-before-stale: 330
|
||||
operations-per-run: 1
|
||||
# stale rules for PRs
|
||||
days-before-pr-stale: 7
|
||||
stale-issue-label: stale
|
||||
exempt-pr-labels: pinned,security,roadmap
|
||||
days-before-pr-close: 7
|
||||
days-before-issue-close: 30
|
||||
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
operations-per-run: 3
|
||||
# stale rules for high priority bugs
|
||||
days-before-stale: 30
|
||||
only-issue-labels: bug,High priority
|
||||
stale-issue-label: warn
|
||||
days-before-close: 30
|
||||
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
operations-per-run: 3
|
||||
# stale rules for medium priority bugs
|
||||
days-before-stale: 90
|
||||
only-issue-labels: bug,Medium priority
|
||||
stale-issue-label: warn
|
||||
days-before-close: 30
|
||||
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
operations-per-run: 3
|
||||
# stale rules for all bugs
|
||||
days-before-stale: 180
|
||||
stale-issue-label: stale
|
||||
only-issue-labels: bug
|
||||
stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for six months."
|
||||
days-before-close: 30
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
# Issues
|
||||
days-before-stale: 180
|
||||
stale-issue-label: stale
|
||||
days-before-close: 30
|
||||
stale-issue-message: "This issue has been automatically marked as stale as there has been no activity for 6 months."
|
||||
# Pull requests
|
||||
days-before-pr-stale: 7
|
||||
days-before-pr-close: 14
|
||||
exempt-pr-labels: pinned,security,roadmap
|
||||
operations-per-run: 100
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "3.2.32",
|
||||
"version": "3.2.46",
|
||||
"npmClient": "yarn",
|
||||
"concurrency": 20,
|
||||
"command": {
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"scripts": {
|
||||
"prebuild": "rimraf dist/",
|
||||
"prepack": "cp package.json dist",
|
||||
"build": "node ./scripts/build.js && tsc -p tsconfig.build.json --emitDeclarationOnly --paths null",
|
||||
"build": "node ./scripts/build.js && tsc -p tsconfig.build.json --emitDeclarationOnly --paths null && tsc -p tsconfig.test.json --paths null",
|
||||
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
||||
"build:oss": "node ./scripts/build.js",
|
||||
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import env from "../../environment"
|
||||
|
||||
export const getCouchInfo = (connection?: string) => {
|
||||
export const getCouchInfo = (connection?: string | null) => {
|
||||
// clean out any auth credentials
|
||||
const urlInfo = getUrlInfo(connection)
|
||||
let username
|
||||
|
@ -45,7 +45,7 @@ export const getCouchInfo = (connection?: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const getUrlInfo = (url = env.COUCH_DB_URL) => {
|
||||
export const getUrlInfo = (url: string | null = env.COUCH_DB_URL) => {
|
||||
let cleanUrl, username, password, host
|
||||
if (url) {
|
||||
// Ensure the URL starts with a protocol
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
require("../../../tests")
|
||||
const getUrlInfo = require("../couch").getUrlInfo
|
||||
|
||||
import { getUrlInfo } from "../couch"
|
||||
|
||||
describe("pouch", () => {
|
||||
describe("Couch DB URL parsing", () => {
|
|
@ -32,8 +32,12 @@ export async function errorHandling(ctx: any, next: any) {
|
|||
}
|
||||
|
||||
if (environment.isTest() && ctx.headers["x-budibase-include-stacktrace"]) {
|
||||
let rootErr = err
|
||||
while (rootErr.cause) {
|
||||
rootErr = rootErr.cause
|
||||
}
|
||||
// @ts-ignore
|
||||
error.stack = err.stack
|
||||
error.stack = rootErr.stack
|
||||
}
|
||||
|
||||
ctx.body = error
|
||||
|
|
|
@ -272,17 +272,6 @@ class InternalBuilder {
|
|||
return parts.join(".")
|
||||
}
|
||||
|
||||
private isFullSelectStatementRequired(): boolean {
|
||||
for (let column of Object.values(this.table.schema)) {
|
||||
if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(column)) {
|
||||
return true
|
||||
} else if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(column)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private generateSelectStatement(): (string | Knex.Raw)[] | "*" {
|
||||
const { table, resource } = this.query
|
||||
|
||||
|
@ -292,11 +281,9 @@ class InternalBuilder {
|
|||
|
||||
const alias = this.getTableName(table)
|
||||
const schema = this.table.schema
|
||||
if (!this.isFullSelectStatementRequired()) {
|
||||
return [this.knex.raw("??", [`${alias}.*`])]
|
||||
}
|
||||
|
||||
// get just the fields for this table
|
||||
return resource.fields
|
||||
const tableFields = resource.fields
|
||||
.map(field => {
|
||||
const parts = field.split(/\./g)
|
||||
let table: string | undefined = undefined
|
||||
|
@ -311,34 +298,33 @@ class InternalBuilder {
|
|||
return { table, column, field }
|
||||
})
|
||||
.filter(({ table }) => !table || table === alias)
|
||||
.map(({ table, column, field }) => {
|
||||
const columnSchema = schema[column]
|
||||
|
||||
if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(columnSchema)) {
|
||||
return this.knex.raw(`??::money::numeric as ??`, [
|
||||
this.rawQuotedIdentifier([table, column].join(".")),
|
||||
this.knex.raw(this.quote(field)),
|
||||
])
|
||||
}
|
||||
return tableFields.map(({ table, column, field }) => {
|
||||
const columnSchema = schema[column]
|
||||
|
||||
if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(columnSchema)) {
|
||||
// Time gets returned as timestamp from mssql, not matching the expected
|
||||
// HH:mm format
|
||||
if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(columnSchema)) {
|
||||
return this.knex.raw(`??::money::numeric as ??`, [
|
||||
this.rawQuotedIdentifier([table, column].join(".")),
|
||||
this.knex.raw(this.quote(field)),
|
||||
])
|
||||
}
|
||||
|
||||
// TODO: figure out how to express this safely without string
|
||||
// interpolation.
|
||||
return this.knex.raw(`CONVERT(varchar, ??, 108) as ??`, [
|
||||
this.rawQuotedIdentifier(field),
|
||||
this.knex.raw(this.quote(field)),
|
||||
])
|
||||
}
|
||||
if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(columnSchema)) {
|
||||
// Time gets returned as timestamp from mssql, not matching the expected
|
||||
// HH:mm format
|
||||
|
||||
if (table) {
|
||||
return this.rawQuotedIdentifier(`${table}.${column}`)
|
||||
} else {
|
||||
return this.rawQuotedIdentifier(field)
|
||||
}
|
||||
})
|
||||
return this.knex.raw(`CONVERT(varchar, ??, 108) as ??`, [
|
||||
this.rawQuotedIdentifier(field),
|
||||
this.knex.raw(this.quote(field)),
|
||||
])
|
||||
}
|
||||
|
||||
if (table) {
|
||||
return this.rawQuotedIdentifier(`${table}.${column}`)
|
||||
} else {
|
||||
return this.rawQuotedIdentifier(field)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// OracleDB can't use character-large-objects (CLOBs) in WHERE clauses,
|
||||
|
@ -816,14 +802,29 @@ class InternalBuilder {
|
|||
filters.oneOf,
|
||||
ArrayOperator.ONE_OF,
|
||||
(q, key: string, array) => {
|
||||
const schema = this.getFieldSchema(key)
|
||||
const values = Array.isArray(array) ? array : [array]
|
||||
if (shouldOr) {
|
||||
q = q.or
|
||||
}
|
||||
if (this.client === SqlClient.ORACLE) {
|
||||
// @ts-ignore
|
||||
key = this.convertClobs(key)
|
||||
} else if (
|
||||
this.client === SqlClient.SQL_LITE &&
|
||||
schema?.type === FieldType.DATETIME &&
|
||||
schema.dateOnly
|
||||
) {
|
||||
for (const value of values) {
|
||||
if (value != null) {
|
||||
q = q.or.whereLike(key, `${value.toISOString().slice(0, 10)}%`)
|
||||
} else {
|
||||
q = q.or.whereNull(key)
|
||||
}
|
||||
}
|
||||
return q
|
||||
}
|
||||
return q.whereIn(key, Array.isArray(array) ? array : [array])
|
||||
return q.whereIn(key, values)
|
||||
},
|
||||
(q, key: string[], array) => {
|
||||
if (shouldOr) {
|
||||
|
@ -882,6 +883,19 @@ class InternalBuilder {
|
|||
let high = value.high
|
||||
let low = value.low
|
||||
|
||||
if (
|
||||
this.client === SqlClient.SQL_LITE &&
|
||||
schema?.type === FieldType.DATETIME &&
|
||||
schema.dateOnly
|
||||
) {
|
||||
if (high != null) {
|
||||
high = `${high.toISOString().slice(0, 10)}T23:59:59.999Z`
|
||||
}
|
||||
if (low != null) {
|
||||
low = low.toISOString().slice(0, 10)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.client === SqlClient.ORACLE) {
|
||||
rawKey = this.convertClobs(key)
|
||||
} else if (
|
||||
|
@ -914,6 +928,7 @@ class InternalBuilder {
|
|||
}
|
||||
if (filters.equal) {
|
||||
iterate(filters.equal, BasicOperator.EQUAL, (q, key, value) => {
|
||||
const schema = this.getFieldSchema(key)
|
||||
if (shouldOr) {
|
||||
q = q.or
|
||||
}
|
||||
|
@ -928,6 +943,16 @@ class InternalBuilder {
|
|||
// @ts-expect-error knex types are wrong, raw is fine here
|
||||
subq.whereNotNull(identifier).andWhere(identifier, value)
|
||||
)
|
||||
} else if (
|
||||
this.client === SqlClient.SQL_LITE &&
|
||||
schema?.type === FieldType.DATETIME &&
|
||||
schema.dateOnly
|
||||
) {
|
||||
if (value != null) {
|
||||
return q.whereLike(key, `${value.toISOString().slice(0, 10)}%`)
|
||||
} else {
|
||||
return q.whereNull(key)
|
||||
}
|
||||
} else {
|
||||
return q.whereRaw(`COALESCE(?? = ?, FALSE)`, [
|
||||
this.rawQuotedIdentifier(key),
|
||||
|
@ -938,6 +963,7 @@ class InternalBuilder {
|
|||
}
|
||||
if (filters.notEqual) {
|
||||
iterate(filters.notEqual, BasicOperator.NOT_EQUAL, (q, key, value) => {
|
||||
const schema = this.getFieldSchema(key)
|
||||
if (shouldOr) {
|
||||
q = q.or
|
||||
}
|
||||
|
@ -959,6 +985,18 @@ class InternalBuilder {
|
|||
// @ts-expect-error knex types are wrong, raw is fine here
|
||||
.or.whereNull(identifier)
|
||||
)
|
||||
} else if (
|
||||
this.client === SqlClient.SQL_LITE &&
|
||||
schema?.type === FieldType.DATETIME &&
|
||||
schema.dateOnly
|
||||
) {
|
||||
if (value != null) {
|
||||
return q.not
|
||||
.whereLike(key, `${value.toISOString().slice(0, 10)}%`)
|
||||
.or.whereNull(key)
|
||||
} else {
|
||||
return q.not.whereNull(key)
|
||||
}
|
||||
} else {
|
||||
return q.whereRaw(`COALESCE(?? != ?, TRUE)`, [
|
||||
this.rawQuotedIdentifier(key),
|
||||
|
@ -1134,20 +1172,22 @@ class InternalBuilder {
|
|||
nulls = value.direction === SortOrder.ASCENDING ? "first" : "last"
|
||||
}
|
||||
|
||||
const composite = `${aliased}.${key}`
|
||||
let identifier
|
||||
|
||||
if (this.isAggregateField(key)) {
|
||||
query = query.orderBy(key, direction, nulls)
|
||||
identifier = this.rawQuotedIdentifier(key)
|
||||
} else if (this.client === SqlClient.ORACLE) {
|
||||
identifier = this.convertClobs(composite)
|
||||
} else {
|
||||
let composite = `${aliased}.${key}`
|
||||
if (this.client === SqlClient.ORACLE) {
|
||||
query = query.orderByRaw(`?? ?? nulls ??`, [
|
||||
this.convertClobs(composite),
|
||||
this.knex.raw(direction),
|
||||
this.knex.raw(nulls as string),
|
||||
])
|
||||
} else {
|
||||
query = query.orderBy(composite, direction, nulls)
|
||||
}
|
||||
identifier = this.rawQuotedIdentifier(composite)
|
||||
}
|
||||
|
||||
query = query.orderByRaw(`?? ?? ${nulls ? "nulls ??" : ""}`, [
|
||||
identifier,
|
||||
this.knex.raw(direction),
|
||||
...(nulls ? [this.knex.raw(nulls as string)] : []),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1239,6 +1279,7 @@ class InternalBuilder {
|
|||
if (!toTable || !fromTable) {
|
||||
continue
|
||||
}
|
||||
|
||||
const relatedTable = tables[toTable]
|
||||
if (!relatedTable) {
|
||||
throw new Error(`related table "${toTable}" not found in datasource`)
|
||||
|
@ -1267,6 +1308,10 @@ class InternalBuilder {
|
|||
const fieldList = relationshipFields.map(field =>
|
||||
this.buildJsonField(relatedTable, field)
|
||||
)
|
||||
if (!fieldList.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
const fieldListFormatted = fieldList
|
||||
.map(f => {
|
||||
const separator = this.client === SqlClient.ORACLE ? " VALUE " : ","
|
||||
|
@ -1301,13 +1346,17 @@ class InternalBuilder {
|
|||
|
||||
// add the correlation to the overall query
|
||||
subQuery = subQuery.where(
|
||||
correlatedTo,
|
||||
this.rawQuotedIdentifier(correlatedTo),
|
||||
"=",
|
||||
this.rawQuotedIdentifier(correlatedFrom)
|
||||
)
|
||||
|
||||
const standardWrap = (select: Knex.Raw): Knex.QueryBuilder => {
|
||||
subQuery = subQuery.select(`${toAlias}.*`).limit(getRelationshipLimit())
|
||||
subQuery = subQuery
|
||||
.select(
|
||||
relationshipFields.map(field => this.rawQuotedIdentifier(field))
|
||||
)
|
||||
.limit(getRelationshipLimit())
|
||||
// @ts-ignore - the from alias syntax isn't in Knex typing
|
||||
return knex.select(select).from({
|
||||
[toAlias]: subQuery,
|
||||
|
@ -1537,11 +1586,12 @@ class InternalBuilder {
|
|||
limits?: { base: number; query: number }
|
||||
} = {}
|
||||
): Knex.QueryBuilder {
|
||||
let { operation, filters, paginate, relationships, table } = this.query
|
||||
const { operation, filters, paginate, relationships, table } = this.query
|
||||
const { limits } = opts
|
||||
|
||||
// start building the query
|
||||
let query = this.qualifiedKnex()
|
||||
|
||||
// handle pagination
|
||||
let foundOffset: number | null = null
|
||||
let foundLimit = limits?.query || limits?.base
|
||||
|
@ -1590,7 +1640,7 @@ class InternalBuilder {
|
|||
const mainTable = this.query.tableAliases?.[table.name] || table.name
|
||||
const cte = this.addSorting(
|
||||
this.knex
|
||||
.with("paginated", query)
|
||||
.with("paginated", query.clone().clearSelect().select("*"))
|
||||
.select(this.generateSelectStatement())
|
||||
.from({
|
||||
[mainTable]: "paginated",
|
||||
|
|
|
@ -14,7 +14,7 @@ import environment from "../environment"
|
|||
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
|
||||
const ROW_ID_REGEX = /^\[.*]$/g
|
||||
const ENCODED_SPACE = encodeURIComponent(" ")
|
||||
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/
|
||||
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}.\d{3}Z)?$/
|
||||
const TIME_REGEX = /^(?:\d{2}:)?(?:\d{2}:)(?:\d{2})$/
|
||||
|
||||
export function isExternalTableID(tableId: string) {
|
||||
|
@ -149,15 +149,7 @@ export function isInvalidISODateString(str: string) {
|
|||
}
|
||||
|
||||
export function isValidISODateString(str: string) {
|
||||
const trimmedValue = str.trim()
|
||||
if (!ISO_DATE_REGEX.test(trimmedValue)) {
|
||||
return false
|
||||
}
|
||||
let d = new Date(trimmedValue)
|
||||
if (isNaN(d.getTime())) {
|
||||
return false
|
||||
}
|
||||
return d.toISOString() === trimmedValue
|
||||
return ISO_DATE_REGEX.test(str.trim())
|
||||
}
|
||||
|
||||
export function isValidFilter(value: any) {
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
const _ = require("lodash/fp")
|
||||
const { structures } = require("../../../tests")
|
||||
import { range } from "lodash/fp"
|
||||
import { structures } from "../.."
|
||||
|
||||
jest.mock("../../../src/context")
|
||||
jest.mock("../../../src/db")
|
||||
|
||||
const context = require("../../../src/context")
|
||||
const db = require("../../../src/db")
|
||||
import * as context from "../../../src/context"
|
||||
import * as db from "../../../src/db"
|
||||
|
||||
const { getCreatorCount } = require("../../../src/users/users")
|
||||
import { getCreatorCount } from "../../../src/users/users"
|
||||
|
||||
describe("Users", () => {
|
||||
let getGlobalDBMock
|
||||
let paginationMock
|
||||
let getGlobalDBMock: jest.SpyInstance
|
||||
let paginationMock: jest.SpyInstance
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks()
|
||||
|
@ -22,11 +22,10 @@ describe("Users", () => {
|
|||
jest.spyOn(db, "getGlobalUserParams")
|
||||
})
|
||||
|
||||
it("Retrieves the number of creators", async () => {
|
||||
const getUsers = (offset, limit, creators = false) => {
|
||||
const range = _.range(offset, limit)
|
||||
it("retrieves the number of creators", async () => {
|
||||
const getUsers = (offset: number, limit: number, creators = false) => {
|
||||
const opts = creators ? { builder: { global: true } } : undefined
|
||||
return range.map(() => structures.users.user(opts))
|
||||
return range(offset, limit).map(() => structures.users.user(opts))
|
||||
}
|
||||
const page1Data = getUsers(0, 8)
|
||||
const page2Data = getUsers(8, 12, true)
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../../tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["tests/**/*.js", "tests/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "0.0.0",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"module": "dist/bbui.mjs",
|
||||
"exports": {
|
||||
".": {
|
||||
|
@ -14,7 +14,8 @@
|
|||
"./spectrum-icons-vite.js": "./src/spectrum-icons-vite.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build"
|
||||
"build": "vite build",
|
||||
"dev": "vite build --watch --mode=dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "1.4.0",
|
||||
|
|
|
@ -10,12 +10,12 @@
|
|||
export let size = "M"
|
||||
export let hoverable = false
|
||||
export let disabled = false
|
||||
export let color
|
||||
export let hoverColor
|
||||
export let tooltip
|
||||
export let color = undefined
|
||||
export let hoverColor = undefined
|
||||
export let tooltip = undefined
|
||||
export let tooltipPosition = TooltipPosition.Bottom
|
||||
export let tooltipType = TooltipType.Default
|
||||
export let tooltipColor
|
||||
export let tooltipColor = undefined
|
||||
export let tooltipWrap = true
|
||||
export let newStyles = false
|
||||
</script>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
export let size = "M"
|
||||
export let tooltip = ""
|
||||
export let muted
|
||||
export let muted = undefined
|
||||
</script>
|
||||
|
||||
<TooltipWrapper {tooltip} {size}>
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
import "@spectrum-css/typography/dist/index-vars.css"
|
||||
|
||||
// Sizes
|
||||
export let size = "M"
|
||||
export let textAlign = undefined
|
||||
export let noPadding = false
|
||||
export let weight = "default" // light, heavy, default
|
||||
export let size: "XS" | "S" | "M" | "L" = "M"
|
||||
export let textAlign: string | undefined = undefined
|
||||
export let noPadding: boolean = false
|
||||
export let weight: "light" | "heavy" | "default" = "default"
|
||||
</script>
|
||||
|
||||
<h1
|
||||
|
|
|
@ -6,9 +6,8 @@ export const deepGet = helpers.deepGet
|
|||
/**
|
||||
* Generates a DOM safe UUID.
|
||||
* Starting with a letter is important to make it DOM safe.
|
||||
* @return {string} a random DOM safe UUID
|
||||
*/
|
||||
export function uuid() {
|
||||
export function uuid(): string {
|
||||
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
|
||||
const r = (Math.random() * 16) | 0
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8
|
||||
|
@ -18,22 +17,18 @@ export function uuid() {
|
|||
|
||||
/**
|
||||
* Capitalises a string
|
||||
* @param string the string to capitalise
|
||||
* @return {string} the capitalised string
|
||||
*/
|
||||
export const capitalise = string => {
|
||||
export const capitalise = (string?: string | null): string => {
|
||||
if (!string) {
|
||||
return string
|
||||
return ""
|
||||
}
|
||||
return string.substring(0, 1).toUpperCase() + string.substring(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a short hash of a string
|
||||
* @param string the string to compute a hash of
|
||||
* @return {string} the hash string
|
||||
*/
|
||||
export const hashString = string => {
|
||||
export const hashString = (string?: string | null): string => {
|
||||
if (!string) {
|
||||
return "0"
|
||||
}
|
||||
|
@ -54,11 +49,12 @@ export const hashString = string => {
|
|||
* will override the value "foo" rather than "bar".
|
||||
* If a deep path is specified and the parent keys don't exist then these will
|
||||
* be created.
|
||||
* @param obj the object
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
*/
|
||||
export const deepSet = (obj, key, value) => {
|
||||
export const deepSet = (
|
||||
obj: Record<string, any> | null,
|
||||
key: string | null,
|
||||
value: any
|
||||
): void => {
|
||||
if (!obj || !key) {
|
||||
return
|
||||
}
|
||||
|
@ -82,9 +78,8 @@ export const deepSet = (obj, key, value) => {
|
|||
|
||||
/**
|
||||
* Deeply clones an object. Functions are not supported.
|
||||
* @param obj the object to clone
|
||||
*/
|
||||
export const cloneDeep = obj => {
|
||||
export const cloneDeep = <T>(obj: T): T => {
|
||||
if (!obj) {
|
||||
return obj
|
||||
}
|
||||
|
@ -93,9 +88,8 @@ export const cloneDeep = obj => {
|
|||
|
||||
/**
|
||||
* Copies a value to the clipboard
|
||||
* @param value the value to copy
|
||||
*/
|
||||
export const copyToClipboard = value => {
|
||||
export const copyToClipboard = (value: any): Promise<void> => {
|
||||
return new Promise(res => {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// Try using the clipboard API first
|
||||
|
@ -117,9 +111,12 @@ export const copyToClipboard = value => {
|
|||
})
|
||||
}
|
||||
|
||||
// Parsed a date value. This is usually an ISO string, but can be a
|
||||
// Parse a date value. This is usually an ISO string, but can be a
|
||||
// bunch of different formats and shapes depending on schema flags.
|
||||
export const parseDate = (value, { enableTime = true }) => {
|
||||
export const parseDate = (
|
||||
value: string | dayjs.Dayjs | null,
|
||||
{ enableTime = true }
|
||||
): dayjs.Dayjs | null => {
|
||||
// If empty then invalid
|
||||
if (!value) {
|
||||
return null
|
||||
|
@ -128,7 +125,7 @@ export const parseDate = (value, { enableTime = true }) => {
|
|||
// Certain string values need transformed
|
||||
if (typeof value === "string") {
|
||||
// Check for time only values
|
||||
if (!isNaN(new Date(`0-${value}`))) {
|
||||
if (!isNaN(new Date(`0-${value}`).valueOf())) {
|
||||
value = `0-${value}`
|
||||
}
|
||||
|
||||
|
@ -153,9 +150,9 @@ export const parseDate = (value, { enableTime = true }) => {
|
|||
// Stringifies a dayjs object to create an ISO string that respects the various
|
||||
// schema flags
|
||||
export const stringifyDate = (
|
||||
value,
|
||||
value: null | dayjs.Dayjs,
|
||||
{ enableTime = true, timeOnly = false, ignoreTimezones = false } = {}
|
||||
) => {
|
||||
): string | null => {
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
@ -192,7 +189,7 @@ export const stringifyDate = (
|
|||
}
|
||||
|
||||
// Determine the dayjs-compatible format of the browser's default locale
|
||||
const getPatternForPart = part => {
|
||||
const getPatternForPart = (part: Intl.DateTimeFormatPart): string => {
|
||||
switch (part.type) {
|
||||
case "day":
|
||||
return "D".repeat(part.value.length)
|
||||
|
@ -214,9 +211,9 @@ const localeDateFormat = new Intl.DateTimeFormat()
|
|||
|
||||
// Formats a dayjs date according to schema flags
|
||||
export const getDateDisplayValue = (
|
||||
value,
|
||||
value: dayjs.Dayjs | null,
|
||||
{ enableTime = true, timeOnly = false } = {}
|
||||
) => {
|
||||
): string => {
|
||||
if (!value?.isValid()) {
|
||||
return ""
|
||||
}
|
||||
|
@ -229,7 +226,7 @@ export const getDateDisplayValue = (
|
|||
}
|
||||
}
|
||||
|
||||
export const hexToRGBA = (color, opacity) => {
|
||||
export const hexToRGBA = (color: string, opacity: number): string => {
|
||||
if (color.includes("#")) {
|
||||
color = color.replace("#", "")
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
const { vitePreprocess } = require("@sveltejs/vite-plugin-svelte")
|
||||
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
|
||||
module.exports = config
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"outDir": "./dist",
|
||||
"lib": ["ESNext"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@budibase/*": [
|
||||
"../*/src/index.ts",
|
||||
"../*/src/index.js",
|
||||
"../*",
|
||||
"../../node_modules/@budibase/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["node_modules", "**/*.json", "**/*.spec.ts", "**/*.spec.js"]
|
||||
}
|
|
@ -9,7 +9,7 @@ export default defineConfig(({ mode }) => {
|
|||
build: {
|
||||
sourcemap: !isProduction,
|
||||
lib: {
|
||||
entry: "src/index.js",
|
||||
entry: "src/index.ts",
|
||||
formats: ["es"],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -74,7 +74,6 @@
|
|||
"dayjs": "^1.10.8",
|
||||
"downloadjs": "1.4.7",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"json-format-highlight": "^1.0.4",
|
||||
"lodash": "4.17.21",
|
||||
"posthog-js": "^1.118.0",
|
||||
"remixicon": "2.5.0",
|
||||
|
@ -94,6 +93,7 @@
|
|||
"@sveltejs/vite-plugin-svelte": "1.4.0",
|
||||
"@testing-library/jest-dom": "6.4.2",
|
||||
"@testing-library/svelte": "^4.1.0",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"@types/shortid": "^2.2.0",
|
||||
"babel-jest": "^29.6.2",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
} from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import { flags } from "@/stores/builder"
|
||||
import { featureFlags, licensing } from "@/stores/portal"
|
||||
import { licensing } from "@/stores/portal"
|
||||
import { API } from "@/api"
|
||||
import MagicWand from "../../../../assets/MagicWand.svelte"
|
||||
|
||||
|
@ -27,8 +27,7 @@
|
|||
let loadingAICronExpression = false
|
||||
|
||||
$: aiEnabled =
|
||||
($featureFlags.AI_CUSTOM_CONFIGS && $licensing.customAIConfigsEnabled) ||
|
||||
($featureFlags.BUDIBASE_AI && $licensing.budibaseAIEnabled)
|
||||
$licensing.customAIConfigsEnabled || $licensing.budibaseAIEnabled
|
||||
$: {
|
||||
if (cronExpression) {
|
||||
try {
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { tables, datasources } from "@/stores/builder"
|
||||
import { featureFlags } from "@/stores/portal"
|
||||
import { licensing } from "@/stores/portal"
|
||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "@/constants"
|
||||
import {
|
||||
FIELDS,
|
||||
|
@ -49,7 +49,6 @@
|
|||
import { RowUtils, canBeDisplayColumn } from "@budibase/frontend-core"
|
||||
import ServerBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
|
||||
import OptionsEditor from "./OptionsEditor.svelte"
|
||||
import { isEnabled } from "@/helpers/featureFlags"
|
||||
import { getUserBindings } from "@/dataBinding"
|
||||
|
||||
export let field
|
||||
|
@ -101,7 +100,8 @@
|
|||
let optionsValid = true
|
||||
|
||||
$: rowGoldenSample = RowUtils.generateGoldenSample($rows)
|
||||
$: aiEnabled = $featureFlags.BUDIBASE_AI || $featureFlags.AI_CUSTOM_CONFIGS
|
||||
$: aiEnabled =
|
||||
$licensing.customAIConfigsEnabled || $licensing.budibaseAiEnabled
|
||||
$: if (primaryDisplay) {
|
||||
editableColumn.constraints.presence = { allowEmpty: false }
|
||||
}
|
||||
|
@ -168,7 +168,6 @@
|
|||
// used to select what different options can be displayed for column type
|
||||
$: canBeDisplay =
|
||||
canBeDisplayColumn(editableColumn) && !editableColumn.autocolumn
|
||||
$: defaultValuesEnabled = isEnabled("DEFAULT_VALUES")
|
||||
$: canHaveDefault = !required && canHaveDefaultColumn(editableColumn.type)
|
||||
$: canBeRequired =
|
||||
editableColumn?.type !== FieldType.LINK &&
|
||||
|
@ -300,7 +299,7 @@
|
|||
}
|
||||
|
||||
// Ensure we don't have a default value if we can't have one
|
||||
if (!canHaveDefault || !defaultValuesEnabled) {
|
||||
if (!canHaveDefault) {
|
||||
delete saveColumn.default
|
||||
}
|
||||
|
||||
|
@ -848,51 +847,49 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if defaultValuesEnabled}
|
||||
{#if editableColumn.type === FieldType.OPTIONS}
|
||||
<Select
|
||||
disabled={!canHaveDefault}
|
||||
options={editableColumn.constraints?.inclusion || []}
|
||||
label="Default value"
|
||||
value={editableColumn.default}
|
||||
on:change={e => (editableColumn.default = e.detail)}
|
||||
placeholder="None"
|
||||
/>
|
||||
{:else if editableColumn.type === FieldType.ARRAY}
|
||||
<Multiselect
|
||||
disabled={!canHaveDefault}
|
||||
options={editableColumn.constraints?.inclusion || []}
|
||||
label="Default value"
|
||||
value={editableColumn.default}
|
||||
on:change={e =>
|
||||
(editableColumn.default = e.detail?.length ? e.detail : undefined)}
|
||||
placeholder="None"
|
||||
/>
|
||||
{:else if editableColumn.subtype === BBReferenceFieldSubType.USER}
|
||||
{@const defaultValue =
|
||||
editableColumn.type === FieldType.BB_REFERENCE_SINGLE
|
||||
? SingleUserDefault
|
||||
: MultiUserDefault}
|
||||
<Toggle
|
||||
disabled={!canHaveDefault}
|
||||
text="Default to current user"
|
||||
value={editableColumn.default === defaultValue}
|
||||
on:change={e =>
|
||||
(editableColumn.default = e.detail ? defaultValue : undefined)}
|
||||
/>
|
||||
{:else}
|
||||
<ModalBindableInput
|
||||
disabled={!canHaveDefault}
|
||||
panel={ServerBindingPanel}
|
||||
title="Default value"
|
||||
label="Default value"
|
||||
placeholder="None"
|
||||
value={editableColumn.default}
|
||||
on:change={e => (editableColumn.default = e.detail)}
|
||||
bindings={defaultValueBindings}
|
||||
allowJS
|
||||
/>
|
||||
{/if}
|
||||
{#if editableColumn.type === FieldType.OPTIONS}
|
||||
<Select
|
||||
disabled={!canHaveDefault}
|
||||
options={editableColumn.constraints?.inclusion || []}
|
||||
label="Default value"
|
||||
value={editableColumn.default}
|
||||
on:change={e => (editableColumn.default = e.detail)}
|
||||
placeholder="None"
|
||||
/>
|
||||
{:else if editableColumn.type === FieldType.ARRAY}
|
||||
<Multiselect
|
||||
disabled={!canHaveDefault}
|
||||
options={editableColumn.constraints?.inclusion || []}
|
||||
label="Default value"
|
||||
value={editableColumn.default}
|
||||
on:change={e =>
|
||||
(editableColumn.default = e.detail?.length ? e.detail : undefined)}
|
||||
placeholder="None"
|
||||
/>
|
||||
{:else if editableColumn.subtype === BBReferenceFieldSubType.USER}
|
||||
{@const defaultValue =
|
||||
editableColumn.type === FieldType.BB_REFERENCE_SINGLE
|
||||
? SingleUserDefault
|
||||
: MultiUserDefault}
|
||||
<Toggle
|
||||
disabled={!canHaveDefault}
|
||||
text="Default to current user"
|
||||
value={editableColumn.default === defaultValue}
|
||||
on:change={e =>
|
||||
(editableColumn.default = e.detail ? defaultValue : undefined)}
|
||||
/>
|
||||
{:else}
|
||||
<ModalBindableInput
|
||||
disabled={!canHaveDefault}
|
||||
panel={ServerBindingPanel}
|
||||
title="Default value"
|
||||
label="Default value"
|
||||
placeholder="None"
|
||||
value={editableColumn.default}
|
||||
on:change={e => (editableColumn.default = e.detail)}
|
||||
bindings={defaultValueBindings}
|
||||
allowJS
|
||||
/>
|
||||
{/if}
|
||||
</Layout>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { Label } from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher, onDestroy } from "svelte"
|
||||
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
|
||||
|
@ -12,7 +12,6 @@
|
|||
completionStatus,
|
||||
} from "@codemirror/autocomplete"
|
||||
import {
|
||||
EditorView,
|
||||
lineNumbers,
|
||||
keymap,
|
||||
highlightSpecialChars,
|
||||
|
@ -25,6 +24,7 @@
|
|||
MatchDecorator,
|
||||
ViewPlugin,
|
||||
Decoration,
|
||||
EditorView,
|
||||
} from "@codemirror/view"
|
||||
import {
|
||||
bracketMatching,
|
||||
|
@ -44,12 +44,14 @@
|
|||
import { javascript } from "@codemirror/lang-javascript"
|
||||
import { EditorModes } from "./"
|
||||
import { themeStore } from "@/stores/portal"
|
||||
import type { EditorMode } from "@budibase/types"
|
||||
|
||||
export let label
|
||||
export let completions = []
|
||||
export let mode = EditorModes.Handlebars
|
||||
export let value = ""
|
||||
export let placeholder = null
|
||||
export let label: string | undefined = undefined
|
||||
// TODO: work out what best type fits this
|
||||
export let completions: any[] = []
|
||||
export let mode: EditorMode = EditorModes.Handlebars
|
||||
export let value: string | null = ""
|
||||
export let placeholder: string | null = null
|
||||
export let autocompleteEnabled = true
|
||||
export let autofocus = false
|
||||
export let jsBindingWrapping = true
|
||||
|
@ -58,8 +60,8 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let textarea
|
||||
let editor
|
||||
let textarea: HTMLDivElement
|
||||
let editor: EditorView
|
||||
let mounted = false
|
||||
let isEditorInitialised = false
|
||||
let queuedRefresh = false
|
||||
|
@ -100,15 +102,22 @@
|
|||
/**
|
||||
* Will refresh the editor contents only after
|
||||
* it has been fully initialised
|
||||
* @param value {string} the editor value
|
||||
*/
|
||||
const refresh = (value, initialised, mounted) => {
|
||||
const refresh = (
|
||||
value: string | null,
|
||||
initialised?: boolean,
|
||||
mounted?: boolean
|
||||
) => {
|
||||
if (!initialised || !mounted) {
|
||||
queuedRefresh = true
|
||||
return
|
||||
}
|
||||
|
||||
if (editor.state.doc.toString() !== value || queuedRefresh) {
|
||||
if (
|
||||
editor &&
|
||||
value &&
|
||||
(editor.state.doc.toString() !== value || queuedRefresh)
|
||||
) {
|
||||
editor.dispatch({
|
||||
changes: { from: 0, to: editor.state.doc.length, insert: value },
|
||||
})
|
||||
|
@ -120,12 +129,17 @@
|
|||
export const getCaretPosition = () => {
|
||||
const selection_range = editor.state.selection.ranges[0]
|
||||
return {
|
||||
start: selection_range.from,
|
||||
end: selection_range.to,
|
||||
start: selection_range?.from,
|
||||
end: selection_range?.to,
|
||||
}
|
||||
}
|
||||
|
||||
export const insertAtPos = opts => {
|
||||
export const insertAtPos = (opts: {
|
||||
start: number
|
||||
end?: number
|
||||
value: string
|
||||
cursor: { anchor: number }
|
||||
}) => {
|
||||
// Updating the value inside.
|
||||
// Retain focus
|
||||
editor.dispatch({
|
||||
|
@ -192,7 +206,7 @@
|
|||
|
||||
const indentWithTabCustom = {
|
||||
key: "Tab",
|
||||
run: view => {
|
||||
run: (view: EditorView) => {
|
||||
if (completionStatus(view.state) === "active") {
|
||||
acceptCompletion(view)
|
||||
return true
|
||||
|
@ -200,7 +214,7 @@
|
|||
indentMore(view)
|
||||
return true
|
||||
},
|
||||
shift: view => {
|
||||
shift: (view: EditorView) => {
|
||||
indentLess(view)
|
||||
return true
|
||||
},
|
||||
|
@ -232,7 +246,8 @@
|
|||
|
||||
// None of this is reactive, but it never has been, so we just assume most
|
||||
// config flags aren't changed at runtime
|
||||
const buildExtensions = base => {
|
||||
// TODO: work out type for base
|
||||
const buildExtensions = (base: any[]) => {
|
||||
let complete = [...base]
|
||||
|
||||
if (autocompleteEnabled) {
|
||||
|
@ -242,7 +257,7 @@
|
|||
closeOnBlur: true,
|
||||
icons: false,
|
||||
optionClass: completion =>
|
||||
completion.simple
|
||||
"simple" in completion && completion.simple
|
||||
? "autocomplete-option-simple"
|
||||
: "autocomplete-option",
|
||||
})
|
||||
|
@ -347,7 +362,7 @@
|
|||
|
||||
{#if label}
|
||||
<div>
|
||||
<Label small>{label}</Label>
|
||||
<Label size="S">{label}</Label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import { getManifest } from "@budibase/string-templates"
|
||||
import sanitizeHtml from "sanitize-html"
|
||||
import { groupBy } from "lodash"
|
||||
import {
|
||||
BindingCompletion,
|
||||
EditorModesMap,
|
||||
Helper,
|
||||
Snippet,
|
||||
} from "@budibase/types"
|
||||
import { CompletionContext } from "@codemirror/autocomplete"
|
||||
|
||||
export const EditorModes = {
|
||||
export const EditorModes: EditorModesMap = {
|
||||
JS: {
|
||||
name: "javascript",
|
||||
json: false,
|
||||
|
@ -26,7 +33,7 @@ export const SECTIONS = {
|
|||
},
|
||||
}
|
||||
|
||||
export const buildHelperInfoNode = (completion, helper) => {
|
||||
export const buildHelperInfoNode = (completion: any, helper: Helper) => {
|
||||
const ele = document.createElement("div")
|
||||
ele.classList.add("info-bubble")
|
||||
|
||||
|
@ -46,7 +53,7 @@ export const buildHelperInfoNode = (completion, helper) => {
|
|||
return ele
|
||||
}
|
||||
|
||||
const toSpectrumIcon = name => {
|
||||
const toSpectrumIcon = (name: string) => {
|
||||
return `<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeS"
|
||||
focusable="false"
|
||||
|
@ -58,7 +65,12 @@ const toSpectrumIcon = name => {
|
|||
</svg>`
|
||||
}
|
||||
|
||||
export const buildSectionHeader = (type, sectionName, icon, rank) => {
|
||||
export const buildSectionHeader = (
|
||||
type: string,
|
||||
sectionName: string,
|
||||
icon: string,
|
||||
rank: number
|
||||
) => {
|
||||
const ele = document.createElement("div")
|
||||
ele.classList.add("info-section")
|
||||
if (type) {
|
||||
|
@ -72,43 +84,52 @@ export const buildSectionHeader = (type, sectionName, icon, rank) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const helpersToCompletion = (helpers, mode) => {
|
||||
export const helpersToCompletion = (
|
||||
helpers: Record<string, Helper>,
|
||||
mode: { name: "javascript" | "handlebars" }
|
||||
) => {
|
||||
const { type, name: sectionName, icon } = SECTIONS.HB_HELPER
|
||||
const helperSection = buildSectionHeader(type, sectionName, icon, 99)
|
||||
|
||||
return Object.keys(helpers).reduce((acc, key) => {
|
||||
let helper = helpers[key]
|
||||
acc.push({
|
||||
label: key,
|
||||
info: completion => {
|
||||
return Object.keys(helpers).flatMap(helperName => {
|
||||
let helper = helpers[helperName]
|
||||
return {
|
||||
label: helperName,
|
||||
info: (completion: BindingCompletion) => {
|
||||
return buildHelperInfoNode(completion, helper)
|
||||
},
|
||||
type: "helper",
|
||||
section: helperSection,
|
||||
detail: "Function",
|
||||
apply: (view, completion, from, to) => {
|
||||
insertBinding(view, from, to, key, mode)
|
||||
apply: (
|
||||
view: any,
|
||||
completion: BindingCompletion,
|
||||
from: number,
|
||||
to: number
|
||||
) => {
|
||||
insertBinding(view, from, to, helperName, mode)
|
||||
},
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const getHelperCompletions = mode => {
|
||||
const manifest = getManifest()
|
||||
return Object.keys(manifest).reduce((acc, key) => {
|
||||
acc = acc || []
|
||||
return [...acc, ...helpersToCompletion(manifest[key], mode)]
|
||||
}, [])
|
||||
export const getHelperCompletions = (mode: {
|
||||
name: "javascript" | "handlebars"
|
||||
}) => {
|
||||
// TODO: manifest needs to be properly typed
|
||||
const manifest: any = getManifest()
|
||||
return Object.keys(manifest).flatMap(key => {
|
||||
return helpersToCompletion(manifest[key], mode)
|
||||
})
|
||||
}
|
||||
|
||||
export const snippetAutoComplete = snippets => {
|
||||
return function myCompletions(context) {
|
||||
export const snippetAutoComplete = (snippets: Snippet[]) => {
|
||||
return function myCompletions(context: CompletionContext) {
|
||||
if (!snippets?.length) {
|
||||
return null
|
||||
}
|
||||
const word = context.matchBefore(/\w*/)
|
||||
if (word.from == word.to && !context.explicit) {
|
||||
if (!word || (word.from == word.to && !context.explicit)) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
|
@ -117,7 +138,12 @@ export const snippetAutoComplete = snippets => {
|
|||
label: `snippets.${snippet.name}`,
|
||||
type: "text",
|
||||
simple: true,
|
||||
apply: (view, completion, from, to) => {
|
||||
apply: (
|
||||
view: any,
|
||||
completion: BindingCompletion,
|
||||
from: number,
|
||||
to: number
|
||||
) => {
|
||||
insertSnippet(view, from, to, completion.label)
|
||||
},
|
||||
})),
|
||||
|
@ -125,7 +151,7 @@ export const snippetAutoComplete = snippets => {
|
|||
}
|
||||
}
|
||||
|
||||
const bindingFilter = (options, query) => {
|
||||
const bindingFilter = (options: BindingCompletion[], query: string) => {
|
||||
return options.filter(completion => {
|
||||
const section_parsed = completion.section.name.toLowerCase()
|
||||
const label_parsed = completion.label.toLowerCase()
|
||||
|
@ -138,8 +164,8 @@ const bindingFilter = (options, query) => {
|
|||
})
|
||||
}
|
||||
|
||||
export const hbAutocomplete = baseCompletions => {
|
||||
async function coreCompletion(context) {
|
||||
export const hbAutocomplete = (baseCompletions: BindingCompletion[]) => {
|
||||
async function coreCompletion(context: CompletionContext) {
|
||||
let bindingStart = context.matchBefore(EditorModes.Handlebars.match)
|
||||
|
||||
let options = baseCompletions || []
|
||||
|
@ -149,6 +175,9 @@ export const hbAutocomplete = baseCompletions => {
|
|||
}
|
||||
// Accommodate spaces
|
||||
const match = bindingStart.text.match(/{{[\s]*/)
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
const query = bindingStart.text.replace(match[0], "")
|
||||
let filtered = bindingFilter(options, query)
|
||||
|
||||
|
@ -162,14 +191,17 @@ export const hbAutocomplete = baseCompletions => {
|
|||
return coreCompletion
|
||||
}
|
||||
|
||||
export const jsAutocomplete = baseCompletions => {
|
||||
async function coreCompletion(context) {
|
||||
export const jsAutocomplete = (baseCompletions: BindingCompletion[]) => {
|
||||
async function coreCompletion(context: CompletionContext) {
|
||||
let jsBinding = context.matchBefore(/\$\("[\s\w]*/)
|
||||
let options = baseCompletions || []
|
||||
|
||||
if (jsBinding) {
|
||||
// Accommodate spaces
|
||||
const match = jsBinding.text.match(/\$\("[\s]*/)
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
const query = jsBinding.text.replace(match[0], "")
|
||||
let filtered = bindingFilter(options, query)
|
||||
return {
|
||||
|
@ -185,7 +217,10 @@ export const jsAutocomplete = baseCompletions => {
|
|||
return coreCompletion
|
||||
}
|
||||
|
||||
export const buildBindingInfoNode = (completion, binding) => {
|
||||
export const buildBindingInfoNode = (
|
||||
completion: BindingCompletion,
|
||||
binding: any
|
||||
) => {
|
||||
if (!binding.valueHTML || binding.value == null) {
|
||||
return null
|
||||
}
|
||||
|
@ -196,7 +231,12 @@ export const buildBindingInfoNode = (completion, binding) => {
|
|||
}
|
||||
|
||||
// Readdress these methods. They shouldn't be used
|
||||
export const hbInsert = (value, from, to, text) => {
|
||||
export const hbInsert = (
|
||||
value: string,
|
||||
from: number,
|
||||
to: number,
|
||||
text: string
|
||||
) => {
|
||||
let parsedInsert = ""
|
||||
|
||||
const left = from ? value.substring(0, from) : ""
|
||||
|
@ -212,11 +252,14 @@ export const hbInsert = (value, from, to, text) => {
|
|||
}
|
||||
|
||||
export function jsInsert(
|
||||
value,
|
||||
from,
|
||||
to,
|
||||
text,
|
||||
{ helper, disableWrapping } = {}
|
||||
value: string,
|
||||
from: number,
|
||||
to: number,
|
||||
text: string,
|
||||
{
|
||||
helper,
|
||||
disableWrapping,
|
||||
}: { helper?: boolean; disableWrapping?: boolean } = {}
|
||||
) {
|
||||
let parsedInsert = ""
|
||||
|
||||
|
@ -236,7 +279,13 @@ export function jsInsert(
|
|||
}
|
||||
|
||||
// Autocomplete apply behaviour
|
||||
export const insertBinding = (view, from, to, text, mode) => {
|
||||
export const insertBinding = (
|
||||
view: any,
|
||||
from: number,
|
||||
to: number,
|
||||
text: string,
|
||||
mode: { name: "javascript" | "handlebars" }
|
||||
) => {
|
||||
let parsedInsert
|
||||
|
||||
if (mode.name == "javascript") {
|
||||
|
@ -270,7 +319,12 @@ export const insertBinding = (view, from, to, text, mode) => {
|
|||
})
|
||||
}
|
||||
|
||||
export const insertSnippet = (view, from, to, text) => {
|
||||
export const insertSnippet = (
|
||||
view: any,
|
||||
from: number,
|
||||
to: number,
|
||||
text: string
|
||||
) => {
|
||||
let cursorPos = from + text.length
|
||||
view.dispatch({
|
||||
changes: {
|
||||
|
@ -284,9 +338,13 @@ export const insertSnippet = (view, from, to, text) => {
|
|||
})
|
||||
}
|
||||
|
||||
export const bindingsToCompletions = (bindings, mode) => {
|
||||
// TODO: typing in this function isn't great
|
||||
export const bindingsToCompletions = (
|
||||
bindings: any,
|
||||
mode: { name: "javascript" | "handlebars" }
|
||||
) => {
|
||||
const bindingByCategory = groupBy(bindings, "category")
|
||||
const categoryMeta = bindings?.reduce((acc, ele) => {
|
||||
const categoryMeta = bindings?.reduce((acc: any, ele: any) => {
|
||||
acc[ele.category] = acc[ele.category] || {}
|
||||
|
||||
if (ele.icon) {
|
||||
|
@ -298,36 +356,46 @@ export const bindingsToCompletions = (bindings, mode) => {
|
|||
return acc
|
||||
}, {})
|
||||
|
||||
const completions = Object.keys(bindingByCategory).reduce((comps, catKey) => {
|
||||
const { icon, rank } = categoryMeta[catKey] || {}
|
||||
const completions = Object.keys(bindingByCategory).reduce(
|
||||
(comps: any, catKey: string) => {
|
||||
const { icon, rank } = categoryMeta[catKey] || {}
|
||||
|
||||
const bindindSectionHeader = buildSectionHeader(
|
||||
bindingByCategory.type,
|
||||
catKey,
|
||||
icon || "",
|
||||
typeof rank == "number" ? rank : 1
|
||||
)
|
||||
const bindingSectionHeader = buildSectionHeader(
|
||||
// @ts-ignore something wrong with this - logically this should be dictionary
|
||||
bindingByCategory.type,
|
||||
catKey,
|
||||
icon || "",
|
||||
typeof rank == "number" ? rank : 1
|
||||
)
|
||||
|
||||
return [
|
||||
...comps,
|
||||
...bindingByCategory[catKey].reduce((acc, binding) => {
|
||||
let displayType = binding.fieldSchema?.type || binding.display?.type
|
||||
acc.push({
|
||||
label: binding.display?.name || binding.readableBinding || "NO NAME",
|
||||
info: completion => {
|
||||
return buildBindingInfoNode(completion, binding)
|
||||
},
|
||||
type: "binding",
|
||||
detail: displayType,
|
||||
section: bindindSectionHeader,
|
||||
apply: (view, completion, from, to) => {
|
||||
insertBinding(view, from, to, binding.readableBinding, mode)
|
||||
},
|
||||
})
|
||||
return acc
|
||||
}, []),
|
||||
]
|
||||
}, [])
|
||||
return [
|
||||
...comps,
|
||||
...bindingByCategory[catKey].reduce((acc, binding) => {
|
||||
let displayType = binding.fieldSchema?.type || binding.display?.type
|
||||
acc.push({
|
||||
label:
|
||||
binding.display?.name || binding.readableBinding || "NO NAME",
|
||||
info: (completion: BindingCompletion) => {
|
||||
return buildBindingInfoNode(completion, binding)
|
||||
},
|
||||
type: "binding",
|
||||
detail: displayType,
|
||||
section: bindingSectionHeader,
|
||||
apply: (
|
||||
view: any,
|
||||
completion: BindingCompletion,
|
||||
from: number,
|
||||
to: number
|
||||
) => {
|
||||
insertBinding(view, from, to, binding.readableBinding, mode)
|
||||
},
|
||||
})
|
||||
return acc
|
||||
}, []),
|
||||
]
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return completions
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import {
|
||||
DrawerContent,
|
||||
ActionButton,
|
||||
|
@ -28,45 +28,45 @@
|
|||
import EvaluationSidePanel from "./EvaluationSidePanel.svelte"
|
||||
import SnippetSidePanel from "./SnippetSidePanel.svelte"
|
||||
import { BindingHelpers } from "./utils"
|
||||
import formatHighlight from "json-format-highlight"
|
||||
import { capitalise } from "@/helpers"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
import { Utils, JsonFormatter } from "@budibase/frontend-core"
|
||||
import { licensing } from "@/stores/portal"
|
||||
import { BindingMode, SidePanel } from "@budibase/types"
|
||||
import type {
|
||||
EnrichedBinding,
|
||||
BindingCompletion,
|
||||
Snippet,
|
||||
Helper,
|
||||
CaretPositionFn,
|
||||
InsertAtPositionFn,
|
||||
JSONValue,
|
||||
} from "@budibase/types"
|
||||
import type { CompletionContext } from "@codemirror/autocomplete"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let bindings = []
|
||||
export let value = ""
|
||||
export let bindings: EnrichedBinding[] = []
|
||||
export let value: string = ""
|
||||
export let allowHBS = true
|
||||
export let allowJS = false
|
||||
export let allowHelpers = true
|
||||
export let allowSnippets = true
|
||||
export let context = null
|
||||
export let snippets = null
|
||||
export let snippets: Snippet[] | null = null
|
||||
export let autofocusEditor = false
|
||||
export let placeholder = null
|
||||
export let showTabBar = true
|
||||
|
||||
const Modes = {
|
||||
Text: "Text",
|
||||
JavaScript: "JavaScript",
|
||||
}
|
||||
const SidePanels = {
|
||||
Bindings: "FlashOn",
|
||||
Evaluation: "Play",
|
||||
Snippets: "Code",
|
||||
}
|
||||
|
||||
let mode
|
||||
let sidePanel
|
||||
let mode: BindingMode | null
|
||||
let sidePanel: SidePanel | null
|
||||
let initialValueJS = value?.startsWith?.("{{ js ")
|
||||
let jsValue = initialValueJS ? value : null
|
||||
let hbsValue = initialValueJS ? null : value
|
||||
let getCaretPosition
|
||||
let insertAtPos
|
||||
let targetMode = null
|
||||
let expressionResult
|
||||
let expressionError
|
||||
let jsValue: string | null = initialValueJS ? value : null
|
||||
let hbsValue: string | null = initialValueJS ? null : value
|
||||
let getCaretPosition: CaretPositionFn | undefined
|
||||
let insertAtPos: InsertAtPositionFn | undefined
|
||||
let targetMode: BindingMode | null = null
|
||||
let expressionResult: string | undefined
|
||||
let expressionError: string | undefined
|
||||
let evaluating = false
|
||||
|
||||
$: useSnippets = allowSnippets && !$licensing.isFreePlan
|
||||
|
@ -78,10 +78,12 @@
|
|||
mode
|
||||
)
|
||||
$: enrichedBindings = enrichBindings(bindings, context, snippets)
|
||||
$: usingJS = mode === Modes.JavaScript
|
||||
$: usingJS = mode === BindingMode.JavaScript
|
||||
$: editorMode =
|
||||
mode === Modes.JavaScript ? EditorModes.JS : EditorModes.Handlebars
|
||||
$: editorValue = editorMode === EditorModes.JS ? jsValue : hbsValue
|
||||
mode === BindingMode.JavaScript ? EditorModes.JS : EditorModes.Handlebars
|
||||
$: editorValue = (editorMode === EditorModes.JS ? jsValue : hbsValue) as
|
||||
| string
|
||||
| null
|
||||
$: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value)
|
||||
$: requestEval(runtimeExpression, context, snippets)
|
||||
$: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode)
|
||||
|
@ -95,7 +97,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
const getHBSCompletions = bindingCompletions => {
|
||||
const getHBSCompletions = (bindingCompletions: BindingCompletion[]) => {
|
||||
return [
|
||||
hbAutocomplete([
|
||||
...bindingCompletions,
|
||||
|
@ -104,71 +106,87 @@
|
|||
]
|
||||
}
|
||||
|
||||
const getJSCompletions = (bindingCompletions, snippets, useSnippets) => {
|
||||
const completions = [
|
||||
const getJSCompletions = (
|
||||
bindingCompletions: BindingCompletion[],
|
||||
snippets: Snippet[] | null,
|
||||
useSnippets?: boolean
|
||||
) => {
|
||||
const completions: ((_: CompletionContext) => any)[] = [
|
||||
jsAutocomplete([
|
||||
...bindingCompletions,
|
||||
...getHelperCompletions(EditorModes.JS),
|
||||
]),
|
||||
]
|
||||
if (useSnippets) {
|
||||
if (useSnippets && snippets) {
|
||||
completions.push(snippetAutoComplete(snippets))
|
||||
}
|
||||
return completions
|
||||
}
|
||||
|
||||
const getModeOptions = (allowHBS, allowJS) => {
|
||||
const getModeOptions = (allowHBS: boolean, allowJS: boolean) => {
|
||||
let options = []
|
||||
if (allowHBS) {
|
||||
options.push(Modes.Text)
|
||||
options.push(BindingMode.Text)
|
||||
}
|
||||
if (allowJS) {
|
||||
options.push(Modes.JavaScript)
|
||||
options.push(BindingMode.JavaScript)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
const getSidePanelOptions = (bindings, context, useSnippets, mode) => {
|
||||
const getSidePanelOptions = (
|
||||
bindings: EnrichedBinding[],
|
||||
context: any,
|
||||
useSnippets: boolean,
|
||||
mode: BindingMode | null
|
||||
) => {
|
||||
let options = []
|
||||
if (bindings?.length) {
|
||||
options.push(SidePanels.Bindings)
|
||||
options.push(SidePanel.Bindings)
|
||||
}
|
||||
if (context && Object.keys(context).length > 0) {
|
||||
options.push(SidePanels.Evaluation)
|
||||
options.push(SidePanel.Evaluation)
|
||||
}
|
||||
if (useSnippets && mode === Modes.JavaScript) {
|
||||
options.push(SidePanels.Snippets)
|
||||
if (useSnippets && mode === BindingMode.JavaScript) {
|
||||
options.push(SidePanel.Snippets)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
const debouncedEval = Utils.debounce((expression, context, snippets) => {
|
||||
try {
|
||||
expressionError = null
|
||||
expressionResult = processStringSync(
|
||||
expression || "",
|
||||
{
|
||||
...context,
|
||||
snippets,
|
||||
},
|
||||
{
|
||||
noThrow: false,
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
expressionResult = null
|
||||
expressionError = err
|
||||
}
|
||||
evaluating = false
|
||||
}, 260)
|
||||
const debouncedEval = Utils.debounce(
|
||||
(expression: string | null, context: any, snippets: Snippet[]) => {
|
||||
try {
|
||||
expressionError = undefined
|
||||
expressionResult = processStringSync(
|
||||
expression || "",
|
||||
{
|
||||
...context,
|
||||
snippets,
|
||||
},
|
||||
{
|
||||
noThrow: false,
|
||||
}
|
||||
)
|
||||
} catch (err: any) {
|
||||
expressionResult = undefined
|
||||
expressionError = err
|
||||
}
|
||||
evaluating = false
|
||||
},
|
||||
260
|
||||
)
|
||||
|
||||
const requestEval = (expression, context, snippets) => {
|
||||
const requestEval = (
|
||||
expression: string | null,
|
||||
context: any,
|
||||
snippets: Snippet[] | null
|
||||
) => {
|
||||
evaluating = true
|
||||
debouncedEval(expression, context, snippets)
|
||||
}
|
||||
|
||||
const highlightJSON = json => {
|
||||
return formatHighlight(json, {
|
||||
const highlightJSON = (json: JSONValue) => {
|
||||
return JsonFormatter.format(json, {
|
||||
keyColor: "#e06c75",
|
||||
numberColor: "#e5c07b",
|
||||
stringColor: "#98c379",
|
||||
|
@ -178,7 +196,11 @@
|
|||
})
|
||||
}
|
||||
|
||||
const enrichBindings = (bindings, context, snippets) => {
|
||||
const enrichBindings = (
|
||||
bindings: EnrichedBinding[],
|
||||
context: any,
|
||||
snippets: Snippet[] | null
|
||||
) => {
|
||||
// Create a single big array to enrich in one go
|
||||
const bindingStrings = bindings.map(binding => {
|
||||
if (binding.runtimeBinding.startsWith('trim "')) {
|
||||
|
@ -189,17 +211,18 @@
|
|||
return `{{ literal ${binding.runtimeBinding} }}`
|
||||
}
|
||||
})
|
||||
const bindingEvauations = processObjectSync(bindingStrings, {
|
||||
const bindingEvaluations = processObjectSync(bindingStrings, {
|
||||
...context,
|
||||
snippets,
|
||||
})
|
||||
|
||||
// Enrich bindings with evaluations and highlighted HTML
|
||||
return bindings.map((binding, idx) => {
|
||||
if (!context) {
|
||||
if (!context || typeof bindingEvaluations !== "object") {
|
||||
return binding
|
||||
}
|
||||
const value = JSON.stringify(bindingEvauations[idx], null, 2)
|
||||
const evalObj: Record<any, any> = bindingEvaluations
|
||||
const value = JSON.stringify(evalObj[idx], null, 2)
|
||||
return {
|
||||
...binding,
|
||||
value,
|
||||
|
@ -208,29 +231,38 @@
|
|||
})
|
||||
}
|
||||
|
||||
const updateValue = val => {
|
||||
const updateValue = (val: any) => {
|
||||
const runtimeExpression = readableToRuntimeBinding(enrichedBindings, val)
|
||||
dispatch("change", val)
|
||||
requestEval(runtimeExpression, context, snippets)
|
||||
}
|
||||
|
||||
const onSelectHelper = (helper, js) => {
|
||||
bindingHelpers.onSelectHelper(js ? jsValue : hbsValue, helper, { js })
|
||||
const onSelectHelper = (helper: Helper, js?: boolean) => {
|
||||
bindingHelpers.onSelectHelper(js ? jsValue : hbsValue, helper, {
|
||||
js,
|
||||
dontDecode: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const onSelectBinding = (binding, { forceJS } = {}) => {
|
||||
const onSelectBinding = (
|
||||
binding: EnrichedBinding,
|
||||
{ forceJS }: { forceJS?: boolean } = {}
|
||||
) => {
|
||||
const js = usingJS || forceJS
|
||||
bindingHelpers.onSelectBinding(js ? jsValue : hbsValue, binding, { js })
|
||||
bindingHelpers.onSelectBinding(js ? jsValue : hbsValue, binding, {
|
||||
js,
|
||||
dontDecode: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const changeMode = newMode => {
|
||||
const changeMode = (newMode: BindingMode) => {
|
||||
if (targetMode || newMode === mode) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the raw editor value to see if we are abandoning changes
|
||||
let rawValue = editorValue
|
||||
if (mode === Modes.JavaScript) {
|
||||
if (mode === BindingMode.JavaScript && rawValue) {
|
||||
rawValue = decodeJSBinding(rawValue)
|
||||
}
|
||||
|
||||
|
@ -249,16 +281,16 @@
|
|||
targetMode = null
|
||||
}
|
||||
|
||||
const changeSidePanel = newSidePanel => {
|
||||
const changeSidePanel = (newSidePanel: SidePanel) => {
|
||||
sidePanel = newSidePanel === sidePanel ? null : newSidePanel
|
||||
}
|
||||
|
||||
const onChangeHBSValue = e => {
|
||||
const onChangeHBSValue = (e: { detail: string }) => {
|
||||
hbsValue = e.detail
|
||||
updateValue(hbsValue)
|
||||
}
|
||||
|
||||
const onChangeJSValue = e => {
|
||||
const onChangeJSValue = (e: { detail: string }) => {
|
||||
jsValue = encodeJSBinding(e.detail)
|
||||
if (!e.detail?.trim()) {
|
||||
// Don't bother saving empty values as JS
|
||||
|
@ -268,9 +300,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
const addSnippet = (snippet: Snippet) =>
|
||||
bindingHelpers.onSelectSnippet(snippet)
|
||||
|
||||
onMount(() => {
|
||||
// Set the initial mode appropriately
|
||||
const initialValueMode = initialValueJS ? Modes.JavaScript : Modes.Text
|
||||
const initialValueMode = initialValueJS
|
||||
? BindingMode.JavaScript
|
||||
: BindingMode.Text
|
||||
if (editorModeOptions.includes(initialValueMode)) {
|
||||
mode = initialValueMode
|
||||
} else {
|
||||
|
@ -314,7 +351,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
<div class="editor">
|
||||
{#if mode === Modes.Text}
|
||||
{#if mode === BindingMode.Text}
|
||||
{#key hbsCompletions}
|
||||
<CodeEditor
|
||||
value={hbsValue}
|
||||
|
@ -328,10 +365,10 @@
|
|||
jsBindingWrapping={false}
|
||||
/>
|
||||
{/key}
|
||||
{:else if mode === Modes.JavaScript}
|
||||
{:else if mode === BindingMode.JavaScript}
|
||||
{#key jsCompletions}
|
||||
<CodeEditor
|
||||
value={decodeJSBinding(jsValue)}
|
||||
value={jsValue ? decodeJSBinding(jsValue) : jsValue}
|
||||
on:change={onChangeJSValue}
|
||||
completions={jsCompletions}
|
||||
mode={EditorModes.JS}
|
||||
|
@ -371,7 +408,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="side" class:visible={!!sidePanel}>
|
||||
{#if sidePanel === SidePanels.Bindings}
|
||||
{#if sidePanel === SidePanel.Bindings}
|
||||
<BindingSidePanel
|
||||
bindings={enrichedBindings}
|
||||
{allowHelpers}
|
||||
|
@ -380,18 +417,15 @@
|
|||
addBinding={onSelectBinding}
|
||||
mode={editorMode}
|
||||
/>
|
||||
{:else if sidePanel === SidePanels.Evaluation}
|
||||
{:else if sidePanel === SidePanel.Evaluation}
|
||||
<EvaluationSidePanel
|
||||
{expressionResult}
|
||||
{expressionError}
|
||||
{evaluating}
|
||||
expression={editorValue}
|
||||
/>
|
||||
{:else if sidePanel === SidePanels.Snippets}
|
||||
<SnippetSidePanel
|
||||
addSnippet={snippet => bindingHelpers.onSelectSnippet(snippet)}
|
||||
{snippets}
|
||||
expression={editorValue ? editorValue : ""}
|
||||
/>
|
||||
{:else if sidePanel === SidePanel.Snippets}
|
||||
<SnippetSidePanel {addSnippet} {snippets} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,28 +1,31 @@
|
|||
<script>
|
||||
import formatHighlight from "json-format-highlight"
|
||||
<script lang="ts">
|
||||
import { JsonFormatter } from "@budibase/frontend-core"
|
||||
import { Icon, ProgressCircle, notifications } from "@budibase/bbui"
|
||||
import { copyToClipboard } from "@budibase/bbui/helpers"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { fade } from "svelte/transition"
|
||||
import { UserScriptError } from "@budibase/string-templates"
|
||||
import type { JSONValue } from "@budibase/types"
|
||||
|
||||
export let expressionResult
|
||||
export let expressionError
|
||||
// this can be essentially any primitive response from the JS function
|
||||
export let expressionResult: JSONValue | undefined = undefined
|
||||
export let expressionError: string | undefined = undefined
|
||||
export let evaluating = false
|
||||
export let expression = null
|
||||
export let expression: string | null = null
|
||||
|
||||
$: error = expressionError != null
|
||||
$: empty = expression == null || expression?.trim() === ""
|
||||
$: success = !error && !empty
|
||||
$: highlightedResult = highlight(expressionResult)
|
||||
|
||||
const formatError = err => {
|
||||
const formatError = (err: any) => {
|
||||
if (err.code === UserScriptError.code) {
|
||||
return err.userScriptError.toString()
|
||||
}
|
||||
return err.toString()
|
||||
}
|
||||
|
||||
const highlight = json => {
|
||||
// json can be any primitive type
|
||||
const highlight = (json?: any | null) => {
|
||||
if (json == null) {
|
||||
return ""
|
||||
}
|
||||
|
@ -31,10 +34,10 @@
|
|||
try {
|
||||
json = JSON.stringify(JSON.parse(json), null, 2)
|
||||
} catch (err) {
|
||||
// Ignore
|
||||
// couldn't parse/stringify, just treat it as the raw input
|
||||
}
|
||||
|
||||
return formatHighlight(json, {
|
||||
return JsonFormatter.format(json, {
|
||||
keyColor: "#e06c75",
|
||||
numberColor: "#e5c07b",
|
||||
stringColor: "#98c379",
|
||||
|
@ -45,11 +48,11 @@
|
|||
}
|
||||
|
||||
const copy = () => {
|
||||
let clipboardVal = expressionResult.result
|
||||
let clipboardVal = expressionResult
|
||||
if (typeof clipboardVal === "object") {
|
||||
clipboardVal = JSON.stringify(clipboardVal, null, 2)
|
||||
}
|
||||
copyToClipboard(clipboardVal)
|
||||
Helpers.copyToClipboard(clipboardVal)
|
||||
notifications.success("Value copied to clipboard")
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -28,7 +28,9 @@
|
|||
let loading = false
|
||||
let deleteConfirmationDialog
|
||||
|
||||
$: defaultName = getSequentialName($snippets, "MySnippet", x => x.name)
|
||||
$: defaultName = getSequentialName($snippets, "MySnippet", {
|
||||
getName: x => x.name,
|
||||
})
|
||||
$: key = snippet?.name
|
||||
$: name = snippet?.name || defaultName
|
||||
$: code = snippet?.code ? encodeJSBinding(snippet.code) : ""
|
||||
|
|
|
@ -43,7 +43,6 @@
|
|||
export let showDataProviders = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const arrayTypes = ["attachment", "array"]
|
||||
|
||||
let anchorRight, dropdownRight
|
||||
let drawer
|
||||
|
@ -116,8 +115,11 @@
|
|||
}
|
||||
})
|
||||
$: fields = bindings
|
||||
.filter(x => arrayTypes.includes(x.fieldSchema?.type))
|
||||
.filter(x => x.fieldSchema?.tableId != null)
|
||||
.filter(
|
||||
x =>
|
||||
x.fieldSchema?.type === "attachment" ||
|
||||
(x.fieldSchema?.type === "array" && x.tableId)
|
||||
)
|
||||
.map(binding => {
|
||||
const { providerId, readableBinding, runtimeBinding } = binding
|
||||
const { name, type, tableId } = binding.fieldSchema
|
||||
|
|
|
@ -16,7 +16,10 @@ export {
|
|||
|
||||
export const AUTO_COLUMN_SUB_TYPES = AutoFieldSubType
|
||||
|
||||
export const AUTO_COLUMN_DISPLAY_NAMES = {
|
||||
export const AUTO_COLUMN_DISPLAY_NAMES: Record<
|
||||
keyof typeof AUTO_COLUMN_SUB_TYPES,
|
||||
string
|
||||
> = {
|
||||
AUTO_ID: "Auto ID",
|
||||
CREATED_BY: "Created By",
|
||||
CREATED_AT: "Created At",
|
||||
|
@ -209,13 +212,6 @@ export const Roles = {
|
|||
BUILDER: "BUILDER",
|
||||
}
|
||||
|
||||
export function isAutoColumnUserRelationship(subtype) {
|
||||
return (
|
||||
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY ||
|
||||
subtype === AUTO_COLUMN_SUB_TYPES.UPDATED_BY
|
||||
)
|
||||
}
|
||||
|
||||
export const PrettyRelationshipDefinitions = {
|
||||
MANY: "Many rows",
|
||||
ONE: "One row",
|
|
@ -10,13 +10,13 @@
|
|||
*
|
||||
* Repl
|
||||
*/
|
||||
export const duplicateName = (name, allNames) => {
|
||||
export const duplicateName = (name: string, allNames: string[]) => {
|
||||
const duplicatePattern = new RegExp(`\\s(\\d+)$`)
|
||||
const baseName = name.split(duplicatePattern)[0]
|
||||
const isDuplicate = new RegExp(`${baseName}\\s(\\d+)$`)
|
||||
|
||||
// get the sequence from matched names
|
||||
const sequence = []
|
||||
const sequence: number[] = []
|
||||
allNames.filter(n => {
|
||||
if (n === baseName) {
|
||||
return true
|
||||
|
@ -70,12 +70,18 @@ export const duplicateName = (name, allNames) => {
|
|||
* @param getName optional function to extract the name for an item, if not a
|
||||
* flat array of strings
|
||||
*/
|
||||
export const getSequentialName = (
|
||||
items,
|
||||
prefix,
|
||||
{ getName = x => x, numberFirstItem = false } = {}
|
||||
export const getSequentialName = <T extends any>(
|
||||
items: T[] | null,
|
||||
prefix: string | null,
|
||||
{
|
||||
getName,
|
||||
numberFirstItem,
|
||||
}: {
|
||||
getName?: (item: T) => string
|
||||
numberFirstItem?: boolean
|
||||
} = {}
|
||||
) => {
|
||||
if (!prefix?.length || !getName) {
|
||||
if (!prefix?.length) {
|
||||
return null
|
||||
}
|
||||
const trimmedPrefix = prefix.trim()
|
||||
|
@ -85,7 +91,7 @@ export const getSequentialName = (
|
|||
}
|
||||
let max = 0
|
||||
items.forEach(item => {
|
||||
const name = getName(item)
|
||||
const name = getName?.(item) ?? item
|
||||
if (typeof name !== "string" || !name.startsWith(trimmedPrefix)) {
|
||||
return
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { FeatureFlag } from "@budibase/types"
|
||||
import { auth } from "../stores/portal"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export const isEnabled = featureFlag => {
|
||||
export const isEnabled = (featureFlag: FeatureFlag | `${FeatureFlag}`) => {
|
||||
const user = get(auth).user
|
||||
return !!user?.flags?.[featureFlag]
|
||||
}
|
|
@ -1,13 +1,21 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
|
||||
export default function (url) {
|
||||
const store = writable({ status: "LOADING", data: {}, error: {} })
|
||||
export default function (url: string) {
|
||||
const store = writable<{
|
||||
status: "LOADING" | "SUCCESS" | "ERROR"
|
||||
data: object
|
||||
error?: unknown
|
||||
}>({
|
||||
status: "LOADING",
|
||||
data: {},
|
||||
error: {},
|
||||
})
|
||||
|
||||
async function get() {
|
||||
store.update(u => ({ ...u, status: "LOADING" }))
|
||||
try {
|
||||
const data = await API.get({ url })
|
||||
const data = await API.get<object>({ url })
|
||||
store.set({ data, status: "SUCCESS" })
|
||||
} catch (e) {
|
||||
store.set({ data: {}, error: e, status: "ERROR" })
|
|
@ -1,46 +0,0 @@
|
|||
import { last, flow } from "lodash/fp"
|
||||
|
||||
export const buildStyle = styles => {
|
||||
let str = ""
|
||||
for (let s in styles) {
|
||||
if (styles[s]) {
|
||||
let key = convertCamel(s)
|
||||
str += `${key}: ${styles[s]}; `
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
export const convertCamel = str => {
|
||||
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
|
||||
}
|
||||
|
||||
export const pipe = (arg, funcs) => flow(funcs)(arg)
|
||||
|
||||
export const capitalise = s => {
|
||||
if (!s) {
|
||||
return s
|
||||
}
|
||||
return s.substring(0, 1).toUpperCase() + s.substring(1)
|
||||
}
|
||||
|
||||
export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
|
||||
|
||||
export const lowercaseExceptFirst = s =>
|
||||
s.charAt(0) + s.substring(1).toLowerCase()
|
||||
|
||||
export const get_name = s => (!s ? "" : last(s.split("/")))
|
||||
|
||||
export const get_capitalised_name = name => pipe(name, [get_name, capitalise])
|
||||
|
||||
export const isBuilderInputFocused = e => {
|
||||
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||
const inCodeEditor = document.activeElement?.classList?.contains("cm-content")
|
||||
if (
|
||||
(inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) &&
|
||||
e.key !== "Escape"
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import type { Many } from "lodash"
|
||||
import { last, flow } from "lodash/fp"
|
||||
|
||||
export const buildStyle = (styles: Record<string, any>) => {
|
||||
let str = ""
|
||||
for (let s in styles) {
|
||||
if (styles[s]) {
|
||||
let key = convertCamel(s)
|
||||
str += `${key}: ${styles[s]}; `
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
export const convertCamel = (str: string) => {
|
||||
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
|
||||
}
|
||||
|
||||
export const pipe = (arg: string, funcs: Many<(...args: any[]) => any>) =>
|
||||
flow(funcs)(arg)
|
||||
|
||||
export const capitalise = (s: string) => {
|
||||
if (!s) {
|
||||
return s
|
||||
}
|
||||
return s.substring(0, 1).toUpperCase() + s.substring(1)
|
||||
}
|
||||
|
||||
export const lowercase = (s: string) =>
|
||||
s.substring(0, 1).toLowerCase() + s.substring(1)
|
||||
|
||||
export const lowercaseExceptFirst = (s: string) =>
|
||||
s.charAt(0) + s.substring(1).toLowerCase()
|
||||
|
||||
export const get_name = (s: string) => (!s ? "" : last(s.split("/")))
|
||||
|
||||
export const get_capitalised_name = (name: string) =>
|
||||
pipe(name, [get_name, capitalise])
|
||||
|
||||
export const isBuilderInputFocused = (e: KeyboardEvent) => {
|
||||
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||
const inCodeEditor = document.activeElement?.classList?.contains("cm-content")
|
||||
if (
|
||||
(inCodeEditor || ["input", "textarea"].indexOf(activeTag!) !== -1) &&
|
||||
e.key !== "Escape"
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
function handleEnter(fnc) {
|
||||
return e => e.key === "Enter" && fnc()
|
||||
}
|
||||
|
||||
export const keyUtils = {
|
||||
handleEnter,
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
function handleEnter(fnc: () => void) {
|
||||
return (e: KeyboardEvent) => e.key === "Enter" && fnc()
|
||||
}
|
||||
|
||||
export const keyUtils = {
|
||||
handleEnter,
|
||||
}
|
|
@ -1,6 +1,16 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
function defaultValue() {
|
||||
interface PaginationStore {
|
||||
nextPage: string | null | undefined
|
||||
page: string | null | undefined
|
||||
hasPrevPage: boolean
|
||||
hasNextPage: boolean
|
||||
loading: boolean
|
||||
pageNumber: number
|
||||
pages: string[]
|
||||
}
|
||||
|
||||
function defaultValue(): PaginationStore {
|
||||
return {
|
||||
nextPage: null,
|
||||
page: undefined,
|
||||
|
@ -29,13 +39,13 @@ export function createPaginationStore() {
|
|||
update(state => {
|
||||
state.pageNumber++
|
||||
state.page = state.nextPage
|
||||
state.pages.push(state.page)
|
||||
state.pages.push(state.page!)
|
||||
state.hasPrevPage = state.pageNumber > 1
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
function fetched(hasNextPage, nextPage) {
|
||||
function fetched(hasNextPage: boolean, nextPage: string) {
|
||||
update(state => {
|
||||
state.hasNextPage = hasNextPage
|
||||
state.nextPage = nextPage
|
|
@ -1,6 +1,6 @@
|
|||
import { PlanType } from "@budibase/types"
|
||||
|
||||
export function getFormattedPlanName(userPlanType) {
|
||||
export function getFormattedPlanName(userPlanType: PlanType) {
|
||||
let planName
|
||||
switch (userPlanType) {
|
||||
case PlanType.PRO:
|
||||
|
@ -29,6 +29,6 @@ export function getFormattedPlanName(userPlanType) {
|
|||
return `${planName} Plan`
|
||||
}
|
||||
|
||||
export function isPremiumOrAbove(userPlanType) {
|
||||
export function isPremiumOrAbove(userPlanType: PlanType) {
|
||||
return ![PlanType.PRO, PlanType.TEAM, PlanType.FREE].includes(userPlanType)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export default function (url) {
|
||||
export default function (url: string) {
|
||||
return url
|
||||
.split("/")
|
||||
.map(part => {
|
|
@ -1,75 +0,0 @@
|
|||
import { FieldType } from "@budibase/types"
|
||||
import { ActionStepID } from "@/constants/backend/automations"
|
||||
import { TableNames } from "@/constants"
|
||||
import {
|
||||
AUTO_COLUMN_DISPLAY_NAMES,
|
||||
AUTO_COLUMN_SUB_TYPES,
|
||||
FIELDS,
|
||||
isAutoColumnUserRelationship,
|
||||
} from "@/constants/backend"
|
||||
import { isEnabled } from "@/helpers/featureFlags"
|
||||
|
||||
export function getAutoColumnInformation(enabled = true) {
|
||||
let info = {}
|
||||
for (const [key, subtype] of Object.entries(AUTO_COLUMN_SUB_TYPES)) {
|
||||
// Because it's possible to replicate the functionality of CREATED_AT and
|
||||
// CREATED_BY columns, we disable their creation when the DEFAULT_VALUES
|
||||
// feature flag is enabled.
|
||||
if (isEnabled("DEFAULT_VALUES")) {
|
||||
if (
|
||||
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_AT ||
|
||||
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY
|
||||
) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
info[subtype] = { enabled, name: AUTO_COLUMN_DISPLAY_NAMES[key] }
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
export function buildAutoColumn(tableName, name, subtype) {
|
||||
let type, constraints
|
||||
switch (subtype) {
|
||||
case AUTO_COLUMN_SUB_TYPES.UPDATED_BY:
|
||||
case AUTO_COLUMN_SUB_TYPES.CREATED_BY:
|
||||
type = FieldType.LINK
|
||||
constraints = FIELDS.LINK.constraints
|
||||
break
|
||||
case AUTO_COLUMN_SUB_TYPES.AUTO_ID:
|
||||
type = FieldType.NUMBER
|
||||
constraints = FIELDS.NUMBER.constraints
|
||||
break
|
||||
case AUTO_COLUMN_SUB_TYPES.UPDATED_AT:
|
||||
case AUTO_COLUMN_SUB_TYPES.CREATED_AT:
|
||||
type = FieldType.DATETIME
|
||||
constraints = FIELDS.DATETIME.constraints
|
||||
break
|
||||
default:
|
||||
type = FieldType.STRING
|
||||
constraints = FIELDS.STRING.constraints
|
||||
break
|
||||
}
|
||||
if (Object.values(AUTO_COLUMN_SUB_TYPES).indexOf(subtype) === -1) {
|
||||
throw "Cannot build auto column with supplied subtype"
|
||||
}
|
||||
const base = {
|
||||
name,
|
||||
type,
|
||||
subtype,
|
||||
icon: "ri-magic-line",
|
||||
autocolumn: true,
|
||||
constraints,
|
||||
}
|
||||
if (isAutoColumnUserRelationship(subtype)) {
|
||||
base.tableId = TableNames.USERS
|
||||
base.fieldName = `${tableName}-${name}`
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
export function checkForCollectStep(automation) {
|
||||
return automation.definition.steps.some(
|
||||
step => step.stepId === ActionStepID.COLLECT
|
||||
)
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
import {
|
||||
AutoFieldSubType,
|
||||
Automation,
|
||||
DateFieldMetadata,
|
||||
FieldType,
|
||||
NumberFieldMetadata,
|
||||
RelationshipFieldMetadata,
|
||||
RelationshipType,
|
||||
} from "@budibase/types"
|
||||
import { ActionStepID } from "@/constants/backend/automations"
|
||||
import { TableNames } from "@/constants"
|
||||
import {
|
||||
AUTO_COLUMN_DISPLAY_NAMES,
|
||||
AUTO_COLUMN_SUB_TYPES,
|
||||
FIELDS,
|
||||
} from "@/constants/backend"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
|
||||
type AutoColumnInformation = Partial<
|
||||
Record<AutoFieldSubType, { enabled: boolean; name: string }>
|
||||
>
|
||||
|
||||
export function getAutoColumnInformation(
|
||||
enabled = true
|
||||
): AutoColumnInformation {
|
||||
const info: AutoColumnInformation = {}
|
||||
for (const [key, subtype] of Object.entries(AUTO_COLUMN_SUB_TYPES)) {
|
||||
// Because it's possible to replicate the functionality of CREATED_AT and
|
||||
// CREATED_BY columns with user column default values, we disable their creation
|
||||
if (
|
||||
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_AT ||
|
||||
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const typedKey = key as keyof typeof AUTO_COLUMN_SUB_TYPES
|
||||
info[subtype] = {
|
||||
enabled,
|
||||
name: AUTO_COLUMN_DISPLAY_NAMES[typedKey],
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
export function buildAutoColumn(
|
||||
tableName: string,
|
||||
name: string,
|
||||
subtype: AutoFieldSubType
|
||||
): RelationshipFieldMetadata | NumberFieldMetadata | DateFieldMetadata {
|
||||
const base = {
|
||||
name,
|
||||
icon: "ri-magic-line",
|
||||
autocolumn: true,
|
||||
}
|
||||
|
||||
switch (subtype) {
|
||||
case AUTO_COLUMN_SUB_TYPES.UPDATED_BY:
|
||||
case AUTO_COLUMN_SUB_TYPES.CREATED_BY:
|
||||
return {
|
||||
...base,
|
||||
type: FieldType.LINK,
|
||||
subtype,
|
||||
constraints: FIELDS.LINK.constraints,
|
||||
tableId: TableNames.USERS,
|
||||
fieldName: `${tableName}-${name}`,
|
||||
relationshipType: RelationshipType.MANY_TO_ONE,
|
||||
}
|
||||
|
||||
case AUTO_COLUMN_SUB_TYPES.AUTO_ID:
|
||||
return {
|
||||
...base,
|
||||
type: FieldType.NUMBER,
|
||||
subtype,
|
||||
constraints: FIELDS.NUMBER.constraints,
|
||||
}
|
||||
case AUTO_COLUMN_SUB_TYPES.UPDATED_AT:
|
||||
case AUTO_COLUMN_SUB_TYPES.CREATED_AT:
|
||||
return {
|
||||
...base,
|
||||
type: FieldType.DATETIME,
|
||||
subtype,
|
||||
constraints: FIELDS.DATETIME.constraints,
|
||||
}
|
||||
|
||||
default:
|
||||
throw utils.unreachable(subtype, {
|
||||
message: "Cannot build auto column with supplied subtype",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function checkForCollectStep(automation: Automation) {
|
||||
return automation.definition.steps.some(
|
||||
step => step.stepId === ActionStepID.COLLECT
|
||||
)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export const suppressWarnings = warnings => {
|
||||
export const suppressWarnings = (warnings: string[]) => {
|
||||
if (!warnings?.length) {
|
||||
return
|
||||
}
|
|
@ -236,13 +236,13 @@
|
|||
}
|
||||
|
||||
if (!role) {
|
||||
await groups.actions.removeApp(target._id, prodAppId)
|
||||
await groups.removeApp(target._id, prodAppId)
|
||||
} else {
|
||||
await groups.actions.addApp(target._id, prodAppId, role)
|
||||
await groups.addApp(target._id, prodAppId, role)
|
||||
}
|
||||
|
||||
await usersFetch.refresh()
|
||||
await groups.actions.init()
|
||||
await groups.init()
|
||||
}
|
||||
|
||||
const onUpdateGroup = async (group, role) => {
|
||||
|
@ -268,7 +268,7 @@
|
|||
if (!group.roles) {
|
||||
return false
|
||||
}
|
||||
return groups.actions.getGroupAppIds(group).includes(appId)
|
||||
return groups.getGroupAppIds(group).includes(appId)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -299,7 +299,7 @@
|
|||
role: group?.builder?.apps.includes(prodAppId)
|
||||
? Constants.Roles.CREATOR
|
||||
: group.roles?.[
|
||||
groups.actions.getGroupAppIds(group).find(x => x === prodAppId)
|
||||
groups.getGroupAppIds(group).find(x => x === prodAppId)
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -442,13 +442,11 @@
|
|||
|
||||
const onUpdateUserInvite = async (invite, role) => {
|
||||
let updateBody = {
|
||||
code: invite.code,
|
||||
apps: {
|
||||
...invite.apps,
|
||||
[prodAppId]: role,
|
||||
},
|
||||
}
|
||||
|
||||
if (role === Constants.Roles.CREATOR) {
|
||||
updateBody.builder = updateBody.builder || {}
|
||||
updateBody.builder.apps = [...(updateBody.builder.apps ?? []), prodAppId]
|
||||
|
@ -456,7 +454,7 @@
|
|||
} else if (role !== Constants.Roles.CREATOR && invite?.builder?.apps) {
|
||||
invite.builder.apps = []
|
||||
}
|
||||
await users.updateInvite(updateBody)
|
||||
await users.updateInvite(invite.code, updateBody)
|
||||
await filterInvites(query)
|
||||
}
|
||||
|
||||
|
@ -470,8 +468,7 @@
|
|||
let updated = { ...invite }
|
||||
delete updated.info.apps[prodAppId]
|
||||
|
||||
return await users.updateInvite({
|
||||
code: updated.code,
|
||||
return await users.updateInvite(updated.code, {
|
||||
apps: updated.apps,
|
||||
})
|
||||
}
|
||||
|
@ -485,12 +482,12 @@
|
|||
}
|
||||
|
||||
const removeGroupAppBuilder = async groupId => {
|
||||
await groups.actions.removeGroupAppBuilder(groupId, prodAppId)
|
||||
await groups.removeGroupAppBuilder(groupId, prodAppId)
|
||||
}
|
||||
|
||||
const initSidePanel = async sidePaneOpen => {
|
||||
if (sidePaneOpen === true) {
|
||||
await groups.actions.init()
|
||||
await groups.init()
|
||||
}
|
||||
loaded = true
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { viewsV2, rowActions } from "@/stores/builder"
|
||||
import { admin, themeStore, featureFlags } from "@/stores/portal"
|
||||
import { admin, themeStore, licensing } from "@/stores/portal"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
import { API } from "@/api"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
@ -53,7 +53,7 @@
|
|||
{buttons}
|
||||
allowAddRows
|
||||
allowDeleteRows
|
||||
aiEnabled={$featureFlags.BUDIBASE_AI || $featureFlags.AI_CUSTOM_CONFIGS}
|
||||
aiEnabled={$licensing.customAIConfigsEnabled || $licensing.budibaseAiEnabled}
|
||||
showAvatars={false}
|
||||
on:updatedatasource={handleGridViewUpdate}
|
||||
isCloud={$admin.cloud}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
rowActions,
|
||||
roles,
|
||||
} from "@/stores/builder"
|
||||
import { themeStore, admin, featureFlags } from "@/stores/portal"
|
||||
import { themeStore, admin, licensing } from "@/stores/portal"
|
||||
import { TableNames } from "@/constants"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
import { API } from "@/api"
|
||||
|
@ -130,7 +130,8 @@
|
|||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||
showAvatars={false}
|
||||
isCloud={$admin.cloud}
|
||||
aiEnabled={$featureFlags.BUDIBASE_AI || $featureFlags.AI_CUSTOM_CONFIGS}
|
||||
aiEnabled={$licensing.customAIConfigsEnabled ||
|
||||
$licensing.budibaseAIEnabled}
|
||||
{buttons}
|
||||
buttonsCollapsed
|
||||
on:updatedatasource={handleGridTableUpdate}
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
}
|
||||
if (!Object.keys(user?.roles).length && user?.userGroups) {
|
||||
return userGroups.find(group => {
|
||||
return groups.actions
|
||||
return groups
|
||||
.getGroupAppIds(group)
|
||||
.map(role => appsStore.extractAppId(role))
|
||||
.includes(app.appId)
|
||||
|
@ -86,7 +86,7 @@
|
|||
try {
|
||||
await organisation.init()
|
||||
await appsStore.load()
|
||||
await groups.actions.init()
|
||||
await groups.init()
|
||||
} catch (error) {
|
||||
notifications.error("Error loading apps")
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
promises.push(templates.load())
|
||||
}
|
||||
|
||||
promises.push(groups.actions.init())
|
||||
promises.push(groups.init())
|
||||
|
||||
// Always load latest
|
||||
await Promise.all(promises)
|
||||
|
|
|
@ -191,8 +191,14 @@
|
|||
? "View errors"
|
||||
: "View error"}
|
||||
on:dismiss={async () => {
|
||||
await automationStore.actions.clearLogErrors({ appId })
|
||||
await appsStore.load()
|
||||
const automationId = Object.keys(automationErrors[appId] || {})[0]
|
||||
if (automationId) {
|
||||
await automationStore.actions.clearLogErrors({
|
||||
appId,
|
||||
automationId,
|
||||
})
|
||||
await appsStore.load()
|
||||
}
|
||||
}}
|
||||
message={automationErrorMessage(appId)}
|
||||
/>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
Tags,
|
||||
Tag,
|
||||
} from "@budibase/bbui"
|
||||
import { admin, licensing, featureFlags } from "@/stores/portal"
|
||||
import { admin, licensing } from "@/stores/portal"
|
||||
import { API } from "@/api"
|
||||
import AIConfigModal from "./ConfigModal.svelte"
|
||||
import AIConfigTile from "./AIConfigTile.svelte"
|
||||
|
@ -27,8 +27,7 @@
|
|||
let editingUuid
|
||||
|
||||
$: isCloud = $admin.cloud
|
||||
$: customAIConfigsEnabled =
|
||||
$featureFlags.AI_CUSTOM_CONFIGS && $licensing.customAIConfigsEnabled
|
||||
$: customAIConfigsEnabled = $licensing.customAIConfigsEnabled
|
||||
|
||||
async function fetchAIConfig() {
|
||||
try {
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
import { featureFlags } from "@/stores/portal"
|
||||
|
||||
if ($featureFlags.AI_CUSTOM_CONFIGS) {
|
||||
$redirect("./ai")
|
||||
} else {
|
||||
$redirect("./auth")
|
||||
}
|
||||
$redirect("./ai")
|
||||
</script>
|
||||
|
|
|
@ -53,9 +53,7 @@
|
|||
$: readonly = !isAdmin || isScimGroup
|
||||
$: groupApps = $appsStore.apps
|
||||
.filter(app =>
|
||||
groups.actions
|
||||
.getGroupAppIds(group)
|
||||
.includes(appsStore.getProdAppID(app.devId))
|
||||
groups.getGroupAppIds(group).includes(appsStore.getProdAppID(app.devId))
|
||||
)
|
||||
.map(app => ({
|
||||
...app,
|
||||
|
@ -72,7 +70,7 @@
|
|||
|
||||
async function deleteGroup() {
|
||||
try {
|
||||
await groups.actions.delete(group)
|
||||
await groups.delete(group)
|
||||
notifications.success("User group deleted successfully")
|
||||
$goto("./")
|
||||
} catch (error) {
|
||||
|
@ -82,7 +80,7 @@
|
|||
|
||||
async function saveGroup(group) {
|
||||
try {
|
||||
await groups.actions.save(group)
|
||||
await groups.save(group)
|
||||
} catch (error) {
|
||||
if (error.message) {
|
||||
notifications.error(error.message)
|
||||
|
@ -93,7 +91,7 @@
|
|||
}
|
||||
|
||||
const removeApp = async app => {
|
||||
await groups.actions.removeApp(groupId, appsStore.getProdAppID(app.devId))
|
||||
await groups.removeApp(groupId, appsStore.getProdAppID(app.devId))
|
||||
}
|
||||
setContext("roles", {
|
||||
updateRole: () => {},
|
||||
|
@ -102,7 +100,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await Promise.all([groups.actions.init(), roles.fetch()])
|
||||
await Promise.all([groups.init(), roles.fetch()])
|
||||
loaded = true
|
||||
} catch (error) {
|
||||
notifications.error("Error fetching user group data")
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
return keepOpen
|
||||
} else {
|
||||
await groups.actions.addApp(group._id, prodAppId, selectedRoleId)
|
||||
await groups.addApp(group._id, prodAppId, selectedRoleId)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -50,11 +50,11 @@
|
|||
selected={group.users?.map(user => user._id)}
|
||||
list={$users.data}
|
||||
on:select={async e => {
|
||||
await groups.actions.addUser(groupId, e.detail)
|
||||
await groups.addUser(groupId, e.detail)
|
||||
onUsersUpdated()
|
||||
}}
|
||||
on:deselect={async e => {
|
||||
await groups.actions.removeUser(groupId, e.detail)
|
||||
await groups.removeUser(groupId, e.detail)
|
||||
onUsersUpdated()
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
]
|
||||
|
||||
const removeUser = async id => {
|
||||
await groups.actions.removeUser(groupId, id)
|
||||
await groups.removeUser(groupId, id)
|
||||
fetchGroupUsers.refresh()
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
|
||||
async function saveGroup(group) {
|
||||
try {
|
||||
group = await groups.actions.save(group)
|
||||
group = await groups.save(group)
|
||||
$goto(`./${group._id}`)
|
||||
notifications.success(`User group created successfully`)
|
||||
} catch (error) {
|
||||
|
@ -83,7 +83,7 @@
|
|||
try {
|
||||
// always load latest
|
||||
await licensing.init()
|
||||
await groups.actions.init()
|
||||
await groups.init()
|
||||
} catch (error) {
|
||||
notifications.error("Error getting user groups")
|
||||
}
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
let popover
|
||||
let user, tenantOwner
|
||||
let loaded = false
|
||||
let userFieldsToUpdate = {}
|
||||
|
||||
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
|
||||
|
||||
|
@ -164,40 +165,45 @@
|
|||
return label
|
||||
}
|
||||
|
||||
async function updateUserFirstName(evt) {
|
||||
async function saveUser() {
|
||||
try {
|
||||
await users.save({ ...user, firstName: evt.target.value })
|
||||
await users.save({ ...user, ...userFieldsToUpdate })
|
||||
userFieldsToUpdate = {}
|
||||
await fetchUser()
|
||||
} catch (error) {
|
||||
notifications.error("Error updating user")
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUserFirstName(evt) {
|
||||
userFieldsToUpdate.firstName = evt.target.value
|
||||
}
|
||||
|
||||
async function updateUserLastName(evt) {
|
||||
try {
|
||||
await users.save({ ...user, lastName: evt.target.value })
|
||||
await fetchUser()
|
||||
} catch (error) {
|
||||
notifications.error("Error updating user")
|
||||
}
|
||||
userFieldsToUpdate.lastName = evt.target.value
|
||||
}
|
||||
|
||||
async function updateUserRole({ detail }) {
|
||||
let flags = {}
|
||||
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) {
|
||||
toggleFlags({ admin: { global: true }, builder: { global: true } })
|
||||
flags = { admin: { global: true }, builder: { global: true } }
|
||||
} 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) {
|
||||
toggleFlags({
|
||||
flags = {
|
||||
admin: { global: false },
|
||||
builder: {
|
||||
global: false,
|
||||
creator: true,
|
||||
apps: user?.builder?.apps || [],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
userFieldsToUpdate = {
|
||||
...userFieldsToUpdate,
|
||||
...flags,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,22 +215,13 @@
|
|||
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 => {
|
||||
await groups.actions.addUser(groupId, userId)
|
||||
await groups.addUser(groupId, userId)
|
||||
await fetchUser()
|
||||
}
|
||||
|
||||
const removeGroup = async groupId => {
|
||||
await groups.actions.removeUser(groupId, userId)
|
||||
await groups.removeUser(groupId, userId)
|
||||
await fetchUser()
|
||||
}
|
||||
|
||||
|
@ -234,7 +231,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await Promise.all([fetchUser(), groups.actions.init(), roles.fetch()])
|
||||
await Promise.all([fetchUser(), groups.init(), roles.fetch()])
|
||||
loaded = true
|
||||
} catch (error) {
|
||||
notifications.error("Error getting user groups")
|
||||
|
@ -296,7 +293,7 @@
|
|||
<Input
|
||||
disabled={readonly}
|
||||
value={user?.firstName}
|
||||
on:blur={updateUserFirstName}
|
||||
on:input={updateUserFirstName}
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
@ -304,7 +301,7 @@
|
|||
<Input
|
||||
disabled={readonly}
|
||||
value={user?.lastName}
|
||||
on:blur={updateUserLastName}
|
||||
on:input={updateUserLastName}
|
||||
/>
|
||||
</div>
|
||||
<!-- don't let a user remove the privileges that let them be here -->
|
||||
|
@ -325,6 +322,13 @@
|
|||
{/if}
|
||||
</div>
|
||||
</Layout>
|
||||
<div>
|
||||
<Button
|
||||
cta
|
||||
disabled={Object.keys(userFieldsToUpdate).length === 0}
|
||||
on:click={saveUser}>Save</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if $licensing.groupsEnabled}
|
||||
<!-- User groups -->
|
||||
|
|
|
@ -247,10 +247,11 @@
|
|||
try {
|
||||
bulkSaveResponse = await users.create(await removingDuplicities(userData))
|
||||
notifications.success("Successfully created user")
|
||||
await groups.actions.init()
|
||||
await groups.init()
|
||||
passwordModal.show()
|
||||
await fetch.refresh()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error creating user")
|
||||
}
|
||||
}
|
||||
|
@ -317,7 +318,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await groups.actions.init()
|
||||
await groups.init()
|
||||
groupsLoaded = true
|
||||
} catch (error) {
|
||||
notifications.error("Error fetching user group data")
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
|
||||
export function createPermissionStore() {
|
||||
const { subscribe } = writable([])
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
save: async ({ level, role, resource }) => {
|
||||
return await API.updatePermissionForResource(resource, role, level)
|
||||
},
|
||||
remove: async ({ level, role, resource }) => {
|
||||
return await API.removePermissionFromResource(resource, role, level)
|
||||
},
|
||||
forResource: async resourceId => {
|
||||
return (await API.getPermissionForResource(resourceId)).permissions
|
||||
},
|
||||
forResourceDetailed: async resourceId => {
|
||||
return await API.getPermissionForResource(resourceId)
|
||||
},
|
||||
getDependantsInfo: async resourceId => {
|
||||
return await API.getDependants(resourceId)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const permissions = createPermissionStore()
|
|
@ -0,0 +1,50 @@
|
|||
import { BudiStore } from "../BudiStore"
|
||||
import { API } from "@/api"
|
||||
import {
|
||||
PermissionLevel,
|
||||
GetResourcePermsResponse,
|
||||
GetDependantResourcesResponse,
|
||||
ResourcePermissionInfo,
|
||||
} from "@budibase/types"
|
||||
|
||||
interface Permission {
|
||||
level: PermissionLevel
|
||||
role: string
|
||||
resource: string
|
||||
}
|
||||
|
||||
export class PermissionStore extends BudiStore<Permission[]> {
|
||||
constructor() {
|
||||
super([])
|
||||
}
|
||||
|
||||
save = async (permission: Permission) => {
|
||||
const { level, role, resource } = permission
|
||||
return await API.updatePermissionForResource(resource, role, level)
|
||||
}
|
||||
|
||||
remove = async (permission: Permission) => {
|
||||
const { level, role, resource } = permission
|
||||
return await API.removePermissionFromResource(resource, role, level)
|
||||
}
|
||||
|
||||
forResource = async (
|
||||
resourceId: string
|
||||
): Promise<Record<string, ResourcePermissionInfo>> => {
|
||||
return (await API.getPermissionForResource(resourceId)).permissions
|
||||
}
|
||||
|
||||
forResourceDetailed = async (
|
||||
resourceId: string
|
||||
): Promise<GetResourcePermsResponse> => {
|
||||
return await API.getPermissionForResource(resourceId)
|
||||
}
|
||||
|
||||
getDependantsInfo = async (
|
||||
resourceId: string
|
||||
): Promise<GetDependantResourcesResponse> => {
|
||||
return await API.getDependants(resourceId)
|
||||
}
|
||||
}
|
||||
|
||||
export const permissions = new PermissionStore()
|
|
@ -1,13 +1,16 @@
|
|||
import { appStore } from "./app"
|
||||
import { appsStore } from "@/stores/portal/apps"
|
||||
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]) => {
|
||||
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
|
||||
}
|
||||
)
|
|
@ -1,130 +0,0 @@
|
|||
import { writable, get, derived } from "svelte/store"
|
||||
import { datasources } from "./datasources"
|
||||
import { integrations } from "./integrations"
|
||||
import { API } from "@/api"
|
||||
import { duplicateName } from "@/helpers/duplicate"
|
||||
|
||||
const sortQueries = queryList => {
|
||||
queryList.sort((q1, q2) => {
|
||||
return q1.name.localeCompare(q2.name)
|
||||
})
|
||||
}
|
||||
|
||||
export function createQueriesStore() {
|
||||
const store = writable({
|
||||
list: [],
|
||||
selectedQueryId: null,
|
||||
})
|
||||
const derivedStore = derived(store, $store => ({
|
||||
...$store,
|
||||
selected: $store.list?.find(q => q._id === $store.selectedQueryId),
|
||||
}))
|
||||
|
||||
const fetch = async () => {
|
||||
const queries = await API.getQueries()
|
||||
sortQueries(queries)
|
||||
store.update(state => ({
|
||||
...state,
|
||||
list: queries,
|
||||
}))
|
||||
}
|
||||
|
||||
const save = async (datasourceId, query) => {
|
||||
const _integrations = get(integrations)
|
||||
const dataSource = get(datasources).list.filter(
|
||||
ds => ds._id === datasourceId
|
||||
)
|
||||
// Check if readable attribute is found
|
||||
if (dataSource.length !== 0) {
|
||||
const integration = _integrations[dataSource[0].source]
|
||||
const readable = integration.query[query.queryVerb].readable
|
||||
if (readable) {
|
||||
query.readable = readable
|
||||
}
|
||||
}
|
||||
query.datasourceId = datasourceId
|
||||
const savedQuery = await API.saveQuery(query)
|
||||
store.update(state => {
|
||||
const idx = state.list.findIndex(query => query._id === savedQuery._id)
|
||||
const queries = state.list
|
||||
if (idx >= 0) {
|
||||
queries.splice(idx, 1, savedQuery)
|
||||
} else {
|
||||
queries.push(savedQuery)
|
||||
}
|
||||
sortQueries(queries)
|
||||
return {
|
||||
list: queries,
|
||||
selectedQueryId: savedQuery._id,
|
||||
}
|
||||
})
|
||||
return savedQuery
|
||||
}
|
||||
|
||||
const importQueries = async ({ data, datasourceId }) => {
|
||||
return await API.importQueries(datasourceId, data)
|
||||
}
|
||||
|
||||
const select = id => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
selectedQueryId: id,
|
||||
}))
|
||||
}
|
||||
|
||||
const preview = async query => {
|
||||
const result = await API.previewQuery(query)
|
||||
// Assume all the fields are strings and create a basic schema from the
|
||||
// unique fields returned by the server
|
||||
const schema = {}
|
||||
for (let [field, metadata] of Object.entries(result.schema)) {
|
||||
schema[field] = metadata || { type: "string" }
|
||||
}
|
||||
return { ...result, schema, rows: result.rows || [] }
|
||||
}
|
||||
|
||||
const deleteQuery = async query => {
|
||||
await API.deleteQuery(query._id, query._rev)
|
||||
store.update(state => {
|
||||
state.list = state.list.filter(existing => existing._id !== query._id)
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const duplicate = async query => {
|
||||
let list = get(store).list
|
||||
const newQuery = { ...query }
|
||||
const datasourceId = query.datasourceId
|
||||
|
||||
delete newQuery._id
|
||||
delete newQuery._rev
|
||||
newQuery.name = duplicateName(
|
||||
query.name,
|
||||
list.map(q => q.name)
|
||||
)
|
||||
|
||||
return await save(datasourceId, newQuery)
|
||||
}
|
||||
|
||||
const removeDatasourceQueries = datasourceId => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
list: state.list.filter(table => table.datasourceId !== datasourceId),
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: derivedStore.subscribe,
|
||||
fetch,
|
||||
init: fetch,
|
||||
select,
|
||||
save,
|
||||
import: importQueries,
|
||||
delete: deleteQuery,
|
||||
preview,
|
||||
duplicate,
|
||||
removeDatasourceQueries,
|
||||
}
|
||||
}
|
||||
|
||||
export const queries = createQueriesStore()
|
|
@ -0,0 +1,156 @@
|
|||
import { derived, get, Writable } from "svelte/store"
|
||||
import { datasources } from "./datasources"
|
||||
import { integrations } from "./integrations"
|
||||
import { API } from "@/api"
|
||||
import { duplicateName } from "@/helpers/duplicate"
|
||||
import { DerivedBudiStore } from "@/stores/BudiStore"
|
||||
import {
|
||||
Query,
|
||||
QueryPreview,
|
||||
PreviewQueryResponse,
|
||||
SaveQueryRequest,
|
||||
ImportRestQueryRequest,
|
||||
QuerySchema,
|
||||
} from "@budibase/types"
|
||||
|
||||
const sortQueries = (queryList: Query[]) => {
|
||||
queryList.sort((q1, q2) => {
|
||||
return q1.name.localeCompare(q2.name)
|
||||
})
|
||||
}
|
||||
|
||||
interface BuilderQueryStore {
|
||||
list: Query[]
|
||||
selectedQueryId: string | null
|
||||
}
|
||||
|
||||
interface DerivedQueryStore extends BuilderQueryStore {
|
||||
selected?: Query
|
||||
}
|
||||
|
||||
export class QueryStore extends DerivedBudiStore<
|
||||
BuilderQueryStore,
|
||||
DerivedQueryStore
|
||||
> {
|
||||
constructor() {
|
||||
const makeDerivedStore = (store: Writable<BuilderQueryStore>) => {
|
||||
return derived(store, ($store): DerivedQueryStore => {
|
||||
return {
|
||||
list: $store.list,
|
||||
selectedQueryId: $store.selectedQueryId,
|
||||
selected: $store.list?.find(q => q._id === $store.selectedQueryId),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
super(
|
||||
{
|
||||
list: [],
|
||||
selectedQueryId: null,
|
||||
},
|
||||
makeDerivedStore
|
||||
)
|
||||
|
||||
this.select = this.select.bind(this)
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
const queries = await API.getQueries()
|
||||
sortQueries(queries)
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
list: queries,
|
||||
}))
|
||||
}
|
||||
|
||||
async save(datasourceId: string, query: SaveQueryRequest) {
|
||||
const _integrations = get(integrations)
|
||||
const dataSource = get(datasources).list.filter(
|
||||
ds => ds._id === datasourceId
|
||||
)
|
||||
// Check if readable attribute is found
|
||||
if (dataSource.length !== 0) {
|
||||
const integration = _integrations[dataSource[0].source]
|
||||
const readable = integration.query[query.queryVerb].readable
|
||||
if (readable) {
|
||||
query.readable = readable
|
||||
}
|
||||
}
|
||||
query.datasourceId = datasourceId
|
||||
const savedQuery = await API.saveQuery(query)
|
||||
this.store.update(state => {
|
||||
const idx = state.list.findIndex(query => query._id === savedQuery._id)
|
||||
const queries = state.list
|
||||
if (idx >= 0) {
|
||||
queries.splice(idx, 1, savedQuery)
|
||||
} else {
|
||||
queries.push(savedQuery)
|
||||
}
|
||||
sortQueries(queries)
|
||||
return {
|
||||
list: queries,
|
||||
selectedQueryId: savedQuery._id || null,
|
||||
}
|
||||
})
|
||||
return savedQuery
|
||||
}
|
||||
|
||||
async importQueries(data: ImportRestQueryRequest) {
|
||||
return await API.importQueries(data)
|
||||
}
|
||||
|
||||
select(id: string | null) {
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
selectedQueryId: id,
|
||||
}))
|
||||
}
|
||||
|
||||
async preview(query: QueryPreview): Promise<PreviewQueryResponse> {
|
||||
const result = await API.previewQuery(query)
|
||||
// Assume all the fields are strings and create a basic schema from the
|
||||
// unique fields returned by the server
|
||||
const schema: Record<string, QuerySchema> = {}
|
||||
for (let [field, metadata] of Object.entries(result.schema)) {
|
||||
schema[field] = (metadata as QuerySchema) || { type: "string" }
|
||||
}
|
||||
return { ...result, schema, rows: result.rows || [] }
|
||||
}
|
||||
|
||||
async delete(query: Query) {
|
||||
if (!query._id || !query._rev) {
|
||||
throw new Error("Query ID or Revision is missing")
|
||||
}
|
||||
await API.deleteQuery(query._id, query._rev)
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
list: state.list.filter(existing => existing._id !== query._id),
|
||||
}))
|
||||
}
|
||||
|
||||
async duplicate(query: Query) {
|
||||
let list = get(this.store).list
|
||||
const newQuery = { ...query }
|
||||
const datasourceId = query.datasourceId
|
||||
|
||||
delete newQuery._id
|
||||
delete newQuery._rev
|
||||
newQuery.name = duplicateName(
|
||||
query.name,
|
||||
list.map(q => q.name)
|
||||
)
|
||||
|
||||
return await this.save(datasourceId, newQuery)
|
||||
}
|
||||
|
||||
removeDatasourceQueries(datasourceId: string) {
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
list: state.list.filter(table => table.datasourceId !== datasourceId),
|
||||
}))
|
||||
}
|
||||
|
||||
init = this.fetch
|
||||
}
|
||||
|
||||
export const queries = new QueryStore()
|
|
@ -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()
|
|
@ -62,7 +62,7 @@ export class RowActionStore extends BudiStore<RowActionState> {
|
|||
const existingRowActions = get(this)[tableId] || []
|
||||
name = getSequentialName(existingRowActions, "New row action ", {
|
||||
getName: x => x.name,
|
||||
})
|
||||
})!
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
import { writable, derived } from "svelte/store"
|
||||
import { tables } from "./tables"
|
||||
import { API } from "@/api"
|
||||
|
||||
export function createViewsStore() {
|
||||
const store = writable({
|
||||
selectedViewName: null,
|
||||
})
|
||||
const derivedStore = derived([store, tables], ([$store, $tables]) => {
|
||||
let list = []
|
||||
$tables.list?.forEach(table => {
|
||||
const views = Object.values(table?.views || {}).filter(view => {
|
||||
return view.version !== 2
|
||||
})
|
||||
list = list.concat(views)
|
||||
})
|
||||
return {
|
||||
...$store,
|
||||
list,
|
||||
selected: list.find(view => view.name === $store.selectedViewName),
|
||||
}
|
||||
})
|
||||
|
||||
const select = name => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
selectedViewName: name,
|
||||
}))
|
||||
}
|
||||
|
||||
const deleteView = async view => {
|
||||
await API.deleteView(view.name)
|
||||
|
||||
// Update tables
|
||||
tables.update(state => {
|
||||
const table = state.list.find(table => table._id === view.tableId)
|
||||
delete table.views[view.name]
|
||||
return { ...state }
|
||||
})
|
||||
}
|
||||
|
||||
const save = async view => {
|
||||
const savedView = await API.saveView(view)
|
||||
select(view.name)
|
||||
|
||||
// Update tables
|
||||
tables.update(state => {
|
||||
const table = state.list.find(table => table._id === view.tableId)
|
||||
if (table) {
|
||||
if (view.originalName) {
|
||||
delete table.views[view.originalName]
|
||||
}
|
||||
table.views[view.name] = savedView
|
||||
}
|
||||
return { ...state }
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: derivedStore.subscribe,
|
||||
select,
|
||||
delete: deleteView,
|
||||
save,
|
||||
}
|
||||
}
|
||||
|
||||
export const views = createViewsStore()
|
|
@ -0,0 +1,94 @@
|
|||
import { DerivedBudiStore } from "../BudiStore"
|
||||
import { tables } from "./tables"
|
||||
import { API } from "@/api"
|
||||
import { View } from "@budibase/types"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
import { derived, Writable } from "svelte/store"
|
||||
|
||||
interface BuilderViewStore {
|
||||
selectedViewName: string | null
|
||||
}
|
||||
|
||||
interface DerivedViewStore extends BuilderViewStore {
|
||||
list: View[]
|
||||
selected?: View
|
||||
}
|
||||
|
||||
export class ViewsStore extends DerivedBudiStore<
|
||||
BuilderViewStore,
|
||||
DerivedViewStore
|
||||
> {
|
||||
constructor() {
|
||||
const makeDerivedStore = (store: Writable<BuilderViewStore>) => {
|
||||
return derived([store, tables], ([$store, $tables]): DerivedViewStore => {
|
||||
let list: View[] = []
|
||||
$tables.list?.forEach(table => {
|
||||
const views = Object.values(table?.views || {}).filter(
|
||||
(view): view is View => !helpers.views.isV2(view)
|
||||
)
|
||||
list = list.concat(views)
|
||||
})
|
||||
return {
|
||||
selectedViewName: $store.selectedViewName,
|
||||
list,
|
||||
selected: list.find(view => view.name === $store.selectedViewName),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
super(
|
||||
{
|
||||
selectedViewName: null,
|
||||
},
|
||||
makeDerivedStore
|
||||
)
|
||||
|
||||
this.select = this.select.bind(this)
|
||||
}
|
||||
|
||||
select = (name: string) => {
|
||||
this.store.update(state => ({
|
||||
...state,
|
||||
selectedViewName: name,
|
||||
}))
|
||||
}
|
||||
|
||||
delete = async (view: View) => {
|
||||
if (!view.name) {
|
||||
throw new Error("View name is required")
|
||||
}
|
||||
await API.deleteView(view.name)
|
||||
|
||||
// Update tables
|
||||
tables.update(state => {
|
||||
const table = state.list.find(table => table._id === view.tableId)
|
||||
if (table?.views && view.name) {
|
||||
delete table.views[view.name]
|
||||
}
|
||||
return { ...state }
|
||||
})
|
||||
}
|
||||
|
||||
save = async (view: View & { originalName?: string }) => {
|
||||
if (!view.name) {
|
||||
throw new Error("View name is required")
|
||||
}
|
||||
|
||||
const savedView = await API.saveView(view)
|
||||
this.select(view.name)
|
||||
|
||||
// Update tables
|
||||
tables.update(state => {
|
||||
const table = state.list.find(table => table._id === view.tableId)
|
||||
if (table?.views && view.name) {
|
||||
if (view.originalName) {
|
||||
delete table.views[view.originalName]
|
||||
}
|
||||
table.views[view.name] = savedView
|
||||
}
|
||||
return { ...state }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const views = new ViewsStore()
|
|
@ -1,103 +0,0 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { licensing } from "@/stores/portal"
|
||||
|
||||
export function createGroupsStore() {
|
||||
const store = writable([])
|
||||
|
||||
const updateStore = group => {
|
||||
store.update(state => {
|
||||
const currentIdx = state.findIndex(gr => gr._id === group._id)
|
||||
if (currentIdx >= 0) {
|
||||
state.splice(currentIdx, 1, group)
|
||||
} else {
|
||||
state.push(group)
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const getGroup = async groupId => {
|
||||
const group = await API.getGroup(groupId)
|
||||
updateStore(group)
|
||||
}
|
||||
|
||||
const actions = {
|
||||
init: async () => {
|
||||
// only init if there is a groups license, just to be sure but the feature will be blocked
|
||||
// on the backend anyway
|
||||
if (get(licensing).groupsEnabled) {
|
||||
const groups = await API.getGroups()
|
||||
store.set(groups.data)
|
||||
}
|
||||
},
|
||||
|
||||
get: getGroup,
|
||||
|
||||
save: async group => {
|
||||
const { ...dataToSave } = group
|
||||
delete dataToSave.scimInfo
|
||||
delete dataToSave.userGroups
|
||||
const response = await API.saveGroup(dataToSave)
|
||||
group._id = response._id
|
||||
group._rev = response._rev
|
||||
updateStore(group)
|
||||
return group
|
||||
},
|
||||
|
||||
delete: async group => {
|
||||
await API.deleteGroup(group._id, group._rev)
|
||||
store.update(state => {
|
||||
state = state.filter(state => state._id !== group._id)
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
||||
addUser: async (groupId, userId) => {
|
||||
await API.addUsersToGroup(groupId, userId)
|
||||
// refresh the group enrichment
|
||||
await getGroup(groupId)
|
||||
},
|
||||
|
||||
removeUser: async (groupId, userId) => {
|
||||
await API.removeUsersFromGroup(groupId, userId)
|
||||
// refresh the group enrichment
|
||||
await getGroup(groupId)
|
||||
},
|
||||
|
||||
addApp: async (groupId, appId, roleId) => {
|
||||
await API.addAppsToGroup(groupId, [{ appId, roleId }])
|
||||
// refresh the group roles
|
||||
await getGroup(groupId)
|
||||
},
|
||||
|
||||
removeApp: async (groupId, appId) => {
|
||||
await API.removeAppsFromGroup(groupId, [{ appId }])
|
||||
// refresh the group roles
|
||||
await getGroup(groupId)
|
||||
},
|
||||
|
||||
getGroupAppIds: group => {
|
||||
let groupAppIds = Object.keys(group?.roles || {})
|
||||
if (group?.builder?.apps) {
|
||||
groupAppIds = groupAppIds.concat(group.builder.apps)
|
||||
}
|
||||
return groupAppIds
|
||||
},
|
||||
|
||||
addGroupAppBuilder: async (groupId, appId) => {
|
||||
return await API.addGroupAppBuilder(groupId, appId)
|
||||
},
|
||||
|
||||
removeGroupAppBuilder: async (groupId, appId) => {
|
||||
return await API.removeGroupAppBuilder(groupId, appId)
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
actions,
|
||||
}
|
||||
}
|
||||
|
||||
export const groups = createGroupsStore()
|
|
@ -0,0 +1,96 @@
|
|||
import { get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { licensing } from "@/stores/portal"
|
||||
import { UserGroup } from "@budibase/types"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
|
||||
class GroupStore extends BudiStore<UserGroup[]> {
|
||||
constructor() {
|
||||
super([])
|
||||
}
|
||||
|
||||
updateStore = (group: UserGroup) => {
|
||||
this.update(state => {
|
||||
const currentIdx = state.findIndex(gr => gr._id === group._id)
|
||||
if (currentIdx >= 0) {
|
||||
state.splice(currentIdx, 1, group)
|
||||
} else {
|
||||
state.push(group)
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Only init if there is a groups license, just to be sure but the feature will be blocked
|
||||
// on the backend anyway
|
||||
if (get(licensing).groupsEnabled) {
|
||||
const groups = await API.getGroups()
|
||||
this.set(groups)
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshGroup(groupId: string) {
|
||||
const group = await API.getGroup(groupId)
|
||||
this.updateStore(group)
|
||||
}
|
||||
|
||||
async save(group: UserGroup) {
|
||||
const { ...dataToSave } = group
|
||||
delete dataToSave.scimInfo
|
||||
const response = await API.saveGroup(dataToSave)
|
||||
group._id = response._id
|
||||
group._rev = response._rev
|
||||
this.updateStore(group)
|
||||
return group
|
||||
}
|
||||
|
||||
async delete(group: UserGroup) {
|
||||
await API.deleteGroup(group._id!, group._rev!)
|
||||
this.update(groups => {
|
||||
const index = groups.findIndex(g => g._id === group._id)
|
||||
if (index !== -1) {
|
||||
groups.splice(index, 1)
|
||||
}
|
||||
return groups
|
||||
})
|
||||
}
|
||||
|
||||
async addUser(groupId: string, userId: string) {
|
||||
await API.addUsersToGroup(groupId, [userId])
|
||||
await this.refreshGroup(groupId)
|
||||
}
|
||||
|
||||
async removeUser(groupId: string, userId: string) {
|
||||
await API.removeUsersFromGroup(groupId, [userId])
|
||||
await this.refreshGroup(groupId)
|
||||
}
|
||||
|
||||
async addApp(groupId: string, appId: string, roleId: string) {
|
||||
await API.addAppsToGroup(groupId, [{ appId, roleId }])
|
||||
await this.refreshGroup(groupId)
|
||||
}
|
||||
|
||||
async removeApp(groupId: string, appId: string) {
|
||||
await API.removeAppsFromGroup(groupId, [{ appId }])
|
||||
await this.refreshGroup(groupId)
|
||||
}
|
||||
|
||||
getGroupAppIds(group: UserGroup) {
|
||||
let groupAppIds = Object.keys(group?.roles || {})
|
||||
if (group?.builder?.apps) {
|
||||
groupAppIds = groupAppIds.concat(group.builder.apps)
|
||||
}
|
||||
return groupAppIds
|
||||
}
|
||||
|
||||
async addGroupAppBuilder(groupId: string, appId: string) {
|
||||
return await API.addGroupAppBuilder(groupId, appId)
|
||||
}
|
||||
|
||||
async removeGroupAppBuilder(groupId: string, appId: string) {
|
||||
return await API.removeGroupAppBuilder(groupId, appId)
|
||||
}
|
||||
}
|
||||
|
||||
export const groups = new GroupStore()
|
|
@ -1,279 +0,0 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { auth, admin } from "@/stores/portal"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { StripeStatus } from "@/components/portal/licensing/constants"
|
||||
import { PlanModel } from "@budibase/types"
|
||||
|
||||
const UNLIMITED = -1
|
||||
|
||||
export const createLicensingStore = () => {
|
||||
const DEFAULT = {
|
||||
// navigation
|
||||
goToUpgradePage: () => {},
|
||||
goToPricingPage: () => {},
|
||||
// the top level license
|
||||
license: undefined,
|
||||
isFreePlan: true,
|
||||
isEnterprisePlan: true,
|
||||
isBusinessPlan: true,
|
||||
// features
|
||||
groupsEnabled: false,
|
||||
backupsEnabled: false,
|
||||
brandingEnabled: false,
|
||||
scimEnabled: false,
|
||||
environmentVariablesEnabled: false,
|
||||
budibaseAIEnabled: false,
|
||||
customAIConfigsEnabled: false,
|
||||
auditLogsEnabled: false,
|
||||
// the currently used quotas from the db
|
||||
quotaUsage: undefined,
|
||||
// derived quota metrics for percentages used
|
||||
usageMetrics: undefined,
|
||||
// quota reset
|
||||
quotaResetDaysRemaining: undefined,
|
||||
quotaResetDate: undefined,
|
||||
// failed payments
|
||||
accountPastDue: undefined,
|
||||
pastDueEndDate: undefined,
|
||||
pastDueDaysRemaining: undefined,
|
||||
accountDowngraded: undefined,
|
||||
// user limits
|
||||
userCount: undefined,
|
||||
userLimit: undefined,
|
||||
userLimitReached: false,
|
||||
errUserLimit: false,
|
||||
}
|
||||
|
||||
const oneDayInMilliseconds = 86400000
|
||||
|
||||
const store = writable(DEFAULT)
|
||||
|
||||
function usersLimitReached(userCount, userLimit) {
|
||||
if (userLimit === UNLIMITED) {
|
||||
return false
|
||||
}
|
||||
return userCount >= userLimit
|
||||
}
|
||||
|
||||
function usersLimitExceeded(userCount, userLimit) {
|
||||
if (userLimit === UNLIMITED) {
|
||||
return false
|
||||
}
|
||||
return userCount > userLimit
|
||||
}
|
||||
|
||||
async function isCloud() {
|
||||
let adminStore = get(admin)
|
||||
if (!adminStore.loaded) {
|
||||
await admin.init()
|
||||
adminStore = get(admin)
|
||||
}
|
||||
return adminStore.cloud
|
||||
}
|
||||
|
||||
const actions = {
|
||||
init: async () => {
|
||||
actions.setNavigation()
|
||||
actions.setLicense()
|
||||
await actions.setQuotaUsage()
|
||||
},
|
||||
setNavigation: () => {
|
||||
const adminStore = get(admin)
|
||||
const authStore = get(auth)
|
||||
|
||||
const upgradeUrl = authStore?.user?.accountPortalAccess
|
||||
? `${adminStore.accountPortalUrl}/portal/upgrade`
|
||||
: "/builder/portal/account/upgrade"
|
||||
|
||||
const goToUpgradePage = () => {
|
||||
window.location.href = upgradeUrl
|
||||
}
|
||||
const goToPricingPage = () => {
|
||||
window.open("https://budibase.com/pricing/", "_blank")
|
||||
}
|
||||
store.update(state => {
|
||||
return {
|
||||
...state,
|
||||
goToUpgradePage,
|
||||
goToPricingPage,
|
||||
}
|
||||
})
|
||||
},
|
||||
setLicense: () => {
|
||||
const license = get(auth).user.license
|
||||
const planType = license?.plan.type
|
||||
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
|
||||
const isFreePlan = planType === Constants.PlanType.FREE
|
||||
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
|
||||
const isEnterpriseTrial =
|
||||
planType === Constants.PlanType.ENTERPRISE_BASIC_TRIAL
|
||||
const groupsEnabled = license.features.includes(
|
||||
Constants.Features.USER_GROUPS
|
||||
)
|
||||
const backupsEnabled = license.features.includes(
|
||||
Constants.Features.APP_BACKUPS
|
||||
)
|
||||
const scimEnabled = license.features.includes(Constants.Features.SCIM)
|
||||
const environmentVariablesEnabled = license.features.includes(
|
||||
Constants.Features.ENVIRONMENT_VARIABLES
|
||||
)
|
||||
const enforceableSSO = license.features.includes(
|
||||
Constants.Features.ENFORCEABLE_SSO
|
||||
)
|
||||
const brandingEnabled = license.features.includes(
|
||||
Constants.Features.BRANDING
|
||||
)
|
||||
const auditLogsEnabled = license.features.includes(
|
||||
Constants.Features.AUDIT_LOGS
|
||||
)
|
||||
const syncAutomationsEnabled = license.features.includes(
|
||||
Constants.Features.SYNC_AUTOMATIONS
|
||||
)
|
||||
const triggerAutomationRunEnabled = license.features.includes(
|
||||
Constants.Features.TRIGGER_AUTOMATION_RUN
|
||||
)
|
||||
const perAppBuildersEnabled = license.features.includes(
|
||||
Constants.Features.APP_BUILDERS
|
||||
)
|
||||
const budibaseAIEnabled = license.features.includes(
|
||||
Constants.Features.BUDIBASE_AI
|
||||
)
|
||||
const customAIConfigsEnabled = license.features.includes(
|
||||
Constants.Features.AI_CUSTOM_CONFIGS
|
||||
)
|
||||
store.update(state => {
|
||||
return {
|
||||
...state,
|
||||
license,
|
||||
isEnterprisePlan,
|
||||
isFreePlan,
|
||||
isBusinessPlan,
|
||||
isEnterpriseTrial,
|
||||
groupsEnabled,
|
||||
backupsEnabled,
|
||||
brandingEnabled,
|
||||
budibaseAIEnabled,
|
||||
customAIConfigsEnabled,
|
||||
scimEnabled,
|
||||
environmentVariablesEnabled,
|
||||
auditLogsEnabled,
|
||||
enforceableSSO,
|
||||
syncAutomationsEnabled,
|
||||
triggerAutomationRunEnabled,
|
||||
perAppBuildersEnabled,
|
||||
}
|
||||
})
|
||||
},
|
||||
setQuotaUsage: async () => {
|
||||
const quotaUsage = await API.getQuotaUsage()
|
||||
store.update(state => {
|
||||
return {
|
||||
...state,
|
||||
quotaUsage,
|
||||
}
|
||||
})
|
||||
await actions.setUsageMetrics()
|
||||
},
|
||||
usersLimitReached: userCount => {
|
||||
return usersLimitReached(userCount, get(store).userLimit)
|
||||
},
|
||||
usersLimitExceeded(userCount) {
|
||||
return usersLimitExceeded(userCount, get(store).userLimit)
|
||||
},
|
||||
setUsageMetrics: async () => {
|
||||
const usage = get(store).quotaUsage
|
||||
const license = get(auth).user.license
|
||||
const now = new Date()
|
||||
|
||||
const getMetrics = (keys, license, quota) => {
|
||||
if (!license || !quota || !keys) {
|
||||
return {}
|
||||
}
|
||||
return keys.reduce((acc, key) => {
|
||||
const quotaLimit = license[key].value
|
||||
const quotaUsed = (quota[key] / quotaLimit) * 100
|
||||
acc[key] = quotaLimit > -1 ? Math.floor(quotaUsed) : -1
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
const monthlyMetrics = getMetrics(
|
||||
["queries", "automations"],
|
||||
license.quotas.usage.monthly,
|
||||
usage.monthly.current
|
||||
)
|
||||
const staticMetrics = getMetrics(
|
||||
["apps", "rows"],
|
||||
license.quotas.usage.static,
|
||||
usage.usageQuota
|
||||
)
|
||||
|
||||
const getDaysBetween = (dateStart, dateEnd) => {
|
||||
return dateEnd > dateStart
|
||||
? Math.round(
|
||||
(dateEnd.getTime() - dateStart.getTime()) / oneDayInMilliseconds
|
||||
)
|
||||
: 0
|
||||
}
|
||||
|
||||
const quotaResetDate = new Date(usage.quotaReset)
|
||||
const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
|
||||
|
||||
const accountDowngraded =
|
||||
license?.billing?.subscription?.downgradeAt &&
|
||||
license?.billing?.subscription?.downgradeAt <= now.getTime() &&
|
||||
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
|
||||
license?.plan.type === Constants.PlanType.FREE
|
||||
|
||||
const pastDueAtMilliseconds = license?.billing?.subscription?.pastDueAt
|
||||
const downgradeAtMilliseconds =
|
||||
license?.billing?.subscription?.downgradeAt
|
||||
let pastDueDaysRemaining
|
||||
let pastDueEndDate
|
||||
|
||||
if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
|
||||
pastDueEndDate = new Date(downgradeAtMilliseconds)
|
||||
pastDueDaysRemaining = getDaysBetween(
|
||||
new Date(pastDueAtMilliseconds),
|
||||
pastDueEndDate
|
||||
)
|
||||
}
|
||||
|
||||
const userQuota = license.quotas.usage.static.users
|
||||
const userLimit = userQuota?.value
|
||||
const userCount = usage.usageQuota.users
|
||||
const userLimitReached = usersLimitReached(userCount, userLimit)
|
||||
const userLimitExceeded = usersLimitExceeded(userCount, userLimit)
|
||||
const isCloudAccount = await isCloud()
|
||||
const errUserLimit =
|
||||
isCloudAccount &&
|
||||
license.plan.model === PlanModel.PER_USER &&
|
||||
userLimitExceeded
|
||||
|
||||
store.update(state => {
|
||||
return {
|
||||
...state,
|
||||
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
|
||||
quotaResetDaysRemaining,
|
||||
quotaResetDate,
|
||||
accountDowngraded,
|
||||
accountPastDue: pastDueAtMilliseconds != null,
|
||||
pastDueEndDate,
|
||||
pastDueDaysRemaining,
|
||||
// user limits
|
||||
userCount,
|
||||
userLimit,
|
||||
userLimitReached,
|
||||
errUserLimit,
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
...actions,
|
||||
}
|
||||
}
|
||||
|
||||
export const licensing = createLicensingStore()
|
|
@ -0,0 +1,305 @@
|
|||
import { get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { auth, admin } from "@/stores/portal"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { StripeStatus } from "@/components/portal/licensing/constants"
|
||||
import {
|
||||
License,
|
||||
MonthlyQuotaName,
|
||||
PlanModel,
|
||||
QuotaUsage,
|
||||
StaticQuotaName,
|
||||
} from "@budibase/types"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
|
||||
const UNLIMITED = -1
|
||||
const ONE_DAY_MILLIS = 86400000
|
||||
|
||||
type MonthlyMetrics = { [key in MonthlyQuotaName]?: number }
|
||||
type StaticMetrics = { [key in StaticQuotaName]?: number }
|
||||
type UsageMetrics = MonthlyMetrics & StaticMetrics
|
||||
|
||||
interface LicensingState {
|
||||
goToUpgradePage: () => void
|
||||
goToPricingPage: () => void
|
||||
// the top level license
|
||||
license?: License
|
||||
isFreePlan: boolean
|
||||
isEnterprisePlan: boolean
|
||||
isBusinessPlan: boolean
|
||||
// features
|
||||
groupsEnabled: boolean
|
||||
backupsEnabled: boolean
|
||||
brandingEnabled: boolean
|
||||
scimEnabled: boolean
|
||||
environmentVariablesEnabled: boolean
|
||||
budibaseAIEnabled: boolean
|
||||
customAIConfigsEnabled: boolean
|
||||
auditLogsEnabled: boolean
|
||||
// the currently used quotas from the db
|
||||
quotaUsage?: QuotaUsage
|
||||
// derived quota metrics for percentages used
|
||||
usageMetrics?: UsageMetrics
|
||||
// quota reset
|
||||
quotaResetDaysRemaining?: number
|
||||
quotaResetDate?: Date
|
||||
// failed payments
|
||||
accountPastDue: boolean
|
||||
pastDueEndDate?: Date
|
||||
pastDueDaysRemaining?: number
|
||||
accountDowngraded: boolean
|
||||
// user limits
|
||||
userCount?: number
|
||||
userLimit?: number
|
||||
userLimitReached: boolean
|
||||
errUserLimit: boolean
|
||||
}
|
||||
|
||||
class LicensingStore extends BudiStore<LicensingState> {
|
||||
constructor() {
|
||||
super({
|
||||
// navigation
|
||||
goToUpgradePage: () => {},
|
||||
goToPricingPage: () => {},
|
||||
// the top level license
|
||||
license: undefined,
|
||||
isFreePlan: true,
|
||||
isEnterprisePlan: true,
|
||||
isBusinessPlan: true,
|
||||
// features
|
||||
groupsEnabled: false,
|
||||
backupsEnabled: false,
|
||||
brandingEnabled: false,
|
||||
scimEnabled: false,
|
||||
environmentVariablesEnabled: false,
|
||||
budibaseAIEnabled: false,
|
||||
customAIConfigsEnabled: false,
|
||||
auditLogsEnabled: false,
|
||||
// the currently used quotas from the db
|
||||
quotaUsage: undefined,
|
||||
// derived quota metrics for percentages used
|
||||
usageMetrics: undefined,
|
||||
// quota reset
|
||||
quotaResetDaysRemaining: undefined,
|
||||
quotaResetDate: undefined,
|
||||
// failed payments
|
||||
accountPastDue: false,
|
||||
pastDueEndDate: undefined,
|
||||
pastDueDaysRemaining: undefined,
|
||||
accountDowngraded: false,
|
||||
// user limits
|
||||
userCount: undefined,
|
||||
userLimit: undefined,
|
||||
userLimitReached: false,
|
||||
errUserLimit: false,
|
||||
})
|
||||
}
|
||||
|
||||
usersLimitReached(userCount: number, userLimit = get(this.store).userLimit) {
|
||||
if (userLimit === UNLIMITED || userLimit === undefined) {
|
||||
return false
|
||||
}
|
||||
return userCount >= userLimit
|
||||
}
|
||||
|
||||
usersLimitExceeded(userCount: number, userLimit = get(this.store).userLimit) {
|
||||
if (userLimit === UNLIMITED || userLimit === undefined) {
|
||||
return false
|
||||
}
|
||||
return userCount > userLimit
|
||||
}
|
||||
|
||||
async isCloud() {
|
||||
let adminStore = get(admin)
|
||||
if (!adminStore.loaded) {
|
||||
await admin.init()
|
||||
adminStore = get(admin)
|
||||
}
|
||||
return adminStore.cloud
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.setNavigation()
|
||||
this.setLicense()
|
||||
await this.setQuotaUsage()
|
||||
}
|
||||
|
||||
setNavigation() {
|
||||
const adminStore = get(admin)
|
||||
const authStore = get(auth)
|
||||
|
||||
const upgradeUrl = authStore?.user?.accountPortalAccess
|
||||
? `${adminStore.accountPortalUrl}/portal/upgrade`
|
||||
: "/builder/portal/account/upgrade"
|
||||
|
||||
const goToUpgradePage = () => {
|
||||
window.location.href = upgradeUrl
|
||||
}
|
||||
const goToPricingPage = () => {
|
||||
window.open("https://budibase.com/pricing/", "_blank")
|
||||
}
|
||||
this.update(state => {
|
||||
return {
|
||||
...state,
|
||||
goToUpgradePage,
|
||||
goToPricingPage,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setLicense() {
|
||||
const license = get(auth).user?.license
|
||||
const planType = license?.plan.type
|
||||
const features = license?.features || []
|
||||
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
|
||||
const isFreePlan = planType === Constants.PlanType.FREE
|
||||
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
|
||||
const isEnterpriseTrial =
|
||||
planType === Constants.PlanType.ENTERPRISE_BASIC_TRIAL
|
||||
const groupsEnabled = features.includes(Constants.Features.USER_GROUPS)
|
||||
const backupsEnabled = features.includes(Constants.Features.APP_BACKUPS)
|
||||
const scimEnabled = features.includes(Constants.Features.SCIM)
|
||||
const environmentVariablesEnabled = features.includes(
|
||||
Constants.Features.ENVIRONMENT_VARIABLES
|
||||
)
|
||||
const enforceableSSO = features.includes(Constants.Features.ENFORCEABLE_SSO)
|
||||
const brandingEnabled = features.includes(Constants.Features.BRANDING)
|
||||
const auditLogsEnabled = features.includes(Constants.Features.AUDIT_LOGS)
|
||||
const syncAutomationsEnabled = features.includes(
|
||||
Constants.Features.SYNC_AUTOMATIONS
|
||||
)
|
||||
const triggerAutomationRunEnabled = features.includes(
|
||||
Constants.Features.TRIGGER_AUTOMATION_RUN
|
||||
)
|
||||
const perAppBuildersEnabled = features.includes(
|
||||
Constants.Features.APP_BUILDERS
|
||||
)
|
||||
const budibaseAIEnabled = features.includes(Constants.Features.BUDIBASE_AI)
|
||||
const customAIConfigsEnabled = features.includes(
|
||||
Constants.Features.AI_CUSTOM_CONFIGS
|
||||
)
|
||||
this.update(state => {
|
||||
return {
|
||||
...state,
|
||||
license,
|
||||
isEnterprisePlan,
|
||||
isFreePlan,
|
||||
isBusinessPlan,
|
||||
isEnterpriseTrial,
|
||||
groupsEnabled,
|
||||
backupsEnabled,
|
||||
brandingEnabled,
|
||||
budibaseAIEnabled,
|
||||
customAIConfigsEnabled,
|
||||
scimEnabled,
|
||||
environmentVariablesEnabled,
|
||||
auditLogsEnabled,
|
||||
enforceableSSO,
|
||||
syncAutomationsEnabled,
|
||||
triggerAutomationRunEnabled,
|
||||
perAppBuildersEnabled,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async setQuotaUsage() {
|
||||
const quotaUsage = await API.getQuotaUsage()
|
||||
this.update(state => {
|
||||
return {
|
||||
...state,
|
||||
quotaUsage,
|
||||
}
|
||||
})
|
||||
await this.setUsageMetrics()
|
||||
}
|
||||
|
||||
async setUsageMetrics() {
|
||||
const usage = get(this.store).quotaUsage
|
||||
const license = get(auth).user?.license
|
||||
const now = new Date()
|
||||
if (!license || !usage) {
|
||||
return
|
||||
}
|
||||
|
||||
// Process monthly metrics
|
||||
const monthlyMetrics = [
|
||||
MonthlyQuotaName.QUERIES,
|
||||
MonthlyQuotaName.AUTOMATIONS,
|
||||
].reduce((acc: MonthlyMetrics, key) => {
|
||||
const limit = license.quotas.usage.monthly[key].value
|
||||
const used = ((usage.monthly.current?.[key] || 0) / limit) * 100
|
||||
acc[key] = limit > -1 ? Math.floor(used) : -1
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// Process static metrics
|
||||
const staticMetrics = [StaticQuotaName.APPS, StaticQuotaName.ROWS].reduce(
|
||||
(acc: StaticMetrics, key) => {
|
||||
const limit = license.quotas.usage.static[key].value
|
||||
const used = ((usage.usageQuota[key] || 0) / limit) * 100
|
||||
acc[key] = limit > -1 ? Math.floor(used) : -1
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const getDaysBetween = (dateStart: Date, dateEnd: Date) => {
|
||||
return dateEnd > dateStart
|
||||
? Math.round((dateEnd.getTime() - dateStart.getTime()) / ONE_DAY_MILLIS)
|
||||
: 0
|
||||
}
|
||||
|
||||
const quotaResetDate = new Date(usage.quotaReset)
|
||||
const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
|
||||
|
||||
const accountDowngraded =
|
||||
!!license.billing?.subscription?.downgradeAt &&
|
||||
license.billing?.subscription?.downgradeAt <= now.getTime() &&
|
||||
license.billing?.subscription?.status === StripeStatus.PAST_DUE &&
|
||||
license.plan.type === Constants.PlanType.FREE
|
||||
|
||||
const pastDueAtMilliseconds = license.billing?.subscription?.pastDueAt
|
||||
const downgradeAtMilliseconds = license.billing?.subscription?.downgradeAt
|
||||
let pastDueDaysRemaining: number
|
||||
let pastDueEndDate: Date
|
||||
|
||||
if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
|
||||
pastDueEndDate = new Date(downgradeAtMilliseconds)
|
||||
pastDueDaysRemaining = getDaysBetween(
|
||||
new Date(pastDueAtMilliseconds),
|
||||
pastDueEndDate
|
||||
)
|
||||
}
|
||||
|
||||
const userQuota = license.quotas.usage.static.users
|
||||
const userLimit = userQuota.value
|
||||
const userCount = usage.usageQuota.users
|
||||
const userLimitReached = this.usersLimitReached(userCount, userLimit)
|
||||
const userLimitExceeded = this.usersLimitExceeded(userCount, userLimit)
|
||||
const isCloudAccount = await this.isCloud()
|
||||
const errUserLimit =
|
||||
isCloudAccount &&
|
||||
license.plan.model === PlanModel.PER_USER &&
|
||||
userLimitExceeded
|
||||
|
||||
this.update(state => {
|
||||
return {
|
||||
...state,
|
||||
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
|
||||
quotaResetDaysRemaining,
|
||||
quotaResetDate,
|
||||
accountDowngraded,
|
||||
accountPastDue: pastDueAtMilliseconds != null,
|
||||
pastDueEndDate,
|
||||
pastDueDaysRemaining,
|
||||
// user limits
|
||||
userCount,
|
||||
userLimit,
|
||||
userLimitReached,
|
||||
errUserLimit,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const licensing = new LicensingStore()
|
|
@ -1,138 +0,0 @@
|
|||
import { derived } from "svelte/store"
|
||||
import { admin } from "./admin"
|
||||
import { auth } from "./auth"
|
||||
import { isEnabled } from "@/helpers/featureFlags"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { FeatureFlag } from "@budibase/types"
|
||||
|
||||
export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||
const user = $auth?.user
|
||||
const isAdmin = sdk.users.isAdmin(user)
|
||||
const cloud = $admin?.cloud
|
||||
// Determine user sub pages
|
||||
let userSubPages = [
|
||||
{
|
||||
title: "Users",
|
||||
href: "/builder/portal/users/users",
|
||||
},
|
||||
]
|
||||
userSubPages.push({
|
||||
title: "Groups",
|
||||
href: "/builder/portal/users/groups",
|
||||
})
|
||||
|
||||
// Pages that all devs and admins can access
|
||||
let menu = [
|
||||
{
|
||||
title: "Apps",
|
||||
href: "/builder/portal/apps",
|
||||
},
|
||||
]
|
||||
if (sdk.users.isGlobalBuilder(user)) {
|
||||
menu.push({
|
||||
title: "Users",
|
||||
href: "/builder/portal/users",
|
||||
subPages: userSubPages,
|
||||
})
|
||||
menu.push({
|
||||
title: "Plugins",
|
||||
href: "/builder/portal/plugins",
|
||||
})
|
||||
}
|
||||
|
||||
// Add settings page for admins
|
||||
if (isAdmin) {
|
||||
let settingsSubPages = [
|
||||
{
|
||||
title: "Auth",
|
||||
href: "/builder/portal/settings/auth",
|
||||
},
|
||||
{
|
||||
title: "Email",
|
||||
href: "/builder/portal/settings/email",
|
||||
},
|
||||
{
|
||||
title: "Organisation",
|
||||
href: "/builder/portal/settings/organisation",
|
||||
},
|
||||
{
|
||||
title: "Branding",
|
||||
href: "/builder/portal/settings/branding",
|
||||
},
|
||||
{
|
||||
title: "Environment",
|
||||
href: "/builder/portal/settings/environment",
|
||||
},
|
||||
]
|
||||
if (isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) {
|
||||
settingsSubPages.push({
|
||||
title: "AI",
|
||||
href: "/builder/portal/settings/ai",
|
||||
})
|
||||
}
|
||||
|
||||
if (!cloud) {
|
||||
settingsSubPages.push({
|
||||
title: "Version",
|
||||
href: "/builder/portal/settings/version",
|
||||
})
|
||||
settingsSubPages.push({
|
||||
title: "Diagnostics",
|
||||
href: "/builder/portal/settings/diagnostics",
|
||||
})
|
||||
}
|
||||
menu.push({
|
||||
title: "Settings",
|
||||
href: "/builder/portal/settings",
|
||||
subPages: [...settingsSubPages].sort((a, b) =>
|
||||
a.title.localeCompare(b.title)
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
// Add account page
|
||||
let accountSubPages = [
|
||||
{
|
||||
title: "Usage",
|
||||
href: "/builder/portal/account/usage",
|
||||
},
|
||||
]
|
||||
if (isAdmin) {
|
||||
accountSubPages.push({
|
||||
title: "Audit Logs",
|
||||
href: "/builder/portal/account/auditLogs",
|
||||
})
|
||||
|
||||
if (!cloud) {
|
||||
accountSubPages.push({
|
||||
title: "System Logs",
|
||||
href: "/builder/portal/account/systemLogs",
|
||||
})
|
||||
}
|
||||
}
|
||||
if (cloud && user?.accountPortalAccess) {
|
||||
accountSubPages.push({
|
||||
title: "Upgrade",
|
||||
href: $admin?.accountPortalUrl + "/portal/upgrade",
|
||||
})
|
||||
} else if (!cloud && isAdmin) {
|
||||
accountSubPages.push({
|
||||
title: "Upgrade",
|
||||
href: "/builder/portal/account/upgrade",
|
||||
})
|
||||
}
|
||||
// add license check here
|
||||
if (user?.accountPortalAccess && user.account.stripeCustomerId) {
|
||||
accountSubPages.push({
|
||||
title: "Billing",
|
||||
href: $admin?.accountPortalUrl + "/portal/billing",
|
||||
})
|
||||
}
|
||||
menu.push({
|
||||
title: "Account",
|
||||
href: "/builder/portal/account",
|
||||
subPages: accountSubPages,
|
||||
})
|
||||
|
||||
return menu
|
||||
})
|
|
@ -0,0 +1,145 @@
|
|||
import { derived, Readable } from "svelte/store"
|
||||
import { admin } from "./admin"
|
||||
import { auth } from "./auth"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
|
||||
interface MenuItem {
|
||||
title: string
|
||||
href: string
|
||||
subPages?: MenuItem[]
|
||||
}
|
||||
|
||||
export const menu: Readable<MenuItem[]> = derived(
|
||||
[admin, auth],
|
||||
([$admin, $auth]) => {
|
||||
const user = $auth?.user
|
||||
const isAdmin = user != null && sdk.users.isAdmin(user)
|
||||
const isGlobalBuilder = user != null && sdk.users.isGlobalBuilder(user)
|
||||
const cloud = $admin?.cloud
|
||||
|
||||
// Determine user sub pages
|
||||
let userSubPages: MenuItem[] = [
|
||||
{
|
||||
title: "Users",
|
||||
href: "/builder/portal/users/users",
|
||||
},
|
||||
]
|
||||
userSubPages.push({
|
||||
title: "Groups",
|
||||
href: "/builder/portal/users/groups",
|
||||
})
|
||||
|
||||
// Pages that all devs and admins can access
|
||||
let menu: MenuItem[] = [
|
||||
{
|
||||
title: "Apps",
|
||||
href: "/builder/portal/apps",
|
||||
},
|
||||
]
|
||||
if (isGlobalBuilder) {
|
||||
menu.push({
|
||||
title: "Users",
|
||||
href: "/builder/portal/users",
|
||||
subPages: userSubPages,
|
||||
})
|
||||
menu.push({
|
||||
title: "Plugins",
|
||||
href: "/builder/portal/plugins",
|
||||
})
|
||||
}
|
||||
|
||||
// Add settings page for admins
|
||||
if (isAdmin) {
|
||||
let settingsSubPages: MenuItem[] = [
|
||||
{
|
||||
title: "Auth",
|
||||
href: "/builder/portal/settings/auth",
|
||||
},
|
||||
{
|
||||
title: "Email",
|
||||
href: "/builder/portal/settings/email",
|
||||
},
|
||||
{
|
||||
title: "Organisation",
|
||||
href: "/builder/portal/settings/organisation",
|
||||
},
|
||||
{
|
||||
title: "Branding",
|
||||
href: "/builder/portal/settings/branding",
|
||||
},
|
||||
{
|
||||
title: "Environment",
|
||||
href: "/builder/portal/settings/environment",
|
||||
},
|
||||
{
|
||||
title: "AI",
|
||||
href: "/builder/portal/settings/ai",
|
||||
},
|
||||
]
|
||||
|
||||
if (!cloud) {
|
||||
settingsSubPages.push({
|
||||
title: "Version",
|
||||
href: "/builder/portal/settings/version",
|
||||
})
|
||||
settingsSubPages.push({
|
||||
title: "Diagnostics",
|
||||
href: "/builder/portal/settings/diagnostics",
|
||||
})
|
||||
}
|
||||
menu.push({
|
||||
title: "Settings",
|
||||
href: "/builder/portal/settings",
|
||||
subPages: [...settingsSubPages].sort((a, b) =>
|
||||
a.title.localeCompare(b.title)
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
// Add account page
|
||||
let accountSubPages: MenuItem[] = [
|
||||
{
|
||||
title: "Usage",
|
||||
href: "/builder/portal/account/usage",
|
||||
},
|
||||
]
|
||||
if (isAdmin) {
|
||||
accountSubPages.push({
|
||||
title: "Audit Logs",
|
||||
href: "/builder/portal/account/auditLogs",
|
||||
})
|
||||
|
||||
if (!cloud) {
|
||||
accountSubPages.push({
|
||||
title: "System Logs",
|
||||
href: "/builder/portal/account/systemLogs",
|
||||
})
|
||||
}
|
||||
}
|
||||
if (cloud && user?.accountPortalAccess) {
|
||||
accountSubPages.push({
|
||||
title: "Upgrade",
|
||||
href: $admin?.accountPortalUrl + "/portal/upgrade",
|
||||
})
|
||||
} else if (!cloud && isAdmin) {
|
||||
accountSubPages.push({
|
||||
title: "Upgrade",
|
||||
href: "/builder/portal/account/upgrade",
|
||||
})
|
||||
}
|
||||
// add license check here
|
||||
if (user?.accountPortalAccess && user?.account?.stripeCustomerId) {
|
||||
accountSubPages.push({
|
||||
title: "Billing",
|
||||
href: $admin?.accountPortalUrl + "/portal/billing",
|
||||
})
|
||||
}
|
||||
menu.push({
|
||||
title: "Account",
|
||||
href: "/builder/portal/account",
|
||||
subPages: accountSubPages,
|
||||
})
|
||||
|
||||
return menu
|
||||
}
|
||||
)
|
|
@ -1,31 +0,0 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { auth } from "@/stores/portal"
|
||||
|
||||
const OIDC_CONFIG = {
|
||||
logo: undefined,
|
||||
name: undefined,
|
||||
uuid: undefined,
|
||||
}
|
||||
|
||||
export function createOidcStore() {
|
||||
const store = writable(OIDC_CONFIG)
|
||||
const { set, subscribe } = store
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
init: async () => {
|
||||
const tenantId = get(auth).tenantId
|
||||
const config = await API.getOIDCConfig(tenantId)
|
||||
if (Object.keys(config || {}).length) {
|
||||
// Just use the first config for now.
|
||||
// We will be support multiple logins buttons later on.
|
||||
set(...config)
|
||||
} else {
|
||||
set(OIDC_CONFIG)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const oidc = createOidcStore()
|
|
@ -0,0 +1,21 @@
|
|||
import { get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { auth } from "@/stores/portal"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import { PublicOIDCConfig } from "@budibase/types"
|
||||
|
||||
class OIDCStore extends BudiStore<PublicOIDCConfig> {
|
||||
constructor() {
|
||||
super({})
|
||||
}
|
||||
|
||||
async init() {
|
||||
const tenantId = get(auth).tenantId
|
||||
const configs = await API.getOIDCConfigs(tenantId)
|
||||
// Just use the first config for now.
|
||||
// We will be support multiple logins buttons later on.
|
||||
this.set(configs[0] || {})
|
||||
}
|
||||
}
|
||||
|
||||
export const oidc = new OIDCStore()
|
|
@ -1,66 +0,0 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { auth } from "@/stores/portal"
|
||||
import _ from "lodash"
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
platformUrl: "",
|
||||
logoUrl: undefined,
|
||||
faviconUrl: undefined,
|
||||
emailBrandingEnabled: true,
|
||||
testimonialsEnabled: true,
|
||||
platformTitle: "Budibase",
|
||||
loginHeading: undefined,
|
||||
loginButton: undefined,
|
||||
metaDescription: undefined,
|
||||
metaImageUrl: undefined,
|
||||
metaTitle: undefined,
|
||||
docsUrl: undefined,
|
||||
company: "Budibase",
|
||||
oidc: undefined,
|
||||
google: undefined,
|
||||
googleDatasourceConfigured: undefined,
|
||||
oidcCallbackUrl: "",
|
||||
googleCallbackUrl: "",
|
||||
isSSOEnforced: false,
|
||||
loaded: false,
|
||||
}
|
||||
|
||||
export function createOrganisationStore() {
|
||||
const store = writable(DEFAULT_CONFIG)
|
||||
const { subscribe, set } = store
|
||||
|
||||
async function init() {
|
||||
const tenantId = get(auth).tenantId
|
||||
const settingsConfigDoc = await API.getTenantConfig(tenantId)
|
||||
set({ ...DEFAULT_CONFIG, ...settingsConfigDoc.config, loaded: true })
|
||||
}
|
||||
|
||||
async function save(config) {
|
||||
// Delete non-persisted fields
|
||||
const storeConfig = _.cloneDeep(get(store))
|
||||
delete storeConfig.oidc
|
||||
delete storeConfig.google
|
||||
delete storeConfig.googleDatasourceConfigured
|
||||
delete storeConfig.oidcCallbackUrl
|
||||
delete storeConfig.googleCallbackUrl
|
||||
|
||||
// delete internal store field
|
||||
delete storeConfig.loaded
|
||||
|
||||
await API.saveConfig({
|
||||
type: "settings",
|
||||
config: { ...storeConfig, ...config },
|
||||
})
|
||||
await init()
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
save,
|
||||
init,
|
||||
}
|
||||
}
|
||||
|
||||
export const organisation = createOrganisationStore()
|
|
@ -0,0 +1,71 @@
|
|||
import { get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { auth } from "@/stores/portal"
|
||||
import {
|
||||
ConfigType,
|
||||
PublicSettingsInnerConfig,
|
||||
SettingsBrandingConfig,
|
||||
SettingsInnerConfig,
|
||||
} from "@budibase/types"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
|
||||
interface LocalOrganisationState {
|
||||
loaded: boolean
|
||||
}
|
||||
|
||||
type SavedOrganisationState = SettingsInnerConfig & SettingsBrandingConfig
|
||||
type OrganisationState = SavedOrganisationState &
|
||||
PublicSettingsInnerConfig &
|
||||
LocalOrganisationState
|
||||
|
||||
const DEFAULT_STATE: OrganisationState = {
|
||||
platformUrl: "",
|
||||
emailBrandingEnabled: true,
|
||||
testimonialsEnabled: true,
|
||||
platformTitle: "Budibase",
|
||||
company: "Budibase",
|
||||
google: false,
|
||||
googleDatasourceConfigured: false,
|
||||
oidc: false,
|
||||
oidcCallbackUrl: "",
|
||||
googleCallbackUrl: "",
|
||||
loaded: false,
|
||||
}
|
||||
|
||||
class OrganisationStore extends BudiStore<OrganisationState> {
|
||||
constructor() {
|
||||
super(DEFAULT_STATE)
|
||||
}
|
||||
|
||||
async init() {
|
||||
const tenantId = get(auth).tenantId
|
||||
const settingsConfigDoc = await API.getTenantConfig(tenantId)
|
||||
this.set({ ...DEFAULT_STATE, ...settingsConfigDoc.config, loaded: true })
|
||||
}
|
||||
|
||||
async save(changes: Partial<SavedOrganisationState>) {
|
||||
// Strip non persisted fields
|
||||
const {
|
||||
oidc,
|
||||
google,
|
||||
googleDatasourceConfigured,
|
||||
oidcCallbackUrl,
|
||||
googleCallbackUrl,
|
||||
loaded,
|
||||
...config
|
||||
} = get(this.store)
|
||||
|
||||
// Save new config
|
||||
const newConfig: SavedOrganisationState = {
|
||||
...config,
|
||||
...changes,
|
||||
}
|
||||
await API.saveConfig({
|
||||
type: ConfigType.SETTINGS,
|
||||
config: newConfig,
|
||||
})
|
||||
await this.init()
|
||||
}
|
||||
}
|
||||
|
||||
export const organisation = new OrganisationStore()
|
|
@ -1,41 +1,71 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { update } from "lodash"
|
||||
import { licensing } from "."
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import {
|
||||
DeleteInviteUsersRequest,
|
||||
InviteUsersRequest,
|
||||
SearchUsersRequest,
|
||||
SearchUsersResponse,
|
||||
UpdateInviteRequest,
|
||||
User,
|
||||
UserIdentifier,
|
||||
UnsavedUser,
|
||||
} from "@budibase/types"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
|
||||
export function createUsersStore() {
|
||||
const { subscribe, set } = writable({})
|
||||
interface UserInfo {
|
||||
email: string
|
||||
password: string
|
||||
forceResetPassword?: boolean
|
||||
role: keyof typeof Constants.BudibaseRoles
|
||||
}
|
||||
|
||||
// opts can contain page and search params
|
||||
async function search(opts = {}) {
|
||||
type UserState = SearchUsersResponse & SearchUsersRequest
|
||||
|
||||
class UserStore extends BudiStore<UserState> {
|
||||
constructor() {
|
||||
super({
|
||||
data: [],
|
||||
})
|
||||
}
|
||||
|
||||
async search(opts: SearchUsersRequest = {}) {
|
||||
const paged = await API.searchUsers(opts)
|
||||
set({
|
||||
this.set({
|
||||
...paged,
|
||||
...opts,
|
||||
})
|
||||
return paged
|
||||
}
|
||||
|
||||
async function get(userId) {
|
||||
async get(userId: string) {
|
||||
try {
|
||||
return await API.getUser(userId)
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
const fetch = async () => {
|
||||
|
||||
async fetch() {
|
||||
return await API.getUsers()
|
||||
}
|
||||
|
||||
// One or more users.
|
||||
async function onboard(payload) {
|
||||
async onboard(payload: InviteUsersRequest) {
|
||||
return await API.onboardUsers(payload)
|
||||
}
|
||||
|
||||
async function invite(payload) {
|
||||
const users = payload.map(user => {
|
||||
async invite(
|
||||
payload: {
|
||||
admin?: boolean
|
||||
builder?: boolean
|
||||
creator?: boolean
|
||||
email: string
|
||||
apps?: any[]
|
||||
groups?: any[]
|
||||
}[]
|
||||
) {
|
||||
const users: InviteUsersRequest = payload.map(user => {
|
||||
let builder = undefined
|
||||
if (user.admin || user.builder) {
|
||||
builder = { global: true }
|
||||
|
@ -55,11 +85,16 @@ export function createUsersStore() {
|
|||
return API.inviteUsers(users)
|
||||
}
|
||||
|
||||
async function removeInvites(payload) {
|
||||
async removeInvites(payload: DeleteInviteUsersRequest) {
|
||||
return API.removeUserInvites(payload)
|
||||
}
|
||||
|
||||
async function acceptInvite(inviteCode, password, firstName, lastName) {
|
||||
async acceptInvite(
|
||||
inviteCode: string,
|
||||
password: string,
|
||||
firstName: string,
|
||||
lastName?: string
|
||||
) {
|
||||
return API.acceptInvite({
|
||||
inviteCode,
|
||||
password,
|
||||
|
@ -68,21 +103,25 @@ export function createUsersStore() {
|
|||
})
|
||||
}
|
||||
|
||||
async function fetchInvite(inviteCode) {
|
||||
async fetchInvite(inviteCode: string) {
|
||||
return API.getUserInvite(inviteCode)
|
||||
}
|
||||
|
||||
async function getInvites() {
|
||||
async getInvites() {
|
||||
return API.getUserInvites()
|
||||
}
|
||||
|
||||
async function updateInvite(invite) {
|
||||
return API.updateUserInvite(invite.code, invite)
|
||||
async updateInvite(code: string, invite: UpdateInviteRequest) {
|
||||
return API.updateUserInvite(code, invite)
|
||||
}
|
||||
|
||||
async function create(data) {
|
||||
let mappedUsers = data.users.map(user => {
|
||||
const body = {
|
||||
async getUserCountByApp(appId: string) {
|
||||
return await API.getUserCountByApp(appId)
|
||||
}
|
||||
|
||||
async create(data: { users: UserInfo[]; groups: any[] }) {
|
||||
let mappedUsers: UnsavedUser[] = data.users.map((user: any) => {
|
||||
const body: UnsavedUser = {
|
||||
email: user.email,
|
||||
password: user.password,
|
||||
roles: {},
|
||||
|
@ -92,17 +131,17 @@ export function createUsersStore() {
|
|||
}
|
||||
|
||||
switch (user.role) {
|
||||
case "appUser":
|
||||
case Constants.BudibaseRoles.AppUser:
|
||||
body.builder = { global: false }
|
||||
body.admin = { global: false }
|
||||
break
|
||||
case "developer":
|
||||
case Constants.BudibaseRoles.Developer:
|
||||
body.builder = { global: true }
|
||||
break
|
||||
case "creator":
|
||||
case Constants.BudibaseRoles.Creator:
|
||||
body.builder = { creator: true, global: false }
|
||||
break
|
||||
case "admin":
|
||||
case Constants.BudibaseRoles.Admin:
|
||||
body.admin = { global: true }
|
||||
body.builder = { global: true }
|
||||
break
|
||||
|
@ -111,43 +150,47 @@ export function createUsersStore() {
|
|||
return body
|
||||
})
|
||||
const response = await API.createUsers(mappedUsers, data.groups)
|
||||
licensing.setQuotaUsage()
|
||||
|
||||
// re-search from first page
|
||||
await search()
|
||||
await this.search()
|
||||
return response
|
||||
}
|
||||
|
||||
async function del(id) {
|
||||
async delete(id: string) {
|
||||
await API.deleteUser(id)
|
||||
update(users => users.filter(user => user._id !== id))
|
||||
licensing.setQuotaUsage()
|
||||
}
|
||||
|
||||
async function getUserCountByApp(appId) {
|
||||
return await API.getUserCountByApp(appId)
|
||||
async bulkDelete(users: UserIdentifier[]) {
|
||||
const res = API.deleteUsers(users)
|
||||
licensing.setQuotaUsage()
|
||||
return res
|
||||
}
|
||||
|
||||
async function bulkDelete(users) {
|
||||
return API.deleteUsers(users)
|
||||
async save(user: User) {
|
||||
const res = await API.saveUser(user)
|
||||
licensing.setQuotaUsage()
|
||||
return res
|
||||
}
|
||||
|
||||
async function save(user) {
|
||||
return await API.saveUser(user)
|
||||
}
|
||||
|
||||
async function addAppBuilder(userId, appId) {
|
||||
async addAppBuilder(userId: string, appId: string) {
|
||||
return await API.addAppBuilder(userId, appId)
|
||||
}
|
||||
|
||||
async function removeAppBuilder(userId, appId) {
|
||||
async removeAppBuilder(userId: string, appId: string) {
|
||||
return await API.removeAppBuilder(userId, appId)
|
||||
}
|
||||
|
||||
async function getAccountHolder() {
|
||||
async getAccountHolder() {
|
||||
return await API.getAccountHolder()
|
||||
}
|
||||
|
||||
const getUserRole = user => {
|
||||
if (user && user.email === user.tenantOwnerEmail) {
|
||||
getUserRole(user?: User & { tenantOwnerEmail?: string }) {
|
||||
if (!user) {
|
||||
return Constants.BudibaseRoles.AppUser
|
||||
}
|
||||
if (user.email === user.tenantOwnerEmail) {
|
||||
return Constants.BudibaseRoles.Owner
|
||||
} else if (sdk.users.isAdmin(user)) {
|
||||
return Constants.BudibaseRoles.Admin
|
||||
|
@ -159,38 +202,6 @@ export function createUsersStore() {
|
|||
return Constants.BudibaseRoles.AppUser
|
||||
}
|
||||
}
|
||||
|
||||
const refreshUsage =
|
||||
fn =>
|
||||
async (...args) => {
|
||||
const response = await fn(...args)
|
||||
await licensing.setQuotaUsage()
|
||||
return response
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
search,
|
||||
get,
|
||||
getUserRole,
|
||||
fetch,
|
||||
invite,
|
||||
onboard,
|
||||
fetchInvite,
|
||||
getInvites,
|
||||
removeInvites,
|
||||
updateInvite,
|
||||
getUserCountByApp,
|
||||
addAppBuilder,
|
||||
removeAppBuilder,
|
||||
// any operation that adds or deletes users
|
||||
acceptInvite,
|
||||
create: refreshUsage(create),
|
||||
save: refreshUsage(save),
|
||||
bulkDelete: refreshUsage(bulkDelete),
|
||||
delete: refreshUsage(del),
|
||||
getAccountHolder,
|
||||
}
|
||||
}
|
||||
|
||||
export const users = createUsersStore()
|
||||
export const users = new UserStore()
|
|
@ -5139,7 +5139,8 @@
|
|||
{
|
||||
"type": "text",
|
||||
"label": "File name",
|
||||
"key": "key"
|
||||
"key": "key",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { createAPIClient } from "@budibase/frontend-core"
|
||||
import { authStore } from "../stores/auth.js"
|
||||
import { notificationStore, devToolsEnabled, devToolsStore } from "../stores/"
|
||||
import { authStore } from "../stores/auth"
|
||||
import {
|
||||
notificationStore,
|
||||
devToolsEnabled,
|
||||
devToolsStore,
|
||||
} from "../stores/index"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export const API = createAPIClient({
|
|
@ -1,5 +1,5 @@
|
|||
import { API } from "./api.js"
|
||||
import { patchAPI } from "./patches.js"
|
||||
import { API } from "./api"
|
||||
import { patchAPI } from "./patches"
|
||||
|
||||
// Certain endpoints which return rows need patched so that they transform
|
||||
// and enrich the row docs, so that they can be correctly handled by the
|
|
@ -1,19 +1,20 @@
|
|||
import { Constants } from "@budibase/frontend-core"
|
||||
import { Constants, APIClient } from "@budibase/frontend-core"
|
||||
import { FieldTypes } from "../constants"
|
||||
import { Row, Table } from "@budibase/types"
|
||||
|
||||
export const patchAPI = API => {
|
||||
export const patchAPI = (API: APIClient) => {
|
||||
/**
|
||||
* Enriches rows which contain certain field types so that they can
|
||||
* be properly displayed.
|
||||
* The ability to create these bindings has been removed, but they will still
|
||||
* exist in client apps to support backwards compatibility.
|
||||
*/
|
||||
const enrichRows = async (rows, tableId) => {
|
||||
const enrichRows = async (rows: Row[], tableId: string) => {
|
||||
if (!Array.isArray(rows)) {
|
||||
return []
|
||||
}
|
||||
if (rows.length) {
|
||||
const tables = {}
|
||||
const tables: Record<string, Table> = {}
|
||||
for (let row of rows) {
|
||||
// Fall back to passed in tableId if row doesn't have it specified
|
||||
let rowTableId = row.tableId || tableId
|
||||
|
@ -54,7 +55,7 @@ export const patchAPI = API => {
|
|||
const fetchSelf = API.fetchSelf
|
||||
API.fetchSelf = async () => {
|
||||
const user = await fetchSelf()
|
||||
if (user && user._id) {
|
||||
if (user && "_id" in user && user._id) {
|
||||
if (user.roleId === "PUBLIC") {
|
||||
// Don't try to enrich a public user as it will 403
|
||||
return user
|
||||
|
@ -66,10 +67,9 @@ export const patchAPI = API => {
|
|||
}
|
||||
}
|
||||
const fetchRelationshipData = API.fetchRelationshipData
|
||||
API.fetchRelationshipData = async params => {
|
||||
const tableId = params?.tableId
|
||||
const rows = await fetchRelationshipData(params)
|
||||
return await enrichRows(rows, tableId)
|
||||
API.fetchRelationshipData = async (sourceId, rowId, fieldName) => {
|
||||
const rows = await fetchRelationshipData(sourceId, rowId, fieldName)
|
||||
return await enrichRows(rows, sourceId)
|
||||
}
|
||||
const fetchTableData = API.fetchTableData
|
||||
API.fetchTableData = async tableId => {
|
||||
|
@ -85,19 +85,20 @@ export const patchAPI = API => {
|
|||
}
|
||||
}
|
||||
const fetchViewData = API.fetchViewData
|
||||
API.fetchViewData = async params => {
|
||||
API.fetchViewData = async (viewName, params) => {
|
||||
const tableId = params?.tableId
|
||||
const rows = await fetchViewData(params)
|
||||
const rows = await fetchViewData(viewName, params)
|
||||
return await enrichRows(rows, tableId)
|
||||
}
|
||||
|
||||
// Wipe any HBS formulae from table definitions, as these interfere with
|
||||
// Wipe any HBS formulas from table definitions, as these interfere with
|
||||
// handlebars enrichment
|
||||
const fetchTableDefinition = API.fetchTableDefinition
|
||||
API.fetchTableDefinition = async tableId => {
|
||||
const definition = await fetchTableDefinition(tableId)
|
||||
Object.keys(definition?.schema || {}).forEach(field => {
|
||||
if (definition.schema[field]?.type === "formula") {
|
||||
// @ts-expect-error TODO check what use case removing that would break
|
||||
delete definition.schema[field].formula
|
||||
}
|
||||
})
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { getContext, onDestroy, onMount, setContext } from "svelte"
|
||||
import { builderStore } from "stores/builder.js"
|
||||
import { blockStore } from "stores/blocks.js"
|
||||
import { blockStore } from "stores/blocks"
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable } = getContext("sdk")
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
getActionContextKey,
|
||||
getActionDependentContextKeys,
|
||||
} from "../utils/buttonActions.js"
|
||||
import { gridLayout } from "utils/grid.js"
|
||||
import { gridLayout } from "utils/grid"
|
||||
|
||||
export let instance = {}
|
||||
export let parent = null
|
||||
|
|
|
@ -1,22 +1,39 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte"
|
||||
import { Pagination, ProgressCircle } from "@budibase/bbui"
|
||||
import { fetchData, QueryUtils } from "@budibase/frontend-core"
|
||||
import { LogicalOperator, EmptyFilterOption } from "@budibase/types"
|
||||
import {
|
||||
LogicalOperator,
|
||||
EmptyFilterOption,
|
||||
TableSchema,
|
||||
SortOrder,
|
||||
SearchFilters,
|
||||
UISearchFilter,
|
||||
DataFetchDatasource,
|
||||
UserDatasource,
|
||||
GroupUserDatasource,
|
||||
DataFetchOptions,
|
||||
} from "@budibase/types"
|
||||
import { SDK, Component } from "../../index"
|
||||
|
||||
export let dataSource
|
||||
export let filter
|
||||
export let sortColumn
|
||||
export let sortOrder
|
||||
export let limit
|
||||
export let paginate
|
||||
export let autoRefresh
|
||||
type ProviderDatasource = Exclude<
|
||||
DataFetchDatasource,
|
||||
UserDatasource | GroupUserDatasource
|
||||
>
|
||||
|
||||
const { styleable, Provider, ActionTypes, API } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
export let dataSource: ProviderDatasource
|
||||
export let filter: UISearchFilter
|
||||
export let sortColumn: string
|
||||
export let sortOrder: SortOrder
|
||||
export let limit: number
|
||||
export let paginate: boolean
|
||||
export let autoRefresh: number
|
||||
|
||||
let interval
|
||||
let queryExtensions = {}
|
||||
const { styleable, Provider, ActionTypes, API } = getContext<SDK>("sdk")
|
||||
const component = getContext<Component>("component")
|
||||
|
||||
let interval: ReturnType<typeof setInterval>
|
||||
let queryExtensions: Record<string, any> = {}
|
||||
|
||||
$: defaultQuery = QueryUtils.buildQuery(filter)
|
||||
|
||||
|
@ -49,8 +66,14 @@
|
|||
},
|
||||
{
|
||||
type: ActionTypes.SetDataProviderSorting,
|
||||
callback: ({ column, order }) => {
|
||||
let newOptions = {}
|
||||
callback: ({
|
||||
column,
|
||||
order,
|
||||
}: {
|
||||
column: string
|
||||
order: SortOrder | undefined
|
||||
}) => {
|
||||
let newOptions: Partial<DataFetchOptions> = {}
|
||||
if (column) {
|
||||
newOptions.sortColumn = column
|
||||
}
|
||||
|
@ -63,6 +86,7 @@
|
|||
},
|
||||
},
|
||||
]
|
||||
|
||||
$: dataContext = {
|
||||
rows: $fetch.rows,
|
||||
info: $fetch.info,
|
||||
|
@ -75,14 +99,12 @@
|
|||
id: $component?.id,
|
||||
state: {
|
||||
query: $fetch.query,
|
||||
sortColumn: $fetch.sortColumn,
|
||||
sortOrder: $fetch.sortOrder,
|
||||
},
|
||||
limit,
|
||||
primaryDisplay: $fetch.definition?.primaryDisplay,
|
||||
primaryDisplay: ($fetch.definition as any)?.primaryDisplay,
|
||||
}
|
||||
|
||||
const createFetch = datasource => {
|
||||
const createFetch = (datasource: ProviderDatasource) => {
|
||||
return fetchData({
|
||||
API,
|
||||
datasource,
|
||||
|
@ -96,7 +118,7 @@
|
|||
})
|
||||
}
|
||||
|
||||
const sanitizeSchema = schema => {
|
||||
const sanitizeSchema = (schema: TableSchema | null) => {
|
||||
if (!schema) {
|
||||
return schema
|
||||
}
|
||||
|
@ -109,14 +131,14 @@
|
|||
return cloned
|
||||
}
|
||||
|
||||
const addQueryExtension = (key, extension) => {
|
||||
const addQueryExtension = (key: string, extension: any) => {
|
||||
if (!key || !extension) {
|
||||
return
|
||||
}
|
||||
queryExtensions = { ...queryExtensions, [key]: extension }
|
||||
}
|
||||
|
||||
const removeQueryExtension = key => {
|
||||
const removeQueryExtension = (key: string) => {
|
||||
if (!key) {
|
||||
return
|
||||
}
|
||||
|
@ -125,11 +147,14 @@
|
|||
queryExtensions = newQueryExtensions
|
||||
}
|
||||
|
||||
const extendQuery = (defaultQuery, extensions) => {
|
||||
const extendQuery = (
|
||||
defaultQuery: SearchFilters,
|
||||
extensions: Record<string, any>
|
||||
): SearchFilters => {
|
||||
if (!Object.keys(extensions).length) {
|
||||
return defaultQuery
|
||||
}
|
||||
const extended = {
|
||||
const extended: SearchFilters = {
|
||||
[LogicalOperator.AND]: {
|
||||
conditions: [
|
||||
...(defaultQuery ? [defaultQuery] : []),
|
||||
|
@ -140,12 +165,12 @@
|
|||
}
|
||||
|
||||
// If there are no conditions applied at all, clear the request.
|
||||
return extended[LogicalOperator.AND]?.conditions?.length > 0
|
||||
return (extended[LogicalOperator.AND]?.conditions?.length ?? 0) > 0
|
||||
? extended
|
||||
: null
|
||||
: {}
|
||||
}
|
||||
|
||||
const setUpAutoRefresh = autoRefresh => {
|
||||
const setUpAutoRefresh = (autoRefresh: number) => {
|
||||
clearInterval(interval)
|
||||
if (autoRefresh) {
|
||||
interval = setInterval(fetch.refresh, Math.max(10000, autoRefresh * 1000))
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import Block from "components/Block.svelte"
|
||||
import BlockComponent from "components/BlockComponent.svelte"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export let title
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import Block from "components/Block.svelte"
|
||||
import BlockComponent from "components/BlockComponent.svelte"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
|
||||
export let title
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
import Field from "./Field.svelte"
|
||||
import { CoreDropzone, ProgressCircle, Helpers } from "@budibase/bbui"
|
||||
import { getContext, onMount, onDestroy } from "svelte"
|
||||
import { builderStore } from "stores/builder.js"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
|
||||
export let datasourceId
|
||||
export let bucket
|
||||
|
@ -12,6 +14,8 @@
|
|||
export let validation
|
||||
export let onChange
|
||||
|
||||
const context = getContext("context")
|
||||
|
||||
let fieldState
|
||||
let fieldApi
|
||||
let localFiles = []
|
||||
|
@ -42,6 +46,9 @@
|
|||
// Process the file input and return a serializable structure expected by
|
||||
// the dropzone component to display the file
|
||||
const processFiles = async fileList => {
|
||||
if ($builderStore.inBuilder) {
|
||||
return []
|
||||
}
|
||||
return await new Promise(resolve => {
|
||||
if (!fileList?.length) {
|
||||
return []
|
||||
|
@ -78,9 +85,15 @@
|
|||
}
|
||||
|
||||
const upload = async () => {
|
||||
const processedFileKey = processStringSync(key, $context)
|
||||
loading = true
|
||||
try {
|
||||
const res = await API.externalUpload(datasourceId, bucket, key, data)
|
||||
const res = await API.externalUpload(
|
||||
datasourceId,
|
||||
bucket,
|
||||
processedFileKey,
|
||||
data
|
||||
)
|
||||
notificationStore.actions.success("File uploaded successfully")
|
||||
loading = false
|
||||
return res
|
||||
|
@ -126,7 +139,7 @@
|
|||
bind:fieldApi
|
||||
defaultValue={[]}
|
||||
>
|
||||
<div class="content">
|
||||
<div class="content" class:builder={$builderStore.inBuilder}>
|
||||
{#if fieldState}
|
||||
<CoreDropzone
|
||||
value={localFiles}
|
||||
|
@ -149,6 +162,9 @@
|
|||
</Field>
|
||||
|
||||
<style>
|
||||
.content.builder :global(.spectrum-Dropzone) {
|
||||
pointer-events: none;
|
||||
}
|
||||
.content {
|
||||
position: relative;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
interface Window {
|
||||
"##BUDIBASE_APP_ID##": string
|
||||
"##BUDIBASE_IN_BUILDER##": string
|
||||
MIGRATING_APP: boolean
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { APIClient } from "@budibase/frontend-core"
|
||||
import type { ActionTypes } from "./constants"
|
||||
import { Readable } from "svelte/store"
|
||||
|
||||
export interface SDK {
|
||||
API: APIClient
|
||||
styleable: any
|
||||
Provider: any
|
||||
ActionTypes: typeof ActionTypes
|
||||
}
|
||||
|
||||
export type Component = Readable<{
|
||||
id: string
|
||||
styles: any
|
||||
}>
|
|
@ -29,7 +29,7 @@ import { ActionTypes } from "./constants"
|
|||
import {
|
||||
fetchDatasourceSchema,
|
||||
fetchDatasourceDefinition,
|
||||
} from "./utils/schema.js"
|
||||
} from "./utils/schema"
|
||||
import { getAPIKey } from "./utils/api.js"
|
||||
import { enrichButtonActions } from "./utils/buttonActions.js"
|
||||
import { processStringSync, makePropSafe } from "@budibase/string-templates"
|
||||
|
@ -74,6 +74,7 @@ export default {
|
|||
fetchData,
|
||||
QueryUtils,
|
||||
ContextScopes: Constants.ContextScopes,
|
||||
// This is not used internally but exposed to users to be used in plugins
|
||||
getAPIKey,
|
||||
enrichButtonActions,
|
||||
processStringSync,
|
||||
|
|
|
@ -2,7 +2,9 @@ import { API } from "api"
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
const createAuthStore = () => {
|
||||
const store = writable(null)
|
||||
const store = writable<{
|
||||
csrfToken?: string
|
||||
} | null>(null)
|
||||
|
||||
// Fetches the user object if someone is logged in and has reloaded the page
|
||||
const fetchUser = async () => {
|
|
@ -6,7 +6,7 @@ import { screenStore } from "./screens"
|
|||
import { builderStore } from "./builder"
|
||||
import Router from "../components/Router.svelte"
|
||||
import * as AppComponents from "../components/app/index.js"
|
||||
import { ScreenslotType } from "../constants.js"
|
||||
import { ScreenslotType } from "../constants"
|
||||
|
||||
export const BudibasePrefix = "@budibase/standard-components/"
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue