Merge branch 'master' into fix/relationship-select
This commit is contained in:
commit
79c960d86f
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "3.4.9",
|
"version": "3.4.11",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"concurrency": 20,
|
"concurrency": 20,
|
||||||
"command": {
|
"command": {
|
||||||
|
|
|
@ -67,6 +67,15 @@ describe("utils", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("gets appId from query params", async () => {
|
||||||
|
const ctx = structures.koa.newContext()
|
||||||
|
const expected = db.generateAppID()
|
||||||
|
ctx.query = { appId: expected }
|
||||||
|
|
||||||
|
const actual = await utils.getAppIdFromCtx(ctx)
|
||||||
|
expect(actual).toBe(expected)
|
||||||
|
})
|
||||||
|
|
||||||
it("doesn't get appId from url when previewing", async () => {
|
it("doesn't get appId from url when previewing", async () => {
|
||||||
const ctx = structures.koa.newContext()
|
const ctx = structures.koa.newContext()
|
||||||
const appId = db.generateAppID()
|
const appId = db.generateAppID()
|
||||||
|
|
|
@ -101,6 +101,11 @@ export async function getAppIdFromCtx(ctx: Ctx) {
|
||||||
appId = confirmAppId(pathId)
|
appId = confirmAppId(pathId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// look in queryParams
|
||||||
|
if (!appId && ctx.query?.appId) {
|
||||||
|
appId = confirmAppId(ctx.query?.appId as string)
|
||||||
|
}
|
||||||
|
|
||||||
// lookup using custom url - prod apps only
|
// lookup using custom url - prod apps only
|
||||||
// filter out the builder preview path which collides with the prod app path
|
// filter out the builder preview path which collides with the prod app path
|
||||||
// to ensure we don't load all apps excessively
|
// to ensure we don't load all apps excessively
|
||||||
|
|
|
@ -386,7 +386,7 @@
|
||||||
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
||||||
} else if (editableColumn.type === FieldType.FORMULA) {
|
} else if (editableColumn.type === FieldType.FORMULA) {
|
||||||
editableColumn.formulaType = "dynamic"
|
editableColumn.formulaType = "dynamic"
|
||||||
editableColumn.responseType = field.responseType || FIELDS.STRING.type
|
editableColumn.responseType = field?.responseType || FIELDS.STRING.type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,16 +40,19 @@
|
||||||
indentMore,
|
indentMore,
|
||||||
indentLess,
|
indentLess,
|
||||||
} from "@codemirror/commands"
|
} from "@codemirror/commands"
|
||||||
|
import { setDiagnostics } from "@codemirror/lint"
|
||||||
import { Compartment, EditorState } from "@codemirror/state"
|
import { Compartment, EditorState } from "@codemirror/state"
|
||||||
|
import type { Extension } from "@codemirror/state"
|
||||||
import { javascript } from "@codemirror/lang-javascript"
|
import { javascript } from "@codemirror/lang-javascript"
|
||||||
import { EditorModes } from "./"
|
import { EditorModes } from "./"
|
||||||
import { themeStore } from "@/stores/portal"
|
import { themeStore } from "@/stores/portal"
|
||||||
import type { EditorMode } from "@budibase/types"
|
import type { EditorMode } from "@budibase/types"
|
||||||
import type { BindingCompletion } from "@/types"
|
import type { BindingCompletion, CodeValidator } from "@/types"
|
||||||
|
import { validateHbsTemplate } from "./validator/hbs"
|
||||||
|
|
||||||
export let label: string | undefined = undefined
|
export let label: string | undefined = undefined
|
||||||
// TODO: work out what best type fits this
|
|
||||||
export let completions: BindingCompletion[] = []
|
export let completions: BindingCompletion[] = []
|
||||||
|
export let validations: CodeValidator | null = null
|
||||||
export let mode: EditorMode = EditorModes.Handlebars
|
export let mode: EditorMode = EditorModes.Handlebars
|
||||||
export let value: string | null = ""
|
export let value: string | null = ""
|
||||||
export let placeholder: string | null = null
|
export let placeholder: string | null = null
|
||||||
|
@ -248,7 +251,7 @@
|
||||||
// None of this is reactive, but it never has been, so we just assume most
|
// None of this is reactive, but it never has been, so we just assume most
|
||||||
// config flags aren't changed at runtime
|
// config flags aren't changed at runtime
|
||||||
// TODO: work out type for base
|
// TODO: work out type for base
|
||||||
const buildExtensions = (base: any[]) => {
|
const buildExtensions = (base: Extension[]) => {
|
||||||
let complete = [...base]
|
let complete = [...base]
|
||||||
|
|
||||||
if (autocompleteEnabled) {
|
if (autocompleteEnabled) {
|
||||||
|
@ -340,6 +343,24 @@
|
||||||
return complete
|
return complete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validate(
|
||||||
|
value: string | null,
|
||||||
|
editor: EditorView | undefined,
|
||||||
|
mode: EditorMode,
|
||||||
|
validations: CodeValidator | null
|
||||||
|
) {
|
||||||
|
if (!value || !validations || !editor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === EditorModes.Handlebars) {
|
||||||
|
const diagnostics = validateHbsTemplate(value, validations)
|
||||||
|
editor.dispatch(setDiagnostics(editor.state, diagnostics))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: validate(value, editor, mode, validations)
|
||||||
|
|
||||||
const initEditor = () => {
|
const initEditor = () => {
|
||||||
const baseExtensions = buildBaseExtensions()
|
const baseExtensions = buildBaseExtensions()
|
||||||
|
|
||||||
|
@ -366,7 +387,6 @@
|
||||||
<Label size="S">{label}</Label>
|
<Label size="S">{label}</Label>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class={`code-editor ${mode?.name || ""}`}>
|
<div class={`code-editor ${mode?.name || ""}`}>
|
||||||
<div tabindex="-1" bind:this={textarea} />
|
<div tabindex="-1" bind:this={textarea} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -83,6 +83,8 @@ const helpersToCompletion = (
|
||||||
const helper = helpers[helperName]
|
const helper = helpers[helperName]
|
||||||
return {
|
return {
|
||||||
label: helperName,
|
label: helperName,
|
||||||
|
args: helper.args,
|
||||||
|
requiresBlock: helper.requiresBlock,
|
||||||
info: () => buildHelperInfoNode(helper),
|
info: () => buildHelperInfoNode(helper),
|
||||||
type: "helper",
|
type: "helper",
|
||||||
section: helperSection,
|
section: helperSection,
|
||||||
|
@ -136,9 +138,13 @@ export const hbAutocomplete = (
|
||||||
baseCompletions: BindingCompletionOption[]
|
baseCompletions: BindingCompletionOption[]
|
||||||
): BindingCompletion => {
|
): BindingCompletion => {
|
||||||
function coreCompletion(context: CompletionContext) {
|
function coreCompletion(context: CompletionContext) {
|
||||||
let bindingStart = context.matchBefore(EditorModes.Handlebars.match)
|
if (!baseCompletions.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
let options = baseCompletions || []
|
const bindingStart = context.matchBefore(EditorModes.Handlebars.match)
|
||||||
|
|
||||||
|
const options = baseCompletions
|
||||||
|
|
||||||
if (!bindingStart) {
|
if (!bindingStart) {
|
||||||
return null
|
return null
|
||||||
|
@ -149,7 +155,7 @@ export const hbAutocomplete = (
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const query = bindingStart.text.replace(match[0], "")
|
const query = bindingStart.text.replace(match[0], "")
|
||||||
let filtered = bindingFilter(options, query)
|
const filtered = bindingFilter(options, query)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from: bindingStart.from + match[0].length,
|
from: bindingStart.from + match[0].length,
|
||||||
|
@ -169,8 +175,12 @@ export const jsAutocomplete = (
|
||||||
baseCompletions: BindingCompletionOption[]
|
baseCompletions: BindingCompletionOption[]
|
||||||
): BindingCompletion => {
|
): BindingCompletion => {
|
||||||
function coreCompletion(context: CompletionContext) {
|
function coreCompletion(context: CompletionContext) {
|
||||||
let jsBinding = wrappedAutocompleteMatch(context)
|
if (!baseCompletions.length) {
|
||||||
let options = baseCompletions || []
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsBinding = wrappedAutocompleteMatch(context)
|
||||||
|
const options = baseCompletions
|
||||||
|
|
||||||
if (jsBinding) {
|
if (jsBinding) {
|
||||||
// Accommodate spaces
|
// Accommodate spaces
|
||||||
|
@ -209,6 +219,10 @@ function setAutocomplete(
|
||||||
options: BindingCompletionOption[]
|
options: BindingCompletionOption[]
|
||||||
): BindingCompletion {
|
): BindingCompletion {
|
||||||
return function (context: CompletionContext) {
|
return function (context: CompletionContext) {
|
||||||
|
if (!options.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (wrappedAutocompleteMatch(context)) {
|
if (wrappedAutocompleteMatch(context)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
/* global hbs */
|
||||||
|
import Handlebars from "handlebars"
|
||||||
|
import type { Diagnostic } from "@codemirror/lint"
|
||||||
|
import { CodeValidator } from "@/types"
|
||||||
|
|
||||||
|
function isMustacheStatement(
|
||||||
|
node: hbs.AST.Statement
|
||||||
|
): node is hbs.AST.MustacheStatement {
|
||||||
|
return node.type === "MustacheStatement"
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBlockStatement(
|
||||||
|
node: hbs.AST.Statement
|
||||||
|
): node is hbs.AST.BlockStatement {
|
||||||
|
return node.type === "BlockStatement"
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPathExpression(
|
||||||
|
node: hbs.AST.Statement
|
||||||
|
): node is hbs.AST.PathExpression {
|
||||||
|
return node.type === "PathExpression"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateHbsTemplate(
|
||||||
|
text: string,
|
||||||
|
validations: CodeValidator
|
||||||
|
): Diagnostic[] {
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ast = Handlebars.parse(text, {})
|
||||||
|
|
||||||
|
const lineOffsets: number[] = []
|
||||||
|
let offset = 0
|
||||||
|
for (const line of text.split("\n")) {
|
||||||
|
lineOffsets.push(offset)
|
||||||
|
offset += line.length + 1 // +1 for newline character
|
||||||
|
}
|
||||||
|
|
||||||
|
function traverseNodes(
|
||||||
|
nodes: hbs.AST.Statement[],
|
||||||
|
options?: {
|
||||||
|
ignoreMissing?: boolean
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const ignoreMissing = options?.ignoreMissing || false
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (isMustacheStatement(node) && isPathExpression(node.path)) {
|
||||||
|
const helperName = node.path.original
|
||||||
|
|
||||||
|
const from =
|
||||||
|
lineOffsets[node.loc.start.line - 1] + node.loc.start.column
|
||||||
|
const to = lineOffsets[node.loc.end.line - 1] + node.loc.end.column
|
||||||
|
|
||||||
|
if (!(helperName in validations)) {
|
||||||
|
if (!ignoreMissing) {
|
||||||
|
diagnostics.push({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
severity: "warning",
|
||||||
|
message: `"${helperName}" handler does not exist.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { arguments: expectedArguments = [], requiresBlock } =
|
||||||
|
validations[helperName]
|
||||||
|
|
||||||
|
if (requiresBlock && !isBlockStatement(node)) {
|
||||||
|
diagnostics.push({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
severity: "error",
|
||||||
|
message: `Helper "${helperName}" requires a body:\n{{#${helperName} ...}} [body] {{/${helperName}}}`,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const providedParams = node.params
|
||||||
|
|
||||||
|
if (providedParams.length !== expectedArguments.length) {
|
||||||
|
diagnostics.push({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
severity: "error",
|
||||||
|
message: `Helper "${helperName}" expects ${
|
||||||
|
expectedArguments.length
|
||||||
|
} parameters (${expectedArguments.join(", ")}), but got ${
|
||||||
|
providedParams.length
|
||||||
|
}.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBlockStatement(node)) {
|
||||||
|
traverseNodes(node.program.body, { ignoreMissing: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
traverseNodes(ast.body, { ignoreMissing: true })
|
||||||
|
} catch (e: any) {
|
||||||
|
diagnostics.push({
|
||||||
|
from: 0,
|
||||||
|
to: text.length,
|
||||||
|
severity: "error",
|
||||||
|
message: `The handlebars code is not valid:\n${e.message}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostics
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { validateHbsTemplate } from "../hbs"
|
||||||
|
import { CodeValidator } from "@/types"
|
||||||
|
|
||||||
|
describe("hbs validator", () => {
|
||||||
|
it("validate empty strings", () => {
|
||||||
|
const text = ""
|
||||||
|
const validators = {}
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("validate strings without hbs expressions", () => {
|
||||||
|
const text = "first line\nand another one"
|
||||||
|
const validators = {}
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("basic expressions", () => {
|
||||||
|
const validators = {
|
||||||
|
fieldName: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
it("validate valid expressions", () => {
|
||||||
|
const text = "{{ fieldName }}"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not throw on missing validations", () => {
|
||||||
|
const text = "{{ anotherFieldName }}"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Waiting for missing fields validation
|
||||||
|
it.skip("throws on untrimmed invalid expressions", () => {
|
||||||
|
const text = " {{ anotherFieldName }}"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
from: 4,
|
||||||
|
message: `"anotherFieldName" handler does not exist.`,
|
||||||
|
severity: "warning",
|
||||||
|
to: 26,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Waiting for missing fields validation
|
||||||
|
it.skip("throws on invalid expressions between valid lines", () => {
|
||||||
|
const text =
|
||||||
|
"literal expression\nthe value is {{ anotherFieldName }}\nanother expression"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
from: 32,
|
||||||
|
message: `"anotherFieldName" handler does not exist.`,
|
||||||
|
severity: "warning",
|
||||||
|
to: 54,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("expressions with whitespaces", () => {
|
||||||
|
const validators = {
|
||||||
|
[`field name`]: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
it("validates expressions with whitespaces", () => {
|
||||||
|
const text = `{{ [field name] }}`
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Waiting for missing fields validation
|
||||||
|
it.skip("throws if not wrapped between brackets", () => {
|
||||||
|
const text = `{{ field name }}`
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
from: 0,
|
||||||
|
message: `"field" handler does not exist.`,
|
||||||
|
severity: "warning",
|
||||||
|
to: 16,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("expressions with parameters", () => {
|
||||||
|
const validators: CodeValidator = {
|
||||||
|
helperFunction: {
|
||||||
|
arguments: ["a", "b", "c"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
it("validate valid params", () => {
|
||||||
|
const text = "{{ helperFunction 1 99 'a' }}"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws on too few params", () => {
|
||||||
|
const text = "{{ helperFunction 100 }}"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
from: 0,
|
||||||
|
message: `Helper "helperFunction" expects 3 parameters (a, b, c), but got 1.`,
|
||||||
|
severity: "error",
|
||||||
|
to: 24,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws on too many params", () => {
|
||||||
|
const text = "{{ helperFunction 1 99 'a' 100 }}"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
from: 0,
|
||||||
|
message: `Helper "helperFunction" expects 3 parameters (a, b, c), but got 4.`,
|
||||||
|
severity: "error",
|
||||||
|
to: 34,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -42,7 +42,7 @@
|
||||||
JSONValue,
|
JSONValue,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import type { Log } from "@budibase/string-templates"
|
import type { Log } from "@budibase/string-templates"
|
||||||
import type { BindingCompletion, BindingCompletionOption } from "@/types"
|
import type { CodeValidator } from "@/types"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
export let showTabBar = true
|
export let showTabBar = true
|
||||||
|
|
||||||
let mode: BindingMode | null
|
let mode: BindingMode
|
||||||
let sidePanel: SidePanel | null
|
let sidePanel: SidePanel | null
|
||||||
let initialValueJS = value?.startsWith?.("{{ js ")
|
let initialValueJS = value?.startsWith?.("{{ js ")
|
||||||
let jsValue: string | null = initialValueJS ? value : null
|
let jsValue: string | null = initialValueJS ? value : null
|
||||||
|
@ -88,13 +88,37 @@
|
||||||
| null
|
| null
|
||||||
$: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value)
|
$: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value)
|
||||||
$: requestEval(runtimeExpression, context, snippets)
|
$: requestEval(runtimeExpression, context, snippets)
|
||||||
$: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode)
|
|
||||||
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
|
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
|
||||||
$: hbsCompletions = getHBSCompletions(bindingCompletions)
|
|
||||||
$: jsCompletions = getJSCompletions(bindingCompletions, snippets, {
|
$: bindingOptions = bindingsToCompletions(bindings, editorMode)
|
||||||
useHelpers: allowHelpers,
|
$: helperOptions = allowHelpers ? getHelperCompletions(editorMode) : []
|
||||||
useSnippets,
|
$: snippetsOptions =
|
||||||
})
|
usingJS && useSnippets && snippets?.length ? snippets : []
|
||||||
|
|
||||||
|
$: completions = !usingJS
|
||||||
|
? [hbAutocomplete([...bindingOptions, ...helperOptions])]
|
||||||
|
: [
|
||||||
|
jsAutocomplete(bindingOptions),
|
||||||
|
jsHelperAutocomplete(helperOptions),
|
||||||
|
snippetAutoComplete(snippetsOptions),
|
||||||
|
]
|
||||||
|
|
||||||
|
$: validations = {
|
||||||
|
...bindingOptions.reduce<CodeValidator>((validations, option) => {
|
||||||
|
validations[option.label] = {
|
||||||
|
arguments: [],
|
||||||
|
}
|
||||||
|
return validations
|
||||||
|
}, {}),
|
||||||
|
...helperOptions.reduce<CodeValidator>((validations, option) => {
|
||||||
|
validations[option.label] = {
|
||||||
|
arguments: option.args,
|
||||||
|
requiresBlock: option.requiresBlock,
|
||||||
|
}
|
||||||
|
return validations
|
||||||
|
}, {}),
|
||||||
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// Ensure a valid side panel option is always selected
|
// Ensure a valid side panel option is always selected
|
||||||
if (sidePanel && !sidePanelOptions.includes(sidePanel)) {
|
if (sidePanel && !sidePanelOptions.includes(sidePanel)) {
|
||||||
|
@ -102,38 +126,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHBSCompletions = (bindingCompletions: BindingCompletionOption[]) => {
|
|
||||||
return [
|
|
||||||
hbAutocomplete([
|
|
||||||
...bindingCompletions,
|
|
||||||
...getHelperCompletions(EditorModes.Handlebars),
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const getJSCompletions = (
|
|
||||||
bindingCompletions: BindingCompletionOption[],
|
|
||||||
snippets: Snippet[] | null,
|
|
||||||
config: {
|
|
||||||
useHelpers: boolean
|
|
||||||
useSnippets: boolean
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const completions: BindingCompletion[] = []
|
|
||||||
if (bindingCompletions.length) {
|
|
||||||
completions.push(jsAutocomplete([...bindingCompletions]))
|
|
||||||
}
|
|
||||||
if (config.useHelpers) {
|
|
||||||
completions.push(
|
|
||||||
jsHelperAutocomplete([...getHelperCompletions(EditorModes.JS)])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (config.useSnippets && snippets) {
|
|
||||||
completions.push(snippetAutoComplete(snippets))
|
|
||||||
}
|
|
||||||
return completions
|
|
||||||
}
|
|
||||||
|
|
||||||
const getModeOptions = (allowHBS: boolean, allowJS: boolean) => {
|
const getModeOptions = (allowHBS: boolean, allowJS: boolean) => {
|
||||||
let options = []
|
let options = []
|
||||||
if (allowHBS) {
|
if (allowHBS) {
|
||||||
|
@ -213,7 +205,7 @@
|
||||||
bindings: EnrichedBinding[],
|
bindings: EnrichedBinding[],
|
||||||
context: any,
|
context: any,
|
||||||
snippets: Snippet[] | null
|
snippets: Snippet[] | null
|
||||||
) => {
|
): EnrichedBinding[] => {
|
||||||
// Create a single big array to enrich in one go
|
// Create a single big array to enrich in one go
|
||||||
const bindingStrings = bindings.map(binding => {
|
const bindingStrings = bindings.map(binding => {
|
||||||
if (binding.runtimeBinding.startsWith('trim "')) {
|
if (binding.runtimeBinding.startsWith('trim "')) {
|
||||||
|
@ -290,7 +282,7 @@
|
||||||
jsValue = null
|
jsValue = null
|
||||||
hbsValue = null
|
hbsValue = null
|
||||||
updateValue(null)
|
updateValue(null)
|
||||||
mode = targetMode
|
mode = targetMode!
|
||||||
targetMode = null
|
targetMode = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,13 +357,14 @@
|
||||||
{/if}
|
{/if}
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
{#if mode === BindingMode.Text}
|
{#if mode === BindingMode.Text}
|
||||||
{#key hbsCompletions}
|
{#key completions}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={hbsValue}
|
value={hbsValue}
|
||||||
on:change={onChangeHBSValue}
|
on:change={onChangeHBSValue}
|
||||||
bind:getCaretPosition
|
bind:getCaretPosition
|
||||||
bind:insertAtPos
|
bind:insertAtPos
|
||||||
completions={hbsCompletions}
|
{completions}
|
||||||
|
{validations}
|
||||||
autofocus={autofocusEditor}
|
autofocus={autofocusEditor}
|
||||||
placeholder={placeholder ||
|
placeholder={placeholder ||
|
||||||
"Add bindings by typing {{ or use the menu on the right"}
|
"Add bindings by typing {{ or use the menu on the right"}
|
||||||
|
@ -379,18 +372,18 @@
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
{:else if mode === BindingMode.JavaScript}
|
{:else if mode === BindingMode.JavaScript}
|
||||||
{#key jsCompletions}
|
{#key completions}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={jsValue ? decodeJSBinding(jsValue) : jsValue}
|
value={jsValue ? decodeJSBinding(jsValue) : jsValue}
|
||||||
on:change={onChangeJSValue}
|
on:change={onChangeJSValue}
|
||||||
completions={jsCompletions}
|
{completions}
|
||||||
mode={EditorModes.JS}
|
mode={EditorModes.JS}
|
||||||
bind:getCaretPosition
|
bind:getCaretPosition
|
||||||
bind:insertAtPos
|
bind:insertAtPos
|
||||||
autofocus={autofocusEditor}
|
autofocus={autofocusEditor}
|
||||||
placeholder={placeholder ||
|
placeholder={placeholder ||
|
||||||
"Add bindings by typing $ or use the menu on the right"}
|
"Add bindings by typing $ or use the menu on the right"}
|
||||||
jsBindingWrapping={bindingCompletions.length > 0}
|
jsBindingWrapping={completions.length > 0}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -43,7 +43,6 @@
|
||||||
<EditComponentPopover
|
<EditComponentPopover
|
||||||
{anchor}
|
{anchor}
|
||||||
componentInstance={item}
|
componentInstance={item}
|
||||||
{componentBindings}
|
|
||||||
{bindings}
|
{bindings}
|
||||||
on:change
|
on:change
|
||||||
parseSettings={updatedNestedFlags}
|
parseSettings={updatedNestedFlags}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Popover, Layout } from "@budibase/bbui"
|
import { Icon, Popover, Layout } from "@budibase/bbui"
|
||||||
import { componentStore } from "@/stores/builder"
|
import { componentStore, selectedScreen } from "@/stores/builder"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
import ComponentSettingsSection from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
|
import ComponentSettingsSection from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
|
||||||
|
import { getComponentBindableProperties } from "@/dataBinding"
|
||||||
|
|
||||||
export let anchor
|
export let anchor
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let componentBindings
|
|
||||||
export let bindings
|
export let bindings
|
||||||
export let parseSettings
|
export let parseSettings
|
||||||
|
|
||||||
|
@ -28,6 +28,10 @@
|
||||||
}
|
}
|
||||||
$: componentDef = componentStore.getDefinition(componentInstance._component)
|
$: componentDef = componentStore.getDefinition(componentInstance._component)
|
||||||
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
|
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
|
||||||
|
$: componentBindings = getComponentBindableProperties(
|
||||||
|
$selectedScreen,
|
||||||
|
$componentStore.selectedComponentId
|
||||||
|
)
|
||||||
|
|
||||||
const open = () => {
|
const open = () => {
|
||||||
isOpen = true
|
isOpen = true
|
||||||
|
|
|
@ -45,7 +45,6 @@
|
||||||
<EditComponentPopover
|
<EditComponentPopover
|
||||||
{anchor}
|
{anchor}
|
||||||
componentInstance={item}
|
componentInstance={item}
|
||||||
{componentBindings}
|
|
||||||
{bindings}
|
{bindings}
|
||||||
{parseSettings}
|
{parseSettings}
|
||||||
on:change
|
on:change
|
||||||
|
|
|
@ -22,25 +22,59 @@
|
||||||
export let propertyFocus = false
|
export let propertyFocus = false
|
||||||
export let info = null
|
export let info = null
|
||||||
export let disableBindings = false
|
export let disableBindings = false
|
||||||
export let wide
|
export let wide = false
|
||||||
|
export let contextAccess = null
|
||||||
|
|
||||||
let highlightType
|
let highlightType
|
||||||
let domElement
|
let domElement
|
||||||
|
|
||||||
$: highlightedProp = $builderStore.highlightedSetting
|
$: highlightedProp = $builderStore.highlightedSetting
|
||||||
$: allBindings = getAllBindings(bindings, componentBindings, nested)
|
$: allBindings = getAllBindings(
|
||||||
|
bindings,
|
||||||
|
componentBindings,
|
||||||
|
nested,
|
||||||
|
contextAccess
|
||||||
|
)
|
||||||
$: safeValue = getSafeValue(value, defaultValue, allBindings)
|
$: safeValue = getSafeValue(value, defaultValue, allBindings)
|
||||||
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
|
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
|
||||||
|
|
||||||
$: isHighlighted = highlightedProp?.key === key
|
$: isHighlighted = highlightedProp?.key === key
|
||||||
|
|
||||||
$: highlightType = isHighlighted ? `highlighted-${highlightedProp?.type}` : ""
|
$: highlightType = isHighlighted ? `highlighted-${highlightedProp?.type}` : ""
|
||||||
|
$: highlightedProp && isHighlighted && scrollToElement(domElement)
|
||||||
|
|
||||||
const getAllBindings = (bindings, componentBindings, nested) => {
|
const getAllBindings = (
|
||||||
if (!nested) {
|
bindings,
|
||||||
|
componentBindings,
|
||||||
|
nested,
|
||||||
|
contextAccess
|
||||||
|
) => {
|
||||||
|
// contextAccess is a bit of an escape hatch to get around how we render
|
||||||
|
// certain settings types by using a pseudo component definition, leading
|
||||||
|
// to problems with the nested flag
|
||||||
|
if (contextAccess != null) {
|
||||||
|
// Optionally include global bindings
|
||||||
|
let allBindings = contextAccess.global ? bindings : []
|
||||||
|
|
||||||
|
// Optionally include or exclude self (component) bindings.
|
||||||
|
// If this is a nested setting then we will already have our own context
|
||||||
|
// bindings mixed in, so if we don't want self context we need to filter
|
||||||
|
// them out.
|
||||||
|
if (contextAccess.self) {
|
||||||
|
return [...allBindings, ...componentBindings]
|
||||||
|
} else {
|
||||||
|
return allBindings.filter(binding => {
|
||||||
|
return !componentBindings.some(componentBinding => {
|
||||||
|
return componentBinding.runtimeBinding === binding.runtimeBinding
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise just honour the normal nested flag
|
||||||
|
if (nested) {
|
||||||
|
return [...bindings, ...componentBindings]
|
||||||
|
} else {
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
return [...(componentBindings || []), ...(bindings || [])]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle a value change of any type
|
// Handle a value change of any type
|
||||||
|
@ -81,8 +115,6 @@
|
||||||
block: "center",
|
block: "center",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$: highlightedProp && isHighlighted && scrollToElement(domElement)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -147,6 +147,7 @@
|
||||||
{componentInstance}
|
{componentInstance}
|
||||||
{componentDefinition}
|
{componentDefinition}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
{componentBindings}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
@ -151,6 +151,7 @@
|
||||||
propertyFocus={$builderStore.propertyFocus === setting.key}
|
propertyFocus={$builderStore.propertyFocus === setting.key}
|
||||||
info={setting.info}
|
info={setting.info}
|
||||||
disableBindings={setting.disableBindings}
|
disableBindings={setting.disableBindings}
|
||||||
|
contextAccess={setting.contextAccess}
|
||||||
props={{
|
props={{
|
||||||
// Generic settings
|
// Generic settings
|
||||||
placeholder: setting.placeholder || null,
|
placeholder: setting.placeholder || null,
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
export let conditions = []
|
export let conditions = []
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
export let componentBindings = []
|
||||||
|
|
||||||
const flipDurationMs = 150
|
const flipDurationMs = 150
|
||||||
const actionOptions = [
|
const actionOptions = [
|
||||||
|
@ -55,6 +56,7 @@
|
||||||
]
|
]
|
||||||
|
|
||||||
let dragDisabled = true
|
let dragDisabled = true
|
||||||
|
|
||||||
$: settings = componentStore
|
$: settings = componentStore
|
||||||
.getComponentSettings($selectedComponent?._component)
|
.getComponentSettings($selectedComponent?._component)
|
||||||
?.concat({
|
?.concat({
|
||||||
|
@ -213,7 +215,10 @@
|
||||||
options: definition.options,
|
options: definition.options,
|
||||||
placeholder: definition.placeholder,
|
placeholder: definition.placeholder,
|
||||||
}}
|
}}
|
||||||
|
nested={definition.nested}
|
||||||
|
contextAccess={definition.contextAccess}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
{componentBindings}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Select disabled placeholder=" " />
|
<Select disabled placeholder=" " />
|
||||||
|
|
|
@ -64,7 +64,12 @@
|
||||||
Show, hide and update components in response to conditions being met.
|
Show, hide and update components in response to conditions being met.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<Button cta slot="buttons" on:click={() => save()}>Save</Button>
|
<Button cta slot="buttons" on:click={() => save()}>Save</Button>
|
||||||
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} {bindings} />
|
<ConditionalUIDrawer
|
||||||
|
slot="body"
|
||||||
|
bind:conditions={tempValue}
|
||||||
|
{bindings}
|
||||||
|
{componentBindings}
|
||||||
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { findComponentParent, findComponentPath } from "@/helpers/components"
|
import { findComponentParent, findComponentPath } from "@/helpers/components"
|
||||||
import { selectedScreen, componentStore } from "@/stores/builder"
|
import { selectedScreen, componentStore } from "@/stores/builder"
|
||||||
|
import { DropPosition } from "@budibase/types"
|
||||||
export const DropPosition = {
|
export { DropPosition } from "@budibase/types"
|
||||||
ABOVE: "above",
|
|
||||||
BELOW: "below",
|
|
||||||
INSIDE: "inside",
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
source: null,
|
source: null,
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import { layoutStore } from "./layouts.js"
|
import { layoutStore } from "./layouts"
|
||||||
import { appStore } from "./app.js"
|
import { appStore } from "./app"
|
||||||
import { componentStore, selectedComponent } from "./components"
|
import { componentStore, selectedComponent } from "./components"
|
||||||
import { navigationStore } from "./navigation.js"
|
import { navigationStore } from "./navigation"
|
||||||
import { themeStore } from "./theme.js"
|
import { themeStore } from "./theme"
|
||||||
import { screenStore, selectedScreen, sortedScreens } from "./screens"
|
import { screenStore, selectedScreen, sortedScreens } from "./screens"
|
||||||
import { builderStore } from "./builder.js"
|
import { builderStore } from "./builder"
|
||||||
import { hoverStore } from "./hover.js"
|
import { hoverStore } from "./hover"
|
||||||
import { previewStore } from "./preview.js"
|
import { previewStore } from "./preview"
|
||||||
import {
|
import {
|
||||||
automationStore,
|
automationStore,
|
||||||
selectedAutomation,
|
selectedAutomation,
|
||||||
automationHistoryStore,
|
automationHistoryStore,
|
||||||
} from "./automations.js"
|
} from "./automations"
|
||||||
import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js"
|
import { userStore, userSelectedResourceMap, isOnlyUser } from "./users"
|
||||||
import { deploymentStore } from "./deployments.js"
|
import { deploymentStore } from "./deployments"
|
||||||
import { contextMenuStore } from "./contextMenu.js"
|
import { contextMenuStore } from "./contextMenu"
|
||||||
import { snippets } from "./snippets"
|
import { snippets } from "./snippets"
|
||||||
import {
|
import {
|
||||||
screenComponentsList,
|
screenComponentsList,
|
||||||
|
|
|
@ -5,4 +5,15 @@ export type BindingCompletion = (context: CompletionContext) => {
|
||||||
options: Completion[]
|
options: Completion[]
|
||||||
} | null
|
} | null
|
||||||
|
|
||||||
export type BindingCompletionOption = Completion
|
export interface BindingCompletionOption extends Completion {
|
||||||
|
args?: any[]
|
||||||
|
requiresBlock?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CodeValidator = Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
arguments?: any[]
|
||||||
|
requiresBlock?: boolean
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
|
@ -14,5 +14,6 @@
|
||||||
"assets/*": ["assets/*"],
|
"assets/*": ["assets/*"],
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"exclude": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -3089,7 +3089,21 @@
|
||||||
{
|
{
|
||||||
"type": "tableConditions",
|
"type": "tableConditions",
|
||||||
"label": "Conditions",
|
"label": "Conditions",
|
||||||
"key": "conditions"
|
"key": "conditions",
|
||||||
|
"contextAccess": {
|
||||||
|
"global": true,
|
||||||
|
"self": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Format",
|
||||||
|
"key": "format",
|
||||||
|
"info": "Changing format will display values as text",
|
||||||
|
"contextAccess": {
|
||||||
|
"global": false,
|
||||||
|
"self": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -7686,7 +7700,8 @@
|
||||||
{
|
{
|
||||||
"type": "columns/grid",
|
"type": "columns/grid",
|
||||||
"key": "columns",
|
"key": "columns",
|
||||||
"resetOn": "table"
|
"resetOn": "table",
|
||||||
|
"nested": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onDestroy, onMount, setContext } from "svelte"
|
import { getContext, onDestroy, onMount, setContext } from "svelte"
|
||||||
import { builderStore } from "@/stores/builder.js"
|
import { builderStore } from "@/stores/builder"
|
||||||
import { blockStore } from "@/stores/blocks"
|
import { blockStore } from "@/stores/blocks"
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onDestroy } from "svelte"
|
import { getContext, onDestroy } from "svelte"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { builderStore } from "../stores/builder.js"
|
import { builderStore } from "../stores/builder"
|
||||||
import Component from "@/components/Component.svelte"
|
import Component from "@/components/Component.svelte"
|
||||||
|
|
||||||
export let type
|
export let type
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import { get, derived, readable } from "svelte/store"
|
import { get, derived, readable } from "svelte/store"
|
||||||
import { featuresStore } from "@/stores"
|
import { featuresStore } from "@/stores"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import { Grid } from "@budibase/frontend-core"
|
||||||
// import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
|
||||||
// table is actually any datasource, but called table for legacy compatibility
|
// table is actually any datasource, but called table for legacy compatibility
|
||||||
export let table
|
export let table
|
||||||
|
@ -47,8 +47,8 @@
|
||||||
$: currentTheme = $context?.device?.theme
|
$: currentTheme = $context?.device?.theme
|
||||||
$: darkMode = !currentTheme?.includes("light")
|
$: darkMode = !currentTheme?.includes("light")
|
||||||
$: parsedColumns = getParsedColumns(columns)
|
$: parsedColumns = getParsedColumns(columns)
|
||||||
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
|
||||||
$: enrichedButtons = enrichButtons(buttons)
|
$: enrichedButtons = enrichButtons(buttons)
|
||||||
|
$: schemaOverrides = getSchemaOverrides(parsedColumns, $context)
|
||||||
$: selectedRows = deriveSelectedRows(gridContext)
|
$: selectedRows = deriveSelectedRows(gridContext)
|
||||||
$: styles = patchStyles($component.styles, minHeight)
|
$: styles = patchStyles($component.styles, minHeight)
|
||||||
$: data = { selectedRows: $selectedRows }
|
$: data = { selectedRows: $selectedRows }
|
||||||
|
@ -97,15 +97,19 @@
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSchemaOverrides = columns => {
|
const getSchemaOverrides = (columns, context) => {
|
||||||
let overrides = {}
|
let overrides = {}
|
||||||
columns.forEach((column, idx) => {
|
columns.forEach((column, idx) => {
|
||||||
overrides[column.field] = {
|
overrides[column.field] = {
|
||||||
displayName: column.label,
|
displayName: column.label,
|
||||||
order: idx,
|
order: idx,
|
||||||
conditions: column.conditions,
|
|
||||||
visible: !!column.active,
|
visible: !!column.active,
|
||||||
// format: createFormatter(column),
|
conditions: enrichConditions(column.conditions, context),
|
||||||
|
format: createFormatter(column),
|
||||||
|
|
||||||
|
// Small hack to ensure we react to all changes, as our
|
||||||
|
// memoization cannot compare differences in functions
|
||||||
|
rand: column.conditions?.length ? Math.random() : null,
|
||||||
}
|
}
|
||||||
if (column.width) {
|
if (column.width) {
|
||||||
overrides[column.field].width = column.width
|
overrides[column.field].width = column.width
|
||||||
|
@ -114,12 +118,24 @@
|
||||||
return overrides
|
return overrides
|
||||||
}
|
}
|
||||||
|
|
||||||
// const createFormatter = column => {
|
const enrichConditions = (conditions, context) => {
|
||||||
// if (typeof column.format !== "string" || !column.format.trim().length) {
|
return conditions?.map(condition => {
|
||||||
// return null
|
return {
|
||||||
// }
|
...condition,
|
||||||
// return row => processStringSync(column.format, { [id]: row })
|
referenceValue: processStringSync(
|
||||||
// }
|
condition.referenceValue || "",
|
||||||
|
context
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createFormatter = column => {
|
||||||
|
if (typeof column.format !== "string" || !column.format.trim().length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return row => processStringSync(column.format, { [id]: row })
|
||||||
|
}
|
||||||
|
|
||||||
const enrichButtons = buttons => {
|
const enrichButtons = buttons => {
|
||||||
if (!buttons?.length) {
|
if (!buttons?.length) {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
import { CoreDropzone, ProgressCircle, Helpers } from "@budibase/bbui"
|
import { CoreDropzone, ProgressCircle, Helpers } from "@budibase/bbui"
|
||||||
import { getContext, onMount, onDestroy } from "svelte"
|
import { getContext, onMount, onDestroy } from "svelte"
|
||||||
import { builderStore } from "@/stores/builder.js"
|
import { builderStore } from "@/stores/builder"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
|
||||||
export let datasourceId
|
export let datasourceId
|
||||||
|
|
|
@ -19,7 +19,6 @@ import type { ActionTypes } from "@/constants"
|
||||||
import { Readable } from "svelte/store"
|
import { Readable } from "svelte/store"
|
||||||
import {
|
import {
|
||||||
Screen,
|
Screen,
|
||||||
Layout,
|
|
||||||
Theme,
|
Theme,
|
||||||
AppCustomTheme,
|
AppCustomTheme,
|
||||||
PreviewDevice,
|
PreviewDevice,
|
||||||
|
@ -48,7 +47,6 @@ declare global {
|
||||||
// Data from builder
|
// Data from builder
|
||||||
"##BUDIBASE_APP_ID##"?: string
|
"##BUDIBASE_APP_ID##"?: string
|
||||||
"##BUDIBASE_IN_BUILDER##"?: true
|
"##BUDIBASE_IN_BUILDER##"?: true
|
||||||
"##BUDIBASE_PREVIEW_LAYOUT##"?: Layout
|
|
||||||
"##BUDIBASE_PREVIEW_SCREEN##"?: Screen
|
"##BUDIBASE_PREVIEW_SCREEN##"?: Screen
|
||||||
"##BUDIBASE_SELECTED_COMPONENT_ID##"?: string
|
"##BUDIBASE_SELECTED_COMPONENT_ID##"?: string
|
||||||
"##BUDIBASE_PREVIEW_ID##"?: number
|
"##BUDIBASE_PREVIEW_ID##"?: number
|
||||||
|
@ -59,13 +57,8 @@ declare global {
|
||||||
"##BUDIBASE_PREVIEW_NAVIGATION##"?: AppNavigation
|
"##BUDIBASE_PREVIEW_NAVIGATION##"?: AppNavigation
|
||||||
"##BUDIBASE_HIDDEN_COMPONENT_IDS##"?: string[]
|
"##BUDIBASE_HIDDEN_COMPONENT_IDS##"?: string[]
|
||||||
"##BUDIBASE_USED_PLUGINS##"?: Plugin[]
|
"##BUDIBASE_USED_PLUGINS##"?: Plugin[]
|
||||||
"##BUDIBASE_LOCATION##"?: {
|
|
||||||
protocol: string
|
|
||||||
hostname: string
|
|
||||||
port: string
|
|
||||||
}
|
|
||||||
"##BUDIBASE_SNIPPETS##"?: Snippet[]
|
"##BUDIBASE_SNIPPETS##"?: Snippet[]
|
||||||
"##BUDIBASE_COMPONENT_ERRORS##"?: Record<string, UIComponentError>[]
|
"##BUDIBASE_COMPONENT_ERRORS##"?: Record<string, UIComponentError[]>
|
||||||
"##BUDIBASE_CUSTOM_COMPONENTS##"?: CustomComponent[]
|
"##BUDIBASE_CUSTOM_COMPONENTS##"?: CustomComponent[]
|
||||||
|
|
||||||
// Other flags
|
// Other flags
|
||||||
|
@ -115,7 +108,6 @@ const loadBudibase = async () => {
|
||||||
builderStore.set({
|
builderStore.set({
|
||||||
...get(builderStore),
|
...get(builderStore),
|
||||||
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
|
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
|
||||||
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
|
|
||||||
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
||||||
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
|
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
|
||||||
previewId: window["##BUDIBASE_PREVIEW_ID##"],
|
previewId: window["##BUDIBASE_PREVIEW_ID##"],
|
||||||
|
@ -125,7 +117,6 @@ const loadBudibase = async () => {
|
||||||
navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"],
|
navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"],
|
||||||
hiddenComponentIds: window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"],
|
hiddenComponentIds: window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"],
|
||||||
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
|
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
|
||||||
location: window["##BUDIBASE_LOCATION##"],
|
|
||||||
snippets: window["##BUDIBASE_SNIPPETS##"],
|
snippets: window["##BUDIBASE_SNIPPETS##"],
|
||||||
componentErrors: window["##BUDIBASE_COMPONENT_ERRORS##"],
|
componentErrors: window["##BUDIBASE_COMPONENT_ERRORS##"],
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,9 +2,39 @@ import { writable, get } from "svelte/store"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { devToolsStore } from "./devTools.js"
|
import { devToolsStore } from "./devTools.js"
|
||||||
import { eventStore } from "./events.js"
|
import { eventStore } from "./events.js"
|
||||||
|
import {
|
||||||
|
ComponentDefinition,
|
||||||
|
DropPosition,
|
||||||
|
PingSource,
|
||||||
|
PreviewDevice,
|
||||||
|
Screen,
|
||||||
|
Theme,
|
||||||
|
AppCustomTheme,
|
||||||
|
AppNavigation,
|
||||||
|
Plugin,
|
||||||
|
Snippet,
|
||||||
|
UIComponentError,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
interface BuilderStore {
|
||||||
|
inBuilder: boolean
|
||||||
|
screen?: Screen | null
|
||||||
|
selectedComponentId?: string | null
|
||||||
|
editMode: boolean
|
||||||
|
previewId?: number | null
|
||||||
|
theme?: Theme | null
|
||||||
|
customTheme?: AppCustomTheme | null
|
||||||
|
previewDevice?: PreviewDevice
|
||||||
|
navigation?: AppNavigation | null
|
||||||
|
hiddenComponentIds?: string[]
|
||||||
|
usedPlugins?: Plugin[] | null
|
||||||
|
metadata: { componentId: string; step: number } | null
|
||||||
|
snippets?: Snippet[] | null
|
||||||
|
componentErrors?: Record<string, UIComponentError[]>
|
||||||
|
}
|
||||||
|
|
||||||
const createBuilderStore = () => {
|
const createBuilderStore = () => {
|
||||||
const initialState = {
|
const initialState: BuilderStore = {
|
||||||
inBuilder: false,
|
inBuilder: false,
|
||||||
screen: null,
|
screen: null,
|
||||||
selectedComponentId: null,
|
selectedComponentId: null,
|
||||||
|
@ -16,17 +46,13 @@ const createBuilderStore = () => {
|
||||||
navigation: null,
|
navigation: null,
|
||||||
hiddenComponentIds: [],
|
hiddenComponentIds: [],
|
||||||
usedPlugins: null,
|
usedPlugins: null,
|
||||||
eventResolvers: {},
|
|
||||||
metadata: null,
|
metadata: null,
|
||||||
snippets: null,
|
snippets: null,
|
||||||
componentErrors: {},
|
componentErrors: {},
|
||||||
|
|
||||||
// Legacy - allow the builder to specify a layout
|
|
||||||
layout: null,
|
|
||||||
}
|
}
|
||||||
const store = writable(initialState)
|
const store = writable(initialState)
|
||||||
const actions = {
|
const actions = {
|
||||||
selectComponent: id => {
|
selectComponent: (id: string) => {
|
||||||
if (id === get(store).selectedComponentId) {
|
if (id === get(store).selectedComponentId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -38,46 +64,59 @@ const createBuilderStore = () => {
|
||||||
devToolsStore.actions.setAllowSelection(false)
|
devToolsStore.actions.setAllowSelection(false)
|
||||||
eventStore.actions.dispatchEvent("select-component", { id })
|
eventStore.actions.dispatchEvent("select-component", { id })
|
||||||
},
|
},
|
||||||
updateProp: (prop, value) => {
|
updateProp: (prop: string, value: any) => {
|
||||||
eventStore.actions.dispatchEvent("update-prop", { prop, value })
|
eventStore.actions.dispatchEvent("update-prop", { prop, value })
|
||||||
},
|
},
|
||||||
updateStyles: async (styles, id) => {
|
updateStyles: async (styles: Record<string, any>, id: string) => {
|
||||||
await eventStore.actions.dispatchEvent("update-styles", {
|
await eventStore.actions.dispatchEvent("update-styles", {
|
||||||
styles,
|
styles,
|
||||||
id,
|
id,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
keyDown: (key, ctrlKey) => {
|
keyDown: (key: string, ctrlKey: boolean) => {
|
||||||
eventStore.actions.dispatchEvent("key-down", { key, ctrlKey })
|
eventStore.actions.dispatchEvent("key-down", { key, ctrlKey })
|
||||||
},
|
},
|
||||||
duplicateComponent: (id, mode = "below", selectComponent = true) => {
|
duplicateComponent: (
|
||||||
|
id: string,
|
||||||
|
mode = DropPosition.BELOW,
|
||||||
|
selectComponent = true
|
||||||
|
) => {
|
||||||
eventStore.actions.dispatchEvent("duplicate-component", {
|
eventStore.actions.dispatchEvent("duplicate-component", {
|
||||||
id,
|
id,
|
||||||
mode,
|
mode,
|
||||||
selectComponent,
|
selectComponent,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteComponent: id => {
|
deleteComponent: (id: string) => {
|
||||||
eventStore.actions.dispatchEvent("delete-component", { id })
|
eventStore.actions.dispatchEvent("delete-component", { id })
|
||||||
},
|
},
|
||||||
notifyLoaded: () => {
|
notifyLoaded: () => {
|
||||||
eventStore.actions.dispatchEvent("preview-loaded")
|
eventStore.actions.dispatchEvent("preview-loaded")
|
||||||
},
|
},
|
||||||
analyticsPing: async ({ embedded }) => {
|
analyticsPing: async ({ embedded }: { embedded: boolean }) => {
|
||||||
try {
|
try {
|
||||||
await API.analyticsPing({ source: "app", embedded })
|
await API.analyticsPing({ source: PingSource.APP, embedded })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
moveComponent: async (componentId, destinationComponentId, mode) => {
|
moveComponent: async (
|
||||||
|
componentId: string,
|
||||||
|
destinationComponentId: string,
|
||||||
|
mode: DropPosition
|
||||||
|
) => {
|
||||||
await eventStore.actions.dispatchEvent("move-component", {
|
await eventStore.actions.dispatchEvent("move-component", {
|
||||||
componentId,
|
componentId,
|
||||||
destinationComponentId,
|
destinationComponentId,
|
||||||
mode,
|
mode,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
dropNewComponent: (component, parent, index, props) => {
|
dropNewComponent: (
|
||||||
|
component: string,
|
||||||
|
parent: string,
|
||||||
|
index: number,
|
||||||
|
props: Record<string, any>
|
||||||
|
) => {
|
||||||
eventStore.actions.dispatchEvent("drop-new-component", {
|
eventStore.actions.dispatchEvent("drop-new-component", {
|
||||||
component,
|
component,
|
||||||
parent,
|
parent,
|
||||||
|
@ -85,7 +124,7 @@ const createBuilderStore = () => {
|
||||||
props,
|
props,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setEditMode: enabled => {
|
setEditMode: (enabled: boolean) => {
|
||||||
if (enabled === get(store).editMode) {
|
if (enabled === get(store).editMode) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -94,18 +133,18 @@ const createBuilderStore = () => {
|
||||||
requestAddComponent: () => {
|
requestAddComponent: () => {
|
||||||
eventStore.actions.dispatchEvent("request-add-component")
|
eventStore.actions.dispatchEvent("request-add-component")
|
||||||
},
|
},
|
||||||
highlightSetting: setting => {
|
highlightSetting: (setting: string) => {
|
||||||
eventStore.actions.dispatchEvent("highlight-setting", { setting })
|
eventStore.actions.dispatchEvent("highlight-setting", { setting })
|
||||||
},
|
},
|
||||||
ejectBlock: (id, definition) => {
|
ejectBlock: (id: string, definition: ComponentDefinition) => {
|
||||||
eventStore.actions.dispatchEvent("eject-block", { id, definition })
|
eventStore.actions.dispatchEvent("eject-block", { id, definition })
|
||||||
},
|
},
|
||||||
updateUsedPlugin: (name, hash) => {
|
updateUsedPlugin: (name: string, hash: string) => {
|
||||||
// Check if we used this plugin
|
// Check if we used this plugin
|
||||||
const used = get(store)?.usedPlugins?.find(x => x.name === name)
|
const used = get(store)?.usedPlugins?.find(x => x.name === name)
|
||||||
if (used) {
|
if (used) {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.usedPlugins = state.usedPlugins.filter(x => x.name !== name)
|
state.usedPlugins = state.usedPlugins!.filter(x => x.name !== name)
|
||||||
state.usedPlugins.push({
|
state.usedPlugins.push({
|
||||||
...used,
|
...used,
|
||||||
hash,
|
hash,
|
||||||
|
@ -117,13 +156,13 @@ const createBuilderStore = () => {
|
||||||
// Notify the builder so we can reload component definitions
|
// Notify the builder so we can reload component definitions
|
||||||
eventStore.actions.dispatchEvent("reload-plugin")
|
eventStore.actions.dispatchEvent("reload-plugin")
|
||||||
},
|
},
|
||||||
addParentComponent: (componentId, parentType) => {
|
addParentComponent: (componentId: string, parentType: string) => {
|
||||||
eventStore.actions.dispatchEvent("add-parent-component", {
|
eventStore.actions.dispatchEvent("add-parent-component", {
|
||||||
componentId,
|
componentId,
|
||||||
parentType,
|
parentType,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setMetadata: metadata => {
|
setMetadata: (metadata: { componentId: string; step: number }) => {
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
metadata,
|
metadata,
|
||||||
|
@ -132,7 +171,7 @@ const createBuilderStore = () => {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...store,
|
...store,
|
||||||
set: state => store.set({ ...initialState, ...state }),
|
set: (state: BuilderStore) => store.set({ ...initialState, ...state }),
|
||||||
actions,
|
actions,
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import { appStore } from "../app.js"
|
import { appStore } from "../app.js"
|
||||||
import { builderStore } from "../builder.js"
|
import { builderStore } from "../builder"
|
||||||
|
|
||||||
export const devToolsEnabled = derived(
|
export const devToolsEnabled = derived(
|
||||||
[appStore, builderStore],
|
[appStore, builderStore],
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { appStore } from "../app.js"
|
import { appStore } from "../app.js"
|
||||||
import { builderStore } from "../builder.js"
|
import { builderStore } from "../builder"
|
||||||
import { derivedMemo } from "@budibase/frontend-core"
|
import { derivedMemo } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export const snippets = derivedMemo(
|
export const snippets = derivedMemo(
|
||||||
|
|
|
@ -36,11 +36,6 @@ const createScreenStore = () => {
|
||||||
activeScreen = Helpers.cloneDeep($builderStore.screen)
|
activeScreen = Helpers.cloneDeep($builderStore.screen)
|
||||||
screens = [activeScreen]
|
screens = [activeScreen]
|
||||||
|
|
||||||
// Legacy - allow the builder to specify a layout
|
|
||||||
if ($builderStore.layout) {
|
|
||||||
activeLayout = $builderStore.layout
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach meta
|
// Attach meta
|
||||||
const errors = $builderStore.componentErrors || {}
|
const errors = $builderStore.componentErrors || {}
|
||||||
const attachComponentMeta = component => {
|
const attachComponentMeta = component => {
|
||||||
|
|
|
@ -31,7 +31,7 @@ export const deriveStores = (context: StoreContext): ConditionDerivedStore => {
|
||||||
// Derive and memoize the cell conditions present in our columns so that we
|
// Derive and memoize the cell conditions present in our columns so that we
|
||||||
// only recompute condition metadata when absolutely necessary
|
// only recompute condition metadata when absolutely necessary
|
||||||
const conditions = derivedMemo(columns, $columns => {
|
const conditions = derivedMemo(columns, $columns => {
|
||||||
let newConditions = []
|
let newConditions: UICondition[] = []
|
||||||
for (let column of $columns) {
|
for (let column of $columns) {
|
||||||
for (let condition of column.conditions || []) {
|
for (let condition of column.conditions || []) {
|
||||||
newConditions.push({
|
newConditions.push({
|
||||||
|
|
|
@ -80,7 +80,6 @@
|
||||||
// Set some flags so the app knows we're in the builder
|
// Set some flags so the app knows we're in the builder
|
||||||
window["##BUDIBASE_IN_BUILDER##"] = true
|
window["##BUDIBASE_IN_BUILDER##"] = true
|
||||||
window["##BUDIBASE_APP_ID##"] = appId
|
window["##BUDIBASE_APP_ID##"] = appId
|
||||||
window["##BUDIBASE_PREVIEW_LAYOUT##"] = layout
|
|
||||||
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
|
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
|
||||||
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
|
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
|
||||||
window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
|
window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
|
||||||
|
@ -90,7 +89,6 @@
|
||||||
window["##BUDIBASE_PREVIEW_NAVIGATION##"] = navigation
|
window["##BUDIBASE_PREVIEW_NAVIGATION##"] = navigation
|
||||||
window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"] = hiddenComponentIds
|
window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"] = hiddenComponentIds
|
||||||
window["##BUDIBASE_USED_PLUGINS##"] = usedPlugins
|
window["##BUDIBASE_USED_PLUGINS##"] = usedPlugins
|
||||||
window["##BUDIBASE_LOCATION##"] = location
|
|
||||||
window["##BUDIBASE_SNIPPETS##"] = snippets
|
window["##BUDIBASE_SNIPPETS##"] = snippets
|
||||||
window['##BUDIBASE_COMPONENT_ERRORS##'] = componentErrors
|
window['##BUDIBASE_COMPONENT_ERRORS##'] = componentErrors
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,14 @@ import Router from "@koa/router"
|
||||||
import * as controller from "../controllers/backup"
|
import * as controller from "../controllers/backup"
|
||||||
import authorized from "../../middleware/authorized"
|
import authorized from "../../middleware/authorized"
|
||||||
import { permissions } from "@budibase/backend-core"
|
import { permissions } from "@budibase/backend-core"
|
||||||
|
import ensureTenantAppOwnership from "../../middleware/ensureTenantAppOwnership"
|
||||||
|
|
||||||
const router: Router = new Router()
|
const router: Router = new Router()
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/api/backups/export",
|
"/api/backups/export",
|
||||||
authorized(permissions.BUILDER),
|
authorized(permissions.BUILDER),
|
||||||
|
ensureTenantAppOwnership,
|
||||||
controller.exportAppDump
|
controller.exportAppDump
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { tenancy, utils, context } from "@budibase/backend-core"
|
||||||
|
import { UserCtx } from "@budibase/types"
|
||||||
|
|
||||||
|
async function ensureTenantAppOwnership(ctx: UserCtx, next: any) {
|
||||||
|
const appId = await utils.getAppIdFromCtx(ctx)
|
||||||
|
if (!appId) {
|
||||||
|
ctx.throw(400, "appId must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
const appTenantId = context.getTenantIDFromAppID(appId)
|
||||||
|
const tenantId = tenancy.getTenantId()
|
||||||
|
|
||||||
|
if (appTenantId !== tenantId) {
|
||||||
|
ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
await next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ensureTenantAppOwnership
|
|
@ -0,0 +1,75 @@
|
||||||
|
import ensureTenantAppOwnership from "../ensureTenantAppOwnership"
|
||||||
|
import { tenancy, utils } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
jest.mock("@budibase/backend-core", () => ({
|
||||||
|
...jest.requireActual("@budibase/backend-core"),
|
||||||
|
tenancy: {
|
||||||
|
getTenantId: jest.fn(),
|
||||||
|
},
|
||||||
|
utils: {
|
||||||
|
getAppIdFromCtx: jest.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
class TestConfiguration {
|
||||||
|
constructor(appId = "tenant_1") {
|
||||||
|
this.next = jest.fn()
|
||||||
|
this.throw = jest.fn()
|
||||||
|
this.middleware = ensureTenantAppOwnership
|
||||||
|
|
||||||
|
this.ctx = {
|
||||||
|
next: this.next,
|
||||||
|
throw: this.throw,
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.getAppIdFromCtx.mockResolvedValue(appId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeMiddleware() {
|
||||||
|
return this.middleware(this.ctx, this.next)
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach() {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Ensure Tenant Ownership Middleware", () => {
|
||||||
|
let config
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
config = new TestConfiguration()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.afterEach()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls next() when appId matches tenant ID", async () => {
|
||||||
|
tenancy.getTenantId.mockReturnValue("tenant_1")
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(utils.getAppIdFromCtx).toHaveBeenCalledWith(config.ctx)
|
||||||
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws when tenant appId does not match tenant ID", async () => {
|
||||||
|
const appId = "app_dev_tenant3_fce449c4d75b4e4a9c7a6980d82a3e22"
|
||||||
|
utils.getAppIdFromCtx.mockResolvedValue(appId)
|
||||||
|
tenancy.getTenantId.mockReturnValue("tenant_2")
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(utils.getAppIdFromCtx).toHaveBeenCalledWith(config.ctx)
|
||||||
|
expect(config.throw).toHaveBeenCalledWith(403, "Unauthorized")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws 400 when appId is missing", async () => {
|
||||||
|
utils.getAppIdFromCtx.mockResolvedValue(null)
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.throw).toHaveBeenCalledWith(400, "appId must be provided")
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,4 +1,6 @@
|
||||||
export interface Helper {
|
export interface Helper {
|
||||||
example: string
|
example: string
|
||||||
description: string
|
description: string
|
||||||
|
args: any[]
|
||||||
|
requiresBlock?: boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,11 @@ export interface ComponentSetting {
|
||||||
selectAllFields?: boolean
|
selectAllFields?: boolean
|
||||||
resetOn?: string | string[]
|
resetOn?: string | string[]
|
||||||
settings?: ComponentSetting[]
|
settings?: ComponentSetting[]
|
||||||
|
nested?: boolean
|
||||||
dependsOn?: DependsOnComponentSetting
|
dependsOn?: DependsOnComponentSetting
|
||||||
sectionDependsOn?: DependsOnComponentSetting
|
sectionDependsOn?: DependsOnComponentSetting
|
||||||
|
contextAccess?: {
|
||||||
|
global: boolean
|
||||||
|
self: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
import { CalculationType, FieldSchema, FieldType, UIRow } from "@budibase/types"
|
import {
|
||||||
|
CalculationType,
|
||||||
|
FieldSchema,
|
||||||
|
FieldType,
|
||||||
|
UICondition,
|
||||||
|
UIRow,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
export type UIColumn = FieldSchema & {
|
export type UIColumn = FieldSchema & {
|
||||||
label: string
|
label: string
|
||||||
readonly: boolean
|
readonly: boolean
|
||||||
conditions: any
|
conditions?: UICondition[]
|
||||||
format?: (row: UIRow) => any
|
format?: (row: UIRow) => any
|
||||||
related?: {
|
related?: {
|
||||||
field: string
|
field: string
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { FieldType, SearchFilter } from "@budibase/types"
|
||||||
export interface UICondition {
|
export interface UICondition {
|
||||||
column: string
|
column: string
|
||||||
type: FieldType
|
type: FieldType
|
||||||
referenceValue: string
|
referenceValue: any
|
||||||
operator: SearchFilter["operator"]
|
operator: SearchFilter["operator"]
|
||||||
metadataKey: string
|
metadataKey: string
|
||||||
metadataValue: string
|
metadataValue: string
|
||||||
|
|
|
@ -1,2 +1,8 @@
|
||||||
// type purely to capture structures that the type is unknown, but maybe known later
|
// type purely to capture structures that the type is unknown, but maybe known later
|
||||||
export type UIObject = Record<string, any>
|
export type UIObject = Record<string, any>
|
||||||
|
|
||||||
|
export const enum DropPosition {
|
||||||
|
ABOVE = "above",
|
||||||
|
BELOW = "below",
|
||||||
|
INSIDE = "inside",
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue