Merge branch 'master' of github.com:Budibase/budibase into dependabot/npm_and_yarn/ws-7.5.10
This commit is contained in:
commit
8172b6391e
|
@ -92,7 +92,8 @@
|
||||||
// differs to external, but the API is broadly the same
|
// differs to external, but the API is broadly the same
|
||||||
"jest/no-conditional-expect": "off",
|
"jest/no-conditional-expect": "off",
|
||||||
// have to turn this off to allow function overloading in typescript
|
// have to turn this off to allow function overloading in typescript
|
||||||
"no-dupe-class-members": "off"
|
"no-dupe-class-members": "off",
|
||||||
|
"no-redeclare": "off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -162,6 +162,7 @@
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
.modal-inner-wrapper {
|
.modal-inner-wrapper {
|
||||||
|
padding: 40px;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -176,7 +177,6 @@
|
||||||
border: 2px solid var(--spectrum-global-color-gray-200);
|
border: 2px solid var(--spectrum-global-color-gray-200);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
margin: 40px 0;
|
|
||||||
transform: none;
|
transform: none;
|
||||||
--spectrum-dialog-confirm-border-radius: var(
|
--spectrum-dialog-confirm-border-radius: var(
|
||||||
--spectrum-global-dimension-size-100
|
--spectrum-global-dimension-size-100
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
DatePicker,
|
DatePicker,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
Toggle,
|
Toggle,
|
||||||
|
Icon,
|
||||||
|
Divider,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
||||||
|
@ -89,6 +91,8 @@
|
||||||
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||||
: []
|
: []
|
||||||
|
|
||||||
|
let testDataRowVisibility = {}
|
||||||
|
|
||||||
const getInputData = (testData, blockInputs) => {
|
const getInputData = (testData, blockInputs) => {
|
||||||
// Test data is not cloned for reactivity
|
// Test data is not cloned for reactivity
|
||||||
let newInputData = testData || cloneDeep(blockInputs)
|
let newInputData = testData || cloneDeep(blockInputs)
|
||||||
|
@ -196,7 +200,8 @@
|
||||||
(automation.trigger?.event === "row:update" ||
|
(automation.trigger?.event === "row:update" ||
|
||||||
automation.trigger?.event === "row:save")
|
automation.trigger?.event === "row:save")
|
||||||
) {
|
) {
|
||||||
if (name !== "id" && name !== "revision") return `trigger.row.${name}`
|
let noRowKeywordBindings = ["id", "revision", "oldRow"]
|
||||||
|
if (!noRowKeywordBindings.includes(name)) return `trigger.row.${name}`
|
||||||
}
|
}
|
||||||
/* End special cases for generating custom schemas based on triggers */
|
/* End special cases for generating custom schemas based on triggers */
|
||||||
|
|
||||||
|
@ -372,7 +377,11 @@
|
||||||
|
|
||||||
function getFieldLabel(key, value) {
|
function getFieldLabel(key, value) {
|
||||||
const requiredSuffix = requiredProperties.includes(key) ? "*" : ""
|
const requiredSuffix = requiredProperties.includes(key) ? "*" : ""
|
||||||
return `${value.title || (key === "row" ? "Table" : key)} ${requiredSuffix}`
|
return `${value.title || (key === "row" ? "Row" : key)} ${requiredSuffix}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTestDataRowVisibility(key) {
|
||||||
|
testDataRowVisibility[key] = !testDataRowVisibility[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAttachmentParams(keyValueObj) {
|
function handleAttachmentParams(keyValueObj) {
|
||||||
|
@ -607,20 +616,48 @@
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "row"}
|
{:else if value.customType === "row"}
|
||||||
<RowSelector
|
{#if isTestModal}
|
||||||
value={inputData[key]}
|
<div class="align-horizontally">
|
||||||
meta={inputData["meta"] || {}}
|
<Icon
|
||||||
on:change={e => {
|
name={testDataRowVisibility[key] ? "Remove" : "Add"}
|
||||||
if (e.detail?.key) {
|
hoverable
|
||||||
onChange(e, e.detail.key)
|
on:click={() => toggleTestDataRowVisibility(key)}
|
||||||
} else {
|
/>
|
||||||
onChange(e, key)
|
<Label size="XL">{label}</Label>
|
||||||
}
|
</div>
|
||||||
}}
|
{#if testDataRowVisibility[key]}
|
||||||
{bindings}
|
<RowSelector
|
||||||
{isTestModal}
|
value={inputData[key]}
|
||||||
{isUpdateRow}
|
meta={inputData["meta"] || {}}
|
||||||
/>
|
on:change={e => {
|
||||||
|
if (e.detail?.key) {
|
||||||
|
onChange(e, e.detail.key)
|
||||||
|
} else {
|
||||||
|
onChange(e, key)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{bindings}
|
||||||
|
{isTestModal}
|
||||||
|
{isUpdateRow}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<Divider />
|
||||||
|
{:else}
|
||||||
|
<RowSelector
|
||||||
|
value={inputData[key]}
|
||||||
|
meta={inputData["meta"] || {}}
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail?.key) {
|
||||||
|
onChange(e, e.detail.key)
|
||||||
|
} else {
|
||||||
|
onChange(e, key)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{bindings}
|
||||||
|
{isTestModal}
|
||||||
|
{isUpdateRow}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{:else if value.customType === "webhookUrl"}
|
{:else if value.customType === "webhookUrl"}
|
||||||
<WebhookDisplay
|
<WebhookDisplay
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
|
@ -736,6 +773,12 @@
|
||||||
width: 320px;
|
width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.align-horizontally {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<div class="root">This action doesn't require any settings.</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script>
|
||||||
|
import { Select, Label } from "@budibase/bbui"
|
||||||
|
import { selectedScreen } from "stores/builder"
|
||||||
|
import { findAllMatchingComponents } from "helpers/components"
|
||||||
|
|
||||||
|
export let parameters
|
||||||
|
|
||||||
|
$: modalOptions = getModalOptions($selectedScreen)
|
||||||
|
|
||||||
|
const getModalOptions = screen => {
|
||||||
|
const modalComponents = findAllMatchingComponents(screen.props, component =>
|
||||||
|
component._component.endsWith("/modal")
|
||||||
|
)
|
||||||
|
return modalComponents.map(modal => ({
|
||||||
|
label: modal._instanceName,
|
||||||
|
value: modal._id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Label small>Modal</Label>
|
||||||
|
<Select bind:value={parameters.id} options={modalOptions} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
display: grid;
|
||||||
|
column-gap: var(--spacing-l);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
|
grid-template-columns: 60px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -21,5 +21,7 @@ export { default as ShowNotification } from "./ShowNotification.svelte"
|
||||||
export { default as PromptUser } from "./PromptUser.svelte"
|
export { default as PromptUser } from "./PromptUser.svelte"
|
||||||
export { default as OpenSidePanel } from "./OpenSidePanel.svelte"
|
export { default as OpenSidePanel } from "./OpenSidePanel.svelte"
|
||||||
export { default as CloseSidePanel } from "./CloseSidePanel.svelte"
|
export { default as CloseSidePanel } from "./CloseSidePanel.svelte"
|
||||||
|
export { default as OpenModal } from "./OpenModal.svelte"
|
||||||
|
export { default as CloseModal } from "./CloseModal.svelte"
|
||||||
export { default as ClearRowSelection } from "./ClearRowSelection.svelte"
|
export { default as ClearRowSelection } from "./ClearRowSelection.svelte"
|
||||||
export { default as DownloadFile } from "./DownloadFile.svelte"
|
export { default as DownloadFile } from "./DownloadFile.svelte"
|
||||||
|
|
|
@ -157,6 +157,18 @@
|
||||||
"component": "CloseSidePanel",
|
"component": "CloseSidePanel",
|
||||||
"dependsOnFeature": "sidePanel"
|
"dependsOnFeature": "sidePanel"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Open Modal",
|
||||||
|
"type": "application",
|
||||||
|
"component": "OpenModal",
|
||||||
|
"dependsOnFeature": "modal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Close Modal",
|
||||||
|
"type": "application",
|
||||||
|
"component": "CloseModal",
|
||||||
|
"dependsOnFeature": "modal"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Clear Row Selection",
|
"name": "Clear Row Selection",
|
||||||
"type": "data",
|
"type": "data",
|
||||||
|
|
|
@ -59,7 +59,14 @@
|
||||||
// Build up list of illegal children from ancestors
|
// Build up list of illegal children from ancestors
|
||||||
let illegalChildren = definition.illegalChildren || []
|
let illegalChildren = definition.illegalChildren || []
|
||||||
path.forEach(ancestor => {
|
path.forEach(ancestor => {
|
||||||
if (ancestor._component === `@budibase/standard-components/sidepanel`) {
|
// Sidepanels and modals can be nested anywhere in the component tree, but really they are always rendered at the top level.
|
||||||
|
// Because of this, it doesn't make sense to carry over any parent illegal children to them, so the array is reset here.
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
"@budibase/standard-components/sidepanel",
|
||||||
|
"@budibase/standard-components/modal",
|
||||||
|
].includes(ancestor._component)
|
||||||
|
) {
|
||||||
illegalChildren = []
|
illegalChildren = []
|
||||||
}
|
}
|
||||||
const def = componentStore.getDefinition(ancestor._component)
|
const def = componentStore.getDefinition(ancestor._component)
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
{
|
{
|
||||||
"name": "Layout",
|
"name": "Layout",
|
||||||
"icon": "ClassicGridView",
|
"icon": "ClassicGridView",
|
||||||
"children": ["container", "section", "sidepanel"]
|
"children": ["container", "section", "sidepanel", "modal"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Data",
|
"name": "Data",
|
||||||
|
|
|
@ -125,7 +125,14 @@ export class ScreenStore extends BudiStore {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "@budibase/standard-components/sidepanel") {
|
// Sidepanels and modals can be nested anywhere in the component tree, but really they are always rendered at the top level.
|
||||||
|
// Because of this, it doesn't make sense to carry over any parent illegal children to them, so the array is reset here.
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
"@budibase/standard-components/sidepanel",
|
||||||
|
"@budibase/standard-components/modal",
|
||||||
|
].includes(type)
|
||||||
|
) {
|
||||||
illegalChildren = []
|
illegalChildren = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"continueIfAction": true,
|
"continueIfAction": true,
|
||||||
"showNotificationAction": true,
|
"showNotificationAction": true,
|
||||||
"sidePanel": true,
|
"sidePanel": true,
|
||||||
|
"modal": true,
|
||||||
"skeletonLoader": true
|
"skeletonLoader": true
|
||||||
},
|
},
|
||||||
"typeSupportPresets": {
|
"typeSupportPresets": {
|
||||||
|
@ -6975,7 +6976,7 @@
|
||||||
"name": "Side Panel",
|
"name": "Side Panel",
|
||||||
"icon": "RailRight",
|
"icon": "RailRight",
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"illegalChildren": ["section", "sidepanel"],
|
"illegalChildren": ["section", "sidepanel", "modal"],
|
||||||
"showEmptyState": false,
|
"showEmptyState": false,
|
||||||
"draggable": false,
|
"draggable": false,
|
||||||
"info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action.",
|
"info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action.",
|
||||||
|
@ -6993,6 +6994,52 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"modal": {
|
||||||
|
"name": "Modal",
|
||||||
|
"icon": "MBox",
|
||||||
|
"hasChildren": true,
|
||||||
|
"illegalChildren": ["section", "modal", "sidepanel"],
|
||||||
|
"showEmptyState": false,
|
||||||
|
"draggable": false,
|
||||||
|
"info": "Modals are hidden by default. They will only be revealed when triggered by the 'Open Modal' action.",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"key": "ignoreClicksOutside",
|
||||||
|
"label": "Ignore clicks outside",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"key": "onClose",
|
||||||
|
"label": "On close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Size",
|
||||||
|
"key": "size",
|
||||||
|
"defaultValue": "small",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Small",
|
||||||
|
"value": "small"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Medium",
|
||||||
|
"value": "medium"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Large",
|
||||||
|
"value": "large"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Fullscreen",
|
||||||
|
"value": "fullscreen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"rowexplorer": {
|
"rowexplorer": {
|
||||||
"block": true,
|
"block": true,
|
||||||
"name": "Row Explorer Block",
|
"name": "Row Explorer Block",
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
devToolsEnabled,
|
devToolsEnabled,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
sidePanelStore,
|
sidePanelStore,
|
||||||
|
modalStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
|
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
|
||||||
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
|
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
|
||||||
|
@ -104,10 +105,15 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const handleHashChange = () => {
|
const handleHashChange = () => {
|
||||||
const { open } = $sidePanelStore
|
const { open: sidePanelOpen } = $sidePanelStore
|
||||||
if (open) {
|
if (sidePanelOpen) {
|
||||||
sidePanelStore.actions.close()
|
sidePanelStore.actions.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { open: modalOpen } = $modalStore
|
||||||
|
if (modalOpen) {
|
||||||
|
modalStore.actions.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
window.addEventListener("hashchange", handleHashChange)
|
window.addEventListener("hashchange", handleHashChange)
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
linkable,
|
linkable,
|
||||||
builderStore,
|
builderStore,
|
||||||
sidePanelStore,
|
sidePanelStore,
|
||||||
|
modalStore,
|
||||||
appStore,
|
appStore,
|
||||||
} = sdk
|
} = sdk
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
|
@ -77,6 +78,7 @@
|
||||||
!$builderStore.inBuilder &&
|
!$builderStore.inBuilder &&
|
||||||
$sidePanelStore.open &&
|
$sidePanelStore.open &&
|
||||||
!$sidePanelStore.ignoreClicksOutside
|
!$sidePanelStore.ignoreClicksOutside
|
||||||
|
|
||||||
$: screenId = $builderStore.inBuilder
|
$: screenId = $builderStore.inBuilder
|
||||||
? `${$builderStore.screen?._id}-screen`
|
? `${$builderStore.screen?._id}-screen`
|
||||||
: "screen"
|
: "screen"
|
||||||
|
@ -198,6 +200,7 @@
|
||||||
const handleClickLink = () => {
|
const handleClickLink = () => {
|
||||||
mobileOpen = false
|
mobileOpen = false
|
||||||
sidePanelStore.actions.close()
|
sidePanelStore.actions.close()
|
||||||
|
modalStore.actions.close()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
const { linkable, styleable, builderStore, sidePanelStore } =
|
const { linkable, styleable, builderStore, sidePanelStore, modalStore } =
|
||||||
getContext("sdk")
|
getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
|
@ -29,6 +29,11 @@
|
||||||
// overrides the color when it's passed as inline style.
|
// overrides the color when it's passed as inline style.
|
||||||
$: styles = enrichStyles($component.styles, color)
|
$: styles = enrichStyles($component.styles, color)
|
||||||
|
|
||||||
|
const handleUrlChange = () => {
|
||||||
|
sidePanelStore.actions.close()
|
||||||
|
modalStore.actions.close()
|
||||||
|
}
|
||||||
|
|
||||||
const getSanitizedUrl = (url, externalLink, newTab) => {
|
const getSanitizedUrl = (url, externalLink, newTab) => {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return externalLink || newTab ? "#/" : "/"
|
return externalLink || newTab ? "#/" : "/"
|
||||||
|
@ -109,7 +114,7 @@
|
||||||
class:italic
|
class:italic
|
||||||
class:underline
|
class:underline
|
||||||
class="align--{align || 'left'} size--{size || 'M'}"
|
class="align--{align || 'left'} size--{size || 'M'}"
|
||||||
on:click={sidePanelStore.actions.close}
|
on:click={handleUrlChange}
|
||||||
>
|
>
|
||||||
{componentText}
|
{componentText}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import { Modal, Icon } from "@budibase/bbui"
|
||||||
|
|
||||||
|
const component = getContext("component")
|
||||||
|
const { styleable, modalStore, builderStore, dndIsDragging } =
|
||||||
|
getContext("sdk")
|
||||||
|
|
||||||
|
export let onClose
|
||||||
|
export let ignoreClicksOutside
|
||||||
|
export let size
|
||||||
|
let modal
|
||||||
|
|
||||||
|
// Open modal automatically in builder
|
||||||
|
$: {
|
||||||
|
if ($builderStore.inBuilder) {
|
||||||
|
if (
|
||||||
|
$component.inSelectedPath &&
|
||||||
|
$modalStore.contentId !== $component.id
|
||||||
|
) {
|
||||||
|
modalStore.actions.open($component.id)
|
||||||
|
} else if (
|
||||||
|
!$component.inSelectedPath &&
|
||||||
|
$modalStore.contentId === $component.id &&
|
||||||
|
!$dndIsDragging
|
||||||
|
) {
|
||||||
|
modalStore.actions.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: open = $modalStore.contentId === $component.id
|
||||||
|
|
||||||
|
const handleModalClose = async () => {
|
||||||
|
if (onClose) {
|
||||||
|
await onClose()
|
||||||
|
}
|
||||||
|
modalStore.actions.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpen = (open, modal) => {
|
||||||
|
if (!modal) return
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
modal.show()
|
||||||
|
} else {
|
||||||
|
modal.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: handleOpen(open, modal)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Conditional displaying in the builder is necessary otherwise previews don't update properly upon component deletion -->
|
||||||
|
{#if !$builderStore.inBuilder || open}
|
||||||
|
<Modal
|
||||||
|
on:cancel={handleModalClose}
|
||||||
|
bind:this={modal}
|
||||||
|
disableCancel={$builderStore.inBuilder}
|
||||||
|
zIndex={2}
|
||||||
|
>
|
||||||
|
<div use:styleable={$component.styles} class={`modal-content ${size}`}>
|
||||||
|
<div class="modal-header">
|
||||||
|
<Icon
|
||||||
|
color="var(--spectrum-global-color-gray-800)"
|
||||||
|
name="Close"
|
||||||
|
hoverable
|
||||||
|
on:click={handleModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-main">
|
||||||
|
<div class="modal-main-inner">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 12px 0px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
width: 400px;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
width: 600px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
width: 800px;
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen {
|
||||||
|
width: calc(100vw - 80px);
|
||||||
|
min-height: calc(100vh - 80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-main {
|
||||||
|
padding: 0 40px;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-main :global(.component > *) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-main-inner {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-main-inner:empty {
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 2px dashed var(--spectrum-global-color-gray-400);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -31,41 +31,23 @@
|
||||||
|
|
||||||
let schema
|
let schema
|
||||||
|
|
||||||
$: formattedFields = convertOldFieldFormat(fields)
|
|
||||||
$: fieldsOrDefault = getDefaultFields(formattedFields, schema)
|
|
||||||
$: fetchSchema(dataSource)
|
$: fetchSchema(dataSource)
|
||||||
$: id = $component.id
|
$: id = $component.id
|
||||||
// We could simply spread $$props into the inner form and append our
|
$: formattedFields = convertOldFieldFormat(fields)
|
||||||
// additions, but that would create svelte warnings about unused props and
|
$: fieldsOrDefault = getDefaultFields(formattedFields, schema)
|
||||||
// make maintenance in future more confusing as we typically always have a
|
$: buttonsOrDefault =
|
||||||
// proper mapping of schema settings to component exports, without having to
|
buttons ||
|
||||||
// search multiple files
|
Utils.buildFormBlockButtonConfig({
|
||||||
$: innerProps = {
|
_id: id,
|
||||||
dataSource,
|
showDeleteButton,
|
||||||
actionUrl,
|
showSaveButton,
|
||||||
actionType,
|
saveButtonLabel,
|
||||||
size,
|
deleteButtonLabel,
|
||||||
disabled,
|
notificationOverride,
|
||||||
fields: fieldsOrDefault,
|
actionType,
|
||||||
title,
|
actionUrl,
|
||||||
description,
|
dataSource,
|
||||||
schema,
|
})
|
||||||
notificationOverride,
|
|
||||||
buttons:
|
|
||||||
buttons ||
|
|
||||||
Utils.buildFormBlockButtonConfig({
|
|
||||||
_id: id,
|
|
||||||
showDeleteButton,
|
|
||||||
showSaveButton,
|
|
||||||
saveButtonLabel,
|
|
||||||
deleteButtonLabel,
|
|
||||||
notificationOverride,
|
|
||||||
actionType,
|
|
||||||
actionUrl,
|
|
||||||
dataSource,
|
|
||||||
}),
|
|
||||||
buttonPosition: buttons ? buttonPosition : "top",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide additional data context for live binding eval
|
// Provide additional data context for live binding eval
|
||||||
export const getAdditionalDataContext = () => {
|
export const getAdditionalDataContext = () => {
|
||||||
|
@ -123,5 +105,18 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FormBlockWrapper {actionType} {dataSource} {rowId} {noRowsMessage}>
|
<FormBlockWrapper {actionType} {dataSource} {rowId} {noRowsMessage}>
|
||||||
<InnerFormBlock {...innerProps} />
|
<InnerFormBlock
|
||||||
|
{dataSource}
|
||||||
|
{actionUrl}
|
||||||
|
{actionType}
|
||||||
|
{size}
|
||||||
|
{disabled}
|
||||||
|
fields={fieldsOrDefault}
|
||||||
|
{title}
|
||||||
|
{description}
|
||||||
|
{schema}
|
||||||
|
{notificationOverride}
|
||||||
|
buttons={buttonsOrDefault}
|
||||||
|
buttonPosition={buttons ? buttonPosition : "top"}
|
||||||
|
/>
|
||||||
</FormBlockWrapper>
|
</FormBlockWrapper>
|
||||||
|
|
|
@ -91,15 +91,13 @@
|
||||||
{#if description}
|
{#if description}
|
||||||
<BlockComponent type="text" props={{ text: description }} order={1} />
|
<BlockComponent type="text" props={{ text: description }} order={1} />
|
||||||
{/if}
|
{/if}
|
||||||
{#key fields}
|
<BlockComponent type="container">
|
||||||
<BlockComponent type="container">
|
<div class="form-block fields" class:mobile={$context.device.mobile}>
|
||||||
<div class="form-block fields" class:mobile={$context.device.mobile}>
|
{#each fields as field, idx}
|
||||||
{#each fields as field, idx}
|
<FormBlockComponent {field} {schema} order={idx} />
|
||||||
<FormBlockComponent {field} {schema} order={idx} />
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
</BlockComponent>
|
||||||
</BlockComponent>
|
|
||||||
{/key}
|
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
{#if buttonPosition === "bottom"}
|
{#if buttonPosition === "bottom"}
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
|
|
|
@ -37,6 +37,7 @@ export { default as markdownviewer } from "./MarkdownViewer.svelte"
|
||||||
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
||||||
export { default as grid } from "./Grid.svelte"
|
export { default as grid } from "./Grid.svelte"
|
||||||
export { default as sidepanel } from "./SidePanel.svelte"
|
export { default as sidepanel } from "./SidePanel.svelte"
|
||||||
|
export { default as modal } from "./Modal.svelte"
|
||||||
export { default as gridblock } from "./GridBlock.svelte"
|
export { default as gridblock } from "./GridBlock.svelte"
|
||||||
export * from "./charts"
|
export * from "./charts"
|
||||||
export * from "./forms"
|
export * from "./forms"
|
||||||
|
|
|
@ -57,7 +57,9 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nextState.indicators[idx].visible =
|
nextState.indicators[idx].visible =
|
||||||
nextState.indicators[idx].insideSidePanel || entries[0].isIntersecting
|
nextState.indicators[idx].insideModal ||
|
||||||
|
nextState.indicators[idx].insideSidePanel ||
|
||||||
|
entries[0].isIntersecting
|
||||||
if (++callbackCount === observers.length) {
|
if (++callbackCount === observers.length) {
|
||||||
state = nextState
|
state = nextState
|
||||||
updating = false
|
updating = false
|
||||||
|
@ -139,6 +141,7 @@
|
||||||
height: elBounds.height + 4,
|
height: elBounds.height + 4,
|
||||||
visible: false,
|
visible: false,
|
||||||
insideSidePanel: !!child.closest(".side-panel"),
|
insideSidePanel: !!child.closest(".side-panel"),
|
||||||
|
insideModal: !!child.closest(".modal-content"),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
currentRole,
|
currentRole,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
sidePanelStore,
|
sidePanelStore,
|
||||||
|
modalStore,
|
||||||
dndIsDragging,
|
dndIsDragging,
|
||||||
confirmationStore,
|
confirmationStore,
|
||||||
roleStore,
|
roleStore,
|
||||||
|
@ -53,6 +54,7 @@ export default {
|
||||||
componentStore,
|
componentStore,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
sidePanelStore,
|
sidePanelStore,
|
||||||
|
modalStore,
|
||||||
dndIsDragging,
|
dndIsDragging,
|
||||||
currentRole,
|
currentRole,
|
||||||
confirmationStore,
|
confirmationStore,
|
||||||
|
|
|
@ -27,6 +27,7 @@ export {
|
||||||
dndIsDragging,
|
dndIsDragging,
|
||||||
} from "./dnd"
|
} from "./dnd"
|
||||||
export { sidePanelStore } from "./sidePanel"
|
export { sidePanelStore } from "./sidePanel"
|
||||||
|
export { modalStore } from "./modal"
|
||||||
export { hoverStore } from "./hover"
|
export { hoverStore } from "./hover"
|
||||||
|
|
||||||
// Context stores are layered and duplicated, so it is not a singleton
|
// Context stores are layered and duplicated, so it is not a singleton
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
export const createModalStore = () => {
|
||||||
|
const initialState = {
|
||||||
|
contentId: null,
|
||||||
|
}
|
||||||
|
const store = writable(initialState)
|
||||||
|
|
||||||
|
const open = id => {
|
||||||
|
store.update(state => {
|
||||||
|
state.contentId = id
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
store.update(state => {
|
||||||
|
state.contentId = null
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: {
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modalStore = createModalStore()
|
|
@ -12,6 +12,7 @@ import {
|
||||||
uploadStore,
|
uploadStore,
|
||||||
rowSelectionStore,
|
rowSelectionStore,
|
||||||
sidePanelStore,
|
sidePanelStore,
|
||||||
|
modalStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { ActionTypes } from "constants"
|
import { ActionTypes } from "constants"
|
||||||
|
@ -436,6 +437,17 @@ const closeSidePanelHandler = () => {
|
||||||
sidePanelStore.actions.close()
|
sidePanelStore.actions.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openModalHandler = action => {
|
||||||
|
const { id } = action.parameters
|
||||||
|
if (id) {
|
||||||
|
modalStore.actions.open(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModalHandler = () => {
|
||||||
|
modalStore.actions.close()
|
||||||
|
}
|
||||||
|
|
||||||
const downloadFileHandler = async action => {
|
const downloadFileHandler = async action => {
|
||||||
const { url, fileName } = action.parameters
|
const { url, fileName } = action.parameters
|
||||||
try {
|
try {
|
||||||
|
@ -499,6 +511,8 @@ const handlerMap = {
|
||||||
["Prompt User"]: promptUserHandler,
|
["Prompt User"]: promptUserHandler,
|
||||||
["Open Side Panel"]: openSidePanelHandler,
|
["Open Side Panel"]: openSidePanelHandler,
|
||||||
["Close Side Panel"]: closeSidePanelHandler,
|
["Close Side Panel"]: closeSidePanelHandler,
|
||||||
|
["Open Modal"]: openModalHandler,
|
||||||
|
["Close Modal"]: closeModalHandler,
|
||||||
["Download File"]: downloadFileHandler,
|
["Download File"]: downloadFileHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -161,6 +161,9 @@ export const buildFormBlockButtonConfig = props => {
|
||||||
{
|
{
|
||||||
"##eventHandlerType": "Close Side Panel",
|
"##eventHandlerType": "Close Side Panel",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Modal",
|
||||||
|
},
|
||||||
// Clear a create form once submitted
|
// Clear a create form once submitted
|
||||||
...(actionType !== "Create"
|
...(actionType !== "Create"
|
||||||
? []
|
? []
|
||||||
|
|
|
@ -39,9 +39,10 @@ export async function handleRequest<T extends Operation>(
|
||||||
|
|
||||||
export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
const { _id, ...rowData } = ctx.request.body
|
|
||||||
|
|
||||||
|
const { _id, ...rowData } = ctx.request.body
|
||||||
const table = await sdk.tables.getTable(tableId)
|
const table = await sdk.tables.getTable(tableId)
|
||||||
|
|
||||||
const { row: dataToUpdate } = await inputProcessing(
|
const { row: dataToUpdate } = await inputProcessing(
|
||||||
ctx.user?._id,
|
ctx.user?._id,
|
||||||
cloneDeep(table),
|
cloneDeep(table),
|
||||||
|
@ -79,6 +80,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||||
...response,
|
...response,
|
||||||
row: enrichedRow,
|
row: enrichedRow,
|
||||||
table,
|
table,
|
||||||
|
oldRow: beforeRow,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,13 +55,13 @@ export async function patch(
|
||||||
return save(ctx)
|
return save(ctx)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { row, table } = await pickApi(tableId).patch(ctx)
|
const { row, table, oldRow } = await pickApi(tableId).patch(ctx)
|
||||||
if (!row) {
|
if (!row) {
|
||||||
ctx.throw(404, "Row not found")
|
ctx.throw(404, "Row not found")
|
||||||
}
|
}
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.eventEmitter &&
|
ctx.eventEmitter &&
|
||||||
ctx.eventEmitter.emitRow(`row:update`, appId, row, table)
|
ctx.eventEmitter.emitRow(`row:update`, appId, row, table, oldRow)
|
||||||
ctx.message = `${table.name} updated successfully.`
|
ctx.message = `${table.name} updated successfully.`
|
||||||
ctx.body = row
|
ctx.body = row
|
||||||
gridSocket?.emitRowUpdate(ctx, row)
|
gridSocket?.emitRowUpdate(ctx, row)
|
||||||
|
|
|
@ -85,13 +85,15 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||||
// the row has been updated, need to put it into the ctx
|
// the row has been updated, need to put it into the ctx
|
||||||
ctx.request.body = row as any
|
ctx.request.body = row as any
|
||||||
await userController.updateMetadata(ctx as any)
|
await userController.updateMetadata(ctx as any)
|
||||||
return { row: ctx.body as Row, table }
|
return { row: ctx.body as Row, table, oldRow }
|
||||||
}
|
}
|
||||||
|
|
||||||
return finaliseRow(table, row, {
|
const result = await finaliseRow(table, row, {
|
||||||
oldTable: dbTable,
|
oldTable: dbTable,
|
||||||
updateFormula: true,
|
updateFormula: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return { ...result, oldRow }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function find(ctx: UserCtx): Promise<Row> {
|
export async function find(ctx: UserCtx): Promise<Row> {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { events } from "@budibase/backend-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { Automation } from "@budibase/types"
|
import { Automation } from "@budibase/types"
|
||||||
import { mocks } from "@budibase/backend-core/tests"
|
import { mocks } from "@budibase/backend-core/tests"
|
||||||
|
import { FilterConditions } from "../../../automations/steps/filter"
|
||||||
|
|
||||||
const MAX_RETRIES = 4
|
const MAX_RETRIES = 4
|
||||||
let {
|
let {
|
||||||
|
@ -21,6 +22,7 @@ let {
|
||||||
automationTrigger,
|
automationTrigger,
|
||||||
automationStep,
|
automationStep,
|
||||||
collectAutomation,
|
collectAutomation,
|
||||||
|
filterAutomation,
|
||||||
} = setup.structures
|
} = setup.structures
|
||||||
|
|
||||||
describe("/automations", () => {
|
describe("/automations", () => {
|
||||||
|
@ -155,7 +157,12 @@ describe("/automations", () => {
|
||||||
automation.appId = config.appId
|
automation.appId = config.appId
|
||||||
automation = await config.createAutomation(automation)
|
automation = await config.createAutomation(automation)
|
||||||
await setup.delay(500)
|
await setup.delay(500)
|
||||||
const res = await testAutomation(config, automation)
|
const res = await testAutomation(config, automation, {
|
||||||
|
row: {
|
||||||
|
name: "Test",
|
||||||
|
description: "TEST",
|
||||||
|
},
|
||||||
|
})
|
||||||
expect(events.automation.tested).toHaveBeenCalledTimes(1)
|
expect(events.automation.tested).toHaveBeenCalledTimes(1)
|
||||||
// this looks a bit mad but we don't actually have a way to wait for a response from the automation to
|
// this looks a bit mad but we don't actually have a way to wait for a response from the automation to
|
||||||
// know that it has finished all of its actions - this is currently the best way
|
// know that it has finished all of its actions - this is currently the best way
|
||||||
|
@ -436,4 +443,38 @@ describe("/automations", () => {
|
||||||
expect(res).toEqual(true)
|
expect(res).toEqual(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Update Row Old / New Row comparison", () => {
|
||||||
|
it.each([
|
||||||
|
{ oldCity: "asdsadsadsad", newCity: "new" },
|
||||||
|
{ oldCity: "Belfast", newCity: "Belfast" },
|
||||||
|
])(
|
||||||
|
"triggers an update row automation and compares new to old rows with old city '%s' and new city '%s'",
|
||||||
|
async ({ oldCity, newCity }) => {
|
||||||
|
const expectedResult = oldCity === newCity
|
||||||
|
|
||||||
|
let table = await config.createTable()
|
||||||
|
|
||||||
|
let automation = await filterAutomation()
|
||||||
|
automation.definition.trigger.inputs.tableId = table._id
|
||||||
|
automation.definition.steps[0].inputs = {
|
||||||
|
condition: FilterConditions.EQUAL,
|
||||||
|
field: "{{ trigger.row.City }}",
|
||||||
|
value: "{{ trigger.oldRow.City }}",
|
||||||
|
}
|
||||||
|
automation.appId = config.appId!
|
||||||
|
automation = await config.createAutomation(automation)
|
||||||
|
let triggerInputs = {
|
||||||
|
oldRow: {
|
||||||
|
City: oldCity,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
City: newCity,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const res = await testAutomation(config, automation, triggerInputs)
|
||||||
|
expect(res.body.steps[1].outputs.result).toEqual(expectedResult)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
|
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
|
||||||
|
|
||||||
import tk from "timekeeper"
|
import tk from "timekeeper"
|
||||||
|
import emitter from "../../../../src/events"
|
||||||
import { outputProcessing } from "../../../utilities/rowProcessor"
|
import { outputProcessing } from "../../../utilities/rowProcessor"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import { context, InternalTable, tenancy } from "@budibase/backend-core"
|
import { context, InternalTable, tenancy } from "@budibase/backend-core"
|
||||||
|
@ -24,6 +25,7 @@ import {
|
||||||
StaticQuotaName,
|
StaticQuotaName,
|
||||||
Table,
|
Table,
|
||||||
TableSourceType,
|
TableSourceType,
|
||||||
|
UpdatedRowEventEmitter,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { generator, mocks } from "@budibase/backend-core/tests"
|
import { generator, mocks } from "@budibase/backend-core/tests"
|
||||||
import _, { merge } from "lodash"
|
import _, { merge } from "lodash"
|
||||||
|
@ -31,6 +33,28 @@ import * as uuid from "uuid"
|
||||||
|
|
||||||
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
|
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
|
||||||
tk.freeze(timestamp)
|
tk.freeze(timestamp)
|
||||||
|
interface WaitOptions {
|
||||||
|
name: string
|
||||||
|
matchFn?: (event: any) => boolean
|
||||||
|
}
|
||||||
|
async function waitForEvent(
|
||||||
|
opts: WaitOptions,
|
||||||
|
callback: () => Promise<void>
|
||||||
|
): Promise<any> {
|
||||||
|
const p = new Promise((resolve: any) => {
|
||||||
|
const listener = (event: any) => {
|
||||||
|
if (opts.matchFn && !opts.matchFn(event)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve(event)
|
||||||
|
emitter.off(opts.name, listener)
|
||||||
|
}
|
||||||
|
emitter.on(opts.name, listener)
|
||||||
|
})
|
||||||
|
|
||||||
|
await callback()
|
||||||
|
return await p
|
||||||
|
}
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
["internal", undefined],
|
["internal", undefined],
|
||||||
|
@ -608,6 +632,31 @@ describe.each([
|
||||||
await assertRowUsage(rowUsage)
|
await assertRowUsage(rowUsage)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should update only the fields that are supplied and emit the correct oldRow", async () => {
|
||||||
|
let beforeRow = await config.api.row.save(table._id!, {
|
||||||
|
name: "test",
|
||||||
|
description: "test",
|
||||||
|
})
|
||||||
|
const opts = {
|
||||||
|
name: "row:update",
|
||||||
|
matchFn: (event: UpdatedRowEventEmitter) =>
|
||||||
|
event.row._id === beforeRow._id,
|
||||||
|
}
|
||||||
|
const event = await waitForEvent(opts, async () => {
|
||||||
|
await config.api.row.patch(table._id!, {
|
||||||
|
_id: beforeRow._id!,
|
||||||
|
_rev: beforeRow._rev!,
|
||||||
|
tableId: table._id!,
|
||||||
|
name: "Updated Name",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(event.oldRow).toBeDefined()
|
||||||
|
expect(event.oldRow.name).toEqual("test")
|
||||||
|
expect(event.row.name).toEqual("Updated Name")
|
||||||
|
expect(event.oldRow.description).toEqual(beforeRow.description)
|
||||||
|
expect(event.row.description).toEqual(beforeRow.description)
|
||||||
|
})
|
||||||
it("should throw an error when given improper types", async () => {
|
it("should throw an error when given improper types", async () => {
|
||||||
const existing = await config.api.row.save(table._id!, {})
|
const existing = await config.api.row.save(table._id!, {})
|
||||||
const rowUsage = await getRowUsage()
|
const rowUsage = await getRowUsage()
|
||||||
|
|
|
@ -158,15 +158,16 @@ export const getDB = () => {
|
||||||
return context.getAppDB()
|
return context.getAppDB()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const testAutomation = async (config: any, automation: any) => {
|
export const testAutomation = async (
|
||||||
|
config: any,
|
||||||
|
automation: any,
|
||||||
|
triggerInputs: any
|
||||||
|
) => {
|
||||||
return runRequest(automation.appId, async () => {
|
return runRequest(automation.appId, async () => {
|
||||||
return await config.request
|
return await config.request
|
||||||
.post(`/api/automations/${automation._id}/test`)
|
.post(`/api/automations/${automation._id}/test`)
|
||||||
.send({
|
.send({
|
||||||
row: {
|
...triggerInputs,
|
||||||
name: "Test",
|
|
||||||
description: "TEST",
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
|
|
|
@ -27,10 +27,17 @@ export const definition: AutomationTriggerSchema = {
|
||||||
},
|
},
|
||||||
outputs: {
|
outputs: {
|
||||||
properties: {
|
properties: {
|
||||||
row: {
|
oldRow: {
|
||||||
type: AutomationIOType.OBJECT,
|
type: AutomationIOType.OBJECT,
|
||||||
customType: AutomationCustomIOType.ROW,
|
customType: AutomationCustomIOType.ROW,
|
||||||
description: "The row that was updated",
|
description: "The row that was updated",
|
||||||
|
title: "Old Row",
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
type: AutomationIOType.OBJECT,
|
||||||
|
customType: AutomationCustomIOType.ROW,
|
||||||
|
description: "The row before it was updated",
|
||||||
|
title: "Row",
|
||||||
},
|
},
|
||||||
id: {
|
id: {
|
||||||
type: AutomationIOType.STRING,
|
type: AutomationIOType.STRING,
|
||||||
|
|
|
@ -8,7 +8,13 @@ import { checkTestFlag } from "../utilities/redis"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { context, db as dbCore } from "@budibase/backend-core"
|
import { context, db as dbCore } from "@budibase/backend-core"
|
||||||
import { Automation, Row, AutomationData, AutomationJob } from "@budibase/types"
|
import {
|
||||||
|
Automation,
|
||||||
|
Row,
|
||||||
|
AutomationData,
|
||||||
|
AutomationJob,
|
||||||
|
UpdatedRowEventEmitter,
|
||||||
|
} from "@budibase/types"
|
||||||
import { executeInThread } from "../threads/automation"
|
import { executeInThread } from "../threads/automation"
|
||||||
|
|
||||||
export const TRIGGER_DEFINITIONS = definitions
|
export const TRIGGER_DEFINITIONS = definitions
|
||||||
|
@ -65,7 +71,7 @@ async function queueRelevantRowAutomations(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter.on("row:save", async function (event) {
|
emitter.on("row:save", async function (event: UpdatedRowEventEmitter) {
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
if (!event || !event.row || !event.row.tableId) {
|
if (!event || !event.row || !event.row.tableId) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -13,8 +13,14 @@ import { Table, Row } from "@budibase/types"
|
||||||
* This is specifically quite important for template strings used in automations.
|
* This is specifically quite important for template strings used in automations.
|
||||||
*/
|
*/
|
||||||
class BudibaseEmitter extends EventEmitter {
|
class BudibaseEmitter extends EventEmitter {
|
||||||
emitRow(eventName: string, appId: string, row: Row, table?: Table) {
|
emitRow(
|
||||||
rowEmission({ emitter: this, eventName, appId, row, table })
|
eventName: string,
|
||||||
|
appId: string,
|
||||||
|
row: Row,
|
||||||
|
table?: Table,
|
||||||
|
oldRow?: Row
|
||||||
|
) {
|
||||||
|
rowEmission({ emitter: this, eventName, appId, row, table, oldRow })
|
||||||
}
|
}
|
||||||
|
|
||||||
emitTable(eventName: string, appId: string, table?: Table) {
|
emitTable(eventName: string, appId: string, table?: Table) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ type BBEventOpts = {
|
||||||
appId: string
|
appId: string
|
||||||
table?: Table
|
table?: Table
|
||||||
row?: Row
|
row?: Row
|
||||||
|
oldRow?: Row
|
||||||
metadata?: any
|
metadata?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ type BBEvent = {
|
||||||
appId: string
|
appId: string
|
||||||
tableId?: string
|
tableId?: string
|
||||||
row?: Row
|
row?: Row
|
||||||
|
oldRow?: Row
|
||||||
table?: BBEventTable
|
table?: BBEventTable
|
||||||
id?: string
|
id?: string
|
||||||
revision?: string
|
revision?: string
|
||||||
|
@ -31,9 +33,11 @@ export function rowEmission({
|
||||||
row,
|
row,
|
||||||
table,
|
table,
|
||||||
metadata,
|
metadata,
|
||||||
|
oldRow,
|
||||||
}: BBEventOpts) {
|
}: BBEventOpts) {
|
||||||
let event: BBEvent = {
|
let event: BBEvent = {
|
||||||
row,
|
row,
|
||||||
|
oldRow,
|
||||||
appId,
|
appId,
|
||||||
tableId: row?.tableId,
|
tableId: row?.tableId,
|
||||||
}
|
}
|
||||||
|
|
|
@ -359,6 +359,36 @@ export function collectAutomation(tableId?: string): Automation {
|
||||||
return automation as Automation
|
return automation as Automation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function filterAutomation(tableId?: string): Automation {
|
||||||
|
const automation: any = {
|
||||||
|
name: "looping",
|
||||||
|
type: "automation",
|
||||||
|
definition: {
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: "b",
|
||||||
|
type: "ACTION",
|
||||||
|
internal: true,
|
||||||
|
stepId: AutomationActionStepId.FILTER,
|
||||||
|
inputs: {},
|
||||||
|
schema: BUILTIN_ACTION_DEFINITIONS.EXECUTE_SCRIPT.schema,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
trigger: {
|
||||||
|
id: "a",
|
||||||
|
type: "TRIGGER",
|
||||||
|
event: "row:save",
|
||||||
|
stepId: AutomationTriggerStepId.ROW_SAVED,
|
||||||
|
inputs: {
|
||||||
|
tableId,
|
||||||
|
},
|
||||||
|
schema: TRIGGER_DEFINITIONS.ROW_SAVED.schema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return automation as Automation
|
||||||
|
}
|
||||||
|
|
||||||
export function basicAutomationResults(
|
export function basicAutomationResults(
|
||||||
automationId: string
|
automationId: string
|
||||||
): AutomationResults {
|
): AutomationResults {
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { Document } from "../document"
|
||||||
import { EventEmitter } from "events"
|
import { EventEmitter } from "events"
|
||||||
import { User } from "../global"
|
import { User } from "../global"
|
||||||
import { ReadStream } from "fs"
|
import { ReadStream } from "fs"
|
||||||
|
import { Row } from "./row"
|
||||||
|
import { Table } from "./table"
|
||||||
|
|
||||||
export enum AutomationIOType {
|
export enum AutomationIOType {
|
||||||
OBJECT = "object",
|
OBJECT = "object",
|
||||||
|
@ -252,3 +254,10 @@ export type BucketedContent = AutomationAttachmentContent & {
|
||||||
bucket: string
|
bucket: string
|
||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdatedRowEventEmitter = {
|
||||||
|
row: Row
|
||||||
|
oldRow: Row
|
||||||
|
table: Table
|
||||||
|
appId: string
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue