Merge branch 'master' into BUDI-8986/convert-screen-store
This commit is contained in:
commit
588d4b7485
|
@ -41,11 +41,12 @@ module.exports = {
|
|||
if (
|
||||
/^@budibase\/[^/]+\/.*$/.test(importPath) &&
|
||||
importPath !== "@budibase/backend-core/tests" &&
|
||||
importPath !== "@budibase/string-templates/test/utils"
|
||||
importPath !== "@budibase/string-templates/test/utils" &&
|
||||
importPath !== "@budibase/client/manifest.json"
|
||||
) {
|
||||
context.report({
|
||||
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",
|
||||
"version": "3.2.46",
|
||||
"version": "3.3.1",
|
||||
"npmClient": "yarn",
|
||||
"concurrency": 20,
|
||||
"command": {
|
||||
|
|
|
@ -45,6 +45,11 @@
|
|||
--purple: #806fde;
|
||||
--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-medium: 8px;
|
||||
--rounded-large: 16px;
|
||||
|
|
|
@ -293,7 +293,7 @@
|
|||
type: RowSelector,
|
||||
props: {
|
||||
row: inputData["oldRow"] || {
|
||||
tableId: inputData["row"].tableId,
|
||||
tableId: inputData["row"]?.tableId,
|
||||
},
|
||||
meta: {
|
||||
fields: inputData["meta"]?.oldFields || {},
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
decodeJSBinding,
|
||||
encodeJSBinding,
|
||||
processObjectSync,
|
||||
processStringSync,
|
||||
processStringWithLogsSync,
|
||||
} from "@budibase/string-templates"
|
||||
import { readableToRuntimeBinding } from "@/dataBinding"
|
||||
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
||||
|
@ -41,6 +41,7 @@
|
|||
InsertAtPositionFn,
|
||||
JSONValue,
|
||||
} from "@budibase/types"
|
||||
import type { Log } from "@budibase/string-templates"
|
||||
import type { CompletionContext } from "@codemirror/autocomplete"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -66,6 +67,7 @@
|
|||
let insertAtPos: InsertAtPositionFn | undefined
|
||||
let targetMode: BindingMode | null = null
|
||||
let expressionResult: string | undefined
|
||||
let expressionLogs: Log[] | undefined
|
||||
let expressionError: string | undefined
|
||||
let evaluating = false
|
||||
|
||||
|
@ -157,7 +159,7 @@
|
|||
(expression: string | null, context: any, snippets: Snippet[]) => {
|
||||
try {
|
||||
expressionError = undefined
|
||||
expressionResult = processStringSync(
|
||||
const output = processStringWithLogsSync(
|
||||
expression || "",
|
||||
{
|
||||
...context,
|
||||
|
@ -167,6 +169,8 @@
|
|||
noThrow: false,
|
||||
}
|
||||
)
|
||||
expressionResult = output.result
|
||||
expressionLogs = output.logs
|
||||
} catch (err: any) {
|
||||
expressionResult = undefined
|
||||
expressionError = err
|
||||
|
@ -421,6 +425,7 @@
|
|||
<EvaluationSidePanel
|
||||
{expressionResult}
|
||||
{expressionError}
|
||||
{expressionLogs}
|
||||
{evaluating}
|
||||
expression={editorValue ? editorValue : ""}
|
||||
/>
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
import { Helpers } from "@budibase/bbui"
|
||||
import { fade } from "svelte/transition"
|
||||
import { UserScriptError } from "@budibase/string-templates"
|
||||
import type { Log } from "@budibase/string-templates"
|
||||
import type { JSONValue } from "@budibase/types"
|
||||
|
||||
// this can be essentially any primitive response from the JS function
|
||||
export let expressionResult: JSONValue | undefined = undefined
|
||||
export let expressionError: string | undefined = undefined
|
||||
export let expressionLogs: Log[] = []
|
||||
export let evaluating = false
|
||||
export let expression: string | null = null
|
||||
|
||||
|
@ -16,6 +18,11 @@
|
|||
$: empty = expression == null || expression?.trim() === ""
|
||||
$: success = !error && !empty
|
||||
$: highlightedResult = highlight(expressionResult)
|
||||
$: highlightedLogs = expressionLogs.map(l => ({
|
||||
log: highlight(l.log.join(", ")),
|
||||
line: l.line,
|
||||
type: l.type,
|
||||
}))
|
||||
|
||||
const formatError = (err: any) => {
|
||||
if (err.code === UserScriptError.code) {
|
||||
|
@ -25,14 +32,14 @@
|
|||
}
|
||||
|
||||
// json can be any primitive type
|
||||
const highlight = (json?: any | null) => {
|
||||
const highlight = (json?: JSONValue | null) => {
|
||||
if (json == null) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Attempt to parse and then stringify, in case this is valid result
|
||||
try {
|
||||
json = JSON.stringify(JSON.parse(json), null, 2)
|
||||
json = JSON.stringify(JSON.parse(json as any), null, 2)
|
||||
} catch (err) {
|
||||
// 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-content">
|
||||
{#if error}
|
||||
<Icon name="Alert" color="var(--spectrum-global-color-red-600)" />
|
||||
<Icon name="Alert" color="var(--error-content)" />
|
||||
<div>Error</div>
|
||||
{#if evaluating}
|
||||
<div transition:fade|local={{ duration: 130 }}>
|
||||
|
@ -90,8 +97,36 @@
|
|||
{:else if error}
|
||||
{formatError(expressionError)}
|
||||
{:else}
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
{@html highlightedResult}
|
||||
<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-->
|
||||
{@html highlightedResult}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -130,20 +165,37 @@
|
|||
height: 100%;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
opacity: 10%;
|
||||
}
|
||||
.header.error::before {
|
||||
background: var(--spectrum-global-color-red-400);
|
||||
background: var(--error-bg);
|
||||
}
|
||||
.body {
|
||||
flex: 1 1 auto;
|
||||
padding: var(--spacing-m) var(--spacing-l);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
white-space: pre-wrap;
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
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>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { datasources } from "@/stores/builder"
|
||||
import { Divider, Heading } from "@budibase/bbui"
|
||||
|
||||
export let dividerState
|
||||
|
@ -6,6 +7,21 @@
|
|||
export let dataSet
|
||||
export let value
|
||||
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>
|
||||
|
||||
{#if dividerState}
|
||||
|
@ -21,15 +37,16 @@
|
|||
{#each dataSet as data}
|
||||
<li
|
||||
class="spectrum-Menu-item"
|
||||
class:is-selected={value?.label === data.label &&
|
||||
value?.type === data.type}
|
||||
class:is-selected={isSelected(data) && value?.type === data.type}
|
||||
role="option"
|
||||
aria-selected="true"
|
||||
tabindex="0"
|
||||
on:click={() => onSelect(data)}
|
||||
>
|
||||
<span class="spectrum-Menu-itemLabel">
|
||||
{data.datasourceName ? `${data.datasourceName} - ` : ""}{data.label}
|
||||
{data.datasourceName && displayDatasourceName
|
||||
? `${data.datasourceName} - `
|
||||
: ""}{data.label}
|
||||
</span>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||
|
|
|
@ -31,10 +31,15 @@
|
|||
import IntegrationQueryEditor from "@/components/integration/index.svelte"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import { findAllComponents } from "@/helpers/components"
|
||||
import {
|
||||
extractFields,
|
||||
extractJSONArrayFields,
|
||||
extractRelationships,
|
||||
} from "@/helpers/bindings"
|
||||
import ClientBindingPanel from "@/components/common/bindings/ClientBindingPanel.svelte"
|
||||
import DataSourceCategory from "@/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
|
||||
import { API } from "@/api"
|
||||
import { datasourceSelect as format } from "@/helpers/data/format"
|
||||
import { sortAndFormat } from "@/helpers/data/format"
|
||||
|
||||
export let value = {}
|
||||
export let otherSources
|
||||
|
@ -51,25 +56,13 @@
|
|||
let modal
|
||||
|
||||
$: text = value?.label ?? "Choose an option"
|
||||
$: tables = $tablesStore.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)
|
||||
})
|
||||
$: tables = sortAndFormat.tables($tablesStore.list, $datasources.list)
|
||||
$: viewsV1 = $viewsStore.list.map(view => ({
|
||||
...view,
|
||||
label: view.name,
|
||||
type: "view",
|
||||
}))
|
||||
$: viewsV2 = $viewsV2Store.list.map(format.viewV2)
|
||||
$: viewsV2 = sortAndFormat.viewsV2($viewsV2Store.list, $datasources.list)
|
||||
$: views = [...(viewsV1 || []), ...(viewsV2 || [])]
|
||||
$: queries = $queriesStore.list
|
||||
.filter(q => showAllQueries || q.queryVerb === "read" || q.readable)
|
||||
|
@ -93,67 +86,9 @@
|
|||
value: `{{ literal ${safe(provider._id)} }}`,
|
||||
type: "provider",
|
||||
}))
|
||||
$: links = 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 = 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} }}`,
|
||||
}
|
||||
})
|
||||
$: links = extractRelationships(bindings)
|
||||
$: fields = extractFields(bindings)
|
||||
$: jsonArrays = extractJSONArrayFields(bindings)
|
||||
$: custom = {
|
||||
type: "custom",
|
||||
label: "JSON / CSV",
|
||||
|
@ -303,6 +238,7 @@
|
|||
dataSet={views}
|
||||
{value}
|
||||
onSelect={handleSelected}
|
||||
identifiers={["tableId", "name"]}
|
||||
/>
|
||||
{/if}
|
||||
{#if queries?.length}
|
||||
|
@ -312,6 +248,7 @@
|
|||
dataSet={queries}
|
||||
{value}
|
||||
onSelect={handleSelected}
|
||||
identifiers={["_id"]}
|
||||
/>
|
||||
{/if}
|
||||
{#if links?.length}
|
||||
|
@ -321,6 +258,7 @@
|
|||
dataSet={links}
|
||||
{value}
|
||||
onSelect={handleSelected}
|
||||
identifiers={["tableId", "fieldName"]}
|
||||
/>
|
||||
{/if}
|
||||
{#if fields?.length}
|
||||
|
@ -330,6 +268,7 @@
|
|||
dataSet={fields}
|
||||
{value}
|
||||
onSelect={handleSelected}
|
||||
identifiers={["providerId", "tableId", "fieldName"]}
|
||||
/>
|
||||
{/if}
|
||||
{#if jsonArrays?.length}
|
||||
|
@ -339,6 +278,7 @@
|
|||
dataSet={jsonArrays}
|
||||
{value}
|
||||
onSelect={handleSelected}
|
||||
identifiers={["providerId", "tableId", "fieldName"]}
|
||||
/>
|
||||
{/if}
|
||||
{#if showDataProviders && dataProviders?.length}
|
||||
|
@ -348,6 +288,7 @@
|
|||
dataSet={dataProviders}
|
||||
{value}
|
||||
onSelect={handleSelected}
|
||||
identifiers={["providerId"]}
|
||||
/>
|
||||
{/if}
|
||||
<DataSourceCategory
|
||||
|
|
|
@ -1,22 +1,32 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { Popover, Select } from "@budibase/bbui"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { tables as tablesStore, viewsV2 } from "@/stores/builder"
|
||||
import { tableSelect as format } from "@/helpers/data/format"
|
||||
import {
|
||||
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
|
||||
|
||||
let anchorRight, dropdownRight
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: tables = $tablesStore.list.map(format.table)
|
||||
$: views = $viewsV2.list.map(format.viewV2)
|
||||
$: tables = sortAndFormat.tables($tableStore.list, $datasourceStore.list)
|
||||
$: views = sortAndFormat.viewsV2($viewsV2Store.list, $datasourceStore.list)
|
||||
$: options = [...(tables || []), ...(views || [])]
|
||||
|
||||
$: text = value?.label ?? "Choose an option"
|
||||
|
||||
const onChange = e => {
|
||||
dispatch(
|
||||
"change",
|
||||
options.find(x => x.resourceId === e.detail)
|
||||
options.find(x => x.resourceId === e.resourceId)
|
||||
)
|
||||
dropdownRight.hide()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
@ -29,10 +39,47 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<Select
|
||||
on:change={onChange}
|
||||
value={value?.resourceId}
|
||||
{options}
|
||||
getOptionValue={x => x.resourceId}
|
||||
getOptionLabel={x => x.label}
|
||||
/>
|
||||
<div class="container" bind:this={anchorRight}>
|
||||
<Select
|
||||
readonly
|
||||
value={text}
|
||||
options={[text]}
|
||||
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 defaultCacheFn = key => {
|
||||
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
|
||||
temporalStore.setExpiring(key, {}, oneDayInSeconds)
|
||||
}
|
||||
|
||||
const dismissableModals = [
|
||||
|
@ -50,7 +50,7 @@
|
|||
},
|
||||
]
|
||||
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 defaultCacheFn = key => {
|
||||
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
|
||||
temporalStore.setExpiring(key, {}, oneDayInSeconds)
|
||||
}
|
||||
|
||||
const upgradeAction = key => {
|
||||
|
@ -148,7 +148,7 @@ export const getBanners = () => {
|
|||
buildUsersAboveLimitBanner(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER),
|
||||
].filter(licensingBanner => {
|
||||
return (
|
||||
!temporalStore.actions.getExpiring(licensingBanner.key) &&
|
||||
!temporalStore.getExpiring(licensingBanner.key) &&
|
||||
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,
|
||||
}
|
||||
},
|
||||
viewV2: view => ({
|
||||
...view,
|
||||
label: view.name,
|
||||
type: "viewV2",
|
||||
}),
|
||||
viewV2: (view, datasources) => {
|
||||
const datasource = datasources
|
||||
?.filter(f => f.entities)
|
||||
.flatMap(d => d.entities)
|
||||
.find(ds => ds._id === view.tableId)
|
||||
return {
|
||||
...view,
|
||||
label: view.name,
|
||||
type: "viewV2",
|
||||
datasourceName: datasource?.name,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const tableSelect = {
|
||||
|
@ -31,3 +38,36 @@ export const tableSelect = {
|
|||
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,
|
||||
isBuilderInputFocused,
|
||||
} 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
|
||||
|
||||
navigation.actions.init($redirect)
|
||||
navigation.init($redirect)
|
||||
|
||||
const validateTenantId = async () => {
|
||||
const host = window.location.host
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
selectedScreen,
|
||||
hoverStore,
|
||||
componentTreeNodesStore,
|
||||
screenComponentErrors,
|
||||
snippets,
|
||||
} from "@/stores/builder"
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||
|
@ -68,6 +69,7 @@
|
|||
port: window.location.port,
|
||||
},
|
||||
snippets: $snippets,
|
||||
componentErrors: $screenComponentErrors,
|
||||
}
|
||||
|
||||
// Refresh the preview when required
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import {
|
||||
appsStore,
|
||||
organisation,
|
||||
admin,
|
||||
auth,
|
||||
groups,
|
||||
licensing,
|
||||
|
@ -42,6 +43,7 @@
|
|||
app => app.status === AppStatus.DEPLOYED
|
||||
)
|
||||
$: userApps = getUserApps(publishedApps, userGroups, $auth.user)
|
||||
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||
|
||||
function getUserApps(publishedApps, userGroups, user) {
|
||||
if (sdk.users.isAdmin(user)) {
|
||||
|
@ -111,7 +113,13 @@
|
|||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="LockClosed"
|
||||
on:click={() => changePasswordModal.show()}
|
||||
on:click={() => {
|
||||
if (isOwner) {
|
||||
window.location.href = `${$admin.accountPortalUrl}/portal/account`
|
||||
} else {
|
||||
changePasswordModal.show()
|
||||
}
|
||||
}}
|
||||
>
|
||||
Update password
|
||||
</MenuItem>
|
||||
|
|
|
@ -30,10 +30,16 @@
|
|||
try {
|
||||
loading = true
|
||||
if (forceResetPassword) {
|
||||
const email = $auth.user.email
|
||||
const tenantId = $auth.user.tenantId
|
||||
await auth.updateSelf({
|
||||
password,
|
||||
forceResetPassword: false,
|
||||
})
|
||||
if (!$auth.user) {
|
||||
// Update self will clear the platform user, so need to login
|
||||
await auth.login(email, password, tenantId)
|
||||
}
|
||||
$goto("../portal/")
|
||||
} else {
|
||||
await auth.resetPassword(password, resetCode)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { auth } from "@/stores/portal"
|
||||
import { admin, auth } from "@/stores/portal"
|
||||
import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ProfileModal from "@/components/settings/ProfileModal.svelte"
|
||||
|
@ -13,6 +13,8 @@
|
|||
let updatePasswordModal
|
||||
let apiKeyModal
|
||||
|
||||
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await auth.logout()
|
||||
|
@ -32,7 +34,16 @@
|
|||
</MenuItem>
|
||||
<MenuItem icon="Moon" on:click={() => themeModal.show()}>Theme</MenuItem>
|
||||
{#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
|
||||
</MenuItem>
|
||||
{/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
|
||||
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)
|
||||
|
||||
|
|
|
@ -33,7 +33,16 @@ import { Utils } from "@budibase/frontend-core"
|
|||
import { Component, FieldType, Screen, Table } from "@budibase/types"
|
||||
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
|
||||
name: string
|
||||
friendlyName?: string
|
||||
|
@ -41,10 +50,11 @@ interface ComponentDefinition {
|
|||
settings?: ComponentSetting[]
|
||||
features?: Record<string, boolean>
|
||||
typeSupportPresets?: Record<string, any>
|
||||
illegalChildren?: string[]
|
||||
legalDirectChildren: string[]
|
||||
illegalChildren: string[]
|
||||
}
|
||||
|
||||
interface ComponentSetting {
|
||||
export interface ComponentSetting {
|
||||
key: string
|
||||
type: string
|
||||
section?: string
|
||||
|
@ -55,20 +65,9 @@ interface 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 = {
|
||||
components: {},
|
||||
customComponents: [],
|
||||
selectedComponentId: null,
|
||||
componentToPaste: null,
|
||||
settingsCache: {},
|
||||
}
|
||||
|
||||
|
@ -441,6 +440,11 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
* @returns
|
||||
*/
|
||||
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)
|
||||
if (!definition) {
|
||||
return null
|
||||
|
@ -462,7 +466,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
// Standard post processing
|
||||
this.enrichEmptySettings(instance, {
|
||||
parent,
|
||||
screen: get(selectedScreen),
|
||||
screen,
|
||||
useDefaultValues: true,
|
||||
})
|
||||
|
||||
|
@ -483,7 +487,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
// Add step name to form steps
|
||||
if (componentName.endsWith("/formstep") && $selectedScreen) {
|
||||
const parentForm = findClosestMatchingComponent(
|
||||
$selectedScreen.props,
|
||||
screen.props,
|
||||
get(selectedComponent)._id,
|
||||
(component: Component) => component._component.endsWith("/form")
|
||||
)
|
||||
|
@ -543,7 +547,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
// Find the selected component
|
||||
let selectedComponentId = state.selectedComponentId
|
||||
if (selectedComponentId?.startsWith(`${screen._id}-`)) {
|
||||
selectedComponentId = screen.props._id || null
|
||||
selectedComponentId = screen.props._id
|
||||
}
|
||||
const currentComponent = findComponent(
|
||||
screen.props,
|
||||
|
@ -654,7 +658,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
// Determine the next component to select, and select it before deletion
|
||||
// to avoid an intermediate state of no component selection
|
||||
const state = get(this.store)
|
||||
let nextId: string | null = ""
|
||||
let nextId = ""
|
||||
if (state.selectedComponentId === component._id) {
|
||||
nextId = this.getNext()
|
||||
if (!nextId) {
|
||||
|
@ -741,7 +745,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
if (!state.componentToPaste) {
|
||||
return
|
||||
}
|
||||
let newComponentId: string | null = ""
|
||||
let newComponentId = ""
|
||||
|
||||
// Remove copied component if cutting, regardless if pasting works
|
||||
let componentToPaste = cloneDeep(state.componentToPaste)
|
||||
|
@ -842,7 +846,10 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
getPrevious() {
|
||||
const state = get(this.store)
|
||||
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 index = parent?._children.findIndex(
|
||||
(x: Component) => x._id === componentId
|
||||
|
@ -891,7 +898,10 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
const state = get(this.store)
|
||||
const component = get(selectedComponent)
|
||||
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 index = parent?._children.findIndex(
|
||||
(x: Component) => x._id === componentId
|
||||
|
@ -1158,7 +1168,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
}
|
||||
|
||||
async handleEjectBlock(componentId: string, ejectedDefinition: Component) {
|
||||
let nextSelectedComponentId: string | null = null
|
||||
let nextSelectedComponentId: string | undefined
|
||||
|
||||
await screenStore.patch((screen: Screen) => {
|
||||
const block = findComponent(screen.props, componentId)
|
||||
|
@ -1194,7 +1204,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
(x: Component) => x._id === componentId
|
||||
)
|
||||
parent._children[index] = ejectedDefinition
|
||||
nextSelectedComponentId = ejectedDefinition._id ?? null
|
||||
nextSelectedComponentId = ejectedDefinition._id
|
||||
}, null)
|
||||
|
||||
// Select new root component
|
||||
|
|
|
@ -3,7 +3,7 @@ import { appStore } from "./app.js"
|
|||
import { componentStore, selectedComponent } from "./components"
|
||||
import { navigationStore } from "./navigation.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 { hoverStore } from "./hover.js"
|
||||
import { previewStore } from "./preview.js"
|
||||
|
@ -16,6 +16,7 @@ import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js"
|
|||
import { deploymentStore } from "./deployments.js"
|
||||
import { contextMenuStore } from "./contextMenu.js"
|
||||
import { snippets } from "./snippets"
|
||||
import { screenComponentErrors } from "./screenComponent"
|
||||
|
||||
// Backend
|
||||
import { tables } from "./tables"
|
||||
|
@ -67,6 +68,7 @@ export {
|
|||
snippets,
|
||||
rowActions,
|
||||
appPublished,
|
||||
screenComponentErrors,
|
||||
}
|
||||
|
||||
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,
|
||||
selectedComponent,
|
||||
} from "@/stores/builder"
|
||||
import { createHistoryStore, HistoryStore } from "@/stores/builder/history"
|
||||
import { createHistoryStore } from "@/stores/builder/history"
|
||||
import { API } from "@/api"
|
||||
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 {
|
||||
screens: Screen[]
|
||||
selectedScreenId: string | null | undefined
|
||||
selected?: Screen
|
||||
selectedScreenId?: string
|
||||
}
|
||||
|
||||
export const INITIAL_SCREENS_STATE: ScreenState = {
|
||||
export const initialScreenState: ScreenState = {
|
||||
screens: [],
|
||||
selectedScreenId: null,
|
||||
}
|
||||
|
||||
// Review the nulls
|
||||
export class ScreenStore extends BudiStore<ScreenState> {
|
||||
history: HistoryStore<Screen>
|
||||
save: (doc: Screen) => Promise<Screen>
|
||||
delete: (doc: Screen) => Promise<void>
|
||||
history: any
|
||||
delete: any
|
||||
save: any
|
||||
|
||||
constructor() {
|
||||
super(INITIAL_SCREENS_STATE)
|
||||
super(initialScreenState)
|
||||
|
||||
// Bind scope
|
||||
this.select = this.select.bind(this)
|
||||
|
@ -49,14 +55,16 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
this.removeCustomLayout = this.removeCustomLayout.bind(this)
|
||||
|
||||
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,
|
||||
beforeAction: () => {},
|
||||
afterAction: () => {
|
||||
// Ensure a valid component is selected
|
||||
if (!get(selectedComponent)) {
|
||||
this.update(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() {
|
||||
this.store.set({ ...INITIAL_SCREENS_STATE })
|
||||
this.store.set({ ...initialScreenState })
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace ALL store screens with application package screens
|
||||
* @param {object} pkg
|
||||
* @param {FetchAppPackageResponse} pkg
|
||||
*/
|
||||
syncAppScreens(pkg: { screens: Screen[] }) {
|
||||
syncAppScreens(pkg: FetchAppPackageResponse) {
|
||||
this.update(state => ({
|
||||
...state,
|
||||
screens: [...pkg.screens],
|
||||
|
@ -114,7 +122,7 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
* Recursively parses the entire screen doc and checks for components
|
||||
* violating illegal child configurations.
|
||||
*
|
||||
* @param {object} screen
|
||||
* @param {Screen} screen
|
||||
* @throws Will throw an error containing the name of the component causing
|
||||
* the invalid screen state
|
||||
*/
|
||||
|
@ -125,7 +133,7 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
illegalChildren: string[] = [],
|
||||
legalDirectChildren: string[] = []
|
||||
): string | undefined => {
|
||||
const type = component._component
|
||||
const type: string = component._component
|
||||
|
||||
if (illegalChildren.includes(type)) {
|
||||
return type
|
||||
|
@ -148,7 +156,20 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
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
|
||||
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
|
||||
* screen id to ensure that it is selected in the builder
|
||||
*
|
||||
* @param {object} screen
|
||||
* @returns {object}
|
||||
* @param {Screen} screen The screen being modified/created
|
||||
*/
|
||||
async saveScreen(screen: Screen): Promise<Screen> {
|
||||
async saveScreen(screen: Screen) {
|
||||
const appState = get(appStore)
|
||||
|
||||
// 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
|
||||
* @param {object} savedScreen
|
||||
* @param {Screen} savedScreen
|
||||
*/
|
||||
async syncScreenData(savedScreen: Screen) {
|
||||
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.
|
||||
*/
|
||||
sequentialScreenPatch = Utils.sequential(
|
||||
async (
|
||||
patchFn: (screen: Screen) => any,
|
||||
screenId: string
|
||||
): Promise<Screen | undefined> => {
|
||||
async (patchFn: (screen: Screen) => any, screenId: string) => {
|
||||
const state = get(this.store)
|
||||
const screen = state.screens.find(screen => screen._id === screenId)
|
||||
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
|
||||
* @returns
|
||||
*/
|
||||
async patch(
|
||||
patchFn: (screen: Screen) => void,
|
||||
screenId: string | undefined | null
|
||||
) {
|
||||
patchFn: (screen: Screen) => any,
|
||||
screenId?: string | null
|
||||
): Promise<SaveScreenResponse | void> {
|
||||
// Default to the currently selected screen
|
||||
if (!screenId) {
|
||||
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
|
||||
* been removed by another user and will be filtered from the store.
|
||||
* Used to marshal updates for the websocket
|
||||
* @param {string} screenId
|
||||
* @param {object} screen
|
||||
* @returns
|
||||
*
|
||||
* @param {string} screenId the target screen id
|
||||
* @param {Screen} screen the replacement screen
|
||||
*/
|
||||
async replace(screenId: string, screen: Screen) {
|
||||
if (!screenId) {
|
||||
|
@ -346,20 +362,27 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
* Any deleted screens will then have their routes/links purged
|
||||
*
|
||||
* Wrapped by {@link delete}
|
||||
* @param {object | array} screens
|
||||
* @returns
|
||||
* @param {Screen | Screen[]} screens
|
||||
*/
|
||||
async deleteScreen(screen: Screen) {
|
||||
const screensToDelete = [screen]
|
||||
async deleteScreen(screens: Screen | Screen[]) {
|
||||
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
||||
// Build array of promises to speed up bulk deletions
|
||||
let promises: Promise<any>[] = []
|
||||
let promises: Promise<DeleteScreenResponse>[] = []
|
||||
let deleteUrls: string[] = []
|
||||
screensToDelete.forEach(screen => {
|
||||
// Delete the screen
|
||||
promises.push(API.deleteScreen(screen._id!, screen._rev!))
|
||||
// Remove links to this screen
|
||||
deleteUrls.push(screen.routing.route)
|
||||
})
|
||||
|
||||
// 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
|
||||
promises.push(API.deleteScreen(screen._id, screen._rev))
|
||||
// Remove links to this screen
|
||||
deleteUrls.push(screen.routing.route)
|
||||
})
|
||||
await Promise.all(promises)
|
||||
await navigationStore.deleteLink(deleteUrls)
|
||||
const deletedIds = screensToDelete.map(screen => screen._id)
|
||||
|
@ -375,11 +398,11 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
state.selectedScreenId &&
|
||||
deletedIds.includes(state.selectedScreenId)
|
||||
) {
|
||||
state.selectedScreenId = null
|
||||
componentStore.update(state => ({
|
||||
...state,
|
||||
selectedComponentId: null,
|
||||
}))
|
||||
delete state.selectedScreenId
|
||||
componentStore.update(state => {
|
||||
delete state.selectedComponentId
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
// Update routing
|
||||
|
@ -390,7 +413,6 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
|
||||
return state
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -399,12 +421,11 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
* After a successful update, this method ensures that there is only
|
||||
* ONE home screen per user Role.
|
||||
*
|
||||
* @param {object} screen
|
||||
* @param {Screen} screen
|
||||
* @param {string} name e.g "routing.homeScreen" or "showNavigation"
|
||||
* @param {any} value
|
||||
* @returns
|
||||
*/
|
||||
async updateSetting(screen: Screen, name: string, value: string) {
|
||||
async updateSetting(screen: Screen, name: string, value: any) {
|
||||
if (!screen || !name) {
|
||||
return
|
||||
}
|
||||
|
@ -461,11 +482,14 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
/**
|
||||
* Parse the entire screen component tree and ensure settings are valid
|
||||
* and up-to-date. Ensures stability after a product update.
|
||||
* @param {object} screen
|
||||
* @param {Screen} screen
|
||||
*/
|
||||
async enrichEmptySettings(screen: Screen) {
|
||||
// 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
|
||||
components.forEach(component => {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { get, writable } from "svelte/store"
|
|||
import { API } from "@/api"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { componentStore, appStore } from "@/stores/builder"
|
||||
import { INITIAL_SCREENS_STATE, ScreenStore } from "@/stores/builder/screens"
|
||||
import { initialScreenState, ScreenStore } from "@/stores/builder/screens"
|
||||
import {
|
||||
getScreenFixture,
|
||||
getComponentFixture,
|
||||
|
@ -73,7 +73,7 @@ describe("Screens store", () => {
|
|||
vi.clearAllMocks()
|
||||
|
||||
const screenStore = new ScreenStore()
|
||||
ctx.test = {
|
||||
ctx.bb = {
|
||||
get store() {
|
||||
return get(screenStore)
|
||||
},
|
||||
|
@ -81,74 +81,76 @@ describe("Screens store", () => {
|
|||
}
|
||||
})
|
||||
|
||||
it("Create base screen store with defaults", ctx => {
|
||||
expect(ctx.test.store).toStrictEqual(INITIAL_SCREENS_STATE)
|
||||
it("Create base screen store with defaults", ({ bb }) => {
|
||||
expect(bb.store).toStrictEqual(initialScreenState)
|
||||
})
|
||||
|
||||
it("Syncs all screens from the app package", ctx => {
|
||||
expect(ctx.test.store.screens.length).toBe(0)
|
||||
it("Syncs all screens from the app package", ({ bb }) => {
|
||||
expect(bb.store.screens.length).toBe(0)
|
||||
|
||||
const screens = Array(2)
|
||||
.fill()
|
||||
.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 => {
|
||||
expect(ctx.test.store.screens.length).toBe(0)
|
||||
it("Reset the screen store back to the default state", ({ bb }) => {
|
||||
expect(bb.store.screens.length).toBe(0)
|
||||
|
||||
const screens = Array(2)
|
||||
.fill()
|
||||
.map(() => getScreenFixture().json())
|
||||
|
||||
ctx.test.screenStore.syncAppScreens({ screens })
|
||||
expect(ctx.test.store.screens).toStrictEqual(screens)
|
||||
bb.screenStore.syncAppScreens({ screens })
|
||||
expect(bb.store.screens).toStrictEqual(screens)
|
||||
|
||||
ctx.test.screenStore.update(state => ({
|
||||
bb.screenStore.update(state => ({
|
||||
...state,
|
||||
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)
|
||||
.fill()
|
||||
.map(() => getScreenFixture().json())
|
||||
|
||||
ctx.test.screenStore.syncAppScreens({ screens })
|
||||
expect(ctx.test.store.screens.length).toBe(2)
|
||||
bb.screenStore.syncAppScreens({ screens })
|
||||
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)
|
||||
.fill()
|
||||
.map(() => getScreenFixture().json())
|
||||
|
||||
ctx.test.screenStore.syncAppScreens({ screens })
|
||||
expect(ctx.test.store.screens.length).toBe(2)
|
||||
bb.screenStore.syncAppScreens({ screens })
|
||||
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()
|
||||
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 formBlock = getComponentFixture(`${COMP_PREFIX}/formblock`)
|
||||
|
||||
|
@ -157,12 +159,12 @@ describe("Screens store", () => {
|
|||
const defSpy = vi.spyOn(componentStore, "getDefinition")
|
||||
defSpy.mockReturnValueOnce(COMPONENT_DEFINITIONS.formblock)
|
||||
|
||||
ctx.test.screenStore.validate(coreScreen.json())
|
||||
bb.screenStore.validate(coreScreen.json())
|
||||
|
||||
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 formOne = getComponentFixture(`${COMP_PREFIX}/form`)
|
||||
|
@ -178,14 +180,14 @@ describe("Screens store", () => {
|
|||
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`
|
||||
)
|
||||
|
||||
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 formOne = getComponentFixture(`${COMP_PREFIX}/form`)
|
||||
|
@ -210,14 +212,16 @@ describe("Screens store", () => {
|
|||
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`
|
||||
)
|
||||
|
||||
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 formOne = getComponentFixture(`${COMP_PREFIX}/form`)
|
||||
|
||||
|
@ -225,7 +229,7 @@ describe("Screens store", () => {
|
|||
|
||||
appStore.set({ features: { componentValidation: false } })
|
||||
|
||||
expect(ctx.test.store.screens.length).toBe(0)
|
||||
expect(bb.store.screens.length).toBe(0)
|
||||
|
||||
const newDocId = getScreenDocId()
|
||||
const newDoc = { ...coreScreen.json(), _id: newDocId }
|
||||
|
@ -235,15 +239,15 @@ describe("Screens store", () => {
|
|||
vi.spyOn(API, "fetchAppRoutes").mockResolvedValue({
|
||||
routes: [],
|
||||
})
|
||||
await ctx.test.screenStore.save(coreScreen.json())
|
||||
await bb.screenStore.save(coreScreen.json())
|
||||
|
||||
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
|
||||
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)
|
||||
.fill()
|
||||
.map(() => {
|
||||
|
@ -261,7 +265,7 @@ describe("Screens store", () => {
|
|||
return screenDoc
|
||||
})
|
||||
|
||||
ctx.test.screenStore.update(state => ({
|
||||
bb.screenStore.update(state => ({
|
||||
...state,
|
||||
screens: existingScreens.map(screen => screen.json()),
|
||||
}))
|
||||
|
@ -279,16 +283,18 @@ describe("Screens store", () => {
|
|||
})
|
||||
|
||||
// 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(saveSpy).toHaveBeenCalled()
|
||||
|
||||
// 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 newDocId = getScreenDocId()
|
||||
|
@ -318,7 +324,7 @@ describe("Screens store", () => {
|
|||
routes: [],
|
||||
})
|
||||
|
||||
await ctx.test.screenStore.syncScreenData(newDoc)
|
||||
await bb.screenStore.syncScreenData(newDoc)
|
||||
|
||||
expect(routeSpy).toHaveBeenCalled()
|
||||
expect(appPackageSpy).toHaveBeenCalled()
|
||||
|
@ -326,7 +332,9 @@ describe("Screens store", () => {
|
|||
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 newDocId = getScreenDocId()
|
||||
|
@ -343,7 +351,7 @@ describe("Screens store", () => {
|
|||
routes: [],
|
||||
})
|
||||
|
||||
await ctx.test.screenStore.syncScreenData(newDoc)
|
||||
await bb.screenStore.syncScreenData(newDoc)
|
||||
|
||||
expect(routeSpy).toHaveBeenCalled()
|
||||
expect(appPackageSpy).not.toHaveBeenCalled()
|
||||
|
@ -352,46 +360,48 @@ describe("Screens store", () => {
|
|||
expect(get(appStore).usedPlugins).toStrictEqual([plugin])
|
||||
})
|
||||
|
||||
it("Proceed to patch if appropriate config are supplied", async ctx => {
|
||||
vi.spyOn(ctx.test.screenStore, "sequentialScreenPatch").mockImplementation(
|
||||
() => {
|
||||
return false
|
||||
}
|
||||
)
|
||||
it("Proceed to patch if appropriate config are supplied", async ({ bb }) => {
|
||||
vi.spyOn(bb.screenStore, "sequentialScreenPatch").mockImplementation(() => {
|
||||
return false
|
||||
})
|
||||
const noop = () => {}
|
||||
|
||||
await ctx.test.screenStore.patch(noop, "test")
|
||||
expect(ctx.test.screenStore.sequentialScreenPatch).toHaveBeenCalledWith(
|
||||
await bb.screenStore.patch(noop, "test")
|
||||
expect(bb.screenStore.sequentialScreenPatch).toHaveBeenCalledWith(
|
||||
noop,
|
||||
"test"
|
||||
)
|
||||
})
|
||||
|
||||
it("Return from the patch if all valid config are not present", async ctx => {
|
||||
vi.spyOn(ctx.test.screenStore, "sequentialScreenPatch")
|
||||
await ctx.test.screenStore.patch()
|
||||
expect(ctx.test.screenStore.sequentialScreenPatch).not.toBeCalled()
|
||||
it("Return from the patch if all valid config are not present", async ({
|
||||
bb,
|
||||
}) => {
|
||||
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 => {
|
||||
vi.spyOn(ctx.test.screenStore, "sequentialScreenPatch")
|
||||
await ctx.test.screenStore.patch()
|
||||
it("Acquire the currently selected screen on patch, if not specified", async ({
|
||||
bb,
|
||||
}) => {
|
||||
vi.spyOn(bb.screenStore, "sequentialScreenPatch")
|
||||
await bb.screenStore.patch()
|
||||
|
||||
const noop = () => {}
|
||||
ctx.test.screenStore.update(state => ({
|
||||
bb.screenStore.update(state => ({
|
||||
...state,
|
||||
selectedScreenId: "screen_123",
|
||||
}))
|
||||
|
||||
await ctx.test.screenStore.patch(noop)
|
||||
expect(ctx.test.screenStore.sequentialScreenPatch).toHaveBeenCalledWith(
|
||||
await bb.screenStore.patch(noop)
|
||||
expect(bb.screenStore.sequentialScreenPatch).toHaveBeenCalledWith(
|
||||
noop,
|
||||
"screen_123"
|
||||
)
|
||||
})
|
||||
|
||||
// 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)
|
||||
.fill()
|
||||
.map(() => {
|
||||
|
@ -400,14 +410,16 @@ describe("Screens store", () => {
|
|||
screenDoc._json._id = existingDocId
|
||||
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)
|
||||
.fill()
|
||||
.map(() => {
|
||||
|
@ -416,17 +428,17 @@ describe("Screens store", () => {
|
|||
screenDoc._json._id = existingDocId
|
||||
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(
|
||||
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)
|
||||
.fill()
|
||||
.map(() => {
|
||||
|
@ -436,7 +448,7 @@ describe("Screens store", () => {
|
|||
return screenDoc
|
||||
})
|
||||
|
||||
ctx.test.screenStore.update(state => ({
|
||||
bb.screenStore.update(state => ({
|
||||
...state,
|
||||
screens: existingScreens.map(screen => screen.json()),
|
||||
}))
|
||||
|
@ -444,15 +456,14 @@ describe("Screens store", () => {
|
|||
const formBlock = getComponentFixture(`${COMP_PREFIX}/formblock`)
|
||||
existingScreens[2].addChild(formBlock)
|
||||
|
||||
ctx.test.screenStore.replace(
|
||||
existingScreens[2]._id,
|
||||
existingScreens[2].json()
|
||||
)
|
||||
bb.screenStore.replace(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)
|
||||
.fill()
|
||||
.map(() => {
|
||||
|
@ -462,7 +473,7 @@ describe("Screens store", () => {
|
|||
return screenDoc
|
||||
})
|
||||
|
||||
ctx.test.screenStore.update(state => ({
|
||||
bb.screenStore.update(state => ({
|
||||
...state,
|
||||
screens: existingScreens.map(screen => screen.json()),
|
||||
}))
|
||||
|
@ -470,13 +481,13 @@ describe("Screens store", () => {
|
|||
const newScreenDoc = getScreenFixture()
|
||||
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(ctx.test.store.screens[4]).toStrictEqual(newScreenDoc.json())
|
||||
expect(bb.store.screens.length).toBe(5)
|
||||
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)
|
||||
.fill()
|
||||
.map(() => {
|
||||
|
@ -486,14 +497,14 @@ describe("Screens store", () => {
|
|||
return screenDoc
|
||||
})
|
||||
|
||||
ctx.test.screenStore.update(state => ({
|
||||
bb.screenStore.update(state => ({
|
||||
...state,
|
||||
screens: existingScreens.map(screen => screen.json()),
|
||||
}))
|
||||
|
||||
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({
|
||||
routes: [],
|
||||
|
@ -501,13 +512,15 @@ describe("Screens store", () => {
|
|||
|
||||
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
|
||||
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)
|
||||
.fill()
|
||||
.map(() => {
|
||||
|
@ -517,7 +530,7 @@ describe("Screens store", () => {
|
|||
return screenDoc
|
||||
})
|
||||
|
||||
ctx.test.screenStore.update(state => ({
|
||||
bb.screenStore.update(state => ({
|
||||
...state,
|
||||
screens: existingScreens.map(screen => screen.json()),
|
||||
selectedScreenId: existingScreens[2]._json._id,
|
||||
|
@ -528,14 +541,16 @@ describe("Screens store", () => {
|
|||
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(get(componentStore).selectedComponentId).toBeNull()
|
||||
expect(ctx.test.store.selectedScreenId).toBeNull()
|
||||
expect(bb.store.screens.length).toBe(2)
|
||||
expect(get(componentStore).selectedComponentId).toBeUndefined()
|
||||
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)
|
||||
.fill()
|
||||
.map(() => {
|
||||
|
@ -547,7 +562,7 @@ describe("Screens store", () => {
|
|||
|
||||
const storeScreens = existingScreens.map(screen => screen.json())
|
||||
|
||||
ctx.test.screenStore.update(state => ({
|
||||
bb.screenStore.update(state => ({
|
||||
...state,
|
||||
screens: existingScreens.map(screen => screen.json()),
|
||||
}))
|
||||
|
@ -556,42 +571,40 @@ describe("Screens store", () => {
|
|||
|
||||
const deleteSpy = vi.spyOn(API, "deleteScreen")
|
||||
|
||||
await ctx.test.screenStore.delete(targets)
|
||||
await bb.screenStore.delete(targets)
|
||||
|
||||
expect(deleteSpy).not.toHaveBeenCalled()
|
||||
expect(ctx.test.store.screens.length).toBe(3)
|
||||
expect(ctx.test.store.screens).toStrictEqual(storeScreens)
|
||||
expect(bb.store.screens.length).toBe(3)
|
||||
expect(bb.store.screens).toStrictEqual(storeScreens)
|
||||
})
|
||||
|
||||
it("Update a screen setting", async ctx => {
|
||||
it("Update a screen setting", async ({ bb }) => {
|
||||
const screenDoc = getScreenFixture()
|
||||
const existingDocId = getScreenDocId()
|
||||
screenDoc._json._id = existingDocId
|
||||
|
||||
await ctx.test.screenStore.update(state => ({
|
||||
await bb.screenStore.update(state => ({
|
||||
...state,
|
||||
screens: [screenDoc.json()],
|
||||
}))
|
||||
|
||||
const patchedDoc = screenDoc.json()
|
||||
const patchSpy = vi
|
||||
.spyOn(ctx.test.screenStore, "patch")
|
||||
.spyOn(bb.screenStore, "patch")
|
||||
.mockImplementation(async patchFn => {
|
||||
patchFn(patchedDoc)
|
||||
return
|
||||
})
|
||||
|
||||
await ctx.test.screenStore.updateSetting(
|
||||
patchedDoc,
|
||||
"showNavigation",
|
||||
false
|
||||
)
|
||||
await bb.screenStore.updateSetting(patchedDoc, "showNavigation", false)
|
||||
|
||||
expect(patchSpy).toBeCalled()
|
||||
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)
|
||||
.fill()
|
||||
.map(() => {
|
||||
|
@ -611,23 +624,21 @@ describe("Screens store", () => {
|
|||
// Set the 2nd screen as the home screen
|
||||
storeScreens[1].routing.homeScreen = true
|
||||
|
||||
await ctx.test.screenStore.update(state => ({
|
||||
await bb.screenStore.update(state => ({
|
||||
...state,
|
||||
screens: storeScreens,
|
||||
}))
|
||||
|
||||
const patchSpy = vi
|
||||
.spyOn(ctx.test.screenStore, "patch")
|
||||
.spyOn(bb.screenStore, "patch")
|
||||
.mockImplementation(async (patchFn, screenId) => {
|
||||
const target = ctx.test.store.screens.find(
|
||||
screen => screen._id === screenId
|
||||
)
|
||||
const target = bb.store.screens.find(screen => screen._id === screenId)
|
||||
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],
|
||||
"routing.homeScreen",
|
||||
true
|
||||
|
@ -637,13 +648,15 @@ describe("Screens store", () => {
|
|||
expect(patchSpy).toBeCalledTimes(2)
|
||||
|
||||
// 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
|
||||
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 = [
|
||||
Constants.Roles.BASIC,
|
||||
Constants.Roles.POWER,
|
||||
|
@ -675,30 +688,24 @@ describe("Screens store", () => {
|
|||
sorted[9].routing.homeScreen = true
|
||||
|
||||
// Set screens state
|
||||
await ctx.test.screenStore.update(state => ({
|
||||
await bb.screenStore.update(state => ({
|
||||
...state,
|
||||
screens: sorted,
|
||||
}))
|
||||
|
||||
const patchSpy = vi
|
||||
.spyOn(ctx.test.screenStore, "patch")
|
||||
.spyOn(bb.screenStore, "patch")
|
||||
.mockImplementation(async (patchFn, screenId) => {
|
||||
const target = ctx.test.store.screens.find(
|
||||
screen => screen._id === screenId
|
||||
)
|
||||
const target = bb.store.screens.find(screen => screen._id === screenId)
|
||||
patchFn(target)
|
||||
|
||||
await ctx.test.screenStore.replace(screenId, target)
|
||||
await bb.screenStore.replace(screenId, target)
|
||||
})
|
||||
|
||||
// ADMIN homeScreen updated from 0 to 2
|
||||
await ctx.test.screenStore.updateSetting(
|
||||
sorted[2],
|
||||
"routing.homeScreen",
|
||||
true
|
||||
)
|
||||
await bb.screenStore.updateSetting(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) {
|
||||
acc[screen.routing.roleId] = acc[screen.routing.roleId] || []
|
||||
acc[screen.routing.roleId].push(screen)
|
||||
|
@ -706,7 +713,7 @@ describe("Screens store", () => {
|
|||
return acc
|
||||
}, {})
|
||||
|
||||
const screens = ctx.test.store.screens
|
||||
const screens = bb.store.screens
|
||||
// Should still only be one of each homescreen
|
||||
expect(results[Constants.Roles.ADMIN].length).toBe(1)
|
||||
expect(screens[2].routing.homeScreen).toBe(true)
|
||||
|
@ -724,74 +731,80 @@ describe("Screens store", () => {
|
|||
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 existingDocId = getScreenDocId()
|
||||
screenDoc._json._id = existingDocId
|
||||
|
||||
const original = screenDoc.json()
|
||||
|
||||
await ctx.test.screenStore.update(state => ({
|
||||
await bb.screenStore.update(state => ({
|
||||
...state,
|
||||
screens: [original],
|
||||
}))
|
||||
|
||||
const saveSpy = vi
|
||||
.spyOn(ctx.test.screenStore, "save")
|
||||
.spyOn(bb.screenStore, "save")
|
||||
.mockImplementation(async () => {
|
||||
return
|
||||
})
|
||||
|
||||
// A screen with this Id does not exist
|
||||
await ctx.test.screenStore.sequentialScreenPatch(() => {}, "123")
|
||||
await bb.screenStore.sequentialScreenPatch(() => {}, "123")
|
||||
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 existingDocId = getScreenDocId()
|
||||
screenDoc._json._id = existingDocId
|
||||
|
||||
const original = screenDoc.json()
|
||||
// Set screens state
|
||||
await ctx.test.screenStore.update(state => ({
|
||||
await bb.screenStore.update(state => ({
|
||||
...state,
|
||||
screens: [original],
|
||||
}))
|
||||
|
||||
const saveSpy = vi
|
||||
.spyOn(ctx.test.screenStore, "save")
|
||||
.spyOn(bb.screenStore, "save")
|
||||
.mockImplementation(async () => {
|
||||
return
|
||||
})
|
||||
|
||||
// Returning false from the patch will abort the save
|
||||
await ctx.test.screenStore.sequentialScreenPatch(() => {
|
||||
await bb.screenStore.sequentialScreenPatch(() => {
|
||||
return false
|
||||
}, "123")
|
||||
|
||||
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 existingDocId = getScreenDocId()
|
||||
screenDoc._json._id = existingDocId
|
||||
|
||||
const original = screenDoc.json()
|
||||
|
||||
await ctx.test.screenStore.update(state => ({
|
||||
await bb.screenStore.update(state => ({
|
||||
...state,
|
||||
screens: [original],
|
||||
}))
|
||||
|
||||
const saveSpy = vi
|
||||
.spyOn(ctx.test.screenStore, "save")
|
||||
.spyOn(bb.screenStore, "save")
|
||||
.mockImplementation(async () => {
|
||||
return
|
||||
})
|
||||
|
||||
await ctx.test.screenStore.sequentialScreenPatch(screen => {
|
||||
await bb.screenStore.sequentialScreenPatch(screen => {
|
||||
screen.name = "updated"
|
||||
}, existingDocId)
|
||||
|
||||
|
|
|
@ -20,9 +20,9 @@ import {
|
|||
Automation,
|
||||
Datasource,
|
||||
Role,
|
||||
Screen,
|
||||
Table,
|
||||
UIUser,
|
||||
Screen,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const createBuilderWebsocket = (appId: string) => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||
import { createAdminStore } from "./admin"
|
||||
import { AdminStore } from "./admin"
|
||||
import { writable, get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { auth } from "@/stores/portal"
|
||||
|
@ -46,16 +46,7 @@ describe("admin store", () => {
|
|||
ctx.writableReturn = { update: vi.fn(), subscribe: vi.fn() }
|
||||
writable.mockReturnValue(ctx.writableReturn)
|
||||
|
||||
ctx.returnedStore = createAdminStore()
|
||||
})
|
||||
|
||||
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(),
|
||||
})
|
||||
ctx.returnedStore = new AdminStore()
|
||||
})
|
||||
|
||||
describe("init method", () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { auth } from "@/stores/portal"
|
||||
import { banner } from "@budibase/bbui"
|
||||
|
@ -7,42 +7,44 @@ import {
|
|||
GetEnvironmentResponse,
|
||||
SystemStatusResponse,
|
||||
} from "@budibase/types"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
|
||||
interface PortalAdminStore extends GetEnvironmentResponse {
|
||||
interface AdminState extends GetEnvironmentResponse {
|
||||
loaded: boolean
|
||||
checklist?: ConfigChecklistResponse
|
||||
status?: SystemStatusResponse
|
||||
}
|
||||
|
||||
export function createAdminStore() {
|
||||
const admin = writable<PortalAdminStore>({
|
||||
loaded: false,
|
||||
multiTenancy: false,
|
||||
cloud: false,
|
||||
isDev: false,
|
||||
disableAccountPortal: false,
|
||||
offlineMode: false,
|
||||
maintenance: [],
|
||||
})
|
||||
export class AdminStore extends BudiStore<AdminState> {
|
||||
constructor() {
|
||||
super({
|
||||
loaded: false,
|
||||
multiTenancy: false,
|
||||
cloud: false,
|
||||
isDev: false,
|
||||
disableAccountPortal: false,
|
||||
offlineMode: false,
|
||||
maintenance: [],
|
||||
})
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await getChecklist()
|
||||
await getEnvironment()
|
||||
async init() {
|
||||
await this.getChecklist()
|
||||
await this.getEnvironment()
|
||||
// enable system status checks in the cloud
|
||||
if (get(admin).cloud) {
|
||||
await getSystemStatus()
|
||||
checkStatus()
|
||||
if (get(this.store).cloud) {
|
||||
await this.getSystemStatus()
|
||||
this.checkStatus()
|
||||
}
|
||||
|
||||
admin.update(store => {
|
||||
this.update(store => {
|
||||
store.loaded = true
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
async function getEnvironment() {
|
||||
async getEnvironment() {
|
||||
const environment = await API.getEnvironment()
|
||||
admin.update(store => {
|
||||
this.update(store => {
|
||||
store.multiTenancy = environment.multiTenancy
|
||||
store.cloud = environment.cloud
|
||||
store.disableAccountPortal = environment.disableAccountPortal
|
||||
|
@ -56,43 +58,36 @@ export function createAdminStore() {
|
|||
})
|
||||
}
|
||||
|
||||
const checkStatus = async () => {
|
||||
const health = get(admin)?.status?.health
|
||||
async checkStatus() {
|
||||
const health = get(this.store).status?.health
|
||||
if (!health?.passing) {
|
||||
await banner.showStatus()
|
||||
}
|
||||
}
|
||||
|
||||
async function getSystemStatus() {
|
||||
async getSystemStatus() {
|
||||
const status = await API.getSystemStatus()
|
||||
admin.update(store => {
|
||||
this.update(store => {
|
||||
store.status = status
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
async function getChecklist() {
|
||||
async getChecklist() {
|
||||
const tenantId = get(auth).tenantId
|
||||
const checklist = await API.getChecklist(tenantId)
|
||||
admin.update(store => {
|
||||
this.update(store => {
|
||||
store.checklist = checklist
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
function unload() {
|
||||
admin.update(store => {
|
||||
unload() {
|
||||
this.update(store => {
|
||||
store.loaded = false
|
||||
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
|
||||
}
|
||||
|
||||
export class AuditLogsStore extends BudiStore<PortalAuditLogsStore> {
|
||||
class AuditLogsStore extends BudiStore<PortalAuditLogsStore> {
|
||||
constructor() {
|
||||
super({})
|
||||
}
|
||||
|
|
|
@ -121,8 +121,8 @@ class AuthStore extends BudiStore<PortalAuthStore> {
|
|||
}
|
||||
}
|
||||
|
||||
async login(username: string, password: string) {
|
||||
const tenantId = get(this.store).tenantId
|
||||
async login(username: string, password: string, targetTenantId?: string) {
|
||||
const tenantId = targetTenantId || get(this.store).tenantId
|
||||
await API.logIn(tenantId, username, password)
|
||||
await this.getSelf()
|
||||
}
|
||||
|
|
|
@ -1,38 +1,31 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
|
||||
type GotoFuncType = (path: string) => void
|
||||
|
||||
interface PortalNavigationStore {
|
||||
interface NavigationState {
|
||||
initialisated: boolean
|
||||
goto: GotoFuncType
|
||||
}
|
||||
|
||||
export function createNavigationStore() {
|
||||
const store = writable<PortalNavigationStore>({
|
||||
initialisated: false,
|
||||
goto: undefined as any,
|
||||
})
|
||||
const { set, subscribe } = store
|
||||
class NavigationStore extends BudiStore<NavigationState> {
|
||||
constructor() {
|
||||
super({
|
||||
initialisated: false,
|
||||
goto: undefined as any,
|
||||
})
|
||||
}
|
||||
|
||||
const init = (gotoFunc: GotoFuncType) => {
|
||||
init(gotoFunc: GotoFuncType) {
|
||||
if (typeof gotoFunc !== "function") {
|
||||
throw new Error(
|
||||
`gotoFunc must be a function, found a "${typeof gotoFunc}" instead`
|
||||
)
|
||||
}
|
||||
|
||||
set({
|
||||
this.set({
|
||||
initialisated: true,
|
||||
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 settingsDefinitionMap
|
||||
let missingRequiredSettings = false
|
||||
let componentErrors = false
|
||||
|
||||
// Temporary styles which can be added in the app preview for things like
|
||||
// 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
|
||||
// coerced to booleans
|
||||
$: componentErrors = instance?._meta?.errors
|
||||
$: hasChildren = !!definition?.hasChildren
|
||||
$: showEmptyState = definition?.showEmptyState !== false
|
||||
$: hasMissingRequiredSettings = missingRequiredSettings?.length > 0
|
||||
$: editable = !!definition?.editable && !hasMissingRequiredSettings
|
||||
$: hasComponentErrors = componentErrors?.length > 0
|
||||
$: requiredAncestors = definition?.requiredAncestors || []
|
||||
$: missingRequiredAncestors = requiredAncestors.filter(
|
||||
ancestor => !$component.ancestors.includes(`${BudibasePrefix}${ancestor}`)
|
||||
)
|
||||
$: hasMissingRequiredAncestors = missingRequiredAncestors?.length > 0
|
||||
$: errorState = hasMissingRequiredSettings || hasMissingRequiredAncestors
|
||||
$: errorState =
|
||||
hasMissingRequiredSettings ||
|
||||
hasMissingRequiredAncestors ||
|
||||
hasComponentErrors
|
||||
|
||||
// Interactive components can be selected, dragged and highlighted inside
|
||||
// the builder preview
|
||||
|
@ -692,6 +698,7 @@
|
|||
<ComponentErrorState
|
||||
{missingRequiredSettings}
|
||||
{missingRequiredAncestors}
|
||||
{componentErrors}
|
||||
/>
|
||||
{:else}
|
||||
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
| { key: string; label: string }[]
|
||||
| undefined
|
||||
export let missingRequiredAncestors: string[] | undefined
|
||||
export let componentErrors: string[] | undefined
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable, builderStore } = getContext("sdk")
|
||||
|
@ -15,6 +16,7 @@
|
|||
$: styles = { ...$component.styles, normal: {}, custom: null, empty: true }
|
||||
$: requiredSetting = missingRequiredSettings?.[0]
|
||||
$: requiredAncestor = missingRequiredAncestors?.[0]
|
||||
$: errorMessage = componentErrors?.[0]
|
||||
</script>
|
||||
|
||||
{#if $builderStore.inBuilder}
|
||||
|
@ -23,6 +25,8 @@
|
|||
<Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" />
|
||||
{#if requiredAncestor}
|
||||
<MissingRequiredAncestor {requiredAncestor} />
|
||||
{:else if errorMessage}
|
||||
{errorMessage}
|
||||
{:else if requiredSetting}
|
||||
<MissingRequiredSetting {requiredSetting} />
|
||||
{/if}
|
||||
|
@ -34,7 +38,7 @@
|
|||
.component-placeholder {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
font-size: var(--font-size-s);
|
||||
|
|
|
@ -43,6 +43,7 @@ const loadBudibase = async () => {
|
|||
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
|
||||
location: window["##BUDIBASE_LOCATION##"],
|
||||
snippets: window["##BUDIBASE_SNIPPETS##"],
|
||||
componentErrors: window["##BUDIBASE_COMPONENT_ERRORS##"],
|
||||
})
|
||||
|
||||
// Set app ID - this window flag is set by both the preview and the real
|
||||
|
|
|
@ -19,6 +19,7 @@ const createBuilderStore = () => {
|
|||
eventResolvers: {},
|
||||
metadata: null,
|
||||
snippets: null,
|
||||
componentErrors: {},
|
||||
|
||||
// Legacy - allow the builder to specify a layout
|
||||
layout: null,
|
||||
|
|
|
@ -42,6 +42,14 @@ const createScreenStore = () => {
|
|||
if ($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 {
|
||||
// Find the correct screen by matching the current route
|
||||
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 {
|
||||
DataFetchDefinition,
|
||||
|
@ -10,12 +8,10 @@ import { enrichSchemaWithRelColumns, memo } from "../../../utils"
|
|||
import { cloneDeep } from "lodash"
|
||||
import {
|
||||
SaveRowRequest,
|
||||
SaveTableRequest,
|
||||
UIDatasource,
|
||||
UIFieldMutation,
|
||||
UIFieldSchema,
|
||||
UIRow,
|
||||
UpdateViewRequest,
|
||||
ViewV2Type,
|
||||
} from "@budibase/types"
|
||||
import { Store as StoreContext, BaseStoreProps } from "."
|
||||
|
@ -79,7 +75,7 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
|
|||
const schema = derived(definition, $definition => {
|
||||
const schema: Record<string, any> | undefined = getDatasourceSchema({
|
||||
API,
|
||||
datasource: get(datasource) as any, // TODO: see line 1
|
||||
datasource: get(datasource),
|
||||
definition: $definition ?? undefined,
|
||||
})
|
||||
if (!schema) {
|
||||
|
@ -137,7 +133,7 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
|
|||
let type = $datasource?.type
|
||||
// @ts-expect-error
|
||||
if (type === "provider") {
|
||||
type = ($datasource as any).value?.datasource?.type // TODO: see line 1
|
||||
type = ($datasource as any).value?.datasource?.type
|
||||
}
|
||||
// Handle calculation views
|
||||
if (
|
||||
|
@ -196,15 +192,13 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
|
|||
const refreshDefinition = async () => {
|
||||
const def = await getDatasourceDefinition({
|
||||
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
|
||||
const saveDefinition = async (
|
||||
newDefinition: SaveTableRequest | UpdateViewRequest
|
||||
) => {
|
||||
const saveDefinition = async (newDefinition: DataFetchDefinition) => {
|
||||
// Update local state
|
||||
const originalDefinition = get(definition)
|
||||
definition.set(newDefinition)
|
||||
|
@ -245,7 +239,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
|
|||
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
|
||||
|
@ -321,7 +315,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
|
|||
await saveDefinition({
|
||||
...$definition,
|
||||
schema: newSchema,
|
||||
} as any) // TODO: see line 1
|
||||
})
|
||||
resetSchemaMutations()
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ export default class ViewFetch extends BaseDataFetch<ViewV1Datasource, Table> {
|
|||
|
||||
getSchema(definition: Table) {
|
||||
const { datasource } = this.options
|
||||
return definition?.views?.[datasource.name]?.schema
|
||||
return definition?.views?.[datasource?.name]?.schema
|
||||
}
|
||||
|
||||
async getData() {
|
||||
|
|
|
@ -101,12 +101,12 @@ export const fetchData = <
|
|||
|
||||
// Creates an empty fetch instance with no datasource configured, so no data
|
||||
// will initially be loaded
|
||||
const createEmptyFetchInstance = ({
|
||||
const createEmptyFetchInstance = <T extends DataFetchDatasource>({
|
||||
API,
|
||||
datasource,
|
||||
}: {
|
||||
API: APIClient
|
||||
datasource: DataFetchDatasource
|
||||
datasource: T
|
||||
}) => {
|
||||
const handler = DataFetchMap[datasource?.type]
|
||||
if (!handler) {
|
||||
|
@ -114,7 +114,7 @@ const createEmptyFetchInstance = ({
|
|||
}
|
||||
return new handler({
|
||||
API,
|
||||
datasource: null as never,
|
||||
datasource: datasource 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
|
||||
* sequentially.
|
||||
* @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 = <
|
||||
TReturn,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"../shared-core/src",
|
||||
"../string-templates/src"
|
||||
],
|
||||
"ext": "js,ts,json,svelte",
|
||||
"ext": "js,ts,json,svelte,hbs",
|
||||
"ignore": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.spec.js",
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as triggers from "../../automations/triggers"
|
|||
import { sdk as coreSdk } from "@budibase/shared-core"
|
||||
import { DocumentType } from "../../db/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 { automations, features } from "@budibase/pro"
|
||||
import {
|
||||
|
@ -231,24 +231,25 @@ export async function test(
|
|||
ctx: UserCtx<TestAutomationRequest, TestAutomationResponse>
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
let automation = await db.get<Automation>(ctx.params.id)
|
||||
await setTestFlag(automation._id!)
|
||||
const testInput = prepareTestInput(ctx.request.body)
|
||||
const response = await triggers.externalTrigger(
|
||||
automation,
|
||||
{
|
||||
...testInput,
|
||||
appId: ctx.appId,
|
||||
user: sdk.users.getUserContextBindings(ctx.user),
|
||||
},
|
||||
{ getResponses: true }
|
||||
)
|
||||
// save a test history run
|
||||
await updateTestHistory(ctx.appId, automation, {
|
||||
...ctx.request.body,
|
||||
occurredAt: new Date().getTime(),
|
||||
const automation = await db.tryGet<Automation>(ctx.params.id)
|
||||
if (!automation) {
|
||||
ctx.throw(404, `Automation ${ctx.params.id} not found`)
|
||||
}
|
||||
|
||||
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,
|
||||
{ ...prepareTestInput(body), appId, user },
|
||||
{ getResponses: true }
|
||||
)
|
||||
})
|
||||
await clearTestFlag(automation._id!)
|
||||
ctx.body = response
|
||||
|
||||
await events.automation.tested(automation)
|
||||
}
|
||||
|
|
|
@ -73,7 +73,8 @@
|
|||
hiddenComponentIds,
|
||||
usedPlugins,
|
||||
location,
|
||||
snippets
|
||||
snippets,
|
||||
componentErrors
|
||||
} = parsed
|
||||
|
||||
// Set some flags so the app knows we're in the builder
|
||||
|
@ -91,6 +92,7 @@
|
|||
window["##BUDIBASE_USED_PLUGINS##"] = usedPlugins
|
||||
window["##BUDIBASE_LOCATION##"] = location
|
||||
window["##BUDIBASE_SNIPPETS##"] = snippets
|
||||
window['##BUDIBASE_COMPONENT_ERRORS##'] = componentErrors
|
||||
|
||||
// Initialise app
|
||||
try {
|
||||
|
|
|
@ -5,8 +5,11 @@ import {
|
|||
sendAutomationAttachmentsToStorage,
|
||||
} from "../automationUtils"
|
||||
import { buildCtx } from "./utils"
|
||||
import { CreateRowStepInputs, CreateRowStepOutputs } from "@budibase/types"
|
||||
import { EventEmitter } from "events"
|
||||
import {
|
||||
ContextEmitter,
|
||||
CreateRowStepInputs,
|
||||
CreateRowStepOutputs,
|
||||
} from "@budibase/types"
|
||||
|
||||
export async function run({
|
||||
inputs,
|
||||
|
@ -15,7 +18,7 @@ export async function run({
|
|||
}: {
|
||||
inputs: CreateRowStepInputs
|
||||
appId: string
|
||||
emitter: EventEmitter
|
||||
emitter: ContextEmitter
|
||||
}): Promise<CreateRowStepOutputs> {
|
||||
if (inputs.row == null || inputs.row.tableId == null) {
|
||||
return {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { EventEmitter } from "events"
|
||||
import { destroy } from "../../api/controllers/row"
|
||||
import { buildCtx } from "./utils"
|
||||
import { getError } from "../automationUtils"
|
||||
import { DeleteRowStepInputs, DeleteRowStepOutputs } from "@budibase/types"
|
||||
import {
|
||||
ContextEmitter,
|
||||
DeleteRowStepInputs,
|
||||
DeleteRowStepOutputs,
|
||||
} from "@budibase/types"
|
||||
|
||||
export async function run({
|
||||
inputs,
|
||||
|
@ -11,7 +14,7 @@ export async function run({
|
|||
}: {
|
||||
inputs: DeleteRowStepInputs
|
||||
appId: string
|
||||
emitter: EventEmitter
|
||||
emitter: ContextEmitter
|
||||
}): Promise<DeleteRowStepOutputs> {
|
||||
if (inputs.id == null) {
|
||||
return {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { EventEmitter } from "events"
|
||||
import * as queryController from "../../api/controllers/query"
|
||||
import { buildCtx } from "./utils"
|
||||
import * as automationUtils from "../automationUtils"
|
||||
import {
|
||||
ContextEmitter,
|
||||
ExecuteQueryStepInputs,
|
||||
ExecuteQueryStepOutputs,
|
||||
} from "@budibase/types"
|
||||
|
@ -14,7 +14,7 @@ export async function run({
|
|||
}: {
|
||||
inputs: ExecuteQueryStepInputs
|
||||
appId: string
|
||||
emitter: EventEmitter
|
||||
emitter: ContextEmitter
|
||||
}): Promise<ExecuteQueryStepOutputs> {
|
||||
if (inputs.query == null) {
|
||||
return {
|
||||
|
|
|
@ -2,10 +2,10 @@ import * as scriptController from "../../api/controllers/script"
|
|||
import { buildCtx } from "./utils"
|
||||
import * as automationUtils from "../automationUtils"
|
||||
import {
|
||||
ContextEmitter,
|
||||
ExecuteScriptStepInputs,
|
||||
ExecuteScriptStepOutputs,
|
||||
} from "@budibase/types"
|
||||
import { EventEmitter } from "events"
|
||||
|
||||
export async function run({
|
||||
inputs,
|
||||
|
@ -16,7 +16,7 @@ export async function run({
|
|||
inputs: ExecuteScriptStepInputs
|
||||
appId: string
|
||||
context: object
|
||||
emitter: EventEmitter
|
||||
emitter: ContextEmitter
|
||||
}): Promise<ExecuteScriptStepOutputs> {
|
||||
if (inputs.code == null) {
|
||||
return {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { EventEmitter } from "events"
|
||||
import * as rowController from "../../api/controllers/row"
|
||||
import * as automationUtils from "../automationUtils"
|
||||
import { buildCtx } from "./utils"
|
||||
import { UpdateRowStepInputs, UpdateRowStepOutputs } from "@budibase/types"
|
||||
import {
|
||||
ContextEmitter,
|
||||
UpdateRowStepInputs,
|
||||
UpdateRowStepOutputs,
|
||||
} from "@budibase/types"
|
||||
|
||||
export async function run({
|
||||
inputs,
|
||||
|
@ -11,7 +14,7 @@ export async function run({
|
|||
}: {
|
||||
inputs: UpdateRowStepInputs
|
||||
appId: string
|
||||
emitter: EventEmitter
|
||||
emitter: ContextEmitter
|
||||
}): Promise<UpdateRowStepOutputs> {
|
||||
if (inputs.rowId == null || inputs.row == null) {
|
||||
return {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { EventEmitter } from "events"
|
||||
import { ContextEmitter } from "@budibase/types"
|
||||
|
||||
export async function getFetchResponse(fetched: any) {
|
||||
let status = fetched.status,
|
||||
|
@ -22,7 +22,7 @@ export async function getFetchResponse(fetched: any) {
|
|||
// opts can contain, body, params and version
|
||||
export function buildCtx(
|
||||
appId: string,
|
||||
emitter?: EventEmitter | null,
|
||||
emitter?: ContextEmitter | null,
|
||||
opts: any = {}
|
||||
) {
|
||||
const ctx: any = {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { v4 as uuidv4 } from "uuid"
|
||||
import { testAutomation } from "../../../api/routes/tests/utilities/TestFunctions"
|
||||
import { BUILTIN_ACTION_DEFINITIONS } from "../../actions"
|
||||
import { TRIGGER_DEFINITIONS } from "../../triggers"
|
||||
import {
|
||||
|
@ -7,7 +6,6 @@ import {
|
|||
AppActionTriggerOutputs,
|
||||
Automation,
|
||||
AutomationActionStepId,
|
||||
AutomationResults,
|
||||
AutomationStep,
|
||||
AutomationStepInputs,
|
||||
AutomationTrigger,
|
||||
|
@ -24,6 +22,7 @@ import {
|
|||
ExecuteQueryStepInputs,
|
||||
ExecuteScriptStepInputs,
|
||||
FilterStepInputs,
|
||||
isDidNotTriggerResponse,
|
||||
LoopStepInputs,
|
||||
OpenAIStepInputs,
|
||||
QueryRowsStepInputs,
|
||||
|
@ -36,6 +35,7 @@ import {
|
|||
SearchFilters,
|
||||
ServerLogStepInputs,
|
||||
SmtpEmailStepInputs,
|
||||
TestAutomationRequest,
|
||||
UpdateRowStepInputs,
|
||||
WebhookTriggerInputs,
|
||||
WebhookTriggerOutputs,
|
||||
|
@ -279,7 +279,7 @@ class StepBuilder extends BaseStepBuilder {
|
|||
class AutomationBuilder extends BaseStepBuilder {
|
||||
private automationConfig: Automation
|
||||
private config: TestConfiguration
|
||||
private triggerOutputs: any
|
||||
private triggerOutputs: TriggerOutputs
|
||||
private triggerSet = false
|
||||
|
||||
constructor(
|
||||
|
@ -398,21 +398,19 @@ class AutomationBuilder extends BaseStepBuilder {
|
|||
|
||||
async run() {
|
||||
const automation = await this.save()
|
||||
const results = await testAutomation(
|
||||
this.config,
|
||||
automation,
|
||||
this.triggerOutputs
|
||||
const response = await this.config.api.automation.test(
|
||||
automation._id!,
|
||||
this.triggerOutputs as TestAutomationRequest
|
||||
)
|
||||
return this.processResults(results)
|
||||
}
|
||||
|
||||
private processResults(results: {
|
||||
body: AutomationResults
|
||||
}): AutomationResults {
|
||||
results.body.steps.shift()
|
||||
if (isDidNotTriggerResponse(response)) {
|
||||
throw new Error(response.message)
|
||||
}
|
||||
|
||||
response.steps.shift()
|
||||
return {
|
||||
trigger: results.body.trigger,
|
||||
steps: results.body.steps,
|
||||
trigger: response.trigger,
|
||||
steps: response.steps,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
AutomationRowEvent,
|
||||
UserBindings,
|
||||
AutomationResults,
|
||||
DidNotTriggerResponse,
|
||||
} from "@budibase/types"
|
||||
import { executeInThread } from "../threads/automation"
|
||||
import { dataFilters, sdk } from "@budibase/shared-core"
|
||||
|
@ -33,14 +34,6 @@ const JOB_OPTS = {
|
|||
import * as automationUtils from "../automations/automationUtils"
|
||||
import { doesTableExist } from "../sdk/app/tables/getters"
|
||||
|
||||
type DidNotTriggerResponse = {
|
||||
outputs: {
|
||||
success: false
|
||||
status: AutomationStatus.STOPPED
|
||||
}
|
||||
message: AutomationStoppedReason.TRIGGER_FILTER_NOT_MET
|
||||
}
|
||||
|
||||
async function getAllAutomations() {
|
||||
const db = context.getAppDB()
|
||||
let automations = await db.allDocs<Automation>(
|
||||
|
@ -156,14 +149,26 @@ export function isAutomationResults(
|
|||
)
|
||||
}
|
||||
|
||||
interface AutomationTriggerParams {
|
||||
fields: Record<string, any>
|
||||
timeout?: number
|
||||
appId?: string
|
||||
user?: UserBindings
|
||||
}
|
||||
|
||||
export async function externalTrigger(
|
||||
automation: Automation,
|
||||
params: {
|
||||
fields: Record<string, any>
|
||||
timeout?: number
|
||||
appId?: string
|
||||
user?: UserBindings
|
||||
},
|
||||
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 } = {}
|
||||
): Promise<AutomationResults | DidNotTriggerResponse | AutomationJob> {
|
||||
if (automation.disabled) {
|
||||
|
|
|
@ -4,12 +4,17 @@ import {
|
|||
JsTimeoutError,
|
||||
setJSRunner,
|
||||
setOnErrorLog,
|
||||
setTestingBackendJS,
|
||||
} from "@budibase/string-templates"
|
||||
import { context, logging } from "@budibase/backend-core"
|
||||
import tracer from "dd-trace"
|
||||
import { IsolatedVM } from "./vm"
|
||||
|
||||
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>) => {
|
||||
return tracer.trace("runJS", {}, () => {
|
||||
try {
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { Automation, FetchAutomationResponse } from "@budibase/types"
|
||||
import {
|
||||
Automation,
|
||||
FetchAutomationResponse,
|
||||
TestAutomationRequest,
|
||||
TestAutomationResponse,
|
||||
} from "@budibase/types"
|
||||
import { Expectations, TestAPI } from "./base"
|
||||
|
||||
export class AutomationAPI extends TestAPI {
|
||||
|
@ -33,4 +38,18 @@ export class AutomationAPI extends TestAPI {
|
|||
})
|
||||
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,
|
||||
UserBindings,
|
||||
isBasicSearchOperator,
|
||||
ContextEmitter,
|
||||
} from "@budibase/types"
|
||||
import {
|
||||
AutomationContext,
|
||||
|
@ -71,6 +72,24 @@ function getLoopIterations(loopStep: LoopStep) {
|
|||
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.
|
||||
* 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 appId: string
|
||||
private automation: Automation
|
||||
private emitter: any
|
||||
private emitter: ContextEmitter
|
||||
private context: AutomationContext
|
||||
private job: Job
|
||||
private loopStepOutputs: LoopStep[]
|
||||
|
@ -270,20 +289,9 @@ class Orchestrator {
|
|||
appId: this.appId,
|
||||
automationId: this.automation._id,
|
||||
})
|
||||
this.context.env = await sdkUtils.getEnvironmentVariables()
|
||||
this.context.user = this.currentUser
|
||||
|
||||
try {
|
||||
const { config } = await configs.getSettingsConfigDoc()
|
||||
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 = {}
|
||||
}
|
||||
await enrichBaseContext(this.context)
|
||||
this.context.user = this.currentUser
|
||||
|
||||
let metadata
|
||||
|
||||
|
|
|
@ -58,30 +58,14 @@ export function checkSlashesInUrl(url: string) {
|
|||
export async function updateEntityMetadata(
|
||||
type: string,
|
||||
entityId: string,
|
||||
updateFn: any
|
||||
updateFn: (metadata: Document) => Document
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
const id = generateMetadataID(type, entityId)
|
||||
// read it to see if it exists, we'll overwrite it no matter what
|
||||
let rev, metadata: Document
|
||||
try {
|
||||
const oldMetadata = await db.get<any>(id)
|
||||
rev = oldMetadata._rev
|
||||
metadata = updateFn(oldMetadata)
|
||||
} catch (err) {
|
||||
rev = null
|
||||
metadata = updateFn({})
|
||||
}
|
||||
const metadata = updateFn((await db.tryGet(id)) || {})
|
||||
metadata._id = id
|
||||
if (rev) {
|
||||
metadata._rev = rev
|
||||
}
|
||||
const response = await db.put(metadata)
|
||||
return {
|
||||
...metadata,
|
||||
_id: id,
|
||||
_rev: response.rev,
|
||||
}
|
||||
return { ...metadata, _id: id, _rev: response.rev }
|
||||
}
|
||||
|
||||
export async function saveEntityMetadata(
|
||||
|
@ -89,26 +73,17 @@ export async function saveEntityMetadata(
|
|||
entityId: string,
|
||||
metadata: Document
|
||||
): Promise<Document> {
|
||||
return updateEntityMetadata(type, entityId, () => {
|
||||
return metadata
|
||||
})
|
||||
return updateEntityMetadata(type, entityId, () => metadata)
|
||||
}
|
||||
|
||||
export async function deleteEntityMetadata(type: string, entityId: string) {
|
||||
const db = context.getAppDB()
|
||||
const id = generateMetadataID(type, entityId)
|
||||
let rev
|
||||
try {
|
||||
const metadata = await db.get<any>(id)
|
||||
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)
|
||||
const metadata = await db.tryGet(id)
|
||||
if (!metadata) {
|
||||
return
|
||||
}
|
||||
await db.remove(metadata)
|
||||
}
|
||||
|
||||
export function escapeDangerousCharacters(string: string) {
|
||||
|
|
|
@ -89,17 +89,22 @@ export async function setDebounce(id: string, seconds: number) {
|
|||
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) {
|
||||
const flag = await flagClient?.get(id)
|
||||
return !!(flag && flag.testing)
|
||||
}
|
||||
|
||||
export async function clearTestFlag(id: string) {
|
||||
await devAppClient.delete(id)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
export function getSocketPubSubClients() {
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@budibase/handlebars-helpers": "^0.13.2",
|
||||
"@budibase/vm-browserify": "^1.1.4",
|
||||
"dayjs": "^1.10.8",
|
||||
"handlebars": "^4.7.8",
|
||||
"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 { getJsHelperList } from "./list"
|
||||
import { iifeWrapper } from "../iife"
|
||||
import { JsTimeoutError, UserScriptError } from "../errors"
|
||||
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.
|
||||
// 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>
|
||||
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
|
||||
// difference in how JS executes on the frontend vs the backend, e.g.
|
||||
// consider this snippet:
|
||||
|
@ -96,10 +103,9 @@ export function processJS(handlebars: string, context: any) {
|
|||
clonedContext = cloneDeep(context)
|
||||
}
|
||||
|
||||
const sandboxContext = {
|
||||
const sandboxContext: Record<string, any> = {
|
||||
$: (path: string) => getContextValue(path, clonedContext),
|
||||
helpers: getJsHelperList(),
|
||||
|
||||
// Proxy to evaluate snippets when running in the browser
|
||||
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
|
||||
const res = { data: runJS(js, sandboxContext) }
|
||||
const res = { data: runJS(js, sandboxContext), logs }
|
||||
return `{{${LITERAL_MARKER} js_result-${JSON.stringify(res)}}}`
|
||||
} catch (error: any) {
|
||||
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 { registerAll, registerMinimum } from "./helpers/index"
|
||||
import { postprocess, preprocess } from "./processors"
|
||||
import { postprocess, postprocessWithLogs, preprocess } from "./processors"
|
||||
import {
|
||||
atob,
|
||||
btoa,
|
||||
FIND_ANY_HBS_REGEX,
|
||||
FIND_HBS_REGEX,
|
||||
findDoubleHbsInstances,
|
||||
frontendWrapJS,
|
||||
isBackendService,
|
||||
prefixStrings,
|
||||
} from "./utilities"
|
||||
import { convertHBSBlock } from "./conversion"
|
||||
import { removeJSRunner, setJSRunner } from "./helpers/javascript"
|
||||
|
||||
import manifest from "./manifest.json"
|
||||
import { ProcessOptions } from "./types"
|
||||
import { Log, ProcessOptions } from "./types"
|
||||
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 { FIND_ANY_HBS_REGEX } from "./utilities"
|
||||
export { setJSRunner, setOnErrorLog } from "./helpers/javascript"
|
||||
|
@ -187,23 +191,27 @@ export function processObjectSync(
|
|||
return object
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
// keep the logging function internal, don't want to add this to the process options directly
|
||||
// as it can't be used for object processing etc.
|
||||
function processStringSyncInternal(
|
||||
str: string,
|
||||
context?: object,
|
||||
opts?: ProcessOptions & { logging: false }
|
||||
): string
|
||||
function processStringSyncInternal(
|
||||
str: string,
|
||||
context?: object,
|
||||
opts?: ProcessOptions & { logging: true }
|
||||
): { result: string; logs: Log[] }
|
||||
function processStringSyncInternal(
|
||||
string: string,
|
||||
context?: object,
|
||||
opts?: ProcessOptions
|
||||
): string {
|
||||
opts?: ProcessOptions & { logging: boolean }
|
||||
): string | { result: string; logs: Log[] } {
|
||||
// Take a copy of input in case of error
|
||||
const input = string
|
||||
if (typeof string !== "string") {
|
||||
throw "Cannot process non-string types."
|
||||
throw new Error("Cannot process non-string types.")
|
||||
}
|
||||
function process(stringPart: string) {
|
||||
// context is needed to check for overlap between helpers and context
|
||||
|
@ -217,16 +225,24 @@ export function processStringSync(
|
|||
},
|
||||
...context,
|
||||
})
|
||||
return postprocess(processedString)
|
||||
return opts?.logging
|
||||
? postprocessWithLogs(processedString)
|
||||
: postprocess(processedString)
|
||||
}
|
||||
try {
|
||||
if (opts && opts.onlyFound) {
|
||||
let logs: Log[] = []
|
||||
const blocks = findHBSBlocks(string)
|
||||
for (let block of blocks) {
|
||||
const outcome = process(block)
|
||||
string = string.replace(block, outcome)
|
||||
if (typeof outcome === "object" && "result" in outcome) {
|
||||
logs = logs.concat(outcome.logs || [])
|
||||
string = string.replace(block, outcome.result)
|
||||
} else {
|
||||
string = string.replace(block, outcome)
|
||||
}
|
||||
}
|
||||
return string
|
||||
return !opts?.logging ? string : { result: string, logs }
|
||||
} else {
|
||||
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
|
||||
* 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 function browserJSSetup() {
|
||||
/**
|
||||
* Use polyfilled vm to run JS scripts in a browser Env
|
||||
*/
|
||||
// tests are in jest - we need to use node VM for these
|
||||
const jsSandbox = isTest() ? vm : browserVM
|
||||
// Use polyfilled vm to run JS scripts in a browser Env
|
||||
setJSRunner((js: string, context: Record<string, any>) => {
|
||||
createContext(context)
|
||||
jsSandbox.createContext(context)
|
||||
|
||||
const wrappedJs = `
|
||||
result = {
|
||||
result: null,
|
||||
error: null,
|
||||
};
|
||||
const wrappedJs = frontendWrapJS(js)
|
||||
|
||||
try {
|
||||
result.result = ${js};
|
||||
} catch (e) {
|
||||
result.error = e;
|
||||
}
|
||||
|
||||
result;
|
||||
`
|
||||
|
||||
const result = runInNewContext(wrappedJs, context, { timeout: 1000 })
|
||||
const result = jsSandbox.runInNewContext(wrappedJs, context)
|
||||
if (result.error) {
|
||||
throw new UserScriptError(result.error)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import { FIND_HBS_REGEX } from "../utilities"
|
||||
import * as preprocessor from "./preprocessor"
|
||||
import type { Preprocessor } from "./preprocessor"
|
||||
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) {
|
||||
// if a literal statement has occurred stop
|
||||
if (typeof output !== "string") {
|
||||
|
@ -16,10 +23,18 @@ function process(output: string, processors: any[], opts?: ProcessOptions) {
|
|||
continue
|
||||
}
|
||||
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) {
|
||||
|
@ -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) {
|
||||
return process(string, postprocessor.processors).result
|
||||
}
|
||||
|
||||
export function postprocessWithLogs(string: string) {
|
||||
return process(string, postprocessor.processors)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { LITERAL_MARKER } from "../helpers/constants"
|
||||
import { Log } from "../types"
|
||||
|
||||
export enum PostProcessorNames {
|
||||
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
|
||||
private readonly fn: PostprocessorFn
|
||||
|
||||
|
@ -23,12 +27,12 @@ class Postprocessor {
|
|||
export const processors = [
|
||||
new Postprocessor(
|
||||
PostProcessorNames.CONVERT_LITERALS,
|
||||
(statement: string) => {
|
||||
(statement: string): { result: any; logs?: Log[] } => {
|
||||
if (
|
||||
typeof statement !== "string" ||
|
||||
!statement.includes(LITERAL_MARKER)
|
||||
) {
|
||||
return statement
|
||||
return { result: statement }
|
||||
}
|
||||
const splitMarkerIndex = statement.indexOf("-")
|
||||
const type = statement.substring(12, splitMarkerIndex)
|
||||
|
@ -38,20 +42,22 @@ export const processors = [
|
|||
)
|
||||
switch (type) {
|
||||
case "string":
|
||||
return value
|
||||
return { result: value }
|
||||
case "number":
|
||||
return parseFloat(value)
|
||||
return { result: parseFloat(value) }
|
||||
case "boolean":
|
||||
return value === "true"
|
||||
return { result: value === "true" }
|
||||
case "object":
|
||||
return JSON.parse(value)
|
||||
case "js_result":
|
||||
return { result: JSON.parse(value) }
|
||||
case "js_result": {
|
||||
// We use the literal helper to process the result of JS expressions
|
||||
// as we want to be able to return any types.
|
||||
// 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",
|
||||
}
|
||||
|
||||
type PreprocessorFn = (statement: string, opts?: ProcessOptions) => string
|
||||
export type PreprocessorFn = (
|
||||
statement: string,
|
||||
opts?: ProcessOptions
|
||||
) => string
|
||||
|
||||
class Preprocessor {
|
||||
export class Preprocessor {
|
||||
name: string
|
||||
private readonly fn: PreprocessorFn
|
||||
|
||||
|
|
|
@ -8,3 +8,11 @@ export interface ProcessOptions {
|
|||
onlyFound?: boolean
|
||||
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
|
||||
|
||||
export const FIND_HBS_REGEX = /{{([^{].*?)}}/g
|
||||
export const FIND_ANY_HBS_REGEX = /{?{{([^{].*?)}}}?/g
|
||||
export const FIND_TRIPLE_HBS_REGEX = /{{{([^{].*?)}}}/g
|
||||
|
||||
const isJest = () => typeof jest !== "undefined"
|
||||
|
||||
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
|
||||
// test the frontend JS functionality.
|
||||
if (isJest()) {
|
||||
if (isTest()) {
|
||||
return false
|
||||
}
|
||||
return typeof window === "undefined"
|
||||
|
@ -86,3 +91,20 @@ export const prefixStrings = (
|
|||
const regexPattern = new RegExp(`\\b(${escapedStrings.join("|")})\\b`, "g")
|
||||
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")
|
||||
})
|
||||
|
||||
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 () => {
|
||||
expect(processJS(`return process`)).toEqual(
|
||||
"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,
|
||||
AutomationActionStepId,
|
||||
AutomationLogPage,
|
||||
AutomationResults,
|
||||
AutomationStatus,
|
||||
AutomationStepDefinition,
|
||||
AutomationTriggerDefinition,
|
||||
AutomationTriggerStepId,
|
||||
DidNotTriggerResponse,
|
||||
Row,
|
||||
} from "../../../documents"
|
||||
import { DocumentDestroyResponse } from "@budibase/nano"
|
||||
|
@ -74,4 +76,10 @@ export interface TestAutomationRequest {
|
|||
fields: Record<string, any>
|
||||
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 { EventEmitter } from "events"
|
||||
import { User } from "../../global"
|
||||
import { ReadStream } from "fs"
|
||||
import { Row } from "../row"
|
||||
import { Table } from "../table"
|
||||
import { AutomationStep, AutomationTrigger } from "./schema"
|
||||
import { ContextEmitter } from "../../../sdk"
|
||||
|
||||
export enum AutomationIOType {
|
||||
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 {
|
||||
automationName: string
|
||||
_rev?: string
|
||||
|
@ -218,7 +226,7 @@ export interface AutomationLogPage {
|
|||
|
||||
export interface AutomationStepInputBase {
|
||||
context: Record<string, any>
|
||||
emitter: EventEmitter
|
||||
emitter: ContextEmitter
|
||||
appId: string
|
||||
apiKey?: string
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export enum FeatureFlag {
|
||||
USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR",
|
||||
CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS = "CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS",
|
||||
|
||||
// Account-portal
|
||||
DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL",
|
||||
|
@ -7,6 +8,7 @@ export enum FeatureFlag {
|
|||
|
||||
export const FeatureFlagDefaults = {
|
||||
[FeatureFlag.USE_ZOD_VALIDATOR]: false,
|
||||
[FeatureFlag.CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS]: false,
|
||||
|
||||
// Account-portal
|
||||
[FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,
|
||||
|
|
|
@ -24,3 +24,18 @@ export type InsertAtPositionFn = (_: {
|
|||
value: string
|
||||
cursor?: { anchor: number }
|
||||
}) => 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 "./components"
|
||||
export * from "./dataFetch"
|
||||
export * from "./datasource"
|
||||
|
|
49
yarn.lock
49
yarn.lock
|
@ -2131,9 +2131,9 @@
|
|||
through2 "^2.0.0"
|
||||
|
||||
"@budibase/pro@npm:@budibase/pro@latest":
|
||||
version "3.2.44"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.44.tgz#90367bb2167aafd8c809e000a57d349e5dc4bb78"
|
||||
integrity sha512-Zv2PBVUZUS6/psOpIRIDlW3jrOHWWPhpQXzCk00kIQJaqjkdcvuTXSedQ70u537sQmLu8JsSWbui9MdfF8ksVw==
|
||||
version "3.2.47"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.47.tgz#150d7b16b14932d03c84bdb0e6d570d490c28a5c"
|
||||
integrity sha512-UeTIq7yzMUK6w/akUsRafoD/Kif6PXv4d7K1arn8GTMjwFm9QYu2hg1YkQ+duNdwyZ/GEPlEAV5SYK+NDgtpdA==
|
||||
dependencies:
|
||||
"@anthropic-ai/sdk" "^0.27.3"
|
||||
"@budibase/backend-core" "*"
|
||||
|
@ -2152,6 +2152,13 @@
|
|||
scim-patch "^0.8.1"
|
||||
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":
|
||||
version "5.10.2"
|
||||
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"
|
||||
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:
|
||||
version "1.0.4"
|
||||
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"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
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:
|
||||
"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==
|
||||
|
@ -18747,7 +18750,7 @@ stringify-object@^3.2.1:
|
|||
is-obj "^1.0.1"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
|
@ -18761,13 +18764,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
|
||||
|
@ -20515,7 +20511,7 @@ worker-farm@1.7.0:
|
|||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
@ -20533,15 +20529,6 @@ wrap-ansi@^5.1.0:
|
|||
string-width "^3.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:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
|
Loading…
Reference in New Issue