diff --git a/packages/nc-gui/components/project/appStore/inputs/attachment.vue b/packages/nc-gui/components/project/appStore/inputs/attachment.vue
index 2237abfb9f..6943097437 100644
--- a/packages/nc-gui/components/project/appStore/inputs/attachment.vue
+++ b/packages/nc-gui/components/project/appStore/inputs/attachment.vue
@@ -246,7 +246,7 @@ export default {
}
this.uploading = true
for (const file of this.$refs.file.files) {
- const item = await this.$store.dispatch('sqlMgr/ActUpload', [{
+ const item = await this.$store.dispatch('sqlMgr/ActUploadOld', [{
dbAlias: this.dbAlias
}, 'xcAttachmentUpload', { public: true }, file])
this.localState.push(item)
diff --git a/packages/nc-gui/components/project/settings/xcMeta.vue b/packages/nc-gui/components/project/settings/xcMeta.vue
index 3e943f6b22..52add14c24 100644
--- a/packages/nc-gui/components/project/settings/xcMeta.vue
+++ b/packages/nc-gui/components/project/settings/xcMeta.vue
@@ -288,7 +288,7 @@ export default {
this.loading = 'import-zip'
try {
this.$refs.importFile.value = ''
- await this.$store.dispatch('sqlMgr/ActUpload', [
+ await this.$store.dispatch('sqlMgr/ActUploadOld', [
{
env: '_noco'
},
diff --git a/packages/nc-gui/components/project/spreadsheet/components/editableCell.vue b/packages/nc-gui/components/project/spreadsheet/components/editableCell.vue
index 914e18fe9f..983bc6c30d 100644
--- a/packages/nc-gui/components/project/spreadsheet/components/editableCell.vue
+++ b/packages/nc-gui/components/project/spreadsheet/components/editableCell.vue
@@ -13,8 +13,10 @@
:active="active"
:db-alias="dbAlias"
:meta="meta"
+ :is-form="isForm"
:column="column"
:is-public-grid="isPublic && !isForm"
+ :is-public-form="isPublic && isForm"
v-on="$listeners"
/>
diff --git a/packages/nc-gui/components/project/spreadsheet/components/editableCell/editableAttachmentCell.vue b/packages/nc-gui/components/project/spreadsheet/components/editableCell/editableAttachmentCell.vue
index 97c0b748d1..97f70f5562 100644
--- a/packages/nc-gui/components/project/spreadsheet/components/editableCell/editableAttachmentCell.vue
+++ b/packages/nc-gui/components/project/spreadsheet/components/editableCell/editableAttachmentCell.vue
@@ -19,8 +19,8 @@
@@ -32,9 +32,9 @@
alt="#"
max-height="33px"
contain
- :src="item.url"
+ :src="item.url || item.data"
v-on="on"
- @click="selectImage(item.url,i)"
+ @click="selectImage(item.url || item.data,i)"
>
-
+
{{
item.icon
}}
-
+
mdi-file
@@ -82,7 +87,13 @@
-
+
mdi-link-variant
@@ -97,13 +108,18 @@
class="row"
@update="onOrderUpdate"
>
-
+
-
+
mdi-close-circle
@@ -114,16 +130,16 @@
v-if="isImage(item.title)"
style="max-height: 100%;max-width: 100%"
alt="#"
- :src="item.url"
+ :src="item.url || item.data"
@click="selectImage(item.url,i)"
>
-
+
{{
item.icon
}}
-
+
mdi-file
@@ -144,7 +160,7 @@
@@ -158,7 +174,7 @@
{{ item.icon }}
@@ -183,7 +199,7 @@
show-arrows
>
@@ -198,7 +214,7 @@
{{ item.icon }}
@@ -229,7 +245,7 @@ import { isImage } from '@/components/project/spreadsheet/helpers/imageExt'
export default {
name: 'EditableAttachmentCell',
components: { draggable },
- props: ['dbAlias', 'value', 'active', 'isLocked', 'meta', 'column', 'isPublicGrid'],
+ props: ['dbAlias', 'value', 'active', 'isLocked', 'meta', 'column', 'isPublicGrid', 'isForm', 'isPublicForm'],
data: () => ({
carousel: null,
uploading: false,
@@ -237,7 +253,8 @@ export default {
dialog: false,
showImage: false,
selectedImage: null,
- dragOver: false
+ dragOver: false,
+ localFilesState: []
}),
watch: {
value(val, prev) {
@@ -287,14 +304,34 @@ export default {
}
},
async onFileSelection() {
- if (this.isPublicGrid) { return }
+ if (this.isPublicGrid) {
+ return
+ }
if (!this.$refs.file.files || !this.$refs.file.files.length) {
return
}
+
+ if (this.isPublicForm) {
+ this.localFilesState.push(...Array.from(this.$refs.file.files).map((file) => {
+ const res = { file, title: file.name }
+ if (isImage(file.name)) {
+ const reader = new FileReader()
+ reader.onload = (e) => {
+ this.$set(res, 'data', e.target.result)
+ }
+ reader.readAsDataURL(file)
+ }
+ return res
+ }))
+
+ this.$emit('input', this.localFilesState.map(f => f.file))
+ return
+ }
+
this.uploading = true
for (const file of this.$refs.file.files) {
try {
- const item = await this.$store.dispatch('sqlMgr/ActUpload', [{
+ const item = await this.$store.dispatch('sqlMgr/ActUploadOld', [{
dbAlias: this.dbAlias
}, 'xcAttachmentUpload', {
appendPath: [this.meta.tn],
@@ -317,12 +354,17 @@ export default {
this.$emit('update')
},
removeItem(i) {
- this.localState.splice(i, 1)
- this.$emit('input', JSON.stringify(this.localState))
+ if (this.isPublicForm) {
+ this.localFilesState.splice(i, 1)
+ this.$emit('input', this.localFilesState.map(f => f.file))
+ } else {
+ this.localState.splice(i, 1)
+ this.$emit('input', JSON.stringify(this.localState))
+ }
this.$emit('update')
},
downloadItem(item) {
- FileSaver.saveAs(item.url, item.title)
+ FileSaver.saveAs(item.url || item.data, item.title)
},
onArrowDown(e) {
if (!this.showImage) {
@@ -512,4 +554,7 @@ export default {
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
- * along with this p
+ * along with this program. If not, see .
+ *
+ */
+-->
diff --git a/packages/nc-gui/components/project/spreadsheet/components/editableCell/editableUrlCell.vue b/packages/nc-gui/components/project/spreadsheet/components/editableCell/editableUrlCell.vue
index 1ea92bd269..fbbaf1ec7e 100644
--- a/packages/nc-gui/components/project/spreadsheet/components/editableCell/editableUrlCell.vue
+++ b/packages/nc-gui/components/project/spreadsheet/components/editableCell/editableUrlCell.vue
@@ -50,3 +50,27 @@ input, textarea {
color: var(--v-textColor-base);
}
+
diff --git a/packages/nc-gui/components/project/spreadsheet/helpers/imageExt.js b/packages/nc-gui/components/project/spreadsheet/helpers/imageExt.js
index ee2325d2af..52c75e74c0 100644
--- a/packages/nc-gui/components/project/spreadsheet/helpers/imageExt.js
+++ b/packages/nc-gui/components/project/spreadsheet/helpers/imageExt.js
@@ -1,4 +1,4 @@
-const imageExt = ['jpeg', 'gif', 'png', 'apng', 'svg', 'bmp', 'ico', 'jpg']
+const imageExt = ['jpeg', 'gif', 'png', 'png', 'svg', 'bmp', 'ico', 'jpg']
export default imageExt
diff --git a/packages/nc-gui/components/project/spreadsheet/public/xcForm.vue b/packages/nc-gui/components/project/spreadsheet/public/xcForm.vue
index 2a358e7dda..d222193117 100644
--- a/packages/nc-gui/components/project/spreadsheet/public/xcForm.vue
+++ b/packages/nc-gui/components/project/spreadsheet/public/xcForm.vue
@@ -171,6 +171,7 @@
:meta="meta"
:sql-ui="sqlUiLoc"
is-form
+ is-public
:hint="localParams.fields[col.alias].description"
@focus="active = col._cn"
@blur="active = ''"
@@ -306,7 +307,9 @@ export default {
const showFields = this.query_params.showFields || {}
let fields = this.query_params.fieldsOrder || []
- if (!fields.length) { fields = Object.keys(showFields) }
+ if (!fields.length) {
+ fields = Object.keys(showFields)
+ }
// eslint-disable-next-line camelcase
let columns = this.meta.columns
@@ -371,12 +374,29 @@ export default {
// if (this.isNew) {
- await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'sharedViewInsert', {
- view_id: this.$route.params.id,
- password: this.password,
- data: this.localState,
- nested: this.virtual
- }])
+ const formData = new FormData()
+ const data = { ...this.localState }
+
+ for (const col of this.meta.columns) {
+ if (col.uidt === 'Attachment') {
+ const files = data[col._cn]
+ delete data[col._cn]
+ for (const file of (files || [])) {
+ formData.append(`${col._cn}`, file)
+ }
+ }
+ }
+
+ await this.$store.dispatch('sqlMgr/ActUpload', {
+ op: 'sharedViewInsert',
+ opArgs: {
+ view_id: this.$route.params.id,
+ password: this.password,
+ data,
+ nested: this.virtual
+ },
+ formData
+ })
//
// data = { ...this.localState, ...data }
@@ -399,7 +419,8 @@ export default {
this.$toast.success(this.localParams.submit.message || 'Saved successfully.', {
position: 'bottom-right'
}).goAway(3000)
- } catch (e) {
+ } catch
+ (e) {
console.log(e)
this.$toast.error(`Failed to update row : ${e.message}`).goAway(3000)
}
diff --git a/packages/nc-gui/pages/projects/index.vue b/packages/nc-gui/pages/projects/index.vue
index a7ee6b1e8e..1c90f97d2f 100644
--- a/packages/nc-gui/pages/projects/index.vue
+++ b/packages/nc-gui/pages/projects/index.vue
@@ -1113,7 +1113,7 @@ export default {
this.loading = 'import-zip'
try {
this.$refs.importFile.value = ''
- await this.$store.dispatch('sqlMgr/ActUpload', [
+ await this.$store.dispatch('sqlMgr/ActUploadOld', [
{
// dbAlias: 'db',
project_id: projectId,
diff --git a/packages/nc-gui/store/sqlMgr.js b/packages/nc-gui/store/sqlMgr.js
index e225a014a1..b12021c0d3 100644
--- a/packages/nc-gui/store/sqlMgr.js
+++ b/packages/nc-gui/store/sqlMgr.js
@@ -354,7 +354,12 @@ export const actions = {
}
},
- async ActSqlOp({ commit, state, rootState, dispatch }, [args, op, opArgs, cusHeaders, cusAxiosOptions, queryParams, returnResponse]) {
+ async ActSqlOp({
+ commit,
+ state,
+ rootState,
+ dispatch
+ }, [args, op, opArgs, cusHeaders, cusAxiosOptions, queryParams, returnResponse]) {
const params = {}
if (this.$router.currentRoute && this.$router.currentRoute.params && this.$router.currentRoute.params.project_id) {
params.project_id = this.$router.currentRoute.params.project_id
@@ -415,7 +420,24 @@ export const actions = {
}
},
- async ActUpload({ commit, state, rootState }, [args, op, opArgs, file, cusHeaders, cusAxiosOptions]) {
+ async ActUploadOld({
+ commit,
+ state,
+ rootState,
+ dispatch
+ }, [args, op, opArgs, file, cusHeaders, cusAxiosOptions, formData]) {
+ return await dispatch('ActUpload', { args, op, opArgs, file, cusHeaders, cusAxiosOptions, formData })
+ },
+
+ async ActUpload({ commit, state, rootState }, {
+ args = {},
+ op,
+ opArgs,
+ file,
+ cusHeaders = {},
+ cusAxiosOptions = {},
+ formData = new FormData()
+ }) {
try {
const params = {}
if (this.$router.currentRoute && this.$router.currentRoute.params && this.$router.currentRoute.params.project_id) {
@@ -435,9 +457,10 @@ export const actions = {
Object.assign(headers, cusHeaders)
}
- const formData = new FormData()
+ if (file) {
+ formData.append('file', file)
+ }
- formData.append('file', file)
formData.append('json', JSON.stringify({ api: op, ...params, ...args, args: opArgs }))
// formData.append('project_id', params.project_id);
diff --git a/packages/nocodb/src/interface/IStorageAdapter.ts b/packages/nocodb/src/interface/IStorageAdapter.ts
index 5aeca6e1ee..847cd5f5bb 100644
--- a/packages/nocodb/src/interface/IStorageAdapter.ts
+++ b/packages/nocodb/src/interface/IStorageAdapter.ts
@@ -11,6 +11,7 @@ interface XcFile {
path: string;
mimetype: string;
size: number | string;
+ buffer?: any;
}
export { XcFile };
diff --git a/packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts b/packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts
index 2245340a2b..6054168b8a 100644
--- a/packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts
+++ b/packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts
@@ -38,6 +38,7 @@ import { packageVersion } from 'nc-help';
import NcMetaIO, { META_TABLES } from './NcMetaIO';
import { promisify } from 'util';
import NcTemplateParser from '../../templateParser/NcTemplateParser';
+import UITypes from '../../sqlUi/UITypes';
const XC_PLUGIN_DET = 'XC_PLUGIN_DET';
@@ -128,19 +129,24 @@ export default class NcMetaMgr {
})
);
+ // todo: add multer middleware only for certain api calls
if (!process.env.NC_SERVERLESS_TYPE && !this.config.try) {
const upload = multer({
- dest: path.join(this.config.toolDir, 'uploads')
+ storage: multer.diskStorage({
+ // dest: path.join(this.config.toolDir, 'uploads')
+ })
});
- router.post(this.config.dashboardPath, upload.single('file'));
+ // router.post(this.config.dashboardPath, upload.single('file'));
+ router.post(this.config.dashboardPath, upload.any());
}
router.post(this.config.dashboardPath, (req, res, next) =>
this.handlePublicRequest(req, res, next)
);
+
// @ts-ignore
router.post(this.config.dashboardPath, async (req: any, res, next) => {
- if (req.file && req.body.json) {
+ if (req.files && req.body.json) {
req.body = JSON.parse(req.body.json);
}
if (req?.session?.passport?.user?.isAuthorized) {
@@ -209,7 +215,7 @@ export default class NcMetaMgr {
}
}
- if (req.file) {
+ if (req.files) {
await this.handleRequestWithFile(req, res, next);
} else {
await this.handleRequest(req, res, next);
@@ -219,11 +225,11 @@ export default class NcMetaMgr {
async (req: any, res) => {
try {
let output;
- if (req.file) {
+ if (req.files && req.body.json) {
req.body = JSON.parse(req.body.json);
output = await this.projectMgr
.getSqlMgr({ id: req.body.project_id })
- .handleRequestWithFile(req.body.api, req.body, req.file);
+ .handleRequestWithFile(req.body.api, req.body, req.files);
} else {
output = await this.projectMgr
.getSqlMgr({ id: req.body.project_id })
@@ -336,7 +342,7 @@ export default class NcMetaMgr {
}
public async handleRequestWithFile(req, res, next) {
- const [operation, args, file] = [req.body.api, req.body, req.file];
+ const [operation, args, file] = [req.body.api, req.body, req.files?.[0]];
let result;
try {
switch (operation) {
@@ -1230,43 +1236,15 @@ export default class NcMetaMgr {
const prependName = args.args.prependName?.length
? args.args.prependName.join('_') + '_'
: '';
- const fileName = `${prependName}${nanoid(6)}_${file.originalname}`;
- let destPath;
- if (args?.args?.public) {
- destPath = path.join('nc', 'public', 'files', 'uploads', ...appendPath);
- } else {
- destPath = path.join(
- 'nc',
- this.getProjectId(args),
- this.getDbAlias(args),
- 'uploads',
- ...appendPath
- );
- }
- let url = await this.storageAdapter.fileCreate(
- slash(path.join(destPath, fileName)),
- file
- );
- if (!url) {
- if (args?.args?.public) {
- url = `${req.ncSiteUrl}/dl/public/files/${
- appendPath?.length ? appendPath.join('/') + '/' : ''
- }${fileName}`;
- } else {
- url = `${req.ncSiteUrl}/dl/${this.getProjectId(
- args
- )}/${this.getDbAlias(args)}/${
- appendPath?.length ? appendPath.join('/') + '/' : ''
- }${fileName}`;
- }
- }
- return {
- url,
- title: file.originalname,
- mimetype: file.mimetype,
- size: file.size,
- icon: mimeIcons[path.extname(file.originalname).slice(1)] || undefined
- };
+ return await this._uploadFile({
+ prependName,
+ file,
+ storeInPublicFolder: args?.args?.public,
+ appendPath,
+ req,
+ dbAlias: this.getDbAlias(args),
+ projectId: this.getProjectId(args)
+ });
} catch (e) {
throw e;
} finally {
@@ -1274,6 +1252,54 @@ export default class NcMetaMgr {
}
}
+ private async _uploadFile({
+ prependName = '',
+ file,
+ storeInPublicFolder = false,
+ appendPath = [],
+ req,
+ projectId,
+ dbAlias
+ }: {
+ prependName?: string;
+ file: any;
+ storeInPublicFolder: boolean;
+ appendPath?: string[];
+ req: express.Request & any;
+ projectId?: string;
+ dbAlias?: string;
+ }) {
+ const fileName = `${prependName}${nanoid(6)}_${file.originalname}`;
+ let destPath;
+ if (storeInPublicFolder) {
+ destPath = path.join('nc', 'public', 'files', 'uploads', ...appendPath);
+ } else {
+ destPath = path.join('nc', projectId, dbAlias, 'uploads', ...appendPath);
+ }
+ let url = await this.storageAdapter.fileCreate(
+ slash(path.join(destPath, fileName)),
+ file
+ );
+ if (!url) {
+ if (storeInPublicFolder) {
+ url = `${req.ncSiteUrl}/dl/public/files/${
+ appendPath?.length ? appendPath.join('/') + '/' : ''
+ }${fileName}`;
+ } else {
+ url = `${req.ncSiteUrl}/dl/${projectId}/${dbAlias}/${
+ appendPath?.length ? appendPath.join('/') + '/' : ''
+ }${fileName}`;
+ }
+ }
+ return {
+ url,
+ title: file.originalname,
+ mimetype: file.mimetype,
+ size: file.size,
+ icon: mimeIcons[path.extname(file.originalname).slice(1)] || undefined
+ };
+ }
+
protected async initTwilio(overwrite = false): Promise {
const activeStorage = await this.xcMeta.metaGet(null, null, 'nc_plugins', {
active: true,
@@ -1294,7 +1320,11 @@ export default class NcMetaMgr {
}
protected async handlePublicRequest(req, res, next) {
- const args = req.body;
+ let args = req.body;
+
+ try {
+ if (req.body.json) args = JSON.parse(req.body.json);
+ } catch {}
let result;
try {
switch (args.api) {
@@ -1304,7 +1334,6 @@ export default class NcMetaMgr {
case 'getSharedViewData':
result = await this.getSharedViewData(req, args);
break;
-
case 'sharedViewGet':
result = await this.sharedViewGet(req, args);
break;
@@ -3723,12 +3752,18 @@ export default class NcMetaMgr {
const queryParams = JSON.parse(viewMeta.query_params);
// const meta = JSON.parse(viewMeta.meta);
- const fields: string[] = Object.keys(queryParams.showFields);
+ const fields: string[] = Object.keys(queryParams.showFields).filter(
+ k => queryParams.showFields[k]
+ );
const apiBuilder = this.app?.projectBuilders
?.find(pb => pb.id === sharedViewMeta.project_id)
?.apiBuilders?.find(ab => ab.dbAlias === sharedViewMeta.db_alias);
+ const tableMeta = (viewMeta.meta = apiBuilder?.getMeta(
+ sharedViewMeta.model_name
+ ));
+
const insertObject = Object.entries(args.args.data).reduce(
(obj, [key, val]) => {
if (fields.includes(key)) {
@@ -3745,7 +3780,31 @@ export default class NcMetaMgr {
}
}
+ const attachments = {};
+ for (const file of req.files || []) {
+ if (
+ fields.includes(file?.fieldname) &&
+ tableMeta.columns.find(
+ c => c._cn === file?.fieldname && c.uidt === UITypes.Attachment
+ )
+ ) {
+ attachments[file.fieldname] = attachments[file.fieldname] || [];
+ attachments[file.fieldname].push(
+ await this._uploadFile({
+ file,
+ storeInPublicFolder: true,
+ req
+ })
+ );
+ }
+ }
+
+ for (const [column, data] of Object.entries(attachments)) {
+ insertObject[column] = JSON.stringify(data);
+ }
+
const model = apiBuilder?.xcModels?.[sharedViewMeta.model_name];
+
if (model) {
req.query.form = viewMeta.view_name;
await model.nestedInsert(insertObject, null, req);