Merge branch 'develop' of github.com:Budibase/budibase into feature/table-fetching-frontend
This commit is contained in:
commit
deffe9d3d4
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.7.6",
|
||||
"version": "2.7.7-alpha.2",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/backend-core",
|
||||
|
@ -31,4 +31,4 @@
|
|||
"loadEnvFiles": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
import * as google from "../sso/google"
|
||||
import { Cookie } from "../../../constants"
|
||||
import { clearCookie, getCookie } from "../../../utils"
|
||||
import * as configs from "../../../configs"
|
||||
import { BBContext, SSOProfile } from "@budibase/types"
|
||||
import * as cache from "../../../cache"
|
||||
import * as utils from "../../../utils"
|
||||
import { UserCtx, SSOProfile } from "@budibase/types"
|
||||
import { ssoSaveUserNoOp } from "../sso/sso"
|
||||
import { cache, utils } from "../../../"
|
||||
|
||||
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||
|
||||
type Passport = {
|
||||
|
@ -22,7 +23,7 @@ async function fetchGoogleCreds() {
|
|||
|
||||
export async function preAuth(
|
||||
passport: Passport,
|
||||
ctx: BBContext,
|
||||
ctx: UserCtx,
|
||||
next: Function
|
||||
) {
|
||||
// get the relevant config
|
||||
|
@ -49,7 +50,7 @@ export async function preAuth(
|
|||
|
||||
export async function postAuth(
|
||||
passport: Passport,
|
||||
ctx: BBContext,
|
||||
ctx: UserCtx,
|
||||
next: Function
|
||||
) {
|
||||
// get the relevant config
|
||||
|
@ -57,7 +58,7 @@ export async function postAuth(
|
|||
const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
|
||||
|
||||
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
||||
const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth)
|
||||
const authStateCookie = utils.getCookie(ctx, Cookie.DatasourceAuth)
|
||||
|
||||
return passport.authenticate(
|
||||
new GoogleStrategy(
|
||||
|
@ -72,7 +73,7 @@ export async function postAuth(
|
|||
_profile: SSOProfile,
|
||||
done: Function
|
||||
) => {
|
||||
clearCookie(ctx, Cookie.DatasourceAuth)
|
||||
utils.clearCookie(ctx, Cookie.DatasourceAuth)
|
||||
done(null, { accessToken, refreshToken })
|
||||
}
|
||||
),
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
Modal,
|
||||
notifications,
|
||||
Icon,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
} from "@budibase/bbui"
|
||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
|
@ -306,6 +308,11 @@
|
|||
drawer.hide()
|
||||
}
|
||||
|
||||
function canShowField(key, value) {
|
||||
const dependsOn = value.dependsOn
|
||||
return !dependsOn || !!inputData[dependsOn]
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await environment.loadVariables()
|
||||
|
@ -317,210 +324,233 @@
|
|||
|
||||
<div class="fields">
|
||||
{#each deprecatedSchemaProperties as [key, value]}
|
||||
<div class="block-field">
|
||||
{#if key !== "fields"}
|
||||
<Label
|
||||
tooltip={value.title === "Binding / Value"
|
||||
? "If using the String input type, please use a comma or newline separated string"
|
||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||
>
|
||||
{/if}
|
||||
{#if value.type === "string" && value.enum}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
placeholder={false}
|
||||
options={value.enum}
|
||||
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
||||
/>
|
||||
{:else if value.type === "json"}
|
||||
<Editor
|
||||
editorHeight="250"
|
||||
editorWidth="448"
|
||||
mode="json"
|
||||
value={inputData[key]?.value}
|
||||
on:change={e => {
|
||||
/**
|
||||
* TODO - Remove after November 2023
|
||||
* *******************************
|
||||
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||
* and the new JSON body.
|
||||
*/
|
||||
delete inputData.value1
|
||||
delete inputData.value2
|
||||
delete inputData.value3
|
||||
delete inputData.value4
|
||||
delete inputData.value5
|
||||
/***********************/
|
||||
onChange(e, key)
|
||||
}}
|
||||
/>
|
||||
{:else if value.customType === "column"}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
options={Object.keys(table?.schema || {})}
|
||||
/>
|
||||
{:else if value.customType === "filters"}
|
||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
|
||||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||
Save
|
||||
</Button>
|
||||
<FilterDrawer
|
||||
slot="body"
|
||||
{filters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
panel={AutomationBindingPanel}
|
||||
fillWidth
|
||||
on:change={e => (tempFilters = e.detail)}
|
||||
/>
|
||||
</Drawer>
|
||||
{:else if value.customType === "password"}
|
||||
<Input
|
||||
type="password"
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "email"}
|
||||
{#if isTestModal}
|
||||
<ModalBindableInput
|
||||
title={value.title}
|
||||
value={inputData[key]}
|
||||
panel={AutomationBindingPanel}
|
||||
type="email"
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
fillWidth
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableInput
|
||||
fillWidth
|
||||
title={value.title}
|
||||
panel={AutomationBindingPanel}
|
||||
type="email"
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
allowJS={false}
|
||||
updateOnChange={false}
|
||||
drawerLeft="260px"
|
||||
/>
|
||||
{#if canShowField(key, value)}
|
||||
<div class="block-field">
|
||||
{#if key !== "fields" && value.type !== "boolean"}
|
||||
<Label
|
||||
tooltip={value.title === "Binding / Value"
|
||||
? "If using the String input type, please use a comma or newline separated string"
|
||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||
>
|
||||
{/if}
|
||||
{:else if value.customType === "query"}
|
||||
<QuerySelector
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "cron"}
|
||||
<CronBuilder on:change={e => onChange(e, key)} value={inputData[key]} />
|
||||
{:else if value.customType === "queryParams"}
|
||||
<QueryParamSelector
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
{bindings}
|
||||
/>
|
||||
{:else if value.customType === "table"}
|
||||
<TableSelector
|
||||
{isTrigger}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
/>
|
||||
{:else if value.customType === "row"}
|
||||
<RowSelector
|
||||
{block}
|
||||
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}
|
||||
/>
|
||||
{:else if value.customType === "webhookUrl"}
|
||||
<WebhookDisplay
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "fields"}
|
||||
<FieldSelector
|
||||
{block}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
{isTestModal}
|
||||
/>
|
||||
{:else if value.customType === "triggerSchema"}
|
||||
<SchemaSetup on:change={e => onChange(e, key)} value={inputData[key]} />
|
||||
{:else if value.customType === "code"}
|
||||
<CodeEditorModal>
|
||||
<CodeEditor
|
||||
value={inputData[key]}
|
||||
on:change={e => {
|
||||
// need to pass without the value inside
|
||||
onChange({ detail: e.detail }, key)
|
||||
inputData[key] = e.detail
|
||||
}}
|
||||
completions={[
|
||||
jsAutocomplete([
|
||||
...bindingsToCompletions(bindings, EditorModes.JS),
|
||||
]),
|
||||
]}
|
||||
mode={EditorModes.JS}
|
||||
height={500}
|
||||
/>
|
||||
<div class="messaging">
|
||||
<Icon name="FlashOn" />
|
||||
<div class="messaging-wrap">
|
||||
<div>Add available bindings by typing <strong>$</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</CodeEditorModal>
|
||||
{:else if value.customType === "loopOption"}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
autoWidth
|
||||
value={inputData[key]}
|
||||
options={["Array", "String"]}
|
||||
defaultValue={"Array"}
|
||||
/>
|
||||
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
||||
{#if isTestModal}
|
||||
<ModalBindableInput
|
||||
title={value.title}
|
||||
value={inputData[key]}
|
||||
panel={AutomationBindingPanel}
|
||||
type={value.customType}
|
||||
{#if value.type === "string" && value.enum && canShowField(key)}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
value={inputData[key]}
|
||||
placeholder={false}
|
||||
options={value.enum}
|
||||
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
||||
/>
|
||||
{:else}
|
||||
<div class="test">
|
||||
{:else if value.type === "json"}
|
||||
<Editor
|
||||
editorHeight="250"
|
||||
editorWidth="448"
|
||||
mode="json"
|
||||
value={inputData[key]?.value}
|
||||
on:change={e => {
|
||||
/**
|
||||
* TODO - Remove after November 2023
|
||||
* *******************************
|
||||
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||
* and the new JSON body.
|
||||
*/
|
||||
delete inputData.value1
|
||||
delete inputData.value2
|
||||
delete inputData.value3
|
||||
delete inputData.value4
|
||||
delete inputData.value5
|
||||
/***********************/
|
||||
onChange(e, key)
|
||||
}}
|
||||
/>
|
||||
{:else if value.type === "boolean"}
|
||||
<div style="margin-top: 10px">
|
||||
<Checkbox
|
||||
text={value.title}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
/>
|
||||
</div>
|
||||
{:else if value.type === "date"}
|
||||
<DatePicker
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
/>
|
||||
{:else if value.customType === "column"}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
options={Object.keys(table?.schema || {})}
|
||||
/>
|
||||
{:else if value.customType === "filters"}
|
||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
|
||||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||
Save
|
||||
</Button>
|
||||
<FilterDrawer
|
||||
slot="body"
|
||||
{filters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
panel={AutomationBindingPanel}
|
||||
fillWidth
|
||||
on:change={e => (tempFilters = e.detail)}
|
||||
/>
|
||||
</Drawer>
|
||||
{:else if value.customType === "password"}
|
||||
<Input
|
||||
type="password"
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "email"}
|
||||
{#if isTestModal}
|
||||
<ModalBindableInput
|
||||
title={value.title}
|
||||
value={inputData[key]}
|
||||
panel={AutomationBindingPanel}
|
||||
type="email"
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
fillWidth
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableInput
|
||||
fillWidth={true}
|
||||
fillWidth
|
||||
title={value.title}
|
||||
panel={AutomationBindingPanel}
|
||||
type={value.customType}
|
||||
type="email"
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
allowJS={false}
|
||||
updateOnChange={false}
|
||||
placeholder={value.customType === "queryLimit" ? queryLimit : ""}
|
||||
drawerLeft="260px"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if value.customType === "query"}
|
||||
<QuerySelector
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "cron"}
|
||||
<CronBuilder
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "queryParams"}
|
||||
<QueryParamSelector
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
{bindings}
|
||||
/>
|
||||
{:else if value.customType === "table"}
|
||||
<TableSelector
|
||||
{isTrigger}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
/>
|
||||
{:else if value.customType === "row"}
|
||||
<RowSelector
|
||||
{block}
|
||||
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}
|
||||
/>
|
||||
{:else if value.customType === "webhookUrl"}
|
||||
<WebhookDisplay
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "fields"}
|
||||
<FieldSelector
|
||||
{block}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
{isTestModal}
|
||||
/>
|
||||
{:else if value.customType === "triggerSchema"}
|
||||
<SchemaSetup
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "code"}
|
||||
<CodeEditorModal>
|
||||
<CodeEditor
|
||||
value={inputData[key]}
|
||||
on:change={e => {
|
||||
// need to pass without the value inside
|
||||
onChange({ detail: e.detail }, key)
|
||||
inputData[key] = e.detail
|
||||
}}
|
||||
completions={[
|
||||
jsAutocomplete([
|
||||
...bindingsToCompletions(bindings, EditorModes.JS),
|
||||
]),
|
||||
]}
|
||||
mode={EditorModes.JS}
|
||||
height={500}
|
||||
/>
|
||||
<div class="messaging">
|
||||
<Icon name="FlashOn" />
|
||||
<div class="messaging-wrap">
|
||||
<div>Add available bindings by typing <strong>$</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</CodeEditorModal>
|
||||
{:else if value.customType === "loopOption"}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
autoWidth
|
||||
value={inputData[key]}
|
||||
options={["Array", "String"]}
|
||||
defaultValue={"Array"}
|
||||
/>
|
||||
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
||||
{#if isTestModal}
|
||||
<ModalBindableInput
|
||||
title={value.title}
|
||||
value={inputData[key]}
|
||||
panel={AutomationBindingPanel}
|
||||
type={value.customType}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{:else}
|
||||
<div class="test">
|
||||
<DrawerBindableInput
|
||||
fillWidth={true}
|
||||
title={value.title}
|
||||
panel={AutomationBindingPanel}
|
||||
type={value.customType}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
placeholder={value.customType === "queryLimit"
|
||||
? queryLimit
|
||||
: ""}
|
||||
drawerLeft="260px"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<Modal bind:this={webhookModal} width="30%">
|
||||
|
|
|
@ -48,6 +48,35 @@ export const definition: AutomationStepSchema = {
|
|||
type: AutomationIOType.STRING,
|
||||
title: "HTML Contents",
|
||||
},
|
||||
addInvite: {
|
||||
type: AutomationIOType.BOOLEAN,
|
||||
title: "Add calendar invite",
|
||||
},
|
||||
startTime: {
|
||||
type: AutomationIOType.DATE,
|
||||
title: "Start Time",
|
||||
dependsOn: "addInvite",
|
||||
},
|
||||
endTime: {
|
||||
type: AutomationIOType.DATE,
|
||||
title: "End Time",
|
||||
dependsOn: "addInvite",
|
||||
},
|
||||
summary: {
|
||||
type: AutomationIOType.STRING,
|
||||
title: "Meeting Summary",
|
||||
dependsOn: "addInvite",
|
||||
},
|
||||
location: {
|
||||
type: AutomationIOType.STRING,
|
||||
title: "Location",
|
||||
dependsOn: "addInvite",
|
||||
},
|
||||
url: {
|
||||
type: AutomationIOType.STRING,
|
||||
title: "URL",
|
||||
dependsOn: "addInvite",
|
||||
},
|
||||
},
|
||||
required: ["to", "from", "subject", "contents"],
|
||||
},
|
||||
|
@ -68,21 +97,43 @@ export const definition: AutomationStepSchema = {
|
|||
}
|
||||
|
||||
export async function run({ inputs }: AutomationStepInput) {
|
||||
let { to, from, subject, contents, cc, bcc } = inputs
|
||||
let {
|
||||
to,
|
||||
from,
|
||||
subject,
|
||||
contents,
|
||||
cc,
|
||||
bcc,
|
||||
addInvite,
|
||||
startTime,
|
||||
endTime,
|
||||
summary,
|
||||
location,
|
||||
url,
|
||||
} = inputs
|
||||
if (!contents) {
|
||||
contents = "<h1>No content</h1>"
|
||||
}
|
||||
to = to || undefined
|
||||
try {
|
||||
let response = await sendSmtpEmail(
|
||||
let response = await sendSmtpEmail({
|
||||
to,
|
||||
from,
|
||||
subject,
|
||||
contents,
|
||||
cc,
|
||||
bcc,
|
||||
true
|
||||
)
|
||||
automation: true,
|
||||
invite: addInvite
|
||||
? {
|
||||
startTime,
|
||||
endTime,
|
||||
summary,
|
||||
location,
|
||||
url,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
return {
|
||||
success: true,
|
||||
response,
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
|
||||
function generateResponse(to, from) {
|
||||
return {
|
||||
"success": true,
|
||||
"response": {
|
||||
"accepted": [
|
||||
to
|
||||
],
|
||||
"envelope": {
|
||||
"from": from,
|
||||
"to": [
|
||||
to
|
||||
]
|
||||
},
|
||||
"message": `Email sent to ${to}.`
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const mockFetch = jest.fn(() => ({
|
||||
headers: {
|
||||
raw: () => {
|
||||
return { "content-type": ["application/json"] }
|
||||
},
|
||||
get: () => ["application/json"],
|
||||
},
|
||||
json: jest.fn(() => response),
|
||||
status: 200,
|
||||
text: jest.fn(),
|
||||
}))
|
||||
jest.mock("node-fetch", () => mockFetch)
|
||||
const setup = require("./utilities")
|
||||
|
||||
|
||||
describe("test the outgoing webhook action", () => {
|
||||
let inputs
|
||||
let config = setup.getConfig()
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
inputs = {
|
||||
to: "user1@test.com",
|
||||
from: "admin@test.com",
|
||||
subject: "hello",
|
||||
contents: "testing",
|
||||
}
|
||||
let resp = generateResponse(inputs.to, inputs.from)
|
||||
mockFetch.mockImplementationOnce(() => ({
|
||||
headers: {
|
||||
raw: () => {
|
||||
return { "content-type": ["application/json"] }
|
||||
},
|
||||
get: () => ["application/json"],
|
||||
},
|
||||
json: jest.fn(() => resp),
|
||||
status: 200,
|
||||
text: jest.fn(),
|
||||
}))
|
||||
const res = await setup.runStep(setup.actions.SEND_EMAIL_SMTP.stepId, inputs)
|
||||
expect(res.response).toEqual(resp)
|
||||
expect(res.success).toEqual(true)
|
||||
|
||||
})
|
||||
|
||||
|
||||
})
|
|
@ -0,0 +1,74 @@
|
|||
import * as workerRequests from "../../utilities/workerRequests"
|
||||
|
||||
jest.mock("../../utilities/workerRequests", () => ({
|
||||
sendSmtpEmail: jest.fn(),
|
||||
}))
|
||||
|
||||
function generateResponse(to: string, from: string) {
|
||||
return {
|
||||
success: true,
|
||||
response: {
|
||||
accepted: [to],
|
||||
envelope: {
|
||||
from: from,
|
||||
to: [to],
|
||||
},
|
||||
message: `Email sent to ${to}.`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const setup = require("./utilities")
|
||||
|
||||
describe("test the outgoing webhook action", () => {
|
||||
let inputs
|
||||
let config = setup.getConfig()
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
jest
|
||||
.spyOn(workerRequests, "sendSmtpEmail")
|
||||
.mockImplementationOnce(async () =>
|
||||
generateResponse("user1@test.com", "admin@test.com")
|
||||
)
|
||||
const invite = {
|
||||
startTime: new Date(),
|
||||
endTime: new Date(),
|
||||
summary: "summary",
|
||||
location: "location",
|
||||
url: "url",
|
||||
}
|
||||
inputs = {
|
||||
to: "user1@test.com",
|
||||
from: "admin@test.com",
|
||||
subject: "hello",
|
||||
contents: "testing",
|
||||
cc: "cc",
|
||||
bcc: "bcc",
|
||||
addInvite: true,
|
||||
...invite,
|
||||
}
|
||||
let resp = generateResponse(inputs.to, inputs.from)
|
||||
const res = await setup.runStep(
|
||||
setup.actions.SEND_EMAIL_SMTP.stepId,
|
||||
inputs
|
||||
)
|
||||
expect(res.response).toEqual(resp)
|
||||
expect(res.success).toEqual(true)
|
||||
expect(workerRequests.sendSmtpEmail).toHaveBeenCalledTimes(1)
|
||||
expect(workerRequests.sendSmtpEmail).toHaveBeenCalledWith({
|
||||
to: "user1@test.com",
|
||||
from: "admin@test.com",
|
||||
subject: "hello",
|
||||
contents: "testing",
|
||||
cc: "cc",
|
||||
bcc: "bcc",
|
||||
invite,
|
||||
automation: true,
|
||||
})
|
||||
})
|
||||
})
|
|
@ -9,7 +9,7 @@ import {
|
|||
env as coreEnv,
|
||||
} from "@budibase/backend-core"
|
||||
import { updateAppRole } from "./global"
|
||||
import { BBContext, User } from "@budibase/types"
|
||||
import { BBContext, User, EmailInvite } from "@budibase/types"
|
||||
|
||||
export function request(ctx?: BBContext, request?: any) {
|
||||
if (!request.headers) {
|
||||
|
@ -65,15 +65,25 @@ async function checkResponse(
|
|||
}
|
||||
|
||||
// have to pass in the tenant ID as this could be coming from an automation
|
||||
export async function sendSmtpEmail(
|
||||
to: string,
|
||||
from: string,
|
||||
subject: string,
|
||||
contents: string,
|
||||
cc: string,
|
||||
bcc: string,
|
||||
export async function sendSmtpEmail({
|
||||
to,
|
||||
from,
|
||||
subject,
|
||||
contents,
|
||||
cc,
|
||||
bcc,
|
||||
automation,
|
||||
invite,
|
||||
}: {
|
||||
to: string
|
||||
from: string
|
||||
subject: string
|
||||
contents: string
|
||||
cc: string
|
||||
bcc: string
|
||||
automation: boolean
|
||||
) {
|
||||
invite?: EmailInvite
|
||||
}) {
|
||||
// tenant ID will be set in header
|
||||
const response = await fetch(
|
||||
checkSlashesInUrl(env.WORKER_URL + `/api/global/email/send`),
|
||||
|
@ -88,6 +98,7 @@ export async function sendSmtpEmail(
|
|||
bcc,
|
||||
purpose: "custom",
|
||||
automation,
|
||||
invite,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Document } from "../document"
|
||||
import { EventEmitter } from "events"
|
||||
import { User } from "../global"
|
||||
|
||||
export enum AutomationIOType {
|
||||
OBJECT = "object",
|
||||
|
@ -8,6 +9,7 @@ export enum AutomationIOType {
|
|||
NUMBER = "number",
|
||||
ARRAY = "array",
|
||||
JSON = "json",
|
||||
DATE = "date",
|
||||
}
|
||||
|
||||
export enum AutomationCustomIOType {
|
||||
|
@ -66,6 +68,33 @@ export enum AutomationActionStepId {
|
|||
integromat = "integromat",
|
||||
}
|
||||
|
||||
export interface EmailInvite {
|
||||
startTime: Date
|
||||
endTime: Date
|
||||
summary: string
|
||||
location?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface SendEmailOpts {
|
||||
// workspaceId If finer grain controls being used then this will lookup config for workspace.
|
||||
workspaceId?: string
|
||||
// user If sending to an existing user the object can be provided, this is used in the context.
|
||||
user: User
|
||||
// from If sending from an address that is not what is configured in the SMTP config.
|
||||
from?: string
|
||||
// contents If sending a custom email then can supply contents which will be added to it.
|
||||
contents?: string
|
||||
// subject A custom subject can be specified if the config one is not desired.
|
||||
subject?: string
|
||||
// info Pass in a structure of information to be stored alongside the invitation.
|
||||
info?: any
|
||||
cc?: boolean
|
||||
bcc?: boolean
|
||||
automation?: boolean
|
||||
invite?: EmailInvite
|
||||
}
|
||||
|
||||
export const AutomationStepIdArray = [
|
||||
...Object.values(AutomationActionStepId),
|
||||
...Object.values(AutomationTriggerStepId),
|
||||
|
@ -90,6 +119,7 @@ interface BaseIOStructure {
|
|||
customType?: AutomationCustomIOType
|
||||
title?: string
|
||||
description?: string
|
||||
dependsOn?: string
|
||||
enum?: string[]
|
||||
pretty?: string[]
|
||||
properties?: {
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
"elastic-apm-node": "3.38.0",
|
||||
"global-agent": "3.0.0",
|
||||
"got": "11.8.3",
|
||||
"ical-generator": "4.1.0",
|
||||
"joi": "17.6.0",
|
||||
"koa": "2.13.4",
|
||||
"koa-body": "4.2.0",
|
||||
|
|
|
@ -14,6 +14,7 @@ export async function sendEmail(ctx: BBContext) {
|
|||
cc,
|
||||
bcc,
|
||||
automation,
|
||||
invite,
|
||||
} = ctx.request.body
|
||||
let user
|
||||
if (userId) {
|
||||
|
@ -29,6 +30,7 @@ export async function sendEmail(ctx: BBContext) {
|
|||
cc,
|
||||
bcc,
|
||||
automation,
|
||||
invite,
|
||||
})
|
||||
ctx.body = {
|
||||
...response,
|
||||
|
|
|
@ -4,28 +4,11 @@ import { getTemplateByPurpose, EmailTemplates } from "../constants/templates"
|
|||
import { getSettingsTemplateContext } from "./templates"
|
||||
import { processString } from "@budibase/string-templates"
|
||||
import { getResetPasswordCode, getInviteCode } from "./redis"
|
||||
import { User, SMTPInnerConfig } from "@budibase/types"
|
||||
import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types"
|
||||
import { configs } from "@budibase/backend-core"
|
||||
import ical from "ical-generator"
|
||||
const nodemailer = require("nodemailer")
|
||||
|
||||
type SendEmailOpts = {
|
||||
// workspaceId If finer grain controls being used then this will lookup config for workspace.
|
||||
workspaceId?: string
|
||||
// user If sending to an existing user the object can be provided, this is used in the context.
|
||||
user: User
|
||||
// from If sending from an address that is not what is configured in the SMTP config.
|
||||
from?: string
|
||||
// contents If sending a custom email then can supply contents which will be added to it.
|
||||
contents?: string
|
||||
// subject A custom subject can be specified if the config one is not desired.
|
||||
subject?: string
|
||||
// info Pass in a structure of information to be stored alongside the invitation.
|
||||
info?: any
|
||||
cc?: boolean
|
||||
bcc?: boolean
|
||||
automation?: boolean
|
||||
}
|
||||
|
||||
const TEST_MODE = env.ENABLE_EMAIL_TEST_MODE && env.isDev()
|
||||
const TYPE = TemplateType.EMAIL
|
||||
|
||||
|
@ -200,6 +183,26 @@ export async function sendEmail(
|
|||
context
|
||||
)
|
||||
}
|
||||
if (opts?.invite) {
|
||||
const calendar = ical({
|
||||
name: "Invite",
|
||||
})
|
||||
calendar.createEvent({
|
||||
start: opts.invite.startTime,
|
||||
end: opts.invite.endTime,
|
||||
summary: opts.invite.summary,
|
||||
location: opts.invite.location,
|
||||
url: opts.invite.url,
|
||||
})
|
||||
message = {
|
||||
...message,
|
||||
icalEvent: {
|
||||
method: "request",
|
||||
content: calendar.toString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const response = await transport.sendMail(message)
|
||||
if (TEST_MODE) {
|
||||
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -13668,6 +13668,13 @@ husky@^8.0.3:
|
|||
resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184"
|
||||
integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==
|
||||
|
||||
ical-generator@4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ical-generator/-/ical-generator-4.1.0.tgz#2a336c951864c5583a2aa715d16f2edcdfd2d90b"
|
||||
integrity sha512-5GrFDJ8SAOj8cB9P1uEZIfKrNxSZ1R2eOQfZePL+CtdWh4RwNXWe8b0goajz+Hu37vcipG3RVldoa2j57Y20IA==
|
||||
dependencies:
|
||||
uuid-random "^1.3.2"
|
||||
|
||||
iconv-lite@0.4.24, iconv-lite@^0.4.15, iconv-lite@^0.4.24, iconv-lite@^0.4.5:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
|
@ -25279,6 +25286,11 @@ utils-merge@1.0.1, utils-merge@1.x.x:
|
|||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||
|
||||
uuid-random@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid-random/-/uuid-random-1.3.2.tgz#96715edbaef4e84b1dcf5024b00d16f30220e2d0"
|
||||
integrity sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ==
|
||||
|
||||
uuid@3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
||||
|
|
Loading…
Reference in New Issue