Browse Source

feat: email notification on form submission

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/642/head
Pranav C 3 years ago
parent
commit
44e75c263c
  1. 3
      packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue
  2. 295
      packages/nocodb/src/lib/noco/common/BaseModel.ts
  3. 2
      packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts

3
packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue

@ -758,7 +758,8 @@ export default {
fields: Object.keys(this.showFields)
.filter(f => this.showFields[f])
.join(','),
extraViewParams: this.extraViewParams
extraViewParams: this.extraViewParams,
selectedViewId: this.selectedViewId
},
type: this.selectedView.type,
show_as: this.selectedView.show_as,

295
packages/nocodb/src/lib/noco/common/BaseModel.ts

@ -1,20 +1,18 @@
import Handlebars from "handlebars";
import {IWebhookNotificationAdapter} from "nc-plugin";
import ejs from "ejs";
import IEmailAdapter from "../../../interface/IEmailAdapter";
import {BaseModelSql} from "../../dataMapper";
import Handlebars from 'handlebars';
import { IWebhookNotificationAdapter } from 'nc-plugin';
import ejs from 'ejs';
import IEmailAdapter from '../../../interface/IEmailAdapter';
import { BaseModelSql } from '../../dataMapper';
// import axios from "axios";
import BaseApiBuilder from "./BaseApiBuilder";
import formSubmissionEmailTemplate from "./formSubmissionEmailTemplate";
import BaseApiBuilder from './BaseApiBuilder';
import formSubmissionEmailTemplate from './formSubmissionEmailTemplate';
Handlebars.registerHelper('json', function (context) {
Handlebars.registerHelper('json', function(context) {
return JSON.stringify(context);
});
class BaseModel<T extends BaseApiBuilder<any>> extends BaseModelSql {
private builder: T;
constructor(args: any, builder: T) {
@ -23,72 +21,82 @@ class BaseModel<T extends BaseApiBuilder<any>> extends BaseModelSql {
}
public async beforeInsert(data: any, _trx: any, req): Promise<void> {
await this.handleHooks('before.insert', data, req)
await this.handleHooks('before.insert', data, req);
}
public async afterInsert(data: any, _trx: any, req): Promise<void> {
await this.handleHooks('after.insert', data, req);
if (req?.headers?.['xc-gui']) {
const id = this._extractPksValues(data);
this.builder.getXcMeta().audit(
this.builder?.getProjectId(),
this.builder?.getDbAlias(),
'nc_audit',
{
model_name: this._tn,
model_id: id,
op_type: 'DATA',
op_sub_type: 'INSERT',
description: `${id} inserted into ${this._tn}`,
// details: JSON.stringify(data),
ip: req?.clientIp,
user: req?.user?.email
}
)
this.builder
.getXcMeta()
.audit(
this.builder?.getProjectId(),
this.builder?.getDbAlias(),
'nc_audit',
{
model_name: this._tn,
model_id: id,
op_type: 'DATA',
op_sub_type: 'INSERT',
description: `${id} inserted into ${this._tn}`,
// details: JSON.stringify(data),
ip: req?.clientIp,
user: req?.user?.email
}
);
}
}
public async beforeUpdate(data: any, _trx: any, req): Promise<void> {
await this.handleHooks('before.update', data, req)
await this.handleHooks('before.update', data, req);
}
public async afterUpdate(data: any, _trx: any, req): Promise<void> {
await this.handleHooks('after.update', data, req)
await this.handleHooks('after.update', data, req);
}
public async beforeDelete(data: any, _trx: any, req): Promise<void> {
await this.handleHooks('before.delete', data, req)
await this.handleHooks('before.delete', data, req);
}
public async afterDelete(data: any, _trx: any, req): Promise<void> {
if (req?.headers?.['xc-gui']) {
this.builder.getXcMeta().audit(
this.builder?.getProjectId(),
this.builder?.getDbAlias(),
'nc_audit',
{
model_name: this._tn,
model_id: req?.params?.id,
op_type: 'DATA',
op_sub_type: 'DELETE',
description: `${req?.params.id} deleted from ${this._tn}`,
ip: req?.clientIp,
user: req?.user?.email
}
)
this.builder
.getXcMeta()
.audit(
this.builder?.getProjectId(),
this.builder?.getDbAlias(),
'nc_audit',
{
model_name: this._tn,
model_id: req?.params?.id,
op_type: 'DATA',
op_sub_type: 'DELETE',
description: `${req?.params.id} deleted from ${this._tn}`,
ip: req?.clientIp,
user: req?.user?.email
}
);
}
await this.handleHooks('after.delete', data, req)
await this.handleHooks('after.delete', data, req);
}
private async handleHooks(hookName, data, req): Promise<void> {
// const data = _data;
// handle form view data submission
if (hookName === 'after.insert' && req?.query?.form && this.builder?.formViews?.[this.tn]?.[req.query.form]) {
if (
hookName === 'after.insert' &&
req?.query?.form &&
this.builder?.formViews?.[this.tn]?.[req.query.form]
) {
const formView = this.builder?.formViews?.[this.tn]?.[req.query.form];
const emails = Object.entries(formView?.query_params?.extraViewParams?.formParams?.emailMe || {}).filter(a => a[1]).map(a => a[0])
const emails = Object.entries(
formView?.query_params?.extraViewParams?.formParams?.emailMe || {}
)
.filter(a => a[1])
.map(a => a[0]);
if (emails?.length) {
const transformedData = data;
for (const col of this.columns) {
@ -96,12 +104,27 @@ class BaseModel<T extends BaseApiBuilder<any>> extends BaseModelSql {
if (typeof transformedData[col._cn] === 'string') {
transformedData[col._cn] = JSON.parse(transformedData[col._cn]);
}
transformedData[col._cn] = (transformedData[col._cn] || []).map((attachment) => {
if (['jpeg', 'gif', 'png', 'apng', 'svg', 'bmp', 'ico', 'jpg'].includes(attachment.title.split('.').pop())) {
return `<a href="${attachment.url}" target="_blank"><img height="50px" src="${attachment.url}"/></a>`
}
return `<a href="${attachment.url}" target="_blank">${attachment.title}</a>`
}).join('&nbsp;');
transformedData[col._cn] = (transformedData[col._cn] || [])
.map(attachment => {
if (
[
'jpeg',
'gif',
'png',
'apng',
'svg',
'bmp',
'ico',
'jpg'
].includes(attachment.title.split('.').pop())
) {
return `<a href="${attachment.url}" target="_blank"><img height="50px" src="${attachment.url}"/></a>`;
}
return `<a href="${attachment.url}" target="_blank">${attachment.title}</a>`;
})
.join('&nbsp;');
} else if (typeof transformedData[col._cn] === 'object') {
transformedData[col._cn] = JSON.stringify(transformedData[col._cn]);
}
}
// todo: notification template
@ -113,16 +136,16 @@ class BaseModel<T extends BaseApiBuilder<any>> extends BaseModelSql {
tn: this.tn,
_tn: this._tn
})
})
});
}
}
try {
if (this.tn in this.builder.hooks
&& hookName in this.builder.hooks[this.tn]
&& this.builder.hooks[this.tn][hookName]
if (
this.tn in this.builder.hooks &&
hookName in this.builder.hooks[this.tn] &&
this.builder.hooks[this.tn][hookName]
) {
/* if (hookName === 'after.update') {
try {
data = await this.nestedRead(req.params.id, this.defaultNestedQueryParams)
@ -131,36 +154,65 @@ class BaseModel<T extends BaseApiBuilder<any>> extends BaseModelSql {
}
}*/
for (const hook of this.builder.hooks[this.tn][hookName]) {
if (!hook.active) {
continue
continue;
}
console.log('Hook handler ::::' + this.tn + '::::', this.builder.hooks[this.tn][hookName])
console.log('Hook handler ::::' + this.tn + '::::', data)
console.log(
'Hook handler ::::' + this.tn + '::::',
this.builder.hooks[this.tn][hookName]
);
console.log('Hook handler ::::' + this.tn + '::::', data);
if (!this.validateCondition(hook.condition, data, req)) {
continue;
}
switch (hook.notification?.type) {
case 'Email':
this.emailAdapter?.mailSend({
to: this.parseBody(hook.notification?.payload?.to, req, data, hook.notification?.payload),
subject: this.parseBody(hook.notification?.payload?.subject, req, data, hook.notification?.payload),
html: this.parseBody(hook.notification?.payload?.body, req, data, hook.notification?.payload)
})
to: this.parseBody(
hook.notification?.payload?.to,
req,
data,
hook.notification?.payload
),
subject: this.parseBody(
hook.notification?.payload?.subject,
req,
data,
hook.notification?.payload
),
html: this.parseBody(
hook.notification?.payload?.body,
req,
data,
hook.notification?.payload
)
});
break;
case 'URL':
this.handleHttpWebHook(hook.notification?.payload, req, data)
this.handleHttpWebHook(hook.notification?.payload, req, data);
break;
default:
if (this.webhookNotificationAdapters && hook.notification?.type && hook.notification?.type in this.webhookNotificationAdapters) {
this.webhookNotificationAdapters[hook.notification.type].sendMessage(this.parseBody(hook.notification?.payload?.body, req, data, hook.notification?.payload), hook.notification?.payload)
if (
this.webhookNotificationAdapters &&
hook.notification?.type &&
hook.notification?.type in this.webhookNotificationAdapters
) {
this.webhookNotificationAdapters[
hook.notification.type
].sendMessage(
this.parseBody(
hook.notification?.payload?.body,
req,
data,
hook.notification?.payload
),
hook.notification?.payload
);
}
break
break;
}
// await axios.post(this.builder.hooks[this.tn][hookName].url, {data}, {
@ -169,7 +221,7 @@ class BaseModel<T extends BaseApiBuilder<any>> extends BaseModelSql {
}
}
} catch (e) {
console.log('hooks :: error', hookName, e.message)
console.log('hooks :: error', hookName, e.message);
}
}
@ -178,7 +230,7 @@ class BaseModel<T extends BaseApiBuilder<any>> extends BaseModelSql {
const req = this.axiosRequestMake(apiMeta, apiReq, data);
await require('axios')(req);
} catch (e) {
console.log(e)
console.log(e);
}
}
@ -186,54 +238,72 @@ class BaseModel<T extends BaseApiBuilder<any>> extends BaseModelSql {
if (apiMeta.body) {
try {
apiMeta.body = JSON.parse(apiMeta.body, (_key, value) => {
return typeof value === 'string' ? this.parseBody(value, apiReq, data, apiMeta) : value;
return typeof value === 'string'
? this.parseBody(value, apiReq, data, apiMeta)
: value;
});
} catch (e) {
apiMeta.body = this.parseBody(apiMeta.body, apiReq, data, apiMeta)
apiMeta.body = this.parseBody(apiMeta.body, apiReq, data, apiMeta);
console.log(e);
}
}
if (apiMeta.auth) {
try {
apiMeta.auth = JSON.parse(apiMeta.auth, (_key, value) => {
return typeof value === 'string' ? this.parseBody(value, apiReq, data, apiMeta) : value;
return typeof value === 'string'
? this.parseBody(value, apiReq, data, apiMeta)
: value;
});
} catch (e) {
apiMeta.auth = this.parseBody(apiMeta.auth, apiReq, data, apiMeta)
apiMeta.auth = this.parseBody(apiMeta.auth, apiReq, data, apiMeta);
console.log(e);
}
}
apiMeta.response = {};
const req = {
params: apiMeta.parameters ? apiMeta.parameters.reduce((paramsObj, param) => {
if (param.name && param.enabled) {
paramsObj[param.name] = this.parseBody(param.value, apiReq, data, apiMeta);
}
return paramsObj;
}, {}) : {},
params: apiMeta.parameters
? apiMeta.parameters.reduce((paramsObj, param) => {
if (param.name && param.enabled) {
paramsObj[param.name] = this.parseBody(
param.value,
apiReq,
data,
apiMeta
);
}
return paramsObj;
}, {})
: {},
url: this.parseBody(apiMeta.path, apiReq, data, apiMeta),
method: apiMeta.method,
data: apiMeta.body,
headers: apiMeta.headers ? apiMeta.headers.reduce((headersObj, header) => {
if (header.name && header.enabled) {
headersObj[header.name] = this.parseBody(header.value, apiReq, data, apiMeta);
}
return headersObj;
}, {}) : {},
headers: apiMeta.headers
? apiMeta.headers.reduce((headersObj, header) => {
if (header.name && header.enabled) {
headersObj[header.name] = this.parseBody(
header.value,
apiReq,
data,
apiMeta
);
}
return headersObj;
}, {})
: {},
withCredentials: true
};
return req;
}
// @ts-ignore
private get emailAdapter(): IEmailAdapter {
return this.builder?.app?.metaMgr?.emailAdapter;
}
// @ts-ignore
private get webhookNotificationAdapters(): { [key: string]: IWebhookNotificationAdapter } {
private get webhookNotificationAdapters(): {
[key: string]: IWebhookNotificationAdapter;
} {
return this.builder?.app?.metaMgr?.webhookNotificationAdapters;
}
@ -253,28 +323,34 @@ class BaseModel<T extends BaseApiBuilder<any>> extends BaseModelSql {
res = data[field] !== con.value;
break;
case 'is like':
res = data[field]?.toLowerCase()?.indexOf(con.value?.toLowerCase()) > -1;
res =
data[field]?.toLowerCase()?.indexOf(con.value?.toLowerCase()) > -1;
break;
case 'is not like':
res = data[field]?.toLowerCase()?.indexOf(con.value?.toLowerCase()) === -1;
res =
data[field]?.toLowerCase()?.indexOf(con.value?.toLowerCase()) ===
-1;
break;
case 'is empty':
res = data[field] === '' || data[field] === null || data[field] === undefined;
res =
data[field] === '' ||
data[field] === null ||
data[field] === undefined;
break;
case 'is not empty':
res = !(data[field] === '' || data[field] === null || data[field] === undefined);
res = !(
data[field] === '' ||
data[field] === null ||
data[field] === undefined
);
break;
case 'is null':
res =
res = data[field] === null;
res = res = data[field] === null;
break;
case 'is not null':
res = data[field] !== null;
break;
/* todo: case '<':
return condition + `~not(${filt.field},lt,${filt.value})`;
case '<=':
@ -285,25 +361,28 @@ class BaseModel<T extends BaseApiBuilder<any>> extends BaseModelSql {
return condition + `~not(${filt.field},ge,${filt.value})`;*/
}
return con.logicOp === 'or' ? valid || res : valid && res;
}, true);
return isValid;
}
private parseBody(template: string, req: any, data: any, payload: any): string {
private parseBody(
template: string,
req: any,
data: any,
payload: any
): string {
if (!template) {
return template;
}
return Handlebars.compile(template, {noEscape: true})({
return Handlebars.compile(template, { noEscape: true })({
data,
user: req?.user,
payload,
env: process.env
})
});
}
}

2
packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts

@ -3540,7 +3540,7 @@ export default class NcMetaMgr {
const model = apiBuilder?.xcModels?.[viewMeta.model_name];
if (model) {
// req.query.form = viewMeta.form_id
req.query.form = queryParams?.selectedViewId;
await model.nestedInsert(insertObject, null, req);
// todo: map nested data

Loading…
Cancel
Save