diff --git a/packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue b/packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue index 10020c2e5f..cb07f88ba9 100644 --- a/packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue +++ b/packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue @@ -75,12 +75,12 @@ mdi-reload @@ -296,6 +296,7 @@ :available-columns="availableColumns" :meta="meta" :data="data" + :id="selectedViewId" :show-fields.sync="showFields" :all-columns="allColumns" :field-list="fieldList" diff --git a/packages/nc-gui/components/project/spreadsheet/views/formView.vue b/packages/nc-gui/components/project/spreadsheet/views/formView.vue index 1d7139117a..227a0a5722 100644 --- a/packages/nc-gui/components/project/spreadsheet/views/formView.vue +++ b/packages/nc-gui/components/project/spreadsheet/views/formView.vue @@ -376,13 +376,21 @@ Show a blank form after 5 seconds - + @@ -405,11 +413,14 @@ import Editable from '../components/editable' import EditColumn from '../components/editColumn' import form from '../mixins/form' +// todo: generate hideCols based on default values +const hiddenCols = ['created_at', 'updated_at'] + export default { name: 'FormView', components: { EditColumn, Editable, EditableCell, VirtualCell, HeaderCell, VirtualHeaderCell, draggable }, mixins: [form, validationMixin], - props: ['meta', 'availableColumns', 'nodes', 'sqlUi', 'formParams', 'showFields', 'fieldsOrder', 'allColumns', 'dbAlias', 'api'], + props: ['meta', 'availableColumns', 'nodes', 'sqlUi', 'formParams', 'showFields', 'fieldsOrder', 'allColumns', 'dbAlias', 'api', 'id'], data: () => ({ localState: {}, moved: false, @@ -427,6 +438,9 @@ export default { validations() { const obj = { localState: {}, virtual: {} } for (const column of this.columns) { + if (!this.localParams || !this.localParams.fields || !this.localParams.fields[column.alias]) { + continue + } if (!column.virtual && ((column.rqd && !column.default) || this.localParams.fields[column.alias].required)) { obj.localState[column._cn] = { required } } else if (column.bt) { @@ -455,12 +469,12 @@ export default { }, hiddenColumns: { get() { - return this.allColumns.filter(c => !this.showFields[c.alias] && !(c.pk && c.ai) && !(this.meta.v || []).some(v => v.bt && v.bt.cn === c.cn)) + return this.allColumns.filter(c => !this.showFields[c.alias] && !hiddenCols.includes(c.cn) && !(c.pk && c.ai) && !(this.meta.v || []).some(v => v.bt && v.bt.cn === c.cn)) } }, columns: { get() { - return this.allColumns.filter(c => this.showFields[c.alias]).sort((a, b) => ((this.fieldsOrder.indexOf(a.alias) + 1) || Infinity) - ((this.fieldsOrder.indexOf(b.alias) + 1) || Infinity)) + return this.allColumns.filter(c => this.showFields[c.alias] && !hiddenCols.includes(c.cn)).sort((a, b) => ((this.fieldsOrder.indexOf(a.alias) + 1) || Infinity) - ((this.fieldsOrder.indexOf(b.alias) + 1) || Infinity)) }, set(val) { const showFields = val.reduce((o, v) => { @@ -499,6 +513,7 @@ export default { name: this.meta._tn, description: 'Form view description', submit: {}, + emailMe: {}, fields: {} }, this.localParams) this.availableColumns.forEach((c) => { @@ -509,6 +524,15 @@ export default { // this.hiddenColumns = this.meta.columns.filter(c => this.availableColumns.find(c1 => c.cn === c1.cn && c._cn === c1._cn)) }, methods: { + async checkSMTPStatus() { + if (this.localParams.emailMe[this.$store.state.users.user.email]) { + const emailPlugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', { title: 'SMTP' }]) + if (!emailPlugin.active) { + this.$set(this.localParams.emailMe, this.$store.state.users.user.email, false) + this.$toast.info('Please activate SMTP plugin in App store for enabling email notification').goAway(5000) + } + } + }, updateCol(_, column, id) { this.$set(this.localState, column, id) }, @@ -546,20 +570,22 @@ export default { // }, {}) // if (this.isNew) { - // const data = - const data = await this.api.insert(this.localState) - this.localState = { ...this.localState, ...data } + + // todo: add params option in GraphQL + let data = await this.api.insert(this.localState, { params: { form: this.id } }) + data = { ...this.localState, ...data } // save hasmany and manytomany relations from local state if (this.$refs.virtual && Array.isArray(this.$refs.virtual)) { for (const vcell of this.$refs.virtual) { if (vcell.save) { - await vcell.save(this.localState) + await vcell.save(data) } } } this.virtual = {} + this.localState = {} this.submitted = true diff --git a/packages/nc-gui/plugins/ncApis/restApi.js b/packages/nc-gui/plugins/ncApis/restApi.js index 1c2b014807..262ecd285a 100644 --- a/packages/nc-gui/plugins/ncApis/restApi.js +++ b/packages/nc-gui/plugins/ncApis/restApi.js @@ -35,7 +35,7 @@ export default class RestApi { } } - get(url, params={}, extras = {}) { + get(url, params = {}, extras = {}) { return this.$axios({ url, params, @@ -57,7 +57,10 @@ export default class RestApi { /// api/v1/Film/m2mNotChildren/film_actor/44 // const list = await this.list(params); // const count = (await this.count({where: params.where || ''})).count; - const { list, info: { count } } = (await this.get(`/nc/${this.$ctx.projectId}/api/v1/${this.table}/m2mNotChildren/${assoc}/${pid}`, params)).data + const { + list, + info: { count } + } = (await this.get(`/nc/${this.$ctx.projectId}/api/v1/${this.table}/m2mNotChildren/${assoc}/${pid}`, params)).data return { list, count } } @@ -79,11 +82,14 @@ export default class RestApi { return res.data } - async insert(data) { + async insert(data, { + params = {} + } = {}) { return (await this.$axios({ method: 'post', url: `/nc/${this.$ctx.projectId}/api/v1/${this.table}`, - data + data, + params })).data } diff --git a/packages/nocodb/src/lib/noco/NcProjectBuilder.ts b/packages/nocodb/src/lib/noco/NcProjectBuilder.ts index e39c35a0d4..062f7eb319 100644 --- a/packages/nocodb/src/lib/noco/NcProjectBuilder.ts +++ b/packages/nocodb/src/lib/noco/NcProjectBuilder.ts @@ -150,6 +150,9 @@ export default class NcProjectBuilder { case 'xcVirtualTableUpdate': await curBuilder.onVirtualTableUpdate(data.req.args); break; + case 'xcVirtualTableCreate': + await curBuilder.loadFormViews(); + break; case 'tableCreate': diff --git a/packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts b/packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts index 9df47da72e..71f65d7d54 100644 --- a/packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts +++ b/packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts @@ -107,7 +107,7 @@ export default abstract class BaseApiBuilder implements XcDynami public readonly app: T; public hooks: { - [key: string]: { + [tableName: string]: { [key: string]: Array<{ event: string; url: string; @@ -115,6 +115,11 @@ export default abstract class BaseApiBuilder implements XcDynami }> } } + + public formViews: { + [tableName: string]: any + } + protected tablesCount = 0; protected relationsCount = 0; protected viewsCount = 0; @@ -151,6 +156,7 @@ export default abstract class BaseApiBuilder implements XcDynami this.acls = {}; this.procedureOrFunctionAcls = {}; this.hooks = {}; + this.formViews = {}; this.projectBuilder = projectBuilder; } @@ -305,6 +311,7 @@ export default abstract class BaseApiBuilder implements XcDynami }, {'parent_model_title': oldTableName, type: 'vtable'}) await this.loadHooks(); + await this.loadFormViews(); await this.modifyTableNameInACL(oldTableName, newTableName); } @@ -1221,6 +1228,29 @@ export default abstract class BaseApiBuilder implements XcDynami } } + + // NOTE: xc-meta + public async loadFormViews(): Promise { + this.baseLog(`loadFormViews :`); + this.formViews = {}; + const formViewList = await this.xcMeta.metaList(this.projectId, this.dbAlias, 'nc_models', { + condition: { + show_as: 'form' + } + }); + + for (const formView of formViewList) { + if (!(formView.parent_model_title in this.formViews)) { + this.formViews[formView.parent_model_title] = {}; + } + try { + formView.query_params = formView.query_params && JSON.parse(formView.query_params); + } catch (e) { + } + this.formViews[formView.parent_model_title][formView.id] = formView; + } + } + protected async generateAndSaveAcl(name: string, type = 'table'): Promise { this.baseLog(`generateAndSaveAcl : '%s' %s`, name, type); @@ -1338,7 +1368,7 @@ export default abstract class BaseApiBuilder implements XcDynami // todo: insert parallelly for (const relation of relations) { relation.enabled = true; - relation.fkn= relation?.cstn ; + relation.fkn = relation?.cstn; await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_relations', { tn: relation.tn, _tn: this.getTableNameAlias(relation.tn), @@ -1389,7 +1419,7 @@ export default abstract class BaseApiBuilder implements XcDynami continue; } - const tableMetaA = this. metas[meta.belongsTo[0].rtn]; + const tableMetaA = this.metas[meta.belongsTo[0].rtn]; const tableMetaB = this.metas[meta.belongsTo[1].rtn]; /* // remove hasmany relation with associate table from tables @@ -1929,6 +1959,9 @@ export default abstract class BaseApiBuilder implements XcDynami XcCache.del([this.projectId, this.dbAlias, 'table', args.tn].join('::')); // todo: update meta and model } + if (args?.query_params?.extraViewParams?.formParams) { + this.formViews[args.tn][args.id].query_params = args.query_params + } } } diff --git a/packages/nocodb/src/lib/noco/common/BaseModel.ts b/packages/nocodb/src/lib/noco/common/BaseModel.ts index 87867816c4..264ab72e7d 100644 --- a/packages/nocodb/src/lib/noco/common/BaseModel.ts +++ b/packages/nocodb/src/lib/noco/common/BaseModel.ts @@ -1,13 +1,14 @@ 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"; -Handlebars.registerHelper('json', function(context) { +Handlebars.registerHelper('json', function (context) { return JSON.stringify(context); }); @@ -84,19 +85,51 @@ class BaseModel> extends BaseModelSql { // const data = _data; + // handle form view data submission + 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]) + if (emails?.length) { + const transformedData = data; + for (const col of this.columns) { + if (col.uidt === 'Attachment') { + 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 `` + } + return `${attachment.title}` + }).join(' '); + } + } + // todo: notification template + this.emailAdapter?.mailSend({ + to: emails.join(','), + subject: this.parseBody('NocoDB Form', req, data, {}), + html: ejs.render(formSubmissionEmailTemplate, { + data: transformedData, + 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 (hookName === 'after.update') { - try { - data = await this.nestedRead(req.params.id, this.defaultNestedQueryParams) - } catch (_) { - /!* ignore *!/ - } - }*/ + /* if (hookName === 'after.update') { + try { + data = await this.nestedRead(req.params.id, this.defaultNestedQueryParams) + } catch (_) { + /!* ignore *!/ + } + }*/ for (const hook of this.builder.hooks[this.tn][hookName]) { diff --git a/packages/nocodb/src/lib/noco/common/formSubmissionEmailTemplate.ts b/packages/nocodb/src/lib/noco/common/formSubmissionEmailTemplate.ts new file mode 100644 index 0000000000..7023b7f8a2 --- /dev/null +++ b/packages/nocodb/src/lib/noco/common/formSubmissionEmailTemplate.ts @@ -0,0 +1,219 @@ +export default ` + + + + + NocoDB forms: someone has responded to Form + + + + + + + + + + + + + +` + +/** + * @copyright Copyright (c) 2021, Xgene Cloud Ltd + * + * @author Naveen MR + * @author Pranav C Balan + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ diff --git a/packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts b/packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts index 9635e98761..3858aaacb5 100644 --- a/packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts +++ b/packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts @@ -213,6 +213,7 @@ export class GqlApiBuilder extends BaseApiBuilder implements XcMetaMgr { } await this.loadHooks(); + await this.loadFormViews(); await this.initGraphqlRoute(); await super.loadCommon(); diff --git a/packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts b/packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts index cc35feb590..7ca362a3dc 100644 --- a/packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts +++ b/packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts @@ -79,6 +79,7 @@ export class RestApiBuilder extends BaseApiBuilder { } await this.loadHooks(); + await this.loadFormViews(); await super.loadCommon(); const t1 = process.hrtime(t);