Merge branch 'master' into BUDI-8986/convert-screen-store
This commit is contained in:
commit
588d4b7485
|
@ -41,11 +41,12 @@ module.exports = {
|
||||||
if (
|
if (
|
||||||
/^@budibase\/[^/]+\/.*$/.test(importPath) &&
|
/^@budibase\/[^/]+\/.*$/.test(importPath) &&
|
||||||
importPath !== "@budibase/backend-core/tests" &&
|
importPath !== "@budibase/backend-core/tests" &&
|
||||||
importPath !== "@budibase/string-templates/test/utils"
|
importPath !== "@budibase/string-templates/test/utils" &&
|
||||||
|
importPath !== "@budibase/client/manifest.json"
|
||||||
) {
|
) {
|
||||||
context.report({
|
context.report({
|
||||||
node,
|
node,
|
||||||
message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests and @budibase/string-templates/test/utils.`,
|
message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests, @budibase/string-templates/test/utils and @budibase/client/manifest.json.`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "3.2.46",
|
"version": "3.3.1",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"concurrency": 20,
|
"concurrency": 20,
|
||||||
"command": {
|
"command": {
|
||||||
|
|
|
@ -45,6 +45,11 @@
|
||||||
--purple: #806fde;
|
--purple: #806fde;
|
||||||
--purple-dark: #130080;
|
--purple-dark: #130080;
|
||||||
|
|
||||||
|
--error-bg: rgba(226, 109, 105, 0.3);
|
||||||
|
--warning-bg: rgba(255, 210, 106, 0.3);
|
||||||
|
--error-content: rgba(226, 109, 105, 0.6);
|
||||||
|
--warning-content: rgba(255, 210, 106, 0.6);
|
||||||
|
|
||||||
--rounded-small: 4px;
|
--rounded-small: 4px;
|
||||||
--rounded-medium: 8px;
|
--rounded-medium: 8px;
|
||||||
--rounded-large: 16px;
|
--rounded-large: 16px;
|
||||||
|
|
|
@ -293,7 +293,7 @@
|
||||||
type: RowSelector,
|
type: RowSelector,
|
||||||
props: {
|
props: {
|
||||||
row: inputData["oldRow"] || {
|
row: inputData["oldRow"] || {
|
||||||
tableId: inputData["row"].tableId,
|
tableId: inputData["row"]?.tableId,
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
fields: inputData["meta"]?.oldFields || {},
|
fields: inputData["meta"]?.oldFields || {},
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
decodeJSBinding,
|
decodeJSBinding,
|
||||||
encodeJSBinding,
|
encodeJSBinding,
|
||||||
processObjectSync,
|
processObjectSync,
|
||||||
processStringSync,
|
processStringWithLogsSync,
|
||||||
} from "@budibase/string-templates"
|
} from "@budibase/string-templates"
|
||||||
import { readableToRuntimeBinding } from "@/dataBinding"
|
import { readableToRuntimeBinding } from "@/dataBinding"
|
||||||
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
||||||
|
@ -41,6 +41,7 @@
|
||||||
InsertAtPositionFn,
|
InsertAtPositionFn,
|
||||||
JSONValue,
|
JSONValue,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import type { Log } from "@budibase/string-templates"
|
||||||
import type { CompletionContext } from "@codemirror/autocomplete"
|
import type { CompletionContext } from "@codemirror/autocomplete"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -66,6 +67,7 @@
|
||||||
let insertAtPos: InsertAtPositionFn | undefined
|
let insertAtPos: InsertAtPositionFn | undefined
|
||||||
let targetMode: BindingMode | null = null
|
let targetMode: BindingMode | null = null
|
||||||
let expressionResult: string | undefined
|
let expressionResult: string | undefined
|
||||||
|
let expressionLogs: Log[] | undefined
|
||||||
let expressionError: string | undefined
|
let expressionError: string | undefined
|
||||||
let evaluating = false
|
let evaluating = false
|
||||||
|
|
||||||
|
@ -157,7 +159,7 @@
|
||||||
(expression: string | null, context: any, snippets: Snippet[]) => {
|
(expression: string | null, context: any, snippets: Snippet[]) => {
|
||||||
try {
|
try {
|
||||||
expressionError = undefined
|
expressionError = undefined
|
||||||
expressionResult = processStringSync(
|
const output = processStringWithLogsSync(
|
||||||
expression || "",
|
expression || "",
|
||||||
{
|
{
|
||||||
...context,
|
...context,
|
||||||
|
@ -167,6 +169,8 @@
|
||||||
noThrow: false,
|
noThrow: false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
expressionResult = output.result
|
||||||
|
expressionLogs = output.logs
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
expressionResult = undefined
|
expressionResult = undefined
|
||||||
expressionError = err
|
expressionError = err
|
||||||
|
@ -421,6 +425,7 @@
|
||||||
<EvaluationSidePanel
|
<EvaluationSidePanel
|
||||||
{expressionResult}
|
{expressionResult}
|
||||||
{expressionError}
|
{expressionError}
|
||||||
|
{expressionLogs}
|
||||||
{evaluating}
|
{evaluating}
|
||||||
expression={editorValue ? editorValue : ""}
|
expression={editorValue ? editorValue : ""}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,11 +4,13 @@
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import { UserScriptError } from "@budibase/string-templates"
|
import { UserScriptError } from "@budibase/string-templates"
|
||||||
|
import type { Log } from "@budibase/string-templates"
|
||||||
import type { JSONValue } from "@budibase/types"
|
import type { JSONValue } from "@budibase/types"
|
||||||
|
|
||||||
// this can be essentially any primitive response from the JS function
|
// this can be essentially any primitive response from the JS function
|
||||||
export let expressionResult: JSONValue | undefined = undefined
|
export let expressionResult: JSONValue | undefined = undefined
|
||||||
export let expressionError: string | undefined = undefined
|
export let expressionError: string | undefined = undefined
|
||||||
|
export let expressionLogs: Log[] = []
|
||||||
export let evaluating = false
|
export let evaluating = false
|
||||||
export let expression: string | null = null
|
export let expression: string | null = null
|
||||||
|
|
||||||
|
@ -16,6 +18,11 @@
|
||||||
$: empty = expression == null || expression?.trim() === ""
|
$: empty = expression == null || expression?.trim() === ""
|
||||||
$: success = !error && !empty
|
$: success = !error && !empty
|
||||||
$: highlightedResult = highlight(expressionResult)
|
$: highlightedResult = highlight(expressionResult)
|
||||||
|
$: highlightedLogs = expressionLogs.map(l => ({
|
||||||
|
log: highlight(l.log.join(", ")),
|
||||||
|
line: l.line,
|
||||||
|
type: l.type,
|
||||||
|
}))
|
||||||
|
|
||||||
const formatError = (err: any) => {
|
const formatError = (err: any) => {
|
||||||
if (err.code === UserScriptError.code) {
|
if (err.code === UserScriptError.code) {
|
||||||
|
@ -25,14 +32,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// json can be any primitive type
|
// json can be any primitive type
|
||||||
const highlight = (json?: any | null) => {
|
const highlight = (json?: JSONValue | null) => {
|
||||||
if (json == null) {
|
if (json == null) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to parse and then stringify, in case this is valid result
|
// Attempt to parse and then stringify, in case this is valid result
|
||||||
try {
|
try {
|
||||||
json = JSON.stringify(JSON.parse(json), null, 2)
|
json = JSON.stringify(JSON.parse(json as any), null, 2)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// couldn't parse/stringify, just treat it as the raw input
|
// couldn't parse/stringify, just treat it as the raw input
|
||||||
}
|
}
|
||||||
|
@ -61,7 +68,7 @@
|
||||||
<div class="header" class:success class:error>
|
<div class="header" class:success class:error>
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
{#if error}
|
{#if error}
|
||||||
<Icon name="Alert" color="var(--spectrum-global-color-red-600)" />
|
<Icon name="Alert" color="var(--error-content)" />
|
||||||
<div>Error</div>
|
<div>Error</div>
|
||||||
{#if evaluating}
|
{#if evaluating}
|
||||||
<div transition:fade|local={{ duration: 130 }}>
|
<div transition:fade|local={{ duration: 130 }}>
|
||||||
|
@ -90,8 +97,36 @@
|
||||||
{:else if error}
|
{:else if error}
|
||||||
{formatError(expressionError)}
|
{formatError(expressionError)}
|
||||||
{:else}
|
{:else}
|
||||||
|
<div class="output-lines">
|
||||||
|
{#each highlightedLogs as logLine}
|
||||||
|
<div
|
||||||
|
class="line"
|
||||||
|
class:error-log={logLine.type === "error"}
|
||||||
|
class:warn-log={logLine.type === "warn"}
|
||||||
|
>
|
||||||
|
<div class="icon-log">
|
||||||
|
{#if logLine.type === "error"}
|
||||||
|
<Icon
|
||||||
|
size="XS"
|
||||||
|
name="CloseCircle"
|
||||||
|
color="var(--error-content)"
|
||||||
|
/>
|
||||||
|
{:else if logLine.type === "warn"}
|
||||||
|
<Icon size="XS" name="Alert" color="var(--warning-content)" />
|
||||||
|
{/if}
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||||
|
<span>{@html logLine.log}</span>
|
||||||
|
</div>
|
||||||
|
{#if logLine.line}
|
||||||
|
<span style="color: var(--blue)">:{logLine.line}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="line">
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||||
{@html highlightedResult}
|
{@html highlightedResult}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -130,20 +165,37 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 10%;
|
|
||||||
}
|
}
|
||||||
.header.error::before {
|
.header.error::before {
|
||||||
background: var(--spectrum-global-color-red-400);
|
background: var(--error-bg);
|
||||||
}
|
}
|
||||||
.body {
|
.body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding: var(--spacing-m) var(--spacing-l);
|
padding: var(--spacing-m) var(--spacing-l);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
overflow-y: scroll;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
white-space: pre-wrap;
|
white-space: pre-line;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
.output-lines {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: end;
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.icon-log {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { datasources } from "@/stores/builder"
|
||||||
import { Divider, Heading } from "@budibase/bbui"
|
import { Divider, Heading } from "@budibase/bbui"
|
||||||
|
|
||||||
export let dividerState
|
export let dividerState
|
||||||
|
@ -6,6 +7,21 @@
|
||||||
export let dataSet
|
export let dataSet
|
||||||
export let value
|
export let value
|
||||||
export let onSelect
|
export let onSelect
|
||||||
|
export let identifiers = ["resourceId"]
|
||||||
|
|
||||||
|
$: displayDatasourceName = $datasources.list.length > 1
|
||||||
|
|
||||||
|
function isSelected(entry) {
|
||||||
|
if (!identifiers.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (const identifier of identifiers) {
|
||||||
|
if (entry[identifier] !== value?.[identifier]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if dividerState}
|
{#if dividerState}
|
||||||
|
@ -21,15 +37,16 @@
|
||||||
{#each dataSet as data}
|
{#each dataSet as data}
|
||||||
<li
|
<li
|
||||||
class="spectrum-Menu-item"
|
class="spectrum-Menu-item"
|
||||||
class:is-selected={value?.label === data.label &&
|
class:is-selected={isSelected(data) && value?.type === data.type}
|
||||||
value?.type === data.type}
|
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected="true"
|
aria-selected="true"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
on:click={() => onSelect(data)}
|
on:click={() => onSelect(data)}
|
||||||
>
|
>
|
||||||
<span class="spectrum-Menu-itemLabel">
|
<span class="spectrum-Menu-itemLabel">
|
||||||
{data.datasourceName ? `${data.datasourceName} - ` : ""}{data.label}
|
{data.datasourceName && displayDatasourceName
|
||||||
|
? `${data.datasourceName} - `
|
||||||
|
: ""}{data.label}
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
|
|
|
@ -31,10 +31,15 @@
|
||||||
import IntegrationQueryEditor from "@/components/integration/index.svelte"
|
import IntegrationQueryEditor from "@/components/integration/index.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { findAllComponents } from "@/helpers/components"
|
import { findAllComponents } from "@/helpers/components"
|
||||||
|
import {
|
||||||
|
extractFields,
|
||||||
|
extractJSONArrayFields,
|
||||||
|
extractRelationships,
|
||||||
|
} from "@/helpers/bindings"
|
||||||
import ClientBindingPanel from "@/components/common/bindings/ClientBindingPanel.svelte"
|
import ClientBindingPanel from "@/components/common/bindings/ClientBindingPanel.svelte"
|
||||||
import DataSourceCategory from "@/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
|
import DataSourceCategory from "@/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { datasourceSelect as format } from "@/helpers/data/format"
|
import { sortAndFormat } from "@/helpers/data/format"
|
||||||
|
|
||||||
export let value = {}
|
export let value = {}
|
||||||
export let otherSources
|
export let otherSources
|
||||||
|
@ -51,25 +56,13 @@
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
$: text = value?.label ?? "Choose an option"
|
$: text = value?.label ?? "Choose an option"
|
||||||
$: tables = $tablesStore.list
|
$: tables = sortAndFormat.tables($tablesStore.list, $datasources.list)
|
||||||
.map(table => format.table(table, $datasources.list))
|
|
||||||
.sort((a, b) => {
|
|
||||||
// sort tables alphabetically, grouped by datasource
|
|
||||||
const dsA = a.datasourceName ?? ""
|
|
||||||
const dsB = b.datasourceName ?? ""
|
|
||||||
|
|
||||||
const dsComparison = dsA.localeCompare(dsB)
|
|
||||||
if (dsComparison !== 0) {
|
|
||||||
return dsComparison
|
|
||||||
}
|
|
||||||
return a.label.localeCompare(b.label)
|
|
||||||
})
|
|
||||||
$: viewsV1 = $viewsStore.list.map(view => ({
|
$: viewsV1 = $viewsStore.list.map(view => ({
|
||||||
...view,
|
...view,
|
||||||
label: view.name,
|
label: view.name,
|
||||||
type: "view",
|
type: "view",
|
||||||
}))
|
}))
|
||||||
$: viewsV2 = $viewsV2Store.list.map(format.viewV2)
|
$: viewsV2 = sortAndFormat.viewsV2($viewsV2Store.list, $datasources.list)
|
||||||
$: views = [...(viewsV1 || []), ...(viewsV2 || [])]
|
$: views = [...(viewsV1 || []), ...(viewsV2 || [])]
|
||||||
$: queries = $queriesStore.list
|
$: queries = $queriesStore.list
|
||||||
.filter(q => showAllQueries || q.queryVerb === "read" || q.readable)
|
.filter(q => showAllQueries || q.queryVerb === "read" || q.readable)
|
||||||
|
@ -93,67 +86,9 @@
|
||||||
value: `{{ literal ${safe(provider._id)} }}`,
|
value: `{{ literal ${safe(provider._id)} }}`,
|
||||||
type: "provider",
|
type: "provider",
|
||||||
}))
|
}))
|
||||||
$: links = bindings
|
$: links = extractRelationships(bindings)
|
||||||
// Get only link bindings
|
$: fields = extractFields(bindings)
|
||||||
.filter(x => x.fieldSchema?.type === "link")
|
$: jsonArrays = extractJSONArrayFields(bindings)
|
||||||
// Filter out bindings provided by forms
|
|
||||||
.filter(x => !x.component?.endsWith("/form"))
|
|
||||||
.map(binding => {
|
|
||||||
const { providerId, readableBinding, fieldSchema } = binding || {}
|
|
||||||
const { name, tableId } = fieldSchema || {}
|
|
||||||
const safeProviderId = safe(providerId)
|
|
||||||
return {
|
|
||||||
providerId,
|
|
||||||
label: readableBinding,
|
|
||||||
fieldName: name,
|
|
||||||
tableId,
|
|
||||||
type: "link",
|
|
||||||
// These properties will be enriched by the client library and provide
|
|
||||||
// details of the parent row of the relationship field, from context
|
|
||||||
rowId: `{{ ${safeProviderId}.${safe("_id")} }}`,
|
|
||||||
rowTableId: `{{ ${safeProviderId}.${safe("tableId")} }}`,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
$: fields = bindings
|
|
||||||
.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
|
|
||||||
return {
|
|
||||||
providerId,
|
|
||||||
label: readableBinding,
|
|
||||||
fieldName: name,
|
|
||||||
fieldType: type,
|
|
||||||
tableId,
|
|
||||||
type: "field",
|
|
||||||
value: `{{ literal ${runtimeBinding} }}`,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
$: jsonArrays = bindings
|
|
||||||
.filter(
|
|
||||||
x =>
|
|
||||||
x.fieldSchema?.type === "jsonarray" ||
|
|
||||||
(x.fieldSchema?.type === "json" && x.fieldSchema?.subtype === "array")
|
|
||||||
)
|
|
||||||
.map(binding => {
|
|
||||||
const { providerId, readableBinding, runtimeBinding, tableId } = binding
|
|
||||||
const { name, type, prefixKeys, subtype } = binding.fieldSchema
|
|
||||||
return {
|
|
||||||
providerId,
|
|
||||||
label: readableBinding,
|
|
||||||
fieldName: name,
|
|
||||||
fieldType: type,
|
|
||||||
tableId,
|
|
||||||
prefixKeys,
|
|
||||||
type: type === "jsonarray" ? "jsonarray" : "queryarray",
|
|
||||||
subtype,
|
|
||||||
value: `{{ literal ${runtimeBinding} }}`,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
$: custom = {
|
$: custom = {
|
||||||
type: "custom",
|
type: "custom",
|
||||||
label: "JSON / CSV",
|
label: "JSON / CSV",
|
||||||
|
@ -303,6 +238,7 @@
|
||||||
dataSet={views}
|
dataSet={views}
|
||||||
{value}
|
{value}
|
||||||
onSelect={handleSelected}
|
onSelect={handleSelected}
|
||||||
|
identifiers={["tableId", "name"]}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if queries?.length}
|
{#if queries?.length}
|
||||||
|
@ -312,6 +248,7 @@
|
||||||
dataSet={queries}
|
dataSet={queries}
|
||||||
{value}
|
{value}
|
||||||
onSelect={handleSelected}
|
onSelect={handleSelected}
|
||||||
|
identifiers={["_id"]}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if links?.length}
|
{#if links?.length}
|
||||||
|
@ -321,6 +258,7 @@
|
||||||
dataSet={links}
|
dataSet={links}
|
||||||
{value}
|
{value}
|
||||||
onSelect={handleSelected}
|
onSelect={handleSelected}
|
||||||
|
identifiers={["tableId", "fieldName"]}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if fields?.length}
|
{#if fields?.length}
|
||||||
|
@ -330,6 +268,7 @@
|
||||||
dataSet={fields}
|
dataSet={fields}
|
||||||
{value}
|
{value}
|
||||||
onSelect={handleSelected}
|
onSelect={handleSelected}
|
||||||
|
identifiers={["providerId", "tableId", "fieldName"]}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if jsonArrays?.length}
|
{#if jsonArrays?.length}
|
||||||
|
@ -339,6 +278,7 @@
|
||||||
dataSet={jsonArrays}
|
dataSet={jsonArrays}
|
||||||
{value}
|
{value}
|
||||||
onSelect={handleSelected}
|
onSelect={handleSelected}
|
||||||
|
identifiers={["providerId", "tableId", "fieldName"]}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showDataProviders && dataProviders?.length}
|
{#if showDataProviders && dataProviders?.length}
|
||||||
|
@ -348,6 +288,7 @@
|
||||||
dataSet={dataProviders}
|
dataSet={dataProviders}
|
||||||
{value}
|
{value}
|
||||||
onSelect={handleSelected}
|
onSelect={handleSelected}
|
||||||
|
identifiers={["providerId"]}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<DataSourceCategory
|
<DataSourceCategory
|
||||||
|
|
|
@ -1,22 +1,32 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select } from "@budibase/bbui"
|
import { Popover, Select } from "@budibase/bbui"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
import { tables as tablesStore, viewsV2 } from "@/stores/builder"
|
import {
|
||||||
import { tableSelect as format } from "@/helpers/data/format"
|
tables as tableStore,
|
||||||
|
datasources as datasourceStore,
|
||||||
|
viewsV2 as viewsV2Store,
|
||||||
|
} from "@/stores/builder"
|
||||||
|
import DataSourceCategory from "./DataSourceSelect/DataSourceCategory.svelte"
|
||||||
|
import { sortAndFormat } from "@/helpers/data/format"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
|
let anchorRight, dropdownRight
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: tables = $tablesStore.list.map(format.table)
|
$: tables = sortAndFormat.tables($tableStore.list, $datasourceStore.list)
|
||||||
$: views = $viewsV2.list.map(format.viewV2)
|
$: views = sortAndFormat.viewsV2($viewsV2Store.list, $datasourceStore.list)
|
||||||
$: options = [...(tables || []), ...(views || [])]
|
$: options = [...(tables || []), ...(views || [])]
|
||||||
|
|
||||||
|
$: text = value?.label ?? "Choose an option"
|
||||||
|
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
dispatch(
|
dispatch(
|
||||||
"change",
|
"change",
|
||||||
options.find(x => x.resourceId === e.detail)
|
options.find(x => x.resourceId === e.resourceId)
|
||||||
)
|
)
|
||||||
|
dropdownRight.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -29,10 +39,47 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select
|
<div class="container" bind:this={anchorRight}>
|
||||||
on:change={onChange}
|
<Select
|
||||||
value={value?.resourceId}
|
readonly
|
||||||
{options}
|
value={text}
|
||||||
getOptionValue={x => x.resourceId}
|
options={[text]}
|
||||||
getOptionLabel={x => x.label}
|
on:click={dropdownRight.show}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<Popover bind:this={dropdownRight} anchor={anchorRight}>
|
||||||
|
<div class="dropdown">
|
||||||
|
<DataSourceCategory
|
||||||
|
heading="Tables"
|
||||||
|
dataSet={tables}
|
||||||
|
{value}
|
||||||
|
onSelect={onChange}
|
||||||
|
/>
|
||||||
|
{#if views?.length}
|
||||||
|
<DataSourceCategory
|
||||||
|
dividerState={true}
|
||||||
|
heading="Views"
|
||||||
|
dataSet={views}
|
||||||
|
{value}
|
||||||
|
onSelect={onChange}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.container :global(:first-child) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
padding: var(--spacing-m) 0;
|
||||||
|
z-index: 99999999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
const processModals = () => {
|
const processModals = () => {
|
||||||
const defaultCacheFn = key => {
|
const defaultCacheFn = key => {
|
||||||
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
|
temporalStore.setExpiring(key, {}, oneDayInSeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
const dismissableModals = [
|
const dismissableModals = [
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
return dismissableModals.filter(modal => {
|
return dismissableModals.filter(modal => {
|
||||||
return !temporalStore.actions.getExpiring(modal.key) && modal.criteria()
|
return !temporalStore.getExpiring(modal.key) && modal.criteria()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { BANNER_TYPES } from "@budibase/bbui"
|
||||||
const oneDayInSeconds = 86400
|
const oneDayInSeconds = 86400
|
||||||
|
|
||||||
const defaultCacheFn = key => {
|
const defaultCacheFn = key => {
|
||||||
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
|
temporalStore.setExpiring(key, {}, oneDayInSeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
const upgradeAction = key => {
|
const upgradeAction = key => {
|
||||||
|
@ -148,7 +148,7 @@ export const getBanners = () => {
|
||||||
buildUsersAboveLimitBanner(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER),
|
buildUsersAboveLimitBanner(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER),
|
||||||
].filter(licensingBanner => {
|
].filter(licensingBanner => {
|
||||||
return (
|
return (
|
||||||
!temporalStore.actions.getExpiring(licensingBanner.key) &&
|
!temporalStore.getExpiring(licensingBanner.key) &&
|
||||||
licensingBanner.criteria()
|
licensingBanner.criteria()
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
|
import { UIBinding } from "@budibase/types"
|
||||||
|
|
||||||
|
export function extractRelationships(bindings: UIBinding[]) {
|
||||||
|
return (
|
||||||
|
bindings
|
||||||
|
// Get only link bindings
|
||||||
|
.filter(x => x.fieldSchema?.type === "link")
|
||||||
|
// Filter out bindings provided by forms
|
||||||
|
.filter(x => !x.component?.endsWith("/form"))
|
||||||
|
.map(binding => {
|
||||||
|
const { providerId, readableBinding, fieldSchema } = binding || {}
|
||||||
|
const { name, tableId } = fieldSchema || {}
|
||||||
|
const safeProviderId = makePropSafe(providerId)
|
||||||
|
return {
|
||||||
|
providerId,
|
||||||
|
label: readableBinding,
|
||||||
|
fieldName: name,
|
||||||
|
tableId,
|
||||||
|
type: "link",
|
||||||
|
// These properties will be enriched by the client library and provide
|
||||||
|
// details of the parent row of the relationship field, from context
|
||||||
|
rowId: `{{ ${safeProviderId}.${makePropSafe("_id")} }}`,
|
||||||
|
rowTableId: `{{ ${safeProviderId}.${makePropSafe("tableId")} }}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractFields(bindings: UIBinding[]) {
|
||||||
|
return bindings
|
||||||
|
.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!
|
||||||
|
return {
|
||||||
|
providerId,
|
||||||
|
label: readableBinding,
|
||||||
|
fieldName: name,
|
||||||
|
fieldType: type,
|
||||||
|
tableId,
|
||||||
|
type: "field",
|
||||||
|
value: `{{ literal ${runtimeBinding} }}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractJSONArrayFields(bindings: UIBinding[]) {
|
||||||
|
return bindings
|
||||||
|
.filter(
|
||||||
|
x =>
|
||||||
|
x.fieldSchema?.type === "jsonarray" ||
|
||||||
|
(x.fieldSchema?.type === "json" && x.fieldSchema?.subtype === "array")
|
||||||
|
)
|
||||||
|
.map(binding => {
|
||||||
|
const { providerId, readableBinding, runtimeBinding, tableId } = binding
|
||||||
|
const { name, type, prefixKeys, subtype } = binding.fieldSchema!
|
||||||
|
return {
|
||||||
|
providerId,
|
||||||
|
label: readableBinding,
|
||||||
|
fieldName: name,
|
||||||
|
fieldType: type,
|
||||||
|
tableId,
|
||||||
|
prefixKeys,
|
||||||
|
type: type === "jsonarray" ? "jsonarray" : "queryarray",
|
||||||
|
subtype,
|
||||||
|
value: `{{ literal ${runtimeBinding} }}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -9,11 +9,18 @@ export const datasourceSelect = {
|
||||||
datasourceName: datasource?.name,
|
datasourceName: datasource?.name,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
viewV2: view => ({
|
viewV2: (view, datasources) => {
|
||||||
|
const datasource = datasources
|
||||||
|
?.filter(f => f.entities)
|
||||||
|
.flatMap(d => d.entities)
|
||||||
|
.find(ds => ds._id === view.tableId)
|
||||||
|
return {
|
||||||
...view,
|
...view,
|
||||||
label: view.name,
|
label: view.name,
|
||||||
type: "viewV2",
|
type: "viewV2",
|
||||||
}),
|
datasourceName: datasource?.name,
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tableSelect = {
|
export const tableSelect = {
|
||||||
|
@ -31,3 +38,36 @@ export const tableSelect = {
|
||||||
resourceId: view.id,
|
resourceId: view.id,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sortAndFormat = {
|
||||||
|
tables: (tables, datasources) => {
|
||||||
|
return tables
|
||||||
|
.map(table => {
|
||||||
|
const formatted = datasourceSelect.table(table, datasources)
|
||||||
|
return {
|
||||||
|
...formatted,
|
||||||
|
resourceId: table._id,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
// sort tables alphabetically, grouped by datasource
|
||||||
|
const dsA = a.datasourceName ?? ""
|
||||||
|
const dsB = b.datasourceName ?? ""
|
||||||
|
|
||||||
|
const dsComparison = dsA.localeCompare(dsB)
|
||||||
|
if (dsComparison !== 0) {
|
||||||
|
return dsComparison
|
||||||
|
}
|
||||||
|
return a.label.localeCompare(b.label)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
viewsV2: (views, datasources) => {
|
||||||
|
return views.map(view => {
|
||||||
|
const formatted = datasourceSelect.viewV2(view, datasources)
|
||||||
|
return {
|
||||||
|
...formatted,
|
||||||
|
resourceId: view.id,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -9,3 +9,5 @@ export {
|
||||||
lowercase,
|
lowercase,
|
||||||
isBuilderInputFocused,
|
isBuilderInputFocused,
|
||||||
} from "./helpers"
|
} from "./helpers"
|
||||||
|
export * as featureFlag from "./featureFlags"
|
||||||
|
export * as bindings from "./bindings"
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Component, Screen, ScreenProps } from "@budibase/types"
|
||||||
|
import clientManifest from "@budibase/client/manifest.json"
|
||||||
|
|
||||||
|
export function findComponentsBySettingsType(
|
||||||
|
screen: Screen,
|
||||||
|
type: string | string[]
|
||||||
|
) {
|
||||||
|
const typesArray = Array.isArray(type) ? type : [type]
|
||||||
|
|
||||||
|
const result: {
|
||||||
|
component: Component
|
||||||
|
setting: {
|
||||||
|
type: string
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
}[] = []
|
||||||
|
function recurseFieldComponentsInChildren(component: ScreenProps) {
|
||||||
|
if (!component) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = getManifestDefinition(component)
|
||||||
|
const setting =
|
||||||
|
"settings" in definition &&
|
||||||
|
definition.settings.find((s: any) => typesArray.includes(s.type))
|
||||||
|
if (setting && "type" in setting) {
|
||||||
|
result.push({
|
||||||
|
component,
|
||||||
|
setting: { type: setting.type!, key: setting.key! },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
component._children?.forEach(child => {
|
||||||
|
recurseFieldComponentsInChildren(child)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
recurseFieldComponentsInChildren(screen?.props)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManifestDefinition(component: Component) {
|
||||||
|
const componentType = component._component.split("/").slice(-1)[0]
|
||||||
|
const definition =
|
||||||
|
clientManifest[componentType as keyof typeof clientManifest]
|
||||||
|
return definition
|
||||||
|
}
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
$: useAccountPortal = cloud && !$admin.disableAccountPortal
|
$: useAccountPortal = cloud && !$admin.disableAccountPortal
|
||||||
|
|
||||||
navigation.actions.init($redirect)
|
navigation.init($redirect)
|
||||||
|
|
||||||
const validateTenantId = async () => {
|
const validateTenantId = async () => {
|
||||||
const host = window.location.host
|
const host = window.location.host
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
selectedScreen,
|
selectedScreen,
|
||||||
hoverStore,
|
hoverStore,
|
||||||
componentTreeNodesStore,
|
componentTreeNodesStore,
|
||||||
|
screenComponentErrors,
|
||||||
snippets,
|
snippets,
|
||||||
} from "@/stores/builder"
|
} from "@/stores/builder"
|
||||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||||
|
@ -68,6 +69,7 @@
|
||||||
port: window.location.port,
|
port: window.location.port,
|
||||||
},
|
},
|
||||||
snippets: $snippets,
|
snippets: $snippets,
|
||||||
|
componentErrors: $screenComponentErrors,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the preview when required
|
// Refresh the preview when required
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
import {
|
import {
|
||||||
appsStore,
|
appsStore,
|
||||||
organisation,
|
organisation,
|
||||||
|
admin,
|
||||||
auth,
|
auth,
|
||||||
groups,
|
groups,
|
||||||
licensing,
|
licensing,
|
||||||
|
@ -42,6 +43,7 @@
|
||||||
app => app.status === AppStatus.DEPLOYED
|
app => app.status === AppStatus.DEPLOYED
|
||||||
)
|
)
|
||||||
$: userApps = getUserApps(publishedApps, userGroups, $auth.user)
|
$: userApps = getUserApps(publishedApps, userGroups, $auth.user)
|
||||||
|
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||||
|
|
||||||
function getUserApps(publishedApps, userGroups, user) {
|
function getUserApps(publishedApps, userGroups, user) {
|
||||||
if (sdk.users.isAdmin(user)) {
|
if (sdk.users.isAdmin(user)) {
|
||||||
|
@ -111,7 +113,13 @@
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="LockClosed"
|
icon="LockClosed"
|
||||||
on:click={() => changePasswordModal.show()}
|
on:click={() => {
|
||||||
|
if (isOwner) {
|
||||||
|
window.location.href = `${$admin.accountPortalUrl}/portal/account`
|
||||||
|
} else {
|
||||||
|
changePasswordModal.show()
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Update password
|
Update password
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
|
@ -30,10 +30,16 @@
|
||||||
try {
|
try {
|
||||||
loading = true
|
loading = true
|
||||||
if (forceResetPassword) {
|
if (forceResetPassword) {
|
||||||
|
const email = $auth.user.email
|
||||||
|
const tenantId = $auth.user.tenantId
|
||||||
await auth.updateSelf({
|
await auth.updateSelf({
|
||||||
password,
|
password,
|
||||||
forceResetPassword: false,
|
forceResetPassword: false,
|
||||||
})
|
})
|
||||||
|
if (!$auth.user) {
|
||||||
|
// Update self will clear the platform user, so need to login
|
||||||
|
await auth.login(email, password, tenantId)
|
||||||
|
}
|
||||||
$goto("../portal/")
|
$goto("../portal/")
|
||||||
} else {
|
} else {
|
||||||
await auth.resetPassword(password, resetCode)
|
await auth.resetPassword(password, resetCode)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { auth } from "@/stores/portal"
|
import { admin, auth } from "@/stores/portal"
|
||||||
import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui"
|
import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import ProfileModal from "@/components/settings/ProfileModal.svelte"
|
import ProfileModal from "@/components/settings/ProfileModal.svelte"
|
||||||
|
@ -13,6 +13,8 @@
|
||||||
let updatePasswordModal
|
let updatePasswordModal
|
||||||
let apiKeyModal
|
let apiKeyModal
|
||||||
|
|
||||||
|
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
try {
|
try {
|
||||||
await auth.logout()
|
await auth.logout()
|
||||||
|
@ -32,7 +34,16 @@
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon="Moon" on:click={() => themeModal.show()}>Theme</MenuItem>
|
<MenuItem icon="Moon" on:click={() => themeModal.show()}>Theme</MenuItem>
|
||||||
{#if !$auth.isSSO}
|
{#if !$auth.isSSO}
|
||||||
<MenuItem icon="LockClosed" on:click={() => updatePasswordModal.show()}>
|
<MenuItem
|
||||||
|
icon="LockClosed"
|
||||||
|
on:click={() => {
|
||||||
|
if (isOwner) {
|
||||||
|
window.location.href = `${$admin.accountPortalUrl}/portal/account`
|
||||||
|
} else {
|
||||||
|
updatePasswordModal.show()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
Update password
|
Update password
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -49,7 +49,12 @@ export class ComponentTreeNodesStore extends BudiStore<OpenNodesState> {
|
||||||
|
|
||||||
// Will ensure all parents of a node are expanded so that it is visible in the tree
|
// Will ensure all parents of a node are expanded so that it is visible in the tree
|
||||||
makeNodeVisible(componentId: string) {
|
makeNodeVisible(componentId: string) {
|
||||||
const selectedScreen = get(selectedScreenStore)
|
const selectedScreen: Screen | undefined = get(selectedScreenStore)
|
||||||
|
|
||||||
|
if (!selectedScreen) {
|
||||||
|
console.error("Invalid node " + componentId)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
const path = findComponentPath(selectedScreen?.props, componentId)
|
const path = findComponentPath(selectedScreen?.props, componentId)
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,16 @@ import { Utils } from "@budibase/frontend-core"
|
||||||
import { Component, FieldType, Screen, Table } from "@budibase/types"
|
import { Component, FieldType, Screen, Table } from "@budibase/types"
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
|
|
||||||
interface ComponentDefinition {
|
export interface ComponentState {
|
||||||
|
components: Record<string, ComponentDefinition>
|
||||||
|
customComponents: string[]
|
||||||
|
selectedComponentId?: string
|
||||||
|
componentToPaste?: Component
|
||||||
|
settingsCache: Record<string, ComponentSetting[]>
|
||||||
|
selectedScreenId?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComponentDefinition {
|
||||||
component: string
|
component: string
|
||||||
name: string
|
name: string
|
||||||
friendlyName?: string
|
friendlyName?: string
|
||||||
|
@ -41,10 +50,11 @@ interface ComponentDefinition {
|
||||||
settings?: ComponentSetting[]
|
settings?: ComponentSetting[]
|
||||||
features?: Record<string, boolean>
|
features?: Record<string, boolean>
|
||||||
typeSupportPresets?: Record<string, any>
|
typeSupportPresets?: Record<string, any>
|
||||||
illegalChildren?: string[]
|
legalDirectChildren: string[]
|
||||||
|
illegalChildren: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComponentSetting {
|
export interface ComponentSetting {
|
||||||
key: string
|
key: string
|
||||||
type: string
|
type: string
|
||||||
section?: string
|
section?: string
|
||||||
|
@ -55,20 +65,9 @@ interface ComponentSetting {
|
||||||
settings?: ComponentSetting[]
|
settings?: ComponentSetting[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComponentState {
|
|
||||||
components: Record<string, ComponentDefinition>
|
|
||||||
customComponents: string[]
|
|
||||||
selectedComponentId: string | null | undefined
|
|
||||||
componentToPaste?: Component | null
|
|
||||||
settingsCache: Record<string, ComponentSetting[]>
|
|
||||||
selectedScreenId?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const INITIAL_COMPONENTS_STATE: ComponentState = {
|
export const INITIAL_COMPONENTS_STATE: ComponentState = {
|
||||||
components: {},
|
components: {},
|
||||||
customComponents: [],
|
customComponents: [],
|
||||||
selectedComponentId: null,
|
|
||||||
componentToPaste: null,
|
|
||||||
settingsCache: {},
|
settingsCache: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,6 +440,11 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
createInstance(componentName: string, presetProps: any, parent: any) {
|
createInstance(componentName: string, presetProps: any, parent: any) {
|
||||||
|
const screen = get(selectedScreen)
|
||||||
|
if (!screen) {
|
||||||
|
throw "A valid screen must be selected"
|
||||||
|
}
|
||||||
|
|
||||||
const definition = this.getDefinition(componentName)
|
const definition = this.getDefinition(componentName)
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
return null
|
return null
|
||||||
|
@ -462,7 +466,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
// Standard post processing
|
// Standard post processing
|
||||||
this.enrichEmptySettings(instance, {
|
this.enrichEmptySettings(instance, {
|
||||||
parent,
|
parent,
|
||||||
screen: get(selectedScreen),
|
screen,
|
||||||
useDefaultValues: true,
|
useDefaultValues: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -483,7 +487,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
// Add step name to form steps
|
// Add step name to form steps
|
||||||
if (componentName.endsWith("/formstep") && $selectedScreen) {
|
if (componentName.endsWith("/formstep") && $selectedScreen) {
|
||||||
const parentForm = findClosestMatchingComponent(
|
const parentForm = findClosestMatchingComponent(
|
||||||
$selectedScreen.props,
|
screen.props,
|
||||||
get(selectedComponent)._id,
|
get(selectedComponent)._id,
|
||||||
(component: Component) => component._component.endsWith("/form")
|
(component: Component) => component._component.endsWith("/form")
|
||||||
)
|
)
|
||||||
|
@ -543,7 +547,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
// Find the selected component
|
// Find the selected component
|
||||||
let selectedComponentId = state.selectedComponentId
|
let selectedComponentId = state.selectedComponentId
|
||||||
if (selectedComponentId?.startsWith(`${screen._id}-`)) {
|
if (selectedComponentId?.startsWith(`${screen._id}-`)) {
|
||||||
selectedComponentId = screen.props._id || null
|
selectedComponentId = screen.props._id
|
||||||
}
|
}
|
||||||
const currentComponent = findComponent(
|
const currentComponent = findComponent(
|
||||||
screen.props,
|
screen.props,
|
||||||
|
@ -654,7 +658,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
// Determine the next component to select, and select it before deletion
|
// Determine the next component to select, and select it before deletion
|
||||||
// to avoid an intermediate state of no component selection
|
// to avoid an intermediate state of no component selection
|
||||||
const state = get(this.store)
|
const state = get(this.store)
|
||||||
let nextId: string | null = ""
|
let nextId = ""
|
||||||
if (state.selectedComponentId === component._id) {
|
if (state.selectedComponentId === component._id) {
|
||||||
nextId = this.getNext()
|
nextId = this.getNext()
|
||||||
if (!nextId) {
|
if (!nextId) {
|
||||||
|
@ -741,7 +745,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
if (!state.componentToPaste) {
|
if (!state.componentToPaste) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let newComponentId: string | null = ""
|
let newComponentId = ""
|
||||||
|
|
||||||
// Remove copied component if cutting, regardless if pasting works
|
// Remove copied component if cutting, regardless if pasting works
|
||||||
let componentToPaste = cloneDeep(state.componentToPaste)
|
let componentToPaste = cloneDeep(state.componentToPaste)
|
||||||
|
@ -842,7 +846,10 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
getPrevious() {
|
getPrevious() {
|
||||||
const state = get(this.store)
|
const state = get(this.store)
|
||||||
const componentId = state.selectedComponentId
|
const componentId = state.selectedComponentId
|
||||||
const screen = get(selectedScreen)!
|
const screen = get(selectedScreen)
|
||||||
|
if (!screen) {
|
||||||
|
throw "A valid screen must be selected"
|
||||||
|
}
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
const index = parent?._children.findIndex(
|
const index = parent?._children.findIndex(
|
||||||
(x: Component) => x._id === componentId
|
(x: Component) => x._id === componentId
|
||||||
|
@ -891,7 +898,10 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
const state = get(this.store)
|
const state = get(this.store)
|
||||||
const component = get(selectedComponent)
|
const component = get(selectedComponent)
|
||||||
const componentId = component?._id
|
const componentId = component?._id
|
||||||
const screen = get(selectedScreen)!
|
const screen = get(selectedScreen)
|
||||||
|
if (!screen) {
|
||||||
|
throw "A valid screen must be selected"
|
||||||
|
}
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
const index = parent?._children.findIndex(
|
const index = parent?._children.findIndex(
|
||||||
(x: Component) => x._id === componentId
|
(x: Component) => x._id === componentId
|
||||||
|
@ -1158,7 +1168,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleEjectBlock(componentId: string, ejectedDefinition: Component) {
|
async handleEjectBlock(componentId: string, ejectedDefinition: Component) {
|
||||||
let nextSelectedComponentId: string | null = null
|
let nextSelectedComponentId: string | undefined
|
||||||
|
|
||||||
await screenStore.patch((screen: Screen) => {
|
await screenStore.patch((screen: Screen) => {
|
||||||
const block = findComponent(screen.props, componentId)
|
const block = findComponent(screen.props, componentId)
|
||||||
|
@ -1194,7 +1204,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
(x: Component) => x._id === componentId
|
(x: Component) => x._id === componentId
|
||||||
)
|
)
|
||||||
parent._children[index] = ejectedDefinition
|
parent._children[index] = ejectedDefinition
|
||||||
nextSelectedComponentId = ejectedDefinition._id ?? null
|
nextSelectedComponentId = ejectedDefinition._id
|
||||||
}, null)
|
}, null)
|
||||||
|
|
||||||
// Select new root component
|
// Select new root component
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { appStore } from "./app.js"
|
||||||
import { componentStore, selectedComponent } from "./components"
|
import { componentStore, selectedComponent } from "./components"
|
||||||
import { navigationStore } from "./navigation.js"
|
import { navigationStore } from "./navigation.js"
|
||||||
import { themeStore } from "./theme.js"
|
import { themeStore } from "./theme.js"
|
||||||
import { screenStore, selectedScreen, sortedScreens } from "./screens.js"
|
import { screenStore, selectedScreen, sortedScreens } from "./screens"
|
||||||
import { builderStore } from "./builder.js"
|
import { builderStore } from "./builder.js"
|
||||||
import { hoverStore } from "./hover.js"
|
import { hoverStore } from "./hover.js"
|
||||||
import { previewStore } from "./preview.js"
|
import { previewStore } from "./preview.js"
|
||||||
|
@ -16,6 +16,7 @@ import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js"
|
||||||
import { deploymentStore } from "./deployments.js"
|
import { deploymentStore } from "./deployments.js"
|
||||||
import { contextMenuStore } from "./contextMenu.js"
|
import { contextMenuStore } from "./contextMenu.js"
|
||||||
import { snippets } from "./snippets"
|
import { snippets } from "./snippets"
|
||||||
|
import { screenComponentErrors } from "./screenComponent"
|
||||||
|
|
||||||
// Backend
|
// Backend
|
||||||
import { tables } from "./tables"
|
import { tables } from "./tables"
|
||||||
|
@ -67,6 +68,7 @@ export {
|
||||||
snippets,
|
snippets,
|
||||||
rowActions,
|
rowActions,
|
||||||
appPublished,
|
appPublished,
|
||||||
|
screenComponentErrors,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reset = () => {
|
export const reset = () => {
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { derived } from "svelte/store"
|
||||||
|
import { tables } from "./tables"
|
||||||
|
import { selectedScreen } from "./screens"
|
||||||
|
import { viewsV2 } from "./viewsV2"
|
||||||
|
import { findComponentsBySettingsType } from "@/helpers/screen"
|
||||||
|
import { UIDatasourceType, Screen } from "@budibase/types"
|
||||||
|
import { queries } from "./queries"
|
||||||
|
import { views } from "./views"
|
||||||
|
import { bindings, featureFlag } from "@/helpers"
|
||||||
|
import { getBindableProperties } from "@/dataBinding"
|
||||||
|
|
||||||
|
function reduceBy<TItem extends {}, TKey extends keyof TItem>(
|
||||||
|
key: TKey,
|
||||||
|
list: TItem[]
|
||||||
|
): Record<string, any> {
|
||||||
|
return list.reduce(
|
||||||
|
(result, item) => ({
|
||||||
|
...result,
|
||||||
|
[item[key] as string]: item,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const friendlyNameByType: Partial<Record<UIDatasourceType, string>> = {
|
||||||
|
viewV2: "view",
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationKeyByType: Record<UIDatasourceType, string | null> = {
|
||||||
|
table: "tableId",
|
||||||
|
view: "name",
|
||||||
|
viewV2: "id",
|
||||||
|
query: "_id",
|
||||||
|
custom: null,
|
||||||
|
link: "rowId",
|
||||||
|
field: "value",
|
||||||
|
jsonarray: "value",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const screenComponentErrors = derived(
|
||||||
|
[selectedScreen, tables, views, viewsV2, queries],
|
||||||
|
([$selectedScreen, $tables, $views, $viewsV2, $queries]): Record<
|
||||||
|
string,
|
||||||
|
string[]
|
||||||
|
> => {
|
||||||
|
if (!featureFlag.isEnabled("CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS")) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
function getInvalidDatasources(
|
||||||
|
screen: Screen,
|
||||||
|
datasources: Record<string, any>
|
||||||
|
) {
|
||||||
|
const result: Record<string, string[]> = {}
|
||||||
|
for (const { component, setting } of findComponentsBySettingsType(
|
||||||
|
screen,
|
||||||
|
["table", "dataSource"]
|
||||||
|
)) {
|
||||||
|
const componentSettings = component[setting.key]
|
||||||
|
if (!componentSettings) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const { label } = componentSettings
|
||||||
|
const type = componentSettings.type as UIDatasourceType
|
||||||
|
|
||||||
|
const validationKey = validationKeyByType[type]
|
||||||
|
if (!validationKey) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentBindings = getBindableProperties(
|
||||||
|
$selectedScreen,
|
||||||
|
component._id
|
||||||
|
)
|
||||||
|
|
||||||
|
const componentDatasources = {
|
||||||
|
...reduceBy(
|
||||||
|
"rowId",
|
||||||
|
bindings.extractRelationships(componentBindings)
|
||||||
|
),
|
||||||
|
...reduceBy("value", bindings.extractFields(componentBindings)),
|
||||||
|
...reduceBy(
|
||||||
|
"value",
|
||||||
|
bindings.extractJSONArrayFields(componentBindings)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceId = componentSettings[validationKey]
|
||||||
|
if (!{ ...datasources, ...componentDatasources }[resourceId]) {
|
||||||
|
const friendlyTypeName = friendlyNameByType[type] ?? type
|
||||||
|
result[component._id!] = [
|
||||||
|
`The ${friendlyTypeName} named "${label}" could not be found`,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const datasources = {
|
||||||
|
...reduceBy("_id", $tables.list),
|
||||||
|
...reduceBy("name", $views.list),
|
||||||
|
...reduceBy("id", $viewsV2.list),
|
||||||
|
...reduceBy("_id", $queries.list),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$selectedScreen) {
|
||||||
|
// Skip validation if a screen is not selected.
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getInvalidDatasources($selectedScreen, datasources)
|
||||||
|
}
|
||||||
|
)
|
|
@ -10,29 +10,35 @@ import {
|
||||||
navigationStore,
|
navigationStore,
|
||||||
selectedComponent,
|
selectedComponent,
|
||||||
} from "@/stores/builder"
|
} from "@/stores/builder"
|
||||||
import { createHistoryStore, HistoryStore } from "@/stores/builder/history"
|
import { createHistoryStore } from "@/stores/builder/history"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { BudiStore } from "../BudiStore"
|
import { BudiStore } from "../BudiStore"
|
||||||
import { Component, Screen } from "@budibase/types"
|
import {
|
||||||
|
FetchAppPackageResponse,
|
||||||
|
DeleteScreenResponse,
|
||||||
|
Screen,
|
||||||
|
Component,
|
||||||
|
SaveScreenResponse,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { ComponentDefinition } from "./components"
|
||||||
|
|
||||||
interface ScreenState {
|
interface ScreenState {
|
||||||
screens: Screen[]
|
screens: Screen[]
|
||||||
selectedScreenId: string | null | undefined
|
selectedScreenId?: string
|
||||||
selected?: Screen
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const INITIAL_SCREENS_STATE: ScreenState = {
|
export const initialScreenState: ScreenState = {
|
||||||
screens: [],
|
screens: [],
|
||||||
selectedScreenId: null,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Review the nulls
|
||||||
export class ScreenStore extends BudiStore<ScreenState> {
|
export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
history: HistoryStore<Screen>
|
history: any
|
||||||
save: (doc: Screen) => Promise<Screen>
|
delete: any
|
||||||
delete: (doc: Screen) => Promise<void>
|
save: any
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(INITIAL_SCREENS_STATE)
|
super(initialScreenState)
|
||||||
|
|
||||||
// Bind scope
|
// Bind scope
|
||||||
this.select = this.select.bind(this)
|
this.select = this.select.bind(this)
|
||||||
|
@ -49,14 +55,16 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
this.removeCustomLayout = this.removeCustomLayout.bind(this)
|
this.removeCustomLayout = this.removeCustomLayout.bind(this)
|
||||||
|
|
||||||
this.history = createHistoryStore({
|
this.history = createHistoryStore({
|
||||||
getDoc: id => get(this.store).screens?.find(screen => screen._id === id),
|
getDoc: (id: string) =>
|
||||||
|
get(this.store).screens?.find(screen => screen._id === id),
|
||||||
selectDoc: this.select,
|
selectDoc: this.select,
|
||||||
|
beforeAction: () => {},
|
||||||
afterAction: () => {
|
afterAction: () => {
|
||||||
// Ensure a valid component is selected
|
// Ensure a valid component is selected
|
||||||
if (!get(selectedComponent)) {
|
if (!get(selectedComponent)) {
|
||||||
this.update(state => ({
|
this.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
selectedComponentId: get(this.store).selected?.props._id,
|
selectedComponentId: get(selectedScreen)?.props._id,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -70,14 +78,14 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
* Reset entire store back to base config
|
* Reset entire store back to base config
|
||||||
*/
|
*/
|
||||||
reset() {
|
reset() {
|
||||||
this.store.set({ ...INITIAL_SCREENS_STATE })
|
this.store.set({ ...initialScreenState })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace ALL store screens with application package screens
|
* Replace ALL store screens with application package screens
|
||||||
* @param {object} pkg
|
* @param {FetchAppPackageResponse} pkg
|
||||||
*/
|
*/
|
||||||
syncAppScreens(pkg: { screens: Screen[] }) {
|
syncAppScreens(pkg: FetchAppPackageResponse) {
|
||||||
this.update(state => ({
|
this.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
screens: [...pkg.screens],
|
screens: [...pkg.screens],
|
||||||
|
@ -114,7 +122,7 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
* Recursively parses the entire screen doc and checks for components
|
* Recursively parses the entire screen doc and checks for components
|
||||||
* violating illegal child configurations.
|
* violating illegal child configurations.
|
||||||
*
|
*
|
||||||
* @param {object} screen
|
* @param {Screen} screen
|
||||||
* @throws Will throw an error containing the name of the component causing
|
* @throws Will throw an error containing the name of the component causing
|
||||||
* the invalid screen state
|
* the invalid screen state
|
||||||
*/
|
*/
|
||||||
|
@ -125,7 +133,7 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
illegalChildren: string[] = [],
|
illegalChildren: string[] = [],
|
||||||
legalDirectChildren: string[] = []
|
legalDirectChildren: string[] = []
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
const type = component._component
|
const type: string = component._component
|
||||||
|
|
||||||
if (illegalChildren.includes(type)) {
|
if (illegalChildren.includes(type)) {
|
||||||
return type
|
return type
|
||||||
|
@ -148,7 +156,20 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
illegalChildren = []
|
illegalChildren = []
|
||||||
}
|
}
|
||||||
|
|
||||||
const definition = componentStore.getDefinition(component._component)
|
const definition: ComponentDefinition | null =
|
||||||
|
componentStore.getDefinition(component._component)
|
||||||
|
|
||||||
|
if (definition == null) {
|
||||||
|
throw `Invalid defintion ${component._component}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset whitelist for direct children
|
||||||
|
legalDirectChildren = []
|
||||||
|
if (definition?.legalDirectChildren?.length) {
|
||||||
|
legalDirectChildren = definition.legalDirectChildren.map(x => {
|
||||||
|
return `@budibase/standard-components/${x}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Append blacklisted components and remove duplicates
|
// Append blacklisted components and remove duplicates
|
||||||
if (definition?.illegalChildren?.length) {
|
if (definition?.illegalChildren?.length) {
|
||||||
|
@ -184,10 +205,9 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
* Core save method. If creating a new screen, the store will sync the target
|
* Core save method. If creating a new screen, the store will sync the target
|
||||||
* screen id to ensure that it is selected in the builder
|
* screen id to ensure that it is selected in the builder
|
||||||
*
|
*
|
||||||
* @param {object} screen
|
* @param {Screen} screen The screen being modified/created
|
||||||
* @returns {object}
|
|
||||||
*/
|
*/
|
||||||
async saveScreen(screen: Screen): Promise<Screen> {
|
async saveScreen(screen: Screen) {
|
||||||
const appState = get(appStore)
|
const appState = get(appStore)
|
||||||
|
|
||||||
// Validate screen structure if the app supports it
|
// Validate screen structure if the app supports it
|
||||||
|
@ -232,7 +252,7 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After saving a screen, sync plugins and routes to the appStore
|
* After saving a screen, sync plugins and routes to the appStore
|
||||||
* @param {object} savedScreen
|
* @param {Screen} savedScreen
|
||||||
*/
|
*/
|
||||||
async syncScreenData(savedScreen: Screen) {
|
async syncScreenData(savedScreen: Screen) {
|
||||||
const appState = get(appStore)
|
const appState = get(appStore)
|
||||||
|
@ -261,10 +281,7 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
* supports deeply mutating the current doc rather than just appending data.
|
* supports deeply mutating the current doc rather than just appending data.
|
||||||
*/
|
*/
|
||||||
sequentialScreenPatch = Utils.sequential(
|
sequentialScreenPatch = Utils.sequential(
|
||||||
async (
|
async (patchFn: (screen: Screen) => any, screenId: string) => {
|
||||||
patchFn: (screen: Screen) => any,
|
|
||||||
screenId: string
|
|
||||||
): Promise<Screen | undefined> => {
|
|
||||||
const state = get(this.store)
|
const state = get(this.store)
|
||||||
const screen = state.screens.find(screen => screen._id === screenId)
|
const screen = state.screens.find(screen => screen._id === screenId)
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
|
@ -282,14 +299,13 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {function} patchFn
|
* @param {Function} patchFn the patch action to be applied
|
||||||
* @param {string | null} screenId
|
* @param {string | null} screenId
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
async patch(
|
async patch(
|
||||||
patchFn: (screen: Screen) => void,
|
patchFn: (screen: Screen) => any,
|
||||||
screenId: string | undefined | null
|
screenId?: string | null
|
||||||
) {
|
): Promise<SaveScreenResponse | void> {
|
||||||
// Default to the currently selected screen
|
// Default to the currently selected screen
|
||||||
if (!screenId) {
|
if (!screenId) {
|
||||||
const state = get(this.store)
|
const state = get(this.store)
|
||||||
|
@ -306,9 +322,9 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
* the screen supplied. If no screen is provided, the target has
|
* the screen supplied. If no screen is provided, the target has
|
||||||
* been removed by another user and will be filtered from the store.
|
* been removed by another user and will be filtered from the store.
|
||||||
* Used to marshal updates for the websocket
|
* Used to marshal updates for the websocket
|
||||||
* @param {string} screenId
|
*
|
||||||
* @param {object} screen
|
* @param {string} screenId the target screen id
|
||||||
* @returns
|
* @param {Screen} screen the replacement screen
|
||||||
*/
|
*/
|
||||||
async replace(screenId: string, screen: Screen) {
|
async replace(screenId: string, screen: Screen) {
|
||||||
if (!screenId) {
|
if (!screenId) {
|
||||||
|
@ -346,17 +362,24 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
* Any deleted screens will then have their routes/links purged
|
* Any deleted screens will then have their routes/links purged
|
||||||
*
|
*
|
||||||
* Wrapped by {@link delete}
|
* Wrapped by {@link delete}
|
||||||
* @param {object | array} screens
|
* @param {Screen | Screen[]} screens
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
async deleteScreen(screen: Screen) {
|
async deleteScreen(screens: Screen | Screen[]) {
|
||||||
const screensToDelete = [screen]
|
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
||||||
// Build array of promises to speed up bulk deletions
|
// Build array of promises to speed up bulk deletions
|
||||||
let promises: Promise<any>[] = []
|
let promises: Promise<DeleteScreenResponse>[] = []
|
||||||
let deleteUrls: string[] = []
|
let deleteUrls: string[] = []
|
||||||
screensToDelete.forEach(screen => {
|
|
||||||
|
// In this instance _id will have been set
|
||||||
|
// Underline the expectation that _id and _rev will be set after filtering
|
||||||
|
screensToDelete
|
||||||
|
.filter(
|
||||||
|
(screen): screen is Screen & { _id: string; _rev: string } =>
|
||||||
|
!!screen._id || !!screen._rev
|
||||||
|
)
|
||||||
|
.forEach(screen => {
|
||||||
// Delete the screen
|
// Delete the screen
|
||||||
promises.push(API.deleteScreen(screen._id!, screen._rev!))
|
promises.push(API.deleteScreen(screen._id, screen._rev))
|
||||||
// Remove links to this screen
|
// Remove links to this screen
|
||||||
deleteUrls.push(screen.routing.route)
|
deleteUrls.push(screen.routing.route)
|
||||||
})
|
})
|
||||||
|
@ -375,11 +398,11 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
state.selectedScreenId &&
|
state.selectedScreenId &&
|
||||||
deletedIds.includes(state.selectedScreenId)
|
deletedIds.includes(state.selectedScreenId)
|
||||||
) {
|
) {
|
||||||
state.selectedScreenId = null
|
delete state.selectedScreenId
|
||||||
componentStore.update(state => ({
|
componentStore.update(state => {
|
||||||
...state,
|
delete state.selectedComponentId
|
||||||
selectedComponentId: null,
|
return state
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update routing
|
// Update routing
|
||||||
|
@ -390,7 +413,6 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
|
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -399,12 +421,11 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
* After a successful update, this method ensures that there is only
|
* After a successful update, this method ensures that there is only
|
||||||
* ONE home screen per user Role.
|
* ONE home screen per user Role.
|
||||||
*
|
*
|
||||||
* @param {object} screen
|
* @param {Screen} screen
|
||||||
* @param {string} name e.g "routing.homeScreen" or "showNavigation"
|
* @param {string} name e.g "routing.homeScreen" or "showNavigation"
|
||||||
* @param {any} value
|
* @param {any} value
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
async updateSetting(screen: Screen, name: string, value: string) {
|
async updateSetting(screen: Screen, name: string, value: any) {
|
||||||
if (!screen || !name) {
|
if (!screen || !name) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -461,11 +482,14 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
/**
|
/**
|
||||||
* Parse the entire screen component tree and ensure settings are valid
|
* Parse the entire screen component tree and ensure settings are valid
|
||||||
* and up-to-date. Ensures stability after a product update.
|
* and up-to-date. Ensures stability after a product update.
|
||||||
* @param {object} screen
|
* @param {Screen} screen
|
||||||
*/
|
*/
|
||||||
async enrichEmptySettings(screen: Screen) {
|
async enrichEmptySettings(screen: Screen) {
|
||||||
// Flatten the recursive component tree
|
// Flatten the recursive component tree
|
||||||
const components = findAllMatchingComponents(screen.props, (x: string) => x)
|
const components = findAllMatchingComponents(
|
||||||
|
screen.props,
|
||||||
|
(x: Component) => x
|
||||||
|
)
|
||||||
|
|
||||||
// Iterate over all components and run checks
|
// Iterate over all components and run checks
|
||||||
components.forEach(component => {
|
components.forEach(component => {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { get, writable } from "svelte/store"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
import { componentStore, appStore } from "@/stores/builder"
|
import { componentStore, appStore } from "@/stores/builder"
|
||||||
import { INITIAL_SCREENS_STATE, ScreenStore } from "@/stores/builder/screens"
|
import { initialScreenState, ScreenStore } from "@/stores/builder/screens"
|
||||||
import {
|
import {
|
||||||
getScreenFixture,
|
getScreenFixture,
|
||||||
getComponentFixture,
|
getComponentFixture,
|
||||||
|
@ -73,7 +73,7 @@ describe("Screens store", () => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
|
||||||
const screenStore = new ScreenStore()
|
const screenStore = new ScreenStore()
|
||||||
ctx.test = {
|
ctx.bb = {
|
||||||
get store() {
|
get store() {
|
||||||
return get(screenStore)
|
return get(screenStore)
|
||||||
},
|
},
|
||||||
|
@ -81,74 +81,76 @@ describe("Screens store", () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Create base screen store with defaults", ctx => {
|
it("Create base screen store with defaults", ({ bb }) => {
|
||||||
expect(ctx.test.store).toStrictEqual(INITIAL_SCREENS_STATE)
|
expect(bb.store).toStrictEqual(initialScreenState)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Syncs all screens from the app package", ctx => {
|
it("Syncs all screens from the app package", ({ bb }) => {
|
||||||
expect(ctx.test.store.screens.length).toBe(0)
|
expect(bb.store.screens.length).toBe(0)
|
||||||
|
|
||||||
const screens = Array(2)
|
const screens = Array(2)
|
||||||
.fill()
|
.fill()
|
||||||
.map(() => getScreenFixture().json())
|
.map(() => getScreenFixture().json())
|
||||||
|
|
||||||
ctx.test.screenStore.syncAppScreens({ screens })
|
bb.screenStore.syncAppScreens({ screens })
|
||||||
|
|
||||||
expect(ctx.test.store.screens).toStrictEqual(screens)
|
expect(bb.store.screens).toStrictEqual(screens)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Reset the screen store back to the default state", ctx => {
|
it("Reset the screen store back to the default state", ({ bb }) => {
|
||||||
expect(ctx.test.store.screens.length).toBe(0)
|
expect(bb.store.screens.length).toBe(0)
|
||||||
|
|
||||||
const screens = Array(2)
|
const screens = Array(2)
|
||||||
.fill()
|
.fill()
|
||||||
.map(() => getScreenFixture().json())
|
.map(() => getScreenFixture().json())
|
||||||
|
|
||||||
ctx.test.screenStore.syncAppScreens({ screens })
|
bb.screenStore.syncAppScreens({ screens })
|
||||||
expect(ctx.test.store.screens).toStrictEqual(screens)
|
expect(bb.store.screens).toStrictEqual(screens)
|
||||||
|
|
||||||
ctx.test.screenStore.update(state => ({
|
bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
selectedScreenId: screens[0]._id,
|
selectedScreenId: screens[0]._id,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
ctx.test.screenStore.reset()
|
bb.screenStore.reset()
|
||||||
|
|
||||||
expect(ctx.test.store).toStrictEqual(INITIAL_SCREENS_STATE)
|
expect(bb.store).toStrictEqual(initialScreenState)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Marks a valid screen as selected", ctx => {
|
it("Marks a valid screen as selected", ({ bb }) => {
|
||||||
const screens = Array(2)
|
const screens = Array(2)
|
||||||
.fill()
|
.fill()
|
||||||
.map(() => getScreenFixture().json())
|
.map(() => getScreenFixture().json())
|
||||||
|
|
||||||
ctx.test.screenStore.syncAppScreens({ screens })
|
bb.screenStore.syncAppScreens({ screens })
|
||||||
expect(ctx.test.store.screens.length).toBe(2)
|
expect(bb.store.screens.length).toBe(2)
|
||||||
|
|
||||||
ctx.test.screenStore.select(screens[0]._id)
|
bb.screenStore.select(screens[0]._id)
|
||||||
|
|
||||||
expect(ctx.test.store.selectedScreenId).toEqual(screens[0]._id)
|
expect(bb.store.selectedScreenId).toEqual(screens[0]._id)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Skip selecting a screen if it is not present", ctx => {
|
it("Skip selecting a screen if it is not present", ({ bb }) => {
|
||||||
const screens = Array(2)
|
const screens = Array(2)
|
||||||
.fill()
|
.fill()
|
||||||
.map(() => getScreenFixture().json())
|
.map(() => getScreenFixture().json())
|
||||||
|
|
||||||
ctx.test.screenStore.syncAppScreens({ screens })
|
bb.screenStore.syncAppScreens({ screens })
|
||||||
expect(ctx.test.store.screens.length).toBe(2)
|
expect(bb.store.screens.length).toBe(2)
|
||||||
|
|
||||||
ctx.test.screenStore.select("screen_abc")
|
bb.screenStore.select("screen_abc")
|
||||||
|
|
||||||
expect(ctx.test.store.selectedScreenId).toBeNull()
|
expect(bb.store.selectedScreenId).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Approve a valid empty screen config", ctx => {
|
it("Approve a valid empty screen config", ({ bb }) => {
|
||||||
const coreScreen = getScreenFixture()
|
const coreScreen = getScreenFixture()
|
||||||
ctx.test.screenStore.validate(coreScreen.json())
|
bb.screenStore.validate(coreScreen.json())
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Approve a valid screen config with one component and no illegal children", ctx => {
|
it("Approve a valid screen config with one component and no illegal children", ({
|
||||||
|
bb,
|
||||||
|
}) => {
|
||||||
const coreScreen = getScreenFixture()
|
const coreScreen = getScreenFixture()
|
||||||
const formBlock = getComponentFixture(`${COMP_PREFIX}/formblock`)
|
const formBlock = getComponentFixture(`${COMP_PREFIX}/formblock`)
|
||||||
|
|
||||||
|
@ -157,12 +159,12 @@ describe("Screens store", () => {
|
||||||
const defSpy = vi.spyOn(componentStore, "getDefinition")
|
const defSpy = vi.spyOn(componentStore, "getDefinition")
|
||||||
defSpy.mockReturnValueOnce(COMPONENT_DEFINITIONS.formblock)
|
defSpy.mockReturnValueOnce(COMPONENT_DEFINITIONS.formblock)
|
||||||
|
|
||||||
ctx.test.screenStore.validate(coreScreen.json())
|
bb.screenStore.validate(coreScreen.json())
|
||||||
|
|
||||||
expect(defSpy).toHaveBeenCalled()
|
expect(defSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Reject an attempt to nest invalid components", ctx => {
|
it("Reject an attempt to nest invalid components", ({ bb }) => {
|
||||||
const coreScreen = getScreenFixture()
|
const coreScreen = getScreenFixture()
|
||||||
|
|
||||||
const formOne = getComponentFixture(`${COMP_PREFIX}/form`)
|
const formOne = getComponentFixture(`${COMP_PREFIX}/form`)
|
||||||
|
@ -178,14 +180,14 @@ describe("Screens store", () => {
|
||||||
return defMap[comp]
|
return defMap[comp]
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(() => ctx.test.screenStore.validate(coreScreen.json())).toThrowError(
|
expect(() => bb.screenStore.validate(coreScreen.json())).toThrowError(
|
||||||
`You can't place a ${COMPONENT_DEFINITIONS.form.name} here`
|
`You can't place a ${COMPONENT_DEFINITIONS.form.name} here`
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(defSpy).toHaveBeenCalled()
|
expect(defSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Reject an attempt to deeply nest invalid components", ctx => {
|
it("Reject an attempt to deeply nest invalid components", ({ bb }) => {
|
||||||
const coreScreen = getScreenFixture()
|
const coreScreen = getScreenFixture()
|
||||||
|
|
||||||
const formOne = getComponentFixture(`${COMP_PREFIX}/form`)
|
const formOne = getComponentFixture(`${COMP_PREFIX}/form`)
|
||||||
|
@ -210,14 +212,16 @@ describe("Screens store", () => {
|
||||||
return defMap[comp]
|
return defMap[comp]
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(() => ctx.test.screenStore.validate(coreScreen.json())).toThrowError(
|
expect(() => bb.screenStore.validate(coreScreen.json())).toThrowError(
|
||||||
`You can't place a ${COMPONENT_DEFINITIONS.form.name} here`
|
`You can't place a ${COMPONENT_DEFINITIONS.form.name} here`
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(defSpy).toHaveBeenCalled()
|
expect(defSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Save a brand new screen and add it to the store. No validation", async ctx => {
|
it("Save a brand new screen and add it to the store. No validation", async ({
|
||||||
|
bb,
|
||||||
|
}) => {
|
||||||
const coreScreen = getScreenFixture()
|
const coreScreen = getScreenFixture()
|
||||||
const formOne = getComponentFixture(`${COMP_PREFIX}/form`)
|
const formOne = getComponentFixture(`${COMP_PREFIX}/form`)
|
||||||
|
|
||||||
|
@ -225,7 +229,7 @@ describe("Screens store", () => {
|
||||||
|
|
||||||
appStore.set({ features: { componentValidation: false } })
|
appStore.set({ features: { componentValidation: false } })
|
||||||
|
|
||||||
expect(ctx.test.store.screens.length).toBe(0)
|
expect(bb.store.screens.length).toBe(0)
|
||||||
|
|
||||||
const newDocId = getScreenDocId()
|
const newDocId = getScreenDocId()
|
||||||
const newDoc = { ...coreScreen.json(), _id: newDocId }
|
const newDoc = { ...coreScreen.json(), _id: newDocId }
|
||||||
|
@ -235,15 +239,15 @@ describe("Screens store", () => {
|
||||||
vi.spyOn(API, "fetchAppRoutes").mockResolvedValue({
|
vi.spyOn(API, "fetchAppRoutes").mockResolvedValue({
|
||||||
routes: [],
|
routes: [],
|
||||||
})
|
})
|
||||||
await ctx.test.screenStore.save(coreScreen.json())
|
await bb.screenStore.save(coreScreen.json())
|
||||||
|
|
||||||
expect(saveSpy).toHaveBeenCalled()
|
expect(saveSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
expect(ctx.test.store.screens.length).toBe(1)
|
expect(bb.store.screens.length).toBe(1)
|
||||||
|
|
||||||
expect(ctx.test.store.screens[0]).toStrictEqual(newDoc)
|
expect(bb.store.screens[0]).toStrictEqual(newDoc)
|
||||||
|
|
||||||
expect(ctx.test.store.selectedScreenId).toBe(newDocId)
|
expect(bb.store.selectedScreenId).toBe(newDocId)
|
||||||
|
|
||||||
// The new screen should be selected
|
// The new screen should be selected
|
||||||
expect(get(componentStore).selectedComponentId).toBe(
|
expect(get(componentStore).selectedComponentId).toBe(
|
||||||
|
@ -251,7 +255,7 @@ describe("Screens store", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Sync an updated screen to the screen store on save", async ctx => {
|
it("Sync an updated screen to the screen store on save", async ({ bb }) => {
|
||||||
const existingScreens = Array(4)
|
const existingScreens = Array(4)
|
||||||
.fill()
|
.fill()
|
||||||
.map(() => {
|
.map(() => {
|
||||||
|
@ -261,7 +265,7 @@ describe("Screens store", () => {
|
||||||
return screenDoc
|
return screenDoc
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.test.screenStore.update(state => ({
|
bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
screens: existingScreens.map(screen => screen.json()),
|
screens: existingScreens.map(screen => screen.json()),
|
||||||
}))
|
}))
|
||||||
|
@ -279,16 +283,18 @@ describe("Screens store", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Saved the existing screen having modified it.
|
// Saved the existing screen having modified it.
|
||||||
await ctx.test.screenStore.save(existingScreens[2].json())
|
await bb.screenStore.save(existingScreens[2].json())
|
||||||
|
|
||||||
expect(routeSpy).toHaveBeenCalled()
|
expect(routeSpy).toHaveBeenCalled()
|
||||||
expect(saveSpy).toHaveBeenCalled()
|
expect(saveSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
// On save, the screen is spliced back into the store with the saved content
|
// On save, the screen is spliced back into the store with the saved content
|
||||||
expect(ctx.test.store.screens[2]).toStrictEqual(existingScreens[2].json())
|
expect(bb.store.screens[2]).toStrictEqual(existingScreens[2].json())
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Sync API data to relevant stores on save. Updated plugins", async ctx => {
|
it("Sync API data to relevant stores on save. Updated plugins", async ({
|
||||||
|
bb,
|
||||||
|
}) => {
|
||||||
const coreScreen = getScreenFixture()
|
const coreScreen = getScreenFixture()
|
||||||
|
|
||||||
const newDocId = getScreenDocId()
|
const newDocId = getScreenDocId()
|
||||||
|
@ -318,7 +324,7 @@ describe("Screens store", () => {
|
||||||
routes: [],
|
routes: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
await ctx.test.screenStore.syncScreenData(newDoc)
|
await bb.screenStore.syncScreenData(newDoc)
|
||||||
|
|
||||||
expect(routeSpy).toHaveBeenCalled()
|
expect(routeSpy).toHaveBeenCalled()
|
||||||
expect(appPackageSpy).toHaveBeenCalled()
|
expect(appPackageSpy).toHaveBeenCalled()
|
||||||
|
@ -326,7 +332,9 @@ describe("Screens store", () => {
|
||||||
expect(get(appStore).usedPlugins).toStrictEqual(plugins)
|
expect(get(appStore).usedPlugins).toStrictEqual(plugins)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Sync API updates to relevant stores on save. Plugins unchanged", async ctx => {
|
it("Sync API updates to relevant stores on save. Plugins unchanged", async ({
|
||||||
|
bb,
|
||||||
|
}) => {
|
||||||
const coreScreen = getScreenFixture()
|
const coreScreen = getScreenFixture()
|
||||||
|
|
||||||
const newDocId = getScreenDocId()
|
const newDocId = getScreenDocId()
|
||||||
|
@ -343,7 +351,7 @@ describe("Screens store", () => {
|
||||||
routes: [],
|
routes: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
await ctx.test.screenStore.syncScreenData(newDoc)
|
await bb.screenStore.syncScreenData(newDoc)
|
||||||
|
|
||||||
expect(routeSpy).toHaveBeenCalled()
|
expect(routeSpy).toHaveBeenCalled()
|
||||||
expect(appPackageSpy).not.toHaveBeenCalled()
|
expect(appPackageSpy).not.toHaveBeenCalled()
|
||||||
|
@ -352,46 +360,48 @@ describe("Screens store", () => {
|
||||||
expect(get(appStore).usedPlugins).toStrictEqual([plugin])
|
expect(get(appStore).usedPlugins).toStrictEqual([plugin])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Proceed to patch if appropriate config are supplied", async ctx => {
|
it("Proceed to patch if appropriate config are supplied", async ({ bb }) => {
|
||||||
vi.spyOn(ctx.test.screenStore, "sequentialScreenPatch").mockImplementation(
|
vi.spyOn(bb.screenStore, "sequentialScreenPatch").mockImplementation(() => {
|
||||||
() => {
|
|
||||||
return false
|
return false
|
||||||
}
|
})
|
||||||
)
|
|
||||||
const noop = () => {}
|
const noop = () => {}
|
||||||
|
|
||||||
await ctx.test.screenStore.patch(noop, "test")
|
await bb.screenStore.patch(noop, "test")
|
||||||
expect(ctx.test.screenStore.sequentialScreenPatch).toHaveBeenCalledWith(
|
expect(bb.screenStore.sequentialScreenPatch).toHaveBeenCalledWith(
|
||||||
noop,
|
noop,
|
||||||
"test"
|
"test"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Return from the patch if all valid config are not present", async ctx => {
|
it("Return from the patch if all valid config are not present", async ({
|
||||||
vi.spyOn(ctx.test.screenStore, "sequentialScreenPatch")
|
bb,
|
||||||
await ctx.test.screenStore.patch()
|
}) => {
|
||||||
expect(ctx.test.screenStore.sequentialScreenPatch).not.toBeCalled()
|
vi.spyOn(bb.screenStore, "sequentialScreenPatch")
|
||||||
|
await bb.screenStore.patch()
|
||||||
|
expect(bb.screenStore.sequentialScreenPatch).not.toBeCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Acquire the currently selected screen on patch, if not specified", async ctx => {
|
it("Acquire the currently selected screen on patch, if not specified", async ({
|
||||||
vi.spyOn(ctx.test.screenStore, "sequentialScreenPatch")
|
bb,
|
||||||
await ctx.test.screenStore.patch()
|
}) => {
|
||||||
|
vi.spyOn(bb.screenStore, "sequentialScreenPatch")
|
||||||
|
await bb.screenStore.patch()
|
||||||
|
|
||||||
const noop = () => {}
|
const noop = () => {}
|
||||||
ctx.test.screenStore.update(state => ({
|
bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
selectedScreenId: "screen_123",
|
selectedScreenId: "screen_123",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
await ctx.test.screenStore.patch(noop)
|
await bb.screenStore.patch(noop)
|
||||||
expect(ctx.test.screenStore.sequentialScreenPatch).toHaveBeenCalledWith(
|
expect(bb.screenStore.sequentialScreenPatch).toHaveBeenCalledWith(
|
||||||
noop,
|
noop,
|
||||||
"screen_123"
|
"screen_123"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Used by the websocket
|
// Used by the websocket
|
||||||
it("Ignore a call to replace if no screenId is provided", ctx => {
|
it("Ignore a call to replace if no screenId is provided", ({ bb }) => {
|
||||||
const existingScreens = Array(4)
|
const existingScreens = Array(4)
|
||||||
.fill()
|
.fill()
|
||||||
.map(() => {
|
.map(() => {
|
||||||
|
@ -400,14 +410,16 @@ describe("Screens store", () => {
|
||||||
screenDoc._json._id = existingDocId
|
screenDoc._json._id = existingDocId
|
||||||
return screenDoc.json()
|
return screenDoc.json()
|
||||||
})
|
})
|
||||||
ctx.test.screenStore.syncAppScreens({ screens: existingScreens })
|
bb.screenStore.syncAppScreens({ screens: existingScreens })
|
||||||
|
|
||||||
ctx.test.screenStore.replace()
|
bb.screenStore.replace()
|
||||||
|
|
||||||
expect(ctx.test.store.screens).toStrictEqual(existingScreens)
|
expect(bb.store.screens).toStrictEqual(existingScreens)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Remove a screen from the store if a single screenId is supplied", ctx => {
|
it("Remove a screen from the store if a single screenId is supplied", ({
|
||||||
|
bb,
|
||||||
|
}) => {
|
||||||
const existingScreens = Array(4)
|
const existingScreens = Array(4)
|
||||||
.fill()
|
.fill()
|
||||||
.map(() => {
|
.map(() => {
|
||||||
|
@ -416,17 +428,17 @@ describe("Screens store", () => {
|
||||||
screenDoc._json._id = existingDocId
|
screenDoc._json._id = existingDocId
|
||||||
return screenDoc.json()
|
return screenDoc.json()
|
||||||
})
|
})
|
||||||
ctx.test.screenStore.syncAppScreens({ screens: existingScreens })
|
bb.screenStore.syncAppScreens({ screens: existingScreens })
|
||||||
|
|
||||||
ctx.test.screenStore.replace(existingScreens[1]._id)
|
bb.screenStore.replace(existingScreens[1]._id)
|
||||||
|
|
||||||
const filtered = existingScreens.filter(
|
const filtered = existingScreens.filter(
|
||||||
screen => screen._id != existingScreens[1]._id
|
screen => screen._id != existingScreens[1]._id
|
||||||
)
|
)
|
||||||
expect(ctx.test.store.screens).toStrictEqual(filtered)
|
expect(bb.store.screens).toStrictEqual(filtered)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Replace an existing screen with a new version of itself", ctx => {
|
it("Replace an existing screen with a new version of itself", ({ bb }) => {
|
||||||
const existingScreens = Array(4)
|
const existingScreens = Array(4)
|
||||||
.fill()
|
.fill()
|
||||||
.map(() => {
|
.map(() => {
|
||||||
|
@ -436,7 +448,7 @@ describe("Screens store", () => {
|
||||||
return screenDoc
|
return screenDoc
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.test.screenStore.update(state => ({
|
bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
screens: existingScreens.map(screen => screen.json()),
|
screens: existingScreens.map(screen => screen.json()),
|
||||||
}))
|
}))
|
||||||
|
@ -444,15 +456,14 @@ describe("Screens store", () => {
|
||||||
const formBlock = getComponentFixture(`${COMP_PREFIX}/formblock`)
|
const formBlock = getComponentFixture(`${COMP_PREFIX}/formblock`)
|
||||||
existingScreens[2].addChild(formBlock)
|
existingScreens[2].addChild(formBlock)
|
||||||
|
|
||||||
ctx.test.screenStore.replace(
|
bb.screenStore.replace(existingScreens[2]._id, existingScreens[2].json())
|
||||||
existingScreens[2]._id,
|
|
||||||
existingScreens[2].json()
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(ctx.test.store.screens.length).toBe(4)
|
expect(bb.store.screens.length).toBe(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Add a screen when attempting to replace one not present in the store", ctx => {
|
it("Add a screen when attempting to replace one not present in the store", ({
|
||||||
|
bb,
|
||||||
|
}) => {
|
||||||
const existingScreens = Array(4)
|
const existingScreens = Array(4)
|
||||||
.fill()
|
.fill()
|
||||||
.map(() => {
|
.map(() => {
|
||||||
|
@ -462,7 +473,7 @@ describe("Screens store", () => {
|
||||||
return screenDoc
|
return screenDoc
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.test.screenStore.update(state => ({
|
bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
screens: existingScreens.map(screen => screen.json()),
|
screens: existingScreens.map(screen => screen.json()),
|
||||||
}))
|
}))
|
||||||
|
@ -470,13 +481,13 @@ describe("Screens store", () => {
|
||||||
const newScreenDoc = getScreenFixture()
|
const newScreenDoc = getScreenFixture()
|
||||||
newScreenDoc._json._id = getScreenDocId()
|
newScreenDoc._json._id = getScreenDocId()
|
||||||
|
|
||||||
ctx.test.screenStore.replace(newScreenDoc._json._id, newScreenDoc.json())
|
bb.screenStore.replace(newScreenDoc._json._id, newScreenDoc.json())
|
||||||
|
|
||||||
expect(ctx.test.store.screens.length).toBe(5)
|
expect(bb.store.screens.length).toBe(5)
|
||||||
expect(ctx.test.store.screens[4]).toStrictEqual(newScreenDoc.json())
|
expect(bb.store.screens[4]).toStrictEqual(newScreenDoc.json())
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Delete a single screen and remove it from the store", async ctx => {
|
it("Delete a single screen and remove it from the store", async ({ bb }) => {
|
||||||
const existingScreens = Array(3)
|
const existingScreens = Array(3)
|
||||||
.fill()
|
.fill()
|
||||||
.map(() => {
|
.map(() => {
|
||||||
|
@ -486,14 +497,14 @@ describe("Screens store", () => {
|
||||||
return screenDoc
|
return screenDoc
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.test.screenStore.update(state => ({
|
bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
screens: existingScreens.map(screen => screen.json()),
|
screens: existingScreens.map(screen => screen.json()),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const deleteSpy = vi.spyOn(API, "deleteScreen")
|
const deleteSpy = vi.spyOn(API, "deleteScreen")
|
||||||
|
|
||||||
await ctx.test.screenStore.delete(existingScreens[2].json())
|
await bb.screenStore.delete(existingScreens[2].json())
|
||||||
|
|
||||||
vi.spyOn(API, "fetchAppRoutes").mockResolvedValue({
|
vi.spyOn(API, "fetchAppRoutes").mockResolvedValue({
|
||||||
routes: [],
|
routes: [],
|
||||||
|
@ -501,13 +512,15 @@ describe("Screens store", () => {
|
||||||
|
|
||||||
expect(deleteSpy).toBeCalled()
|
expect(deleteSpy).toBeCalled()
|
||||||
|
|
||||||
expect(ctx.test.store.screens.length).toBe(2)
|
expect(bb.store.screens.length).toBe(2)
|
||||||
|
|
||||||
// Just confirm that the routes at are being initialised
|
// Just confirm that the routes at are being initialised
|
||||||
expect(get(appStore).routes).toEqual([])
|
expect(get(appStore).routes).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Upon delete, reset selected screen and component ids if the screen was selected", async ctx => {
|
it("Upon delete, reset selected screen and component ids if the screen was selected", async ({
|
||||||
|
bb,
|
||||||
|
}) => {
|
||||||
const existingScreens = Array(3)
|
const existingScreens = Array(3)
|
||||||
.fill()
|
.fill()
|
||||||
.map(() => {
|
.map(() => {
|
||||||
|
@ -517,7 +530,7 @@ describe("Screens store", () => {
|
||||||
return screenDoc
|
return screenDoc
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.test.screenStore.update(state => ({
|
bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
screens: existingScreens.map(screen => screen.json()),
|
screens: existingScreens.map(screen => screen.json()),
|
||||||
selectedScreenId: existingScreens[2]._json._id,
|
selectedScreenId: existingScreens[2]._json._id,
|
||||||
|
@ -528,14 +541,16 @@ describe("Screens store", () => {
|
||||||
selectedComponentId: existingScreens[2]._json._id,
|
selectedComponentId: existingScreens[2]._json._id,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
await ctx.test.screenStore.delete(existingScreens[2].json())
|
await bb.screenStore.delete(existingScreens[2].json())
|
||||||
|
|
||||||
expect(ctx.test.store.screens.length).toBe(2)
|
expect(bb.store.screens.length).toBe(2)
|
||||||
expect(get(componentStore).selectedComponentId).toBeNull()
|
expect(get(componentStore).selectedComponentId).toBeUndefined()
|
||||||
expect(ctx.test.store.selectedScreenId).toBeNull()
|
expect(bb.store.selectedScreenId).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Delete multiple is not supported and should leave the store unchanged", async ctx => {
|
it("Delete multiple is not supported and should leave the store unchanged", async ({
|
||||||
|
bb,
|
||||||
|
}) => {
|
||||||
const existingScreens = Array(3)
|
const existingScreens = Array(3)
|
||||||
.fill()
|
.fill()
|
||||||
.map(() => {
|
.map(() => {
|
||||||
|
@ -547,7 +562,7 @@ describe("Screens store", () => {
|
||||||
|
|
||||||
const storeScreens = existingScreens.map(screen => screen.json())
|
const storeScreens = existingScreens.map(screen => screen.json())
|
||||||
|
|
||||||
ctx.test.screenStore.update(state => ({
|
bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
screens: existingScreens.map(screen => screen.json()),
|
screens: existingScreens.map(screen => screen.json()),
|
||||||
}))
|
}))
|
||||||
|
@ -556,42 +571,40 @@ describe("Screens store", () => {
|
||||||
|
|
||||||
const deleteSpy = vi.spyOn(API, "deleteScreen")
|
const deleteSpy = vi.spyOn(API, "deleteScreen")
|
||||||
|
|
||||||
await ctx.test.screenStore.delete(targets)
|
await bb.screenStore.delete(targets)
|
||||||
|
|
||||||
expect(deleteSpy).not.toHaveBeenCalled()
|
expect(deleteSpy).not.toHaveBeenCalled()
|
||||||
expect(ctx.test.store.screens.length).toBe(3)
|
expect(bb.store.screens.length).toBe(3)
|
||||||
expect(ctx.test.store.screens).toStrictEqual(storeScreens)
|
expect(bb.store.screens).toStrictEqual(storeScreens)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Update a screen setting", async ctx => {
|
it("Update a screen setting", async ({ bb }) => {
|
||||||
const screenDoc = getScreenFixture()
|
const screenDoc = getScreenFixture()
|
||||||
const existingDocId = getScreenDocId()
|
const existingDocId = getScreenDocId()
|
||||||
screenDoc._json._id = existingDocId
|
screenDoc._json._id = existingDocId
|
||||||
|
|
||||||
await ctx.test.screenStore.update(state => ({
|
await bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
screens: [screenDoc.json()],
|
screens: [screenDoc.json()],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const patchedDoc = screenDoc.json()
|
const patchedDoc = screenDoc.json()
|
||||||
const patchSpy = vi
|
const patchSpy = vi
|
||||||
.spyOn(ctx.test.screenStore, "patch")
|
.spyOn(bb.screenStore, "patch")
|
||||||
.mockImplementation(async patchFn => {
|
.mockImplementation(async patchFn => {
|
||||||
patchFn(patchedDoc)
|
patchFn(patchedDoc)
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
await ctx.test.screenStore.updateSetting(
|
await bb.screenStore.updateSetting(patchedDoc, "showNavigation", false)
|
||||||
patchedDoc,
|
|
||||||
"showNavigation",
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(patchSpy).toBeCalled()
|
expect(patchSpy).toBeCalled()
|
||||||
expect(patchedDoc.showNavigation).toBe(false)
|
expect(patchedDoc.showNavigation).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Ensure only one homescreen per role after updating setting. All screens same role", async ctx => {
|
it("Ensure only one homescreen per role after updating setting. All screens same role", async ({
|
||||||
|
bb,
|
||||||
|
}) => {
|
||||||
const existingScreens = Array(3)
|
const existingScreens = Array(3)
|
||||||
.fill()
|
.fill()
|
||||||
.map(() => {
|
.map(() => {
|
||||||
|
@ -611,23 +624,21 @@ describe("Screens store", () => {
|
||||||
// Set the 2nd screen as the home screen
|
// Set the 2nd screen as the home screen
|
||||||
storeScreens[1].routing.homeScreen = true
|
storeScreens[1].routing.homeScreen = true
|
||||||
|
|
||||||
await ctx.test.screenStore.update(state => ({
|
await bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
screens: storeScreens,
|
screens: storeScreens,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const patchSpy = vi
|
const patchSpy = vi
|
||||||
.spyOn(ctx.test.screenStore, "patch")
|
.spyOn(bb.screenStore, "patch")
|
||||||
.mockImplementation(async (patchFn, screenId) => {
|
.mockImplementation(async (patchFn, screenId) => {
|
||||||
const target = ctx.test.store.screens.find(
|
const target = bb.store.screens.find(screen => screen._id === screenId)
|
||||||
screen => screen._id === screenId
|
|
||||||
)
|
|
||||||
patchFn(target)
|
patchFn(target)
|
||||||
|
|
||||||
await ctx.test.screenStore.replace(screenId, target)
|
await bb.screenStore.replace(screenId, target)
|
||||||
})
|
})
|
||||||
|
|
||||||
await ctx.test.screenStore.updateSetting(
|
await bb.screenStore.updateSetting(
|
||||||
storeScreens[0],
|
storeScreens[0],
|
||||||
"routing.homeScreen",
|
"routing.homeScreen",
|
||||||
true
|
true
|
||||||
|
@ -637,13 +648,15 @@ describe("Screens store", () => {
|
||||||
expect(patchSpy).toBeCalledTimes(2)
|
expect(patchSpy).toBeCalledTimes(2)
|
||||||
|
|
||||||
// The new homescreen for BASIC
|
// The new homescreen for BASIC
|
||||||
expect(ctx.test.store.screens[0].routing.homeScreen).toBe(true)
|
expect(bb.store.screens[0].routing.homeScreen).toBe(true)
|
||||||
|
|
||||||
// The previous home screen for the BASIC role is now unset
|
// The previous home screen for the BASIC role is now unset
|
||||||
expect(ctx.test.store.screens[1].routing.homeScreen).toBe(false)
|
expect(bb.store.screens[1].routing.homeScreen).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Ensure only one homescreen per role when updating screen setting. Multiple screen roles", async ctx => {
|
it("Ensure only one homescreen per role when updating screen setting. Multiple screen roles", async ({
|
||||||
|
bb,
|
||||||
|
}) => {
|
||||||
const expectedRoles = [
|
const expectedRoles = [
|
||||||
Constants.Roles.BASIC,
|
Constants.Roles.BASIC,
|
||||||
Constants.Roles.POWER,
|
Constants.Roles.POWER,
|
||||||
|
@ -675,30 +688,24 @@ describe("Screens store", () => {
|
||||||
sorted[9].routing.homeScreen = true
|
sorted[9].routing.homeScreen = true
|
||||||
|
|
||||||
// Set screens state
|
// Set screens state
|
||||||
await ctx.test.screenStore.update(state => ({
|
await bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
screens: sorted,
|
screens: sorted,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const patchSpy = vi
|
const patchSpy = vi
|
||||||
.spyOn(ctx.test.screenStore, "patch")
|
.spyOn(bb.screenStore, "patch")
|
||||||
.mockImplementation(async (patchFn, screenId) => {
|
.mockImplementation(async (patchFn, screenId) => {
|
||||||
const target = ctx.test.store.screens.find(
|
const target = bb.store.screens.find(screen => screen._id === screenId)
|
||||||
screen => screen._id === screenId
|
|
||||||
)
|
|
||||||
patchFn(target)
|
patchFn(target)
|
||||||
|
|
||||||
await ctx.test.screenStore.replace(screenId, target)
|
await bb.screenStore.replace(screenId, target)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ADMIN homeScreen updated from 0 to 2
|
// ADMIN homeScreen updated from 0 to 2
|
||||||
await ctx.test.screenStore.updateSetting(
|
await bb.screenStore.updateSetting(sorted[2], "routing.homeScreen", true)
|
||||||
sorted[2],
|
|
||||||
"routing.homeScreen",
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
const results = ctx.test.store.screens.reduce((acc, screen) => {
|
const results = bb.store.screens.reduce((acc, screen) => {
|
||||||
if (screen.routing.homeScreen) {
|
if (screen.routing.homeScreen) {
|
||||||
acc[screen.routing.roleId] = acc[screen.routing.roleId] || []
|
acc[screen.routing.roleId] = acc[screen.routing.roleId] || []
|
||||||
acc[screen.routing.roleId].push(screen)
|
acc[screen.routing.roleId].push(screen)
|
||||||
|
@ -706,7 +713,7 @@ describe("Screens store", () => {
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
const screens = ctx.test.store.screens
|
const screens = bb.store.screens
|
||||||
// Should still only be one of each homescreen
|
// Should still only be one of each homescreen
|
||||||
expect(results[Constants.Roles.ADMIN].length).toBe(1)
|
expect(results[Constants.Roles.ADMIN].length).toBe(1)
|
||||||
expect(screens[2].routing.homeScreen).toBe(true)
|
expect(screens[2].routing.homeScreen).toBe(true)
|
||||||
|
@ -724,74 +731,80 @@ describe("Screens store", () => {
|
||||||
expect(patchSpy).toBeCalledTimes(2)
|
expect(patchSpy).toBeCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Sequential patch check. Exit if the screenId is not valid.", async ctx => {
|
it("Sequential patch check. Exit if the screenId is not valid.", async ({
|
||||||
|
bb,
|
||||||
|
}) => {
|
||||||
const screenDoc = getScreenFixture()
|
const screenDoc = getScreenFixture()
|
||||||
const existingDocId = getScreenDocId()
|
const existingDocId = getScreenDocId()
|
||||||
screenDoc._json._id = existingDocId
|
screenDoc._json._id = existingDocId
|
||||||
|
|
||||||
const original = screenDoc.json()
|
const original = screenDoc.json()
|
||||||
|
|
||||||
await ctx.test.screenStore.update(state => ({
|
await bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
screens: [original],
|
screens: [original],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const saveSpy = vi
|
const saveSpy = vi
|
||||||
.spyOn(ctx.test.screenStore, "save")
|
.spyOn(bb.screenStore, "save")
|
||||||
.mockImplementation(async () => {
|
.mockImplementation(async () => {
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
// A screen with this Id does not exist
|
// A screen with this Id does not exist
|
||||||
await ctx.test.screenStore.sequentialScreenPatch(() => {}, "123")
|
await bb.screenStore.sequentialScreenPatch(() => {}, "123")
|
||||||
expect(saveSpy).not.toBeCalled()
|
expect(saveSpy).not.toBeCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Sequential patch check. Exit if the patchFn result is false", async ctx => {
|
it("Sequential patch check. Exit if the patchFn result is false", async ({
|
||||||
|
bb,
|
||||||
|
}) => {
|
||||||
const screenDoc = getScreenFixture()
|
const screenDoc = getScreenFixture()
|
||||||
const existingDocId = getScreenDocId()
|
const existingDocId = getScreenDocId()
|
||||||
screenDoc._json._id = existingDocId
|
screenDoc._json._id = existingDocId
|
||||||
|
|
||||||
const original = screenDoc.json()
|
const original = screenDoc.json()
|
||||||
// Set screens state
|
// Set screens state
|
||||||
await ctx.test.screenStore.update(state => ({
|
await bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
screens: [original],
|
screens: [original],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const saveSpy = vi
|
const saveSpy = vi
|
||||||
.spyOn(ctx.test.screenStore, "save")
|
.spyOn(bb.screenStore, "save")
|
||||||
.mockImplementation(async () => {
|
.mockImplementation(async () => {
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
// Returning false from the patch will abort the save
|
// Returning false from the patch will abort the save
|
||||||
await ctx.test.screenStore.sequentialScreenPatch(() => {
|
await bb.screenStore.sequentialScreenPatch(() => {
|
||||||
return false
|
return false
|
||||||
}, "123")
|
}, "123")
|
||||||
|
|
||||||
expect(saveSpy).not.toBeCalled()
|
expect(saveSpy).not.toBeCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Sequential patch check. Patch applied and save requested", async ctx => {
|
it("Sequential patch check. Patch applied and save requested", async ({
|
||||||
|
bb,
|
||||||
|
}) => {
|
||||||
const screenDoc = getScreenFixture()
|
const screenDoc = getScreenFixture()
|
||||||
const existingDocId = getScreenDocId()
|
const existingDocId = getScreenDocId()
|
||||||
screenDoc._json._id = existingDocId
|
screenDoc._json._id = existingDocId
|
||||||
|
|
||||||
const original = screenDoc.json()
|
const original = screenDoc.json()
|
||||||
|
|
||||||
await ctx.test.screenStore.update(state => ({
|
await bb.screenStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
screens: [original],
|
screens: [original],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const saveSpy = vi
|
const saveSpy = vi
|
||||||
.spyOn(ctx.test.screenStore, "save")
|
.spyOn(bb.screenStore, "save")
|
||||||
.mockImplementation(async () => {
|
.mockImplementation(async () => {
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
await ctx.test.screenStore.sequentialScreenPatch(screen => {
|
await bb.screenStore.sequentialScreenPatch(screen => {
|
||||||
screen.name = "updated"
|
screen.name = "updated"
|
||||||
}, existingDocId)
|
}, existingDocId)
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,9 @@ import {
|
||||||
Automation,
|
Automation,
|
||||||
Datasource,
|
Datasource,
|
||||||
Role,
|
Role,
|
||||||
Screen,
|
|
||||||
Table,
|
Table,
|
||||||
UIUser,
|
UIUser,
|
||||||
|
Screen,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
export const createBuilderWebsocket = (appId: string) => {
|
export const createBuilderWebsocket = (appId: string) => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { it, expect, describe, beforeEach, vi } from "vitest"
|
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||||
import { createAdminStore } from "./admin"
|
import { AdminStore } from "./admin"
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { auth } from "@/stores/portal"
|
import { auth } from "@/stores/portal"
|
||||||
|
@ -46,16 +46,7 @@ describe("admin store", () => {
|
||||||
ctx.writableReturn = { update: vi.fn(), subscribe: vi.fn() }
|
ctx.writableReturn = { update: vi.fn(), subscribe: vi.fn() }
|
||||||
writable.mockReturnValue(ctx.writableReturn)
|
writable.mockReturnValue(ctx.writableReturn)
|
||||||
|
|
||||||
ctx.returnedStore = createAdminStore()
|
ctx.returnedStore = new AdminStore()
|
||||||
})
|
|
||||||
|
|
||||||
it("returns the created store", ctx => {
|
|
||||||
expect(ctx.returnedStore).toEqual({
|
|
||||||
subscribe: expect.toBe(ctx.writableReturn.subscribe),
|
|
||||||
init: expect.toBeFunc(),
|
|
||||||
unload: expect.toBeFunc(),
|
|
||||||
getChecklist: expect.toBeFunc(),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("init method", () => {
|
describe("init method", () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { auth } from "@/stores/portal"
|
import { auth } from "@/stores/portal"
|
||||||
import { banner } from "@budibase/bbui"
|
import { banner } from "@budibase/bbui"
|
||||||
|
@ -7,15 +7,17 @@ import {
|
||||||
GetEnvironmentResponse,
|
GetEnvironmentResponse,
|
||||||
SystemStatusResponse,
|
SystemStatusResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import { BudiStore } from "../BudiStore"
|
||||||
|
|
||||||
interface PortalAdminStore extends GetEnvironmentResponse {
|
interface AdminState extends GetEnvironmentResponse {
|
||||||
loaded: boolean
|
loaded: boolean
|
||||||
checklist?: ConfigChecklistResponse
|
checklist?: ConfigChecklistResponse
|
||||||
status?: SystemStatusResponse
|
status?: SystemStatusResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAdminStore() {
|
export class AdminStore extends BudiStore<AdminState> {
|
||||||
const admin = writable<PortalAdminStore>({
|
constructor() {
|
||||||
|
super({
|
||||||
loaded: false,
|
loaded: false,
|
||||||
multiTenancy: false,
|
multiTenancy: false,
|
||||||
cloud: false,
|
cloud: false,
|
||||||
|
@ -24,25 +26,25 @@ export function createAdminStore() {
|
||||||
offlineMode: false,
|
offlineMode: false,
|
||||||
maintenance: [],
|
maintenance: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
async function init() {
|
|
||||||
await getChecklist()
|
|
||||||
await getEnvironment()
|
|
||||||
// enable system status checks in the cloud
|
|
||||||
if (get(admin).cloud) {
|
|
||||||
await getSystemStatus()
|
|
||||||
checkStatus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
admin.update(store => {
|
async init() {
|
||||||
|
await this.getChecklist()
|
||||||
|
await this.getEnvironment()
|
||||||
|
// enable system status checks in the cloud
|
||||||
|
if (get(this.store).cloud) {
|
||||||
|
await this.getSystemStatus()
|
||||||
|
this.checkStatus()
|
||||||
|
}
|
||||||
|
this.update(store => {
|
||||||
store.loaded = true
|
store.loaded = true
|
||||||
return store
|
return store
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getEnvironment() {
|
async getEnvironment() {
|
||||||
const environment = await API.getEnvironment()
|
const environment = await API.getEnvironment()
|
||||||
admin.update(store => {
|
this.update(store => {
|
||||||
store.multiTenancy = environment.multiTenancy
|
store.multiTenancy = environment.multiTenancy
|
||||||
store.cloud = environment.cloud
|
store.cloud = environment.cloud
|
||||||
store.disableAccountPortal = environment.disableAccountPortal
|
store.disableAccountPortal = environment.disableAccountPortal
|
||||||
|
@ -56,43 +58,36 @@ export function createAdminStore() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkStatus = async () => {
|
async checkStatus() {
|
||||||
const health = get(admin)?.status?.health
|
const health = get(this.store).status?.health
|
||||||
if (!health?.passing) {
|
if (!health?.passing) {
|
||||||
await banner.showStatus()
|
await banner.showStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSystemStatus() {
|
async getSystemStatus() {
|
||||||
const status = await API.getSystemStatus()
|
const status = await API.getSystemStatus()
|
||||||
admin.update(store => {
|
this.update(store => {
|
||||||
store.status = status
|
store.status = status
|
||||||
return store
|
return store
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getChecklist() {
|
async getChecklist() {
|
||||||
const tenantId = get(auth).tenantId
|
const tenantId = get(auth).tenantId
|
||||||
const checklist = await API.getChecklist(tenantId)
|
const checklist = await API.getChecklist(tenantId)
|
||||||
admin.update(store => {
|
this.update(store => {
|
||||||
store.checklist = checklist
|
store.checklist = checklist
|
||||||
return store
|
return store
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function unload() {
|
unload() {
|
||||||
admin.update(store => {
|
this.update(store => {
|
||||||
store.loaded = false
|
store.loaded = false
|
||||||
return store
|
return store
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe: admin.subscribe,
|
|
||||||
init,
|
|
||||||
unload,
|
|
||||||
getChecklist,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const admin = createAdminStore()
|
export const admin = new AdminStore()
|
||||||
|
|
|
@ -13,7 +13,7 @@ interface PortalAuditLogsStore {
|
||||||
logs?: SearchAuditLogsResponse
|
logs?: SearchAuditLogsResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuditLogsStore extends BudiStore<PortalAuditLogsStore> {
|
class AuditLogsStore extends BudiStore<PortalAuditLogsStore> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({})
|
super({})
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,8 +121,8 @@ class AuthStore extends BudiStore<PortalAuthStore> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(username: string, password: string) {
|
async login(username: string, password: string, targetTenantId?: string) {
|
||||||
const tenantId = get(this.store).tenantId
|
const tenantId = targetTenantId || get(this.store).tenantId
|
||||||
await API.logIn(tenantId, username, password)
|
await API.logIn(tenantId, username, password)
|
||||||
await this.getSelf()
|
await this.getSelf()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,31 @@
|
||||||
import { writable } from "svelte/store"
|
import { BudiStore } from "../BudiStore"
|
||||||
|
|
||||||
type GotoFuncType = (path: string) => void
|
type GotoFuncType = (path: string) => void
|
||||||
|
|
||||||
interface PortalNavigationStore {
|
interface NavigationState {
|
||||||
initialisated: boolean
|
initialisated: boolean
|
||||||
goto: GotoFuncType
|
goto: GotoFuncType
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createNavigationStore() {
|
class NavigationStore extends BudiStore<NavigationState> {
|
||||||
const store = writable<PortalNavigationStore>({
|
constructor() {
|
||||||
|
super({
|
||||||
initialisated: false,
|
initialisated: false,
|
||||||
goto: undefined as any,
|
goto: undefined as any,
|
||||||
})
|
})
|
||||||
const { set, subscribe } = store
|
}
|
||||||
|
|
||||||
const init = (gotoFunc: GotoFuncType) => {
|
init(gotoFunc: GotoFuncType) {
|
||||||
if (typeof gotoFunc !== "function") {
|
if (typeof gotoFunc !== "function") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`gotoFunc must be a function, found a "${typeof gotoFunc}" instead`
|
`gotoFunc must be a function, found a "${typeof gotoFunc}" instead`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
this.set({
|
||||||
set({
|
|
||||||
initialisated: true,
|
initialisated: true,
|
||||||
goto: gotoFunc,
|
goto: gotoFunc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe,
|
|
||||||
actions: {
|
|
||||||
init,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const navigation = createNavigationStore()
|
export const navigation = new NavigationStore()
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { writable } from "svelte/store"
|
|
||||||
import { API } from "@/api"
|
|
||||||
|
|
||||||
export function templatesStore() {
|
|
||||||
const { subscribe, set } = writable([])
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe,
|
|
||||||
load: async () => {
|
|
||||||
const templates = await API.getAppTemplates()
|
|
||||||
set(templates)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const templates = templatesStore()
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { API } from "@/api"
|
||||||
|
import { BudiStore } from "../BudiStore"
|
||||||
|
import { TemplateMetadata } from "@budibase/types"
|
||||||
|
|
||||||
|
class TemplateStore extends BudiStore<TemplateMetadata[]> {
|
||||||
|
constructor() {
|
||||||
|
super([])
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
const templates = await API.getAppTemplates()
|
||||||
|
this.set(templates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const templates = new TemplateStore()
|
|
@ -1,45 +0,0 @@
|
||||||
import { createLocalStorageStore } from "@budibase/frontend-core"
|
|
||||||
import { get } from "svelte/store"
|
|
||||||
|
|
||||||
export const createTemporalStore = () => {
|
|
||||||
const initialValue = {}
|
|
||||||
|
|
||||||
const localStorageKey = `bb-temporal`
|
|
||||||
const store = createLocalStorageStore(localStorageKey, initialValue)
|
|
||||||
|
|
||||||
const setExpiring = (key, data, duration) => {
|
|
||||||
const updated = {
|
|
||||||
...data,
|
|
||||||
expiry: Date.now() + duration * 1000,
|
|
||||||
}
|
|
||||||
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
[key]: updated,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const getExpiring = key => {
|
|
||||||
const entry = get(store)[key]
|
|
||||||
if (!entry) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const currentExpiry = entry.expiry
|
|
||||||
if (currentExpiry < Date.now()) {
|
|
||||||
store.update(state => {
|
|
||||||
delete state[key]
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
return entry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe: store.subscribe,
|
|
||||||
actions: { setExpiring, getExpiring },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const temporalStore = createTemporalStore()
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { BudiStore, PersistenceType } from "../BudiStore"
|
||||||
|
|
||||||
|
type TemporalItem = Record<string, any> & { expiry: number }
|
||||||
|
type TemporalState = Record<string, TemporalItem>
|
||||||
|
|
||||||
|
class TemporalStore extends BudiStore<TemporalState> {
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
persistence: {
|
||||||
|
key: "bb-temporal",
|
||||||
|
type: PersistenceType.LOCAL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setExpiring = (
|
||||||
|
key: string,
|
||||||
|
data: Record<string, any>,
|
||||||
|
durationSeconds: number
|
||||||
|
) => {
|
||||||
|
const updated: TemporalItem = {
|
||||||
|
...data,
|
||||||
|
expiry: Date.now() + durationSeconds * 1000,
|
||||||
|
}
|
||||||
|
this.update(state => ({
|
||||||
|
...state,
|
||||||
|
[key]: updated,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
getExpiring(key: string) {
|
||||||
|
const entry = get(this.store)[key]
|
||||||
|
if (!entry) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const currentExpiry = entry.expiry
|
||||||
|
if (currentExpiry < Date.now()) {
|
||||||
|
this.update(state => {
|
||||||
|
delete state[key]
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const temporalStore = new TemporalStore()
|
|
@ -1,37 +0,0 @@
|
||||||
import { createLocalStorageStore } from "@budibase/frontend-core"
|
|
||||||
import { derived } from "svelte/store"
|
|
||||||
import {
|
|
||||||
DefaultBuilderTheme,
|
|
||||||
ensureValidTheme,
|
|
||||||
getThemeClassNames,
|
|
||||||
ThemeOptions,
|
|
||||||
ThemeClassPrefix,
|
|
||||||
} from "@budibase/shared-core"
|
|
||||||
|
|
||||||
export const getThemeStore = () => {
|
|
||||||
const themeElement = document.documentElement
|
|
||||||
const initialValue = {
|
|
||||||
theme: DefaultBuilderTheme,
|
|
||||||
}
|
|
||||||
const store = createLocalStorageStore("bb-theme", initialValue)
|
|
||||||
const derivedStore = derived(store, $store => ({
|
|
||||||
...$store,
|
|
||||||
theme: ensureValidTheme($store.theme, DefaultBuilderTheme),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Update theme class when store changes
|
|
||||||
derivedStore.subscribe(({ theme }) => {
|
|
||||||
const classNames = getThemeClassNames(theme).split(" ")
|
|
||||||
ThemeOptions.forEach(option => {
|
|
||||||
const className = `${ThemeClassPrefix}${option.id}`
|
|
||||||
themeElement.classList.toggle(className, classNames.includes(className))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
...store,
|
|
||||||
subscribe: derivedStore.subscribe,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const themeStore = getThemeStore()
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { derived, Writable } from "svelte/store"
|
||||||
|
import {
|
||||||
|
DefaultBuilderTheme,
|
||||||
|
ensureValidTheme,
|
||||||
|
getThemeClassNames,
|
||||||
|
ThemeOptions,
|
||||||
|
ThemeClassPrefix,
|
||||||
|
} from "@budibase/shared-core"
|
||||||
|
import { Theme } from "@budibase/types"
|
||||||
|
import { DerivedBudiStore, PersistenceType } from "../BudiStore"
|
||||||
|
|
||||||
|
interface ThemeState {
|
||||||
|
theme: Theme
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThemeStore extends DerivedBudiStore<ThemeState, ThemeState> {
|
||||||
|
constructor() {
|
||||||
|
const makeDerivedStore = (store: Writable<ThemeState>) => {
|
||||||
|
return derived(store, $store => ({
|
||||||
|
...$store,
|
||||||
|
theme: ensureValidTheme($store.theme, DefaultBuilderTheme),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
super({ theme: DefaultBuilderTheme }, makeDerivedStore, {
|
||||||
|
persistence: {
|
||||||
|
key: "bb-theme",
|
||||||
|
type: PersistenceType.LOCAL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update theme class when store changes
|
||||||
|
this.subscribe(({ theme }) => {
|
||||||
|
const classNames = getThemeClassNames(theme).split(" ")
|
||||||
|
ThemeOptions.forEach(option => {
|
||||||
|
const className = `${ThemeClassPrefix}${option.id}`
|
||||||
|
document.documentElement.classList.toggle(
|
||||||
|
className,
|
||||||
|
classNames.includes(className)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const themeStore = new ThemeStore()
|
|
@ -103,6 +103,7 @@
|
||||||
let settingsDefinition
|
let settingsDefinition
|
||||||
let settingsDefinitionMap
|
let settingsDefinitionMap
|
||||||
let missingRequiredSettings = false
|
let missingRequiredSettings = false
|
||||||
|
let componentErrors = false
|
||||||
|
|
||||||
// Temporary styles which can be added in the app preview for things like
|
// Temporary styles which can be added in the app preview for things like
|
||||||
// DND. We clear these whenever a new instance is received.
|
// DND. We clear these whenever a new instance is received.
|
||||||
|
@ -137,16 +138,21 @@
|
||||||
|
|
||||||
// Derive definition properties which can all be optional, so need to be
|
// Derive definition properties which can all be optional, so need to be
|
||||||
// coerced to booleans
|
// coerced to booleans
|
||||||
|
$: componentErrors = instance?._meta?.errors
|
||||||
$: hasChildren = !!definition?.hasChildren
|
$: hasChildren = !!definition?.hasChildren
|
||||||
$: showEmptyState = definition?.showEmptyState !== false
|
$: showEmptyState = definition?.showEmptyState !== false
|
||||||
$: hasMissingRequiredSettings = missingRequiredSettings?.length > 0
|
$: hasMissingRequiredSettings = missingRequiredSettings?.length > 0
|
||||||
$: editable = !!definition?.editable && !hasMissingRequiredSettings
|
$: editable = !!definition?.editable && !hasMissingRequiredSettings
|
||||||
|
$: hasComponentErrors = componentErrors?.length > 0
|
||||||
$: requiredAncestors = definition?.requiredAncestors || []
|
$: requiredAncestors = definition?.requiredAncestors || []
|
||||||
$: missingRequiredAncestors = requiredAncestors.filter(
|
$: missingRequiredAncestors = requiredAncestors.filter(
|
||||||
ancestor => !$component.ancestors.includes(`${BudibasePrefix}${ancestor}`)
|
ancestor => !$component.ancestors.includes(`${BudibasePrefix}${ancestor}`)
|
||||||
)
|
)
|
||||||
$: hasMissingRequiredAncestors = missingRequiredAncestors?.length > 0
|
$: hasMissingRequiredAncestors = missingRequiredAncestors?.length > 0
|
||||||
$: errorState = hasMissingRequiredSettings || hasMissingRequiredAncestors
|
$: errorState =
|
||||||
|
hasMissingRequiredSettings ||
|
||||||
|
hasMissingRequiredAncestors ||
|
||||||
|
hasComponentErrors
|
||||||
|
|
||||||
// Interactive components can be selected, dragged and highlighted inside
|
// Interactive components can be selected, dragged and highlighted inside
|
||||||
// the builder preview
|
// the builder preview
|
||||||
|
@ -692,6 +698,7 @@
|
||||||
<ComponentErrorState
|
<ComponentErrorState
|
||||||
{missingRequiredSettings}
|
{missingRequiredSettings}
|
||||||
{missingRequiredAncestors}
|
{missingRequiredAncestors}
|
||||||
|
{componentErrors}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
| { key: string; label: string }[]
|
| { key: string; label: string }[]
|
||||||
| undefined
|
| undefined
|
||||||
export let missingRequiredAncestors: string[] | undefined
|
export let missingRequiredAncestors: string[] | undefined
|
||||||
|
export let componentErrors: string[] | undefined
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable, builderStore } = getContext("sdk")
|
const { styleable, builderStore } = getContext("sdk")
|
||||||
|
@ -15,6 +16,7 @@
|
||||||
$: styles = { ...$component.styles, normal: {}, custom: null, empty: true }
|
$: styles = { ...$component.styles, normal: {}, custom: null, empty: true }
|
||||||
$: requiredSetting = missingRequiredSettings?.[0]
|
$: requiredSetting = missingRequiredSettings?.[0]
|
||||||
$: requiredAncestor = missingRequiredAncestors?.[0]
|
$: requiredAncestor = missingRequiredAncestors?.[0]
|
||||||
|
$: errorMessage = componentErrors?.[0]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $builderStore.inBuilder}
|
{#if $builderStore.inBuilder}
|
||||||
|
@ -23,6 +25,8 @@
|
||||||
<Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" />
|
<Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" />
|
||||||
{#if requiredAncestor}
|
{#if requiredAncestor}
|
||||||
<MissingRequiredAncestor {requiredAncestor} />
|
<MissingRequiredAncestor {requiredAncestor} />
|
||||||
|
{:else if errorMessage}
|
||||||
|
{errorMessage}
|
||||||
{:else if requiredSetting}
|
{:else if requiredSetting}
|
||||||
<MissingRequiredSetting {requiredSetting} />
|
<MissingRequiredSetting {requiredSetting} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -34,7 +38,7 @@
|
||||||
.component-placeholder {
|
.component-placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--spectrum-global-color-gray-600);
|
color: var(--spectrum-global-color-gray-600);
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
|
|
|
@ -43,6 +43,7 @@ const loadBudibase = async () => {
|
||||||
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
|
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
|
||||||
location: window["##BUDIBASE_LOCATION##"],
|
location: window["##BUDIBASE_LOCATION##"],
|
||||||
snippets: window["##BUDIBASE_SNIPPETS##"],
|
snippets: window["##BUDIBASE_SNIPPETS##"],
|
||||||
|
componentErrors: window["##BUDIBASE_COMPONENT_ERRORS##"],
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set app ID - this window flag is set by both the preview and the real
|
// Set app ID - this window flag is set by both the preview and the real
|
||||||
|
|
|
@ -19,6 +19,7 @@ const createBuilderStore = () => {
|
||||||
eventResolvers: {},
|
eventResolvers: {},
|
||||||
metadata: null,
|
metadata: null,
|
||||||
snippets: null,
|
snippets: null,
|
||||||
|
componentErrors: {},
|
||||||
|
|
||||||
// Legacy - allow the builder to specify a layout
|
// Legacy - allow the builder to specify a layout
|
||||||
layout: null,
|
layout: null,
|
||||||
|
|
|
@ -42,6 +42,14 @@ const createScreenStore = () => {
|
||||||
if ($builderStore.layout) {
|
if ($builderStore.layout) {
|
||||||
activeLayout = $builderStore.layout
|
activeLayout = $builderStore.layout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attach meta
|
||||||
|
const errors = $builderStore.componentErrors || {}
|
||||||
|
const attachComponentMeta = component => {
|
||||||
|
component._meta = { errors: errors[component._id] || [] }
|
||||||
|
component._children?.forEach(attachComponentMeta)
|
||||||
|
}
|
||||||
|
attachComponentMeta(activeScreen.props)
|
||||||
} else {
|
} else {
|
||||||
// Find the correct screen by matching the current route
|
// Find the correct screen by matching the current route
|
||||||
screens = $appStore.screens || []
|
screens = $appStore.screens || []
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
// TODO: datasource and defitions are unions of the different implementations. At this point, the datasource does not know what type is being used, and the assignations will cause TS exceptions. Casting it "as any" for now. This should be fixed improving the type usages.
|
|
||||||
|
|
||||||
import { derived, get, Readable, Writable } from "svelte/store"
|
import { derived, get, Readable, Writable } from "svelte/store"
|
||||||
import {
|
import {
|
||||||
DataFetchDefinition,
|
DataFetchDefinition,
|
||||||
|
@ -10,12 +8,10 @@ import { enrichSchemaWithRelColumns, memo } from "../../../utils"
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
import {
|
import {
|
||||||
SaveRowRequest,
|
SaveRowRequest,
|
||||||
SaveTableRequest,
|
|
||||||
UIDatasource,
|
UIDatasource,
|
||||||
UIFieldMutation,
|
UIFieldMutation,
|
||||||
UIFieldSchema,
|
UIFieldSchema,
|
||||||
UIRow,
|
UIRow,
|
||||||
UpdateViewRequest,
|
|
||||||
ViewV2Type,
|
ViewV2Type,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { Store as StoreContext, BaseStoreProps } from "."
|
import { Store as StoreContext, BaseStoreProps } from "."
|
||||||
|
@ -79,7 +75,7 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
|
||||||
const schema = derived(definition, $definition => {
|
const schema = derived(definition, $definition => {
|
||||||
const schema: Record<string, any> | undefined = getDatasourceSchema({
|
const schema: Record<string, any> | undefined = getDatasourceSchema({
|
||||||
API,
|
API,
|
||||||
datasource: get(datasource) as any, // TODO: see line 1
|
datasource: get(datasource),
|
||||||
definition: $definition ?? undefined,
|
definition: $definition ?? undefined,
|
||||||
})
|
})
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
|
@ -137,7 +133,7 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
|
||||||
let type = $datasource?.type
|
let type = $datasource?.type
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
if (type === "provider") {
|
if (type === "provider") {
|
||||||
type = ($datasource as any).value?.datasource?.type // TODO: see line 1
|
type = ($datasource as any).value?.datasource?.type
|
||||||
}
|
}
|
||||||
// Handle calculation views
|
// Handle calculation views
|
||||||
if (
|
if (
|
||||||
|
@ -196,15 +192,13 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
|
||||||
const refreshDefinition = async () => {
|
const refreshDefinition = async () => {
|
||||||
const def = await getDatasourceDefinition({
|
const def = await getDatasourceDefinition({
|
||||||
API,
|
API,
|
||||||
datasource: get(datasource) as any, // TODO: see line 1
|
datasource: get(datasource),
|
||||||
})
|
})
|
||||||
definition.set(def as any) // TODO: see line 1
|
definition.set(def ?? null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saves the datasource definition
|
// Saves the datasource definition
|
||||||
const saveDefinition = async (
|
const saveDefinition = async (newDefinition: DataFetchDefinition) => {
|
||||||
newDefinition: SaveTableRequest | UpdateViewRequest
|
|
||||||
) => {
|
|
||||||
// Update local state
|
// Update local state
|
||||||
const originalDefinition = get(definition)
|
const originalDefinition = get(definition)
|
||||||
definition.set(newDefinition)
|
definition.set(newDefinition)
|
||||||
|
@ -245,7 +239,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
|
||||||
delete newDefinition.schema[column].default
|
delete newDefinition.schema[column].default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return await saveDefinition(newDefinition as any) // TODO: see line 1
|
return await saveDefinition(newDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a schema mutation for a single field
|
// Adds a schema mutation for a single field
|
||||||
|
@ -321,7 +315,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
|
||||||
await saveDefinition({
|
await saveDefinition({
|
||||||
...$definition,
|
...$definition,
|
||||||
schema: newSchema,
|
schema: newSchema,
|
||||||
} as any) // TODO: see line 1
|
})
|
||||||
resetSchemaMutations()
|
resetSchemaMutations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default class ViewFetch extends BaseDataFetch<ViewV1Datasource, Table> {
|
||||||
|
|
||||||
getSchema(definition: Table) {
|
getSchema(definition: Table) {
|
||||||
const { datasource } = this.options
|
const { datasource } = this.options
|
||||||
return definition?.views?.[datasource.name]?.schema
|
return definition?.views?.[datasource?.name]?.schema
|
||||||
}
|
}
|
||||||
|
|
||||||
async getData() {
|
async getData() {
|
||||||
|
|
|
@ -101,12 +101,12 @@ export const fetchData = <
|
||||||
|
|
||||||
// Creates an empty fetch instance with no datasource configured, so no data
|
// Creates an empty fetch instance with no datasource configured, so no data
|
||||||
// will initially be loaded
|
// will initially be loaded
|
||||||
const createEmptyFetchInstance = ({
|
const createEmptyFetchInstance = <T extends DataFetchDatasource>({
|
||||||
API,
|
API,
|
||||||
datasource,
|
datasource,
|
||||||
}: {
|
}: {
|
||||||
API: APIClient
|
API: APIClient
|
||||||
datasource: DataFetchDatasource
|
datasource: T
|
||||||
}) => {
|
}) => {
|
||||||
const handler = DataFetchMap[datasource?.type]
|
const handler = DataFetchMap[datasource?.type]
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
|
@ -114,7 +114,7 @@ const createEmptyFetchInstance = ({
|
||||||
}
|
}
|
||||||
return new handler({
|
return new handler({
|
||||||
API,
|
API,
|
||||||
datasource: null as never,
|
datasource: datasource as any,
|
||||||
query: null as any,
|
query: null as any,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const sleep = (ms: number) =>
|
||||||
* Utility to wrap an async function and ensure all invocations happen
|
* Utility to wrap an async function and ensure all invocations happen
|
||||||
* sequentially.
|
* sequentially.
|
||||||
* @param fn the async function to run
|
* @param fn the async function to run
|
||||||
* @return {Promise} a sequential version of the function
|
* @return {Function} a sequential version of the function
|
||||||
*/
|
*/
|
||||||
export const sequential = <
|
export const sequential = <
|
||||||
TReturn,
|
TReturn,
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"../shared-core/src",
|
"../shared-core/src",
|
||||||
"../string-templates/src"
|
"../string-templates/src"
|
||||||
],
|
],
|
||||||
"ext": "js,ts,json,svelte",
|
"ext": "js,ts,json,svelte,hbs",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"**/*.spec.ts",
|
"**/*.spec.ts",
|
||||||
"**/*.spec.js",
|
"**/*.spec.js",
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as triggers from "../../automations/triggers"
|
||||||
import { sdk as coreSdk } from "@budibase/shared-core"
|
import { sdk as coreSdk } from "@budibase/shared-core"
|
||||||
import { DocumentType } from "../../db/utils"
|
import { DocumentType } from "../../db/utils"
|
||||||
import { updateTestHistory, removeDeprecated } from "../../automations/utils"
|
import { updateTestHistory, removeDeprecated } from "../../automations/utils"
|
||||||
import { setTestFlag, clearTestFlag } from "../../utilities/redis"
|
import { withTestFlag } from "../../utilities/redis"
|
||||||
import { context, cache, events, db as dbCore } from "@budibase/backend-core"
|
import { context, cache, events, db as dbCore } from "@budibase/backend-core"
|
||||||
import { automations, features } from "@budibase/pro"
|
import { automations, features } from "@budibase/pro"
|
||||||
import {
|
import {
|
||||||
|
@ -231,24 +231,25 @@ export async function test(
|
||||||
ctx: UserCtx<TestAutomationRequest, TestAutomationResponse>
|
ctx: UserCtx<TestAutomationRequest, TestAutomationResponse>
|
||||||
) {
|
) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
let automation = await db.get<Automation>(ctx.params.id)
|
const automation = await db.tryGet<Automation>(ctx.params.id)
|
||||||
await setTestFlag(automation._id!)
|
if (!automation) {
|
||||||
const testInput = prepareTestInput(ctx.request.body)
|
ctx.throw(404, `Automation ${ctx.params.id} not found`)
|
||||||
const response = await triggers.externalTrigger(
|
}
|
||||||
|
|
||||||
|
const { request, appId } = ctx
|
||||||
|
const { body } = request
|
||||||
|
|
||||||
|
ctx.body = await withTestFlag(automation._id!, async () => {
|
||||||
|
const occurredAt = new Date().getTime()
|
||||||
|
await updateTestHistory(appId, automation, { ...body, occurredAt })
|
||||||
|
|
||||||
|
const user = sdk.users.getUserContextBindings(ctx.user)
|
||||||
|
return await triggers.externalTrigger(
|
||||||
automation,
|
automation,
|
||||||
{
|
{ ...prepareTestInput(body), appId, user },
|
||||||
...testInput,
|
|
||||||
appId: ctx.appId,
|
|
||||||
user: sdk.users.getUserContextBindings(ctx.user),
|
|
||||||
},
|
|
||||||
{ getResponses: true }
|
{ getResponses: true }
|
||||||
)
|
)
|
||||||
// save a test history run
|
|
||||||
await updateTestHistory(ctx.appId, automation, {
|
|
||||||
...ctx.request.body,
|
|
||||||
occurredAt: new Date().getTime(),
|
|
||||||
})
|
})
|
||||||
await clearTestFlag(automation._id!)
|
|
||||||
ctx.body = response
|
|
||||||
await events.automation.tested(automation)
|
await events.automation.tested(automation)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,8 @@
|
||||||
hiddenComponentIds,
|
hiddenComponentIds,
|
||||||
usedPlugins,
|
usedPlugins,
|
||||||
location,
|
location,
|
||||||
snippets
|
snippets,
|
||||||
|
componentErrors
|
||||||
} = parsed
|
} = parsed
|
||||||
|
|
||||||
// Set some flags so the app knows we're in the builder
|
// Set some flags so the app knows we're in the builder
|
||||||
|
@ -91,6 +92,7 @@
|
||||||
window["##BUDIBASE_USED_PLUGINS##"] = usedPlugins
|
window["##BUDIBASE_USED_PLUGINS##"] = usedPlugins
|
||||||
window["##BUDIBASE_LOCATION##"] = location
|
window["##BUDIBASE_LOCATION##"] = location
|
||||||
window["##BUDIBASE_SNIPPETS##"] = snippets
|
window["##BUDIBASE_SNIPPETS##"] = snippets
|
||||||
|
window['##BUDIBASE_COMPONENT_ERRORS##'] = componentErrors
|
||||||
|
|
||||||
// Initialise app
|
// Initialise app
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -5,8 +5,11 @@ import {
|
||||||
sendAutomationAttachmentsToStorage,
|
sendAutomationAttachmentsToStorage,
|
||||||
} from "../automationUtils"
|
} from "../automationUtils"
|
||||||
import { buildCtx } from "./utils"
|
import { buildCtx } from "./utils"
|
||||||
import { CreateRowStepInputs, CreateRowStepOutputs } from "@budibase/types"
|
import {
|
||||||
import { EventEmitter } from "events"
|
ContextEmitter,
|
||||||
|
CreateRowStepInputs,
|
||||||
|
CreateRowStepOutputs,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
export async function run({
|
export async function run({
|
||||||
inputs,
|
inputs,
|
||||||
|
@ -15,7 +18,7 @@ export async function run({
|
||||||
}: {
|
}: {
|
||||||
inputs: CreateRowStepInputs
|
inputs: CreateRowStepInputs
|
||||||
appId: string
|
appId: string
|
||||||
emitter: EventEmitter
|
emitter: ContextEmitter
|
||||||
}): Promise<CreateRowStepOutputs> {
|
}): Promise<CreateRowStepOutputs> {
|
||||||
if (inputs.row == null || inputs.row.tableId == null) {
|
if (inputs.row == null || inputs.row.tableId == null) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import { EventEmitter } from "events"
|
|
||||||
import { destroy } from "../../api/controllers/row"
|
import { destroy } from "../../api/controllers/row"
|
||||||
import { buildCtx } from "./utils"
|
import { buildCtx } from "./utils"
|
||||||
import { getError } from "../automationUtils"
|
import { getError } from "../automationUtils"
|
||||||
import { DeleteRowStepInputs, DeleteRowStepOutputs } from "@budibase/types"
|
import {
|
||||||
|
ContextEmitter,
|
||||||
|
DeleteRowStepInputs,
|
||||||
|
DeleteRowStepOutputs,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
export async function run({
|
export async function run({
|
||||||
inputs,
|
inputs,
|
||||||
|
@ -11,7 +14,7 @@ export async function run({
|
||||||
}: {
|
}: {
|
||||||
inputs: DeleteRowStepInputs
|
inputs: DeleteRowStepInputs
|
||||||
appId: string
|
appId: string
|
||||||
emitter: EventEmitter
|
emitter: ContextEmitter
|
||||||
}): Promise<DeleteRowStepOutputs> {
|
}): Promise<DeleteRowStepOutputs> {
|
||||||
if (inputs.id == null) {
|
if (inputs.id == null) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { EventEmitter } from "events"
|
|
||||||
import * as queryController from "../../api/controllers/query"
|
import * as queryController from "../../api/controllers/query"
|
||||||
import { buildCtx } from "./utils"
|
import { buildCtx } from "./utils"
|
||||||
import * as automationUtils from "../automationUtils"
|
import * as automationUtils from "../automationUtils"
|
||||||
import {
|
import {
|
||||||
|
ContextEmitter,
|
||||||
ExecuteQueryStepInputs,
|
ExecuteQueryStepInputs,
|
||||||
ExecuteQueryStepOutputs,
|
ExecuteQueryStepOutputs,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
@ -14,7 +14,7 @@ export async function run({
|
||||||
}: {
|
}: {
|
||||||
inputs: ExecuteQueryStepInputs
|
inputs: ExecuteQueryStepInputs
|
||||||
appId: string
|
appId: string
|
||||||
emitter: EventEmitter
|
emitter: ContextEmitter
|
||||||
}): Promise<ExecuteQueryStepOutputs> {
|
}): Promise<ExecuteQueryStepOutputs> {
|
||||||
if (inputs.query == null) {
|
if (inputs.query == null) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -2,10 +2,10 @@ import * as scriptController from "../../api/controllers/script"
|
||||||
import { buildCtx } from "./utils"
|
import { buildCtx } from "./utils"
|
||||||
import * as automationUtils from "../automationUtils"
|
import * as automationUtils from "../automationUtils"
|
||||||
import {
|
import {
|
||||||
|
ContextEmitter,
|
||||||
ExecuteScriptStepInputs,
|
ExecuteScriptStepInputs,
|
||||||
ExecuteScriptStepOutputs,
|
ExecuteScriptStepOutputs,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { EventEmitter } from "events"
|
|
||||||
|
|
||||||
export async function run({
|
export async function run({
|
||||||
inputs,
|
inputs,
|
||||||
|
@ -16,7 +16,7 @@ export async function run({
|
||||||
inputs: ExecuteScriptStepInputs
|
inputs: ExecuteScriptStepInputs
|
||||||
appId: string
|
appId: string
|
||||||
context: object
|
context: object
|
||||||
emitter: EventEmitter
|
emitter: ContextEmitter
|
||||||
}): Promise<ExecuteScriptStepOutputs> {
|
}): Promise<ExecuteScriptStepOutputs> {
|
||||||
if (inputs.code == null) {
|
if (inputs.code == null) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import { EventEmitter } from "events"
|
|
||||||
import * as rowController from "../../api/controllers/row"
|
import * as rowController from "../../api/controllers/row"
|
||||||
import * as automationUtils from "../automationUtils"
|
import * as automationUtils from "../automationUtils"
|
||||||
import { buildCtx } from "./utils"
|
import { buildCtx } from "./utils"
|
||||||
import { UpdateRowStepInputs, UpdateRowStepOutputs } from "@budibase/types"
|
import {
|
||||||
|
ContextEmitter,
|
||||||
|
UpdateRowStepInputs,
|
||||||
|
UpdateRowStepOutputs,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
export async function run({
|
export async function run({
|
||||||
inputs,
|
inputs,
|
||||||
|
@ -11,7 +14,7 @@ export async function run({
|
||||||
}: {
|
}: {
|
||||||
inputs: UpdateRowStepInputs
|
inputs: UpdateRowStepInputs
|
||||||
appId: string
|
appId: string
|
||||||
emitter: EventEmitter
|
emitter: ContextEmitter
|
||||||
}): Promise<UpdateRowStepOutputs> {
|
}): Promise<UpdateRowStepOutputs> {
|
||||||
if (inputs.rowId == null || inputs.row == null) {
|
if (inputs.rowId == null || inputs.row == null) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EventEmitter } from "events"
|
import { ContextEmitter } from "@budibase/types"
|
||||||
|
|
||||||
export async function getFetchResponse(fetched: any) {
|
export async function getFetchResponse(fetched: any) {
|
||||||
let status = fetched.status,
|
let status = fetched.status,
|
||||||
|
@ -22,7 +22,7 @@ export async function getFetchResponse(fetched: any) {
|
||||||
// opts can contain, body, params and version
|
// opts can contain, body, params and version
|
||||||
export function buildCtx(
|
export function buildCtx(
|
||||||
appId: string,
|
appId: string,
|
||||||
emitter?: EventEmitter | null,
|
emitter?: ContextEmitter | null,
|
||||||
opts: any = {}
|
opts: any = {}
|
||||||
) {
|
) {
|
||||||
const ctx: any = {
|
const ctx: any = {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { v4 as uuidv4 } from "uuid"
|
import { v4 as uuidv4 } from "uuid"
|
||||||
import { testAutomation } from "../../../api/routes/tests/utilities/TestFunctions"
|
|
||||||
import { BUILTIN_ACTION_DEFINITIONS } from "../../actions"
|
import { BUILTIN_ACTION_DEFINITIONS } from "../../actions"
|
||||||
import { TRIGGER_DEFINITIONS } from "../../triggers"
|
import { TRIGGER_DEFINITIONS } from "../../triggers"
|
||||||
import {
|
import {
|
||||||
|
@ -7,7 +6,6 @@ import {
|
||||||
AppActionTriggerOutputs,
|
AppActionTriggerOutputs,
|
||||||
Automation,
|
Automation,
|
||||||
AutomationActionStepId,
|
AutomationActionStepId,
|
||||||
AutomationResults,
|
|
||||||
AutomationStep,
|
AutomationStep,
|
||||||
AutomationStepInputs,
|
AutomationStepInputs,
|
||||||
AutomationTrigger,
|
AutomationTrigger,
|
||||||
|
@ -24,6 +22,7 @@ import {
|
||||||
ExecuteQueryStepInputs,
|
ExecuteQueryStepInputs,
|
||||||
ExecuteScriptStepInputs,
|
ExecuteScriptStepInputs,
|
||||||
FilterStepInputs,
|
FilterStepInputs,
|
||||||
|
isDidNotTriggerResponse,
|
||||||
LoopStepInputs,
|
LoopStepInputs,
|
||||||
OpenAIStepInputs,
|
OpenAIStepInputs,
|
||||||
QueryRowsStepInputs,
|
QueryRowsStepInputs,
|
||||||
|
@ -36,6 +35,7 @@ import {
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
ServerLogStepInputs,
|
ServerLogStepInputs,
|
||||||
SmtpEmailStepInputs,
|
SmtpEmailStepInputs,
|
||||||
|
TestAutomationRequest,
|
||||||
UpdateRowStepInputs,
|
UpdateRowStepInputs,
|
||||||
WebhookTriggerInputs,
|
WebhookTriggerInputs,
|
||||||
WebhookTriggerOutputs,
|
WebhookTriggerOutputs,
|
||||||
|
@ -279,7 +279,7 @@ class StepBuilder extends BaseStepBuilder {
|
||||||
class AutomationBuilder extends BaseStepBuilder {
|
class AutomationBuilder extends BaseStepBuilder {
|
||||||
private automationConfig: Automation
|
private automationConfig: Automation
|
||||||
private config: TestConfiguration
|
private config: TestConfiguration
|
||||||
private triggerOutputs: any
|
private triggerOutputs: TriggerOutputs
|
||||||
private triggerSet = false
|
private triggerSet = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -398,21 +398,19 @@ class AutomationBuilder extends BaseStepBuilder {
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
const automation = await this.save()
|
const automation = await this.save()
|
||||||
const results = await testAutomation(
|
const response = await this.config.api.automation.test(
|
||||||
this.config,
|
automation._id!,
|
||||||
automation,
|
this.triggerOutputs as TestAutomationRequest
|
||||||
this.triggerOutputs
|
|
||||||
)
|
)
|
||||||
return this.processResults(results)
|
|
||||||
|
if (isDidNotTriggerResponse(response)) {
|
||||||
|
throw new Error(response.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
private processResults(results: {
|
response.steps.shift()
|
||||||
body: AutomationResults
|
|
||||||
}): AutomationResults {
|
|
||||||
results.body.steps.shift()
|
|
||||||
return {
|
return {
|
||||||
trigger: results.body.trigger,
|
trigger: response.trigger,
|
||||||
steps: results.body.steps,
|
steps: response.steps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
AutomationRowEvent,
|
AutomationRowEvent,
|
||||||
UserBindings,
|
UserBindings,
|
||||||
AutomationResults,
|
AutomationResults,
|
||||||
|
DidNotTriggerResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { executeInThread } from "../threads/automation"
|
import { executeInThread } from "../threads/automation"
|
||||||
import { dataFilters, sdk } from "@budibase/shared-core"
|
import { dataFilters, sdk } from "@budibase/shared-core"
|
||||||
|
@ -33,14 +34,6 @@ const JOB_OPTS = {
|
||||||
import * as automationUtils from "../automations/automationUtils"
|
import * as automationUtils from "../automations/automationUtils"
|
||||||
import { doesTableExist } from "../sdk/app/tables/getters"
|
import { doesTableExist } from "../sdk/app/tables/getters"
|
||||||
|
|
||||||
type DidNotTriggerResponse = {
|
|
||||||
outputs: {
|
|
||||||
success: false
|
|
||||||
status: AutomationStatus.STOPPED
|
|
||||||
}
|
|
||||||
message: AutomationStoppedReason.TRIGGER_FILTER_NOT_MET
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAllAutomations() {
|
async function getAllAutomations() {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
let automations = await db.allDocs<Automation>(
|
let automations = await db.allDocs<Automation>(
|
||||||
|
@ -156,14 +149,26 @@ export function isAutomationResults(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function externalTrigger(
|
interface AutomationTriggerParams {
|
||||||
automation: Automation,
|
|
||||||
params: {
|
|
||||||
fields: Record<string, any>
|
fields: Record<string, any>
|
||||||
timeout?: number
|
timeout?: number
|
||||||
appId?: string
|
appId?: string
|
||||||
user?: UserBindings
|
user?: UserBindings
|
||||||
},
|
}
|
||||||
|
|
||||||
|
export async function externalTrigger(
|
||||||
|
automation: Automation,
|
||||||
|
params: AutomationTriggerParams,
|
||||||
|
options: { getResponses: true }
|
||||||
|
): Promise<AutomationResults | DidNotTriggerResponse>
|
||||||
|
export async function externalTrigger(
|
||||||
|
automation: Automation,
|
||||||
|
params: AutomationTriggerParams,
|
||||||
|
options?: { getResponses: false }
|
||||||
|
): Promise<AutomationJob | DidNotTriggerResponse>
|
||||||
|
export async function externalTrigger(
|
||||||
|
automation: Automation,
|
||||||
|
params: AutomationTriggerParams,
|
||||||
{ getResponses }: { getResponses?: boolean } = {}
|
{ getResponses }: { getResponses?: boolean } = {}
|
||||||
): Promise<AutomationResults | DidNotTriggerResponse | AutomationJob> {
|
): Promise<AutomationResults | DidNotTriggerResponse | AutomationJob> {
|
||||||
if (automation.disabled) {
|
if (automation.disabled) {
|
||||||
|
|
|
@ -4,12 +4,17 @@ import {
|
||||||
JsTimeoutError,
|
JsTimeoutError,
|
||||||
setJSRunner,
|
setJSRunner,
|
||||||
setOnErrorLog,
|
setOnErrorLog,
|
||||||
|
setTestingBackendJS,
|
||||||
} from "@budibase/string-templates"
|
} from "@budibase/string-templates"
|
||||||
import { context, logging } from "@budibase/backend-core"
|
import { context, logging } from "@budibase/backend-core"
|
||||||
import tracer from "dd-trace"
|
import tracer from "dd-trace"
|
||||||
import { IsolatedVM } from "./vm"
|
import { IsolatedVM } from "./vm"
|
||||||
|
|
||||||
export function init() {
|
export function init() {
|
||||||
|
// enforce that if we're using isolated-VM runner then we are running backend JS
|
||||||
|
if (env.isTest()) {
|
||||||
|
setTestingBackendJS()
|
||||||
|
}
|
||||||
setJSRunner((js: string, ctx: Record<string, any>) => {
|
setJSRunner((js: string, ctx: Record<string, any>) => {
|
||||||
return tracer.trace("runJS", {}, () => {
|
return tracer.trace("runJS", {}, () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
import { Automation, FetchAutomationResponse } from "@budibase/types"
|
import {
|
||||||
|
Automation,
|
||||||
|
FetchAutomationResponse,
|
||||||
|
TestAutomationRequest,
|
||||||
|
TestAutomationResponse,
|
||||||
|
} from "@budibase/types"
|
||||||
import { Expectations, TestAPI } from "./base"
|
import { Expectations, TestAPI } from "./base"
|
||||||
|
|
||||||
export class AutomationAPI extends TestAPI {
|
export class AutomationAPI extends TestAPI {
|
||||||
|
@ -33,4 +38,18 @@ export class AutomationAPI extends TestAPI {
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test = async (
|
||||||
|
id: string,
|
||||||
|
body: TestAutomationRequest,
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<TestAutomationResponse> => {
|
||||||
|
return await this._post<TestAutomationResponse>(
|
||||||
|
`/api/automations/${id}/test`,
|
||||||
|
{
|
||||||
|
body,
|
||||||
|
expectations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
LoopStep,
|
LoopStep,
|
||||||
UserBindings,
|
UserBindings,
|
||||||
isBasicSearchOperator,
|
isBasicSearchOperator,
|
||||||
|
ContextEmitter,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
AutomationContext,
|
AutomationContext,
|
||||||
|
@ -71,6 +72,24 @@ function getLoopIterations(loopStep: LoopStep) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function enrichBaseContext(context: Record<string, any>) {
|
||||||
|
context.env = await sdkUtils.getEnvironmentVariables()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { config } = await configs.getSettingsConfigDoc()
|
||||||
|
context.settings = {
|
||||||
|
url: config.platformUrl,
|
||||||
|
logo: config.logoUrl,
|
||||||
|
company: config.company,
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// if settings doc doesn't exist, make the settings blank
|
||||||
|
context.settings = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The automation orchestrator is a class responsible for executing automations.
|
* The automation orchestrator is a class responsible for executing automations.
|
||||||
* It handles the context of the automation and makes sure each step gets the correct
|
* It handles the context of the automation and makes sure each step gets the correct
|
||||||
|
@ -80,7 +99,7 @@ class Orchestrator {
|
||||||
private chainCount: number
|
private chainCount: number
|
||||||
private appId: string
|
private appId: string
|
||||||
private automation: Automation
|
private automation: Automation
|
||||||
private emitter: any
|
private emitter: ContextEmitter
|
||||||
private context: AutomationContext
|
private context: AutomationContext
|
||||||
private job: Job
|
private job: Job
|
||||||
private loopStepOutputs: LoopStep[]
|
private loopStepOutputs: LoopStep[]
|
||||||
|
@ -270,20 +289,9 @@ class Orchestrator {
|
||||||
appId: this.appId,
|
appId: this.appId,
|
||||||
automationId: this.automation._id,
|
automationId: this.automation._id,
|
||||||
})
|
})
|
||||||
this.context.env = await sdkUtils.getEnvironmentVariables()
|
|
||||||
this.context.user = this.currentUser
|
|
||||||
|
|
||||||
try {
|
await enrichBaseContext(this.context)
|
||||||
const { config } = await configs.getSettingsConfigDoc()
|
this.context.user = this.currentUser
|
||||||
this.context.settings = {
|
|
||||||
url: config.platformUrl,
|
|
||||||
logo: config.logoUrl,
|
|
||||||
company: config.company,
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// if settings doc doesn't exist, make the settings blank
|
|
||||||
this.context.settings = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata
|
let metadata
|
||||||
|
|
||||||
|
|
|
@ -58,30 +58,14 @@ export function checkSlashesInUrl(url: string) {
|
||||||
export async function updateEntityMetadata(
|
export async function updateEntityMetadata(
|
||||||
type: string,
|
type: string,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
updateFn: any
|
updateFn: (metadata: Document) => Document
|
||||||
) {
|
) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const id = generateMetadataID(type, entityId)
|
const id = generateMetadataID(type, entityId)
|
||||||
// read it to see if it exists, we'll overwrite it no matter what
|
const metadata = updateFn((await db.tryGet(id)) || {})
|
||||||
let rev, metadata: Document
|
|
||||||
try {
|
|
||||||
const oldMetadata = await db.get<any>(id)
|
|
||||||
rev = oldMetadata._rev
|
|
||||||
metadata = updateFn(oldMetadata)
|
|
||||||
} catch (err) {
|
|
||||||
rev = null
|
|
||||||
metadata = updateFn({})
|
|
||||||
}
|
|
||||||
metadata._id = id
|
metadata._id = id
|
||||||
if (rev) {
|
|
||||||
metadata._rev = rev
|
|
||||||
}
|
|
||||||
const response = await db.put(metadata)
|
const response = await db.put(metadata)
|
||||||
return {
|
return { ...metadata, _id: id, _rev: response.rev }
|
||||||
...metadata,
|
|
||||||
_id: id,
|
|
||||||
_rev: response.rev,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveEntityMetadata(
|
export async function saveEntityMetadata(
|
||||||
|
@ -89,26 +73,17 @@ export async function saveEntityMetadata(
|
||||||
entityId: string,
|
entityId: string,
|
||||||
metadata: Document
|
metadata: Document
|
||||||
): Promise<Document> {
|
): Promise<Document> {
|
||||||
return updateEntityMetadata(type, entityId, () => {
|
return updateEntityMetadata(type, entityId, () => metadata)
|
||||||
return metadata
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEntityMetadata(type: string, entityId: string) {
|
export async function deleteEntityMetadata(type: string, entityId: string) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const id = generateMetadataID(type, entityId)
|
const id = generateMetadataID(type, entityId)
|
||||||
let rev
|
const metadata = await db.tryGet(id)
|
||||||
try {
|
if (!metadata) {
|
||||||
const metadata = await db.get<any>(id)
|
return
|
||||||
if (metadata) {
|
|
||||||
rev = metadata._rev
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// don't need to error if it doesn't exist
|
|
||||||
}
|
|
||||||
if (id && rev) {
|
|
||||||
await db.remove(id, rev)
|
|
||||||
}
|
}
|
||||||
|
await db.remove(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function escapeDangerousCharacters(string: string) {
|
export function escapeDangerousCharacters(string: string) {
|
||||||
|
|
|
@ -89,17 +89,22 @@ export async function setDebounce(id: string, seconds: number) {
|
||||||
await debounceClient.store(id, "debouncing", seconds)
|
await debounceClient.store(id, "debouncing", seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setTestFlag(id: string) {
|
|
||||||
await flagClient.store(id, { testing: true }, AUTOMATION_TEST_FLAG_SECONDS)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkTestFlag(id: string) {
|
export async function checkTestFlag(id: string) {
|
||||||
const flag = await flagClient?.get(id)
|
const flag = await flagClient?.get(id)
|
||||||
return !!(flag && flag.testing)
|
return !!(flag && flag.testing)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearTestFlag(id: string) {
|
export async function withTestFlag<R>(id: string, fn: () => Promise<R>) {
|
||||||
|
// TODO(samwho): this has a bit of a problem where if 2 automations are tested
|
||||||
|
// at the same time, the second one will overwrite the first one's flag. We
|
||||||
|
// should instead use an atomic counter and only clear the flag when the
|
||||||
|
// counter reaches 0.
|
||||||
|
await flagClient.store(id, { testing: true }, AUTOMATION_TEST_FLAG_SECONDS)
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} finally {
|
||||||
await devAppClient.delete(id)
|
await devAppClient.delete(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSocketPubSubClients() {
|
export function getSocketPubSubClients() {
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/handlebars-helpers": "^0.13.2",
|
"@budibase/handlebars-helpers": "^0.13.2",
|
||||||
|
"@budibase/vm-browserify": "^1.1.4",
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"lodash.clonedeep": "^4.5.0"
|
"lodash.clonedeep": "^4.5.0"
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
function isJest() {
|
||||||
|
return (
|
||||||
|
process.env.NODE_ENV === "jest" ||
|
||||||
|
(process.env.JEST_WORKER_ID != null &&
|
||||||
|
process.env.JEST_WORKER_ID !== "null")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTest() {
|
||||||
|
return isJest()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isJSAllowed = () => {
|
||||||
|
return process && !process.env.NO_JS
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isTestingBackendJS = () => {
|
||||||
|
return process && process.env.BACKEND_JS
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setTestingBackendJS = () => {
|
||||||
|
process.env.BACKEND_JS = "1"
|
||||||
|
}
|
|
@ -1,9 +1,16 @@
|
||||||
import { atob, isBackendService, isJSAllowed } from "../utilities"
|
import {
|
||||||
|
atob,
|
||||||
|
frontendWrapJS,
|
||||||
|
isBackendService,
|
||||||
|
isJSAllowed,
|
||||||
|
} from "../utilities"
|
||||||
import { LITERAL_MARKER } from "../helpers/constants"
|
import { LITERAL_MARKER } from "../helpers/constants"
|
||||||
import { getJsHelperList } from "./list"
|
import { getJsHelperList } from "./list"
|
||||||
import { iifeWrapper } from "../iife"
|
import { iifeWrapper } from "../iife"
|
||||||
import { JsTimeoutError, UserScriptError } from "../errors"
|
import { JsTimeoutError, UserScriptError } from "../errors"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import { Log, LogType } from "../types"
|
||||||
|
import { isTest } from "../environment"
|
||||||
|
|
||||||
// The method of executing JS scripts depends on the bundle being built.
|
// The method of executing JS scripts depends on the bundle being built.
|
||||||
// This setter is used in the entrypoint (either index.js or index.mjs).
|
// This setter is used in the entrypoint (either index.js or index.mjs).
|
||||||
|
@ -81,7 +88,7 @@ export function processJS(handlebars: string, context: any) {
|
||||||
|
|
||||||
let clonedContext: Record<string, any>
|
let clonedContext: Record<string, any>
|
||||||
if (isBackendService()) {
|
if (isBackendService()) {
|
||||||
// On the backned, values are copied across the isolated-vm boundary and
|
// On the backend, values are copied across the isolated-vm boundary and
|
||||||
// so we don't need to do any cloning here. This does create a fundamental
|
// so we don't need to do any cloning here. This does create a fundamental
|
||||||
// difference in how JS executes on the frontend vs the backend, e.g.
|
// difference in how JS executes on the frontend vs the backend, e.g.
|
||||||
// consider this snippet:
|
// consider this snippet:
|
||||||
|
@ -96,10 +103,9 @@ export function processJS(handlebars: string, context: any) {
|
||||||
clonedContext = cloneDeep(context)
|
clonedContext = cloneDeep(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sandboxContext = {
|
const sandboxContext: Record<string, any> = {
|
||||||
$: (path: string) => getContextValue(path, clonedContext),
|
$: (path: string) => getContextValue(path, clonedContext),
|
||||||
helpers: getJsHelperList(),
|
helpers: getJsHelperList(),
|
||||||
|
|
||||||
// Proxy to evaluate snippets when running in the browser
|
// Proxy to evaluate snippets when running in the browser
|
||||||
snippets: new Proxy(
|
snippets: new Proxy(
|
||||||
{},
|
{},
|
||||||
|
@ -114,8 +120,49 @@ export function processJS(handlebars: string, context: any) {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logs: Log[] = []
|
||||||
|
// logging only supported on frontend
|
||||||
|
if (!isBackendService()) {
|
||||||
|
// this counts the lines in the wrapped JS *before* the user's code, so that we can minus it
|
||||||
|
const jsLineCount = frontendWrapJS(js).split(js)[0].split("\n").length
|
||||||
|
const buildLogResponse = (type: LogType) => {
|
||||||
|
return (...props: any[]) => {
|
||||||
|
if (!isTest()) {
|
||||||
|
console[type](...props)
|
||||||
|
}
|
||||||
|
props.forEach((prop, index) => {
|
||||||
|
if (typeof prop === "object") {
|
||||||
|
props[index] = JSON.stringify(prop)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// quick way to find out what line this is being called from
|
||||||
|
// its an anonymous function and we look for the overall length to find the
|
||||||
|
// line number we care about (from the users function)
|
||||||
|
// JS stack traces are in the format function:line:column
|
||||||
|
const lineNumber = new Error().stack?.match(
|
||||||
|
/<anonymous>:(\d+):\d+/
|
||||||
|
)?.[1]
|
||||||
|
logs.push({
|
||||||
|
log: props,
|
||||||
|
line: lineNumber ? parseInt(lineNumber) - jsLineCount : undefined,
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sandboxContext.console = {
|
||||||
|
log: buildLogResponse("log"),
|
||||||
|
info: buildLogResponse("info"),
|
||||||
|
debug: buildLogResponse("debug"),
|
||||||
|
warn: buildLogResponse("warn"),
|
||||||
|
error: buildLogResponse("error"),
|
||||||
|
// table should be treated differently, but works the same
|
||||||
|
// as the rest of the logs for now
|
||||||
|
table: buildLogResponse("table"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create a sandbox with our context and run the JS
|
// Create a sandbox with our context and run the JS
|
||||||
const res = { data: runJS(js, sandboxContext) }
|
const res = { data: runJS(js, sandboxContext), logs }
|
||||||
return `{{${LITERAL_MARKER} js_result-${JSON.stringify(res)}}}`
|
return `{{${LITERAL_MARKER} js_result-${JSON.stringify(res)}}}`
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
onErrorLog && onErrorLog(error)
|
onErrorLog && onErrorLog(error)
|
||||||
|
|
|
@ -1,23 +1,27 @@
|
||||||
import { createContext, runInNewContext } from "vm"
|
import browserVM from "@budibase/vm-browserify"
|
||||||
|
import vm from "vm"
|
||||||
import { create, TemplateDelegate } from "handlebars"
|
import { create, TemplateDelegate } from "handlebars"
|
||||||
import { registerAll, registerMinimum } from "./helpers/index"
|
import { registerAll, registerMinimum } from "./helpers/index"
|
||||||
import { postprocess, preprocess } from "./processors"
|
import { postprocess, postprocessWithLogs, preprocess } from "./processors"
|
||||||
import {
|
import {
|
||||||
atob,
|
atob,
|
||||||
btoa,
|
btoa,
|
||||||
FIND_ANY_HBS_REGEX,
|
FIND_ANY_HBS_REGEX,
|
||||||
FIND_HBS_REGEX,
|
FIND_HBS_REGEX,
|
||||||
findDoubleHbsInstances,
|
findDoubleHbsInstances,
|
||||||
|
frontendWrapJS,
|
||||||
isBackendService,
|
isBackendService,
|
||||||
prefixStrings,
|
prefixStrings,
|
||||||
} from "./utilities"
|
} from "./utilities"
|
||||||
import { convertHBSBlock } from "./conversion"
|
import { convertHBSBlock } from "./conversion"
|
||||||
import { removeJSRunner, setJSRunner } from "./helpers/javascript"
|
import { removeJSRunner, setJSRunner } from "./helpers/javascript"
|
||||||
|
|
||||||
import manifest from "./manifest.json"
|
import manifest from "./manifest.json"
|
||||||
import { ProcessOptions } from "./types"
|
import { Log, ProcessOptions } from "./types"
|
||||||
import { UserScriptError } from "./errors"
|
import { UserScriptError } from "./errors"
|
||||||
|
import { isTest } from "./environment"
|
||||||
|
|
||||||
|
export type { Log, LogType } from "./types"
|
||||||
|
export { setTestingBackendJS } from "./environment"
|
||||||
export { helpersToRemoveForJs, getJsHelperList } from "./helpers/list"
|
export { helpersToRemoveForJs, getJsHelperList } from "./helpers/list"
|
||||||
export { FIND_ANY_HBS_REGEX } from "./utilities"
|
export { FIND_ANY_HBS_REGEX } from "./utilities"
|
||||||
export { setJSRunner, setOnErrorLog } from "./helpers/javascript"
|
export { setJSRunner, setOnErrorLog } from "./helpers/javascript"
|
||||||
|
@ -187,23 +191,27 @@ export function processObjectSync(
|
||||||
return object
|
return object
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// keep the logging function internal, don't want to add this to the process options directly
|
||||||
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
|
// as it can't be used for object processing etc.
|
||||||
* then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call.
|
function processStringSyncInternal(
|
||||||
* @param {string} string The template string which is the filled from the context object.
|
str: string,
|
||||||
* @param {object} context An object of information which will be used to enrich the string.
|
context?: object,
|
||||||
* @param {object|undefined} [opts] optional - specify some options for processing.
|
opts?: ProcessOptions & { logging: false }
|
||||||
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
): string
|
||||||
*/
|
function processStringSyncInternal(
|
||||||
export function processStringSync(
|
str: string,
|
||||||
|
context?: object,
|
||||||
|
opts?: ProcessOptions & { logging: true }
|
||||||
|
): { result: string; logs: Log[] }
|
||||||
|
function processStringSyncInternal(
|
||||||
string: string,
|
string: string,
|
||||||
context?: object,
|
context?: object,
|
||||||
opts?: ProcessOptions
|
opts?: ProcessOptions & { logging: boolean }
|
||||||
): string {
|
): string | { result: string; logs: Log[] } {
|
||||||
// Take a copy of input in case of error
|
// Take a copy of input in case of error
|
||||||
const input = string
|
const input = string
|
||||||
if (typeof string !== "string") {
|
if (typeof string !== "string") {
|
||||||
throw "Cannot process non-string types."
|
throw new Error("Cannot process non-string types.")
|
||||||
}
|
}
|
||||||
function process(stringPart: string) {
|
function process(stringPart: string) {
|
||||||
// context is needed to check for overlap between helpers and context
|
// context is needed to check for overlap between helpers and context
|
||||||
|
@ -217,16 +225,24 @@ export function processStringSync(
|
||||||
},
|
},
|
||||||
...context,
|
...context,
|
||||||
})
|
})
|
||||||
return postprocess(processedString)
|
return opts?.logging
|
||||||
|
? postprocessWithLogs(processedString)
|
||||||
|
: postprocess(processedString)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (opts && opts.onlyFound) {
|
if (opts && opts.onlyFound) {
|
||||||
|
let logs: Log[] = []
|
||||||
const blocks = findHBSBlocks(string)
|
const blocks = findHBSBlocks(string)
|
||||||
for (let block of blocks) {
|
for (let block of blocks) {
|
||||||
const outcome = process(block)
|
const outcome = process(block)
|
||||||
|
if (typeof outcome === "object" && "result" in outcome) {
|
||||||
|
logs = logs.concat(outcome.logs || [])
|
||||||
|
string = string.replace(block, outcome.result)
|
||||||
|
} else {
|
||||||
string = string.replace(block, outcome)
|
string = string.replace(block, outcome)
|
||||||
}
|
}
|
||||||
return string
|
}
|
||||||
|
return !opts?.logging ? string : { result: string, logs }
|
||||||
} else {
|
} else {
|
||||||
return process(string)
|
return process(string)
|
||||||
}
|
}
|
||||||
|
@ -239,6 +255,42 @@ export function processStringSync(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
|
||||||
|
* then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call.
|
||||||
|
* @param {string} string The template string which is the filled from the context object.
|
||||||
|
* @param {object} context An object of information which will be used to enrich the string.
|
||||||
|
* @param {object|undefined} [opts] optional - specify some options for processing.
|
||||||
|
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
||||||
|
*/
|
||||||
|
export function processStringSync(
|
||||||
|
string: string,
|
||||||
|
context?: object,
|
||||||
|
opts?: ProcessOptions
|
||||||
|
): string {
|
||||||
|
return processStringSyncInternal(string, context, {
|
||||||
|
...opts,
|
||||||
|
logging: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as function above, but allows logging to be returned - this is only for JS bindings.
|
||||||
|
*/
|
||||||
|
export function processStringWithLogsSync(
|
||||||
|
string: string,
|
||||||
|
context?: object,
|
||||||
|
opts?: ProcessOptions
|
||||||
|
): { result: string; logs: Log[] } {
|
||||||
|
if (isBackendService()) {
|
||||||
|
throw new Error("Logging disabled for backend bindings")
|
||||||
|
}
|
||||||
|
return processStringSyncInternal(string, context, {
|
||||||
|
...opts,
|
||||||
|
logging: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* By default with expressions like {{ name }} handlebars will escape various
|
* By default with expressions like {{ name }} handlebars will escape various
|
||||||
* characters, which can be problematic. To fix this we use the syntax {{{ name }}},
|
* characters, which can be problematic. To fix this we use the syntax {{{ name }}},
|
||||||
|
@ -456,28 +508,15 @@ export function convertToJS(hbs: string) {
|
||||||
export { JsTimeoutError, UserScriptError } from "./errors"
|
export { JsTimeoutError, UserScriptError } from "./errors"
|
||||||
|
|
||||||
export function browserJSSetup() {
|
export function browserJSSetup() {
|
||||||
/**
|
// tests are in jest - we need to use node VM for these
|
||||||
* Use polyfilled vm to run JS scripts in a browser Env
|
const jsSandbox = isTest() ? vm : browserVM
|
||||||
*/
|
// Use polyfilled vm to run JS scripts in a browser Env
|
||||||
setJSRunner((js: string, context: Record<string, any>) => {
|
setJSRunner((js: string, context: Record<string, any>) => {
|
||||||
createContext(context)
|
jsSandbox.createContext(context)
|
||||||
|
|
||||||
const wrappedJs = `
|
const wrappedJs = frontendWrapJS(js)
|
||||||
result = {
|
|
||||||
result: null,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
const result = jsSandbox.runInNewContext(wrappedJs, context)
|
||||||
result.result = ${js};
|
|
||||||
} catch (e) {
|
|
||||||
result.error = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
result;
|
|
||||||
`
|
|
||||||
|
|
||||||
const result = runInNewContext(wrappedJs, context, { timeout: 1000 })
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
throw new UserScriptError(result.error)
|
throw new UserScriptError(result.error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
import { FIND_HBS_REGEX } from "../utilities"
|
import { FIND_HBS_REGEX } from "../utilities"
|
||||||
import * as preprocessor from "./preprocessor"
|
import * as preprocessor from "./preprocessor"
|
||||||
|
import type { Preprocessor } from "./preprocessor"
|
||||||
import * as postprocessor from "./postprocessor"
|
import * as postprocessor from "./postprocessor"
|
||||||
import { ProcessOptions } from "../types"
|
import type { Postprocessor } from "./postprocessor"
|
||||||
|
import { Log, ProcessOptions } from "../types"
|
||||||
|
|
||||||
function process(output: string, processors: any[], opts?: ProcessOptions) {
|
function process(
|
||||||
|
output: string,
|
||||||
|
processors: (Preprocessor | Postprocessor)[],
|
||||||
|
opts?: ProcessOptions
|
||||||
|
) {
|
||||||
|
let logs: Log[] = []
|
||||||
for (let processor of processors) {
|
for (let processor of processors) {
|
||||||
// if a literal statement has occurred stop
|
// if a literal statement has occurred stop
|
||||||
if (typeof output !== "string") {
|
if (typeof output !== "string") {
|
||||||
|
@ -16,10 +23,18 @@ function process(output: string, processors: any[], opts?: ProcessOptions) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for (let match of matches) {
|
for (let match of matches) {
|
||||||
output = processor.process(output, match, opts)
|
const res = processor.process(output, match, opts || {})
|
||||||
|
if (typeof res === "object") {
|
||||||
|
if ("logs" in res && res.logs) {
|
||||||
|
logs = logs.concat(res.logs)
|
||||||
|
}
|
||||||
|
output = res.result
|
||||||
|
} else {
|
||||||
|
output = res as string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return output
|
}
|
||||||
|
return { result: output, logs }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function preprocess(string: string, opts: ProcessOptions) {
|
export function preprocess(string: string, opts: ProcessOptions) {
|
||||||
|
@ -30,8 +45,13 @@ export function preprocess(string: string, opts: ProcessOptions) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return process(string, processors, opts)
|
return process(string, processors, opts).result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postprocess(string: string) {
|
export function postprocess(string: string) {
|
||||||
|
return process(string, postprocessor.processors).result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postprocessWithLogs(string: string) {
|
||||||
return process(string, postprocessor.processors)
|
return process(string, postprocessor.processors)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import { LITERAL_MARKER } from "../helpers/constants"
|
import { LITERAL_MARKER } from "../helpers/constants"
|
||||||
|
import { Log } from "../types"
|
||||||
|
|
||||||
export enum PostProcessorNames {
|
export enum PostProcessorNames {
|
||||||
CONVERT_LITERALS = "convert-literals",
|
CONVERT_LITERALS = "convert-literals",
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostprocessorFn = (statement: string) => string
|
export type PostprocessorFn = (statement: string) => {
|
||||||
|
result: any
|
||||||
|
logs?: Log[]
|
||||||
|
}
|
||||||
|
|
||||||
class Postprocessor {
|
export class Postprocessor {
|
||||||
name: PostProcessorNames
|
name: PostProcessorNames
|
||||||
private readonly fn: PostprocessorFn
|
private readonly fn: PostprocessorFn
|
||||||
|
|
||||||
|
@ -23,12 +27,12 @@ class Postprocessor {
|
||||||
export const processors = [
|
export const processors = [
|
||||||
new Postprocessor(
|
new Postprocessor(
|
||||||
PostProcessorNames.CONVERT_LITERALS,
|
PostProcessorNames.CONVERT_LITERALS,
|
||||||
(statement: string) => {
|
(statement: string): { result: any; logs?: Log[] } => {
|
||||||
if (
|
if (
|
||||||
typeof statement !== "string" ||
|
typeof statement !== "string" ||
|
||||||
!statement.includes(LITERAL_MARKER)
|
!statement.includes(LITERAL_MARKER)
|
||||||
) {
|
) {
|
||||||
return statement
|
return { result: statement }
|
||||||
}
|
}
|
||||||
const splitMarkerIndex = statement.indexOf("-")
|
const splitMarkerIndex = statement.indexOf("-")
|
||||||
const type = statement.substring(12, splitMarkerIndex)
|
const type = statement.substring(12, splitMarkerIndex)
|
||||||
|
@ -38,20 +42,22 @@ export const processors = [
|
||||||
)
|
)
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "string":
|
case "string":
|
||||||
return value
|
return { result: value }
|
||||||
case "number":
|
case "number":
|
||||||
return parseFloat(value)
|
return { result: parseFloat(value) }
|
||||||
case "boolean":
|
case "boolean":
|
||||||
return value === "true"
|
return { result: value === "true" }
|
||||||
case "object":
|
case "object":
|
||||||
return JSON.parse(value)
|
return { result: JSON.parse(value) }
|
||||||
case "js_result":
|
case "js_result": {
|
||||||
// We use the literal helper to process the result of JS expressions
|
// We use the literal helper to process the result of JS expressions
|
||||||
// as we want to be able to return any types.
|
// as we want to be able to return any types.
|
||||||
// We wrap the value in an abject to be able to use undefined properly.
|
// We wrap the value in an abject to be able to use undefined properly.
|
||||||
return JSON.parse(value).data
|
const parsed = JSON.parse(value)
|
||||||
|
return { result: parsed.data, logs: parsed.logs }
|
||||||
}
|
}
|
||||||
return value
|
}
|
||||||
|
return { result: value }
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,9 +11,12 @@ export enum PreprocessorNames {
|
||||||
NORMALIZE_SPACES = "normalize-spaces",
|
NORMALIZE_SPACES = "normalize-spaces",
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreprocessorFn = (statement: string, opts?: ProcessOptions) => string
|
export type PreprocessorFn = (
|
||||||
|
statement: string,
|
||||||
|
opts?: ProcessOptions
|
||||||
|
) => string
|
||||||
|
|
||||||
class Preprocessor {
|
export class Preprocessor {
|
||||||
name: string
|
name: string
|
||||||
private readonly fn: PreprocessorFn
|
private readonly fn: PreprocessorFn
|
||||||
|
|
||||||
|
|
|
@ -8,3 +8,11 @@ export interface ProcessOptions {
|
||||||
onlyFound?: boolean
|
onlyFound?: boolean
|
||||||
disabledHelpers?: string[]
|
disabledHelpers?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LogType = "log" | "info" | "debug" | "warn" | "error" | "table"
|
||||||
|
|
||||||
|
export interface Log {
|
||||||
|
log: any[]
|
||||||
|
line?: number
|
||||||
|
type?: LogType
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
|
import { isTest, isTestingBackendJS } from "./environment"
|
||||||
|
|
||||||
const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g
|
const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g
|
||||||
|
|
||||||
export const FIND_HBS_REGEX = /{{([^{].*?)}}/g
|
export const FIND_HBS_REGEX = /{{([^{].*?)}}/g
|
||||||
export const FIND_ANY_HBS_REGEX = /{?{{([^{].*?)}}}?/g
|
export const FIND_ANY_HBS_REGEX = /{?{{([^{].*?)}}}?/g
|
||||||
export const FIND_TRIPLE_HBS_REGEX = /{{{([^{].*?)}}}/g
|
export const FIND_TRIPLE_HBS_REGEX = /{{{([^{].*?)}}}/g
|
||||||
|
|
||||||
const isJest = () => typeof jest !== "undefined"
|
|
||||||
|
|
||||||
export const isBackendService = () => {
|
export const isBackendService = () => {
|
||||||
|
// allow configuring backend JS mode when testing - we default to assuming
|
||||||
|
// frontend, but need a method to control this
|
||||||
|
if (isTest() && isTestingBackendJS()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
// We consider the tests for string-templates to be frontend, so that they
|
// We consider the tests for string-templates to be frontend, so that they
|
||||||
// test the frontend JS functionality.
|
// test the frontend JS functionality.
|
||||||
if (isJest()) {
|
if (isTest()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return typeof window === "undefined"
|
return typeof window === "undefined"
|
||||||
|
@ -86,3 +91,20 @@ export const prefixStrings = (
|
||||||
const regexPattern = new RegExp(`\\b(${escapedStrings.join("|")})\\b`, "g")
|
const regexPattern = new RegExp(`\\b(${escapedStrings.join("|")})\\b`, "g")
|
||||||
return baseString.replace(regexPattern, `${prefix}$1`)
|
return baseString.replace(regexPattern, `${prefix}$1`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function frontendWrapJS(js: string) {
|
||||||
|
return `
|
||||||
|
result = {
|
||||||
|
result: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
result.result = ${js};
|
||||||
|
} catch (e) {
|
||||||
|
result.error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
result;
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
|
@ -125,11 +125,6 @@ describe("Javascript", () => {
|
||||||
expect(processJS(`throw "Error"`)).toEqual("Error")
|
expect(processJS(`throw "Error"`)).toEqual("Error")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should timeout after one second", () => {
|
|
||||||
const output = processJS(`while (true) {}`)
|
|
||||||
expect(output).toBe("Timed out while executing JS")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should prevent access to the process global", async () => {
|
it("should prevent access to the process global", async () => {
|
||||||
expect(processJS(`return process`)).toEqual(
|
expect(processJS(`return process`)).toEqual(
|
||||||
"ReferenceError: process is not defined"
|
"ReferenceError: process is not defined"
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import {
|
||||||
|
processStringWithLogsSync,
|
||||||
|
encodeJSBinding,
|
||||||
|
defaultJSSetup,
|
||||||
|
} from "../src/index"
|
||||||
|
|
||||||
|
const processJS = (js: string, context?: object) => {
|
||||||
|
return processStringWithLogsSync(encodeJSBinding(js), context)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Javascript", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
defaultJSSetup()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Test logging in JS bindings", () => {
|
||||||
|
it("should execute a simple expression", () => {
|
||||||
|
const output = processJS(
|
||||||
|
`console.log("hello");
|
||||||
|
console.log("world");
|
||||||
|
console.log("foo");
|
||||||
|
return "hello"`
|
||||||
|
)
|
||||||
|
expect(output.result).toEqual("hello")
|
||||||
|
expect(output.logs[0].log).toEqual(["hello"])
|
||||||
|
expect(output.logs[0].line).toEqual(1)
|
||||||
|
expect(output.logs[1].log).toEqual(["world"])
|
||||||
|
expect(output.logs[1].line).toEqual(2)
|
||||||
|
expect(output.logs[2].log).toEqual(["foo"])
|
||||||
|
expect(output.logs[2].line).toEqual(3)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should log comma separated values", () => {
|
||||||
|
const output = processJS(`console.log(1, { a: 1 }); return 1`)
|
||||||
|
expect(output.logs[0].log).toEqual([1, JSON.stringify({ a: 1 })])
|
||||||
|
expect(output.logs[0].line).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return the type working with warn", () => {
|
||||||
|
const output = processJS(`console.warn("warning"); return 1`)
|
||||||
|
expect(output.logs[0].log).toEqual(["warning"])
|
||||||
|
expect(output.logs[0].line).toEqual(1)
|
||||||
|
expect(output.logs[0].type).toEqual("warn")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return the type working with error", () => {
|
||||||
|
const output = processJS(`console.error("error"); return 1`)
|
||||||
|
expect(output.logs[0].log).toEqual(["error"])
|
||||||
|
expect(output.logs[0].line).toEqual(1)
|
||||||
|
expect(output.logs[0].type).toEqual("error")
|
||||||
|
})
|
||||||
|
})
|
|
@ -2,10 +2,12 @@ import {
|
||||||
Automation,
|
Automation,
|
||||||
AutomationActionStepId,
|
AutomationActionStepId,
|
||||||
AutomationLogPage,
|
AutomationLogPage,
|
||||||
|
AutomationResults,
|
||||||
AutomationStatus,
|
AutomationStatus,
|
||||||
AutomationStepDefinition,
|
AutomationStepDefinition,
|
||||||
AutomationTriggerDefinition,
|
AutomationTriggerDefinition,
|
||||||
AutomationTriggerStepId,
|
AutomationTriggerStepId,
|
||||||
|
DidNotTriggerResponse,
|
||||||
Row,
|
Row,
|
||||||
} from "../../../documents"
|
} from "../../../documents"
|
||||||
import { DocumentDestroyResponse } from "@budibase/nano"
|
import { DocumentDestroyResponse } from "@budibase/nano"
|
||||||
|
@ -74,4 +76,10 @@ export interface TestAutomationRequest {
|
||||||
fields: Record<string, any>
|
fields: Record<string, any>
|
||||||
row?: Row
|
row?: Row
|
||||||
}
|
}
|
||||||
export interface TestAutomationResponse {}
|
export type TestAutomationResponse = AutomationResults | DidNotTriggerResponse
|
||||||
|
|
||||||
|
export function isDidNotTriggerResponse(
|
||||||
|
response: TestAutomationResponse
|
||||||
|
): response is DidNotTriggerResponse {
|
||||||
|
return !!("message" in response && response.message)
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Document } from "../../document"
|
import { Document } from "../../document"
|
||||||
import { EventEmitter } from "events"
|
|
||||||
import { User } from "../../global"
|
import { User } from "../../global"
|
||||||
import { ReadStream } from "fs"
|
import { ReadStream } from "fs"
|
||||||
import { Row } from "../row"
|
import { Row } from "../row"
|
||||||
import { Table } from "../table"
|
import { Table } from "../table"
|
||||||
import { AutomationStep, AutomationTrigger } from "./schema"
|
import { AutomationStep, AutomationTrigger } from "./schema"
|
||||||
|
import { ContextEmitter } from "../../../sdk"
|
||||||
|
|
||||||
export enum AutomationIOType {
|
export enum AutomationIOType {
|
||||||
OBJECT = "object",
|
OBJECT = "object",
|
||||||
|
@ -205,6 +205,14 @@ export interface AutomationResults {
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DidNotTriggerResponse {
|
||||||
|
outputs: {
|
||||||
|
success: false
|
||||||
|
status: AutomationStatus.STOPPED
|
||||||
|
}
|
||||||
|
message: AutomationStoppedReason.TRIGGER_FILTER_NOT_MET
|
||||||
|
}
|
||||||
|
|
||||||
export interface AutomationLog extends AutomationResults, Document {
|
export interface AutomationLog extends AutomationResults, Document {
|
||||||
automationName: string
|
automationName: string
|
||||||
_rev?: string
|
_rev?: string
|
||||||
|
@ -218,7 +226,7 @@ export interface AutomationLogPage {
|
||||||
|
|
||||||
export interface AutomationStepInputBase {
|
export interface AutomationStepInputBase {
|
||||||
context: Record<string, any>
|
context: Record<string, any>
|
||||||
emitter: EventEmitter
|
emitter: ContextEmitter
|
||||||
appId: string
|
appId: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export enum FeatureFlag {
|
export enum FeatureFlag {
|
||||||
USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR",
|
USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR",
|
||||||
|
CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS = "CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS",
|
||||||
|
|
||||||
// Account-portal
|
// Account-portal
|
||||||
DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL",
|
DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL",
|
||||||
|
@ -7,6 +8,7 @@ export enum FeatureFlag {
|
||||||
|
|
||||||
export const FeatureFlagDefaults = {
|
export const FeatureFlagDefaults = {
|
||||||
[FeatureFlag.USE_ZOD_VALIDATOR]: false,
|
[FeatureFlag.USE_ZOD_VALIDATOR]: false,
|
||||||
|
[FeatureFlag.CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS]: false,
|
||||||
|
|
||||||
// Account-portal
|
// Account-portal
|
||||||
[FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,
|
[FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,
|
||||||
|
|
|
@ -24,3 +24,18 @@ export type InsertAtPositionFn = (_: {
|
||||||
value: string
|
value: string
|
||||||
cursor?: { anchor: number }
|
cursor?: { anchor: number }
|
||||||
}) => void
|
}) => void
|
||||||
|
|
||||||
|
export interface UIBinding {
|
||||||
|
tableId?: string
|
||||||
|
fieldSchema?: {
|
||||||
|
name: string
|
||||||
|
tableId: string
|
||||||
|
type: string
|
||||||
|
subtype?: string
|
||||||
|
prefixKeys?: string
|
||||||
|
}
|
||||||
|
component?: string
|
||||||
|
providerId: string
|
||||||
|
readableBinding?: string
|
||||||
|
runtimeBinding?: string
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
export type UIDatasourceType =
|
||||||
|
| "table"
|
||||||
|
| "view"
|
||||||
|
| "viewV2"
|
||||||
|
| "query"
|
||||||
|
| "custom"
|
||||||
|
| "link"
|
||||||
|
| "field"
|
||||||
|
| "jsonarray"
|
|
@ -2,3 +2,4 @@ export * from "./stores"
|
||||||
export * from "./bindings"
|
export * from "./bindings"
|
||||||
export * from "./components"
|
export * from "./components"
|
||||||
export * from "./dataFetch"
|
export * from "./dataFetch"
|
||||||
|
export * from "./datasource"
|
||||||
|
|
49
yarn.lock
49
yarn.lock
|
@ -2131,9 +2131,9 @@
|
||||||
through2 "^2.0.0"
|
through2 "^2.0.0"
|
||||||
|
|
||||||
"@budibase/pro@npm:@budibase/pro@latest":
|
"@budibase/pro@npm:@budibase/pro@latest":
|
||||||
version "3.2.44"
|
version "3.2.47"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.44.tgz#90367bb2167aafd8c809e000a57d349e5dc4bb78"
|
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.47.tgz#150d7b16b14932d03c84bdb0e6d570d490c28a5c"
|
||||||
integrity sha512-Zv2PBVUZUS6/psOpIRIDlW3jrOHWWPhpQXzCk00kIQJaqjkdcvuTXSedQ70u537sQmLu8JsSWbui9MdfF8ksVw==
|
integrity sha512-UeTIq7yzMUK6w/akUsRafoD/Kif6PXv4d7K1arn8GTMjwFm9QYu2hg1YkQ+duNdwyZ/GEPlEAV5SYK+NDgtpdA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@anthropic-ai/sdk" "^0.27.3"
|
"@anthropic-ai/sdk" "^0.27.3"
|
||||||
"@budibase/backend-core" "*"
|
"@budibase/backend-core" "*"
|
||||||
|
@ -2152,6 +2152,13 @@
|
||||||
scim-patch "^0.8.1"
|
scim-patch "^0.8.1"
|
||||||
scim2-parse-filter "^0.2.8"
|
scim2-parse-filter "^0.2.8"
|
||||||
|
|
||||||
|
"@budibase/vm-browserify@^1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@budibase/vm-browserify/-/vm-browserify-1.1.4.tgz#eecb001bd9521cb7647e26fb4d2d29d0a4dce262"
|
||||||
|
integrity sha512-/dyOLj+jQNKe6sVfLP6NdwA79OZxEWHCa41VGsjKJC9DYo6l2fEcL5BNXq2pATqrbgWmOlEbcRulfZ+7W0QRUg==
|
||||||
|
dependencies:
|
||||||
|
indexof "^0.0.1"
|
||||||
|
|
||||||
"@bull-board/api@5.10.2":
|
"@bull-board/api@5.10.2":
|
||||||
version "5.10.2"
|
version "5.10.2"
|
||||||
resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-5.10.2.tgz#ae8ff6918b23897bf879a6ead3683f964374c4b3"
|
resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-5.10.2.tgz#ae8ff6918b23897bf879a6ead3683f964374c4b3"
|
||||||
|
@ -11925,6 +11932,11 @@ indexes-of@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
|
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
|
||||||
integrity sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==
|
integrity sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==
|
||||||
|
|
||||||
|
indexof@^0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
|
||||||
|
integrity sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==
|
||||||
|
|
||||||
infer-owner@^1.0.4:
|
infer-owner@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
|
resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
|
||||||
|
@ -18646,16 +18658,7 @@ string-length@^4.0.1:
|
||||||
char-regex "^1.0.2"
|
char-regex "^1.0.2"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
|
||||||
version "4.2.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
|
||||||
dependencies:
|
|
||||||
emoji-regex "^8.0.0"
|
|
||||||
is-fullwidth-code-point "^3.0.0"
|
|
||||||
strip-ansi "^6.0.1"
|
|
||||||
|
|
||||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
|
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
@ -18747,7 +18750,7 @@ stringify-object@^3.2.1:
|
||||||
is-obj "^1.0.1"
|
is-obj "^1.0.1"
|
||||||
is-regexp "^1.0.0"
|
is-regexp "^1.0.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
@ -18761,13 +18764,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex "^4.1.0"
|
ansi-regex "^4.1.0"
|
||||||
|
|
||||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
|
||||||
version "6.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
|
||||||
dependencies:
|
|
||||||
ansi-regex "^5.0.1"
|
|
||||||
|
|
||||||
strip-ansi@^7.0.1:
|
strip-ansi@^7.0.1:
|
||||||
version "7.0.1"
|
version "7.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
|
||||||
|
@ -20515,7 +20511,7 @@ worker-farm@1.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
errno "~0.1.7"
|
errno "~0.1.7"
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
@ -20533,15 +20529,6 @@ wrap-ansi@^5.1.0:
|
||||||
string-width "^3.0.0"
|
string-width "^3.0.0"
|
||||||
strip-ansi "^5.0.0"
|
strip-ansi "^5.0.0"
|
||||||
|
|
||||||
wrap-ansi@^7.0.0:
|
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
|
||||||
dependencies:
|
|
||||||
ansi-styles "^4.0.0"
|
|
||||||
string-width "^4.1.0"
|
|
||||||
strip-ansi "^6.0.0"
|
|
||||||
|
|
||||||
wrap-ansi@^8.1.0:
|
wrap-ansi@^8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||||
|
|
Loading…
Reference in New Issue