Browse Source

fix: enable attachment upload in shared view

re #687

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/743/head
Pranav C 3 years ago
parent
commit
afcb955071
  1. 2
      packages/nc-gui/components/project/appStore/inputs/attachment.vue
  2. 2
      packages/nc-gui/components/project/settings/xcMeta.vue
  3. 2
      packages/nc-gui/components/project/spreadsheet/components/editableCell.vue
  4. 89
      packages/nc-gui/components/project/spreadsheet/components/editableCell/editableAttachmentCell.vue
  5. 24
      packages/nc-gui/components/project/spreadsheet/components/editableCell/editableUrlCell.vue
  6. 2
      packages/nc-gui/components/project/spreadsheet/helpers/imageExt.js
  7. 31
      packages/nc-gui/components/project/spreadsheet/public/xcForm.vue
  8. 2
      packages/nc-gui/pages/projects/index.vue
  9. 31
      packages/nc-gui/store/sqlMgr.js
  10. 1
      packages/nocodb/src/interface/IStorageAdapter.ts
  11. 113
      packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts

2
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)

2
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'
},

2
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"
/>

89
packages/nc-gui/components/project/spreadsheet/components/editableCell/editableAttachmentCell.vue

@ -19,8 +19,8 @@
<div class="d-flex align-center img-container">
<div
v-for="(item,i) in localState"
:key="item.url"
v-for="(item,i) in (isPublicForm ? localFilesState : localState)"
:key="item.url || item.title"
class="thumbnail align-center justify-center d-flex"
>
<v-tooltip bottom>
@ -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)"
>
<template #placeholder>
<v-skeleton-loader
@ -44,12 +44,17 @@
/>
</template>
</v-img>
<v-icon v-else-if="item.icon" :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url,'_blank')">
<v-icon
v-else-if="item.icon"
:size="active ? 33 : 22"
v-on="on"
@click="openUrl(item.url || item.data,'_blank')"
>
{{
item.icon
}}
</v-icon>
<v-icon v-else :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url,'_blank')">
<v-icon v-else :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url|| item.data,'_blank')">
mdi-file
</v-icon>
</template>
@ -82,7 +87,13 @@
<v-card class="h-100 images-modal">
<v-card-text class="h-100 backgroundColor">
<div class="d-flex mx-2">
<v-btn v-if="_isUIAllowed('tableAttachment') && !isPublicGrid" small class="my-4 " :loading="uploading" @click="addFile">
<v-btn
v-if="_isUIAllowed('tableAttachment') && !isPublicGrid"
small
class="my-4 "
:loading="uploading"
@click="addFile"
>
<v-icon small class="mr-2">
mdi-link-variant
</v-icon>
@ -97,13 +108,18 @@
class="row"
@update="onOrderUpdate"
>
<v-col v-for="(item,i) in localState" :key="i" cols="4">
<v-col v-for="(item,i) in (isPublicForm ? localFilesState : localState)" :key="i" cols="4">
<v-card
class="modal-thumbnail-card align-center justify-center d-flex"
height="200px"
style="position: relative"
>
<v-icon v-if="_isUIAllowed('tableAttachment') && !isPublicGrid" small class="remove-icon" @click="removeItem(i)">
<v-icon
v-if="_isUIAllowed('tableAttachment') && !isPublicGrid"
small
class="remove-icon"
@click="removeItem(i)"
>
mdi-close-circle
</v-icon>
<v-icon color="grey" class="download-icon" @click.stop="downloadItem(item,i)">
@ -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)"
>
<v-icon v-else-if="item.icon" size="33" @click="openUrl(item.url,'_blank')">
<v-icon v-else-if="item.icon" size="33" @click="openUrl(item.url || item.data,'_blank')">
{{
item.icon
}}
</v-icon>
<v-icon v-else size="33" @click="openUrl(item.url,'_blank')">
<v-icon v-else size="33" @click="openUrl(item.url || item.data,'_blank')">
mdi-file
</v-icon>
</div>
@ -144,7 +160,7 @@
<template v-if="showImage && selectedImage">
<v-carousel v-model="carousel" height="calc(100vh - 100px)" hide-delimiters>
<v-carousel-item
v-for="(item,i) in localState"
v-for="(item,i) in (isPublicForm ? localFilesState : localState)"
:key="i"
>
<div class="mx-auto d-flex flex-column justify-center align-center" style="min-height:100px">
@ -158,7 +174,7 @@
<img
v-if="isImage(item.title)"
style="max-width:90vh;max-height:calc(100vh - 100px)"
:src="item.url"
:src="item.url || item.data"
>
<v-icon v-else-if="item.icon" size="55">
{{ item.icon }}
@ -183,7 +199,7 @@
show-arrows
>
<v-slide-item
v-for="(item,i) in localState"
v-for="(item,i) in (isPublicForm ? localFilesState : localState)"
:key="i"
>
<!-- <div class="d-flex justify-center" style="height:80px">-->
@ -198,7 +214,7 @@
<img
v-if="isImage(item.title)"
style="max-width:100%;max-height:100%"
:src="item.url"
:src="item.url || item.data"
>
<v-icon v-else-if="item.icon" size="48">
{{ 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) {
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 <http://www.gnu.org/licenses/>.
*
*/
-->

24
packages/nc-gui/components/project/spreadsheet/components/editableCell/editableUrlCell.vue

@ -50,3 +50,27 @@ input, textarea {
color: var(--v-textColor-base);
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
-->

2
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

31
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', {
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: this.localState,
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)
}

2
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,

31
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('json', JSON.stringify({ api: op, ...params, ...args, args: opArgs }))
// formData.append('project_id', params.project_id);

1
packages/nocodb/src/interface/IStorageAdapter.ts

@ -11,6 +11,7 @@ interface XcFile {
path: string;
mimetype: string;
size: number | string;
buffer?: any;
}
export { XcFile };

113
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,32 +1236,57 @@ export default class NcMetaMgr {
const prependName = args.args.prependName?.length
? args.args.prependName.join('_') + '_'
: '';
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 {
Tele.emit('evt', { evt_type: 'image:uploaded' });
}
}
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 (args?.args?.public) {
if (storeInPublicFolder) {
destPath = path.join('nc', 'public', 'files', 'uploads', ...appendPath);
} else {
destPath = path.join(
'nc',
this.getProjectId(args),
this.getDbAlias(args),
'uploads',
...appendPath
);
destPath = path.join('nc', projectId, dbAlias, 'uploads', ...appendPath);
}
let url = await this.storageAdapter.fileCreate(
slash(path.join(destPath, fileName)),
file
);
if (!url) {
if (args?.args?.public) {
if (storeInPublicFolder) {
url = `${req.ncSiteUrl}/dl/public/files/${
appendPath?.length ? appendPath.join('/') + '/' : ''
}${fileName}`;
} else {
url = `${req.ncSiteUrl}/dl/${this.getProjectId(
args
)}/${this.getDbAlias(args)}/${
url = `${req.ncSiteUrl}/dl/${projectId}/${dbAlias}/${
appendPath?.length ? appendPath.join('/') + '/' : ''
}${fileName}`;
}
@ -1267,11 +1298,6 @@ export default class NcMetaMgr {
size: file.size,
icon: mimeIcons[path.extname(file.originalname).slice(1)] || undefined
};
} catch (e) {
throw e;
} finally {
Tele.emit('evt', { evt_type: 'image:uploaded' });
}
}
protected async initTwilio(overwrite = false): Promise<void> {
@ -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);

Loading…
Cancel
Save