Merge branch 'master' into grid-new-component-dnd
This commit is contained in:
commit
7faec8f849
25
README.md
25
README.md
|
@ -54,17 +54,21 @@
|
|||
</h3>
|
||||
|
||||
<br /><br />
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### Build and ship real software
|
||||
### Build and ship real software
|
||||
|
||||
Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing users with a great experience.
|
||||
<br /><br />
|
||||
|
||||
### Open source and extensible
|
||||
|
||||
Budibase is open-source - licensed as GPL v3. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience.
|
||||
<br /><br />
|
||||
|
||||
### Load data or start from scratch
|
||||
|
||||
Budibase pulls data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MariaDB, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
|
@ -82,10 +86,12 @@ Budibase comes out of the box with beautifully designed, powerful components whi
|
|||
<br /><br />
|
||||
|
||||
### Automate processes, integrate with other tools and connect to webhooks
|
||||
|
||||
Save time by automating manual processes and workflows. From connecting to webhooks to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [Request new automation](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
<br /><br />
|
||||
|
||||
### Integrate with your favorite tools
|
||||
|
||||
Budibase integrates with a number of popular tools allowing you to build apps that perfectly fit your stack.
|
||||
|
||||
<p align="center">
|
||||
|
@ -94,6 +100,7 @@ Budibase integrates with a number of popular tools allowing you to build apps th
|
|||
<br /><br />
|
||||
|
||||
### Deploy with confidence and security
|
||||
|
||||
Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user management to the group manager.
|
||||
|
||||
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
|
||||
|
@ -104,15 +111,15 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
|
|||
|
||||
<br />
|
||||
|
||||
|
||||
## Budibase Public API
|
||||
|
||||
As with anything that we build in Budibase, our new public API is simple to use, flexible, and introduces new extensibility. To summarize, the Budibase API enables:
|
||||
|
||||
- Budibase as a backend
|
||||
- Interoperability
|
||||
|
||||
|
||||
#### Docs
|
||||
|
||||
You can learn more about the Budibase API at the following places:
|
||||
|
||||
- [General documentation](https://docs.budibase.com/docs/public-api): Learn how to get your API key, how to use spec, and how to use Postman
|
||||
|
@ -132,10 +139,8 @@ Deploy Budibase using Docker, Kubernetes, and Digital Ocean on your existing inf
|
|||
- [Digital Ocean](https://docs.budibase.com/docs/digitalocean)
|
||||
- [Portainer](https://docs.budibase.com/docs/portainer)
|
||||
|
||||
|
||||
### [Get started with Budibase Cloud](https://budibase.com)
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 🎓 Learning Budibase
|
||||
|
@ -143,7 +148,6 @@ Deploy Budibase using Docker, Kubernetes, and Digital Ocean on your existing inf
|
|||
The Budibase documentation [lives here](https://docs.budibase.com/docs).
|
||||
<br />
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 💬 Community
|
||||
|
@ -152,25 +156,24 @@ If you have a question or would like to talk with other Budibase users and join
|
|||
|
||||
<br /><br /><br />
|
||||
|
||||
|
||||
## ❗ Code of conduct
|
||||
|
||||
Budibase is dedicated to providing everyone a welcoming, diverse, and harassment-free experience. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/docs/CODE_OF_CONDUCT.md). Please read it.
|
||||
<br />
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## 🙌 Contributing to Budibase
|
||||
|
||||
From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API, please create an issue first. This way, we can ensure your work is not in vain.
|
||||
Environment setup instructions are available [here](https://github.com/Budibase/budibase/tree/HEAD/docs/CONTRIBUTING.md).
|
||||
|
||||
### Not Sure Where to Start?
|
||||
A good place to start contributing is the [First time issues project](https://github.com/Budibase/budibase/projects/22).
|
||||
|
||||
A good place to start contributing is by looking for the [good first issue](https://github.com/Budibase/budibase/labels/good%20first%20issue) tag.
|
||||
|
||||
### How the repository is organized
|
||||
|
||||
Budibase is a monorepo managed by lerna. Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up Budibase.
|
||||
|
||||
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contains code for the budibase builder client-side svelte application.
|
||||
|
@ -183,7 +186,6 @@ For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase
|
|||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## 📝 License
|
||||
|
||||
Budibase is open-source, licensed as [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). The client and component libraries are licensed as [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - so the apps you build can be licensed however you like.
|
||||
|
@ -202,7 +204,6 @@ If you are having issues between updates of the builder, please use the guide [h
|
|||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
|
||||
<a href="https://github.com/Budibase/budibase/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=Budibase/budibase" />
|
||||
</a>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "3.4.3",
|
||||
"version": "3.4.4",
|
||||
"npmClient": "yarn",
|
||||
"concurrency": 20,
|
||||
"command": {
|
||||
|
|
|
@ -83,11 +83,15 @@ export function isViewId(id: string): boolean {
|
|||
/**
|
||||
* Check if a given ID is that of a datasource or datasource plus.
|
||||
*/
|
||||
export const isDatasourceId = (id: string): boolean => {
|
||||
export function isDatasourceId(id: string): boolean {
|
||||
// this covers both datasources and datasource plus
|
||||
return !!id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`)
|
||||
}
|
||||
|
||||
export function isQueryId(id: string): boolean {
|
||||
return !!id && id.startsWith(`${DocumentType.QUERY}${SEPARATOR}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets parameters for retrieving workspaces.
|
||||
*/
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import "@spectrum-css/actionbutton/dist/index-vars.css"
|
||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||
import { fade } from "svelte/transition"
|
||||
import { hexToRGBA } from "../helpers"
|
||||
|
||||
export let quiet = false
|
||||
export let selected = false
|
||||
export let disabled = false
|
||||
export let icon = ""
|
||||
export let size = "M"
|
||||
export let active = false
|
||||
export let fullWidth = false
|
||||
export let noPadding = false
|
||||
export let tooltip = ""
|
||||
export let accentColor = null
|
||||
export let quiet: boolean = false
|
||||
export let selected: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let icon: string = ""
|
||||
export let size: "S" | "M" | "L" = "M"
|
||||
export let active: boolean = false
|
||||
export let fullWidth: boolean = false
|
||||
export let noPadding: boolean = false
|
||||
export let tooltip: string = ""
|
||||
export let accentColor: string | null = null
|
||||
|
||||
let showTooltip = false
|
||||
|
||||
$: accentStyle = getAccentStyle(accentColor)
|
||||
|
||||
const getAccentStyle = color => {
|
||||
const getAccentStyle = (color: string | null) => {
|
||||
if (!color) {
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<script lang="ts">
|
||||
import "@spectrum-css/textfield/dist/index-vars.css"
|
||||
import { createEventDispatcher, onMount, tick } from "svelte"
|
||||
import type { UIEvent } from "@budibase/types"
|
||||
|
||||
export let value = null
|
||||
export let value: string | null = null
|
||||
export let placeholder: string | undefined = undefined
|
||||
export let type = "text"
|
||||
export let disabled = false
|
||||
|
@ -11,7 +12,7 @@
|
|||
export let updateOnChange = true
|
||||
export let quiet = false
|
||||
export let align: "left" | "right" | "center" | undefined = undefined
|
||||
export let autofocus = false
|
||||
export let autofocus: boolean | null = false
|
||||
export let autocomplete: boolean | undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -24,7 +25,7 @@
|
|||
return
|
||||
}
|
||||
if (type === "number") {
|
||||
const float = parseFloat(newValue)
|
||||
const float = parseFloat(newValue as string)
|
||||
newValue = isNaN(float) ? null : float
|
||||
}
|
||||
dispatch("change", newValue)
|
||||
|
@ -37,31 +38,31 @@
|
|||
focus = true
|
||||
}
|
||||
|
||||
const onBlur = (event: any) => {
|
||||
const onBlur = (event: UIEvent) => {
|
||||
if (readonly || disabled) {
|
||||
return
|
||||
}
|
||||
focus = false
|
||||
updateValue(event.target.value)
|
||||
updateValue(event?.target?.value)
|
||||
}
|
||||
|
||||
const onInput = (event: any) => {
|
||||
const onInput = (event: UIEvent) => {
|
||||
if (readonly || !updateOnChange || disabled) {
|
||||
return
|
||||
}
|
||||
updateValue(event.target.value)
|
||||
updateValue(event.target?.value)
|
||||
}
|
||||
|
||||
const updateValueOnEnter = (event: any) => {
|
||||
const updateValueOnEnter = (event: UIEvent) => {
|
||||
if (readonly || disabled) {
|
||||
return
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
updateValue(event.target.value)
|
||||
updateValue(event.target?.value)
|
||||
}
|
||||
}
|
||||
|
||||
const getInputMode = (type: any) => {
|
||||
const getInputMode = (type: string) => {
|
||||
if (type === "bigint") {
|
||||
return "numeric"
|
||||
}
|
||||
|
@ -77,7 +78,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
if (disabled) return
|
||||
focus = autofocus
|
||||
focus = autofocus || false
|
||||
if (focus) {
|
||||
await tick()
|
||||
field.focus()
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import StatusLight from "../StatusLight/StatusLight.svelte"
|
||||
|
||||
export let icon = null
|
||||
export let iconColor = null
|
||||
export let title = null
|
||||
export let subtitle = null
|
||||
export let url = null
|
||||
export let hoverable = false
|
||||
export let showArrow = false
|
||||
export let selected = false
|
||||
export let icon: string | undefined = undefined
|
||||
export let iconColor: string | undefined = undefined
|
||||
export let title: string | undefined = undefined
|
||||
export let subtitle: string | undefined = undefined
|
||||
export let url: string | undefined = undefined
|
||||
export let hoverable: boolean = false
|
||||
export let showArrow: boolean = false
|
||||
export let selected: boolean = false
|
||||
</script>
|
||||
|
||||
<a
|
||||
|
|
|
@ -1,20 +1,29 @@
|
|||
<script>
|
||||
import { ActionButton, List, ListItem, Button } from "@budibase/bbui"
|
||||
import DetailPopover from "@/components/common/DetailPopover.svelte"
|
||||
import { screenStore, appStore } from "@/stores/builder"
|
||||
<script lang="ts">
|
||||
import { Button } from "@budibase/bbui"
|
||||
import ScreensPopover from "@/components/common/ScreensPopover.svelte"
|
||||
import { screenStore } from "@/stores/builder"
|
||||
import { getContext, createEventDispatcher } from "svelte"
|
||||
|
||||
const { datasource } = getContext("grid")
|
||||
import type { Screen, ScreenUsage } from "@budibase/types"
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let popover
|
||||
const { datasource }: { datasource: any } = getContext("grid")
|
||||
|
||||
let popover: any
|
||||
|
||||
$: ds = $datasource
|
||||
$: resourceId = ds?.type === "table" ? ds.tableId : ds?.id
|
||||
$: connectedScreens = findConnectedScreens($screenStore.screens, resourceId)
|
||||
$: screenCount = connectedScreens.length
|
||||
$: screenUsage = connectedScreens.map(
|
||||
(screen: Screen): ScreenUsage => ({
|
||||
url: screen.routing?.route,
|
||||
_id: screen._id!,
|
||||
})
|
||||
)
|
||||
|
||||
const findConnectedScreens = (screens, resourceId) => {
|
||||
const findConnectedScreens = (
|
||||
screens: Screen[],
|
||||
resourceId: string
|
||||
): Screen[] => {
|
||||
return screens.filter(screen => {
|
||||
return JSON.stringify(screen).includes(`"${resourceId}"`)
|
||||
})
|
||||
|
@ -26,34 +35,16 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<DetailPopover title="Screens" bind:this={popover}>
|
||||
<svelte:fragment slot="anchor" let:open>
|
||||
<ActionButton
|
||||
icon="WebPage"
|
||||
selected={open || screenCount}
|
||||
quiet
|
||||
accentColor="#364800"
|
||||
>
|
||||
Screens{screenCount ? `: ${screenCount}` : ""}
|
||||
</ActionButton>
|
||||
</svelte:fragment>
|
||||
{#if !connectedScreens.length}
|
||||
There aren't any screens connected to this data.
|
||||
{:else}
|
||||
The following screens are connected to this data.
|
||||
<List>
|
||||
{#each connectedScreens as screen}
|
||||
<ListItem
|
||||
title={screen.routing.route}
|
||||
url={`/builder/app/${$appStore.appId}/design/${screen._id}`}
|
||||
showArrow
|
||||
/>
|
||||
{/each}
|
||||
</List>
|
||||
{/if}
|
||||
<div>
|
||||
<ScreensPopover
|
||||
bind:this={popover}
|
||||
screens={screenUsage}
|
||||
icon="WebPage"
|
||||
accentColor="#364800"
|
||||
showCount
|
||||
>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary icon="WebPage" on:click={generateScreen}>
|
||||
Generate app screen
|
||||
</Button>
|
||||
</div>
|
||||
</DetailPopover>
|
||||
</svelte:fragment>
|
||||
</ScreensPopover>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import IntegrationIcon from "@/components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import UpdateDatasourceModal from "@/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte"
|
||||
import DeleteConfirmationModal from "./DeleteConfirmationModal.svelte"
|
||||
import DeleteDataConfirmModal from "@/components/backend/modals/DeleteDataConfirmationModal.svelte"
|
||||
|
||||
export let datasource
|
||||
|
||||
|
@ -71,7 +71,10 @@
|
|||
{/if}
|
||||
</NavItem>
|
||||
<UpdateDatasourceModal {datasource} bind:this={editModal} />
|
||||
<DeleteConfirmationModal {datasource} bind:this={deleteConfirmationModal} />
|
||||
<DeleteDataConfirmModal
|
||||
source={datasource}
|
||||
bind:this={deleteConfirmationModal}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.datasource-icon {
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { datasources } from "@/stores/builder"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||
|
||||
export let datasource
|
||||
|
||||
let confirmDeleteDialog
|
||||
|
||||
export const show = () => {
|
||||
confirmDeleteDialog.show()
|
||||
}
|
||||
|
||||
async function deleteDatasource() {
|
||||
try {
|
||||
const isSelected = datasource.selected || datasource.containsSelected
|
||||
await datasources.delete(datasource)
|
||||
notifications.success("Datasource deleted")
|
||||
if (isSelected) {
|
||||
$goto("./datasource")
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting datasource")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
okText="Delete Datasource"
|
||||
onOk={deleteDatasource}
|
||||
title="Confirm Deletion"
|
||||
>
|
||||
Are you sure you wish to delete the datasource
|
||||
<i>{datasource.name}</i>? This action cannot be undone.
|
||||
</ConfirmDialog>
|
|
@ -6,19 +6,18 @@
|
|||
} from "@/helpers/data/utils"
|
||||
import { goto as gotoStore, isActive } from "@roxi/routify"
|
||||
import {
|
||||
datasources,
|
||||
queries,
|
||||
userSelectedResourceMap,
|
||||
contextMenuStore,
|
||||
} from "@/stores/builder"
|
||||
import NavItem from "@/components/common/NavItem.svelte"
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||
import DeleteDataConfirmModal from "@/components/backend/modals/DeleteDataConfirmationModal.svelte"
|
||||
import { notifications, Icon } from "@budibase/bbui"
|
||||
|
||||
export let datasource
|
||||
export let query
|
||||
|
||||
let confirmDeleteDialog
|
||||
let confirmDeleteModal
|
||||
|
||||
// goto won't work in the context menu callback if the store is called directly
|
||||
$: goto = $gotoStore
|
||||
|
@ -31,7 +30,7 @@
|
|||
keyBind: null,
|
||||
visible: true,
|
||||
disabled: false,
|
||||
callback: confirmDeleteDialog.show,
|
||||
callback: confirmDeleteModal.show,
|
||||
},
|
||||
{
|
||||
icon: "Duplicate",
|
||||
|
@ -51,20 +50,6 @@
|
|||
]
|
||||
}
|
||||
|
||||
async function deleteQuery() {
|
||||
try {
|
||||
// Go back to the datasource if we are deleting the active query
|
||||
if ($queries.selectedQueryId === query._id) {
|
||||
goto(`./datasource/${query.datasourceId}`)
|
||||
}
|
||||
await queries.delete(query)
|
||||
await datasources.fetch()
|
||||
notifications.success("Query deleted")
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting query")
|
||||
}
|
||||
}
|
||||
|
||||
const openContextMenu = e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
@ -90,14 +75,7 @@
|
|||
<Icon size="S" hoverable name="MoreSmallList" on:click={openContextMenu} />
|
||||
</NavItem>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
okText="Delete Query"
|
||||
onOk={deleteQuery}
|
||||
title="Confirm Deletion"
|
||||
>
|
||||
Are you sure you wish to delete this query? This action cannot be undone.
|
||||
</ConfirmDialog>
|
||||
<DeleteDataConfirmModal source={query} bind:this={confirmDeleteModal} />
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
<script>
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { appStore, tables, datasources, screenStore } from "@/stores/builder"
|
||||
import { InlineAlert, Link, Input, notifications } from "@budibase/bbui"
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
||||
|
||||
export let table
|
||||
|
||||
let confirmDeleteDialog
|
||||
|
||||
let screensPossiblyAffected = []
|
||||
let viewsMessage = ""
|
||||
let deleteTableName
|
||||
|
||||
const getViewsMessage = () => {
|
||||
const views = Object.values(table?.views ?? [])
|
||||
if (views.length < 1) {
|
||||
return ""
|
||||
}
|
||||
if (views.length === 1) {
|
||||
return ", including 1 view"
|
||||
}
|
||||
|
||||
return `, including ${views.length} views`
|
||||
}
|
||||
|
||||
export const show = () => {
|
||||
viewsMessage = getViewsMessage()
|
||||
screensPossiblyAffected = $screenStore.screens
|
||||
.filter(
|
||||
screen => screen.autoTableId === table._id && screen.routing?.route
|
||||
)
|
||||
.map(screen => ({
|
||||
text: screen.routing.route,
|
||||
url: `/builder/app/${$appStore.appId}/design/${screen._id}`,
|
||||
}))
|
||||
|
||||
confirmDeleteDialog.show()
|
||||
}
|
||||
|
||||
async function deleteTable() {
|
||||
const isSelected = $params.tableId === table._id
|
||||
try {
|
||||
await tables.delete(table)
|
||||
|
||||
if (table.sourceType === DB_TYPE_EXTERNAL) {
|
||||
await datasources.fetch()
|
||||
}
|
||||
notifications.success("Table deleted")
|
||||
if (isSelected) {
|
||||
$goto(`./datasource/${table.datasourceId}`)
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error(`Error deleting table - ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
function hideDeleteDialog() {
|
||||
deleteTableName = ""
|
||||
}
|
||||
|
||||
const autofillTableName = () => {
|
||||
deleteTableName = table.name
|
||||
}
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
okText="Delete Table"
|
||||
onOk={deleteTable}
|
||||
onCancel={hideDeleteDialog}
|
||||
title="Confirm Deletion"
|
||||
disabled={deleteTableName !== table.name}
|
||||
>
|
||||
<div class="content">
|
||||
<p class="firstWarning">
|
||||
Are you sure you wish to delete the table
|
||||
<span class="tableNameLine">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<b on:click={autofillTableName} class="tableName">{table.name}</b>
|
||||
<span>?</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class="secondWarning">All table data will be deleted{viewsMessage}.</p>
|
||||
<p class="thirdWarning">This action <b>cannot be undone</b>.</p>
|
||||
|
||||
{#if screensPossiblyAffected.length > 0}
|
||||
<div class="affectedScreens">
|
||||
<InlineAlert
|
||||
header="The following screens were originally generated from this table and may no longer function as expected"
|
||||
>
|
||||
<ul class="affectedScreensList">
|
||||
{#each screensPossiblyAffected as item}
|
||||
<li>
|
||||
<Link quiet overBackground target="_blank" href={item.url}
|
||||
>{item.text}</Link
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</InlineAlert>
|
||||
</div>
|
||||
{/if}
|
||||
<p class="fourthWarning">Please enter the table name below to confirm.</p>
|
||||
<Input bind:value={deleteTableName} placeholder={table.name} />
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
margin-top: 0;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.firstWarning {
|
||||
margin: 0 0 12px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.tableNameLine {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.tableName {
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.secondWarning {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.thirdWarning {
|
||||
margin: 0 0 12px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.affectedScreens {
|
||||
margin: 18px 0;
|
||||
max-width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.affectedScreens :global(.spectrum-InLineAlert) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.affectedScreensList {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.affectedScreensList li {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.fourthWarning {
|
||||
margin: 12px 0 6px;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -8,7 +8,7 @@
|
|||
import NavItem from "@/components/common/NavItem.svelte"
|
||||
import { isActive } from "@roxi/routify"
|
||||
import EditModal from "./EditModal.svelte"
|
||||
import DeleteConfirmationModal from "./DeleteConfirmationModal.svelte"
|
||||
import DeleteConfirmationModal from "../../modals/DeleteDataConfirmationModal.svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
||||
|
||||
|
@ -65,4 +65,4 @@
|
|||
{/if}
|
||||
</NavItem>
|
||||
<EditModal {table} bind:this={editModal} />
|
||||
<DeleteConfirmationModal {table} bind:this={deleteConfirmationModal} />
|
||||
<DeleteConfirmationModal source={table} bind:this={deleteConfirmationModal} />
|
||||
|
|
|
@ -0,0 +1,226 @@
|
|||
<script lang="ts">
|
||||
import { Link, notifications } from "@budibase/bbui"
|
||||
import {
|
||||
appStore,
|
||||
datasources,
|
||||
queries,
|
||||
screenStore,
|
||||
tables,
|
||||
views,
|
||||
viewsV2,
|
||||
} from "@/stores/builder"
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||
import { helpers, utils } from "@budibase/shared-core"
|
||||
import { SourceType } from "@budibase/types"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
||||
import { get } from "svelte/store"
|
||||
import type { Table, ViewV2, View, Datasource, Query } from "@budibase/types"
|
||||
|
||||
export let source: Table | ViewV2 | Datasource | Query | undefined
|
||||
|
||||
let confirmDeleteDialog: any
|
||||
let affectedScreens: { text: string; url: string }[] = []
|
||||
let sourceType: SourceType | undefined = undefined
|
||||
|
||||
const getDatasourceQueries = () => {
|
||||
if (sourceType !== SourceType.DATASOURCE) {
|
||||
return ""
|
||||
}
|
||||
const sourceId = getSourceID()
|
||||
const queryList = get(queries).list.filter(
|
||||
query => query.datasourceId === sourceId
|
||||
)
|
||||
return queryList
|
||||
}
|
||||
|
||||
function getSourceID(): string {
|
||||
if (!source) {
|
||||
throw new Error("No data source provided.")
|
||||
}
|
||||
if ("id" in source) {
|
||||
return source.id
|
||||
}
|
||||
return source._id!
|
||||
}
|
||||
|
||||
export const show = async () => {
|
||||
const usage = await screenStore.usageInScreens(getSourceID())
|
||||
affectedScreens = processScreens(usage.screens)
|
||||
sourceType = usage.sourceType
|
||||
confirmDeleteDialog.show()
|
||||
}
|
||||
|
||||
function processScreens(
|
||||
screens: { url: string; _id: string }[]
|
||||
): { text: string; url: string }[] {
|
||||
return screens.map(({ url, _id }) => ({
|
||||
text: url,
|
||||
url: `/builder/app/${$appStore.appId}/design/${_id}`,
|
||||
}))
|
||||
}
|
||||
|
||||
function hideDeleteDialog() {
|
||||
sourceType = undefined
|
||||
}
|
||||
|
||||
async function deleteTable(table: Table & { datasourceId?: string }) {
|
||||
const isSelected = $params.tableId === table._id
|
||||
try {
|
||||
await tables.delete({
|
||||
_id: table._id!,
|
||||
_rev: table._rev!,
|
||||
})
|
||||
|
||||
if (table.sourceType === DB_TYPE_EXTERNAL) {
|
||||
await datasources.fetch()
|
||||
}
|
||||
notifications.success("Table deleted")
|
||||
if (isSelected) {
|
||||
$goto(`./datasource/${table.datasourceId}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
notifications.error(`Error deleting table - ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteView(view: ViewV2 | View) {
|
||||
try {
|
||||
if (helpers.views.isV2(view)) {
|
||||
await viewsV2.delete(view as ViewV2)
|
||||
} else {
|
||||
await views.delete(view as View)
|
||||
}
|
||||
notifications.success("View deleted")
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting view")
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteDatasource(datasource: Datasource) {
|
||||
try {
|
||||
await datasources.delete(datasource)
|
||||
notifications.success("Datasource deleted")
|
||||
const isSelected =
|
||||
get(datasources).selectedDatasourceId === datasource._id
|
||||
if (isSelected) {
|
||||
$goto("./datasource")
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting datasource")
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteQuery(query: Query) {
|
||||
try {
|
||||
// Go back to the datasource if we are deleting the active query
|
||||
if ($queries.selectedQueryId === query._id) {
|
||||
$goto(`./datasource/${query.datasourceId}`)
|
||||
}
|
||||
await queries.delete(query)
|
||||
await datasources.fetch()
|
||||
notifications.success("Query deleted")
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting query")
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSource() {
|
||||
if (!source || !sourceType) {
|
||||
throw new Error("Unable to delete - no data source found.")
|
||||
}
|
||||
|
||||
switch (sourceType) {
|
||||
case SourceType.TABLE:
|
||||
return await deleteTable(source as Table)
|
||||
case SourceType.VIEW:
|
||||
return await deleteView(source as ViewV2)
|
||||
case SourceType.QUERY:
|
||||
return await deleteQuery(source as Query)
|
||||
case SourceType.DATASOURCE:
|
||||
return await deleteDatasource(source as Datasource)
|
||||
default:
|
||||
utils.unreachable(sourceType)
|
||||
}
|
||||
}
|
||||
|
||||
function buildMessage(sourceType: string) {
|
||||
if (!source) {
|
||||
return ""
|
||||
}
|
||||
const screenCount = affectedScreens.length
|
||||
let message = `Removing ${source?.name} `
|
||||
let initialLength = message.length
|
||||
if (sourceType === SourceType.TABLE) {
|
||||
const views = "views" in source ? Object.values(source?.views ?? []) : []
|
||||
message += `will delete its data${
|
||||
views.length
|
||||
? `${screenCount ? "," : " and"} views (${views.length})`
|
||||
: ""
|
||||
}`
|
||||
} else if (sourceType === SourceType.DATASOURCE) {
|
||||
const queryList = getDatasourceQueries()
|
||||
if (queryList.length) {
|
||||
message += `will delete its queries (${queryList.length})`
|
||||
}
|
||||
}
|
||||
if (screenCount) {
|
||||
message +=
|
||||
initialLength !== message.length
|
||||
? ", and break connected screens:"
|
||||
: "will break connected screens:"
|
||||
} else {
|
||||
message += "."
|
||||
}
|
||||
return message.length !== initialLength ? message : ""
|
||||
}
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
okText="Delete"
|
||||
onOk={deleteSource}
|
||||
onCancel={hideDeleteDialog}
|
||||
title={`Are you sure you want to delete this ${sourceType}?`}
|
||||
>
|
||||
<div class="content">
|
||||
{#if sourceType}
|
||||
<p class="warning">
|
||||
{buildMessage(sourceType)}
|
||||
{#if affectedScreens.length > 0}
|
||||
<span class="screens">
|
||||
{#each affectedScreens as item, idx}
|
||||
<Link overBackground target="_blank" href={item.url}
|
||||
>{item.text}{idx !== affectedScreens.length - 1
|
||||
? ","
|
||||
: ""}</Link
|
||||
>
|
||||
{/each}
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
<p class="warning">
|
||||
<b>This action cannot be undone.</b>
|
||||
</p>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
margin-top: 0;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.screens {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: var(--spacing-l);
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
</style>
|
|
@ -8,7 +8,7 @@
|
|||
export let onOk = undefined
|
||||
export let onCancel = undefined
|
||||
export let warning = true
|
||||
export let disabled
|
||||
export let disabled = false
|
||||
|
||||
let modal
|
||||
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ActionButton,
|
||||
PopoverAlignment,
|
||||
} from "@budibase/bbui"
|
||||
import DetailPopover from "@/components/common/DetailPopover.svelte"
|
||||
import { appStore } from "@/stores/builder"
|
||||
import type { ScreenUsage } from "@budibase/types"
|
||||
|
||||
export let screens: ScreenUsage[] = []
|
||||
export let icon = "DeviceDesktop"
|
||||
export let accentColor: string | null | undefined = null
|
||||
export let showCount = false
|
||||
export let align = PopoverAlignment.Left
|
||||
|
||||
let popover: any
|
||||
|
||||
export function show() {
|
||||
popover?.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
popover?.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<DetailPopover title="Screens" bind:this={popover} {align}>
|
||||
<svelte:fragment slot="anchor" let:open>
|
||||
<ActionButton
|
||||
{icon}
|
||||
quiet
|
||||
selected={open || !!(showCount && screens.length)}
|
||||
{accentColor}
|
||||
on:click={show}
|
||||
>
|
||||
Screens{showCount && screens.length ? `: ${screens.length}` : ""}
|
||||
</ActionButton>
|
||||
</svelte:fragment>
|
||||
|
||||
{#if !screens.length}
|
||||
There aren't any screens connected to this data.
|
||||
{:else}
|
||||
The following screens are connected to this data.
|
||||
<List>
|
||||
{#each screens as screen}
|
||||
<ListItem
|
||||
title={screen.url}
|
||||
url={`/builder/app/${$appStore.appId}/design/${screen._id}`}
|
||||
showArrow
|
||||
/>
|
||||
{/each}
|
||||
</List>
|
||||
{/if}
|
||||
|
||||
<slot name="footer" />
|
||||
</DetailPopover>
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
|
||||
import { screenStore } from "@/stores/builder"
|
||||
import ScreensPopover from "@/components/common/ScreensPopover.svelte"
|
||||
import type { ScreenUsage } from "@budibase/types"
|
||||
|
||||
export let sourceId: string
|
||||
|
||||
let screens: ScreenUsage[] = []
|
||||
let popover: any
|
||||
|
||||
export function show() {
|
||||
popover?.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
popover?.hide()
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
let response = await screenStore.usageInScreens(sourceId)
|
||||
screens = response?.screens
|
||||
})
|
||||
</script>
|
||||
|
||||
<ScreensPopover
|
||||
bind:this={popover}
|
||||
{screens}
|
||||
icon="WebPage"
|
||||
accentColor="#364800"
|
||||
showCount
|
||||
/>
|
|
@ -23,6 +23,7 @@
|
|||
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
||||
import QueryViewerSavePromptModal from "./QueryViewerSavePromptModal.svelte"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
import ConnectedQueryScreens from "./ConnectedQueryScreens.svelte"
|
||||
|
||||
export let query
|
||||
let queryHash
|
||||
|
@ -170,6 +171,7 @@
|
|||
</Body>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<ConnectedQueryScreens sourceId={query._id} />
|
||||
<Button disabled={loading} on:click={runQuery} overBackground>
|
||||
<Icon size="S" name="Play" />
|
||||
Run query</Button
|
||||
|
@ -384,6 +386,8 @@
|
|||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
runtimeToReadableMap,
|
||||
toBindingsArray,
|
||||
} from "@/dataBinding"
|
||||
import ConnectedQueryScreens from "./ConnectedQueryScreens.svelte"
|
||||
|
||||
export let queryId
|
||||
|
||||
|
@ -502,9 +503,12 @@
|
|||
on:change={() => (query.flags.urlName = false)}
|
||||
on:save={saveQuery}
|
||||
/>
|
||||
<div class="access">
|
||||
<Label>Access</Label>
|
||||
<AccessLevelSelect {query} {saveId} />
|
||||
<div class="controls">
|
||||
<ConnectedQueryScreens sourceId={query._id} />
|
||||
<div class="access">
|
||||
<Label>Access</Label>
|
||||
<AccessLevelSelect {query} {saveId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="url-block">
|
||||
|
@ -825,6 +829,12 @@
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.access {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
<script>
|
||||
import { views, viewsV2 } from "@/stores/builder"
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export let view
|
||||
|
||||
let confirmDeleteDialog
|
||||
|
||||
export const show = () => {
|
||||
confirmDeleteDialog.show()
|
||||
}
|
||||
|
||||
async function deleteView() {
|
||||
try {
|
||||
if (view.version === 2) {
|
||||
await viewsV2.delete(view)
|
||||
} else {
|
||||
await views.delete(view)
|
||||
}
|
||||
notifications.success("View deleted")
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error deleting view")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
body={`Are you sure you wish to delete the view '${view.name}'? Your data will be deleted and this action cannot be undone.`}
|
||||
okText="Delete View"
|
||||
onOk={deleteView}
|
||||
title="Confirm Deletion"
|
||||
/>
|
|
@ -10,9 +10,8 @@
|
|||
import { Icon, ActionButton, ActionMenu, MenuItem } from "@budibase/bbui"
|
||||
import { params, url } from "@roxi/routify"
|
||||
import EditViewModal from "./EditViewModal.svelte"
|
||||
import DeleteViewModal from "./DeleteViewModal.svelte"
|
||||
import EditTableModal from "@/components/backend/TableNavigator/TableNavItem/EditModal.svelte"
|
||||
import DeleteTableModal from "@/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte"
|
||||
import DeleteConfirmationModal from "@/components/backend/modals/DeleteDataConfirmationModal.svelte"
|
||||
import { UserAvatars } from "@budibase/frontend-core"
|
||||
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
||||
import { TableNames } from "@/constants"
|
||||
|
@ -314,12 +313,12 @@
|
|||
|
||||
{#if table && tableEditable}
|
||||
<EditTableModal {table} bind:this={editTableModal} />
|
||||
<DeleteTableModal {table} bind:this={deleteTableModal} />
|
||||
<DeleteConfirmationModal source={table} bind:this={deleteTableModal} />
|
||||
{/if}
|
||||
|
||||
{#if editableView}
|
||||
<EditViewModal view={editableView} bind:this={editViewModal} />
|
||||
<DeleteViewModal view={editableView} bind:this={deleteViewModal} />
|
||||
<DeleteConfirmationModal source={editableView} bind:this={deleteViewModal} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
|
|
@ -500,6 +500,13 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a list of screens that are used by a given source ID (table, view, datasource, query)
|
||||
*/
|
||||
async usageInScreens(sourceId: string) {
|
||||
return API.usageInScreens(sourceId)
|
||||
}
|
||||
}
|
||||
|
||||
export const screenStore = new ScreenStore()
|
||||
|
|
|
@ -3089,12 +3089,6 @@
|
|||
"type": "tableConditions",
|
||||
"label": "Conditions",
|
||||
"key": "conditions"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Format",
|
||||
"key": "format",
|
||||
"info": "Changing format will display values as text"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -7691,8 +7685,7 @@
|
|||
{
|
||||
"type": "columns/grid",
|
||||
"key": "columns",
|
||||
"resetOn": "table",
|
||||
"nested": true
|
||||
"resetOn": "table"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import { get, derived, readable } from "svelte/store"
|
||||
import { featuresStore } from "@/stores"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
// import { processStringSync } from "@budibase/string-templates"
|
||||
|
||||
// table is actually any datasource, but called table for legacy compatibility
|
||||
export let table
|
||||
|
@ -105,7 +105,7 @@
|
|||
order: idx,
|
||||
conditions: column.conditions,
|
||||
visible: !!column.active,
|
||||
format: createFormatter(column),
|
||||
// format: createFormatter(column),
|
||||
}
|
||||
if (column.width) {
|
||||
overrides[column.field].width = column.width
|
||||
|
@ -114,12 +114,12 @@
|
|||
return overrides
|
||||
}
|
||||
|
||||
const createFormatter = column => {
|
||||
if (typeof column.format !== "string" || !column.format.trim().length) {
|
||||
return null
|
||||
}
|
||||
return row => processStringSync(column.format, { [id]: row })
|
||||
}
|
||||
// const createFormatter = column => {
|
||||
// if (typeof column.format !== "string" || !column.format.trim().length) {
|
||||
// return null
|
||||
// }
|
||||
// return row => processStringSync(column.format, { [id]: row })
|
||||
// }
|
||||
|
||||
const enrichButtons = buttons => {
|
||||
if (!buttons?.length) {
|
||||
|
|
|
@ -2,12 +2,14 @@ import {
|
|||
DeleteScreenResponse,
|
||||
SaveScreenRequest,
|
||||
SaveScreenResponse,
|
||||
UsageInScreensResponse,
|
||||
} from "@budibase/types"
|
||||
import { BaseAPIClient } from "./types"
|
||||
|
||||
export interface ScreenEndpoints {
|
||||
saveScreen: (screen: SaveScreenRequest) => Promise<SaveScreenResponse>
|
||||
deleteScreen: (id: string, rev: string) => Promise<DeleteScreenResponse>
|
||||
usageInScreens: (sourceId: string) => Promise<UsageInScreensResponse>
|
||||
}
|
||||
|
||||
export const buildScreenEndpoints = (API: BaseAPIClient): ScreenEndpoints => ({
|
||||
|
@ -32,4 +34,10 @@ export const buildScreenEndpoints = (API: BaseAPIClient): ScreenEndpoints => ({
|
|||
url: `/api/screens/${id}/${rev}`,
|
||||
})
|
||||
},
|
||||
|
||||
usageInScreens: async sourceId => {
|
||||
return await API.post({
|
||||
url: `/api/screens/usage/${sourceId}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,34 +1,30 @@
|
|||
import { getScreenParams, generateScreenID, DocumentType } from "../../db/utils"
|
||||
import { DocumentType, generateScreenID } from "../../db/utils"
|
||||
import {
|
||||
events,
|
||||
context,
|
||||
tenancy,
|
||||
db as dbCore,
|
||||
events,
|
||||
roles,
|
||||
tenancy,
|
||||
} from "@budibase/backend-core"
|
||||
import { updateAppPackage } from "./application"
|
||||
import {
|
||||
Plugin,
|
||||
ScreenProps,
|
||||
Screen,
|
||||
UserCtx,
|
||||
DeleteScreenResponse,
|
||||
FetchScreenResponse,
|
||||
Plugin,
|
||||
SaveScreenRequest,
|
||||
SaveScreenResponse,
|
||||
DeleteScreenResponse,
|
||||
Screen,
|
||||
ScreenProps,
|
||||
ScreenUsage,
|
||||
UsageInScreensResponse,
|
||||
UserCtx,
|
||||
} from "@budibase/types"
|
||||
import { builderSocket } from "../../websockets"
|
||||
import sdk from "../../sdk"
|
||||
import { sdk as sharedSdk } from "@budibase/shared-core"
|
||||
|
||||
export async function fetch(ctx: UserCtx<void, FetchScreenResponse>) {
|
||||
const db = context.getAppDB()
|
||||
|
||||
const screens = (
|
||||
await db.allDocs(
|
||||
getScreenParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
).rows.map((el: any) => el.doc)
|
||||
const screens = await sdk.screens.fetch()
|
||||
|
||||
const roleId = ctx.user?.role?._id as string
|
||||
if (!roleId) {
|
||||
|
@ -140,3 +136,23 @@ function findPlugins(component: ScreenProps, foundPlugins: string[]) {
|
|||
}
|
||||
component._children.forEach(child => findPlugins(child, foundPlugins))
|
||||
}
|
||||
|
||||
export async function usage(ctx: UserCtx<void, UsageInScreensResponse>) {
|
||||
const sourceId = ctx.params.sourceId
|
||||
const sourceType = sdk.common.getSourceType(sourceId)
|
||||
const allScreens = await sdk.screens.fetch()
|
||||
const response: ScreenUsage[] = []
|
||||
for (let screen of allScreens) {
|
||||
const found = sharedSdk.screens.findInSettings(screen, sourceId)
|
||||
if (found.length !== 0) {
|
||||
response.push({
|
||||
url: screen.routing.route,
|
||||
_id: screen._id!,
|
||||
})
|
||||
}
|
||||
}
|
||||
ctx.body = {
|
||||
sourceType,
|
||||
screens: response,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,5 +19,10 @@ router
|
|||
authorized(permissions.BUILDER),
|
||||
controller.destroy
|
||||
)
|
||||
.post(
|
||||
"/api/screens/usage/:sourceId",
|
||||
authorized(permissions.BUILDER),
|
||||
controller.usage
|
||||
)
|
||||
|
||||
export default router
|
||||
|
|
|
@ -108,7 +108,7 @@ describe("/automations", () => {
|
|||
|
||||
it("Should ensure you can't have a branch as not a last step", async () => {
|
||||
const automation = createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "active" } })
|
||||
.onAppAction()
|
||||
.branch({
|
||||
activeBranch: {
|
||||
steps: stepBuilder =>
|
||||
|
@ -132,7 +132,7 @@ describe("/automations", () => {
|
|||
|
||||
it("Should check validation on an automation that has a branch step with no children", async () => {
|
||||
const automation = createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "active" } })
|
||||
.onAppAction()
|
||||
.branch({})
|
||||
.serverLog({ text: "Inactive user" })
|
||||
.build()
|
||||
|
@ -148,7 +148,7 @@ describe("/automations", () => {
|
|||
|
||||
it("Should check validation on a branch step with empty conditions", async () => {
|
||||
const automation = createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "active" } })
|
||||
.onAppAction()
|
||||
.branch({
|
||||
activeBranch: {
|
||||
steps: stepBuilder =>
|
||||
|
@ -169,7 +169,7 @@ describe("/automations", () => {
|
|||
|
||||
it("Should check validation on an branch that has a condition that is not valid", async () => {
|
||||
const automation = createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "active" } })
|
||||
.onAppAction()
|
||||
.branch({
|
||||
activeBranch: {
|
||||
steps: stepBuilder =>
|
||||
|
@ -241,6 +241,7 @@ describe("/automations", () => {
|
|||
|
||||
it("should be able to access platformUrl, logoUrl and company in the automation", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.serverLog({
|
||||
text: "{{ settings.url }}",
|
||||
})
|
||||
|
@ -250,7 +251,7 @@ describe("/automations", () => {
|
|||
.serverLog({
|
||||
text: "{{ settings.company }}",
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.message).toEndWith("https://example.com")
|
||||
expect(result.steps[1].outputs.message).toEndWith(
|
||||
|
|
|
@ -1,9 +1,24 @@
|
|||
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||
import * as setup from "./utilities"
|
||||
import { events, roles } from "@budibase/backend-core"
|
||||
import { Screen, Role, BuiltinPermissionID } from "@budibase/types"
|
||||
import {
|
||||
Screen,
|
||||
Role,
|
||||
BuiltinPermissionID,
|
||||
SourceType,
|
||||
UsageInScreensResponse,
|
||||
} from "@budibase/types"
|
||||
|
||||
const { basicScreen } = setup.structures
|
||||
const {
|
||||
basicScreen,
|
||||
createTableScreen,
|
||||
createViewScreen,
|
||||
createQueryScreen,
|
||||
basicTable,
|
||||
viewV2,
|
||||
basicQuery,
|
||||
basicDatasource,
|
||||
} = setup.structures
|
||||
|
||||
describe("/screens", () => {
|
||||
let config = setup.getConfig()
|
||||
|
@ -18,7 +33,7 @@ describe("/screens", () => {
|
|||
|
||||
describe("fetch", () => {
|
||||
it("should be able to create a layout", async () => {
|
||||
const screens = await config.api.screen.list({ status: 200 })
|
||||
const screens = await config.api.screen.list()
|
||||
expect(screens.length).toEqual(1)
|
||||
expect(screens.some(s => s._id === screen._id)).toEqual(true)
|
||||
})
|
||||
|
@ -52,28 +67,22 @@ describe("/screens", () => {
|
|||
inherits: [role1._id!, role2._id!],
|
||||
permissionId: BuiltinPermissionID.WRITE,
|
||||
})
|
||||
screen1 = await config.api.screen.save(
|
||||
{
|
||||
...basicScreen(),
|
||||
routing: {
|
||||
roleId: role1._id!,
|
||||
route: "/foo",
|
||||
homeScreen: false,
|
||||
},
|
||||
screen1 = await config.api.screen.save({
|
||||
...basicScreen(),
|
||||
routing: {
|
||||
roleId: role1._id!,
|
||||
route: "/foo",
|
||||
homeScreen: false,
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
screen2 = await config.api.screen.save(
|
||||
{
|
||||
...basicScreen(),
|
||||
routing: {
|
||||
roleId: role2._id!,
|
||||
route: "/bar",
|
||||
homeScreen: false,
|
||||
},
|
||||
})
|
||||
screen2 = await config.api.screen.save({
|
||||
...basicScreen(),
|
||||
routing: {
|
||||
roleId: role2._id!,
|
||||
route: "/bar",
|
||||
homeScreen: false,
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
})
|
||||
// get into prod app
|
||||
await config.publish()
|
||||
})
|
||||
|
@ -81,10 +90,7 @@ describe("/screens", () => {
|
|||
async function checkScreens(roleId: string, screenIds: string[]) {
|
||||
await config.loginAsRole(roleId, async () => {
|
||||
const res = await config.api.application.getDefinition(
|
||||
config.prodAppId!,
|
||||
{
|
||||
status: 200,
|
||||
}
|
||||
config.getProdAppId()
|
||||
)
|
||||
expect(res.screens.length).toEqual(screenIds.length)
|
||||
expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort())
|
||||
|
@ -114,10 +120,7 @@ describe("/screens", () => {
|
|||
},
|
||||
async () => {
|
||||
const res = await config.api.application.getDefinition(
|
||||
config.prodAppId!,
|
||||
{
|
||||
status: 200,
|
||||
}
|
||||
config.prodAppId!
|
||||
)
|
||||
const screenIds = [screen._id!, screen1._id!]
|
||||
expect(res.screens.length).toEqual(screenIds.length)
|
||||
|
@ -134,9 +137,7 @@ describe("/screens", () => {
|
|||
|
||||
it("should be able to create a screen", async () => {
|
||||
const screen = basicScreen()
|
||||
const responseScreen = await config.api.screen.save(screen, {
|
||||
status: 200,
|
||||
})
|
||||
const responseScreen = await config.api.screen.save(screen)
|
||||
|
||||
expect(responseScreen._rev).toBeDefined()
|
||||
expect(responseScreen.name).toEqual(screen.name)
|
||||
|
@ -145,13 +146,13 @@ describe("/screens", () => {
|
|||
|
||||
it("should be able to update a screen", async () => {
|
||||
const screen = basicScreen()
|
||||
let responseScreen = await config.api.screen.save(screen, { status: 200 })
|
||||
let responseScreen = await config.api.screen.save(screen)
|
||||
screen._id = responseScreen._id
|
||||
screen._rev = responseScreen._rev
|
||||
screen.name = "edit"
|
||||
jest.clearAllMocks()
|
||||
|
||||
responseScreen = await config.api.screen.save(screen, { status: 200 })
|
||||
responseScreen = await config.api.screen.save(screen)
|
||||
|
||||
expect(responseScreen._rev).toBeDefined()
|
||||
expect(responseScreen.name).toEqual(screen.name)
|
||||
|
@ -171,8 +172,7 @@ describe("/screens", () => {
|
|||
it("should be able to delete the screen", async () => {
|
||||
const response = await config.api.screen.destroy(
|
||||
screen._id!,
|
||||
screen._rev!,
|
||||
{ status: 200 }
|
||||
screen._rev!
|
||||
)
|
||||
expect(response.message).toBeDefined()
|
||||
expect(events.screen.deleted).toHaveBeenCalledTimes(1)
|
||||
|
@ -186,4 +186,57 @@ describe("/screens", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("usage", () => {
|
||||
beforeEach(async () => {
|
||||
await config.init()
|
||||
await config.api.screen.save(basicScreen())
|
||||
})
|
||||
|
||||
function confirmScreen(usage: UsageInScreensResponse, screen: Screen) {
|
||||
expect(usage.screens.length).toEqual(1)
|
||||
expect(usage.screens[0].url).toEqual(screen.routing.route)
|
||||
expect(usage.screens[0]._id).toEqual(screen._id!)
|
||||
}
|
||||
|
||||
it("should find table usage", async () => {
|
||||
const table = await config.api.table.save(basicTable())
|
||||
const screen = await config.api.screen.save(
|
||||
createTableScreen("BudibaseDB", table)
|
||||
)
|
||||
const usage = await config.api.screen.usage(table._id!)
|
||||
expect(usage.sourceType).toEqual(SourceType.TABLE)
|
||||
confirmScreen(usage, screen)
|
||||
})
|
||||
|
||||
it("should find view usage", async () => {
|
||||
const table = await config.api.table.save(basicTable())
|
||||
const view = await config.api.viewV2.create(
|
||||
viewV2.createRequest(table._id!),
|
||||
{ status: 201 }
|
||||
)
|
||||
const screen = await config.api.screen.save(
|
||||
createViewScreen("BudibaseDB", view)
|
||||
)
|
||||
const usage = await config.api.screen.usage(view.id)
|
||||
expect(usage.sourceType).toEqual(SourceType.VIEW)
|
||||
confirmScreen(usage, screen)
|
||||
})
|
||||
|
||||
it("should find datasource/query usage", async () => {
|
||||
const datasource = await config.api.datasource.create(
|
||||
basicDatasource().datasource
|
||||
)
|
||||
const query = await config.api.query.save(basicQuery(datasource._id!))
|
||||
const screen = await config.api.screen.save(
|
||||
createQueryScreen(datasource._id!, query)
|
||||
)
|
||||
const dsUsage = await config.api.screen.usage(datasource._id!)
|
||||
expect(dsUsage.sourceType).toEqual(SourceType.DATASOURCE)
|
||||
confirmScreen(dsUsage, screen)
|
||||
const queryUsage = await config.api.screen.usage(query._id!)
|
||||
expect(queryUsage.sourceType).toEqual(SourceType.QUERY)
|
||||
confirmScreen(queryUsage, screen)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -25,6 +25,7 @@ describe("Branching automations", () => {
|
|||
const branch2Id = "44444444-4444-4444-4444-444444444444"
|
||||
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.serverLog(
|
||||
{ text: "Starting automation" },
|
||||
{ stepName: "FirstLog", stepId: firstLogId }
|
||||
|
@ -75,7 +76,7 @@ describe("Branching automations", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[3].outputs.status).toContain("branch1 branch taken")
|
||||
expect(results.steps[4].outputs.message).toContain("Branch 1.1")
|
||||
|
@ -83,7 +84,7 @@ describe("Branching automations", () => {
|
|||
|
||||
it("should execute correct branch based on string equality", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "active" } })
|
||||
.onAppAction()
|
||||
.branch({
|
||||
activeBranch: {
|
||||
steps: stepBuilder => stepBuilder.serverLog({ text: "Active user" }),
|
||||
|
@ -99,7 +100,7 @@ describe("Branching automations", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: { status: "active" } })
|
||||
expect(results.steps[0].outputs.status).toContain(
|
||||
"activeBranch branch taken"
|
||||
)
|
||||
|
@ -108,7 +109,7 @@ describe("Branching automations", () => {
|
|||
|
||||
it("should handle multiple conditions with AND operator", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "active", role: "admin" } })
|
||||
.onAppAction()
|
||||
.branch({
|
||||
activeAdminBranch: {
|
||||
steps: stepBuilder =>
|
||||
|
@ -129,14 +130,14 @@ describe("Branching automations", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: { status: "active", role: "admin" } })
|
||||
|
||||
expect(results.steps[1].outputs.message).toContain("Active admin user")
|
||||
})
|
||||
|
||||
it("should handle multiple conditions with OR operator", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "test", role: "user" } })
|
||||
.onAppAction()
|
||||
.branch({
|
||||
specialBranch: {
|
||||
steps: stepBuilder => stepBuilder.serverLog({ text: "Special user" }),
|
||||
|
@ -161,14 +162,14 @@ describe("Branching automations", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: { status: "test", role: "user" } })
|
||||
|
||||
expect(results.steps[1].outputs.message).toContain("Special user")
|
||||
})
|
||||
|
||||
it("should stop the branch automation when no conditions are met", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "test", role: "user" } })
|
||||
.onAppAction()
|
||||
.createRow({ row: { name: "Test", tableId: table._id } })
|
||||
.branch({
|
||||
specialBranch: {
|
||||
|
@ -194,7 +195,7 @@ describe("Branching automations", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: { status: "test", role: "user" } })
|
||||
|
||||
expect(results.steps[1].outputs.status).toEqual(
|
||||
AutomationStatus.NO_CONDITION_MET
|
||||
|
@ -204,7 +205,7 @@ describe("Branching automations", () => {
|
|||
|
||||
it("evaluate multiple conditions", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { test_trigger: true } })
|
||||
.onAppAction()
|
||||
.serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" })
|
||||
.branch({
|
||||
specialBranch: {
|
||||
|
@ -238,14 +239,14 @@ describe("Branching automations", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: { test_trigger: true } })
|
||||
|
||||
expect(results.steps[2].outputs.message).toContain("Special user")
|
||||
})
|
||||
|
||||
it("evaluate multiple conditions with interpolated text", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { test_trigger: true } })
|
||||
.onAppAction()
|
||||
.serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" })
|
||||
.branch({
|
||||
specialBranch: {
|
||||
|
@ -275,7 +276,7 @@ describe("Branching automations", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: { test_trigger: true } })
|
||||
|
||||
expect(results.steps[2].outputs.message).toContain("Special user")
|
||||
})
|
||||
|
|
|
@ -30,13 +30,7 @@ describe("Automation Scenarios", () => {
|
|||
const table = await config.api.table.save(basicTable())
|
||||
|
||||
const results = await createAutomationBuilder(config)
|
||||
.rowUpdated(
|
||||
{ tableId: table._id! },
|
||||
{
|
||||
row: { name: "Test", description: "TEST" },
|
||||
id: "1234",
|
||||
}
|
||||
)
|
||||
.onRowUpdated({ tableId: table._id! })
|
||||
.createRow({
|
||||
row: {
|
||||
name: "{{trigger.row.name}}",
|
||||
|
@ -44,7 +38,10 @@ describe("Automation Scenarios", () => {
|
|||
tableId: table._id,
|
||||
},
|
||||
})
|
||||
.run()
|
||||
.test({
|
||||
row: { name: "Test", description: "TEST" },
|
||||
id: "1234",
|
||||
})
|
||||
|
||||
expect(results.steps).toHaveLength(1)
|
||||
|
||||
|
@ -66,10 +63,11 @@ describe("Automation Scenarios", () => {
|
|||
await config.api.row.save(table._id!, row)
|
||||
await config.api.row.save(table._id!, row)
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps).toHaveLength(1)
|
||||
expect(results.steps[0].outputs.rows).toHaveLength(2)
|
||||
|
@ -84,6 +82,7 @@ describe("Automation Scenarios", () => {
|
|||
await config.api.row.save(table._id!, row)
|
||||
await config.api.row.save(table._id!, row)
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
|
@ -94,7 +93,7 @@ describe("Automation Scenarios", () => {
|
|||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps).toHaveLength(3)
|
||||
expect(results.steps[1].outputs.success).toBeTruthy()
|
||||
|
@ -125,6 +124,7 @@ describe("Automation Scenarios", () => {
|
|||
})
|
||||
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.createRow(
|
||||
{
|
||||
row: {
|
||||
|
@ -153,7 +153,7 @@ describe("Automation Scenarios", () => {
|
|||
},
|
||||
{ stepName: "QueryRowsStep" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps).toHaveLength(3)
|
||||
|
||||
|
@ -193,6 +193,7 @@ describe("Automation Scenarios", () => {
|
|||
await config.api.row.save(table._id!, row)
|
||||
await config.api.row.save(table._id!, row)
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -206,7 +207,7 @@ describe("Automation Scenarios", () => {
|
|||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps).toHaveLength(3)
|
||||
expect(results.steps[1].outputs.success).toBeTruthy()
|
||||
|
@ -242,6 +243,7 @@ describe("Automation Scenarios", () => {
|
|||
|
||||
it("should stop an automation if the condition is not met", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.createRow({
|
||||
row: {
|
||||
name: "Equal Test",
|
||||
|
@ -258,7 +260,7 @@ describe("Automation Scenarios", () => {
|
|||
value: 20,
|
||||
})
|
||||
.serverLog({ text: "Equal condition met" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[2].outputs.success).toBeTrue()
|
||||
expect(results.steps[2].outputs.result).toBeFalse()
|
||||
|
@ -267,6 +269,7 @@ describe("Automation Scenarios", () => {
|
|||
|
||||
it("should continue the automation if the condition is met", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.createRow({
|
||||
row: {
|
||||
name: "Not Equal Test",
|
||||
|
@ -283,7 +286,7 @@ describe("Automation Scenarios", () => {
|
|||
value: 20,
|
||||
})
|
||||
.serverLog({ text: "Not Equal condition met" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[2].outputs.success).toBeTrue()
|
||||
expect(results.steps[2].outputs.result).toBeTrue()
|
||||
|
@ -333,6 +336,7 @@ describe("Automation Scenarios", () => {
|
|||
"should pass the filter when condition is $condition",
|
||||
async ({ condition, value, rowValue, expectPass }) => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.createRow({
|
||||
row: {
|
||||
name: `${condition} Test`,
|
||||
|
@ -351,7 +355,7 @@ describe("Automation Scenarios", () => {
|
|||
.serverLog({
|
||||
text: `${condition} condition ${expectPass ? "passed" : "failed"}`,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[2].outputs.result).toBe(expectPass)
|
||||
if (expectPass) {
|
||||
|
@ -367,23 +371,21 @@ describe("Automation Scenarios", () => {
|
|||
const table = await config.api.table.save(basicTable())
|
||||
|
||||
const results = await createAutomationBuilder(config)
|
||||
.rowUpdated(
|
||||
{ tableId: table._id! },
|
||||
{
|
||||
row: { name: "Test", description: "TEST" },
|
||||
id: "1234",
|
||||
}
|
||||
)
|
||||
.onRowUpdated({ tableId: table._id! })
|
||||
.serverLog({ text: "{{ [user].[email] }}" })
|
||||
.run()
|
||||
.test({
|
||||
row: { name: "Test", description: "TEST" },
|
||||
id: "1234",
|
||||
})
|
||||
|
||||
expect(results.steps[0].outputs.message).toContain("example.com")
|
||||
})
|
||||
|
||||
it("Check user is passed through from app trigger", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.serverLog({ text: "{{ [user].[email] }}" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.message).toContain("example.com")
|
||||
})
|
||||
|
@ -453,9 +455,7 @@ if (descriptions.length) {
|
|||
})
|
||||
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({
|
||||
fields: {},
|
||||
})
|
||||
.onAppAction()
|
||||
.executeQuery({
|
||||
query: {
|
||||
queryId: query._id!,
|
||||
|
@ -475,7 +475,7 @@ if (descriptions.length) {
|
|||
.queryRows({
|
||||
tableId: newTable._id!,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps).toHaveLength(3)
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ describe("Execute Bash Automations", () => {
|
|||
|
||||
it("should use trigger data in bash command and pass output to subsequent steps", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { command: "hello world" } })
|
||||
.onAppAction()
|
||||
.bash(
|
||||
{ code: "echo '{{ trigger.fields.command }}'" },
|
||||
{ stepName: "Echo Command" }
|
||||
|
@ -34,7 +34,7 @@ describe("Execute Bash Automations", () => {
|
|||
{ text: "Bash output was: {{ steps.[Echo Command].stdout }}" },
|
||||
{ stepName: "Log Output" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: { command: "hello world" } })
|
||||
|
||||
expect(result.steps[0].outputs.stdout).toEqual("hello world\n")
|
||||
expect(result.steps[1].outputs.message).toContain(
|
||||
|
@ -44,7 +44,7 @@ describe("Execute Bash Automations", () => {
|
|||
|
||||
it("should chain multiple bash commands using previous outputs", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { filename: "testfile.txt" } })
|
||||
.onAppAction()
|
||||
.bash(
|
||||
{ code: "echo 'initial content' > {{ trigger.fields.filename }}" },
|
||||
{ stepName: "Create File" }
|
||||
|
@ -57,7 +57,7 @@ describe("Execute Bash Automations", () => {
|
|||
{ code: "rm {{ trigger.fields.filename }}" },
|
||||
{ stepName: "Cleanup" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: { filename: "testfile.txt" } })
|
||||
|
||||
expect(result.steps[1].outputs.stdout).toEqual("INITIAL CONTENT\n")
|
||||
expect(result.steps[1].outputs.success).toEqual(true)
|
||||
|
@ -65,6 +65,7 @@ describe("Execute Bash Automations", () => {
|
|||
|
||||
it("should integrate bash output with row operations", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -82,7 +83,7 @@ describe("Execute Bash Automations", () => {
|
|||
{ text: "{{ steps.[Process Row Data].stdout }}" },
|
||||
{ stepName: "Log Result" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[1].outputs.stdout).toContain(
|
||||
"Row data: test row - test description"
|
||||
|
@ -94,7 +95,7 @@ describe("Execute Bash Automations", () => {
|
|||
|
||||
it("should handle bash output in conditional logic", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { threshold: "5" } })
|
||||
.onAppAction()
|
||||
.bash(
|
||||
{ code: "echo $(( {{ trigger.fields.threshold }} + 5 ))" },
|
||||
{ stepName: "Calculate Value" }
|
||||
|
@ -112,7 +113,7 @@ describe("Execute Bash Automations", () => {
|
|||
{ text: "Value was {{ steps.[Check Value].value }}" },
|
||||
{ stepName: "Log Result" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: { threshold: "5" } })
|
||||
|
||||
expect(result.steps[0].outputs.stdout).toEqual("10\n")
|
||||
expect(result.steps[1].outputs.value).toEqual("high")
|
||||
|
@ -121,12 +122,13 @@ describe("Execute Bash Automations", () => {
|
|||
|
||||
it("should handle null values gracefully", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.bash(
|
||||
// @ts-expect-error - testing null input
|
||||
{ code: null },
|
||||
{ stepName: "Null Command" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.stdout).toBe(
|
||||
"Budibase bash automation failed: Invalid inputs"
|
||||
|
|
|
@ -41,14 +41,14 @@ describe("test the create row action", () => {
|
|||
|
||||
it("should be able to run the action", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "new" } })
|
||||
.onAppAction()
|
||||
.serverLog({ text: "Starting create row flow" }, { stepName: "StartLog" })
|
||||
.createRow({ row }, { stepName: "CreateRow" })
|
||||
.serverLog(
|
||||
{ text: "Row created with ID: {{ stepsByName.CreateRow.row._id }}" },
|
||||
{ stepName: "CreationLog" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: { status: "new" } })
|
||||
|
||||
expect(result.steps[1].outputs.success).toBeDefined()
|
||||
expect(result.steps[1].outputs.id).toBeDefined()
|
||||
|
@ -67,7 +67,7 @@ describe("test the create row action", () => {
|
|||
|
||||
it("should return an error (not throw) when bad info provided", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "error" } })
|
||||
.onAppAction()
|
||||
.serverLog({ text: "Starting error test flow" }, { stepName: "StartLog" })
|
||||
.createRow(
|
||||
{
|
||||
|
@ -78,14 +78,14 @@ describe("test the create row action", () => {
|
|||
},
|
||||
{ stepName: "CreateRow" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: { status: "error" } })
|
||||
|
||||
expect(result.steps[1].outputs.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("should check invalid inputs return an error", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "invalid" } })
|
||||
.onAppAction()
|
||||
.serverLog({ text: "Testing invalid input" }, { stepName: "StartLog" })
|
||||
.createRow({ row: {} }, { stepName: "CreateRow" })
|
||||
.filter({
|
||||
|
@ -97,7 +97,7 @@ describe("test the create row action", () => {
|
|||
{ text: "This log should not appear" },
|
||||
{ stepName: "SkippedLog" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: { status: "invalid" } })
|
||||
|
||||
expect(result.steps[1].outputs.success).toEqual(false)
|
||||
expect(result.steps.length).toBeLessThan(4)
|
||||
|
@ -123,7 +123,7 @@ describe("test the create row action", () => {
|
|||
|
||||
attachmentRow.file_attachment = attachmentObject
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { type: "attachment" } })
|
||||
.onAppAction()
|
||||
.serverLog(
|
||||
{ text: "Processing attachment upload" },
|
||||
{ stepName: "StartLog" }
|
||||
|
@ -140,7 +140,7 @@ describe("test the create row action", () => {
|
|||
},
|
||||
{ stepName: "UploadLog" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: { type: "attachment" } })
|
||||
|
||||
expect(result.steps[1].outputs.success).toEqual(true)
|
||||
expect(result.steps[1].outputs.row.file_attachment[0]).toHaveProperty("key")
|
||||
|
@ -174,7 +174,7 @@ describe("test the create row action", () => {
|
|||
|
||||
attachmentRow.single_file_attachment = attachmentObject
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { type: "single-attachment" } })
|
||||
.onAppAction()
|
||||
.serverLog(
|
||||
{ text: "Processing single attachment" },
|
||||
{ stepName: "StartLog" }
|
||||
|
@ -209,7 +209,7 @@ describe("test the create row action", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: { type: "single-attachment" } })
|
||||
|
||||
expect(result.steps[1].outputs.success).toEqual(true)
|
||||
expect(result.steps[1].outputs.row.single_file_attachment).toHaveProperty(
|
||||
|
@ -245,7 +245,7 @@ describe("test the create row action", () => {
|
|||
|
||||
attachmentRow.single_file_attachment = attachmentObject
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { type: "invalid-attachment" } })
|
||||
.onAppAction()
|
||||
.serverLog(
|
||||
{ text: "Testing invalid attachment keys" },
|
||||
{ stepName: "StartLog" }
|
||||
|
@ -278,7 +278,7 @@ describe("test the create row action", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: { type: "invalid-attachment" } })
|
||||
|
||||
expect(result.steps[1].outputs.success).toEqual(false)
|
||||
expect(result.steps[1].outputs.response).toEqual(
|
||||
|
|
|
@ -27,7 +27,7 @@ describe("cron automations", () => {
|
|||
})
|
||||
|
||||
it("should initialise the automation timestamp", async () => {
|
||||
await createAutomationBuilder(config).cron({ cron: "* * * * *" }).save()
|
||||
await createAutomationBuilder(config).onCron({ cron: "* * * * *" }).save()
|
||||
|
||||
tk.travel(Date.now() + oneMinuteInMs)
|
||||
await config.publish()
|
||||
|
|
|
@ -16,7 +16,10 @@ describe("test the delay logic", () => {
|
|||
const time = 100
|
||||
const before = performance.now()
|
||||
|
||||
await createAutomationBuilder(config).delay({ time }).run()
|
||||
await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.delay({ time })
|
||||
.test({ fields: {} })
|
||||
|
||||
const now = performance.now()
|
||||
|
||||
|
|
|
@ -21,12 +21,13 @@ describe("test the delete row action", () => {
|
|||
|
||||
it("should be able to run the delete row action", async () => {
|
||||
await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.deleteRow({
|
||||
tableId: table._id!,
|
||||
id: row._id!,
|
||||
revision: row._rev,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
await config.api.row.get(table._id!, row._id!, {
|
||||
status: 404,
|
||||
|
@ -35,20 +36,22 @@ describe("test the delete row action", () => {
|
|||
|
||||
it("should check invalid inputs return an error", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.deleteRow({ tableId: "", id: "", revision: "" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("should return an error when table doesn't exist", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.deleteRow({
|
||||
tableId: "invalid",
|
||||
id: "invalid",
|
||||
revision: "invalid",
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
|
|
|
@ -20,12 +20,13 @@ describe("test the outgoing webhook action", () => {
|
|||
it("should be able to run the action", async () => {
|
||||
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.discord({
|
||||
url: "http://www.example.com",
|
||||
username: "joe_bloggs",
|
||||
content: "Hello, world",
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
expect(result.steps[0].outputs.response.foo).toEqual("bar")
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
|
|
|
@ -21,30 +21,32 @@ describe("Execute Script Automations", () => {
|
|||
|
||||
it("should execute a basic script and return the result", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.executeScript({ code: "return 2 + 2" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.value).toEqual(4)
|
||||
})
|
||||
|
||||
it("should access bindings from previous steps", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { data: [1, 2, 3] } })
|
||||
.onAppAction()
|
||||
.executeScript(
|
||||
{
|
||||
code: "return trigger.fields.data.map(x => x * 2)",
|
||||
},
|
||||
{ stepId: "binding-script-step" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: { data: [1, 2, 3] } })
|
||||
|
||||
expect(results.steps[0].outputs.value).toEqual([2, 4, 6])
|
||||
})
|
||||
|
||||
it("should handle script execution errors gracefully", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.executeScript({ code: "return nonexistentVariable.map(x => x)" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.response).toContain(
|
||||
"ReferenceError: nonexistentVariable is not defined"
|
||||
|
@ -54,7 +56,7 @@ describe("Execute Script Automations", () => {
|
|||
|
||||
it("should handle conditional logic in scripts", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { value: 10 } })
|
||||
.onAppAction()
|
||||
.executeScript({
|
||||
code: `
|
||||
if (trigger.fields.value > 5) {
|
||||
|
@ -64,14 +66,14 @@ describe("Execute Script Automations", () => {
|
|||
}
|
||||
`,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: { value: 10 } })
|
||||
|
||||
expect(results.steps[0].outputs.value).toEqual("Value is greater than 5")
|
||||
})
|
||||
|
||||
it("should use multiple steps and validate script execution", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: {} })
|
||||
.onAppAction()
|
||||
.serverLog(
|
||||
{ text: "Starting multi-step automation" },
|
||||
{ stepId: "start-log-step" }
|
||||
|
@ -92,7 +94,7 @@ describe("Execute Script Automations", () => {
|
|||
.serverLog({
|
||||
text: `Final result is {{ steps.ScriptingStep1.value }}`,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.message).toContain(
|
||||
"Starting multi-step automation"
|
||||
|
|
|
@ -43,8 +43,9 @@ describe("test the filter logic", () => {
|
|||
]
|
||||
it.each(pass)("should pass %p %p %p", async (field, condition, value) => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.filter({ field, condition: stringToFilterCondition(condition), value })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.result).toEqual(true)
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
|
@ -60,8 +61,9 @@ describe("test the filter logic", () => {
|
|||
]
|
||||
it.each(fail)("should fail %p %p %p", async (field, condition, value) => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.filter({ field, condition: stringToFilterCondition(condition), value })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.result).toEqual(false)
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
|
|
|
@ -73,17 +73,7 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
|
||||
it("should run an automation with a trigger, loop, and create row step", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.rowSaved(
|
||||
{ tableId: table._id! },
|
||||
{
|
||||
row: {
|
||||
name: "Trigger Row",
|
||||
description: "This row triggers the automation",
|
||||
},
|
||||
id: "1234",
|
||||
revision: "1",
|
||||
}
|
||||
)
|
||||
.onRowSaved({ tableId: table._id! })
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: [1, 2, 3],
|
||||
|
@ -95,7 +85,14 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
tableId: table._id,
|
||||
},
|
||||
})
|
||||
.run()
|
||||
.test({
|
||||
row: {
|
||||
name: "Trigger Row",
|
||||
description: "This row triggers the automation",
|
||||
},
|
||||
id: "1234",
|
||||
revision: "1",
|
||||
})
|
||||
|
||||
expect(results.trigger).toBeDefined()
|
||||
expect(results.steps).toHaveLength(1)
|
||||
|
@ -116,17 +113,7 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
|
||||
it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.rowSaved(
|
||||
{ tableId: table._id! },
|
||||
{
|
||||
row: {
|
||||
name: "Trigger Row",
|
||||
description: "This row triggers the automation",
|
||||
},
|
||||
id: "1234",
|
||||
revision: "1",
|
||||
}
|
||||
)
|
||||
.onRowSaved({ tableId: table._id! })
|
||||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
|
@ -136,7 +123,14 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
})
|
||||
.serverLog({ text: "Message {{loop.currentItem}}" })
|
||||
.serverLog({ text: "{{steps.1.rows.0._id}}" })
|
||||
.run()
|
||||
.test({
|
||||
row: {
|
||||
name: "Trigger Row",
|
||||
description: "This row triggers the automation",
|
||||
},
|
||||
id: "1234",
|
||||
revision: "1",
|
||||
})
|
||||
|
||||
results.steps[1].outputs.items.forEach(
|
||||
(output: ServerLogStepOutputs, index: number) => {
|
||||
|
@ -152,12 +146,13 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
|
||||
it("if an incorrect type is passed to the loop it should return an error", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: "1, 2, 3",
|
||||
})
|
||||
.serverLog({ text: "Message {{loop.currentItem}}" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs).toEqual({
|
||||
success: false,
|
||||
|
@ -167,13 +162,14 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
|
||||
it("ensure the loop stops if the failure condition is reached", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: ["test", "test2", "test3"],
|
||||
failure: "test2",
|
||||
})
|
||||
.serverLog({ text: "Message {{loop.currentItem}}" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -185,6 +181,7 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
|
||||
it("ensure the loop stops if the max iterations are reached", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: ["test", "test2", "test3"],
|
||||
|
@ -192,13 +189,14 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
})
|
||||
.serverLog({ text: "{{loop.currentItem}}" })
|
||||
.serverLog({ text: "{{steps.1.iterations}}" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.iterations).toBe(2)
|
||||
})
|
||||
|
||||
it("should run an automation with loop and max iterations to ensure context correctness further down the tree", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: ["test", "test2", "test3"],
|
||||
|
@ -206,24 +204,14 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
})
|
||||
.serverLog({ text: "{{loop.currentItem}}" })
|
||||
.serverLog({ text: "{{steps.1.iterations}}" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[1].outputs.message).toContain("- 2")
|
||||
})
|
||||
|
||||
it("should run an automation where a loop is successfully run twice", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.rowSaved(
|
||||
{ tableId: table._id! },
|
||||
{
|
||||
row: {
|
||||
name: "Trigger Row",
|
||||
description: "This row triggers the automation",
|
||||
},
|
||||
id: "1234",
|
||||
revision: "1",
|
||||
}
|
||||
)
|
||||
.onRowSaved({ tableId: table._id! })
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: [1, 2, 3],
|
||||
|
@ -240,7 +228,14 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
binding: "Message 1,Message 2,Message 3",
|
||||
})
|
||||
.serverLog({ text: "{{loop.currentItem}}" })
|
||||
.run()
|
||||
.test({
|
||||
row: {
|
||||
name: "Trigger Row",
|
||||
description: "This row triggers the automation",
|
||||
},
|
||||
id: "1234",
|
||||
revision: "1",
|
||||
})
|
||||
|
||||
expect(results.trigger).toBeDefined()
|
||||
expect(results.steps).toHaveLength(2)
|
||||
|
@ -275,6 +270,7 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
|
||||
it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: [1, 2, 3],
|
||||
|
@ -287,7 +283,7 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
})
|
||||
.serverLog({ text: "{{loop.currentItem}}" })
|
||||
.serverLog({ text: "{{steps.3.iterations}}" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
// We want to ensure that bindings are corr
|
||||
expect(results.steps[1].outputs.message).toContain("- 3")
|
||||
|
@ -296,6 +292,7 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
|
||||
it("should use automation names to loop with", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.loop(
|
||||
{
|
||||
option: LoopStepType.ARRAY,
|
||||
|
@ -311,7 +308,7 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
{ text: "{{steps.FirstLoopLog.iterations}}" },
|
||||
{ stepName: "FirstLoopIterationLog" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[1].outputs.message).toContain("- 3")
|
||||
})
|
||||
|
@ -347,6 +344,7 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
await config.api.row.bulkImport(table._id!, { rows })
|
||||
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
|
@ -366,7 +364,7 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
const expectedRows = [
|
||||
{ name: "Updated Row 1", value: 1 },
|
||||
|
@ -426,6 +424,7 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
await config.api.row.bulkImport(table._id!, { rows })
|
||||
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -448,7 +447,7 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
const expectedRows = [
|
||||
{ name: "Updated Row 1", value: 1 },
|
||||
|
@ -508,6 +507,7 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
await config.api.row.bulkImport(table._id!, { rows })
|
||||
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
|
@ -522,7 +522,7 @@ describe("Attempt to run a basic loop automation", () => {
|
|||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps).toHaveLength(3)
|
||||
|
||||
|
|
|
@ -20,11 +20,12 @@ describe("test the outgoing webhook action", () => {
|
|||
it("should be able to run the action", async () => {
|
||||
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.make({
|
||||
url: "http://www.example.com",
|
||||
body: null,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.response.foo).toEqual("bar")
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
|
@ -46,11 +47,12 @@ describe("test the outgoing webhook action", () => {
|
|||
.reply(200, { foo: "bar" })
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.make({
|
||||
body: { value: JSON.stringify(payload) },
|
||||
url: "http://www.example.com",
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.response.foo).toEqual("bar")
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
|
@ -58,11 +60,12 @@ describe("test the outgoing webhook action", () => {
|
|||
|
||||
it("should return a 400 if the JSON payload string is malformed", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.make({
|
||||
body: { value: "{ invalid json }" },
|
||||
url: "http://www.example.com",
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.httpStatus).toEqual(400)
|
||||
expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON")
|
||||
|
|
|
@ -21,12 +21,13 @@ describe("test the outgoing webhook action", () => {
|
|||
it("should be able to run the action and default to 'get'", async () => {
|
||||
nock("http://www.example.com/").get("/").reply(200, { foo: "bar" })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.n8n({
|
||||
url: "http://www.example.com",
|
||||
body: { test: "IGNORE_ME" },
|
||||
authorization: "",
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.response).toEqual({ foo: "bar" })
|
||||
expect(result.steps[0].outputs.httpStatus).toEqual(200)
|
||||
|
@ -39,26 +40,28 @@ describe("test the outgoing webhook action", () => {
|
|||
.reply(200)
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.n8n({
|
||||
url: "http://www.example.com",
|
||||
body: { value: JSON.stringify({ name: "Adam", age: 9 }) },
|
||||
method: HttpMethod.POST,
|
||||
authorization: "",
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
|
||||
it("should return a 400 if the JSON payload string is malformed", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.n8n({
|
||||
url: "http://www.example.com",
|
||||
body: { value: "{ value1 1 }" },
|
||||
method: HttpMethod.POST,
|
||||
authorization: "",
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.httpStatus).toEqual(400)
|
||||
expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON")
|
||||
|
@ -71,13 +74,14 @@ describe("test the outgoing webhook action", () => {
|
|||
.reply(200)
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.n8n({
|
||||
url: "http://www.example.com",
|
||||
method: HttpMethod.HEAD,
|
||||
body: { test: "IGNORE_ME" },
|
||||
authorization: "",
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
|
|
|
@ -58,8 +58,9 @@ describe("test the openai action", () => {
|
|||
// own API key. We don't count this against your quota.
|
||||
const result = await expectAIUsage(0, () =>
|
||||
createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
)
|
||||
|
||||
expect(result.steps[0].outputs.response).toEqual("This is a test")
|
||||
|
@ -69,8 +70,9 @@ describe("test the openai action", () => {
|
|||
it("should present the correct error message when a prompt is not provided", async () => {
|
||||
const result = await expectAIUsage(0, () =>
|
||||
createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.openai({ prompt: "", model: Model.GPT_4O_MINI })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
)
|
||||
|
||||
expect(result.steps[0].outputs.response).toEqual(
|
||||
|
@ -84,8 +86,9 @@ describe("test the openai action", () => {
|
|||
|
||||
const result = await expectAIUsage(0, () =>
|
||||
createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
)
|
||||
|
||||
expect(result.steps[0].outputs.response).toEqual(
|
||||
|
@ -106,8 +109,9 @@ describe("test the openai action", () => {
|
|||
// key, so we charge users for it.
|
||||
const result = await expectAIUsage(14, () =>
|
||||
createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.openai({ model: Model.GPT_4O_MINI, prompt: "Hello, world" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
)
|
||||
|
||||
expect(result.steps[0].outputs.response).toEqual("This is a test")
|
||||
|
|
|
@ -24,13 +24,14 @@ describe("test the outgoing webhook action", () => {
|
|||
.reply(200, { foo: "bar" })
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.outgoingWebhook({
|
||||
requestMethod: RequestType.POST,
|
||||
url: "http://www.example.com",
|
||||
requestBody: JSON.stringify({ a: 1 }),
|
||||
headers: {},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
expect(result.steps[0].outputs.httpStatus).toEqual(200)
|
||||
|
@ -39,13 +40,14 @@ describe("test the outgoing webhook action", () => {
|
|||
|
||||
it("should return an error if something goes wrong in fetch", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.outgoingWebhook({
|
||||
requestMethod: RequestType.GET,
|
||||
url: "www.invalid.com",
|
||||
requestBody: "",
|
||||
headers: {},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
expect(result.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -29,6 +29,7 @@ describe("Test a query step automation", () => {
|
|||
|
||||
it("should be able to run the query step", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -43,7 +44,7 @@ describe("Test a query step automation", () => {
|
|||
},
|
||||
{ stepName: "Query All Rows" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.success).toBe(true)
|
||||
expect(result.steps[0].outputs.rows).toBeDefined()
|
||||
|
@ -53,6 +54,7 @@ describe("Test a query step automation", () => {
|
|||
|
||||
it("Returns all rows when onEmptyFilter has no value and no filters are passed", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -63,7 +65,7 @@ describe("Test a query step automation", () => {
|
|||
},
|
||||
{ stepName: "Query With Empty Filter" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.success).toBe(true)
|
||||
expect(result.steps[0].outputs.rows).toBeDefined()
|
||||
|
@ -73,6 +75,7 @@ describe("Test a query step automation", () => {
|
|||
|
||||
it("Returns no rows when onEmptyFilter is RETURN_NONE and theres no filters", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -85,7 +88,7 @@ describe("Test a query step automation", () => {
|
|||
},
|
||||
{ stepName: "Query With Return None" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.success).toBe(true)
|
||||
expect(result.steps[0].outputs.rows).toBeDefined()
|
||||
|
@ -94,6 +97,7 @@ describe("Test a query step automation", () => {
|
|||
|
||||
it("Returns no rows when onEmptyFilters RETURN_NONE and a filter is passed with a null value", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -110,7 +114,7 @@ describe("Test a query step automation", () => {
|
|||
},
|
||||
{ stepName: "Query With Null Filter" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.success).toBe(true)
|
||||
expect(result.steps[0].outputs.rows).toBeDefined()
|
||||
|
@ -119,6 +123,7 @@ describe("Test a query step automation", () => {
|
|||
|
||||
it("Returns rows when onEmptyFilter is RETURN_ALL and no filter is passed", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -130,7 +135,7 @@ describe("Test a query step automation", () => {
|
|||
},
|
||||
{ stepName: "Query With Return All" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.success).toBe(true)
|
||||
expect(result.steps[0].outputs.rows).toBeDefined()
|
||||
|
@ -146,6 +151,7 @@ describe("Test a query step automation", () => {
|
|||
name: NAME,
|
||||
})
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.queryRows(
|
||||
{
|
||||
tableId: tableWithSpaces._id!,
|
||||
|
@ -154,7 +160,7 @@ describe("Test a query step automation", () => {
|
|||
},
|
||||
{ stepName: "Query table with spaces" }
|
||||
)
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
expect(result.steps[0].outputs.success).toBe(true)
|
||||
expect(result.steps[0].outputs.rows).toBeDefined()
|
||||
expect(result.steps[0].outputs.rows.length).toBe(1)
|
||||
|
|
|
@ -14,8 +14,9 @@ describe("test the server log action", () => {
|
|||
|
||||
it("should be able to log the text", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.serverLog({ text: "Hello World" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
expect(result.steps[0].outputs.message).toEqual(
|
||||
`App ${config.getAppId()} - Hello World`
|
||||
)
|
||||
|
|
|
@ -17,24 +17,27 @@ describe("Test triggering an automation from another automation", () => {
|
|||
})
|
||||
|
||||
it("should trigger an other server log automation", async () => {
|
||||
const automation = await createAutomationBuilder(config)
|
||||
const { automation } = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.serverLog({ text: "Hello World" })
|
||||
.save()
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.triggerAutomationRun({
|
||||
automation: {
|
||||
automationId: automation._id!,
|
||||
},
|
||||
timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.success).toBe(true)
|
||||
})
|
||||
|
||||
it("should fail gracefully if the automation id is incorrect", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.triggerAutomationRun({
|
||||
automation: {
|
||||
// @ts-expect-error - incorrect on purpose
|
||||
|
@ -42,7 +45,7 @@ describe("Test triggering an automation from another automation", () => {
|
|||
},
|
||||
timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT,
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.success).toBe(false)
|
||||
})
|
||||
|
|
|
@ -31,6 +31,7 @@ describe("test the update row action", () => {
|
|||
|
||||
it("should be able to run the update row action", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.updateRow({
|
||||
rowId: row._id!,
|
||||
row: {
|
||||
|
@ -40,7 +41,7 @@ describe("test the update row action", () => {
|
|||
},
|
||||
meta: {},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(true)
|
||||
const updatedRow = await config.api.row.get(
|
||||
|
@ -53,20 +54,22 @@ describe("test the update row action", () => {
|
|||
|
||||
it("should check invalid inputs return an error", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.updateRow({ meta: {}, row: {}, rowId: "" })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("should return an error when table doesn't exist", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.updateRow({
|
||||
row: { _id: "invalid" },
|
||||
rowId: "invalid",
|
||||
meta: {},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
|
@ -104,6 +107,7 @@ describe("test the update row action", () => {
|
|||
})
|
||||
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.updateRow({
|
||||
rowId: row._id!,
|
||||
row: {
|
||||
|
@ -115,7 +119,7 @@ describe("test the update row action", () => {
|
|||
},
|
||||
meta: {},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(true)
|
||||
|
||||
|
@ -157,6 +161,7 @@ describe("test the update row action", () => {
|
|||
})
|
||||
|
||||
const results = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.updateRow({
|
||||
rowId: row._id!,
|
||||
row: {
|
||||
|
@ -174,7 +179,7 @@ describe("test the update row action", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(true)
|
||||
|
||||
|
|
|
@ -21,8 +21,9 @@ describe("test the outgoing webhook action", () => {
|
|||
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.zapier({ url: "http://www.example.com", body: null })
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.response.foo).toEqual("bar")
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
|
@ -44,11 +45,12 @@ describe("test the outgoing webhook action", () => {
|
|||
.reply(200, { foo: "bar" })
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.zapier({
|
||||
url: "http://www.example.com",
|
||||
body: { value: JSON.stringify(payload) },
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.response.foo).toEqual("bar")
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
|
@ -56,11 +58,12 @@ describe("test the outgoing webhook action", () => {
|
|||
|
||||
it("should return a 400 if the JSON payload string is malformed", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.onAppAction()
|
||||
.zapier({
|
||||
url: "http://www.example.com",
|
||||
body: { value: "{ invalid json }" },
|
||||
})
|
||||
.run()
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(result.steps[0].outputs.success).toEqual(false)
|
||||
expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON")
|
||||
|
|
|
@ -25,7 +25,7 @@ describe("cron trigger", () => {
|
|||
})
|
||||
|
||||
await createAutomationBuilder(config)
|
||||
.cron({ cron: "* * * * *" })
|
||||
.onCron({ cron: "* * * * *" })
|
||||
.serverLog({
|
||||
text: "Hello, world!",
|
||||
})
|
||||
|
@ -45,7 +45,7 @@ describe("cron trigger", () => {
|
|||
|
||||
it("should fail if the cron expression is invalid", async () => {
|
||||
await createAutomationBuilder(config)
|
||||
.cron({ cron: "* * * * * *" })
|
||||
.onCron({ cron: "* * * * * *" })
|
||||
.serverLog({
|
||||
text: "Hello, world!",
|
||||
})
|
||||
|
|
|
@ -11,8 +11,8 @@ describe("Branching automations", () => {
|
|||
let webhook: Webhook
|
||||
|
||||
async function createWebhookAutomation() {
|
||||
const automation = await createAutomationBuilder(config)
|
||||
.webhook({ fields: { parameter: "string" } })
|
||||
const { automation } = await createAutomationBuilder(config)
|
||||
.onWebhook({ fields: { parameter: "string" } })
|
||||
.createRow({
|
||||
row: { tableId: table._id!, name: "{{ trigger.parameter }}" },
|
||||
})
|
||||
|
|
|
@ -2,39 +2,26 @@ import { v4 as uuidv4 } from "uuid"
|
|||
import { BUILTIN_ACTION_DEFINITIONS } from "../../actions"
|
||||
import { TRIGGER_DEFINITIONS } from "../../triggers"
|
||||
import {
|
||||
AppActionTriggerOutputs,
|
||||
Automation,
|
||||
AutomationActionStepId,
|
||||
AutomationStep,
|
||||
AutomationStepInputs,
|
||||
AutomationTrigger,
|
||||
AutomationTriggerDefinition,
|
||||
AutomationTriggerInputs,
|
||||
AutomationTriggerOutputs,
|
||||
AutomationTriggerStepId,
|
||||
BranchStepInputs,
|
||||
CronTriggerOutputs,
|
||||
isDidNotTriggerResponse,
|
||||
RowCreatedTriggerOutputs,
|
||||
RowDeletedTriggerOutputs,
|
||||
RowUpdatedTriggerOutputs,
|
||||
SearchFilters,
|
||||
TestAutomationRequest,
|
||||
WebhookTriggerOutputs,
|
||||
} from "@budibase/types"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import * as setup from "../utilities"
|
||||
import { automations } from "@budibase/shared-core"
|
||||
|
||||
type TriggerOutputs =
|
||||
| RowCreatedTriggerOutputs
|
||||
| RowUpdatedTriggerOutputs
|
||||
| RowDeletedTriggerOutputs
|
||||
| AppActionTriggerOutputs
|
||||
| WebhookTriggerOutputs
|
||||
| CronTriggerOutputs
|
||||
| undefined
|
||||
|
||||
type StepBuilderFunction = (stepBuilder: StepBuilder) => void
|
||||
type StepBuilderFunction = <TStep extends AutomationTriggerStepId>(
|
||||
stepBuilder: BranchStepBuilder<TStep>
|
||||
) => void
|
||||
|
||||
type BranchConfig = {
|
||||
[key: string]: {
|
||||
|
@ -43,11 +30,44 @@ type BranchConfig = {
|
|||
}
|
||||
}
|
||||
|
||||
class BaseStepBuilder {
|
||||
protected steps: AutomationStep[] = []
|
||||
protected stepNames: { [key: string]: string } = {}
|
||||
class TriggerBuilder {
|
||||
private readonly config: TestConfiguration
|
||||
|
||||
protected createStepFn<TStep extends AutomationActionStepId>(stepId: TStep) {
|
||||
constructor(config: TestConfiguration) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
protected trigger<
|
||||
TStep extends AutomationTriggerStepId,
|
||||
TInput = AutomationTriggerInputs<TStep>
|
||||
>(stepId: TStep) {
|
||||
return (inputs: TInput) => {
|
||||
const definition: AutomationTriggerDefinition =
|
||||
TRIGGER_DEFINITIONS[stepId]
|
||||
const trigger: AutomationTrigger = {
|
||||
...definition,
|
||||
stepId,
|
||||
inputs: (inputs || {}) as any,
|
||||
id: uuidv4(),
|
||||
}
|
||||
return new StepBuilder<TStep>(this.config, trigger)
|
||||
}
|
||||
}
|
||||
|
||||
onAppAction = this.trigger(AutomationTriggerStepId.APP)
|
||||
|
||||
onRowSaved = this.trigger(AutomationTriggerStepId.ROW_SAVED)
|
||||
onRowUpdated = this.trigger(AutomationTriggerStepId.ROW_UPDATED)
|
||||
onRowDeleted = this.trigger(AutomationTriggerStepId.ROW_DELETED)
|
||||
onWebhook = this.trigger(AutomationTriggerStepId.WEBHOOK)
|
||||
onCron = this.trigger(AutomationTriggerStepId.CRON)
|
||||
}
|
||||
|
||||
class BranchStepBuilder<TStep extends AutomationTriggerStepId> {
|
||||
protected readonly steps: AutomationStep[] = []
|
||||
protected readonly stepNames: { [key: string]: string } = {}
|
||||
|
||||
protected step<TStep extends AutomationActionStepId>(stepId: TStep) {
|
||||
return (
|
||||
inputs: AutomationStepInputs<TStep>,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
|
@ -68,59 +88,49 @@ class BaseStepBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
createRow = this.createStepFn(AutomationActionStepId.CREATE_ROW)
|
||||
updateRow = this.createStepFn(AutomationActionStepId.UPDATE_ROW)
|
||||
deleteRow = this.createStepFn(AutomationActionStepId.DELETE_ROW)
|
||||
sendSmtpEmail = this.createStepFn(AutomationActionStepId.SEND_EMAIL_SMTP)
|
||||
executeQuery = this.createStepFn(AutomationActionStepId.EXECUTE_QUERY)
|
||||
queryRows = this.createStepFn(AutomationActionStepId.QUERY_ROWS)
|
||||
loop = this.createStepFn(AutomationActionStepId.LOOP)
|
||||
serverLog = this.createStepFn(AutomationActionStepId.SERVER_LOG)
|
||||
executeScript = this.createStepFn(AutomationActionStepId.EXECUTE_SCRIPT)
|
||||
filter = this.createStepFn(AutomationActionStepId.FILTER)
|
||||
bash = this.createStepFn(AutomationActionStepId.EXECUTE_BASH)
|
||||
openai = this.createStepFn(AutomationActionStepId.OPENAI)
|
||||
collect = this.createStepFn(AutomationActionStepId.COLLECT)
|
||||
zapier = this.createStepFn(AutomationActionStepId.zapier)
|
||||
triggerAutomationRun = this.createStepFn(
|
||||
createRow = this.step(AutomationActionStepId.CREATE_ROW)
|
||||
updateRow = this.step(AutomationActionStepId.UPDATE_ROW)
|
||||
deleteRow = this.step(AutomationActionStepId.DELETE_ROW)
|
||||
sendSmtpEmail = this.step(AutomationActionStepId.SEND_EMAIL_SMTP)
|
||||
executeQuery = this.step(AutomationActionStepId.EXECUTE_QUERY)
|
||||
queryRows = this.step(AutomationActionStepId.QUERY_ROWS)
|
||||
loop = this.step(AutomationActionStepId.LOOP)
|
||||
serverLog = this.step(AutomationActionStepId.SERVER_LOG)
|
||||
executeScript = this.step(AutomationActionStepId.EXECUTE_SCRIPT)
|
||||
filter = this.step(AutomationActionStepId.FILTER)
|
||||
bash = this.step(AutomationActionStepId.EXECUTE_BASH)
|
||||
openai = this.step(AutomationActionStepId.OPENAI)
|
||||
collect = this.step(AutomationActionStepId.COLLECT)
|
||||
zapier = this.step(AutomationActionStepId.zapier)
|
||||
triggerAutomationRun = this.step(
|
||||
AutomationActionStepId.TRIGGER_AUTOMATION_RUN
|
||||
)
|
||||
outgoingWebhook = this.createStepFn(AutomationActionStepId.OUTGOING_WEBHOOK)
|
||||
n8n = this.createStepFn(AutomationActionStepId.n8n)
|
||||
make = this.createStepFn(AutomationActionStepId.integromat)
|
||||
discord = this.createStepFn(AutomationActionStepId.discord)
|
||||
delay = this.createStepFn(AutomationActionStepId.DELAY)
|
||||
outgoingWebhook = this.step(AutomationActionStepId.OUTGOING_WEBHOOK)
|
||||
n8n = this.step(AutomationActionStepId.n8n)
|
||||
make = this.step(AutomationActionStepId.integromat)
|
||||
discord = this.step(AutomationActionStepId.discord)
|
||||
delay = this.step(AutomationActionStepId.DELAY)
|
||||
|
||||
protected addBranchStep(branchConfig: BranchConfig): void {
|
||||
const branchStepInputs: BranchStepInputs = {
|
||||
const inputs: BranchStepInputs = {
|
||||
branches: [],
|
||||
children: {},
|
||||
}
|
||||
|
||||
Object.entries(branchConfig).forEach(([key, branch]) => {
|
||||
const stepBuilder = new StepBuilder()
|
||||
branch.steps(stepBuilder)
|
||||
let branchId = uuidv4()
|
||||
branchStepInputs.branches.push({
|
||||
name: key,
|
||||
condition: branch.condition,
|
||||
id: branchId,
|
||||
})
|
||||
branchStepInputs.children![branchId] = stepBuilder.build()
|
||||
})
|
||||
const branchStep: AutomationStep = {
|
||||
for (const [name, branch] of Object.entries(branchConfig)) {
|
||||
const builder = new BranchStepBuilder<TStep>()
|
||||
branch.steps(builder)
|
||||
let id = uuidv4()
|
||||
inputs.branches.push({ name, condition: branch.condition, id })
|
||||
inputs.children![id] = builder.steps
|
||||
}
|
||||
|
||||
this.steps.push({
|
||||
...automations.steps.branch.definition,
|
||||
id: uuidv4(),
|
||||
stepId: AutomationActionStepId.BRANCH,
|
||||
inputs: branchStepInputs,
|
||||
}
|
||||
this.steps.push(branchStep)
|
||||
}
|
||||
}
|
||||
|
||||
class StepBuilder extends BaseStepBuilder {
|
||||
build(): AutomationStep[] {
|
||||
return this.steps
|
||||
inputs,
|
||||
})
|
||||
}
|
||||
|
||||
branch(branchConfig: BranchConfig): this {
|
||||
|
@ -129,121 +139,76 @@ class StepBuilder extends BaseStepBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
class AutomationBuilder extends BaseStepBuilder {
|
||||
private automationConfig: Automation
|
||||
private config: TestConfiguration
|
||||
private triggerOutputs: TriggerOutputs
|
||||
private triggerSet = false
|
||||
class StepBuilder<
|
||||
TStep extends AutomationTriggerStepId
|
||||
> extends BranchStepBuilder<TStep> {
|
||||
private readonly config: TestConfiguration
|
||||
private readonly trigger: AutomationTrigger
|
||||
private _name: string | undefined = undefined
|
||||
|
||||
constructor(config?: TestConfiguration) {
|
||||
constructor(config: TestConfiguration, trigger: AutomationTrigger) {
|
||||
super()
|
||||
this.config = config || setup.getConfig()
|
||||
this.triggerOutputs = { fields: {} }
|
||||
this.automationConfig = {
|
||||
name: `Test Automation ${uuidv4()}`,
|
||||
this.config = config
|
||||
this.trigger = trigger
|
||||
}
|
||||
|
||||
name(n: string): this {
|
||||
this._name = n
|
||||
return this
|
||||
}
|
||||
|
||||
build(): Automation {
|
||||
const name = this._name || `Test Automation ${uuidv4()}`
|
||||
return {
|
||||
name,
|
||||
definition: {
|
||||
steps: [],
|
||||
trigger: {
|
||||
...TRIGGER_DEFINITIONS[AutomationTriggerStepId.APP],
|
||||
stepId: AutomationTriggerStepId.APP,
|
||||
inputs: this.triggerOutputs,
|
||||
id: uuidv4(),
|
||||
},
|
||||
stepNames: {},
|
||||
steps: this.steps,
|
||||
trigger: this.trigger,
|
||||
stepNames: this.stepNames,
|
||||
},
|
||||
type: "automation",
|
||||
appId: this.config.getAppId(),
|
||||
}
|
||||
}
|
||||
|
||||
name(n: string): this {
|
||||
this.automationConfig.name = n
|
||||
return this
|
||||
}
|
||||
|
||||
protected triggerInputOutput<
|
||||
TStep extends AutomationTriggerStepId,
|
||||
TInput = AutomationTriggerInputs<TStep>,
|
||||
TOutput = AutomationTriggerOutputs<TStep>
|
||||
>(stepId: TStep) {
|
||||
return (inputs: TInput, outputs?: TOutput) => {
|
||||
if (this.triggerSet) {
|
||||
throw new Error("Only one trigger can be set for an automation.")
|
||||
}
|
||||
this.triggerOutputs = outputs as TriggerOutputs | undefined
|
||||
this.automationConfig.definition.trigger = {
|
||||
...TRIGGER_DEFINITIONS[stepId],
|
||||
stepId,
|
||||
inputs,
|
||||
id: uuidv4(),
|
||||
} as AutomationTrigger
|
||||
this.triggerSet = true
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
protected triggerOutputOnly<
|
||||
TStep extends AutomationTriggerStepId,
|
||||
TOutput = AutomationTriggerOutputs<TStep>
|
||||
>(stepId: TStep) {
|
||||
return (outputs: TOutput) => {
|
||||
this.triggerOutputs = outputs as TriggerOutputs
|
||||
this.automationConfig.definition.trigger = {
|
||||
...TRIGGER_DEFINITIONS[stepId],
|
||||
stepId,
|
||||
id: uuidv4(),
|
||||
} as AutomationTrigger
|
||||
this.triggerSet = true
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
// The input and output for appAction is identical, and we only ever seem to
|
||||
// set the output, so we're ignoring the input for now.
|
||||
appAction = this.triggerOutputOnly(AutomationTriggerStepId.APP)
|
||||
|
||||
rowSaved = this.triggerInputOutput(AutomationTriggerStepId.ROW_SAVED)
|
||||
rowUpdated = this.triggerInputOutput(AutomationTriggerStepId.ROW_UPDATED)
|
||||
rowDeleted = this.triggerInputOutput(AutomationTriggerStepId.ROW_DELETED)
|
||||
webhook = this.triggerInputOutput(AutomationTriggerStepId.WEBHOOK)
|
||||
cron = this.triggerInputOutput(AutomationTriggerStepId.CRON)
|
||||
|
||||
branch(branchConfig: BranchConfig): this {
|
||||
this.addBranchStep(branchConfig)
|
||||
return this
|
||||
}
|
||||
|
||||
build(): Automation {
|
||||
this.automationConfig.definition.steps = this.steps
|
||||
this.automationConfig.definition.stepNames = this.stepNames
|
||||
return this.automationConfig
|
||||
}
|
||||
|
||||
async save() {
|
||||
this.automationConfig.definition.steps = this.steps
|
||||
const { automation } = await this.config.api.automation.post(this.build())
|
||||
return automation
|
||||
return new AutomationRunner<TStep>(this.config, automation)
|
||||
}
|
||||
|
||||
async run() {
|
||||
const automation = await this.save()
|
||||
async test(triggerOutput: AutomationTriggerOutputs<TStep>) {
|
||||
const runner = await this.save()
|
||||
return await runner.test(triggerOutput)
|
||||
}
|
||||
}
|
||||
|
||||
class AutomationRunner<TStep extends AutomationTriggerStepId> {
|
||||
private readonly config: TestConfiguration
|
||||
readonly automation: Automation
|
||||
|
||||
constructor(config: TestConfiguration, automation: Automation) {
|
||||
this.config = config
|
||||
this.automation = automation
|
||||
}
|
||||
|
||||
async test(triggerOutput: AutomationTriggerOutputs<TStep>) {
|
||||
const response = await this.config.api.automation.test(
|
||||
automation._id!,
|
||||
this.triggerOutputs as TestAutomationRequest
|
||||
this.automation._id!,
|
||||
// TODO: figure out why this cast is needed.
|
||||
triggerOutput as TestAutomationRequest
|
||||
)
|
||||
|
||||
if (isDidNotTriggerResponse(response)) {
|
||||
throw new Error(response.message)
|
||||
}
|
||||
|
||||
// Remove the trigger step from the response.
|
||||
response.steps.shift()
|
||||
return {
|
||||
trigger: response.trigger,
|
||||
steps: response.steps,
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
export function createAutomationBuilder(config: TestConfiguration) {
|
||||
return new AutomationBuilder(config)
|
||||
return new TriggerBuilder(config)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { roles } from "@budibase/backend-core"
|
||||
import { BASE_LAYOUT_PROP_IDS } from "./layouts"
|
||||
import { Screen } from "@budibase/types"
|
||||
import { Screen, Table, Query, ViewV2, Component } from "@budibase/types"
|
||||
|
||||
export function createHomeScreen(
|
||||
config: {
|
||||
|
@ -53,3 +53,177 @@ export function createHomeScreen(
|
|||
name: "home-screen",
|
||||
}
|
||||
}
|
||||
|
||||
function heading(text: string): Component {
|
||||
return {
|
||||
_id: "c1bff24cd821e41d18c894ac77a80ef99",
|
||||
_component: "@budibase/standard-components/heading",
|
||||
_styles: {
|
||||
normal: {},
|
||||
hover: {},
|
||||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
_instanceName: "Table heading",
|
||||
_children: [],
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
export function createTableScreen(
|
||||
datasourceName: string,
|
||||
table: Table
|
||||
): Screen {
|
||||
return {
|
||||
props: {
|
||||
_id: "cad0a0904cacd4678a2ac094e293db1a5",
|
||||
_component: "@budibase/standard-components/container",
|
||||
_styles: {
|
||||
normal: {},
|
||||
hover: {},
|
||||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
_children: [
|
||||
heading("table"),
|
||||
{
|
||||
_id: "ca6304be2079147bb9933092c4f8ce6fa",
|
||||
_component: "@budibase/standard-components/gridblock",
|
||||
_styles: {
|
||||
normal: {},
|
||||
hover: {},
|
||||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
_instanceName: "table - Table",
|
||||
_children: [],
|
||||
table: {
|
||||
label: table.name,
|
||||
tableId: table._id!,
|
||||
type: "table",
|
||||
datasourceName,
|
||||
},
|
||||
},
|
||||
],
|
||||
_instanceName: "table - List",
|
||||
layout: "grid",
|
||||
direction: "column",
|
||||
hAlign: "stretch",
|
||||
vAlign: "top",
|
||||
size: "grow",
|
||||
gap: "M",
|
||||
},
|
||||
routing: {
|
||||
route: "/table",
|
||||
roleId: "ADMIN",
|
||||
homeScreen: false,
|
||||
},
|
||||
name: "screen-id",
|
||||
}
|
||||
}
|
||||
|
||||
export function createViewScreen(datasourceName: string, view: ViewV2): Screen {
|
||||
return {
|
||||
props: {
|
||||
_id: "cc359092bbd6c4e10b57827155edb7872",
|
||||
_component: "@budibase/standard-components/container",
|
||||
_styles: {
|
||||
normal: {},
|
||||
hover: {},
|
||||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
_children: [
|
||||
heading("view"),
|
||||
{
|
||||
_id: "ccb4a9e3734794864b5c65b012a0bdc5a",
|
||||
_component: "@budibase/standard-components/gridblock",
|
||||
_styles: {
|
||||
normal: {},
|
||||
hover: {},
|
||||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
_instanceName: "view - Table",
|
||||
_children: [],
|
||||
table: {
|
||||
...view,
|
||||
name: view.name,
|
||||
tableId: view.tableId,
|
||||
id: view.id,
|
||||
label: view.name,
|
||||
type: "viewV2",
|
||||
},
|
||||
},
|
||||
],
|
||||
_instanceName: "view - List",
|
||||
layout: "grid",
|
||||
direction: "column",
|
||||
hAlign: "stretch",
|
||||
vAlign: "top",
|
||||
size: "grow",
|
||||
gap: "M",
|
||||
},
|
||||
routing: {
|
||||
route: "/view",
|
||||
roleId: "ADMIN",
|
||||
homeScreen: false,
|
||||
},
|
||||
name: "view-id",
|
||||
}
|
||||
}
|
||||
|
||||
export function createQueryScreen(datasourceId: string, query: Query): Screen {
|
||||
return {
|
||||
props: {
|
||||
_id: "cc59b217aed264939a6c5249eee39cb25",
|
||||
_component: "@budibase/standard-components/container",
|
||||
_styles: {
|
||||
normal: {},
|
||||
hover: {},
|
||||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
_children: [
|
||||
{
|
||||
_id: "c33a4a6e3cb5343158a08625c06b5cd7c",
|
||||
_component: "@budibase/standard-components/gridblock",
|
||||
_styles: {
|
||||
normal: {},
|
||||
hover: {},
|
||||
active: {},
|
||||
},
|
||||
_instanceName: "New Table",
|
||||
table: {
|
||||
...query,
|
||||
label: query.name,
|
||||
_id: query._id!,
|
||||
name: query.name,
|
||||
datasourceId: datasourceId,
|
||||
type: "query",
|
||||
},
|
||||
initialSortOrder: "Ascending",
|
||||
allowAddRows: true,
|
||||
allowEditRows: true,
|
||||
allowDeleteRows: true,
|
||||
stripeRows: false,
|
||||
quiet: false,
|
||||
columns: null,
|
||||
},
|
||||
],
|
||||
_instanceName: "Blank screen",
|
||||
layout: "grid",
|
||||
direction: "column",
|
||||
hAlign: "stretch",
|
||||
vAlign: "top",
|
||||
size: "grow",
|
||||
gap: "M",
|
||||
},
|
||||
routing: {
|
||||
route: "/query",
|
||||
roleId: "BASIC",
|
||||
homeScreen: false,
|
||||
},
|
||||
name: "screen-id",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from "./utils"
|
|
@ -0,0 +1,15 @@
|
|||
import { SourceType } from "@budibase/types"
|
||||
import { docIds } from "@budibase/backend-core"
|
||||
|
||||
export function getSourceType(sourceId: string): SourceType {
|
||||
if (docIds.isTableId(sourceId)) {
|
||||
return SourceType.TABLE
|
||||
} else if (docIds.isViewId(sourceId)) {
|
||||
return SourceType.VIEW
|
||||
} else if (docIds.isDatasourceId(sourceId)) {
|
||||
return SourceType.DATASOURCE
|
||||
} else if (docIds.isQueryId(sourceId)) {
|
||||
return SourceType.QUERY
|
||||
}
|
||||
throw new Error(`Unknown source type for source "${sourceId}"`)
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./screens"
|
|
@ -0,0 +1,15 @@
|
|||
import { getScreenParams } from "../../../db/utils"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import { Screen } from "@budibase/types"
|
||||
|
||||
export async function fetch(): Promise<Screen[]> {
|
||||
const db = context.getAppDB()
|
||||
|
||||
return (
|
||||
await db.allDocs<Screen>(
|
||||
getScreenParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
).rows.map(el => el.doc!)
|
||||
}
|
|
@ -11,6 +11,10 @@ export function isExternal(opts: { table?: Table; tableId?: string }): boolean {
|
|||
return false
|
||||
}
|
||||
|
||||
export function isInternal(opts: { table?: Table; tableId?: string }): boolean {
|
||||
return !isExternal(opts)
|
||||
}
|
||||
|
||||
export function isTable(table: any): table is Table {
|
||||
return table._id && docIds.isTableId(table._id)
|
||||
}
|
||||
|
|
|
@ -81,6 +81,14 @@ export function isView(view: any): view is ViewV2 {
|
|||
return view.id && docIds.isViewId(view.id) && view.version === 2
|
||||
}
|
||||
|
||||
export function isInternal(viewId: string) {
|
||||
if (!docIds.isViewId(viewId)) {
|
||||
return false
|
||||
}
|
||||
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||
return !isExternalTableID(tableId)
|
||||
}
|
||||
|
||||
function guardDuplicateCalculationFields(view: Omit<ViewV2, "id" | "version">) {
|
||||
const seen: Record<string, Record<CalculationType, boolean>> = {}
|
||||
const calculationFields = helpers.views.calculationFields(view)
|
||||
|
|
|
@ -11,6 +11,8 @@ import { default as plugins } from "./plugins"
|
|||
import * as views from "./app/views"
|
||||
import * as permissions from "./app/permissions"
|
||||
import * as rowActions from "./app/rowActions"
|
||||
import * as screens from "./app/screens"
|
||||
import * as common from "./app/common"
|
||||
|
||||
const sdk = {
|
||||
backups,
|
||||
|
@ -22,10 +24,12 @@ const sdk = {
|
|||
datasources,
|
||||
queries,
|
||||
plugins,
|
||||
screens,
|
||||
views,
|
||||
permissions,
|
||||
links,
|
||||
rowActions,
|
||||
common,
|
||||
}
|
||||
|
||||
// default export for TS
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Screen } from "@budibase/types"
|
||||
import { Screen, UsageInScreensResponse } from "@budibase/types"
|
||||
import { Expectations, TestAPI } from "./base"
|
||||
|
||||
export class ScreenAPI extends TestAPI {
|
||||
|
@ -28,4 +28,16 @@ export class ScreenAPI extends TestAPI {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
usage = async (
|
||||
sourceId: string,
|
||||
expectations?: Expectations
|
||||
): Promise<UsageInScreensResponse> => {
|
||||
return this._post<UsageInScreensResponse>(
|
||||
`/api/screens/usage/${sourceId}`,
|
||||
{
|
||||
expectations,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,11 @@ import {
|
|||
import { LoopInput } from "../../definitions/automations"
|
||||
import { merge } from "lodash"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
export {
|
||||
createTableScreen,
|
||||
createQueryScreen,
|
||||
createViewScreen,
|
||||
} from "../../constants/screens"
|
||||
|
||||
const { BUILTIN_ROLE_IDS } = roles
|
||||
|
||||
|
@ -217,10 +222,8 @@ export function basicAutomation(opts?: DeepPartial<Automation>): Automation {
|
|||
icon: "test",
|
||||
description: "test",
|
||||
type: AutomationStepType.TRIGGER,
|
||||
inputs: {},
|
||||
id: "test",
|
||||
inputs: {
|
||||
fields: {},
|
||||
},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * as applications from "./applications"
|
||||
export * as automations from "./automations"
|
||||
export * as users from "./users"
|
||||
export * as screens from "./screens"
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { Screen, Component } from "@budibase/types"
|
||||
|
||||
export function findInSettings(screen: Screen, toFind: string) {
|
||||
const foundIn: { setting: string; value: string }[] = []
|
||||
function recurse(props: Component, parentKey = "") {
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (!value) {
|
||||
continue
|
||||
}
|
||||
if (typeof value === "string" && value.includes(toFind)) {
|
||||
foundIn.push({
|
||||
setting: parentKey ? `${parentKey}.${key}` : key,
|
||||
value: value,
|
||||
})
|
||||
} else if (typeof value === "object") {
|
||||
recurse(value, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recurse(screen.props)
|
||||
return foundIn
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { ScreenRoutingJson, Screen } from "../../../documents"
|
||||
import { ScreenRoutingJson, Screen, SourceType } from "../../../documents"
|
||||
|
||||
export interface FetchScreenRoutingResponse {
|
||||
routes: ScreenRoutingJson
|
||||
|
@ -15,3 +15,13 @@ export interface SaveScreenResponse extends Screen {}
|
|||
export interface DeleteScreenResponse {
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ScreenUsage {
|
||||
url: string
|
||||
_id: string
|
||||
}
|
||||
|
||||
export interface UsageInScreensResponse {
|
||||
sourceType: SourceType
|
||||
screens: ScreenUsage[]
|
||||
}
|
||||
|
|
|
@ -253,10 +253,6 @@ export type OutgoingWebhookStepInputs = {
|
|||
headers: string | Record<string, string>
|
||||
}
|
||||
|
||||
export type AppActionTriggerInputs = {
|
||||
fields: object
|
||||
}
|
||||
|
||||
export type AppActionTriggerOutputs = {
|
||||
fields: object
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ import {
|
|||
OpenAIStepInputs,
|
||||
OpenAIStepOutputs,
|
||||
LoopStepInputs,
|
||||
AppActionTriggerInputs,
|
||||
CronTriggerInputs,
|
||||
RowUpdatedTriggerInputs,
|
||||
RowCreatedTriggerInputs,
|
||||
|
@ -332,7 +331,7 @@ export type AutomationTriggerDefinition = Omit<
|
|||
|
||||
export type AutomationTriggerInputs<T extends AutomationTriggerStepId> =
|
||||
T extends AutomationTriggerStepId.APP
|
||||
? AppActionTriggerInputs
|
||||
? void | Record<string, any>
|
||||
: T extends AutomationTriggerStepId.CRON
|
||||
? CronTriggerInputs
|
||||
: T extends AutomationTriggerStepId.ROW_ACTION
|
||||
|
|
|
@ -57,3 +57,10 @@ export interface RestConfig {
|
|||
}
|
||||
dynamicVariables?: DynamicVariable[]
|
||||
}
|
||||
|
||||
export enum SourceType {
|
||||
DATASOURCE = "datasource",
|
||||
QUERY = "query",
|
||||
TABLE = "table",
|
||||
VIEW = "view",
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export interface UIEvent extends Omit<Event, "target"> {
|
||||
currentTarget: EventTarget & HTMLInputElement
|
||||
key?: string
|
||||
target?: any
|
||||
}
|
|
@ -3,3 +3,4 @@ export * from "./bindings"
|
|||
export * from "./components"
|
||||
export * from "./dataFetch"
|
||||
export * from "./datasource"
|
||||
export * from "./common"
|
||||
|
|
Loading…
Reference in New Issue