Merge remote-tracking branch 'origin/master' into poc/generate-tables-using-ai
This commit is contained in:
commit
b2a2af01a9
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "3.7.2",
|
||||
"version": "3.8.1",
|
||||
"npmClient": "yarn",
|
||||
"concurrency": 20,
|
||||
"command": {
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
"rotating-file-stream": "3.1.0",
|
||||
"sanitize-s3-objectkey": "0.0.1",
|
||||
"semver": "^7.5.4",
|
||||
"tar-fs": "2.1.1",
|
||||
"tar-fs": "2.1.2",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -96,6 +96,24 @@ async function get<T extends Document>(db: Database, id: string): Promise<T> {
|
|||
return cacheItem.doc
|
||||
}
|
||||
|
||||
async function tryGet<T extends Document>(
|
||||
db: Database,
|
||||
id: string
|
||||
): Promise<T | null> {
|
||||
const cache = await getCache()
|
||||
const cacheKey = makeCacheKey(db, id)
|
||||
let cacheItem: CacheItem<T> | null = await cache.get(cacheKey)
|
||||
if (!cacheItem) {
|
||||
const doc = await db.tryGet<T>(id)
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
cacheItem = makeCacheItem(doc)
|
||||
await cache.store(cacheKey, cacheItem)
|
||||
}
|
||||
return cacheItem.doc
|
||||
}
|
||||
|
||||
async function remove(db: Database, docOrId: any, rev?: any): Promise<void> {
|
||||
const cache = await getCache()
|
||||
if (!docOrId) {
|
||||
|
@ -123,10 +141,17 @@ export class Writethrough {
|
|||
return put(this.db, doc, writeRateMs)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `tryGet` instead
|
||||
*/
|
||||
async get<T extends Document>(id: string) {
|
||||
return get<T>(this.db, id)
|
||||
}
|
||||
|
||||
async tryGet<T extends Document>(id: string) {
|
||||
return tryGet<T>(this.db, id)
|
||||
}
|
||||
|
||||
async remove(docOrId: any, rev?: any) {
|
||||
return remove(this.db, docOrId, rev)
|
||||
}
|
||||
|
|
|
@ -47,6 +47,9 @@ export async function getConfig<T extends Config>(
|
|||
export async function save(
|
||||
config: Config
|
||||
): Promise<{ id: string; rev: string }> {
|
||||
if (!config._id) {
|
||||
config._id = generateConfigID(config.type)
|
||||
}
|
||||
const db = context.getGlobalDB()
|
||||
return db.put(config)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ describe("configs", () => {
|
|||
|
||||
const setDbPlatformUrl = async (dbUrl: string) => {
|
||||
const settingsConfig = {
|
||||
_id: configs.generateConfigID(ConfigType.SETTINGS),
|
||||
type: ConfigType.SETTINGS,
|
||||
config: {
|
||||
platformUrl: dbUrl,
|
||||
|
|
|
@ -60,6 +60,11 @@ export const StaticDatabases = {
|
|||
SCIM_LOGS: {
|
||||
name: "scim-logs",
|
||||
},
|
||||
// Used by self-host users making use of Budicloud resources. Introduced when
|
||||
// we started letting self-host users use Budibase AI in the cloud.
|
||||
SELF_HOST_CLOUD: {
|
||||
name: "self-host-cloud",
|
||||
},
|
||||
}
|
||||
|
||||
export const APP_PREFIX = prefixed(DocumentType.APP)
|
||||
|
|
|
@ -157,6 +157,33 @@ export async function doInTenant<T>(
|
|||
return newContext(updates, task)
|
||||
}
|
||||
|
||||
// We allow self-host licensed users to make use of some Budicloud services
|
||||
// (e.g. Budibase AI). When they do this, they use their license key as an API
|
||||
// key. We use that license key to identify the tenant ID, and we set the
|
||||
// context to be self-host using cloud. This affects things like where their
|
||||
// quota documents get stored (because we want to avoid creating a new global
|
||||
// DB for each self-host tenant).
|
||||
export async function doInSelfHostTenantUsingCloud<T>(
|
||||
tenantId: string,
|
||||
task: () => T
|
||||
): Promise<T> {
|
||||
const updates = { tenantId, isSelfHostUsingCloud: true }
|
||||
return newContext(updates, task)
|
||||
}
|
||||
|
||||
export function isSelfHostUsingCloud() {
|
||||
const context = Context.get()
|
||||
return !!context?.isSelfHostUsingCloud
|
||||
}
|
||||
|
||||
export function getSelfHostCloudDB() {
|
||||
const context = Context.get()
|
||||
if (!context || !context.isSelfHostUsingCloud) {
|
||||
throw new Error("Self-host cloud DB not found")
|
||||
}
|
||||
return getDB(StaticDatabases.SELF_HOST_CLOUD.name)
|
||||
}
|
||||
|
||||
export async function doInAppContext<T>(
|
||||
appId: string,
|
||||
task: () => T
|
||||
|
@ -325,6 +352,11 @@ export function getGlobalDB(): Database {
|
|||
if (!context || (env.MULTI_TENANCY && !context.tenantId)) {
|
||||
throw new Error("Global DB not found")
|
||||
}
|
||||
if (context.isSelfHostUsingCloud) {
|
||||
throw new Error(
|
||||
"Global DB not found - self-host users using cloud don't have a global DB"
|
||||
)
|
||||
}
|
||||
return getDB(baseGlobalDBName(context?.tenantId))
|
||||
}
|
||||
|
||||
|
@ -344,6 +376,11 @@ export function getAppDB(opts?: any): Database {
|
|||
if (!appId) {
|
||||
throw new Error("Unable to retrieve app DB - no app ID.")
|
||||
}
|
||||
if (isSelfHostUsingCloud()) {
|
||||
throw new Error(
|
||||
"App DB not found - self-host users using cloud don't have app DBs"
|
||||
)
|
||||
}
|
||||
return getDB(appId, opts)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { GoogleSpreadsheet } from "google-spreadsheet"
|
|||
// keep this out of Budibase types, don't want to expose context info
|
||||
export type ContextMap = {
|
||||
tenantId?: string
|
||||
isSelfHostUsingCloud?: boolean
|
||||
appId?: string
|
||||
identity?: IdentityContext
|
||||
environmentVariables?: Record<string, string>
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
isInvalidISODateString,
|
||||
isValidFilter,
|
||||
isValidISODateString,
|
||||
isValidTime,
|
||||
sqlLog,
|
||||
validateManyToMany,
|
||||
} from "./utils"
|
||||
|
@ -417,6 +418,11 @@ class InternalBuilder {
|
|||
}
|
||||
|
||||
if (typeof input === "string" && schema.type === FieldType.DATETIME) {
|
||||
if (schema.timeOnly) {
|
||||
if (!isValidTime(input)) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
if (isInvalidISODateString(input)) {
|
||||
return null
|
||||
}
|
||||
|
@ -424,6 +430,7 @@ class InternalBuilder {
|
|||
return new Date(input.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import { isValidISODateString, isInvalidISODateString } from "../utils"
|
||||
|
||||
describe("ISO date string validity checks", () => {
|
||||
it("accepts a valid ISO date string without a time", () => {
|
||||
const str = "2013-02-01"
|
||||
const valid = isValidISODateString(str)
|
||||
const invalid = isInvalidISODateString(str)
|
||||
expect(valid).toEqual(true)
|
||||
expect(invalid).toEqual(false)
|
||||
})
|
||||
|
||||
it("accepts a valid ISO date string with a time", () => {
|
||||
const str = "2013-02-01T01:23:45Z"
|
||||
const valid = isValidISODateString(str)
|
||||
const invalid = isInvalidISODateString(str)
|
||||
expect(valid).toEqual(true)
|
||||
expect(invalid).toEqual(false)
|
||||
})
|
||||
|
||||
it("accepts a valid ISO date string with a time and millis", () => {
|
||||
const str = "2013-02-01T01:23:45.678Z"
|
||||
const valid = isValidISODateString(str)
|
||||
const invalid = isInvalidISODateString(str)
|
||||
expect(valid).toEqual(true)
|
||||
expect(invalid).toEqual(false)
|
||||
})
|
||||
|
||||
it("rejects an invalid ISO date string", () => {
|
||||
const str = "2013-523-814T444:22:11Z"
|
||||
const valid = isValidISODateString(str)
|
||||
const invalid = isInvalidISODateString(str)
|
||||
expect(valid).toEqual(false)
|
||||
expect(invalid).toEqual(true)
|
||||
})
|
||||
})
|
|
@ -14,7 +14,7 @@ import environment from "../environment"
|
|||
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
|
||||
const ROW_ID_REGEX = /^\[.*]$/g
|
||||
const ENCODED_SPACE = encodeURIComponent(" ")
|
||||
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}.\d{3}Z)?$/
|
||||
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:.\d{3})?Z)?$/
|
||||
const TIME_REGEX = /^(?:\d{2}:)?(?:\d{2}:)(?:\d{2})$/
|
||||
|
||||
export function isExternalTableID(tableId: string) {
|
||||
|
@ -142,17 +142,17 @@ export function breakRowIdField(_id: string | { _id: string }): any[] {
|
|||
}
|
||||
}
|
||||
|
||||
export function isInvalidISODateString(str: string) {
|
||||
export function isValidISODateString(str: string) {
|
||||
const trimmedValue = str.trim()
|
||||
if (!ISO_DATE_REGEX.test(trimmedValue)) {
|
||||
return false
|
||||
}
|
||||
let d = new Date(trimmedValue)
|
||||
return isNaN(d.getTime())
|
||||
const d = new Date(trimmedValue)
|
||||
return !isNaN(d.getTime())
|
||||
}
|
||||
|
||||
export function isValidISODateString(str: string) {
|
||||
return ISO_DATE_REGEX.test(str.trim())
|
||||
export function isInvalidISODateString(str: string) {
|
||||
return !isValidISODateString(str)
|
||||
}
|
||||
|
||||
export function isValidFilter(value: any) {
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
export let footer: string | undefined = undefined
|
||||
export let open: boolean = false
|
||||
export let searchTerm: string | undefined = undefined
|
||||
export let loading: boolean | undefined = undefined
|
||||
export let loading: boolean | undefined = false
|
||||
export let onOptionMouseenter = () => {}
|
||||
export let onOptionMouseleave = () => {}
|
||||
export let customPopoverHeight: string | undefined = undefined
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
class:is-disabled={disabled}
|
||||
class:is-focused={isFocused}
|
||||
>
|
||||
<!-- We need to ignore prettier here as we want no whitespace -->
|
||||
<!-- prettier-ignore -->
|
||||
<textarea
|
||||
bind:this={textarea}
|
||||
|
@ -90,6 +91,7 @@
|
|||
on:blur
|
||||
on:keypress
|
||||
>{value || ""}</textarea>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -114,6 +114,7 @@
|
|||
inputmode={getInputMode(type)}
|
||||
autocomplete={autocompleteValue}
|
||||
/>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -41,5 +41,7 @@
|
|||
on:blur
|
||||
on:focus
|
||||
on:keyup
|
||||
/>
|
||||
>
|
||||
<slot />
|
||||
</TextField>
|
||||
</Field>
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
export let compare: any = undefined
|
||||
export let onOptionMouseenter = () => {}
|
||||
export let onOptionMouseleave = () => {}
|
||||
export let loading: boolean | undefined = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = (e: CustomEvent<any>) => {
|
||||
|
@ -56,6 +57,7 @@
|
|||
<Field {helpText} {label} {labelPosition} {error} {tooltip}>
|
||||
<Select
|
||||
{quiet}
|
||||
{loading}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{value}
|
||||
|
|
|
@ -7,11 +7,13 @@
|
|||
export let label: string | undefined = undefined
|
||||
export let labelPosition = "above"
|
||||
export let placeholder: string | undefined = undefined
|
||||
export let disabled = false
|
||||
export let readonly: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let error: string | undefined = undefined
|
||||
export let height: number | undefined = undefined
|
||||
export let minHeight: number | undefined = undefined
|
||||
export let helpText: string | undefined = undefined
|
||||
export let updateOnChange: boolean = false
|
||||
|
||||
let textarea: TextArea
|
||||
export function focus() {
|
||||
|
@ -33,11 +35,16 @@
|
|||
<TextArea
|
||||
bind:this={textarea}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{value}
|
||||
{placeholder}
|
||||
{height}
|
||||
{minHeight}
|
||||
{updateOnChange}
|
||||
on:change={onChange}
|
||||
on:keypress
|
||||
/>
|
||||
on:scrollable
|
||||
>
|
||||
<slot />
|
||||
</TextArea>
|
||||
</Field>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
export let color: string | undefined = undefined
|
||||
export let hoverColor: string | undefined = undefined
|
||||
export let tooltip: string | undefined = undefined
|
||||
export let tooltipPosition = TooltipPosition.Bottom
|
||||
export let tooltipPosition: TooltipPosition = TooltipPosition.Bottom
|
||||
export let tooltipType = TooltipType.Default
|
||||
export let tooltipColor: string | undefined = undefined
|
||||
export let tooltipWrap: boolean = true
|
||||
|
|
|
@ -211,9 +211,12 @@ const localeDateFormat = new Intl.DateTimeFormat()
|
|||
|
||||
// Formats a dayjs date according to schema flags
|
||||
export const getDateDisplayValue = (
|
||||
value: dayjs.Dayjs | null,
|
||||
value: dayjs.Dayjs | string | null,
|
||||
{ enableTime = true, timeOnly = false } = {}
|
||||
): string => {
|
||||
if (typeof value === "string") {
|
||||
value = dayjs(value)
|
||||
}
|
||||
if (!value?.isValid()) {
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
declare module "*.png" {
|
||||
const value: string
|
||||
export default value
|
||||
}
|
||||
|
||||
declare module "*.svg" {
|
||||
const value: string
|
||||
export default value
|
||||
}
|
|
@ -134,9 +134,6 @@
|
|||
// Size of the view port
|
||||
let viewDims = {}
|
||||
|
||||
// Edge around the draggable content
|
||||
let contentDragPadding = 200
|
||||
|
||||
// Auto scroll
|
||||
let scrollInterval
|
||||
|
||||
|
@ -234,7 +231,7 @@
|
|||
: {}),
|
||||
}))
|
||||
|
||||
offsetX = offsetX - xBump
|
||||
offsetX -= xBump
|
||||
} else if (e.ctrlKey || e.metaKey) {
|
||||
// Scale the content on scrolling
|
||||
let updatedScale
|
||||
|
@ -261,7 +258,7 @@
|
|||
}
|
||||
: {}),
|
||||
}))
|
||||
offsetY = offsetY - yBump
|
||||
offsetY -= yBump
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -335,8 +332,8 @@
|
|||
scrollX: state.scrollX + xInterval,
|
||||
scrollY: state.scrollY + yInterval,
|
||||
}))
|
||||
offsetX = offsetX + xInterval
|
||||
offsetY = offsetY + yInterval
|
||||
offsetX += xInterval
|
||||
offsetY += yInterval
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -488,22 +485,35 @@
|
|||
const viewToFocusEle = () => {
|
||||
if ($focusElement) {
|
||||
const viewWidth = viewDims.width
|
||||
const viewHeight = viewDims.height
|
||||
|
||||
// The amount to shift the content in order to center the trigger on load.
|
||||
// The content is also padded with `contentDragPadding`
|
||||
// The sidebar offset factors into the left positioning of the content here.
|
||||
const targetX =
|
||||
contentWrap.getBoundingClientRect().x -
|
||||
$focusElement.x +
|
||||
(viewWidth / 2 - $focusElement.width / 2)
|
||||
(viewWidth - $focusElement.width) / 2
|
||||
|
||||
const targetY =
|
||||
contentWrap.getBoundingClientRect().y -
|
||||
$focusElement.y +
|
||||
(viewHeight - $focusElement.height) / 2
|
||||
|
||||
// Update the content position state
|
||||
// Shift the content up slightly to accommodate the padding
|
||||
contentPos.update(state => ({
|
||||
...state,
|
||||
x: targetX,
|
||||
y: -(contentDragPadding / 2),
|
||||
y: Number.isInteger($focusElement.targetY)
|
||||
? $focusElement.targetY
|
||||
: targetY,
|
||||
}))
|
||||
|
||||
// Be sure to set the initial offset correctly
|
||||
offsetX = targetX
|
||||
offsetY = Number.isInteger($focusElement.targetY)
|
||||
? $focusElement.targetY
|
||||
: targetY
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -550,7 +560,6 @@
|
|||
aria-label="Viewport for building automations"
|
||||
on:mouseup={onMouseUp}
|
||||
on:mousemove={Utils.domDebounce(onMouseMove)}
|
||||
style={`--dragPadding: ${contentDragPadding}px;`}
|
||||
>
|
||||
<svg class="draggable-background" style={`--dotSize: ${dotSize};`}>
|
||||
<!-- Small 2px offset to tuck the points under the viewport on load-->
|
||||
|
@ -617,7 +626,6 @@
|
|||
transform-origin: 50% 50%;
|
||||
transform: scale(var(--scale));
|
||||
user-select: none;
|
||||
padding: var(--dragPadding);
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
Tags,
|
||||
Tag,
|
||||
} from "@budibase/bbui"
|
||||
import { AutomationActionStepId } from "@budibase/types"
|
||||
import { AutomationActionStepId, BlockDefinitionTypes } from "@budibase/types"
|
||||
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||
import { admin, licensing } from "@/stores/portal"
|
||||
import { externalActions } from "./ExternalActions"
|
||||
|
@ -124,15 +124,20 @@
|
|||
|
||||
try {
|
||||
const newBlock = automationStore.actions.constructBlock(
|
||||
"ACTION",
|
||||
BlockDefinitionTypes.ACTION,
|
||||
action.stepId,
|
||||
action
|
||||
)
|
||||
|
||||
await automationStore.actions.addBlockToAutomation(
|
||||
newBlock,
|
||||
blockRef ? blockRef.pathTo : block.pathTo
|
||||
)
|
||||
|
||||
// Determine presence of the block before focusing
|
||||
const createdBlock = $selectedAutomation.blockRefs[newBlock.id]
|
||||
const createdBlockLoc = (createdBlock?.pathTo || []).at(-1)
|
||||
await automationStore.actions.selectNode(createdBlockLoc?.id)
|
||||
|
||||
modal.hide()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -167,8 +172,8 @@
|
|||
<img
|
||||
width={20}
|
||||
height={20}
|
||||
src={getExternalAction(action.stepId).icon}
|
||||
alt={getExternalAction(action.stepId).name}
|
||||
src={getExternalAction(action.stepId)?.icon}
|
||||
alt={getExternalAction(action.stepId)?.name}
|
||||
/>
|
||||
<span class="icon-spacing">
|
||||
<Body size="XS">
|
||||
|
|
|
@ -3,30 +3,28 @@
|
|||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
ActionButton,
|
||||
Icon,
|
||||
Layout,
|
||||
Body,
|
||||
Divider,
|
||||
TooltipPosition,
|
||||
TooltipType,
|
||||
Button,
|
||||
Modal,
|
||||
ModalContent,
|
||||
} from "@budibase/bbui"
|
||||
import PropField from "@/components/automation/SetupPanel/PropField.svelte"
|
||||
import AutomationBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
|
||||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
||||
import FlowItemActions from "./FlowItemActions.svelte"
|
||||
import FlowItemStatus from "./FlowItemStatus.svelte"
|
||||
import {
|
||||
automationStore,
|
||||
selectedAutomation,
|
||||
evaluationContext,
|
||||
contextMenuStore,
|
||||
} from "@/stores/builder"
|
||||
import { QueryUtils, Utils, memo } from "@budibase/frontend-core"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
import DragZone from "./DragZone.svelte"
|
||||
import BlockHeader from "../../SetupPanel/BlockHeader.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -41,7 +39,6 @@
|
|||
const memoContext = memo({})
|
||||
|
||||
let drawer
|
||||
let open = true
|
||||
let confirmDeleteModal
|
||||
|
||||
$: memoContext.set($evaluationContext)
|
||||
|
@ -61,6 +58,65 @@
|
|||
branchNode: true,
|
||||
pathTo: (pathTo || []).concat({ branchIdx, branchStepId: step.id }),
|
||||
}
|
||||
|
||||
const getContextMenuItems = () => {
|
||||
return [
|
||||
{
|
||||
icon: "Delete",
|
||||
name: "Delete",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
disabled: false,
|
||||
callback: async () => {
|
||||
const branchSteps = step.inputs?.children[branch.id]
|
||||
if (branchSteps.length) {
|
||||
confirmDeleteModal.show()
|
||||
} else {
|
||||
await automationStore.actions.deleteBranch(
|
||||
branchBlockRef.pathTo,
|
||||
$selectedAutomation.data
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "ArrowLeft",
|
||||
name: "Move left",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
disabled: branchIdx == 0,
|
||||
callback: async () => {
|
||||
automationStore.actions.branchLeft(
|
||||
branchBlockRef.pathTo,
|
||||
$selectedAutomation.data,
|
||||
step
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "ArrowRight",
|
||||
name: "Move right",
|
||||
keyBind: null,
|
||||
visible: true,
|
||||
disabled: isLast,
|
||||
callback: async () => {
|
||||
automationStore.actions.branchRight(
|
||||
branchBlockRef.pathTo,
|
||||
$selectedAutomation.data,
|
||||
step
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const openContextMenu = e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const items = getContextMenuItems()
|
||||
contextMenuStore.open(branch.id, items, { x: e.clientX, y: e.clientY })
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={confirmDeleteModal}>
|
||||
|
@ -112,7 +168,7 @@
|
|||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="flow-item">
|
||||
<div class="flow-item branch">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={`block branch-node hoverable`}
|
||||
|
@ -121,22 +177,20 @@
|
|||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<FlowItemHeader
|
||||
{automation}
|
||||
{open}
|
||||
itemName={branch.name}
|
||||
<div class="block-float">
|
||||
<FlowItemStatus
|
||||
block={step}
|
||||
deleteStep={async () => {
|
||||
const branchSteps = step.inputs?.children[branch.id]
|
||||
if (branchSteps.length) {
|
||||
confirmDeleteModal.show()
|
||||
} else {
|
||||
await automationStore.actions.deleteBranch(
|
||||
branchBlockRef.pathTo,
|
||||
$selectedAutomation.data
|
||||
)
|
||||
}
|
||||
}}
|
||||
{automation}
|
||||
{branch}
|
||||
hideStatus={$view?.dragging}
|
||||
/>
|
||||
</div>
|
||||
<div class="blockSection">
|
||||
<div class="heading">
|
||||
<BlockHeader
|
||||
{automation}
|
||||
block={step}
|
||||
itemName={branch.name}
|
||||
on:update={async e => {
|
||||
let stepUpdate = cloneDeep(step)
|
||||
let branchUpdate = stepUpdate.inputs?.branches.find(
|
||||
|
@ -151,66 +205,36 @@
|
|||
)
|
||||
await automationStore.actions.save(updatedAuto)
|
||||
}}
|
||||
on:toggle={() => (open = !open)}
|
||||
>
|
||||
<div slot="custom-actions" class="branch-actions">
|
||||
/>
|
||||
<div class="actions">
|
||||
<Icon
|
||||
on:click={() => {
|
||||
automationStore.actions.branchLeft(
|
||||
branchBlockRef.pathTo,
|
||||
$selectedAutomation.data,
|
||||
step
|
||||
)
|
||||
}}
|
||||
tooltip={"Move left"}
|
||||
tooltipType={TooltipType.Info}
|
||||
tooltipPosition={TooltipPosition.Top}
|
||||
hoverable
|
||||
disabled={branchIdx == 0}
|
||||
name="ArrowLeft"
|
||||
name="Info"
|
||||
tooltip="Branch sequencing checks each option in order and follows the first one that matches the rules."
|
||||
/>
|
||||
<Icon
|
||||
on:click={() => {
|
||||
automationStore.actions.branchRight(
|
||||
branchBlockRef.pathTo,
|
||||
$selectedAutomation.data,
|
||||
step
|
||||
)
|
||||
on:click={e => {
|
||||
openContextMenu(e)
|
||||
}}
|
||||
tooltip={"Move right"}
|
||||
tooltipType={TooltipType.Info}
|
||||
tooltipPosition={TooltipPosition.Top}
|
||||
size="S"
|
||||
hoverable
|
||||
disabled={isLast}
|
||||
name="ArrowRight"
|
||||
name="MoreSmallList"
|
||||
/>
|
||||
</div>
|
||||
</FlowItemHeader>
|
||||
{#if open}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider noMargin />
|
||||
<div class="blockSection">
|
||||
<!-- Content body for possible slot -->
|
||||
<Layout noPadding>
|
||||
<PropField label="Only run when">
|
||||
<ActionButton fullWidth on:click={drawer.show}>
|
||||
<div class="blockSection filter-button">
|
||||
<PropField label="Only run when:" fullWidth>
|
||||
<div style="width: 100%">
|
||||
<Button secondary on:click={drawer.show}>
|
||||
{editableConditionUI?.groups?.length
|
||||
? "Update condition"
|
||||
: "Add condition"}
|
||||
</ActionButton>
|
||||
</Button>
|
||||
</div>
|
||||
</PropField>
|
||||
<div class="footer">
|
||||
<Icon
|
||||
name="InfoOutline"
|
||||
size="S"
|
||||
color="var(--spectrum-global-color-gray-700)"
|
||||
/>
|
||||
<Body size="XS" color="var(--spectrum-global-color-gray-700)">
|
||||
Only the first branch which matches its condition will run
|
||||
</Body>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="separator" />
|
||||
|
@ -227,6 +251,9 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.filter-button :global(.spectrum-Button) {
|
||||
width: 100%;
|
||||
}
|
||||
.branch-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
|
@ -264,7 +291,7 @@
|
|||
display: inline-block;
|
||||
}
|
||||
.block {
|
||||
width: 480px;
|
||||
width: 360px;
|
||||
font-size: 16px;
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
|
@ -289,4 +316,30 @@
|
|||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.branch-node {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.block-float {
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: -35px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.blockSection .heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.blockSection .heading .actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import DiscordLogo from "assets/discord.svg"
|
||||
import ZapierLogo from "assets/zapier.png"
|
||||
import n8nLogo from "assets/n8n_square.png"
|
||||
import MakeLogo from "assets/make.svg"
|
||||
import SlackLogo from "assets/slack.svg"
|
||||
|
||||
export const externalActions = {
|
||||
zapier: { name: "zapier", icon: ZapierLogo },
|
||||
discord: { name: "discord", icon: DiscordLogo },
|
||||
slack: { name: "slack", icon: SlackLogo },
|
||||
integromat: { name: "integromat", icon: MakeLogo },
|
||||
n8n: { name: "n8n", icon: n8nLogo },
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import DiscordLogo from "assets/discord.svg"
|
||||
import ZapierLogo from "assets/zapier.png"
|
||||
import n8nLogo from "assets/n8n_square.png"
|
||||
import MakeLogo from "assets/make.svg"
|
||||
import SlackLogo from "assets/slack.svg"
|
||||
import { AutomationActionStepId } from "@budibase/types"
|
||||
|
||||
export type ExternalAction = {
|
||||
name: string
|
||||
icon: string
|
||||
}
|
||||
export const externalActions: Partial<
|
||||
Record<AutomationActionStepId, ExternalAction>
|
||||
> = {
|
||||
[AutomationActionStepId.zapier]: { name: "zapier", icon: ZapierLogo },
|
||||
[AutomationActionStepId.discord]: { name: "discord", icon: DiscordLogo },
|
||||
[AutomationActionStepId.slack]: { name: "slack", icon: SlackLogo },
|
||||
[AutomationActionStepId.integromat]: { name: "integromat", icon: MakeLogo },
|
||||
[AutomationActionStepId.n8n]: { name: "n8n", icon: n8nLogo },
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
automationStore,
|
||||
automationHistoryStore,
|
||||
selectedAutomation,
|
||||
appStore,
|
||||
} from "@/stores/builder"
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||
import TestDataModal from "./TestDataModal.svelte"
|
||||
|
@ -19,6 +20,9 @@
|
|||
import { memo } from "@budibase/frontend-core"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import DraggableCanvas from "../DraggableCanvas.svelte"
|
||||
import { onMount } from "svelte"
|
||||
import { environment } from "@/stores/portal"
|
||||
import Count from "../../SetupPanel/Count.svelte"
|
||||
|
||||
export let automation
|
||||
|
||||
|
@ -31,6 +35,10 @@
|
|||
let treeEle
|
||||
let draggable
|
||||
|
||||
let prodErrors
|
||||
|
||||
$: $automationStore.showTestModal === true && testDataModal.show()
|
||||
|
||||
// Memo auto - selectedAutomation
|
||||
$: memoAutomation.set(automation)
|
||||
|
||||
|
@ -63,10 +71,85 @@
|
|||
notifications.error("Error deleting automation")
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await automationStore.actions.initAppSelf()
|
||||
await environment.loadVariables()
|
||||
const response = await automationStore.actions.getLogs({
|
||||
automationId: automation._id,
|
||||
status: "error",
|
||||
})
|
||||
prodErrors = response?.data?.length || 0
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="header" class:scrolling>
|
||||
<div class="header-left">
|
||||
<div class="automation-heading">
|
||||
<div class="actions-left">
|
||||
<div class="automation-name">
|
||||
{automation.name}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions-right">
|
||||
<ActionButton
|
||||
icon="Play"
|
||||
quiet
|
||||
disabled={!automation?.definition?.trigger}
|
||||
on:click={() => {
|
||||
automationStore.update(state => ({ ...state, showTestModal: true }))
|
||||
}}
|
||||
>
|
||||
Run test
|
||||
</ActionButton>
|
||||
<Count
|
||||
count={prodErrors}
|
||||
tooltip={"There are errors in production"}
|
||||
hoverable={false}
|
||||
>
|
||||
<ActionButton
|
||||
icon="Folder"
|
||||
quiet
|
||||
selected={prodErrors}
|
||||
on:click={() => {
|
||||
const params = new URLSearchParams({
|
||||
...(prodErrors ? { open: "error" } : {}),
|
||||
automationId: automation._id,
|
||||
})
|
||||
window.open(
|
||||
`/builder/app/${
|
||||
$appStore.appId
|
||||
}/settings/automations?${params.toString()}`,
|
||||
"_blank"
|
||||
)
|
||||
}}
|
||||
>
|
||||
Logs
|
||||
</ActionButton>
|
||||
</Count>
|
||||
|
||||
{#if !isRowAction}
|
||||
<div class="toggle-active setting-spacing">
|
||||
<Toggle
|
||||
text={automation.disabled ? "Paused" : "Activated"}
|
||||
on:change={automationStore.actions.toggleDisabled(
|
||||
automation._id,
|
||||
automation.disabled
|
||||
)}
|
||||
disabled={!automation?.definition?.trigger}
|
||||
value={!automation.disabled}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-flow">
|
||||
<div class="canvas-heading" class:scrolling>
|
||||
<div class="canvas-controls">
|
||||
<div class="canvas-heading-left">
|
||||
<UndoRedoControl store={automationHistoryStore} showButtonGroup />
|
||||
|
||||
<div class="zoom">
|
||||
|
@ -85,48 +168,10 @@
|
|||
Zoom to fit
|
||||
</Button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<Button
|
||||
icon={"Play"}
|
||||
cta
|
||||
disabled={!automation?.definition?.trigger}
|
||||
on:click={() => {
|
||||
testDataModal.show()
|
||||
}}
|
||||
>
|
||||
Run test
|
||||
</Button>
|
||||
<div class="buttons">
|
||||
{#if !$automationStore.showTestPanel && $automationStore.testResults}
|
||||
<Button
|
||||
secondary
|
||||
icon={"Multiple"}
|
||||
disabled={!$automationStore.testResults}
|
||||
on:click={() => {
|
||||
$automationStore.showTestPanel = true
|
||||
}}
|
||||
>
|
||||
Test details
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !isRowAction}
|
||||
<div class="toggle-active setting-spacing">
|
||||
<Toggle
|
||||
text={automation.disabled ? "Paused" : "Activated"}
|
||||
on:change={automationStore.actions.toggleDisabled(
|
||||
automation._id,
|
||||
automation.disabled
|
||||
)}
|
||||
disabled={!automation?.definition?.trigger}
|
||||
value={!automation.disabled}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="root" bind:this={treeEle}>
|
||||
<div class="root" bind:this={treeEle}>
|
||||
<DraggableCanvas
|
||||
bind:this={draggable}
|
||||
draggableClasses={[
|
||||
|
@ -153,6 +198,7 @@
|
|||
{/if}
|
||||
</span>
|
||||
</DraggableCanvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
|
@ -166,11 +212,50 @@
|
|||
This action cannot be undone.
|
||||
</ConfirmDialog>
|
||||
|
||||
<Modal bind:this={testDataModal} width="30%" zIndex={5}>
|
||||
<Modal
|
||||
bind:this={testDataModal}
|
||||
zIndex={5}
|
||||
on:hide={() => {
|
||||
automationStore.update(state => ({ ...state, showTestModal: false }))
|
||||
}}
|
||||
>
|
||||
<TestDataModal />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.main-flow {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.canvas-heading {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.automation-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background: var(--background);
|
||||
padding: var(--spacing-m) var(--spacing-l);
|
||||
box-sizing: border-box;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
|
||||
.automation-heading .actions-right {
|
||||
display: flex;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.automation-name :global(.spectrum-Heading) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.toggle-active :global(.spectrum-Switch) {
|
||||
margin: 0px;
|
||||
}
|
||||
|
@ -181,12 +266,12 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
.canvas-heading-left {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.header-left :global(div) {
|
||||
.canvas-heading-left :global(div) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
|
@ -207,50 +292,31 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header.scrolling {
|
||||
.canvas-heading.scrolling {
|
||||
background: var(--background);
|
||||
border-bottom: var(--border-light);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
z-index: 1;
|
||||
.canvas-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-l);
|
||||
flex: 0 0 60px;
|
||||
padding-right: var(--spacing-xl);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.header > * {
|
||||
.canvas-controls > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.controls .toggle-active :global(.spectrum-Switch-label) {
|
||||
.toggle-active :global(.spectrum-Switch-label) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.buttons:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.group {
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
|
@ -266,17 +332,17 @@
|
|||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.header-left .group :global(.spectrum-Button),
|
||||
.header-left .group :global(.spectrum-ActionButton),
|
||||
.header-left .group :global(.spectrum-Icon) {
|
||||
.canvas-heading-left .group :global(.spectrum-Button),
|
||||
.canvas-heading-left .group :global(.spectrum-ActionButton),
|
||||
.canvas-heading-left .group :global(.spectrum-Icon) {
|
||||
color: var(--spectrum-global-color-gray-900) !important;
|
||||
}
|
||||
.header-left .group :global(.spectrum-Button),
|
||||
.header-left .group :global(.spectrum-ActionButton) {
|
||||
.canvas-heading-left .group :global(.spectrum-Button),
|
||||
.canvas-heading-left .group :global(.spectrum-ActionButton) {
|
||||
background: var(--spectrum-global-color-gray-200) !important;
|
||||
}
|
||||
.header-left .group :global(.spectrum-Button:hover),
|
||||
.header-left .group :global(.spectrum-ActionButton:hover) {
|
||||
.canvas-heading-left .group :global(.spectrum-Button:hover),
|
||||
.canvas-heading-left .group :global(.spectrum-ActionButton:hover) {
|
||||
background: var(--spectrum-global-color-gray-300) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,38 +1,21 @@
|
|||
<script>
|
||||
import {
|
||||
automationStore,
|
||||
permissions,
|
||||
selectedAutomation,
|
||||
tables,
|
||||
} from "@/stores/builder"
|
||||
import {
|
||||
Icon,
|
||||
Divider,
|
||||
Layout,
|
||||
Detail,
|
||||
Modal,
|
||||
Label,
|
||||
AbsTooltip,
|
||||
} from "@budibase/bbui"
|
||||
import { automationStore, selectedAutomation, tables } from "@/stores/builder"
|
||||
import { Modal } from "@budibase/bbui"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
||||
import RoleSelect from "@/components/design/settings/controls/RoleSelect.svelte"
|
||||
import { ActionStepID, TriggerStepID } from "@/constants/backend/automations"
|
||||
import { ActionStepID } from "@/constants/backend/automations"
|
||||
import { AutomationStepType } from "@budibase/types"
|
||||
import FlowItemActions from "./FlowItemActions.svelte"
|
||||
import FlowItemStatus from "./FlowItemStatus.svelte"
|
||||
import DragHandle from "@/components/design/settings/controls/DraggableList/drag-handle.svelte"
|
||||
import { getContext } from "svelte"
|
||||
import DragZone from "./DragZone.svelte"
|
||||
import InfoDisplay from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
||||
import BlockHeader from "../../SetupPanel/BlockHeader.svelte"
|
||||
|
||||
export let block
|
||||
export let blockRef
|
||||
export let testDataModal
|
||||
export let idx
|
||||
export let automation
|
||||
export let bindings
|
||||
export let draggable = true
|
||||
|
||||
const view = getContext("draggableView")
|
||||
|
@ -40,13 +23,47 @@
|
|||
const contentPos = getContext("contentPos")
|
||||
|
||||
let webhookModal
|
||||
let open = true
|
||||
let showLooping = false
|
||||
let role
|
||||
let blockEle
|
||||
let positionStyles
|
||||
let blockDims
|
||||
|
||||
$: pathSteps = loadSteps(blockRef)
|
||||
|
||||
$: collectBlockExists = pathSteps.some(
|
||||
step => step.stepId === ActionStepID.COLLECT
|
||||
)
|
||||
|
||||
$: isTrigger = block.type === AutomationStepType.TRIGGER
|
||||
$: lastStep = blockRef?.terminating
|
||||
|
||||
$: triggerInfo = sdk.automations.isRowAction($selectedAutomation?.data) && {
|
||||
title: "Automation trigger",
|
||||
tableName: $tables.list.find(
|
||||
x =>
|
||||
x._id === $selectedAutomation.data?.definition?.trigger?.inputs?.tableId
|
||||
)?.name,
|
||||
}
|
||||
|
||||
$: selectedNodeId = $automationStore.selectedNodeId
|
||||
$: selected = block.id === selectedNodeId
|
||||
$: dragging = $view?.moveStep && $view?.moveStep?.id === block.id
|
||||
|
||||
$: if (dragging && blockEle) {
|
||||
updateBlockDims()
|
||||
}
|
||||
|
||||
$: placeholderDims = buildPlaceholderStyles(blockDims)
|
||||
|
||||
// Move the selected item
|
||||
// Listen for scrolling in the content. As its scrolled this will be updated
|
||||
$: move(
|
||||
blockEle,
|
||||
$view?.dragSpot,
|
||||
dragging,
|
||||
$contentPos?.scrollX,
|
||||
$contentPos?.scrollY
|
||||
)
|
||||
|
||||
const updateBlockDims = () => {
|
||||
if (!blockEle) {
|
||||
return
|
||||
|
@ -66,48 +83,8 @@
|
|||
: []
|
||||
}
|
||||
|
||||
$: pathSteps = loadSteps(blockRef)
|
||||
|
||||
$: collectBlockExists = pathSteps.some(
|
||||
step => step.stepId === ActionStepID.COLLECT
|
||||
)
|
||||
$: automationId = automation?._id
|
||||
$: isTrigger = block.type === AutomationStepType.TRIGGER
|
||||
$: lastStep = blockRef?.terminating
|
||||
|
||||
$: loopBlock = pathSteps.find(x => x.blockToLoop === block.id)
|
||||
$: isAppAction = block?.stepId === TriggerStepID.APP
|
||||
$: isAppAction && setPermissions(role)
|
||||
$: isAppAction && getPermissions(automationId)
|
||||
|
||||
$: triggerInfo = sdk.automations.isRowAction($selectedAutomation?.data) && {
|
||||
title: "Automation trigger",
|
||||
tableName: $tables.list.find(
|
||||
x =>
|
||||
x._id === $selectedAutomation.data?.definition?.trigger?.inputs?.tableId
|
||||
)?.name,
|
||||
}
|
||||
|
||||
$: selected = $view?.moveStep && $view?.moveStep?.id === block.id
|
||||
|
||||
$: if (selected && blockEle) {
|
||||
updateBlockDims()
|
||||
}
|
||||
|
||||
$: placeholderDims = buildPlaceholderStyles(blockDims)
|
||||
|
||||
// Move the selected item
|
||||
// Listen for scrolling in the content. As its scrolled this will be updated
|
||||
$: move(
|
||||
blockEle,
|
||||
$view?.dragSpot,
|
||||
selected,
|
||||
$contentPos?.scrollX,
|
||||
$contentPos?.scrollY
|
||||
)
|
||||
|
||||
const move = (block, dragPos, selected, scrollX, scrollY) => {
|
||||
if ((!block && !selected) || !dragPos) {
|
||||
const move = (block, dragPos, dragging, scrollX, scrollY) => {
|
||||
if ((!block && !dragging) || !dragPos) {
|
||||
return
|
||||
}
|
||||
positionStyles = `
|
||||
|
@ -125,52 +102,6 @@
|
|||
--psheight: ${Math.round(height)}px;`
|
||||
}
|
||||
|
||||
async function setPermissions(role) {
|
||||
if (!role || !automationId) {
|
||||
return
|
||||
}
|
||||
await permissions.save({
|
||||
level: "execute",
|
||||
role,
|
||||
resource: automationId,
|
||||
})
|
||||
}
|
||||
|
||||
async function getPermissions(automationId) {
|
||||
if (!automationId) {
|
||||
return
|
||||
}
|
||||
const perms = await permissions.forResource(automationId)
|
||||
if (!perms["execute"]) {
|
||||
role = "BASIC"
|
||||
} else {
|
||||
role = perms["execute"].role
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteStep() {
|
||||
await automationStore.actions.deleteAutomationBlock(blockRef.pathTo)
|
||||
}
|
||||
|
||||
async function removeLooping() {
|
||||
let loopBlockRef = $selectedAutomation.blockRefs[blockRef.looped]
|
||||
await automationStore.actions.deleteAutomationBlock(loopBlockRef.pathTo)
|
||||
}
|
||||
|
||||
async function addLooping() {
|
||||
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
|
||||
const loopBlock = automationStore.actions.constructBlock(
|
||||
"ACTION",
|
||||
"LOOP",
|
||||
loopDefinition
|
||||
)
|
||||
loopBlock.blockToLoop = block.id
|
||||
await automationStore.actions.addBlockToAutomation(
|
||||
loopBlock,
|
||||
blockRef.pathTo
|
||||
)
|
||||
}
|
||||
|
||||
const onHandleMouseDown = e => {
|
||||
if (isTrigger) {
|
||||
e.preventDefault()
|
||||
|
@ -205,143 +136,58 @@
|
|||
<div
|
||||
id={`block-${block.id}`}
|
||||
class={`block ${block.type} hoverable`}
|
||||
class:selected
|
||||
class:dragging
|
||||
class:draggable
|
||||
class:selected
|
||||
>
|
||||
<div class="wrap">
|
||||
{#if $view.dragging && selected}
|
||||
{#if $view.dragging && dragging}
|
||||
<div class="drag-placeholder" style={placeholderDims} />
|
||||
{/if}
|
||||
|
||||
<div
|
||||
bind:this={blockEle}
|
||||
class="block-content"
|
||||
class:dragging={$view.dragging && selected}
|
||||
class:dragging={$view.dragging && dragging}
|
||||
style={positionStyles}
|
||||
on:mousedown={e => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div class="block-float">
|
||||
<FlowItemStatus {block} {automation} hideStatus={$view?.dragging} />
|
||||
</div>
|
||||
{#if draggable}
|
||||
<div
|
||||
class="handle"
|
||||
class:grabbing={selected}
|
||||
class:grabbing={dragging}
|
||||
on:mousedown={onHandleMouseDown}
|
||||
>
|
||||
<DragHandle />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="block-core">
|
||||
{#if loopBlock}
|
||||
<div class="blockSection">
|
||||
<div
|
||||
on:click={() => {
|
||||
showLooping = !showLooping
|
||||
class="block-core"
|
||||
on:click={async () => {
|
||||
await automationStore.actions.selectNode(block.id)
|
||||
}}
|
||||
class="splitHeader"
|
||||
>
|
||||
<div class="center-items">
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
class="spectrum-Icon"
|
||||
style="color:var(--spectrum-global-color-gray-700);"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-Reuse" />
|
||||
</svg>
|
||||
<div class="iconAlign">
|
||||
<Detail size="S">Looping</Detail>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blockTitle">
|
||||
<AbsTooltip type="negative" text="Remove looping">
|
||||
<Icon
|
||||
on:click={removeLooping}
|
||||
hoverable
|
||||
name="DeleteOutline"
|
||||
/>
|
||||
</AbsTooltip>
|
||||
|
||||
<div style="margin-left: 10px;" on:click={() => {}}>
|
||||
<Icon
|
||||
hoverable
|
||||
name={showLooping ? "ChevronDown" : "ChevronUp"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider noMargin />
|
||||
{#if !showLooping}
|
||||
<div class="blockSection">
|
||||
<Layout noPadding gap="S">
|
||||
<AutomationBlockSetup
|
||||
schemaProperties={Object.entries(
|
||||
$automationStore.blockDefinitions.ACTION.LOOP.schema
|
||||
.inputs.properties
|
||||
)}
|
||||
{webhookModal}
|
||||
block={loopBlock}
|
||||
<div class="blockSection block-info">
|
||||
<BlockHeader
|
||||
disabled
|
||||
{automation}
|
||||
{bindings}
|
||||
/>
|
||||
</Layout>
|
||||
</div>
|
||||
<Divider noMargin />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<FlowItemHeader
|
||||
{automation}
|
||||
{open}
|
||||
{block}
|
||||
{testDataModal}
|
||||
{idx}
|
||||
{addLooping}
|
||||
{deleteStep}
|
||||
on:toggle={() => (open = !open)}
|
||||
on:update={async e => {
|
||||
const newName = e.detail
|
||||
if (newName.length === 0) {
|
||||
await automationStore.actions.deleteAutomationName(block.id)
|
||||
} else {
|
||||
await automationStore.actions.saveAutomationName(
|
||||
block.id,
|
||||
newName
|
||||
)
|
||||
}
|
||||
}}
|
||||
on:update={e =>
|
||||
automationStore.actions.updateBlockTitle(block, e.detail)}
|
||||
/>
|
||||
{#if open}
|
||||
<Divider noMargin />
|
||||
<div class="blockSection">
|
||||
<Layout noPadding gap="S">
|
||||
{#if isAppAction}
|
||||
<div>
|
||||
<Label>Role</Label>
|
||||
<RoleSelect bind:value={role} />
|
||||
</div>
|
||||
{/if}
|
||||
<AutomationBlockSetup
|
||||
schemaProperties={Object.entries(
|
||||
block?.schema?.inputs?.properties || {}
|
||||
)}
|
||||
{block}
|
||||
{webhookModal}
|
||||
{automation}
|
||||
{bindings}
|
||||
/>
|
||||
|
||||
{#if isTrigger && triggerInfo}
|
||||
<div class="blockSection">
|
||||
<InfoDisplay
|
||||
title={triggerInfo.title}
|
||||
body="This trigger is tied to your '{triggerInfo.tableName}' table"
|
||||
icon="InfoOutline"
|
||||
/>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -397,7 +243,7 @@
|
|||
display: inline-block;
|
||||
}
|
||||
.block {
|
||||
width: 480px;
|
||||
width: 360px;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
|
@ -419,6 +265,8 @@
|
|||
padding: 6px;
|
||||
color: var(--grey-6);
|
||||
cursor: grab;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.block.draggable .wrap .handle.grabbing {
|
||||
cursor: grabbing;
|
||||
|
@ -469,4 +317,22 @@
|
|||
top: var(--blockPosY);
|
||||
left: var(--blockPosX);
|
||||
}
|
||||
.block-float {
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: -35px;
|
||||
left: 0px;
|
||||
}
|
||||
.block-core {
|
||||
cursor: pointer;
|
||||
}
|
||||
.block.selected .block-content {
|
||||
border-color: var(--spectrum-global-color-blue-700);
|
||||
transition: border 130ms ease-out;
|
||||
}
|
||||
|
||||
.block-info {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
<script lang="ts">
|
||||
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||
import {
|
||||
type FlowItemStatus,
|
||||
DataMode,
|
||||
FilterableRowTriggers,
|
||||
FlowStatusType,
|
||||
} from "@/types/automations"
|
||||
import {
|
||||
type AutomationStep,
|
||||
type AutomationStepResult,
|
||||
type AutomationTrigger,
|
||||
type AutomationTriggerResult,
|
||||
type Branch,
|
||||
type DidNotTriggerResponse,
|
||||
type TestAutomationResponse,
|
||||
type AutomationTriggerStepId,
|
||||
isDidNotTriggerResponse,
|
||||
isTrigger,
|
||||
isBranchStep,
|
||||
isFilterStep,
|
||||
} from "@budibase/types"
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
|
||||
export let block: AutomationStep | AutomationTrigger | undefined
|
||||
export let branch: Branch | undefined
|
||||
export let hideStatus: boolean | undefined = false
|
||||
|
||||
$: blockRef = block?.id ? $selectedAutomation.blockRefs[block?.id] : null
|
||||
$: isTriggerBlock = block ? isTrigger(block) : false
|
||||
|
||||
$: testResults = $automationStore.testResults as TestAutomationResponse
|
||||
$: blockResult = automationStore.actions.processBlockResults(
|
||||
testResults,
|
||||
block
|
||||
)
|
||||
$: flowStatus = getFlowStatus(blockResult)
|
||||
|
||||
const getFlowStatus = (
|
||||
result?:
|
||||
| AutomationTriggerResult
|
||||
| AutomationStepResult
|
||||
| DidNotTriggerResponse
|
||||
): FlowItemStatus | undefined => {
|
||||
if (!result || !block) {
|
||||
return
|
||||
}
|
||||
const outputs = result?.outputs
|
||||
const isFilteredRowTrigger =
|
||||
isTriggerBlock &&
|
||||
isDidNotTriggerResponse(testResults) &&
|
||||
FilterableRowTriggers.includes(block.stepId as AutomationTriggerStepId)
|
||||
|
||||
if (
|
||||
isFilteredRowTrigger ||
|
||||
(isFilterStep(block) && "result" in outputs && outputs.result === false)
|
||||
) {
|
||||
return {
|
||||
message: "Stopped",
|
||||
icon: "Alert",
|
||||
type: FlowStatusType.WARN,
|
||||
}
|
||||
}
|
||||
|
||||
if (branch && isBranchStep(block)) {
|
||||
// Do not give status markers to branch nodes that were not part of the run.
|
||||
if ("branchId" in outputs && outputs.branchId !== branch.id) return
|
||||
|
||||
// Mark branches as stopped when no branch criteria was met
|
||||
if (outputs.success == false) {
|
||||
return {
|
||||
message: "Stopped",
|
||||
icon: "Alert",
|
||||
type: FlowStatusType.WARN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const success = outputs?.success || isTriggerBlock
|
||||
return {
|
||||
message: success ? "Completed" : "Failed",
|
||||
icon: success ? "CheckmarkCircle" : "AlertCircleFilled",
|
||||
type:
|
||||
success || isTriggerBlock
|
||||
? FlowStatusType.SUCCESS
|
||||
: FlowStatusType.ERROR,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flow-item-status">
|
||||
{#if blockRef}
|
||||
{#if isTriggerBlock}
|
||||
<span class="block-type">
|
||||
<ActionButton size="S" active={false} icon="Workflow">
|
||||
Trigger
|
||||
</ActionButton>
|
||||
</span>
|
||||
{:else if blockRef.looped}
|
||||
<ActionButton size="S" active={false} icon="Reuse">Looping</ActionButton>
|
||||
{:else}
|
||||
<span />
|
||||
{/if}
|
||||
{#if blockResult && flowStatus && !hideStatus}
|
||||
<span class={`flow-${flowStatus.type}`}>
|
||||
<ActionButton
|
||||
size="S"
|
||||
icon={flowStatus.icon}
|
||||
tooltip={flowStatus?.tooltip}
|
||||
on:click={async () => {
|
||||
if (branch || !block) {
|
||||
return
|
||||
}
|
||||
await automationStore.actions.selectNode(
|
||||
block?.id,
|
||||
flowStatus.type == FlowStatusType.SUCCESS
|
||||
? DataMode.OUTPUT
|
||||
: DataMode.ERRORS
|
||||
)
|
||||
}}
|
||||
>
|
||||
{flowStatus.message}
|
||||
</ActionButton>
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.flow-item-status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-s);
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.flow-item-status :global(> *) {
|
||||
pointer-events: all;
|
||||
}
|
||||
.flow-item-status .block-type {
|
||||
pointer-events: none;
|
||||
}
|
||||
.flow-item-status :global(.spectrum-ActionButton),
|
||||
.flow-item-status :global(.spectrum-ActionButton .spectrum-Icon) {
|
||||
color: var(--spectrum-alias-text-color-hover);
|
||||
}
|
||||
.flow-success :global(.spectrum-ActionButton) {
|
||||
background-color: var(--spectrum-semantic-positive-color-status);
|
||||
border-color: var(--spectrum-semantic-positive-color-status);
|
||||
}
|
||||
.flow-error :global(.spectrum-ActionButton) {
|
||||
background-color: var(--spectrum-semantic-negative-color-status);
|
||||
border-color: var(--spectrum-semantic-negative-color-status);
|
||||
}
|
||||
.flow-warn :global(.spectrum-ActionButton) {
|
||||
background-color: var(--spectrum-global-color-gray-300);
|
||||
border-color: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.flow-warn :global(.spectrum-ActionButton .spectrum-Icon) {
|
||||
color: var(--spectrum-global-color-yellow-600);
|
||||
}
|
||||
</style>
|
|
@ -46,16 +46,26 @@
|
|||
...userBindings,
|
||||
...settingBindings,
|
||||
]
|
||||
|
||||
onMount(() => {
|
||||
// Register the trigger as the focus element for the automation
|
||||
// Onload, the canvas will use the dimensions to center the step
|
||||
if (stepEle && step.type === "TRIGGER" && !$view.focusEle) {
|
||||
const { width, height, left, right, top, bottom, x, y } =
|
||||
stepEle.getBoundingClientRect()
|
||||
|
||||
view.update(state => ({
|
||||
...state,
|
||||
focusEle: { width, height, left, right, top, bottom, x, y },
|
||||
focusEle: {
|
||||
width,
|
||||
height,
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
x,
|
||||
y,
|
||||
...(step.type === "TRIGGER" ? { targetY: 100 } : {}),
|
||||
},
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -122,7 +122,6 @@
|
|||
await tick()
|
||||
try {
|
||||
await automationStore.actions.test($selectedAutomation.data, testData)
|
||||
$automationStore.showTestPanel = true
|
||||
} catch (error) {
|
||||
notifications.error(error)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,270 @@
|
|||
<script lang="ts">
|
||||
import { ActionButton, Button, Divider, Icon, Modal } from "@budibase/bbui"
|
||||
import {
|
||||
type Automation,
|
||||
type AutomationStep,
|
||||
type AutomationTrigger,
|
||||
type BlockRef,
|
||||
AutomationTriggerStepId,
|
||||
isBranchStep,
|
||||
isTrigger,
|
||||
AutomationFeature,
|
||||
} from "@budibase/types"
|
||||
import { memo } from "@budibase/frontend-core"
|
||||
import {
|
||||
automationStore,
|
||||
selectedAutomation,
|
||||
evaluationContext,
|
||||
} from "@/stores/builder"
|
||||
import BlockData from "../SetupPanel/BlockData.svelte"
|
||||
import BlockProperties from "../SetupPanel/BlockProperties.svelte"
|
||||
import BlockHeader from "../SetupPanel/BlockHeader.svelte"
|
||||
import { PropField } from "../SetupPanel"
|
||||
import RoleSelect from "@/components/common/RoleSelect.svelte"
|
||||
import { type AutomationContext } from "@/stores/builder/automations"
|
||||
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import { getVerticalResizeActions } from "@/components/common/resizable"
|
||||
|
||||
const [resizable, resizableHandle] = getVerticalResizeActions()
|
||||
|
||||
const memoAutomation = memo<Automation | undefined>($selectedAutomation.data)
|
||||
const memoBlock = memo<AutomationStep | AutomationTrigger | undefined>()
|
||||
const memoContext = memo({} as AutomationContext)
|
||||
|
||||
let role: string | undefined
|
||||
let webhookModal: Modal | undefined
|
||||
|
||||
$: memoAutomation.set($selectedAutomation.data)
|
||||
$: memoContext.set($evaluationContext)
|
||||
|
||||
$: selectedNodeId = $automationStore.selectedNodeId
|
||||
$: blockRefs = $selectedAutomation.blockRefs
|
||||
$: blockRef = selectedNodeId ? blockRefs[selectedNodeId] : undefined
|
||||
$: block = automationStore.actions.getBlockByRef($memoAutomation, blockRef)
|
||||
|
||||
$: memoBlock.set(block)
|
||||
|
||||
$: isStep = !!$memoBlock && !isTrigger($memoBlock)
|
||||
$: pathSteps = loadPathSteps(blockRef, $memoAutomation)
|
||||
$: loopBlock = pathSteps.find(
|
||||
x => $memoBlock && x.blockToLoop === $memoBlock.id
|
||||
)
|
||||
|
||||
$: isAppAction = block?.stepId === AutomationTriggerStepId.APP
|
||||
$: isAppAction && fetchPermissions($memoAutomation?._id)
|
||||
$: isAppAction &&
|
||||
automationStore.actions.setPermissions(role, $memoAutomation)
|
||||
|
||||
const fetchPermissions = async (automationId?: string) => {
|
||||
if (!automationId) {
|
||||
return
|
||||
}
|
||||
role = await automationStore.actions.getPermissions(automationId)
|
||||
}
|
||||
|
||||
const loadPathSteps = (
|
||||
blockRef: BlockRef | undefined,
|
||||
automation: Automation | undefined
|
||||
) => {
|
||||
return blockRef && automation
|
||||
? automationStore.actions.getPathSteps(blockRef.pathTo, automation)
|
||||
: []
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={webhookModal}>
|
||||
<CreateWebhookModal />
|
||||
</Modal>
|
||||
|
||||
<div class="panel heading">
|
||||
<div class="details">
|
||||
<BlockHeader
|
||||
automation={$memoAutomation}
|
||||
block={$memoBlock}
|
||||
on:update={e => {
|
||||
if ($memoBlock && !isTrigger($memoBlock)) {
|
||||
automationStore.actions.updateBlockTitle($memoBlock, e.detail)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
on:click={() => {
|
||||
automationStore.actions.selectNode()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if isStep}
|
||||
<div class="step-actions">
|
||||
{#if $memoBlock && !isBranchStep($memoBlock) && ($memoBlock.features?.[AutomationFeature.LOOPING] || !$memoBlock.features)}
|
||||
<ActionButton
|
||||
quiet
|
||||
noPadding
|
||||
icon="RotateCW"
|
||||
on:click={async () => {
|
||||
if (loopBlock) {
|
||||
await automationStore.actions.removeLooping(blockRef)
|
||||
} else {
|
||||
await automationStore.actions.addLooping(blockRef)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loopBlock ? `Stop Looping` : `Loop`}
|
||||
</ActionButton>
|
||||
{/if}
|
||||
<ActionButton
|
||||
quiet
|
||||
noPadding
|
||||
icon="DeleteOutline"
|
||||
on:click={async () => {
|
||||
if (!blockRef) {
|
||||
return
|
||||
}
|
||||
await automationStore.actions.deleteAutomationBlock(blockRef.pathTo)
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</ActionButton>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Divider noMargin />
|
||||
<div class="panel config" use:resizable>
|
||||
<div class="content">
|
||||
{#if loopBlock}
|
||||
<span class="loop">
|
||||
<BlockProperties
|
||||
block={loopBlock}
|
||||
context={$memoContext}
|
||||
automation={$memoAutomation}
|
||||
/>
|
||||
</span>
|
||||
<Divider noMargin />
|
||||
{:else if isAppAction}
|
||||
<PropField label="Role" fullWidth>
|
||||
<RoleSelect bind:value={role} />
|
||||
</PropField>
|
||||
{/if}
|
||||
<span class="props">
|
||||
<BlockProperties
|
||||
block={$memoBlock}
|
||||
context={$memoContext}
|
||||
automation={$memoAutomation}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{#if block?.stepId === AutomationTriggerStepId.WEBHOOK}
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
if (!webhookModal) return
|
||||
webhookModal.show()
|
||||
}}
|
||||
>
|
||||
Set Up Webhook
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
role="separator"
|
||||
class="divider"
|
||||
class:disabled={false}
|
||||
use:resizableHandle
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel data">
|
||||
<BlockData
|
||||
context={$memoContext}
|
||||
block={$memoBlock}
|
||||
automation={$memoAutomation}
|
||||
on:run={() => {
|
||||
automationStore.update(state => ({
|
||||
...state,
|
||||
showTestModal: true,
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.step-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
.heading.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.panel,
|
||||
.config.panel .content {
|
||||
padding: var(--spacing-l);
|
||||
}
|
||||
.config.panel {
|
||||
padding: 0px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 200px;
|
||||
max-height: 550px;
|
||||
transition: height 300ms ease-out, max-height 300ms ease-out;
|
||||
height: 400px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.config.panel .content {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
.config.panel .loop,
|
||||
.config.panel .content .props {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
.data.panel {
|
||||
padding: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.divider {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
transform: translateY(50%);
|
||||
height: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
.divider:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: background 130ms ease-out;
|
||||
}
|
||||
.divider:hover {
|
||||
cursor: row-resize;
|
||||
}
|
||||
.divider:hover:after {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.divider.disabled {
|
||||
cursor: auto;
|
||||
}
|
||||
.divider.disabled:after {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
.details {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,44 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
type AutomationStep,
|
||||
type AutomationTrigger,
|
||||
type EnrichedBinding,
|
||||
} from "@budibase/types"
|
||||
import { PropField } from "."
|
||||
import { type SchemaConfigProps } from "@/types/automations"
|
||||
|
||||
export let block: AutomationStep | AutomationTrigger | undefined = undefined
|
||||
export let context: {} | undefined
|
||||
export let bindings: EnrichedBinding[] | undefined = undefined
|
||||
export let layout: SchemaConfigProps[] | undefined
|
||||
</script>
|
||||
|
||||
{#if layout}
|
||||
{#each layout as config}
|
||||
{#if config.wrapped === false}
|
||||
<svelte:component
|
||||
this={config.comp}
|
||||
{...config?.props ? config.props() : {}}
|
||||
{bindings}
|
||||
{block}
|
||||
{context}
|
||||
on:change={config.onChange}
|
||||
/>
|
||||
{:else}
|
||||
<PropField
|
||||
label={config.title}
|
||||
labelTooltip={config.tooltip || ""}
|
||||
fullWidth
|
||||
>
|
||||
<svelte:component
|
||||
this={config.comp}
|
||||
{...config?.props ? config.props() : {}}
|
||||
{bindings}
|
||||
{block}
|
||||
{context}
|
||||
on:change={config.onChange}
|
||||
/>
|
||||
</PropField>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
|
@ -0,0 +1,389 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
type BaseIOStructure,
|
||||
type AutomationStep,
|
||||
type AutomationTrigger,
|
||||
AutomationActionStepId,
|
||||
AutomationCustomIOType,
|
||||
AutomationIOType,
|
||||
AutomationStepType,
|
||||
} from "@budibase/types"
|
||||
import {
|
||||
AutomationSelector,
|
||||
CronBuilder,
|
||||
DateSelector,
|
||||
ExecuteScript,
|
||||
ExecuteScriptV2,
|
||||
FieldSelector,
|
||||
FileSelector,
|
||||
FilterSelector,
|
||||
PropField,
|
||||
QueryParamSelector,
|
||||
SchemaSetup,
|
||||
TableSelector,
|
||||
} from "."
|
||||
import { getFieldLabel, getInputValue } from "./layouts"
|
||||
import { automationStore, tables } from "@/stores/builder"
|
||||
import {
|
||||
type AutomationSchemaConfig,
|
||||
type FieldProps,
|
||||
type FormUpdate,
|
||||
type FileSelectorMeta,
|
||||
type StepInputs,
|
||||
SchemaFieldTypes,
|
||||
customTypeToSchema,
|
||||
typeToSchema,
|
||||
} from "@/types/automations"
|
||||
import { Select, Checkbox } from "@budibase/bbui"
|
||||
import {
|
||||
DrawerBindableInput,
|
||||
ServerBindingPanel as AutomationBindingPanel,
|
||||
} from "@/components/common/bindings"
|
||||
import Editor from "@/components/integration/QueryEditor.svelte"
|
||||
import WebhookDisplay from "@/components/automation/Shared/WebhookDisplay.svelte"
|
||||
|
||||
export let block: AutomationStep | AutomationTrigger | undefined = undefined
|
||||
export let context: {} | undefined
|
||||
export let bindings: any[] | undefined = undefined
|
||||
|
||||
const SchemaTypes: AutomationSchemaConfig = {
|
||||
[SchemaFieldTypes.ENUM]: {
|
||||
comp: Select,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const field = opts.field
|
||||
return {
|
||||
placeholder: false,
|
||||
options: field.enum,
|
||||
getOptionLabel: (x: string, idx: number) => {
|
||||
return field.pretty ? field.pretty[idx] : x
|
||||
},
|
||||
disabled: field.readonly,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.TABLE]: {
|
||||
comp: TableSelector,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const field = opts.field
|
||||
return {
|
||||
isTrigger,
|
||||
disabled: field.readonly,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.FILTER]: {
|
||||
comp: FilterSelector,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { key } = opts
|
||||
return {
|
||||
key,
|
||||
}
|
||||
},
|
||||
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||
const update = e.detail
|
||||
if (block) {
|
||||
automationStore.actions.requestUpdate(update, block)
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.COLUMN]: {
|
||||
comp: Select,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const field = opts.field
|
||||
return {
|
||||
options: Object.keys(table?.schema || {}),
|
||||
disabled: field.readonly,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.CODE_V2]: {
|
||||
comp: ExecuteScriptV2,
|
||||
fullWidth: true,
|
||||
},
|
||||
[SchemaFieldTypes.BOOL]: {
|
||||
comp: Checkbox,
|
||||
},
|
||||
[SchemaFieldTypes.DATE]: {
|
||||
comp: DateSelector,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { field, key } = opts
|
||||
return {
|
||||
title: field.title ?? getFieldLabel(key, field),
|
||||
disabled: field.readonly,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.TRIGGER_SCHEMA]: {
|
||||
comp: SchemaSetup,
|
||||
fullWidth: true,
|
||||
},
|
||||
[SchemaFieldTypes.JSON]: {
|
||||
comp: Editor,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { field, value: editorBody } = opts
|
||||
return {
|
||||
value: editorBody?.value,
|
||||
editorHeight: "250",
|
||||
editorWidth: "448",
|
||||
mode: "json",
|
||||
readOnly: field.readonly,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.FILE]: {
|
||||
comp: FileSelector,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { field } = opts
|
||||
const meta = getInputValue(inputData, "meta") as FileSelectorMeta
|
||||
return {
|
||||
buttonText:
|
||||
field.type === "attachment" ? "Add attachment" : "Add signature",
|
||||
useAttachmentBinding: meta?.useAttachmentBinding,
|
||||
}
|
||||
},
|
||||
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||
const update = e.detail
|
||||
if (block) {
|
||||
automationStore.actions.requestUpdate(update, block)
|
||||
}
|
||||
},
|
||||
fullWidth: true,
|
||||
},
|
||||
[SchemaFieldTypes.CRON]: {
|
||||
comp: CronBuilder,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { value } = opts
|
||||
return {
|
||||
cronExpression: value,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.LOOP_OPTION]: {
|
||||
comp: Select,
|
||||
props: () => {
|
||||
return {
|
||||
autoWidth: true,
|
||||
options: ["Array", "String"],
|
||||
defaultValue: "Array",
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.AUTOMATION_FIELDS]: {
|
||||
comp: AutomationSelector,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { field } = opts
|
||||
return {
|
||||
title: field.title,
|
||||
}
|
||||
},
|
||||
fullWidth: true,
|
||||
wrapped: false,
|
||||
},
|
||||
[SchemaFieldTypes.WEBHOOK_URL]: {
|
||||
comp: WebhookDisplay,
|
||||
},
|
||||
[SchemaFieldTypes.QUERY_PARAMS]: {
|
||||
comp: QueryParamSelector,
|
||||
},
|
||||
[SchemaFieldTypes.CODE]: {
|
||||
comp: ExecuteScript,
|
||||
fullWidth: true,
|
||||
},
|
||||
[SchemaFieldTypes.STRING]: {
|
||||
comp: DrawerBindableInput,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { key, field } = opts
|
||||
return {
|
||||
title: field.title ?? getFieldLabel(key, field),
|
||||
panel: AutomationBindingPanel,
|
||||
type: field.customType,
|
||||
updateOnChange: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
[SchemaFieldTypes.QUERY_LIMIT]: {
|
||||
comp: DrawerBindableInput,
|
||||
props: (opts: FieldProps = {} as FieldProps) => {
|
||||
const { key, field } = opts
|
||||
return {
|
||||
title: field.title ?? getFieldLabel(key, field),
|
||||
panel: AutomationBindingPanel,
|
||||
type: field.customType,
|
||||
updateOnChange: false,
|
||||
placeholder: queryLimit,
|
||||
}
|
||||
},
|
||||
},
|
||||
// Not a core schema type.
|
||||
[SchemaFieldTypes.FIELDS]: {
|
||||
comp: FieldSelector,
|
||||
props: () => {
|
||||
return {
|
||||
isTestModal: false,
|
||||
}
|
||||
},
|
||||
fullWidth: true,
|
||||
},
|
||||
}
|
||||
|
||||
$: isTrigger = block?.type === AutomationStepType.TRIGGER
|
||||
|
||||
// The step input properties
|
||||
$: inputData = automationStore.actions.getInputData(block)
|
||||
|
||||
// Automation definition properties
|
||||
$: schemaProperties = Object.entries(block?.schema?.inputs?.properties || {})
|
||||
|
||||
// Used for field labelling
|
||||
$: requiredProperties = block ? block.schema["inputs"].required : []
|
||||
|
||||
$: tableId = inputData && "tableId" in inputData ? inputData.tableId : null
|
||||
$: table = tableId
|
||||
? $tables.list.find(table => table._id === tableId)
|
||||
: { schema: {} }
|
||||
|
||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||
|
||||
const canShowField = (value: BaseIOStructure, inputData?: StepInputs) => {
|
||||
if (!inputData) {
|
||||
console.error("Cannot determine field visibility without field data")
|
||||
return false
|
||||
}
|
||||
const dependsOn = value?.dependsOn
|
||||
return !dependsOn || !!getInputValue(inputData, dependsOn)
|
||||
}
|
||||
|
||||
const defaultChange = (
|
||||
update: FormUpdate,
|
||||
block?: AutomationTrigger | AutomationStep
|
||||
) => {
|
||||
if (block) {
|
||||
automationStore.actions.requestUpdate(update, block)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Build the core props used to page the Automation Schema field.
|
||||
*
|
||||
* @param key
|
||||
* @param field
|
||||
* @param block
|
||||
*/
|
||||
const schemaToUI = (
|
||||
key: string,
|
||||
field: BaseIOStructure,
|
||||
block?: AutomationStep | AutomationTrigger
|
||||
) => {
|
||||
if (!block) {
|
||||
console.error("Could not process UI elements.")
|
||||
return {}
|
||||
}
|
||||
const type = getFieldType(field, block)
|
||||
const config = type ? SchemaTypes[type] : null
|
||||
const title = getFieldLabel(key, field, requiredProperties?.includes(key))
|
||||
const value = getInputValue(inputData, key)
|
||||
const meta = getInputValue(inputData, "meta")
|
||||
|
||||
return {
|
||||
meta,
|
||||
config,
|
||||
value,
|
||||
title,
|
||||
props: config?.props
|
||||
? config.props({ key, field, block, value, meta, title })
|
||||
: {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the correct UI element based on the automation
|
||||
* schema configuration.
|
||||
*
|
||||
* @param field
|
||||
* @param block
|
||||
*/
|
||||
const getFieldType = (
|
||||
field: BaseIOStructure,
|
||||
block?: AutomationStep | AutomationTrigger
|
||||
) => {
|
||||
// Direct customType map
|
||||
const customType = field.customType && customTypeToSchema[field.customType]
|
||||
if (customType) {
|
||||
return customType
|
||||
}
|
||||
|
||||
// Direct type map
|
||||
const fieldType = field.type && typeToSchema[field.type]
|
||||
if (fieldType) {
|
||||
return fieldType
|
||||
}
|
||||
|
||||
// Enum Field
|
||||
if (field.type === AutomationIOType.STRING && field.enum) {
|
||||
return SchemaFieldTypes.ENUM
|
||||
}
|
||||
|
||||
// JS V2
|
||||
if (
|
||||
field.customType === AutomationCustomIOType.CODE &&
|
||||
block?.stepId === AutomationActionStepId.EXECUTE_SCRIPT_V2
|
||||
) {
|
||||
return SchemaFieldTypes.CODE_V2
|
||||
}
|
||||
|
||||
// Filter step or trigger
|
||||
if (
|
||||
field.customType === AutomationCustomIOType.FILTERS ||
|
||||
field.customType === AutomationCustomIOType.TRIGGER_FILTER
|
||||
) {
|
||||
return SchemaFieldTypes.FILTER
|
||||
}
|
||||
|
||||
// Default string/number UX
|
||||
if (
|
||||
field.type === AutomationIOType.STRING ||
|
||||
field.type === AutomationIOType.NUMBER
|
||||
) {
|
||||
// "integer" removed as it isnt present in the type
|
||||
return SchemaFieldTypes.STRING
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each schemaProperties as [key, field]}
|
||||
{#if canShowField(field, inputData)}
|
||||
{@const { config, props, value, title } = schemaToUI(key, field, block)}
|
||||
{#if config}
|
||||
{#if config.wrapped === false}
|
||||
<svelte:component
|
||||
this={config.comp}
|
||||
{bindings}
|
||||
{block}
|
||||
{context}
|
||||
{key}
|
||||
{...{ value, ...props }}
|
||||
on:change={config.onChange ??
|
||||
(e => {
|
||||
defaultChange({ [key]: e.detail }, block)
|
||||
})}
|
||||
/>
|
||||
{:else}
|
||||
<PropField label={title} fullWidth labelTooltip={config.tooltip || ""}>
|
||||
<svelte:component
|
||||
this={config.comp}
|
||||
{bindings}
|
||||
{block}
|
||||
{context}
|
||||
{key}
|
||||
{...{ value, ...props }}
|
||||
on:change={config.onChange ??
|
||||
(e => {
|
||||
defaultChange({ [key]: e.detail }, block)
|
||||
})}
|
||||
/>
|
||||
</PropField>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
|
@ -1,15 +1,18 @@
|
|||
<script>
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||
import { TriggerStepID } from "@/constants/backend/automations"
|
||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
import PropField from "./PropField.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value
|
||||
export let bindings = []
|
||||
export let title
|
||||
|
||||
const onChangeAutomation = e => {
|
||||
value.automationId = e.detail
|
||||
dispatch("change", value)
|
||||
|
@ -33,9 +36,8 @@
|
|||
)
|
||||
</script>
|
||||
|
||||
<div class="schema-field">
|
||||
<Label>Automation</Label>
|
||||
<div class="field-width">
|
||||
<div class="selector">
|
||||
<PropField label={title} fullWidth>
|
||||
<Select
|
||||
on:change={onChangeAutomation}
|
||||
value={value.automationId}
|
||||
|
@ -43,13 +45,12 @@
|
|||
getOptionValue={automation => automation._id}
|
||||
getOptionLabel={automation => automation.name}
|
||||
/>
|
||||
</div>
|
||||
</PropField>
|
||||
</div>
|
||||
{#if Object.keys(automationFields)}
|
||||
{#each Object.keys(automationFields) as field}
|
||||
<div class="schema-field">
|
||||
<Label>{field}</Label>
|
||||
<div class="field-width">
|
||||
<PropField label={field} fullWidth>
|
||||
<DrawerBindableInput
|
||||
panel={AutomationBindingPanel}
|
||||
extraThin
|
||||
|
@ -59,27 +60,12 @@
|
|||
{bindings}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
</div>
|
||||
</PropField>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.field-width {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.schema-field {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.schema-field :global(label) {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,385 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
Divider,
|
||||
ActionButton,
|
||||
notifications,
|
||||
Helpers,
|
||||
Icon,
|
||||
Button,
|
||||
} from "@budibase/bbui"
|
||||
import JSONViewer, {
|
||||
type JSONViewerClickEvent,
|
||||
} from "@/components/common/JSONViewer.svelte"
|
||||
import {
|
||||
type AutomationStep,
|
||||
type AutomationTrigger,
|
||||
type Automation,
|
||||
type LoopStepInputs,
|
||||
type DidNotTriggerResponse,
|
||||
type TestAutomationResponse,
|
||||
type BlockRef,
|
||||
type AutomationIOProps,
|
||||
type AutomationStepResult,
|
||||
AutomationStepStatus,
|
||||
AutomationStatus,
|
||||
AutomationStepType,
|
||||
isDidNotTriggerResponse,
|
||||
isTrigger,
|
||||
isFilterStep,
|
||||
isLoopStep,
|
||||
BlockDefinitionTypes,
|
||||
AutomationActionStepId,
|
||||
} from "@budibase/types"
|
||||
import Count from "./Count.svelte"
|
||||
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||
import {
|
||||
type BlockStatus,
|
||||
BlockStatusSource,
|
||||
BlockStatusType,
|
||||
DataMode,
|
||||
RowSteps,
|
||||
} from "@/types/automations"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { cloneDeep } from "lodash"
|
||||
import { type AutomationContext } from "@/stores/builder/automations"
|
||||
|
||||
export let context: AutomationContext | undefined
|
||||
export let block: AutomationStep | AutomationTrigger | undefined
|
||||
export let automation: Automation | undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const DataModeTabs = {
|
||||
[DataMode.INPUT]: "Data In",
|
||||
[DataMode.OUTPUT]: "Data Out",
|
||||
[DataMode.ERRORS]: "Errors",
|
||||
}
|
||||
|
||||
// Add explicit weight to the issue types
|
||||
const issueOrder = {
|
||||
[BlockStatusType.ERROR]: 0,
|
||||
[BlockStatusType.WARN]: 1,
|
||||
[BlockStatusType.INFO]: 2,
|
||||
}
|
||||
|
||||
let dataMode: DataMode = DataMode.INPUT
|
||||
let issues: BlockStatus[] = []
|
||||
|
||||
$: blockRef = block?.id
|
||||
? $selectedAutomation?.blockRefs?.[block.id]
|
||||
: undefined
|
||||
|
||||
// The loop block associated with the selected block
|
||||
$: loopRef = blockRef?.looped
|
||||
? $selectedAutomation.blockRefs[blockRef.looped]
|
||||
: undefined
|
||||
$: loopBlock = automationStore.actions.getBlockByRef(automation, loopRef)
|
||||
|
||||
$: if ($automationStore.selectedNodeId) {
|
||||
dataMode = $automationStore.selectedNodeMode || DataMode.INPUT
|
||||
}
|
||||
|
||||
$: testResults = $automationStore.testResults as TestAutomationResponse
|
||||
$: blockResults = automationStore.actions.processBlockResults(
|
||||
testResults,
|
||||
block
|
||||
)
|
||||
$: processTestIssues(testResults, block)
|
||||
|
||||
/**
|
||||
* Take the results of an automation and generate a
|
||||
* list of graded issues. If the conditions get bigger they
|
||||
* could be broken out.
|
||||
*
|
||||
* @param testResults
|
||||
* @param block
|
||||
*/
|
||||
const processTestIssues = (
|
||||
testResults: TestAutomationResponse,
|
||||
block: AutomationStep | AutomationTrigger | undefined
|
||||
) => {
|
||||
// Reset issues
|
||||
issues = []
|
||||
|
||||
if (!testResults || !block || !blockResults) {
|
||||
return
|
||||
}
|
||||
|
||||
// Process loop issues
|
||||
if (blockRef?.looped && loopBlock && isLoopStep(loopBlock)) {
|
||||
const inputs = loopBlock.inputs as LoopStepInputs
|
||||
const loopMax = Number(inputs.iterations)
|
||||
const loopOutputs = blockResults?.outputs
|
||||
|
||||
let loopMessage = "There was an error"
|
||||
|
||||
// Not technically failed as it continues to run
|
||||
if (loopOutputs?.status === AutomationStepStatus.MAX_ITERATIONS) {
|
||||
loopMessage = `The maximum number of iterations (${loopMax}) has been reached.`
|
||||
} else if (
|
||||
loopOutputs?.status === AutomationStepStatus.FAILURE_CONDITION
|
||||
) {
|
||||
loopMessage = `The failure condition for the loop was hit: ${inputs.failure}.
|
||||
The loop was terminated`
|
||||
} else if (loopOutputs?.status === AutomationStepStatus.INCORRECT_TYPE) {
|
||||
loopMessage = `An 'Input Type' of '${inputs.option}' was configured which does
|
||||
not match the value supplied`
|
||||
}
|
||||
|
||||
issues.push({
|
||||
message: loopMessage,
|
||||
type: BlockStatusType.ERROR,
|
||||
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||
})
|
||||
}
|
||||
|
||||
// Process filtered row issues
|
||||
else if (isTrigger(block) && isDidNotTriggerResponse(testResults)) {
|
||||
issues.push({
|
||||
message: (blockResults as DidNotTriggerResponse).message,
|
||||
type: BlockStatusType.WARN,
|
||||
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||
})
|
||||
}
|
||||
|
||||
// Filter step
|
||||
else if (
|
||||
isFilterStep(block) &&
|
||||
blockResults?.outputs.status === AutomationStatus.STOPPED
|
||||
) {
|
||||
issues.push({
|
||||
message: `The conditions were not met and
|
||||
the automation flow was stopped.`,
|
||||
type: BlockStatusType.WARN,
|
||||
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||
})
|
||||
}
|
||||
|
||||
// Row step issues
|
||||
else if (!isTrigger(block) && RowSteps.includes(block.stepId)) {
|
||||
const outputs = (blockResults as AutomationStepResult)?.outputs
|
||||
if (outputs.success) return
|
||||
issues.push({
|
||||
message: `Could not complete the row request:
|
||||
${outputs.response?.message || JSON.stringify(outputs.response)}`,
|
||||
type: BlockStatusType.ERROR,
|
||||
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||
})
|
||||
}
|
||||
|
||||
// Default on error.
|
||||
else if (!isTrigger(block) && !blockResults.outputs.success) {
|
||||
issues.push({
|
||||
message: `There was an issue with the step. See 'Data out' for more information`,
|
||||
type: BlockStatusType.ERROR,
|
||||
source: BlockStatusSource.AUTOMATION_RESULTS,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort the issues
|
||||
issues.sort((a, b) => issueOrder[a.type] - issueOrder[b.type])
|
||||
}
|
||||
|
||||
const copyContext = (e: JSONViewerClickEvent) => {
|
||||
Helpers.copyToClipboard(JSON.stringify(e.detail?.value))
|
||||
notifications.success("Copied to clipboard")
|
||||
}
|
||||
|
||||
/**
|
||||
* Take the core AutomationContext and humanise it
|
||||
* Before the test is run, populate the "Data in" values
|
||||
* with the schema definition keyed by the readable step names.
|
||||
*/
|
||||
const parseContext = (context?: AutomationContext, blockRef?: BlockRef) => {
|
||||
if (!blockRef || !automation) {
|
||||
return
|
||||
}
|
||||
|
||||
let clonetext = cloneDeep(context)
|
||||
|
||||
const pathSteps = automationStore.actions.getPathSteps(
|
||||
blockRef.pathTo,
|
||||
automation
|
||||
)
|
||||
|
||||
const defs = $automationStore.blockDefinitions
|
||||
const stepNames = automation.definition.stepNames
|
||||
const loopDef =
|
||||
defs[BlockDefinitionTypes.ACTION]?.[AutomationActionStepId.LOOP]
|
||||
|
||||
// Exclude the trigger and loops from steps
|
||||
const filteredSteps: Record<string, AutomationStep | AutomationIOProps> =
|
||||
pathSteps
|
||||
.slice(0, -1)
|
||||
.filter(
|
||||
(step): step is AutomationStep =>
|
||||
step.type === AutomationStepType.ACTION &&
|
||||
step.stepId !== AutomationActionStepId.LOOP
|
||||
)
|
||||
.reduce(
|
||||
(
|
||||
acc: Record<string, AutomationStep | AutomationIOProps>,
|
||||
step: AutomationStep
|
||||
) => {
|
||||
// Process the block
|
||||
const blockRef = $selectedAutomation.blockRefs[step.id]
|
||||
const blockDefinition =
|
||||
defs[BlockDefinitionTypes.ACTION]?.[step.stepId]
|
||||
|
||||
// Check if the block has a loop
|
||||
const loopRef = blockRef.looped
|
||||
? $selectedAutomation.blockRefs[blockRef.looped]
|
||||
: undefined
|
||||
|
||||
const testData = context?.steps?.[step.id]
|
||||
const sampleDef = loopRef ? loopDef : blockDefinition
|
||||
|
||||
// Prioritise the display of testData and fallback to step defs
|
||||
// This will ensure users have at least an idea of what they are looking at
|
||||
acc[stepNames?.[step.id] || step.name] =
|
||||
testData || sampleDef?.schema.outputs.properties || {}
|
||||
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, AutomationStep | AutomationIOProps>
|
||||
)
|
||||
|
||||
return { ...clonetext, steps: filteredSteps }
|
||||
}
|
||||
|
||||
$: parsedContext = parseContext(context, blockRef)
|
||||
</script>
|
||||
|
||||
<div class="tabs">
|
||||
{#each Object.values(DataMode) as mode}
|
||||
<Count count={mode === DataMode.ERRORS ? issues.length : 0}>
|
||||
<ActionButton
|
||||
selected={mode === dataMode}
|
||||
quiet
|
||||
on:click={() => {
|
||||
dataMode = mode
|
||||
}}
|
||||
>
|
||||
{DataModeTabs[mode]}
|
||||
</ActionButton>
|
||||
</Count>
|
||||
{/each}
|
||||
</div>
|
||||
<Divider noMargin />
|
||||
<div class="viewer">
|
||||
{#if dataMode === DataMode.INPUT}
|
||||
<JSONViewer
|
||||
value={parsedContext}
|
||||
showCopyIcon
|
||||
on:click-copy={copyContext}
|
||||
/>
|
||||
{:else if dataMode === DataMode.OUTPUT}
|
||||
{#if blockResults}
|
||||
<JSONViewer
|
||||
value={blockResults.outputs}
|
||||
showCopyIcon
|
||||
on:click-copy={copyContext}
|
||||
/>
|
||||
{:else if testResults && !blockResults}
|
||||
<div class="content">
|
||||
<span class="info">
|
||||
This step was not executed as part of the test run
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="content">
|
||||
<span class="info">
|
||||
Run the automation to show the output of this step
|
||||
</span>
|
||||
<Button
|
||||
size={"S"}
|
||||
icon={"Play"}
|
||||
secondary
|
||||
on:click={() => {
|
||||
dispatch("run")
|
||||
}}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="issues" class:empty={!issues.length}>
|
||||
{#if issues.length === 0}
|
||||
<span>There are no current issues</span>
|
||||
{:else}
|
||||
{#each issues as issue}
|
||||
<div class={`issue ${issue.type}`}>
|
||||
<div class="icon"><Icon name="Alert" /></div>
|
||||
<!-- For custom automations, the error message needs a default -->
|
||||
<div class="message">
|
||||
{issue.message || "There was an error"}
|
||||
</div>
|
||||
</div>
|
||||
<Divider noMargin />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.content .info {
|
||||
text-align: center;
|
||||
max-width: 70%;
|
||||
}
|
||||
.tabs,
|
||||
.viewer {
|
||||
padding: var(--spacing-l);
|
||||
}
|
||||
.viewer {
|
||||
overflow-y: scroll;
|
||||
flex: 1;
|
||||
padding-right: 0px;
|
||||
}
|
||||
.viewer .content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
padding: var(--spacing-m) var(--spacing-l);
|
||||
}
|
||||
.issue {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: var(--spacing-l);
|
||||
}
|
||||
.issues {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.issues.empty {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.issue.error .icon {
|
||||
color: var(--spectrum-global-color-static-red-600);
|
||||
}
|
||||
.issue.warn .icon {
|
||||
color: var(--spectrum-global-color-static-yellow-600);
|
||||
}
|
||||
.issues :global(hr.spectrum-Divider:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.issues .issue:not(:first-child) {
|
||||
padding-top: var(--spacing-l);
|
||||
}
|
||||
.issues {
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,236 @@
|
|||
<script lang="ts">
|
||||
import { Body, Tags, Tag, Icon, TooltipPosition } from "@budibase/bbui"
|
||||
import {
|
||||
type AutomationStep,
|
||||
type AutomationTrigger,
|
||||
type Automation,
|
||||
AutomationStepType,
|
||||
isBranchStep,
|
||||
isActionStep,
|
||||
} from "@budibase/types"
|
||||
import {
|
||||
type ExternalAction,
|
||||
externalActions,
|
||||
} from "@/components/automation/AutomationBuilder/FlowChart/ExternalActions"
|
||||
import { automationStore } from "@/stores/builder"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let block: AutomationStep | AutomationTrigger | undefined = undefined
|
||||
export let automation: Automation | undefined = undefined
|
||||
export let itemName: string | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let externalAction: ExternalAction | undefined
|
||||
let editing: boolean
|
||||
let validRegex: RegExp = /^[A-Za-z0-9_\s]+$/
|
||||
|
||||
$: stepNames = automation?.definition.stepNames || {}
|
||||
$: blockHeading = getHeading(itemName, block) || ""
|
||||
$: blockNameError = getStepNameError(blockHeading)
|
||||
$: isBranch = block && isBranchStep(block)
|
||||
|
||||
const getHeading = (
|
||||
itemName?: string,
|
||||
block?: AutomationStep | AutomationTrigger
|
||||
) => {
|
||||
if (itemName) {
|
||||
return itemName
|
||||
} else if (block) {
|
||||
return stepNames?.[block?.id] || block?.name
|
||||
}
|
||||
}
|
||||
|
||||
$: isTrigger = block?.type === AutomationStepType.TRIGGER
|
||||
$: allSteps = automation?.definition.steps || []
|
||||
$: blockDefinition = automationStore.actions.getBlockDefinition(block)
|
||||
|
||||
// Put the type in the header if they change the name
|
||||
// Otherwise the info is obscured
|
||||
$: blockTitle = isBranch
|
||||
? "Branch"
|
||||
: `${
|
||||
block &&
|
||||
block?.id in stepNames &&
|
||||
stepNames[block?.id] !== blockDefinition?.name
|
||||
? blockDefinition?.name
|
||||
: "Step"
|
||||
}`
|
||||
|
||||
// Parse external actions
|
||||
$: if (block && isActionStep(block) && block?.stepId in externalActions) {
|
||||
externalAction = externalActions[block?.stepId]
|
||||
} else {
|
||||
externalAction = undefined
|
||||
}
|
||||
|
||||
const getStepNameError = (name: string) => {
|
||||
if (!block) {
|
||||
return
|
||||
}
|
||||
const duplicateError =
|
||||
"This name already exists, please enter a unique name"
|
||||
if (editing) {
|
||||
for (const [key, value] of Object.entries(stepNames)) {
|
||||
if (name !== block.name && name === value && key !== block.id) {
|
||||
return duplicateError
|
||||
}
|
||||
}
|
||||
|
||||
for (const step of allSteps) {
|
||||
if (step.id !== block.id && name === step.name) {
|
||||
return duplicateError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (name !== block.name && name?.length > 0) {
|
||||
let invalidRoleName = !validRegex.test(name)
|
||||
if (invalidRoleName) {
|
||||
return "Please enter a name consisting of only alphanumeric symbols and underscores"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const startEditing = () => {
|
||||
editing = true
|
||||
}
|
||||
|
||||
const stopEditing = () => {
|
||||
editing = false
|
||||
if (blockNameError) {
|
||||
blockHeading =
|
||||
(block ? stepNames[block?.id] : undefined) || block?.name || ""
|
||||
} else {
|
||||
dispatch("update", blockHeading)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
blockHeading = target.value.trim()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="block-details">
|
||||
{#if block}
|
||||
{#if externalAction}
|
||||
<img
|
||||
alt={externalAction.name}
|
||||
src={externalAction.icon}
|
||||
width="28px"
|
||||
height="28px"
|
||||
/>
|
||||
{:else}
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
class="spectrum-Icon"
|
||||
style="color:var(--spectrum-global-color-gray-700);"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{block.icon}" />
|
||||
</svg>
|
||||
{/if}
|
||||
<div class="heading">
|
||||
{#if isTrigger}
|
||||
<Body size="XS"><b>Trigger</b></Body>
|
||||
{:else}
|
||||
<Body size="XS">
|
||||
<div class="step">
|
||||
<b>{blockTitle}</b>
|
||||
{#if blockDefinition?.deprecated}
|
||||
<Tags>
|
||||
<Tag invalid>Deprecated</Tag>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
</Body>
|
||||
{/if}
|
||||
<input
|
||||
class="input-text"
|
||||
placeholder={`Enter step name`}
|
||||
name="name"
|
||||
autocomplete="off"
|
||||
disabled={isTrigger || disabled}
|
||||
value={blockHeading}
|
||||
on:input={handleInput}
|
||||
on:click={e => {
|
||||
e.stopPropagation()
|
||||
startEditing()
|
||||
}}
|
||||
on:keydown={async e => {
|
||||
if (e.key === "Enter") {
|
||||
stopEditing()
|
||||
}
|
||||
}}
|
||||
on:blur={stopEditing}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if blockNameError && editing}
|
||||
<div class="error-container">
|
||||
<div class="error-icon">
|
||||
<Icon
|
||||
size="S"
|
||||
name="Alert"
|
||||
tooltip={blockNameError}
|
||||
tooltipPosition={TooltipPosition.Left}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
input {
|
||||
color: var(--ink);
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-text {
|
||||
font-size: var(--spectrum-alias-font-size-default);
|
||||
font-family: var(--font-sans);
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 0px;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Hide arrows for number fields */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.block-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-l);
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.error-icon :global(.spectrum-Icon) {
|
||||
fill: var(--spectrum-global-color-red-600);
|
||||
}
|
||||
|
||||
.heading {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,65 @@
|
|||
<script lang="ts">
|
||||
import { automationStore } from "@/stores/builder"
|
||||
import { memo } from "@budibase/frontend-core"
|
||||
import { environment } from "@/stores/portal"
|
||||
import {
|
||||
type AutomationStep,
|
||||
type AutomationTrigger,
|
||||
type Automation,
|
||||
} from "@budibase/types"
|
||||
import { type SchemaConfigProps } from "@/types/automations"
|
||||
import { writable } from "svelte/store"
|
||||
import { getCustomStepLayout } from "./layouts"
|
||||
import AutomationSchemaLayout from "./AutomationSchemaLayout.svelte"
|
||||
import AutomationCustomLayout from "./AutomationCustomLayout.svelte"
|
||||
|
||||
export let block: AutomationStep | AutomationTrigger | undefined = undefined
|
||||
export let automation: Automation | undefined = undefined
|
||||
export let context: {} | undefined
|
||||
|
||||
const memoEnvVariables = memo($environment.variables)
|
||||
|
||||
// Databindings requires TypeScript conversion
|
||||
let environmentBindings: any[]
|
||||
|
||||
// All bindings available to this point
|
||||
$: availableBindings = automationStore.actions.getPathBindings(
|
||||
block?.id,
|
||||
automation
|
||||
)
|
||||
|
||||
// Fetch the env bindings
|
||||
$: if ($memoEnvVariables) {
|
||||
environmentBindings = automationStore.actions.buildEnvironmentBindings()
|
||||
}
|
||||
$: userBindings = automationStore.actions.buildUserBindings()
|
||||
$: settingBindings = automationStore.actions.buildSettingBindings()
|
||||
|
||||
// Combine all bindings for the step
|
||||
$: bindings = [
|
||||
...availableBindings,
|
||||
...environmentBindings,
|
||||
...userBindings,
|
||||
...settingBindings,
|
||||
]
|
||||
|
||||
// Store for any UX related data
|
||||
const stepStore = writable<Record<string, any>>({})
|
||||
$: if (block?.id) {
|
||||
stepStore.update(state => ({ ...state, [block.id]: {} }))
|
||||
}
|
||||
|
||||
// Determine if any custom step layouts have been created
|
||||
let customLayout: SchemaConfigProps[] | undefined
|
||||
$: if ($stepStore) {
|
||||
customLayout = getCustomStepLayout(block, stepStore)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if customLayout}
|
||||
<!-- Render custom layout 1 or more components in a custom layout -->
|
||||
<AutomationCustomLayout {context} {bindings} {block} layout={customLayout} />
|
||||
{:else}
|
||||
<!-- Render Automation Step Schema > [string, BaseIOStructure][] -->
|
||||
<AutomationSchemaLayout {context} {bindings} {block} />
|
||||
{/if}
|
|
@ -0,0 +1,51 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { AbsTooltip } from "@budibase/bbui"
|
||||
|
||||
export let count: number = 0
|
||||
export let tooltip: string | undefined = undefined
|
||||
export let hoverable: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<AbsTooltip text={tooltip}>
|
||||
{#if count}
|
||||
<span
|
||||
class="count"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
aria-label={`Notifications ${count}`}
|
||||
class:hoverable
|
||||
on:mouseenter={() => {
|
||||
dispatch("hover")
|
||||
}}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
{/if}
|
||||
</AbsTooltip>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.count {
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
top: -6px;
|
||||
background: var(--spectrum-global-color-static-red-600);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
padding: 0 4px;
|
||||
z-index: 2;
|
||||
font-size: 0.8em;
|
||||
cursor: default;
|
||||
}
|
||||
.count.hoverable {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,37 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
DrawerBindableSlot,
|
||||
ServerBindingPanel as AutomationBindingPanel,
|
||||
} from "@/components/common/bindings"
|
||||
import { DatePicker } from "@budibase/bbui"
|
||||
import { type EnrichedBinding } from "@budibase/types"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let context: Record<any, any> | undefined = undefined
|
||||
export let bindings: EnrichedBinding[] | undefined = undefined
|
||||
export let value: string | undefined = undefined
|
||||
export let title: string | undefined = undefined
|
||||
export let disabled: boolean | undefined = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<DrawerBindableSlot
|
||||
{title}
|
||||
panel={AutomationBindingPanel}
|
||||
type={"date"}
|
||||
{value}
|
||||
on:change={e => dispatch("change", e.detail)}
|
||||
{bindings}
|
||||
allowJS={true}
|
||||
updateOnChange={false}
|
||||
{disabled}
|
||||
{context}
|
||||
>
|
||||
<DatePicker
|
||||
{value}
|
||||
on:change={e => {
|
||||
dispatch("change", e.detail)
|
||||
}}
|
||||
/>
|
||||
</DrawerBindableSlot>
|
|
@ -0,0 +1,111 @@
|
|||
<script lang="ts">
|
||||
import { BindingSidePanel } from "@/components/common/bindings"
|
||||
import {
|
||||
BindingType,
|
||||
BindingHelpers,
|
||||
} from "@/components/common/bindings/utils"
|
||||
import CodeEditor from "@/components/common/CodeEditor/CodeEditor.svelte"
|
||||
import {
|
||||
bindingsToCompletions,
|
||||
hbAutocomplete,
|
||||
EditorModes,
|
||||
} from "@/components/common/CodeEditor"
|
||||
import { CodeEditorModal } from "@/components/automation/SetupPanel"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import {
|
||||
AutomationActionStepId,
|
||||
type AutomationStep,
|
||||
type CaretPositionFn,
|
||||
type InsertAtPositionFn,
|
||||
type EnrichedBinding,
|
||||
BindingMode,
|
||||
} from "@budibase/types"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value: string | undefined
|
||||
export let isJS: boolean = true
|
||||
export let block: AutomationStep
|
||||
export let context
|
||||
export let bindings
|
||||
|
||||
let getCaretPosition: CaretPositionFn | undefined
|
||||
let insertAtPos: InsertAtPositionFn | undefined
|
||||
|
||||
$: codeMode =
|
||||
block.stepId === AutomationActionStepId.EXECUTE_BASH
|
||||
? EditorModes.Handlebars
|
||||
: EditorModes.JS
|
||||
$: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, {
|
||||
disableWrapping: true,
|
||||
})
|
||||
$: editingJs = codeMode === EditorModes.JS
|
||||
$: stepCompletions =
|
||||
codeMode === EditorModes.Handlebars
|
||||
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||
: []
|
||||
|
||||
const addBinding = (binding: EnrichedBinding) =>
|
||||
bindingsHelpers.onSelectBinding(value, binding, {
|
||||
js: true,
|
||||
dontDecode: true,
|
||||
type: BindingType.RUNTIME,
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- DEPRECATED -->
|
||||
<CodeEditorModal
|
||||
on:hide={() => {
|
||||
// Push any pending changes when the window closes
|
||||
dispatch("change", value)
|
||||
}}
|
||||
>
|
||||
<div class:js-editor={isJS}>
|
||||
<div class:js-code={isJS} style="width:100%;height:500px;">
|
||||
<CodeEditor
|
||||
{value}
|
||||
on:change={e => {
|
||||
// need to pass without the value inside
|
||||
value = e.detail
|
||||
}}
|
||||
completions={stepCompletions}
|
||||
mode={codeMode}
|
||||
autocompleteEnabled={codeMode !== EditorModes.JS}
|
||||
bind:getCaretPosition
|
||||
bind:insertAtPos
|
||||
placeholder={codeMode === EditorModes.Handlebars
|
||||
? "Add bindings by typing {{"
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
{#if editingJs}
|
||||
<div class="js-binding-picker">
|
||||
<BindingSidePanel
|
||||
{bindings}
|
||||
allowHelpers={false}
|
||||
{addBinding}
|
||||
mode={BindingMode.JavaScript}
|
||||
{context}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</CodeEditorModal>
|
||||
|
||||
<style>
|
||||
.js-editor {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.js-code {
|
||||
flex: 7;
|
||||
}
|
||||
|
||||
.js-binding-picker {
|
||||
flex: 3;
|
||||
margin-top: calc((var(--spacing-xl) * -1) + 1px);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,67 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
DrawerBindableSlot,
|
||||
ServerBindingPanel as AutomationBindingPanel,
|
||||
} from "@/components/common/bindings"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { type EnrichedBinding, FieldType } from "@budibase/types"
|
||||
import CodeEditorField from "@/components/common/bindings/CodeEditorField.svelte"
|
||||
import { DropdownPosition } from "@/components/common/CodeEditor/CodeEditor.svelte"
|
||||
|
||||
export let value: string
|
||||
export let context: Record<any, any> | undefined = undefined
|
||||
export let bindings: EnrichedBinding[] | undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="scriptv2-wrapper">
|
||||
<DrawerBindableSlot
|
||||
title={"Edit Code"}
|
||||
panel={AutomationBindingPanel}
|
||||
type={FieldType.LONGFORM}
|
||||
on:change={e => dispatch("change", e.detail)}
|
||||
{value}
|
||||
{bindings}
|
||||
allowJS={true}
|
||||
allowHBS={false}
|
||||
updateOnChange={false}
|
||||
{context}
|
||||
>
|
||||
<div class="field-wrap code-editor">
|
||||
<CodeEditorField
|
||||
{value}
|
||||
{bindings}
|
||||
{context}
|
||||
placeholder={"Add bindings by typing $"}
|
||||
on:change={e => dispatch("change", e.detail)}
|
||||
dropdown={DropdownPosition.Relative}
|
||||
/>
|
||||
</div>
|
||||
</DrawerBindableSlot>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.field-wrap :global(.cm-editor),
|
||||
.field-wrap :global(.cm-scroller) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.field-wrap {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--spectrum-global-color-gray-400);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.field-wrap.code-editor {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.scriptv2-wrapper :global(.icon.slot-icon),
|
||||
.scriptv2-wrapper :global(.text-area-slot-icon) {
|
||||
right: 1px;
|
||||
top: 1px;
|
||||
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||
border-bottom-left-radius: var(--spectrum-alias-border-radius-regular);
|
||||
border-right: 0px;
|
||||
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,89 @@
|
|||
<script lang="ts">
|
||||
import KeyValueBuilder from "@/components/integration/KeyValueBuilder.svelte"
|
||||
import { Toggle } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import {
|
||||
DrawerBindableInput,
|
||||
ServerBindingPanel as AutomationBindingPanel,
|
||||
} from "@/components/common/bindings"
|
||||
import { type KeyValuePair } from "@/types/automations"
|
||||
|
||||
export let useAttachmentBinding
|
||||
export let buttonText
|
||||
export let key
|
||||
export let context
|
||||
export let value
|
||||
export let bindings
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
interface Attachment {
|
||||
filename: string
|
||||
url: string
|
||||
}
|
||||
|
||||
const handleAttachmentParams = (keyValueObj: Attachment[]) => {
|
||||
let params: Record<string, string> = {}
|
||||
if (keyValueObj?.length) {
|
||||
for (let param of keyValueObj) {
|
||||
params[param.url] = param.filename
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const handleKeyValueChange = (e: CustomEvent<KeyValuePair[]>) => {
|
||||
const update = {
|
||||
[key]: e.detail.map(({ name, value }) => ({
|
||||
url: name,
|
||||
filename: value,
|
||||
})),
|
||||
}
|
||||
|
||||
dispatch("change", update)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="attachment-field-wrapper">
|
||||
<div class="toggle-container">
|
||||
<Toggle
|
||||
value={useAttachmentBinding}
|
||||
text={"Use bindings"}
|
||||
on:change={e => {
|
||||
dispatch("change", {
|
||||
[key]: null,
|
||||
meta: {
|
||||
useAttachmentBinding: e.detail,
|
||||
},
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="attachment-field-width">
|
||||
{#if !useAttachmentBinding}
|
||||
{#key value}
|
||||
<KeyValueBuilder
|
||||
on:change={handleKeyValueChange}
|
||||
object={handleAttachmentParams(value)}
|
||||
allowJS
|
||||
{bindings}
|
||||
keyBindings
|
||||
customButtonText={buttonText}
|
||||
keyPlaceholder={"URL"}
|
||||
valuePlaceholder={"Filename"}
|
||||
{context}
|
||||
/>
|
||||
{/key}
|
||||
{:else}
|
||||
<DrawerBindableInput
|
||||
panel={AutomationBindingPanel}
|
||||
{value}
|
||||
on:change={e => dispatch("change", { [key]: e.detail })}
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
{context}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,122 @@
|
|||
<script lang="ts">
|
||||
import { ActionButton, Drawer, DrawerContent, Button } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { getSchemaForDatasourcePlus } from "@/dataBinding"
|
||||
import { ServerBindingPanel as AutomationBindingPanel } from "@/components/common/bindings"
|
||||
import FilterBuilder from "@/components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||
import { tables, automationStore } from "@/stores/builder"
|
||||
import type {
|
||||
AutomationStep,
|
||||
AutomationTrigger,
|
||||
BaseIOStructure,
|
||||
UISearchFilter,
|
||||
} from "@budibase/types"
|
||||
import { AutomationCustomIOType } from "@budibase/types"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import { QueryUtils, Utils, search } from "@budibase/frontend-core"
|
||||
import { cloneDeep } from "lodash"
|
||||
import { type DynamicProperties, type StepInputs } from "@/types/automations"
|
||||
|
||||
export let block: AutomationStep | AutomationTrigger | undefined
|
||||
export let context
|
||||
export let bindings
|
||||
export let key
|
||||
|
||||
// Mix in dynamic filter props
|
||||
type DynamicInputs = StepInputs & DynamicProperties
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let drawer: Drawer
|
||||
|
||||
$: inputData = automationStore.actions.getInputData(block)
|
||||
|
||||
$: schemaProperties = Object.entries(block?.schema?.inputs?.properties || {})
|
||||
$: filters = lookForFilters(schemaProperties)
|
||||
$: filterCount =
|
||||
filters?.groups?.reduce((acc: number, group) => {
|
||||
acc = acc += group?.filters?.length || 0
|
||||
return acc
|
||||
}, 0) || 0
|
||||
|
||||
$: tempFilters = cloneDeep(filters)
|
||||
|
||||
$: tableId = inputData && "tableId" in inputData ? inputData.tableId : null
|
||||
|
||||
$: schema = getSchemaForDatasourcePlus(tableId, {
|
||||
searchableSchema: true,
|
||||
}).schema
|
||||
|
||||
$: schemaFields = search.getFields(
|
||||
$tables.list,
|
||||
Object.values(schema || {}),
|
||||
{ allowLinks: false }
|
||||
)
|
||||
|
||||
const lookForFilters = (
|
||||
properties: [string, BaseIOStructure][]
|
||||
): UISearchFilter | undefined => {
|
||||
if (!properties || !inputData) {
|
||||
return
|
||||
}
|
||||
|
||||
let filter: UISearchFilter | undefined
|
||||
|
||||
// Does not appear in the test modal. Check testData
|
||||
const inputs = inputData as DynamicInputs
|
||||
for (let [key, field] of properties) {
|
||||
// need to look for the builder definition (keyed separately, see saveFilters)
|
||||
if (
|
||||
(field.customType === AutomationCustomIOType.FILTERS ||
|
||||
field.customType === AutomationCustomIOType.TRIGGER_FILTER) &&
|
||||
`${key}-def` in inputs
|
||||
) {
|
||||
filter = inputs[`${key}-def`]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return Array.isArray(filter) ? utils.processSearchFilters(filter) : filter
|
||||
}
|
||||
|
||||
function saveFilters(key: string, filters?: UISearchFilter) {
|
||||
const update = filters ? Utils.parseFilter(filters) : filters
|
||||
const query = QueryUtils.buildQuery(update)
|
||||
|
||||
dispatch("change", {
|
||||
[key]: query,
|
||||
[`${key}-def`]: update, // need to store the builder definition in the automation
|
||||
})
|
||||
|
||||
drawer.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton fullWidth on:click={drawer.show}>
|
||||
{filterCount > 0 ? "Update Filter" : "No Filter set"}
|
||||
</ActionButton>
|
||||
|
||||
<Drawer
|
||||
bind:this={drawer}
|
||||
title="Filtering"
|
||||
forceModal
|
||||
on:drawerShow={() => {
|
||||
tempFilters = filters
|
||||
}}
|
||||
>
|
||||
<Button cta slot="buttons" on:click={() => saveFilters(key, tempFilters)}>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
<DrawerContent slot="body">
|
||||
<FilterBuilder
|
||||
filters={tempFilters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
panel={AutomationBindingPanel}
|
||||
on:change={e => (tempFilters = e.detail)}
|
||||
evaluationContext={context}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { Label } from "@budibase/bbui"
|
||||
|
||||
export let label
|
||||
export let labelTooltip
|
||||
export let fullWidth = false
|
||||
export let componentWidth = 320
|
||||
export let label: string | undefined = ""
|
||||
export let labelTooltip: string | undefined = ""
|
||||
export let fullWidth: boolean | undefined = false
|
||||
export let componentWidth: number | undefined = 320
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
<script lang="ts">
|
||||
import { API } from "@/api"
|
||||
import { tables } from "@/stores/builder"
|
||||
import { type TableDatasource, SortOrder } from "@budibase/types"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
import type TableFetch from "@budibase/frontend-core/src/fetch/TableFetch"
|
||||
import { Select } from "@budibase/bbui"
|
||||
|
||||
export let tableId: string | undefined
|
||||
|
||||
let datasource: TableDatasource
|
||||
let fetch: TableFetch | undefined
|
||||
let rowSearchTerm = ""
|
||||
let selectedRow
|
||||
|
||||
$: primaryDisplay = table?.primaryDisplay
|
||||
|
||||
$: table = tableId
|
||||
? $tables.list.find(table => table._id === tableId)
|
||||
: undefined
|
||||
|
||||
$: if (table && tableId) {
|
||||
datasource = { type: "table", tableId }
|
||||
fetch = createFetch(datasource)
|
||||
}
|
||||
|
||||
$: if (rowSearchTerm && primaryDisplay) {
|
||||
fetch?.update({
|
||||
query: {
|
||||
fuzzy: {
|
||||
[primaryDisplay]: rowSearchTerm || "",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const createFetch = (datasource: TableDatasource) => {
|
||||
if (!datasource || !primaryDisplay) {
|
||||
return
|
||||
}
|
||||
|
||||
return fetchData({
|
||||
API,
|
||||
datasource,
|
||||
options: {
|
||||
sortColumn: primaryDisplay,
|
||||
sortOrder: SortOrder.ASCENDING,
|
||||
query: {
|
||||
fuzzy: {
|
||||
[primaryDisplay]: rowSearchTerm || "",
|
||||
},
|
||||
},
|
||||
limit: 20,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
$: fetchedRows = fetch ? $fetch?.rows : []
|
||||
$: fetchLoading = fetch ? $fetch?.loading : false
|
||||
|
||||
const compare = (a: any, b: any) => {
|
||||
primaryDisplay && a?.[primaryDisplay] === b?.[primaryDisplay]
|
||||
}
|
||||
</script>
|
||||
|
||||
<Select
|
||||
placeholder={"Select a row"}
|
||||
options={fetchedRows}
|
||||
loading={fetchLoading}
|
||||
value={selectedRow}
|
||||
autocomplete={true}
|
||||
getOptionLabel={row => (primaryDisplay ? row?.[primaryDisplay] : "")}
|
||||
{compare}
|
||||
/>
|
|
@ -11,8 +11,6 @@
|
|||
import { FieldType } from "@budibase/types"
|
||||
|
||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
import { FIELDS } from "@/constants/backend"
|
||||
import { capitalise } from "@/helpers"
|
||||
import { memo } from "@budibase/frontend-core"
|
||||
|
@ -26,6 +24,8 @@
|
|||
export let bindings
|
||||
export let isTestModal
|
||||
export let context = {}
|
||||
export let componentWidth
|
||||
export let fullWidth = false
|
||||
|
||||
const typeToField = Object.values(FIELDS).reduce((acc, field) => {
|
||||
acc[field.type] = field
|
||||
|
@ -234,21 +234,16 @@
|
|||
)
|
||||
dispatch("change", result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts arrays into strings. The CodeEditor expects a string or encoded JS
|
||||
* @param{object} fieldValue
|
||||
*/
|
||||
const drawerValue = fieldValue => {
|
||||
return Array.isArray(fieldValue) ? fieldValue.join(",") : fieldValue
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each schemaFields || [] as [field, schema]}
|
||||
{#if !schema.autocolumn && Object.hasOwn(editableFields, field)}
|
||||
<PropField label={field} fullWidth={isFullWidth(schema.type)}>
|
||||
<PropField
|
||||
label={field}
|
||||
fullWidth={fullWidth || isFullWidth(schema.type)}
|
||||
{componentWidth}
|
||||
>
|
||||
<div class="prop-control-wrap">
|
||||
{#if isTestModal}
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
{field}
|
||||
|
@ -261,39 +256,6 @@
|
|||
{onChange}
|
||||
{context}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableSlot
|
||||
title={$memoStore?.row?.title || field}
|
||||
panel={AutomationBindingPanel}
|
||||
type={schema.type}
|
||||
{schema}
|
||||
value={drawerValue(editableRow[field])}
|
||||
on:change={e =>
|
||||
onChange({
|
||||
row: {
|
||||
[field]: e.detail,
|
||||
},
|
||||
})}
|
||||
{bindings}
|
||||
allowJS={true}
|
||||
updateOnChange={false}
|
||||
drawerLeft="260px"
|
||||
{context}
|
||||
>
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
{field}
|
||||
{schema}
|
||||
bindings={parsedBindings}
|
||||
value={editableRow}
|
||||
meta={{
|
||||
fields: editableFields,
|
||||
}}
|
||||
{context}
|
||||
onChange={change => onChange(change)}
|
||||
/>
|
||||
</DrawerBindableSlot>
|
||||
{/if}
|
||||
</div>
|
||||
</PropField>
|
||||
{/if}
|
||||
|
@ -305,24 +267,30 @@
|
|||
class:empty={Object.is(editableFields, {})}
|
||||
bind:this={popoverAnchor}
|
||||
>
|
||||
<PropField {componentWidth} {fullWidth}>
|
||||
<div class="prop-control-wrap">
|
||||
<ActionButton
|
||||
icon="Add"
|
||||
on:click={() => {
|
||||
customPopover.show()
|
||||
}}
|
||||
disabled={!schemaFields}
|
||||
>Add fields
|
||||
>
|
||||
Edit fields
|
||||
</ActionButton>
|
||||
{#if schemaFields.length}
|
||||
<ActionButton
|
||||
icon="Remove"
|
||||
on:click={() => {
|
||||
dispatch("change", {
|
||||
meta: { fields: {} },
|
||||
row: {},
|
||||
})
|
||||
}}
|
||||
>Clear
|
||||
>
|
||||
Clear
|
||||
</ActionButton>
|
||||
{/if}
|
||||
</div>
|
||||
</PropField>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -388,11 +356,4 @@
|
|||
.prop-control-wrap :global(.icon.json-slot-icon) {
|
||||
right: 1px !important;
|
||||
}
|
||||
|
||||
.add-fields-btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value
|
||||
export let isTrigger
|
||||
export let isTrigger = false
|
||||
export let disabled = false
|
||||
|
||||
$: filteredTables = $tables.list.filter(table => {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
// Schema UX Types
|
||||
import FilterSelector from "./FilterSelector.svelte"
|
||||
import ExecuteScript from "./ExecuteScript.svelte"
|
||||
import ExecuteScriptV2 from "./ExecuteScriptV2.svelte"
|
||||
import DateSelector from "./DateSelector.svelte"
|
||||
import SchemaSetup from "./SchemaSetup.svelte"
|
||||
import TableSelector from "./TableSelector.svelte"
|
||||
import CronBuilder from "./CronBuilder.svelte"
|
||||
import FieldSelector from "./FieldSelector.svelte"
|
||||
import FileSelector from "./FileSelector.svelte"
|
||||
import RowSelector from "./RowSelector.svelte"
|
||||
import AutomationSelector from "./AutomationSelector.svelte"
|
||||
import PropField from "./PropField.svelte"
|
||||
import QueryParamSelector from "./QueryParamSelector.svelte"
|
||||
import CodeEditorModal from "./CodeEditorModal.svelte"
|
||||
|
||||
export {
|
||||
FilterSelector,
|
||||
ExecuteScriptV2,
|
||||
DateSelector,
|
||||
SchemaSetup,
|
||||
TableSelector,
|
||||
CronBuilder,
|
||||
FieldSelector,
|
||||
FileSelector,
|
||||
RowSelector,
|
||||
AutomationSelector,
|
||||
PropField,
|
||||
QueryParamSelector,
|
||||
CodeEditorModal,
|
||||
// DEPRECATED
|
||||
ExecuteScript,
|
||||
}
|
|
@ -0,0 +1,330 @@
|
|||
import { DrawerBindableInput } from "@/components/common/bindings"
|
||||
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||
import { Divider, Helpers, Select } from "@budibase/bbui"
|
||||
import {
|
||||
AutomationEventType,
|
||||
AutomationStep,
|
||||
AutomationStepType,
|
||||
AutomationTrigger,
|
||||
BaseIOStructure,
|
||||
isTrigger,
|
||||
Row,
|
||||
} from "@budibase/types"
|
||||
import AutomationBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
|
||||
import { RowSelector, TableSelector } from "."
|
||||
import { get, Writable } from "svelte/store"
|
||||
import {
|
||||
type StepInputs,
|
||||
type SchemaConfigProps,
|
||||
type FormUpdate,
|
||||
} from "@/types/automations"
|
||||
import RowFetch from "./RowFetch.svelte"
|
||||
|
||||
export const getInputValue = (inputData: StepInputs, key: string) => {
|
||||
const idxInput = inputData as Record<string, unknown>
|
||||
return idxInput?.[key]
|
||||
}
|
||||
|
||||
// We do not enforce required fields.
|
||||
export const getFieldLabel = (
|
||||
key: string,
|
||||
value: BaseIOStructure,
|
||||
isRequired = false
|
||||
) => {
|
||||
const requiredSuffix = isRequired ? "*" : ""
|
||||
const label = `${
|
||||
value.title || (key === "row" ? "Row" : key)
|
||||
} ${requiredSuffix}`
|
||||
return Helpers.capitalise(label)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a custom step layout. This generates an array of ui elements
|
||||
* intended to be rendered as a uniform column
|
||||
* If you intend to have a completely unique UX,
|
||||
*
|
||||
* @param block
|
||||
* @param stepState
|
||||
* @param isTest
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export const getCustomStepLayout = (
|
||||
block: AutomationStep | AutomationTrigger | undefined,
|
||||
stepState: Writable<Record<string, any>>,
|
||||
isTest = false
|
||||
): SchemaConfigProps[] | undefined => {
|
||||
if (!block) {
|
||||
return
|
||||
}
|
||||
|
||||
// Use the test data for test mode otherwise resolve trigger/step inputs by block
|
||||
const fieldData = isTest
|
||||
? get(selectedAutomation)?.data?.testData
|
||||
: automationStore.actions.getInputData(block)
|
||||
|
||||
const coreDivider = {
|
||||
comp: Divider,
|
||||
props: () => ({
|
||||
noMargin: true,
|
||||
}),
|
||||
wrapped: false,
|
||||
}
|
||||
|
||||
const getIdConfig = (rowIdentifier: string): SchemaConfigProps[] => {
|
||||
const schema =
|
||||
block.type === AutomationStepType.TRIGGER
|
||||
? block.schema.outputs.properties
|
||||
: block.schema.inputs.properties
|
||||
|
||||
const rowIdEntry = schema[rowIdentifier]
|
||||
if (!rowIdEntry) {
|
||||
return []
|
||||
}
|
||||
|
||||
const rowIdlabel = getFieldLabel(rowIdentifier, rowIdEntry)
|
||||
|
||||
const layout: SchemaConfigProps[] = [
|
||||
{
|
||||
comp: DrawerBindableInput,
|
||||
title: rowIdlabel,
|
||||
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||
const update = { [rowIdentifier]: e.detail }
|
||||
if (block) {
|
||||
automationStore.actions.requestUpdate(update, block)
|
||||
}
|
||||
},
|
||||
props: () => {
|
||||
return {
|
||||
panel: AutomationBindingPanel,
|
||||
value: getInputValue(fieldData, rowIdentifier),
|
||||
updateOnChange: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return layout
|
||||
}
|
||||
|
||||
const getRowSelector = (): SchemaConfigProps[] => {
|
||||
const row: Row = getInputValue(fieldData, "row") as Row
|
||||
const meta: Record<string, unknown> = getInputValue(
|
||||
fieldData,
|
||||
"meta"
|
||||
) as Record<string, unknown>
|
||||
|
||||
return [
|
||||
{
|
||||
comp: RowSelector,
|
||||
wrapped: false,
|
||||
props: () => ({
|
||||
row,
|
||||
meta: meta || {},
|
||||
componentWidth: 280,
|
||||
fullWidth: true,
|
||||
}),
|
||||
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||
if (block) {
|
||||
const metaUpdate = e.detail?.meta as Record<string, unknown>
|
||||
|
||||
automationStore.actions.requestUpdate(
|
||||
{
|
||||
row: e.detail.row,
|
||||
meta: {
|
||||
fields: metaUpdate?.fields || {},
|
||||
},
|
||||
},
|
||||
block
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const getTableSelector = (): SchemaConfigProps[] => {
|
||||
const row: Row = getInputValue(fieldData, "row") as Row
|
||||
return [
|
||||
{
|
||||
comp: TableSelector,
|
||||
title: "Table",
|
||||
onChange: (e: CustomEvent) => {
|
||||
const rowKey = get(stepState)[block.id]?.rowType || "row"
|
||||
automationStore.actions.requestUpdate(
|
||||
{
|
||||
_tableId: e.detail,
|
||||
meta: {},
|
||||
[rowKey]: e.detail
|
||||
? {
|
||||
tableId: e.detail,
|
||||
}
|
||||
: {},
|
||||
},
|
||||
block
|
||||
)
|
||||
},
|
||||
props: () => ({
|
||||
isTrigger: isTrigger(block),
|
||||
value: row?.tableId ?? "",
|
||||
disabled: isTest,
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// STEP
|
||||
if (automationStore.actions.isRowStep(block)) {
|
||||
const layout: SchemaConfigProps[] = [
|
||||
...getTableSelector(),
|
||||
...getIdConfig("rowId"),
|
||||
coreDivider,
|
||||
...getRowSelector(),
|
||||
]
|
||||
return layout
|
||||
} else if (automationStore.actions.isRowTrigger(block) && isTest) {
|
||||
// TRIGGER/TEST - Will be used to replace the test modal
|
||||
const schema = block.schema.outputs.properties
|
||||
|
||||
const getRevConfig = (): SchemaConfigProps[] => {
|
||||
// part of outputs for row type
|
||||
const rowRevEntry = schema["revision"]
|
||||
if (!rowRevEntry) {
|
||||
return []
|
||||
}
|
||||
const rowRevlabel = getFieldLabel("revision", rowRevEntry)
|
||||
const selected = get(selectedAutomation)
|
||||
const triggerOutputs = selected.data?.testData
|
||||
|
||||
return [
|
||||
{
|
||||
comp: DrawerBindableInput,
|
||||
title: rowRevlabel,
|
||||
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||
const update = { ["revision"]: e.detail }
|
||||
if (block) {
|
||||
automationStore.actions.requestUpdate(update, block)
|
||||
}
|
||||
},
|
||||
props: () => ({
|
||||
panel: AutomationBindingPanel,
|
||||
value: triggerOutputs ? triggerOutputs["revision"] : undefined,
|
||||
updateOnChange: false,
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// A select to switch from `row` to `oldRow`
|
||||
const getRowTypeConfig = (): SchemaConfigProps[] => {
|
||||
if (block.event !== AutomationEventType.ROW_UPDATE) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!get(stepState)?.[block.id]?.rowType) {
|
||||
stepState.update(state => ({
|
||||
...state,
|
||||
[block.id]: {
|
||||
rowType: "row",
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
type RowType = { name: string; id: string }
|
||||
|
||||
return [
|
||||
{
|
||||
comp: Select,
|
||||
tooltip: `You can configure test data for both the updated row and
|
||||
the old row, if you need it. Just select the one you wish to alter`,
|
||||
title: "Row data",
|
||||
props: () => ({
|
||||
value: get(stepState)?.[block.id].rowType,
|
||||
placeholder: false,
|
||||
getOptionLabel: (type: RowType) => type.name,
|
||||
getOptionValue: (type: RowType) => type.id,
|
||||
options: [
|
||||
{
|
||||
id: "row",
|
||||
name: "Updated row",
|
||||
},
|
||||
{ id: "oldRow", name: "Old row" },
|
||||
] as RowType[],
|
||||
}),
|
||||
onChange: (e: CustomEvent) => {
|
||||
if (e.detail === get(stepState)?.[block.id].rowType) {
|
||||
return
|
||||
}
|
||||
stepState.update(state => ({
|
||||
...state,
|
||||
[block.id]: {
|
||||
rowType: e.detail,
|
||||
},
|
||||
}))
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const getTriggerRowSelector = (): SchemaConfigProps[] => {
|
||||
const rowKey = get(stepState)?.[block.id]?.rowType ?? "row"
|
||||
const rowData: Row = getInputValue(fieldData, rowKey) as Row
|
||||
const row: Row = getInputValue(fieldData, "row") as Row
|
||||
const meta: Record<string, unknown> = getInputValue(
|
||||
fieldData,
|
||||
"meta"
|
||||
) as Record<string, unknown>
|
||||
return [
|
||||
{
|
||||
comp: RowSelector,
|
||||
wrapped: false,
|
||||
props: () => ({
|
||||
componentWidth: 280,
|
||||
row: rowData || {
|
||||
tableId: row?.tableId,
|
||||
},
|
||||
meta: {
|
||||
fields: meta?.oldFields || {},
|
||||
},
|
||||
}),
|
||||
onChange: (e: CustomEvent<FormUpdate>) => {
|
||||
const metaUpdate = e.detail?.meta as Record<string, unknown>
|
||||
|
||||
const update = {
|
||||
[rowKey]: e.detail.row,
|
||||
meta: {
|
||||
fields: meta?.fields || {},
|
||||
oldFields:
|
||||
(metaUpdate?.fields as Record<string, unknown>) || {},
|
||||
},
|
||||
}
|
||||
if (block) {
|
||||
automationStore.actions.requestUpdate(update, block)
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const layout: SchemaConfigProps[] = [
|
||||
...getTableSelector(),
|
||||
{
|
||||
comp: RowFetch,
|
||||
title: "Row",
|
||||
props: () => {
|
||||
return {
|
||||
tableId: "ta_bb_employee",
|
||||
meta: getInputValue(fieldData, "meta"),
|
||||
}
|
||||
},
|
||||
onChange: () => {},
|
||||
},
|
||||
...getIdConfig("id"),
|
||||
...getRevConfig(),
|
||||
...getRowTypeConfig(),
|
||||
coreDivider,
|
||||
...getTriggerRowSelector(),
|
||||
]
|
||||
return layout
|
||||
}
|
||||
}
|
|
@ -6,7 +6,13 @@
|
|||
Multiselect,
|
||||
Button,
|
||||
} from "@budibase/bbui"
|
||||
import { CalculationType, canGroupBy, isNumeric } from "@budibase/types"
|
||||
import {
|
||||
CalculationType,
|
||||
canGroupBy,
|
||||
FieldType,
|
||||
isNumeric,
|
||||
isNumericStaticFormula,
|
||||
} from "@budibase/types"
|
||||
import InfoDisplay from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
||||
import { getContext } from "svelte"
|
||||
import DetailPopover from "@/components/common/DetailPopover.svelte"
|
||||
|
@ -94,10 +100,15 @@
|
|||
if (fieldSchema.calculationType) {
|
||||
return false
|
||||
}
|
||||
// static numeric formulas will work
|
||||
if (isNumericStaticFormula(fieldSchema)) {
|
||||
return true
|
||||
}
|
||||
// Only allow numeric columns for most calculation types
|
||||
if (
|
||||
self.type !== CalculationType.COUNT &&
|
||||
!isNumeric(fieldSchema.type)
|
||||
!isNumeric(fieldSchema.type) &&
|
||||
fieldSchema.responseType !== FieldType.NUMBER
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
},
|
||||
{
|
||||
label: "Multi-select",
|
||||
value: FieldType.ARRAY.type,
|
||||
value: FieldType.ARRAY,
|
||||
},
|
||||
{
|
||||
label: "Barcode/QR",
|
||||
|
|
|
@ -7,15 +7,15 @@
|
|||
import { createEventDispatcher } from "svelte"
|
||||
import { capitalise } from "@/helpers"
|
||||
|
||||
export let value
|
||||
export let error
|
||||
export let value = undefined
|
||||
export let error = undefined
|
||||
export let placeholder = null
|
||||
export let autoWidth = false
|
||||
export let quiet = false
|
||||
export let allowPublic = true
|
||||
export let allowRemove = false
|
||||
export let disabled = false
|
||||
export let align
|
||||
export let align = undefined
|
||||
export let footer = null
|
||||
export let allowedRoles = null
|
||||
export let allowCreator = false
|
||||
|
@ -135,7 +135,6 @@
|
|||
{autoWidth}
|
||||
{quiet}
|
||||
{disabled}
|
||||
{align}
|
||||
{footer}
|
||||
bind:value
|
||||
on:change={onChange}
|
||||
|
|
|
@ -49,11 +49,11 @@
|
|||
|
||||
export let bindings: EnrichedBinding[] = []
|
||||
export let value: string = ""
|
||||
export let allowHBS = true
|
||||
export let allowJS = false
|
||||
export let allowHBS: boolean | undefined = true
|
||||
export let allowJS: boolean | undefined = false
|
||||
export let allowHelpers = true
|
||||
export let allowSnippets = true
|
||||
export let context = null
|
||||
export let context: Record<any, any> | undefined = undefined
|
||||
export let snippets: Snippet[] | null = null
|
||||
export let autofocusEditor = false
|
||||
export let placeholder: string | null = null
|
||||
|
@ -128,7 +128,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
const getModeOptions = (allowHBS: boolean, allowJS: boolean) => {
|
||||
const getModeOptions = (allowHBS = true, allowJS = false) => {
|
||||
let options = []
|
||||
if (allowHBS) {
|
||||
options.push(BindingMode.Text)
|
||||
|
|
|
@ -21,15 +21,15 @@
|
|||
import SnippetDrawer from "./SnippetDrawer.svelte"
|
||||
import UpgradeButton from "@/pages/builder/portal/_components/UpgradeButton.svelte"
|
||||
|
||||
export let addHelper: (_helper: Helper, _js?: boolean) => void
|
||||
export let addBinding: (_binding: EnrichedBinding) => void
|
||||
export let addSnippet: (_snippet: Snippet) => void
|
||||
export let bindings: EnrichedBinding[]
|
||||
export let snippets: Snippet[] | null
|
||||
export let mode: BindingMode
|
||||
export let allowHelpers: boolean
|
||||
export let allowSnippets: boolean
|
||||
export let context = null
|
||||
export let addHelper: (_helper: Helper, _js?: boolean) => void = () => {}
|
||||
export let addBinding: (_binding: EnrichedBinding) => void = () => {}
|
||||
export let addSnippet: (_snippet: Snippet) => void = () => {}
|
||||
export let bindings: EnrichedBinding[] | undefined
|
||||
export let snippets: Snippet[] | null = null
|
||||
export let mode: BindingMode | undefined = BindingMode.Text
|
||||
export let allowHelpers: boolean = true
|
||||
export let allowSnippets: boolean = true
|
||||
export let context: Record<any, any> | undefined = undefined
|
||||
|
||||
let search = ""
|
||||
let searching = false
|
||||
|
@ -93,7 +93,7 @@
|
|||
search
|
||||
)
|
||||
|
||||
function onModeChange(_mode: BindingMode) {
|
||||
function onModeChange(_mode: BindingMode | undefined) {
|
||||
selectedCategory = null
|
||||
}
|
||||
$: onModeChange(mode)
|
||||
|
|
|
@ -36,10 +36,10 @@
|
|||
export let value: string = ""
|
||||
export let allowHelpers = true
|
||||
export let allowSnippets = true
|
||||
export let context = null
|
||||
export let context: Record<any, any> | undefined = undefined
|
||||
export let autofocusEditor = false
|
||||
export let placeholder = null
|
||||
export let height = 180
|
||||
export let placeholder: string | undefined
|
||||
export let dropdown = DropdownPosition.Absolute
|
||||
|
||||
let getCaretPosition: CaretPositionFn | undefined
|
||||
let insertAtPos: InsertAtPositionFn | undefined
|
||||
|
@ -126,26 +126,25 @@
|
|||
}
|
||||
|
||||
const updateValue = (val: any) => {
|
||||
dispatch("change", readableToRuntimeBinding(bindings, val))
|
||||
if (!val) {
|
||||
dispatch("change", null)
|
||||
}
|
||||
const update = readableToRuntimeBinding(bindings, encodeJSBinding(val))
|
||||
dispatch("change", update)
|
||||
}
|
||||
|
||||
const onChangeJSValue = (e: { detail: string }) => {
|
||||
if (!e.detail?.trim()) {
|
||||
const onBlurJSValue = (e: { detail: string }) => {
|
||||
// Don't bother saving empty values as JS
|
||||
updateValue(null)
|
||||
} else {
|
||||
updateValue(encodeJSBinding(e.detail))
|
||||
}
|
||||
updateValue(e.detail?.trim())
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="code-panel" style="height:{height}px;">
|
||||
<div class="code-panel">
|
||||
<div class="editor">
|
||||
{#key jsCompletions}
|
||||
<CodeEditor
|
||||
value={jsValue || ""}
|
||||
on:change={onChangeJSValue}
|
||||
on:blur
|
||||
on:blur={onBlurJSValue}
|
||||
completions={jsCompletions}
|
||||
{bindings}
|
||||
mode={EditorModes.JS}
|
||||
|
@ -155,7 +154,7 @@
|
|||
placeholder={placeholder ||
|
||||
"Add bindings by typing $ or use the menu on the right"}
|
||||
jsBindingWrapping
|
||||
dropdown={DropdownPosition.Absolute}
|
||||
{dropdown}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
|
@ -164,6 +163,7 @@
|
|||
<style>
|
||||
.code-panel {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Editor */
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Icon, Input, Drawer, Button, CoreTextArea } from "@budibase/bbui"
|
||||
import { Icon, Input, Drawer, Button, TextArea } from "@budibase/bbui"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
|
@ -67,7 +67,7 @@
|
|||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="control" class:multiline class:disabled class:scrollable>
|
||||
<svelte:component
|
||||
this={multiline ? CoreTextArea : Input}
|
||||
this={multiline ? TextArea : Input}
|
||||
{label}
|
||||
{disabled}
|
||||
readonly={isJS}
|
||||
|
@ -78,7 +78,7 @@
|
|||
{placeholder}
|
||||
{updateOnChange}
|
||||
{autocomplete}
|
||||
/>
|
||||
>
|
||||
{#if !disabled && !disableBindings}
|
||||
<div
|
||||
class="icon"
|
||||
|
@ -90,6 +90,7 @@
|
|||
<Icon size="S" name="FlashOn" />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:component>
|
||||
</div>
|
||||
<Drawer
|
||||
on:drawerHide={onDrawerHide}
|
||||
|
|
|
@ -14,14 +14,15 @@
|
|||
export let value = ""
|
||||
export let bindings = []
|
||||
export let title = "Bindings"
|
||||
export let placeholder
|
||||
export let label
|
||||
export let placeholder = undefined
|
||||
export let label = undefined
|
||||
export let disabled = false
|
||||
export let allowJS = true
|
||||
export let allowHelpers = true
|
||||
export let updateOnChange = true
|
||||
export let type
|
||||
export let schema
|
||||
export let type = undefined
|
||||
export let schema = undefined
|
||||
|
||||
export let allowHBS = true
|
||||
export let context = {}
|
||||
|
||||
|
@ -230,7 +231,6 @@
|
|||
}
|
||||
|
||||
.icon {
|
||||
right: 1px;
|
||||
bottom: 1px;
|
||||
position: absolute;
|
||||
justify-content: center;
|
||||
|
@ -239,8 +239,6 @@
|
|||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
border-left: 1px solid var(--spectrum-alias-border-color);
|
||||
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||
width: 31px;
|
||||
color: var(--spectrum-alias-text-color);
|
||||
background-color: var(--spectrum-global-color-gray-75);
|
||||
|
|
|
@ -80,5 +80,6 @@
|
|||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -22,6 +22,7 @@ import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelt
|
|||
import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte"
|
||||
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
|
||||
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
||||
import TopLevelColumnEditor from "./controls/ColumnEditor/TopLevelColumnEditor.svelte"
|
||||
import GridColumnEditor from "./controls/GridColumnConfiguration/GridColumnConfiguration.svelte"
|
||||
import BarButtonList from "./controls/BarButtonList.svelte"
|
||||
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
||||
|
@ -62,7 +63,10 @@ const componentMap = {
|
|||
stepConfiguration: FormStepConfiguration,
|
||||
formStepControls: FormStepControls,
|
||||
columns: ColumnEditor,
|
||||
// "Basic" actually includes nested JSON and relationship fields
|
||||
"columns/basic": BasicColumnEditor,
|
||||
// "Top level" is only the top level schema fields
|
||||
"columns/toplevel": TopLevelColumnEditor,
|
||||
"columns/grid": GridColumnEditor,
|
||||
tableConditions: TableConditionEditor,
|
||||
"field/sortable": SortableFieldSelect,
|
||||
|
|
|
@ -145,7 +145,7 @@
|
|||
<div class="column">
|
||||
<div class="wide">
|
||||
<Body size="S">
|
||||
By default, all columns will automatically be shown.
|
||||
The default column configuration will automatically be shown.
|
||||
<br />
|
||||
You can manually control which columns are included by adding them
|
||||
below.
|
||||
|
|
|
@ -10,10 +10,18 @@
|
|||
} from "@/dataBinding"
|
||||
import { selectedScreen, tables } from "@/stores/builder"
|
||||
|
||||
export let componentInstance
|
||||
const getSearchableFields = (schema, tableList) => {
|
||||
return search.getFields(tableList, Object.values(schema || {}), {
|
||||
allowLinks: true,
|
||||
})
|
||||
}
|
||||
|
||||
export let componentInstance = undefined
|
||||
export let value = []
|
||||
export let allowCellEditing = true
|
||||
export let allowReorder = true
|
||||
export let getSchemaFields = getSearchableFields
|
||||
export let placeholder = "All columns"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -28,13 +36,7 @@
|
|||
: enrichedSchemaFields?.map(field => field.name)
|
||||
$: sanitisedValue = getValidColumns(value, options)
|
||||
$: updateBoundValue(sanitisedValue)
|
||||
$: enrichedSchemaFields = search.getFields(
|
||||
$tables.list,
|
||||
Object.values(schema || {}),
|
||||
{
|
||||
allowLinks: true,
|
||||
}
|
||||
)
|
||||
$: enrichedSchemaFields = getSchemaFields(schema, $tables.list)
|
||||
|
||||
$: {
|
||||
value = (value || []).filter(
|
||||
|
@ -44,7 +46,7 @@
|
|||
|
||||
const getText = value => {
|
||||
if (!value?.length) {
|
||||
return "All columns"
|
||||
return placeholder
|
||||
}
|
||||
let text = `${value.length} column`
|
||||
if (value.length !== 1) {
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import ColumnEditor from "./ColumnEditor.svelte"
|
||||
import type { TableSchema } from "@budibase/types"
|
||||
|
||||
const getTopLevelSchemaFields = (schema: TableSchema) => {
|
||||
return Object.values(schema).filter(fieldSchema => !fieldSchema.nestedJSON)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ColumnEditor
|
||||
{...$$props}
|
||||
on:change
|
||||
allowCellEditing={false}
|
||||
getSchemaFields={getTopLevelSchemaFields}
|
||||
/>
|
|
@ -7,15 +7,15 @@
|
|||
readableToRuntimeBinding,
|
||||
} from "@/dataBinding"
|
||||
|
||||
export let schemaFields
|
||||
export let filters
|
||||
export let schemaFields = undefined
|
||||
export let filters = undefined
|
||||
export let bindings = []
|
||||
export let panel = ClientBindingPanel
|
||||
export let allowBindings = true
|
||||
export let allowOnEmpty
|
||||
export let datasource
|
||||
export let builderType
|
||||
export let docsURL
|
||||
export let allowOnEmpty = undefined
|
||||
export let datasource = undefined
|
||||
export let builderType = undefined
|
||||
export let docsURL = undefined
|
||||
export let evaluationContext = {}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -15,24 +15,23 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let defaults
|
||||
export let defaults = undefined
|
||||
export let object = defaults || {}
|
||||
export let activity = {}
|
||||
export let readOnly
|
||||
export let noAddButton
|
||||
export let name
|
||||
export let readOnly = false
|
||||
export let noAddButton = false
|
||||
export let name = ""
|
||||
export let headings = false
|
||||
export let options
|
||||
export let toggle
|
||||
export let options = undefined
|
||||
export let toggle = false
|
||||
export let keyPlaceholder = "Key"
|
||||
export let valuePlaceholder = "Value"
|
||||
export let valueHeading
|
||||
export let keyHeading
|
||||
export let tooltip
|
||||
export let menuItems
|
||||
export let valueHeading = ""
|
||||
export let keyHeading = ""
|
||||
export let tooltip = ""
|
||||
export let menuItems = []
|
||||
export let showMenu = false
|
||||
export let bindings = []
|
||||
export let bindingDrawerLeft
|
||||
export let allowHelpers = true
|
||||
export let customButtonText = null
|
||||
export let keyBindings = false
|
||||
|
@ -140,7 +139,6 @@
|
|||
value={field.name}
|
||||
{allowJS}
|
||||
{allowHelpers}
|
||||
drawerLeft={bindingDrawerLeft}
|
||||
{context}
|
||||
/>
|
||||
{:else}
|
||||
|
@ -167,7 +165,6 @@
|
|||
value={field.value}
|
||||
{allowJS}
|
||||
{allowHelpers}
|
||||
drawerLeft={bindingDrawerLeft}
|
||||
{context}
|
||||
/>
|
||||
{:else}
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
keyPlaceholder="Binding name"
|
||||
valuePlaceholder="Default"
|
||||
bindings={[...userBindings]}
|
||||
bindingDrawerLeft="260px"
|
||||
allowHelpers={false}
|
||||
on:change
|
||||
/>
|
||||
|
|
|
@ -582,7 +582,6 @@
|
|||
...globalDynamicRequestBindings,
|
||||
...dataSourceStaticBindings,
|
||||
]}
|
||||
bindingDrawerLeft="260px"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="Params">
|
||||
|
@ -591,7 +590,6 @@
|
|||
name="param"
|
||||
headings
|
||||
bindings={mergedBindings}
|
||||
bindingDrawerLeft="260px"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="Headers">
|
||||
|
@ -602,7 +600,6 @@
|
|||
name="header"
|
||||
headings
|
||||
bindings={mergedBindings}
|
||||
bindingDrawerLeft="260px"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="Body">
|
||||
|
|
|
@ -11,8 +11,7 @@
|
|||
import { goto } from "@roxi/routify"
|
||||
import { appStore, oauth2 } from "@/stores/builder"
|
||||
import DetailPopover from "@/components/common/DetailPopover.svelte"
|
||||
import { featureFlag } from "@/helpers"
|
||||
import { FeatureFlag, RestAuthType } from "@budibase/types"
|
||||
import { RestAuthType } from "@budibase/types"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
type Config = { label: string; value: string }
|
||||
|
@ -57,15 +56,13 @@
|
|||
|
||||
$: title = !authConfig ? "Authentication" : `Auth: ${authConfig.label}`
|
||||
|
||||
$: oauth2Enabled = featureFlag.isEnabled(FeatureFlag.OAUTH2_CONFIG)
|
||||
|
||||
onMount(() => {
|
||||
oauth2.fetch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<DetailPopover bind:this={popover} {title} align={PopoverAlignment.Right}>
|
||||
<div slot="anchor" class:display-new={!authConfig && oauth2Enabled}>
|
||||
<div slot="anchor" class:display-new={!authConfig}>
|
||||
<ActionButton icon="LockClosed" quiet selected>
|
||||
{#if !authConfig}
|
||||
Authentication
|
||||
|
@ -96,7 +93,6 @@
|
|||
>
|
||||
</div>
|
||||
|
||||
{#if oauth2Enabled}
|
||||
<Divider />
|
||||
|
||||
<Body size="S" color="var(--spectrum-global-color-gray-700)">
|
||||
|
@ -108,13 +104,12 @@
|
|||
{#each $oauth2.configs as config}
|
||||
<ListItem
|
||||
title={config.name}
|
||||
on:click={() =>
|
||||
selectConfiguration(config._id, RestAuthType.OAUTH2)}
|
||||
on:click={() => selectConfiguration(config._id, RestAuthType.OAUTH2)}
|
||||
selected={config._id === authConfigId}
|
||||
/>
|
||||
{/each}
|
||||
</List>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<Button secondary icon="Add" on:click={addOAuth2Configuration}
|
||||
>Add OAuth2</Button
|
||||
|
|
|
@ -71,4 +71,5 @@ export const AutoScreenTypes = {
|
|||
BLANK: "blank",
|
||||
TABLE: "table",
|
||||
FORM: "form",
|
||||
PDF: "pdf",
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
getJsHelperList,
|
||||
} from "@budibase/string-templates"
|
||||
import { TableNames } from "./constants"
|
||||
import { JSONUtils, Constants } from "@budibase/frontend-core"
|
||||
import { JSONUtils, Constants, SchemaUtils } from "@budibase/frontend-core"
|
||||
import ActionDefinitions from "@/components/design/settings/controls/ButtonActionEditor/manifest.json"
|
||||
import { environment, licensing } from "@/stores/portal"
|
||||
import { convertOldFieldFormat } from "@/components/design/settings/controls/FieldConfiguration/utils"
|
||||
|
@ -1026,25 +1026,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
|||
|
||||
// Check for any JSON fields so we can add any top level properties
|
||||
if (schema) {
|
||||
let jsonAdditions = {}
|
||||
Object.keys(schema).forEach(fieldKey => {
|
||||
const fieldSchema = schema[fieldKey]
|
||||
if (fieldSchema?.type === "json") {
|
||||
const jsonSchema = JSONUtils.convertJSONSchemaToTableSchema(
|
||||
fieldSchema,
|
||||
{
|
||||
squashObjects: true,
|
||||
}
|
||||
)
|
||||
Object.keys(jsonSchema).forEach(jsonKey => {
|
||||
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
|
||||
type: jsonSchema[jsonKey].type,
|
||||
nestedJSON: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
schema = { ...schema, ...jsonAdditions }
|
||||
schema = SchemaUtils.addNestedJSONSchemaFields(schema)
|
||||
}
|
||||
|
||||
// Determine if we should add ID and rev to the schema
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
import AutomationPanel from "@/components/automation/AutomationPanel/AutomationPanel.svelte"
|
||||
import CreateAutomationModal from "@/components/automation/AutomationPanel/CreateAutomationModal.svelte"
|
||||
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import TestPanel from "@/components/automation/AutomationBuilder/TestPanel.svelte"
|
||||
import { onDestroy, onMount } from "svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import { syncURLToState } from "@/helpers/urlStateSync"
|
||||
import * as routify from "@roxi/routify"
|
||||
import {
|
||||
|
@ -12,8 +11,10 @@
|
|||
automationStore,
|
||||
selectedAutomation,
|
||||
} from "@/stores/builder"
|
||||
import StepPanel from "@/components/automation/AutomationBuilder/StepPanel.svelte"
|
||||
|
||||
$: automationId = $selectedAutomation?.data?._id
|
||||
$: blockRefs = $selectedAutomation.blockRefs
|
||||
$: builderStore.selectResource(automationId)
|
||||
|
||||
const stopSyncing = syncURLToState({
|
||||
|
@ -29,15 +30,6 @@
|
|||
let modal
|
||||
let webhookModal
|
||||
|
||||
onMount(async () => {
|
||||
await automationStore.actions.initAppSelf()
|
||||
|
||||
// Init the binding evaluation context
|
||||
automationStore.actions.initContext()
|
||||
|
||||
$automationStore.showTestPanel = false
|
||||
})
|
||||
|
||||
onDestroy(stopSyncing)
|
||||
</script>
|
||||
|
||||
|
@ -70,15 +62,16 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $automationStore.showTestPanel}
|
||||
<div class="setup">
|
||||
<TestPanel automation={$selectedAutomation.data} />
|
||||
{#if blockRefs[$automationStore.selectedNodeId] && $automationStore.selectedNodeId}
|
||||
<div class="step-panel">
|
||||
<StepPanel />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<CreateAutomationModal {webhookModal} />
|
||||
</Modal>
|
||||
<Modal bind:this={webhookModal} width="30%">
|
||||
<Modal bind:this={webhookModal}>
|
||||
<CreateWebhookModal />
|
||||
</Modal>
|
||||
</div>
|
||||
|
@ -115,16 +108,17 @@
|
|||
.main {
|
||||
width: 300px;
|
||||
}
|
||||
.setup {
|
||||
padding-top: 9px;
|
||||
|
||||
.step-panel {
|
||||
border-left: var(--border-light);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-l);
|
||||
background-color: var(--background);
|
||||
grid-column: 3;
|
||||
overflow: auto;
|
||||
grid-column: 3;
|
||||
width: 360px;
|
||||
max-width: 360px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
import { getBindableProperties } from "@/dataBinding"
|
||||
import BarButtonList from "@/components/design/settings/controls/BarButtonList.svelte"
|
||||
import URLVariableTestInput from "@/components/design/settings/controls/URLVariableTestInput.svelte"
|
||||
import { DrawerBindableInput } from "@/components/common/bindings"
|
||||
|
||||
$: bindings = getBindableProperties($selectedScreen, null)
|
||||
$: screenSettings = getScreenSettings($selectedScreen)
|
||||
|
@ -23,7 +24,59 @@
|
|||
let errors = {}
|
||||
|
||||
const getScreenSettings = screen => {
|
||||
let settings = [
|
||||
// Determine correct screen settings for the top level component
|
||||
let screenComponentSettings = []
|
||||
switch ($selectedScreen.props._component) {
|
||||
case "@budibase/standard-components/pdf":
|
||||
screenComponentSettings = [
|
||||
{
|
||||
key: "props.fileName",
|
||||
label: "PDF title",
|
||||
defaultValue: "Report",
|
||||
control: DrawerBindableInput,
|
||||
},
|
||||
{
|
||||
key: "props.buttonText",
|
||||
label: "Button text",
|
||||
defaultValue: "Download PDF",
|
||||
control: DrawerBindableInput,
|
||||
},
|
||||
]
|
||||
break
|
||||
default:
|
||||
screenComponentSettings = [
|
||||
{
|
||||
key: "width",
|
||||
label: "Width",
|
||||
control: Select,
|
||||
props: {
|
||||
options: ["Extra small", "Small", "Medium", "Large", "Max"],
|
||||
placeholder: "Default",
|
||||
disabled: !!screen.layoutId,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "props.layout",
|
||||
label: "Layout",
|
||||
defaultValue: "flex",
|
||||
control: BarButtonList,
|
||||
props: {
|
||||
options: [
|
||||
{
|
||||
barIcon: "ModernGridView",
|
||||
value: "flex",
|
||||
},
|
||||
{
|
||||
barIcon: "ViewGrid",
|
||||
value: "grid",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: "routing.homeScreen",
|
||||
control: Checkbox,
|
||||
|
@ -66,34 +119,7 @@
|
|||
label: "On screen load",
|
||||
control: ButtonActionEditor,
|
||||
},
|
||||
{
|
||||
key: "width",
|
||||
label: "Width",
|
||||
control: Select,
|
||||
props: {
|
||||
options: ["Extra small", "Small", "Medium", "Large", "Max"],
|
||||
placeholder: "Default",
|
||||
disabled: !!screen.layoutId,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "props.layout",
|
||||
label: "Layout",
|
||||
defaultValue: "flex",
|
||||
control: BarButtonList,
|
||||
props: {
|
||||
options: [
|
||||
{
|
||||
barIcon: "ModernGridView",
|
||||
value: "flex",
|
||||
},
|
||||
{
|
||||
barIcon: "ViewGrid",
|
||||
value: "grid",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
...screenComponentSettings,
|
||||
{
|
||||
key: "urlTest",
|
||||
control: URLVariableTestInput,
|
||||
|
@ -102,8 +128,6 @@
|
|||
},
|
||||
},
|
||||
]
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
const routeTaken = url => {
|
||||
|
|
|
@ -26,7 +26,9 @@
|
|||
|
||||
<div class="info">
|
||||
<Icon name="InfoOutline" size="S" />
|
||||
<Body size="S">These settings apply to all screens</Body>
|
||||
<Body size="S">
|
||||
These settings apply to all screens. PDFs are always light theme.
|
||||
</Body>
|
||||
</div>
|
||||
<Layout noPadding gap="S">
|
||||
<Layout noPadding gap="XS">
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
// Get initial set of allowed components
|
||||
let allowedComponents = []
|
||||
const definition = componentStore.getDefinition(component?._component)
|
||||
if (definition.legalDirectChildren?.length) {
|
||||
if (definition?.legalDirectChildren?.length) {
|
||||
allowedComponents = definition.legalDirectChildren.map(x => {
|
||||
return `@budibase/standard-components/${x}`
|
||||
})
|
||||
|
@ -67,7 +67,7 @@
|
|||
}
|
||||
|
||||
// Build up list of illegal children from ancestors
|
||||
let illegalChildren = definition.illegalChildren || []
|
||||
let illegalChildren = definition?.illegalChildren || []
|
||||
path.forEach(ancestor => {
|
||||
// Sidepanels and modals can be nested anywhere in the component tree, but really they are always rendered at the top level.
|
||||
// Because of this, it doesn't make sense to carry over any parent illegal children to them, so the array is reset here.
|
||||
|
@ -144,11 +144,6 @@
|
|||
}
|
||||
})
|
||||
|
||||
// Swap blocks and plugins
|
||||
let tmp = enrichedStructure[1]
|
||||
enrichedStructure[1] = enrichedStructure[0]
|
||||
enrichedStructure[0] = tmp
|
||||
|
||||
return enrichedStructure
|
||||
}
|
||||
|
||||
|
|
|
@ -20,9 +20,11 @@
|
|||
"name": "Data",
|
||||
"icon": "Data",
|
||||
"children": [
|
||||
"singlerowprovider",
|
||||
"dataprovider",
|
||||
"repeater",
|
||||
"gridblock",
|
||||
"pdftable",
|
||||
"spreadsheet",
|
||||
"dynamicfilter",
|
||||
"daterangepicker"
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<script>
|
||||
import DevicePreviewSelect from "./DevicePreviewSelect.svelte"
|
||||
import AppPreview from "./AppPreview.svelte"
|
||||
import { screenStore, appStore } from "@/stores/builder"
|
||||
import { screenStore, appStore, selectedScreen } from "@/stores/builder"
|
||||
import UndoRedoControl from "@/components/common/UndoRedoControl.svelte"
|
||||
import ScreenErrorsButton from "./ScreenErrorsButton.svelte"
|
||||
import { Divider } from "@budibase/bbui"
|
||||
import { ScreenVariant } from "@budibase/types"
|
||||
|
||||
$: isPDF = $selectedScreen?.variant === ScreenVariant.PDF
|
||||
</script>
|
||||
|
||||
<div class="app-panel">
|
||||
|
@ -14,10 +17,12 @@
|
|||
<UndoRedoControl store={screenStore.history} />
|
||||
</div>
|
||||
<div class="header-right">
|
||||
{#if !isPDF}
|
||||
{#if $appStore.clientFeatures.devicePreview}
|
||||
<DevicePreviewSelect />
|
||||
{/if}
|
||||
<Divider vertical />
|
||||
{/if}
|
||||
<ScreenErrorsButton />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
// Otherwise choose a datasource
|
||||
datasourceModal.show()
|
||||
}
|
||||
} else if (mode === AutoScreenTypes.BLANK) {
|
||||
} else if (mode === AutoScreenTypes.BLANK || mode === AutoScreenTypes.PDF) {
|
||||
screenDetailsModal.show()
|
||||
} else {
|
||||
throw new Error("Invalid mode provided")
|
||||
|
@ -101,8 +101,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
const createBlankScreen = async ({ route }) => {
|
||||
const screenTemplates = screenTemplating.blank({ route, screens })
|
||||
const createBasicScreen = async ({ route }) => {
|
||||
const screenTemplates =
|
||||
mode === AutoScreenTypes.BLANK
|
||||
? screenTemplating.blank({ route, screens })
|
||||
: screenTemplating.pdf({ route, screens })
|
||||
const newScreens = await createScreens(screenTemplates)
|
||||
loadNewScreen(newScreens[0])
|
||||
}
|
||||
|
@ -243,7 +246,7 @@
|
|||
</Modal>
|
||||
|
||||
<Modal bind:this={screenDetailsModal}>
|
||||
<ScreenDetailsModal onConfirm={createBlankScreen} />
|
||||
<ScreenDetailsModal onConfirm={createBasicScreen} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={formTypeModal}>
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1099_17726)">
|
||||
<g clip-path="url(#clip1_1099_17726)" filter="url(#filter0_d_1099_17726)">
|
||||
<rect width="118" height="65" fill="#D8B500"/>
|
||||
<mask id="mask0_1099_17726" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="118" height="65">
|
||||
<rect width="118" height="65" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_1099_17726)">
|
||||
</g>
|
||||
</g>
|
||||
<rect x="38.0762" y="22.3271" width="41.6766" height="42.6718" fill="white" fill-opacity="0.5"/>
|
||||
<rect x="23.1543" y="11.4121" width="71.3021" height="53.5878" fill="url(#paint0_linear_1099_17726)"/>
|
||||
<rect x="23.6543" y="11.9121" width="70.3021" height="52.5878" stroke="white" stroke-opacity="0.2"/>
|
||||
<path d="M44.1365 45.7637V34.0637H48.4205C49.2725 34.0637 50.0585 34.1837 50.7785 34.4237C51.4985 34.6637 52.0745 35.0657 52.5065 35.6297C52.9505 36.1937 53.1725 36.9677 53.1725 37.9517C53.1725 38.8997 52.9505 39.6797 52.5065 40.2917C52.0745 40.8917 51.4985 41.3357 50.7785 41.6237C50.0705 41.9117 49.3085 42.0557 48.4925 42.0557H47.2325V45.7637H44.1365ZM47.2325 39.6077H48.3485C48.9605 39.6077 49.4105 39.4637 49.6985 39.1757C49.9985 38.8757 50.1485 38.4677 50.1485 37.9517C50.1485 37.4237 49.9865 37.0517 49.6625 36.8357C49.3385 36.6197 48.8765 36.5117 48.2765 36.5117H47.2325V39.6077ZM55.0877 45.7637V34.0637H58.5437C59.7317 34.0637 60.7757 34.2617 61.6757 34.6577C62.5877 35.0417 63.2957 35.6597 63.7997 36.5117C64.3037 37.3637 64.5557 38.4797 64.5557 39.8597C64.5557 41.2397 64.3037 42.3677 63.7997 43.2437C63.2957 44.1077 62.6057 44.7437 61.7297 45.1517C60.8537 45.5597 59.8517 45.7637 58.7237 45.7637H55.0877ZM58.1837 43.2797H58.3637C58.9277 43.2797 59.4377 43.1837 59.8937 42.9917C60.3497 42.7997 60.7097 42.4577 60.9737 41.9657C61.2497 41.4737 61.3877 40.7717 61.3877 39.8597C61.3877 38.9477 61.2497 38.2577 60.9737 37.7897C60.7097 37.3097 60.3497 36.9857 59.8937 36.8177C59.4377 36.6377 58.9277 36.5477 58.3637 36.5477H58.1837V43.2797ZM66.6365 45.7637V34.0637H74.2685V36.6557H69.7325V38.8877H73.6205V41.4797H69.7325V45.7637H66.6365Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_1099_17726" x="-10" y="-6" width="138" height="85" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1099_17726"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1099_17726" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1099_17726" x1="23.1543" y1="11.4121" x2="94.0187" y2="65.5726" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.6"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1099_17726">
|
||||
<rect width="118" height="65" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_1099_17726">
|
||||
<rect width="118" height="65" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
|
@ -4,8 +4,10 @@
|
|||
import blank from "./images/blank.svg"
|
||||
import table from "./images/tableInline.svg"
|
||||
import form from "./images/formUpdate.svg"
|
||||
import pdf from "./images/pdf.svg"
|
||||
import CreateScreenModal from "./CreateScreenModal.svelte"
|
||||
import { screenStore } from "@/stores/builder"
|
||||
import { AutoScreenTypes } from "@/constants"
|
||||
|
||||
export let onClose = null
|
||||
|
||||
|
@ -27,32 +29,54 @@
|
|||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card" on:click={() => createScreenModal.show("blank")}>
|
||||
<div
|
||||
class="card"
|
||||
on:click={() => createScreenModal.show(AutoScreenTypes.BLANK)}
|
||||
>
|
||||
<div class="image">
|
||||
<img alt="A blank screen" src={blank} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Blank</Body>
|
||||
<Body size="M">Blank</Body>
|
||||
<Body size="XS">Add an empty blank screen</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" on:click={() => createScreenModal.show("table")}>
|
||||
<div
|
||||
class="card"
|
||||
on:click={() => createScreenModal.show(AutoScreenTypes.TABLE)}
|
||||
>
|
||||
<div class="image">
|
||||
<img alt="A table of data" src={table} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Table</Body>
|
||||
<Body size="M">Table</Body>
|
||||
<Body size="XS">List rows in a table</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" on:click={() => createScreenModal.show("form")}>
|
||||
<div
|
||||
class="card"
|
||||
on:click={() => createScreenModal.show(AutoScreenTypes.PDF)}
|
||||
>
|
||||
<div class="image">
|
||||
<img alt="A form containing data" src={pdf} width="185" />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="M">PDF</Body>
|
||||
<Body size="XS">Create, edit and export your PDF</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="card"
|
||||
on:click={() => createScreenModal.show(AutoScreenTypes.FORM)}
|
||||
>
|
||||
<div class="image">
|
||||
<img alt="A form containing data" src={form} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Form</Body>
|
||||
<Body size="M">Form</Body>
|
||||
<Body size="XS">Capture data from your users</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -111,14 +135,13 @@
|
|||
.text {
|
||||
border: 1px solid var(--grey-4);
|
||||
border-radius: 0 0 4px 4px;
|
||||
padding: 8px 16px 13px 16px;
|
||||
}
|
||||
|
||||
.text :global(p:nth-child(1)) {
|
||||
margin-bottom: 6px;
|
||||
padding: 12px 16px 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.text :global(p:nth-child(2)) {
|
||||
color: var(--grey-6);
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,12 +4,8 @@
|
|||
import { url, isActive } from "@roxi/routify"
|
||||
import DeleteModal from "@/components/deploy/DeleteModal.svelte"
|
||||
import { isOnlyUser, appStore } from "@/stores/builder"
|
||||
import { featureFlag } from "@/helpers"
|
||||
import { FeatureFlag } from "@budibase/types"
|
||||
|
||||
let deleteModal: DeleteModal
|
||||
|
||||
$: oauth2Enabled = featureFlag.isEnabled(FeatureFlag.OAUTH2_CONFIG)
|
||||
</script>
|
||||
|
||||
<!-- routify:options index=4 -->
|
||||
|
@ -48,13 +44,11 @@
|
|||
url={$url("./version")}
|
||||
active={$isActive("./version")}
|
||||
/>
|
||||
{#if oauth2Enabled}
|
||||
<SideNavItem
|
||||
text="OAuth2"
|
||||
url={$url("./oauth2")}
|
||||
active={$isActive("./oauth2")}
|
||||
/>
|
||||
{/if}
|
||||
<SideNavItem
|
||||
text="App scripts"
|
||||
url={$url("./scripts")}
|
||||
|
|
|
@ -147,6 +147,13 @@
|
|||
await automationStore.actions.fetch()
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const shouldOpen = params.get("open") === ERROR
|
||||
const defaultAutoId = params.get("automationId")
|
||||
const defaultAuto = $automationStore.automations.find(
|
||||
auto => auto._id === defaultAutoId || undefined
|
||||
)
|
||||
|
||||
automationId = defaultAuto?._id || undefined
|
||||
|
||||
if (shouldOpen) {
|
||||
status = ERROR
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
import { API } from "@/api"
|
||||
import { onMount } from "svelte"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { getFormattedPlanName } from "@/helpers/planTitle"
|
||||
|
||||
$: license = $auth.user.license
|
||||
$: upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||
|
@ -260,7 +261,11 @@
|
|||
<Layout gap="XS" noPadding>
|
||||
<Heading size="XS">Plan</Heading>
|
||||
<Layout noPadding gap="S">
|
||||
<Body size="S">You are currently on the {license.plan.type} plan</Body>
|
||||
<Body size="S"
|
||||
>You are currently on the <b
|
||||
>{getFormattedPlanName(license.plan.type)}</b
|
||||
></Body
|
||||
>
|
||||
<div>
|
||||
<Body size="S"
|
||||
>If you purchase or update your plan on the account</Body
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -4,22 +4,24 @@ import { Helpers } from "@budibase/bbui"
|
|||
import { RoleUtils, Utils } from "@budibase/frontend-core"
|
||||
import { findAllMatchingComponents } from "@/helpers/components"
|
||||
import {
|
||||
layoutStore,
|
||||
appStore,
|
||||
componentStore,
|
||||
layoutStore,
|
||||
navigationStore,
|
||||
previewStore,
|
||||
selectedComponent,
|
||||
} from "@/stores/builder"
|
||||
import { createHistoryStore, HistoryStore } from "@/stores/builder/history"
|
||||
import { API } from "@/api"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import {
|
||||
FetchAppPackageResponse,
|
||||
DeleteScreenResponse,
|
||||
Screen,
|
||||
Component,
|
||||
SaveScreenResponse,
|
||||
ComponentDefinition,
|
||||
DeleteScreenResponse,
|
||||
FetchAppPackageResponse,
|
||||
SaveScreenResponse,
|
||||
Screen,
|
||||
ScreenVariant,
|
||||
} from "@budibase/types"
|
||||
|
||||
interface ScreenState {
|
||||
|
@ -115,6 +117,14 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
state.selectedScreenId = screen._id
|
||||
return state
|
||||
})
|
||||
|
||||
// If this is a PDF screen, ensure we're on desktop
|
||||
if (
|
||||
screen.variant === ScreenVariant.PDF &&
|
||||
get(previewStore).previewDevice !== "desktop"
|
||||
) {
|
||||
previewStore.setDevice("desktop")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { BaseStructure } from "../BaseStructure"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { ScreenVariant } from "@budibase/types"
|
||||
|
||||
export class Screen extends BaseStructure {
|
||||
constructor() {
|
||||
|
@ -81,3 +82,25 @@ export class Screen extends BaseStructure {
|
|||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export class PDFScreen extends Screen {
|
||||
constructor() {
|
||||
super()
|
||||
this._json.variant = ScreenVariant.PDF
|
||||
this._json.width = "Max"
|
||||
this._json.showNavigation = false
|
||||
this._json.props = {
|
||||
_id: Helpers.uuid(),
|
||||
_component: "@budibase/standard-components/pdf",
|
||||
_styles: {
|
||||
normal: {},
|
||||
hover: {},
|
||||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
_children: [],
|
||||
_instanceName: "PDF",
|
||||
title: "PDF",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { default as blank } from "./blank"
|
||||
export { default as form } from "./form"
|
||||
export { default as table } from "./table"
|
||||
export { default as pdf } from "./pdf"
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { PDFScreen } from "./Screen"
|
||||
import { capitalise } from "@/helpers"
|
||||
import getValidRoute from "./getValidRoute"
|
||||
import { Roles } from "@/constants/backend"
|
||||
|
||||
const pdf = ({ route, screens }) => {
|
||||
const validRoute = getValidRoute(screens, route, Roles.BASIC)
|
||||
|
||||
const template = new PDFScreen().role(Roles.BASIC).route(validRoute).json()
|
||||
|
||||
return [
|
||||
{
|
||||
data: template,
|
||||
navigationLinkLabel:
|
||||
validRoute === "/" ? null : capitalise(validRoute.split("/")[1]),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export default pdf
|
|
@ -0,0 +1,213 @@
|
|||
import {
|
||||
type AutomationStep,
|
||||
type AutomationStepInputs,
|
||||
type AutomationTrigger,
|
||||
type AutomationTriggerInputs,
|
||||
type BaseIOStructure,
|
||||
type UISearchFilter,
|
||||
type TestAutomationResponse,
|
||||
type AppSelfResponse,
|
||||
type Automation,
|
||||
type BlockDefinitions,
|
||||
type BlockRef,
|
||||
AutomationActionStepId,
|
||||
AutomationTriggerStepId,
|
||||
AutomationCustomIOType,
|
||||
AutomationIOType,
|
||||
} from "@budibase/types"
|
||||
import { SvelteComponent } from "svelte"
|
||||
|
||||
export enum DataMode {
|
||||
INPUT = "data_in",
|
||||
OUTPUT = "data_out",
|
||||
ERRORS = "errors",
|
||||
}
|
||||
|
||||
export enum SchemaFieldTypes {
|
||||
JSON = "json",
|
||||
ENUM = "enum",
|
||||
BOOL = "boolean",
|
||||
DATE = "date",
|
||||
FILE = "file",
|
||||
FILTER = "filter",
|
||||
CRON = "cron",
|
||||
FIELDS = "fields",
|
||||
TABLE = "table",
|
||||
COLUMN = "column",
|
||||
AUTOMATION_FIELDS = "automation_fields",
|
||||
WEBHOOK_URL = "webhook_url",
|
||||
TRIGGER_SCHEMA = "trigger_schema",
|
||||
LOOP_OPTION = "loop_option",
|
||||
CODE = "code",
|
||||
CODE_V2 = "code_v2",
|
||||
STRING = "string",
|
||||
QUERY_PARAMS = "query_params",
|
||||
QUERY_LIMIT = "query_limit",
|
||||
}
|
||||
|
||||
export type KeyValuePair = {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
// Required for filter fields. The have a definition entry
|
||||
// as well as a configuration
|
||||
export type DynamicProperties = {
|
||||
[property: `${string}-def`]: UISearchFilter
|
||||
}
|
||||
|
||||
// Form field update by property key
|
||||
export type FormUpdate = Record<string, unknown>
|
||||
|
||||
export type AutomationSchemaConfig = Record<SchemaFieldTypes, SchemaConfigProps>
|
||||
|
||||
export type FieldProps = {
|
||||
key: string
|
||||
field: BaseIOStructure
|
||||
block: AutomationStep | AutomationTrigger
|
||||
value: any
|
||||
meta: any
|
||||
title: string
|
||||
}
|
||||
|
||||
// Unused for the moment
|
||||
export type InputMeta = {
|
||||
meta?: AutomationStepInputMeta<AutomationActionStepId>
|
||||
}
|
||||
|
||||
// This is still Attachment Specific and technically reusable.
|
||||
export type FileSelectorMeta = { useAttachmentBinding: boolean }
|
||||
|
||||
export type AutomationStepInputMeta<T extends AutomationActionStepId> =
|
||||
T extends AutomationActionStepId.SEND_EMAIL_SMTP
|
||||
? { meta: FileSelectorMeta }
|
||||
: { meta?: Record<string, unknown> }
|
||||
|
||||
export type StepInputs =
|
||||
| AutomationStepInputs<AutomationActionStepId> //& InputMeta
|
||||
| AutomationTriggerInputs<AutomationTriggerStepId> //& InputMeta
|
||||
| undefined
|
||||
|
||||
export const RowTriggers = [
|
||||
AutomationTriggerStepId.ROW_UPDATED,
|
||||
AutomationTriggerStepId.ROW_SAVED,
|
||||
AutomationTriggerStepId.ROW_DELETED,
|
||||
AutomationTriggerStepId.ROW_ACTION,
|
||||
]
|
||||
|
||||
export const RowSteps = [
|
||||
AutomationActionStepId.CREATE_ROW,
|
||||
AutomationActionStepId.UPDATE_ROW,
|
||||
AutomationActionStepId.DELETE_ROW,
|
||||
]
|
||||
|
||||
export const FilterableRowTriggers = [
|
||||
AutomationTriggerStepId.ROW_UPDATED,
|
||||
AutomationTriggerStepId.ROW_SAVED,
|
||||
]
|
||||
|
||||
/**
|
||||
* Used to define how to represent automation setting
|
||||
* forms
|
||||
*/
|
||||
export interface SchemaConfigProps {
|
||||
comp: typeof SvelteComponent<any>
|
||||
onChange?: (e: CustomEvent) => void
|
||||
props?: (opts?: FieldProps) => Record<string, unknown>
|
||||
fullWidth?: boolean
|
||||
title?: string
|
||||
tooltip?: string
|
||||
wrapped?: boolean
|
||||
}
|
||||
|
||||
export interface AutomationState {
|
||||
automations: Automation[]
|
||||
testResults?: TestAutomationResponse
|
||||
showTestModal: boolean
|
||||
blockDefinitions: BlockDefinitions
|
||||
selectedAutomationId: string | null
|
||||
appSelf?: AppSelfResponse
|
||||
selectedNodeId?: string
|
||||
selectedNodeMode?: DataMode
|
||||
}
|
||||
|
||||
export interface DerivedAutomationState extends AutomationState {
|
||||
data?: Automation
|
||||
blockRefs: Record<string, BlockRef>
|
||||
}
|
||||
|
||||
/**
|
||||
* BlockProperties - Direct mapping of customType to SchemaFieldTypes
|
||||
*/
|
||||
export const customTypeToSchema: Record<string, SchemaFieldTypes> = {
|
||||
[AutomationCustomIOType.TRIGGER_SCHEMA]: SchemaFieldTypes.TRIGGER_SCHEMA,
|
||||
[AutomationCustomIOType.TABLE]: SchemaFieldTypes.TABLE,
|
||||
[AutomationCustomIOType.COLUMN]: SchemaFieldTypes.COLUMN,
|
||||
[AutomationCustomIOType.CRON]: SchemaFieldTypes.CRON,
|
||||
[AutomationCustomIOType.LOOP_OPTION]: SchemaFieldTypes.LOOP_OPTION,
|
||||
[AutomationCustomIOType.AUTOMATION_FIELDS]:
|
||||
SchemaFieldTypes.AUTOMATION_FIELDS,
|
||||
[AutomationCustomIOType.WEBHOOK_URL]: SchemaFieldTypes.WEBHOOK_URL,
|
||||
[AutomationCustomIOType.QUERY_LIMIT]: SchemaFieldTypes.QUERY_LIMIT,
|
||||
["fields"]: SchemaFieldTypes.FIELDS,
|
||||
}
|
||||
|
||||
/**
|
||||
* BlockProperties - Direct mapping of type to SchemaFieldTypes
|
||||
*/
|
||||
export const typeToSchema: Partial<Record<AutomationIOType, SchemaFieldTypes>> =
|
||||
{
|
||||
[AutomationIOType.BOOLEAN]: SchemaFieldTypes.BOOL,
|
||||
[AutomationIOType.DATE]: SchemaFieldTypes.DATE,
|
||||
[AutomationIOType.JSON]: SchemaFieldTypes.JSON,
|
||||
[AutomationIOType.ATTACHMENT]: SchemaFieldTypes.FILE,
|
||||
}
|
||||
|
||||
/**
|
||||
* FlowItem - Status type enums for grading issues
|
||||
*/
|
||||
export enum FlowStatusType {
|
||||
INFO = "info",
|
||||
WARN = "warn",
|
||||
ERROR = "error",
|
||||
SUCCESS = "success",
|
||||
}
|
||||
|
||||
/**
|
||||
* FlowItemStatus - Message structure for building information
|
||||
* pills that hover above the steps in the automation tree
|
||||
*/
|
||||
export type FlowItemStatus = {
|
||||
message: string
|
||||
icon: string
|
||||
type: FlowStatusType
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* BlockData - Status type designation for errors or issues arising from
|
||||
* test runs or other automation concerns
|
||||
*/
|
||||
export enum BlockStatusType {
|
||||
INFO = "info",
|
||||
WARN = "warn",
|
||||
ERROR = "error",
|
||||
}
|
||||
|
||||
/**
|
||||
* BlockData - Source type used to discern the source of the issues
|
||||
*/
|
||||
export enum BlockStatusSource {
|
||||
AUTOMATION_RESULTS = "results",
|
||||
VALIDATION = "validation",
|
||||
}
|
||||
|
||||
/**
|
||||
* BlockData - Message structure for building out issues to be displayed
|
||||
* in the Step Panel in automations
|
||||
*/
|
||||
export type BlockStatus = {
|
||||
message: string
|
||||
type: BlockStatusType
|
||||
source?: BlockStatusSource
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"extends": "./tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist",
|
||||
"lib": ["ESNext"],
|
||||
"baseUrl": ".",
|
||||
|
|
|
@ -8017,6 +8017,32 @@
|
|||
"key": "text",
|
||||
"wide": true
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Size",
|
||||
"key": "size",
|
||||
"defaultValue": "14px",
|
||||
"showInBar": true,
|
||||
"placeholder": "Default",
|
||||
"options": [
|
||||
{
|
||||
"label": "Small",
|
||||
"value": "12px"
|
||||
},
|
||||
{
|
||||
"label": "Medium",
|
||||
"value": "14px"
|
||||
},
|
||||
{
|
||||
"label": "Large",
|
||||
"value": "18px"
|
||||
},
|
||||
{
|
||||
"label": "Extra large",
|
||||
"value": "24px"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Alignment",
|
||||
|
@ -8058,5 +8084,133 @@
|
|||
"showInBar": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"pdf": {
|
||||
"name": "PDF Generator",
|
||||
"icon": "Document",
|
||||
"hasChildren": true,
|
||||
"showEmptyState": false,
|
||||
"illegalChildren": ["sidepanel", "modal", "gridblock"],
|
||||
"grid": {
|
||||
"hAlign": "center",
|
||||
"vAlign": "start"
|
||||
},
|
||||
"size": {
|
||||
"width": 800,
|
||||
"height": 1200
|
||||
},
|
||||
"description": "A component to render PDFs from other Budibase components",
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
"label": "PDF title",
|
||||
"key": "fileName",
|
||||
"defaultValue": "Report"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Button text",
|
||||
"key": "buttonText",
|
||||
"defaultValue": "Download PDF"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pdftable": {
|
||||
"name": "PDF Table",
|
||||
"icon": "Table",
|
||||
"styles": ["size"],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 304
|
||||
},
|
||||
"grid": {
|
||||
"hAlign": "stretch",
|
||||
"vAlign": "stretch"
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataSource",
|
||||
"label": "Data",
|
||||
"key": "datasource",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
"label": "Filtering",
|
||||
"key": "filter",
|
||||
"resetOn": "datasource",
|
||||
"dependsOn": {
|
||||
"setting": "datasource.type",
|
||||
"value": "custom",
|
||||
"invert": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "field/sortable",
|
||||
"label": "Sort column",
|
||||
"key": "sortColumn",
|
||||
"placeholder": "Default",
|
||||
"resetOn": "datasource",
|
||||
"dependsOn": {
|
||||
"setting": "datasource.type",
|
||||
"value": "custom",
|
||||
"invert": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Sort order",
|
||||
"key": "sortOrder",
|
||||
"resetOn": "datasource",
|
||||
"options": ["Ascending", "Descending"],
|
||||
"defaultValue": "Ascending",
|
||||
"dependsOn": "sortColumn"
|
||||
},
|
||||
{
|
||||
"type": "columns/toplevel",
|
||||
"label": "Columns",
|
||||
"key": "columns",
|
||||
"resetOn": "datasource",
|
||||
"placeholder": "First 3 columns"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Limit",
|
||||
"key": "limit",
|
||||
"defaultValue": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
"singlerowprovider": {
|
||||
"name": "Single Row Provider",
|
||||
"icon": "SQLQuery",
|
||||
"hasChildren": true,
|
||||
"actions": ["RefreshDatasource"],
|
||||
"size": {
|
||||
"width": 500,
|
||||
"height": 200
|
||||
},
|
||||
"grid": {
|
||||
"hAlign": "stretch",
|
||||
"vAlign": "stretch"
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "table",
|
||||
"label": "Datasource",
|
||||
"key": "datasource",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Row ID",
|
||||
"key": "rowId",
|
||||
"required": true,
|
||||
"resetOn": "datasource"
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "schema"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"apexcharts": "^3.48.0",
|
||||
"dayjs": "^1.10.8",
|
||||
"downloadjs": "1.4.7",
|
||||
"html2pdf.js": "^0.9.3",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"leaflet": "^1.7.1",
|
||||
"sanitize-html": "^2.13.0",
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<script>
|
||||
import { getContext, onDestroy } from "svelte"
|
||||
import { generate } from "shortid"
|
||||
import { builderStore } from "../stores/builder"
|
||||
import { builderStore } from "@/stores/builder"
|
||||
import Component from "@/components/Component.svelte"
|
||||
|
||||
export let type
|
||||
export let props
|
||||
export let styles
|
||||
export let context
|
||||
export let name
|
||||
export let props = undefined
|
||||
export let styles = undefined
|
||||
export let context = undefined
|
||||
export let name = undefined
|
||||
export let order = 0
|
||||
export let containsSlot = false
|
||||
|
||||
// ID is only exposed as a prop so that it can be bound to from parent
|
||||
// block components
|
||||
export let id
|
||||
export let id = undefined
|
||||
|
||||
const component = getContext("component")
|
||||
const block = getContext("block")
|
||||
|
|
|
@ -196,8 +196,6 @@
|
|||
}
|
||||
|
||||
// Metadata to pass into grid action to apply CSS
|
||||
const checkGrid = x =>
|
||||
x?._component?.endsWith("/container") && x?.layout === "grid"
|
||||
$: insideGrid = checkGrid(parent)
|
||||
$: isGrid = checkGrid(instance)
|
||||
$: gridMetadata = {
|
||||
|
@ -601,6 +599,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
const checkGrid = x => {
|
||||
// Check for a grid container
|
||||
if (x?._component?.endsWith("/container") && x?.layout === "grid") {
|
||||
return true
|
||||
}
|
||||
// Check for a PDF (always grid)
|
||||
if (x?._component?.endsWith("/pdf")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Register this component instance for external access
|
||||
if ($appStore.isDevApp) {
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
<script>
|
||||
import { themeStore } from "@/stores"
|
||||
import { setContext } from "svelte"
|
||||
import { Context } from "@budibase/bbui"
|
||||
import { Context, Helpers } from "@budibase/bbui"
|
||||
|
||||
setContext(Context.PopoverRoot, "#theme-root")
|
||||
export let popoverRoot = true
|
||||
|
||||
const id = Helpers.uuid()
|
||||
|
||||
if (popoverRoot) {
|
||||
setContext(Context.PopoverRoot, `#id`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div style={$themeStore.customThemeCss} id="theme-root">
|
||||
<div style={$themeStore.customThemeCss} {id}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from "svelte"
|
||||
import type { Row, TableDatasource, ViewDatasource } from "@budibase/types"
|
||||
|
||||
export let datasource: TableDatasource | ViewDatasource
|
||||
export let rowId: string
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable, API, Provider, ActionTypes } = getContext("sdk")
|
||||
|
||||
let row: Row | undefined
|
||||
|
||||
$: datasourceId =
|
||||
datasource.type === "table" ? datasource.tableId : datasource.id
|
||||
$: fetchRow(datasourceId, rowId)
|
||||
$: actions = [
|
||||
{
|
||||
type: ActionTypes.RefreshDatasource,
|
||||
callback: () => fetchRow(datasourceId, rowId),
|
||||
metadata: { dataSource: datasource },
|
||||
},
|
||||
]
|
||||
|
||||
const fetchRow = async (datasourceId: string, rowId: string) => {
|
||||
try {
|
||||
row = await API.fetchRow(datasourceId, rowId)
|
||||
} catch (e) {
|
||||
row = undefined
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div use:styleable={$component.styles}>
|
||||
<Provider {actions} data={row ?? null}>
|
||||
<slot />
|
||||
</Provider>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
</style>
|
|
@ -2,23 +2,29 @@
|
|||
import { getContext } from "svelte"
|
||||
import { MarkdownViewer } from "@budibase/bbui"
|
||||
|
||||
export let text: string = ""
|
||||
export let text: any = ""
|
||||
export let color: string | undefined = undefined
|
||||
export let align: "left" | "center" | "right" | "justify" = "left"
|
||||
export let size: string | undefined = "14px"
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable } = getContext("sdk")
|
||||
|
||||
// Add in certain settings to styles
|
||||
$: styles = enrichStyles($component.styles, color, align)
|
||||
$: styles = enrichStyles($component.styles, color, align, size)
|
||||
|
||||
// Ensure we're always passing in a string value to the markdown editor
|
||||
$: safeText = stringify(text)
|
||||
|
||||
const enrichStyles = (
|
||||
styles: any,
|
||||
colorStyle: typeof color,
|
||||
alignStyle: typeof align
|
||||
alignStyle: typeof align,
|
||||
size: string | undefined
|
||||
) => {
|
||||
let additions: Record<string, string> = {
|
||||
"text-align": alignStyle,
|
||||
"font-size": size || "14px",
|
||||
}
|
||||
if (colorStyle) {
|
||||
additions.color = colorStyle
|
||||
|
@ -31,10 +37,24 @@
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
const stringify = (text: any): string => {
|
||||
if (text == null) {
|
||||
return ""
|
||||
}
|
||||
if (typeof text !== "string") {
|
||||
try {
|
||||
return JSON.stringify(text)
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
</script>
|
||||
|
||||
<div use:styleable={styles}>
|
||||
<MarkdownViewer value={text} />
|
||||
<MarkdownViewer value={safeText} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -135,12 +135,18 @@
|
|||
use:styleable={$styles}
|
||||
data-cols={GridColumns}
|
||||
data-col-size={colSize}
|
||||
data-required-rows={requiredRows}
|
||||
on:click={onClick}
|
||||
>
|
||||
{#if inBuilder}
|
||||
<div class="underlay">
|
||||
{#each { length: GridColumns * rows } as _, idx}
|
||||
<div class="placeholder" class:first-col={idx % GridColumns === 0} />
|
||||
<div class="underlay-h">
|
||||
{#each { length: rows } as _}
|
||||
<div class="placeholder-h" />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="underlay-v">
|
||||
{#each { length: GridColumns } as _}
|
||||
<div class="placeholder-v" />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -151,7 +157,8 @@
|
|||
|
||||
<style>
|
||||
.grid,
|
||||
.underlay {
|
||||
.underlay-h,
|
||||
.underlay-v {
|
||||
height: var(--height) !important;
|
||||
min-height: var(--min-height) !important;
|
||||
max-height: none !important;
|
||||
|
@ -161,37 +168,45 @@
|
|||
grid-template-columns: repeat(var(--cols), calc(var(--col-size) * 1px));
|
||||
position: relative;
|
||||
}
|
||||
.underlay {
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Underlay grid lines */
|
||||
.underlay-h,
|
||||
.underlay-v {
|
||||
z-index: 0;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-top: 1px solid var(--spectrum-global-color-gray-900);
|
||||
opacity: 0.1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.underlay {
|
||||
z-index: 0;
|
||||
}
|
||||
.placeholder {
|
||||
.placeholder-h {
|
||||
border-bottom: 1px solid var(--spectrum-global-color-gray-900);
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.placeholder-h:first-child {
|
||||
border-top: 1px solid var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
.placeholder-v {
|
||||
border-right: 1px solid var(--spectrum-global-color-gray-900);
|
||||
grid-row: 1 / -1;
|
||||
}
|
||||
.placeholder.first-col {
|
||||
.placeholder-v:first-child {
|
||||
border-left: 1px solid var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Highlight grid lines when resizing children */
|
||||
:global(.grid.highlight > .underlay) {
|
||||
:global(.grid.highlight > .underlay-h),
|
||||
:global(.grid.highlight > .underlay-v) {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
/* Highlight sibling borders when resizing childern */
|
||||
/* Highlight sibling borders when resizing children */
|
||||
:global(.grid.highlight > .component:not(.dragging)) {
|
||||
outline: 2px solid var(--spectrum-global-color-static-blue-200);
|
||||
pointer-events: none !important;
|
||||
|
|
|
@ -36,10 +36,12 @@ export { default as sidepanel } from "./SidePanel.svelte"
|
|||
export { default as modal } from "./Modal.svelte"
|
||||
export { default as gridblock } from "./GridBlock.svelte"
|
||||
export { default as textv2 } from "./Text.svelte"
|
||||
export { default as singlerowprovider } from "./SingleRowProvider.svelte"
|
||||
export * from "./charts"
|
||||
export * from "./forms"
|
||||
export * from "./blocks"
|
||||
export * from "./dynamic-filter"
|
||||
export * from "./pdf"
|
||||
|
||||
// Deprecated component left for compatibility in old apps
|
||||
export * from "./deprecated/table"
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
<script lang="ts">
|
||||
import { getContext, onMount, tick } from "svelte"
|
||||
import { Heading, Button } from "@budibase/bbui"
|
||||
import { htmlToPdf, pxToPt, A4HeightPx, type PDFOptions } from "./pdf"
|
||||
import { GridRowHeight } from "@/constants"
|
||||
import CustomThemeWrapper from "@/components/CustomThemeWrapper.svelte"
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable, Block, BlockComponent } = getContext("sdk")
|
||||
|
||||
export let fileName: string | undefined
|
||||
export let buttonText: string | undefined
|
||||
|
||||
// Derive dimension calculations
|
||||
const DesiredRows = 40
|
||||
const innerPageHeightPx = GridRowHeight * DesiredRows
|
||||
const doubleMarginPx = A4HeightPx - innerPageHeightPx
|
||||
const marginPt = pxToPt(doubleMarginPx / 2)
|
||||
|
||||
let rendering = false
|
||||
let pageCount = 1
|
||||
let ref: HTMLElement
|
||||
let gridRef: HTMLElement
|
||||
|
||||
$: safeName = fileName || "Report"
|
||||
$: safeButtonText = buttonText || "Download PDF"
|
||||
$: heightPx = pageCount * innerPageHeightPx + doubleMarginPx
|
||||
$: pageStyle = `--height:${heightPx}px; --margin:${marginPt}pt;`
|
||||
$: gridMinHeight = pageCount * DesiredRows * GridRowHeight
|
||||
|
||||
const generatePDF = async () => {
|
||||
rendering = true
|
||||
await tick()
|
||||
preprocessCSS()
|
||||
try {
|
||||
const opts: PDFOptions = {
|
||||
fileName: safeName,
|
||||
marginPt,
|
||||
footer: true,
|
||||
}
|
||||
await htmlToPdf(ref, opts)
|
||||
} catch (error) {
|
||||
console.error("Error rendering PDF", error)
|
||||
}
|
||||
rendering = false
|
||||
}
|
||||
|
||||
const preprocessCSS = () => {
|
||||
const els = document.getElementsByClassName("grid-child")
|
||||
for (let el of els) {
|
||||
if (!(el instanceof HTMLElement)) {
|
||||
return
|
||||
}
|
||||
// Get the computed values and assign them back to the style, simplifying
|
||||
// the CSS that gets handled by HTML2PDF
|
||||
const styles = window.getComputedStyle(el)
|
||||
el.style.setProperty("grid-column-end", styles.gridColumnEnd, "important")
|
||||
}
|
||||
}
|
||||
|
||||
const getDividerStyle = (idx: number) => {
|
||||
const top = (idx + 1) * innerPageHeightPx + doubleMarginPx / 2
|
||||
return `--idx:"${idx + 1}"; --top:${top}px;`
|
||||
}
|
||||
|
||||
const handleGridMutation = () => {
|
||||
const rows = parseInt(gridRef.dataset.requiredRows || "1")
|
||||
const nextPageCount = Math.max(1, Math.ceil(rows / DesiredRows))
|
||||
if (nextPageCount > pageCount || !gridRef.classList.contains("highlight")) {
|
||||
pageCount = nextPageCount
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Observe required content rows and use this to determine required pages
|
||||
const gridDOMID = `${$component.id}-grid-dom`
|
||||
gridRef = document.getElementsByClassName(gridDOMID)[0] as HTMLElement
|
||||
const mutationObserver = new MutationObserver(handleGridMutation)
|
||||
mutationObserver.observe(gridRef, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-required-rows", "class"],
|
||||
})
|
||||
return () => {
|
||||
mutationObserver.disconnect()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Block>
|
||||
<div class="wrapper" style="--margin:{marginPt}pt;">
|
||||
<div class="container" use:styleable={$component.styles}>
|
||||
<div class="title">
|
||||
<Heading size="M">{safeName}</Heading>
|
||||
<Button disabled={rendering} cta on:click={generatePDF}>
|
||||
{safeButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="page" style={pageStyle}>
|
||||
{#if pageCount > 1}
|
||||
{#each { length: pageCount } as _, idx}
|
||||
<div
|
||||
class="divider"
|
||||
class:last={idx === pageCount - 1}
|
||||
style={getDividerStyle(idx)}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
<div
|
||||
class="spectrum spectrum--medium spectrum--light pageContent"
|
||||
bind:this={ref}
|
||||
>
|
||||
<CustomThemeWrapper popoverRoot={false}>
|
||||
<BlockComponent
|
||||
type="container"
|
||||
props={{ layout: "grid" }}
|
||||
styles={{
|
||||
normal: {
|
||||
height: `${gridMinHeight}px`,
|
||||
},
|
||||
}}
|
||||
context="grid"
|
||||
>
|
||||
<slot />
|
||||
</BlockComponent>
|
||||
</CustomThemeWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 64px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
width: 595.28pt;
|
||||
gap: var(--spacing-xl);
|
||||
align-self: center;
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.page {
|
||||
width: 595.28pt;
|
||||
min-height: var(--height);
|
||||
padding: var(--margin);
|
||||
background-color: white;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.1);
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
.pageContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
background: white;
|
||||
}
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--spectrum-global-color-static-gray-400);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: var(--top);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.divider.last {
|
||||
top: calc(var(--top) + var(--margin));
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,143 @@
|
|||
<script lang="ts">
|
||||
import type {
|
||||
DataFetchDatasource,
|
||||
FieldSchema,
|
||||
GroupUserDatasource,
|
||||
SortOrder,
|
||||
TableSchema,
|
||||
UISearchFilter,
|
||||
UserDatasource,
|
||||
} from "@budibase/types"
|
||||
import { fetchData, QueryUtils, stringifyRow } from "@budibase/frontend-core"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
type ProviderDatasource = Exclude<
|
||||
DataFetchDatasource,
|
||||
UserDatasource | GroupUserDatasource
|
||||
>
|
||||
type ChosenColumns = Array<{ name: string; displayName?: string }> | undefined
|
||||
type Schema = { [key: string]: FieldSchema & { displayName: string } }
|
||||
|
||||
export let datasource: ProviderDatasource
|
||||
export let filter: UISearchFilter | undefined = undefined
|
||||
export let sortColumn: string | undefined = undefined
|
||||
export let sortOrder: SortOrder | undefined = undefined
|
||||
export let columns: ChosenColumns = undefined
|
||||
export let limit: number = 20
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable, API } = getContext("sdk")
|
||||
|
||||
$: query = QueryUtils.buildQuery(filter)
|
||||
$: fetch = createFetch(datasource)
|
||||
$: fetch.update({
|
||||
query,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
limit,
|
||||
})
|
||||
$: schema = sanitizeSchema($fetch.schema, columns)
|
||||
$: columnCount = Object.keys(schema).length
|
||||
$: rowCount = $fetch.rows?.length || 0
|
||||
$: stringifiedRows = ($fetch?.rows || []).map(row =>
|
||||
stringifyRow(row, schema)
|
||||
)
|
||||
|
||||
const createFetch = (datasource: ProviderDatasource) => {
|
||||
return fetchData({
|
||||
API,
|
||||
datasource,
|
||||
options: {
|
||||
query,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
limit,
|
||||
paginate: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const sanitizeSchema = (
|
||||
schema: TableSchema | null,
|
||||
columns: ChosenColumns
|
||||
): Schema => {
|
||||
if (!schema) {
|
||||
return {}
|
||||
}
|
||||
let sanitized: Schema = {}
|
||||
|
||||
// Clean out hidden fields and ensure we have
|
||||
Object.entries(schema).forEach(([field, fieldSchema]) => {
|
||||
if (fieldSchema.visible !== false) {
|
||||
sanitized[field] = {
|
||||
...fieldSchema,
|
||||
displayName: field,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Clean out unselected columns.
|
||||
// Default to first 3 columns if none specified, as we are width contrained.
|
||||
if (!columns?.length) {
|
||||
columns = Object.values(sanitized).slice(0, 3)
|
||||
}
|
||||
let pruned: Schema = {}
|
||||
for (let col of columns) {
|
||||
if (sanitized[col.name]) {
|
||||
pruned[col.name] = {
|
||||
...sanitized[col.name],
|
||||
displayName: col.displayName || sanitized[col.name].displayName,
|
||||
}
|
||||
}
|
||||
}
|
||||
sanitized = pruned
|
||||
|
||||
return sanitized
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="vars" style="--cols:{columnCount}; --rows:{rowCount};">
|
||||
<div class="table" class:valid={!!schema} use:styleable={$component.styles}>
|
||||
{#if schema}
|
||||
{#each Object.keys(schema) as col}
|
||||
<div class="cell header">{schema[col].displayName}</div>
|
||||
{/each}
|
||||
{#each stringifiedRows as row}
|
||||
{#each Object.keys(schema) as col}
|
||||
<div class="cell">{row[col]}</div>
|
||||
{/each}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.vars {
|
||||
display: contents;
|
||||
--border-color: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.table {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--cols), minmax(40px, auto));
|
||||
grid-template-rows: repeat(var(--rows), max-content);
|
||||
overflow: hidden;
|
||||
background: var(--spectrum-global-color-gray-50);
|
||||
}
|
||||
.table.valid {
|
||||
border-left: 1px solid var(--border-color);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
.cell {
|
||||
border-right: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: var(--spacing-xs) var(--spacing-s);
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
.cell.header {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,2 @@
|
|||
export { default as pdf } from "./PDF.svelte"
|
||||
export { default as pdftable } from "./PDFTable.svelte"
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue