Browse Source

Merge pull request #2851 from LouisDelbosc/feat/export-excel

Feat/export excel
pull/2991/head
navi 2 years ago committed by GitHub
parent
commit
4c6288d9ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 113
      packages/nc-gui/components/project/spreadsheet/components/MoreActions.vue
  2. 1
      packages/nc-gui/lang/da.json
  3. 1
      packages/nc-gui/lang/de.json
  4. 1
      packages/nc-gui/lang/en.json
  5. 1
      packages/nc-gui/lang/es.json
  6. 1
      packages/nc-gui/lang/fa.json
  7. 1
      packages/nc-gui/lang/fi.json
  8. 1
      packages/nc-gui/lang/fr.json
  9. 1
      packages/nc-gui/lang/hr.json
  10. 1
      packages/nc-gui/lang/id.json
  11. 1
      packages/nc-gui/lang/it_IT.json
  12. 1
      packages/nc-gui/lang/iw.json
  13. 1
      packages/nc-gui/lang/ja.json
  14. 1
      packages/nc-gui/lang/ko.json
  15. 1
      packages/nc-gui/lang/lv.json
  16. 1
      packages/nc-gui/lang/nl.json
  17. 1
      packages/nc-gui/lang/no.json
  18. 1
      packages/nc-gui/lang/pl.json
  19. 1
      packages/nc-gui/lang/pt.json
  20. 1
      packages/nc-gui/lang/pt_BR.json
  21. 1
      packages/nc-gui/lang/ru.json
  22. 1
      packages/nc-gui/lang/sl.json
  23. 1
      packages/nc-gui/lang/sv.json
  24. 1
      packages/nc-gui/lang/th.json
  25. 1
      packages/nc-gui/lang/tr.json
  26. 1
      packages/nc-gui/lang/uk.json
  27. 1
      packages/nc-gui/lang/vi.json
  28. 1
      packages/nc-gui/lang/zh_CN.json
  29. 1
      packages/nc-gui/lang/zh_HK.json
  30. 3
      packages/nc-gui/lang/zh_TW.json
  31. 2
      packages/nc-gui/package-lock.json
  32. 2
      packages/nc-gui/package.json
  33. 1
      packages/nocodb-sdk/src/lib/globals.ts
  34. 142
      packages/nocodb/package-lock.json
  35. 5
      packages/nocodb/package.json
  36. 33
      packages/nocodb/src/lib/meta/api/dataApis/dataAliasExportApis.ts
  37. 64
      packages/nocodb/src/lib/meta/api/dataApis/helpers.ts
  38. 109
      packages/nocodb/src/lib/meta/api/publicApis/publicDataExportApis.ts
  39. 3
      packages/nocodb/src/lib/utils/projectAcl.ts
  40. 6
      scripts/cypress/integration/spec/roleValidation.spec.js
  41. 13
      scripts/cypress/support/page_objects/mainPage.js

113
packages/nc-gui/components/project/spreadsheet/components/MoreActions.vue

@ -28,6 +28,15 @@
</span>
</v-list-item-title>
</v-list-item>
<v-list-item v-t="['a:actions:download-excel']" dense @click="exportExcel">
<v-list-item-title>
<v-icon small class="mr-1"> mdi-download-outline </v-icon>
<span class="caption">
<!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }}
</span>
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="_isUIAllowed('csvImport') && !isView"
v-t="['a:actions:upload-csv']"
@ -87,6 +96,7 @@
<script>
import FileSaver from 'file-saver';
import * as XLSX from 'xlsx';
import { ExportTypes } from 'nocodb-sdk';
import DropOrSelectFileModal from '~/components/import/DropOrSelectFileModal';
import ColumnMappingModal from '~/components/project/spreadsheet/components/importExport/ColumnMappingModal';
@ -220,51 +230,34 @@ export default {
})
);
},
async exportCsv() {
async exportExcel() {
let offset = 0;
let c = 1;
try {
while (!isNaN(offset) && offset > -1) {
let res;
if (this.publicViewId) {
res = await this.$api.public.csvExport(this.publicViewId, ExportTypes.CSV, {
responseType: 'blob',
query: {
fields:
this.queryParams &&
this.queryParams.fieldsOrder &&
this.queryParams.fieldsOrder.filter(c => this.queryParams.showFields[c]),
offset,
sortArrJson: JSON.stringify(
this.reqPayload &&
this.reqPayload.sorts &&
this.reqPayload.sorts.map(({ fk_column_id, direction }) => ({
direction,
fk_column_id,
}))
),
filterArrJson: JSON.stringify(this.reqPayload && this.reqPayload.filters),
},
headers: {
'xc-password': this.reqPayload && this.reqPayload.password,
},
});
const res = await this.getExportData({ offset, exportType: ExportTypes.EXCEL, responseType: 'base64' });
const workbook = XLSX.read(res.data, { type: 'base64' });
XLSX.writeFile(workbook, `${this.meta.title}_exported_${c++}.xlsx`);
offset = +res.headers['nc-export-offset'];
if (offset > -1) {
this.$toast.info('Downloading more files').goAway(3000);
} else {
res = await this.$api.dbViewRow.export(
'noco',
this.projectName,
this.meta.title,
this.selectedView.title,
ExportTypes.CSV,
{
responseType: 'blob',
query: {
offset,
},
}
);
this.$toast.success('Successfully exported all table data').goAway(3000);
}
}
} catch (e) {
console.log(e);
this.$toast.error(e.message).goAway(3000);
}
},
async exportCsv() {
let offset = 0;
let c = 1;
try {
while (!isNaN(offset) && offset > -1) {
const res = await this.getExportData({ offset, exportType: ExportTypes.CSV, responseType: 'blob' })
const { data } = res;
offset = +res.headers['nc-export-offset'];
@ -281,6 +274,48 @@ export default {
this.$toast.error(e.message).goAway(3000);
}
},
async getExportData({ offset, exportType, responseType }) {
let res;
if (this.publicViewId) {
res = await this.$api.public.csvExport(this.publicViewId, exportType, {
responseType,
query: {
fields:
this.queryParams &&
this.queryParams.fieldsOrder &&
this.queryParams.fieldsOrder.filter(c => this.queryParams.showFields[c]),
offset,
sortArrJson: JSON.stringify(
this.reqPayload &&
this.reqPayload.sorts &&
this.reqPayload.sorts.map(({ fk_column_id, direction }) => ({
direction,
fk_column_id,
}))
),
filterArrJson: JSON.stringify(this.reqPayload && this.reqPayload.filters),
},
headers: {
'xc-password': this.reqPayload && this.reqPayload.password,
},
});
} else {
res = await this.$api.dbViewRow.export(
'noco',
this.projectName,
this.meta.title,
this.selectedView.title,
exportType,
{
responseType,
query: {
offset,
},
}
);
}
return res;
},
async importData(columnMappings) {
try {
const data = this.parsedCsv.data;

1
packages/nc-gui/lang/da.json

@ -309,6 +309,7 @@
"importExcel": "Import Excel.",
"importCSV": "Import CSV.",
"downloadCSV": "Download som CSV.",
"downloadExcel": "Download som XLSX.",
"uploadCSV": "Upload CSV.",
"import": "Importere",
"importMetadata": "Import metadata.",

1
packages/nc-gui/lang/de.json

@ -309,6 +309,7 @@
"importExcel": "Import Excel",
"importCSV": "Import CSV",
"downloadCSV": "Download als CSV",
"downloadExcel": "Download als XLSX",
"uploadCSV": "Hochladen CSV",
"import": "Importieren",
"importMetadata": "Metadaten importieren",

1
packages/nc-gui/lang/en.json

@ -309,6 +309,7 @@
"importExcel": "Import Excel",
"importCSV": "Import CSV",
"downloadCSV": "Download as CSV",
"downloadExcel": "Download as XLSX",
"uploadCSV": "Upload CSV",
"import": "Import",
"importMetadata": "Import Metadata",

1
packages/nc-gui/lang/es.json

@ -309,6 +309,7 @@
"importExcel": "Importar Excel",
"importCSV": "Import CSV",
"downloadCSV": "Descargar como CSV",
"downloadExcel": "Descargar como XLSX",
"uploadCSV": "Subir CSV",
"import": "Importar",
"importMetadata": "Importar metadatos",

1
packages/nc-gui/lang/fa.json

@ -309,6 +309,7 @@
"importExcel": "وارد کردن فایل Excel",
"importCSV": "Import CSV",
"downloadCSV": "دانلود بهعنوان CSV",
"downloadXLSX": "دانلود بهعنوان XLSX",
"uploadCSV": "بارگذاری CSV",
"import": "وارد کردن",
"importMetadata": "وارد کردن فراداده",

1
packages/nc-gui/lang/fi.json

@ -309,6 +309,7 @@
"importExcel": "Tuonti excel",
"importCSV": "Import CSV",
"downloadCSV": "Lataa CSV",
"downloadXLSX": "Lataa XLSX",
"uploadCSV": "Lataa CSV",
"import": "Tuonti",
"importMetadata": "Tuo metatieto",

1
packages/nc-gui/lang/fr.json

@ -309,6 +309,7 @@
"importExcel": "Importer depuis Excel",
"importCSV": "Import CSV",
"downloadCSV": "Télécharger comme CSV",
"downloadExcel": "Télécharger comme XLSX",
"uploadCSV": "Téléverser un CSV",
"import": "Importer",
"importMetadata": "Importer les métadonnées",

1
packages/nc-gui/lang/hr.json

@ -309,6 +309,7 @@
"importExcel": "Uvoz Excel",
"importCSV": "Import CSV",
"downloadCSV": "Preuzmite kao CSV",
"downloadExcel": "Preuzmite kao XLSX",
"uploadCSV": "Prenesite CSV",
"import": "Uvoz",
"importMetadata": "Uvoz metapodataka",

1
packages/nc-gui/lang/id.json

@ -309,6 +309,7 @@
"importExcel": "Impor Excel.",
"importCSV": "Import CSV",
"downloadCSV": "Unduh sebagai CSV.",
"downloadExcel": "Unduh sebagai XLSX.",
"uploadCSV": "Unggah CSV.",
"import": "Impor",
"importMetadata": "Impor Metadata.",

1
packages/nc-gui/lang/it_IT.json

@ -309,6 +309,7 @@
"importExcel": "Importa Excel.",
"importCSV": "Import CSV",
"downloadCSV": "Scarica come CSV.",
"downloadExcel": "Scarica come XLSX.",
"uploadCSV": "Carica CSV.",
"import": "Importa",
"importMetadata": "Importa metadati",

1
packages/nc-gui/lang/iw.json

@ -309,6 +309,7 @@
"importExcel": "ייבוא Excel",
"importCSV": "Import CSV",
"downloadCSV": "הורד כמו CSV.",
"downloadExcel": "הורד כמו XLSX.",
"uploadCSV": "העלה CSV.",
"import": ְבוּא",
"importMetadata": "ייבוא מטא נתונים",

1
packages/nc-gui/lang/ja.json

@ -309,6 +309,7 @@
"importExcel": "エクセルファイルをインポート",
"importCSV": "Import CSV",
"downloadCSV": "CSVをダウンロード",
"downloadExcel": "XLSXをダウンロード",
"uploadCSV": "CSVをアップロード",
"import": "インポート",
"importMetadata": "メタデータをインポート",

1
packages/nc-gui/lang/ko.json

@ -309,6 +309,7 @@
"importExcel": "엑셀 가져오기",
"importCSV": "CSV 가져오기",
"downloadCSV": "CSV 다운로드",
"downloadExcel": "XLSX 다운로드",
"uploadCSV": "CSV 업로드",
"import": "가져오기",
"importMetadata": "메타 데이터 가져오기",

1
packages/nc-gui/lang/lv.json

@ -309,6 +309,7 @@
"importExcel": "Importēt Excel",
"importCSV": "Import CSV",
"downloadCSV": "Lejupielādēt kā CSV",
"downloadExcel": "Lejupielādēt kā XLSX",
"uploadCSV": "Augšupielādēt CSV",
"import": "Importēt",
"importMetadata": "Importēt metadatus",

1
packages/nc-gui/lang/nl.json

@ -309,6 +309,7 @@
"importExcel": "Excel importeren",
"importCSV": "Import CSV",
"downloadCSV": "Download als CSV",
"downloadExcel": "Download als XLSX",
"uploadCSV": "Upload CSV",
"import": "Importeren",
"importMetadata": "Importeer Metadata",

1
packages/nc-gui/lang/no.json

@ -309,6 +309,7 @@
"importExcel": "Importer Excel.",
"importCSV": "Import CSV",
"downloadCSV": "Last ned som CSV.",
"downloadExcel": "Last ned som XLSX.",
"uploadCSV": "Last opp CSV.",
"import": "Importer",
"importMetadata": "Importer metadata",

1
packages/nc-gui/lang/pl.json

@ -309,6 +309,7 @@
"importExcel": "Importuj Excel.",
"importCSV": "Import CSV",
"downloadCSV": "Pobierz jako CSV.",
"downloadExcel": "Pobierz jako XLSX.",
"uploadCSV": "Prześlij CSV.",
"import": "Import",
"importMetadata": "Importuj metadane",

1
packages/nc-gui/lang/pt.json

@ -309,6 +309,7 @@
"importExcel": "Importar Excel.",
"importCSV": "Import CSV",
"downloadCSV": "Baixe como CSV.",
"downloadExcel": "Baixe como XLSX.",
"uploadCSV": "Carregar CSV.",
"import": "Importar",
"importMetadata": "Importar Metadados",

1
packages/nc-gui/lang/pt_BR.json

@ -309,6 +309,7 @@
"importExcel": "Importar Excel.",
"importCSV": "Import CSV",
"downloadCSV": "Baixe como CSV.",
"downloadExcel": "Baixe como XLSX.",
"uploadCSV": "Carregar CSV.",
"import": "Importar",
"importMetadata": "Importar Metadados",

1
packages/nc-gui/lang/ru.json

@ -308,6 +308,7 @@
"importExcel": "Импорт из Excel",
"importCSV": "Import CSV",
"downloadCSV": "Скачать как CSV.",
"downloadExcel": "Скачать как XLSX.",
"uploadCSV": "Загрузить CSV.",
"import": "Импортировать",
"importMetadata": "Импорт метаданных",

1
packages/nc-gui/lang/sl.json

@ -309,6 +309,7 @@
"importExcel": "Uvoz Excel.",
"importCSV": "Import CSV",
"downloadCSV": "Prenesite kot CSV.",
"downloadExcel": "Prenesite kot XLSX.",
"uploadCSV": "Upload CSV.",
"import": "Uvozi",
"importMetadata": "Uvozi metapodatkov",

1
packages/nc-gui/lang/sv.json

@ -309,6 +309,7 @@
"importExcel": "Import excel",
"importCSV": "Import CSV",
"downloadCSV": "Hämta som CSV",
"downloadExcel": "Hämta som XLSX",
"uploadCSV": "Ladda upp CSV",
"import": "Importera",
"importMetadata": "Importera metadata",

1
packages/nc-gui/lang/th.json

@ -309,6 +309,7 @@
"importExcel": "นำเขา Excel",
"importCSV": "Import CSV",
"downloadCSV": "ดาวนโหลดเปน CSV",
"downloadExcel": "ดาวนโหลดเปน XLSX",
"uploadCSV": "อปโหลด CSV",
"import": "นำเขา",
"importMetadata": "เมทาดานำเขา",

1
packages/nc-gui/lang/tr.json

@ -309,6 +309,7 @@
"importExcel": "Excel'i içe aktar",
"importCSV": "Import CSV",
"downloadCSV": "CSV olarak indir",
"downloadExcel": "XLSX olarak indir",
"uploadCSV": "CSV yükle",
"import": "İçe aktar",
"importMetadata": "Meta verilerini içe aktar",

1
packages/nc-gui/lang/uk.json

@ -309,6 +309,7 @@
"importExcel": "Імпорт Excel",
"importCSV": "Import CSV",
"downloadCSV": "Завантажити як CSV",
"downloadExcel": "Завантажити як XLSX",
"uploadCSV": "Завантажити CSV",
"import": "Імпортувати",
"importMetadata": "Імпорт метаданих",

1
packages/nc-gui/lang/vi.json

@ -309,6 +309,7 @@
"importExcel": "Nhập Excel.",
"importCSV": "Import CSV",
"downloadCSV": "Tải về dưới dạng CSV.",
"downloadExcel": "Tải về dưới dạng XLSX.",
"uploadCSV": "Tải lên CSV.",
"import": "Nhập khẩu",
"importMetadata": "Nhập siêu dữ liệu",

1
packages/nc-gui/lang/zh_CN.json

@ -309,6 +309,7 @@
"importExcel": "导入Excel",
"importCSV": "Import CSV",
"downloadCSV": "下载为CSV",
"downloadExcel": "下载为XLSX",
"uploadCSV": "上传CSV",
"import": "导入",
"importMetadata": "导入元数据",

1
packages/nc-gui/lang/zh_HK.json

@ -309,6 +309,7 @@
"importExcel": "導入Excel.",
"importCSV": "Import CSV",
"downloadCSV": "下載為CSV.",
"downloadExcel": "下載為XLSX.",
"uploadCSV": "上傳CSV.",
"import": "導出去file",
"importMetadata": "import metadata",

3
packages/nc-gui/lang/zh_TW.json

@ -309,6 +309,7 @@
"importExcel": "匯入 Excel",
"importCSV": "匯入 CSV",
"downloadCSV": "下載為 CSV",
"downloadExcel": "下載為 XLSX",
"uploadCSV": "上傳 CSV",
"import": "匯入",
"importMetadata": "匯入中繼資料",
@ -517,4 +518,4 @@
"futureRelease": "即將推出!"
}
}
}
}

2
packages/nc-gui/package-lock.json generated

@ -56,7 +56,7 @@
"vuelidate": "^0.7.6",
"vuetify-datetime-picker": "^2.1.1",
"vuex-persistedstate": "^3.1.0",
"xlsx": "^0.17.3",
"xlsx": "^0.17.5",
"xterm": "^4.8.1",
"xterm-addon-fit": "^0.4.0",
"xterm-addon-web-links": "^0.4.0"

2
packages/nc-gui/package.json

@ -60,7 +60,7 @@
"vuelidate": "^0.7.6",
"vuetify-datetime-picker": "^2.1.1",
"vuex-persistedstate": "^3.1.0",
"xlsx": "^0.17.3",
"xlsx": "^0.17.5",
"xterm": "^4.8.1",
"xterm-addon-fit": "^0.4.0",
"xterm-addon-web-links": "^0.4.0"

1
packages/nocodb-sdk/src/lib/globals.ts

@ -12,6 +12,7 @@ export enum RelationTypes {
}
export enum ExportTypes {
EXCEL = 'excel',
CSV = 'csv',
}

142
packages/nocodb/package-lock.json generated

@ -100,7 +100,8 @@
"unique-names-generator": "^4.3.1",
"uuid": "^8.2.0",
"validator": "^13.1.1",
"xc-core-ts": "^0.1.0"
"xc-core-ts": "^0.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@bitjson/npm-scripts-info": "^1.0.0",
@ -2501,6 +2502,14 @@
"integrity": "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==",
"dev": true
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@ -4387,6 +4396,18 @@
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
"optional": true
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chai": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz",
@ -4926,6 +4947,14 @@
"node": ">=10"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/collection-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@ -9887,6 +9916,14 @@
"node": ">= 0.6"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fragment-cache": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
@ -19930,6 +19967,17 @@
"node": ">= 0.6"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/sshpk": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
@ -24426,6 +24474,22 @@
"node": ">= 6.4.0"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
@ -24582,6 +24646,26 @@
"node": ">=0.10.0"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
@ -26624,6 +26708,11 @@
"integrity": "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==",
"dev": true
},
"adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="
},
"agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@ -28156,6 +28245,15 @@
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
"optional": true
},
"cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"requires": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
}
},
"chai": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz",
@ -28569,6 +28667,11 @@
}
}
},
"codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="
},
"collection-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@ -32481,6 +32584,11 @@
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
},
"frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="
},
"fragment-cache": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
@ -40312,6 +40420,14 @@
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="
},
"ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"requires": {
"frac": "~1.1.2"
}
},
"sshpk": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
@ -43869,6 +43985,16 @@
"triple-beam": "^1.3.0"
}
},
"wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="
},
"word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
@ -43985,6 +44111,20 @@
}
}
},
"xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"requires": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
}
},
"xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",

5
packages/nocodb/package.json

@ -186,7 +186,8 @@
"unique-names-generator": "^4.3.1",
"uuid": "^8.2.0",
"validator": "^13.1.1",
"xc-core-ts": "^0.1.0"
"xc-core-ts": "^0.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@bitjson/npm-scripts-info": "^1.0.0",
@ -263,4 +264,4 @@
"**/*.spec.js"
]
}
}
}

33
packages/nocodb/src/lib/meta/api/dataApis/dataAliasExportApis.ts

@ -1,12 +1,35 @@
import { Request, Response, Router } from 'express';
import * as XLSX from 'xlsx';
import ncMetaAclMw from '../../helpers/ncMetaAclMw';
import {
extractCsvData,
extractXlsxData,
getViewAndModelFromRequestByAliasOrId,
} from './helpers';
import apiMetrics from '../../helpers/apiMetrics';
import View from '../../../models/View';
async function excelDataExport(req: Request, res: Response) {
const {model, view} = await getViewAndModelFromRequestByAliasOrId(req);
let targetView = view;
if (!targetView) {
targetView = await View.getDefaultView(model.id);
}
const { offset, elapsed, data } = await extractXlsxData(targetView, req);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, data, targetView.title);
const buf = XLSX.write(wb, { type: "base64", bookType: "xlsx" });
res.set({
'Access-Control-Expose-Headers': 'nc-export-offset',
'nc-export-offset': offset,
'nc-export-elapsed-time': elapsed,
'Content-Disposition': `attachment; filename="${encodeURI(
targetView.title
)}-export.xlsx"`,
});
res.end(buf);
}
async function csvDataExport(req: Request, res: Response) {
const { model, view } = await getViewAndModelFromRequestByAliasOrId(req);
let targetView = view;
@ -38,5 +61,15 @@ router.get(
apiMetrics,
ncMetaAclMw(csvDataExport, 'exportCsv')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/export/excel',
apiMetrics,
ncMetaAclMw(excelDataExport, 'exportExcel')
);
router.get(
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/export/excel',
apiMetrics,
ncMetaAclMw(excelDataExport, 'exportExcel')
);
export default router;

64
packages/nocodb/src/lib/meta/api/dataApis/helpers.ts

@ -8,6 +8,7 @@ import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import { isSystemColumn, UITypes } from 'nocodb-sdk';
import { nocoExecute } from 'nc-help';
import * as XLSX from 'xlsx';
import Column from '../../../models/Column';
import LookupColumn from '../../../models/LookupColumn';
import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn';
@ -36,6 +37,32 @@ export async function getViewAndModelFromRequestByAliasOrId(
return { model, view };
}
export async function extractXlsxData(view: View, req: Request) {
const base = await Base.get(view.base_id);
await view.getModelWithInfo();
await view.getColumns();
view.model.columns = view.columns
.filter((c) => c.show)
.map(
(c) =>
new Column({ ...c, ...view.model.columnsById[c.fk_column_id] } as any)
)
.filter((column) => !isSystemColumn(column) || view.show_system_fields);
const baseModel = await Model.getBaseModelSQL({
id: view.model.id,
viewId: view?.id,
dbDriver: NcConnectionMgrv2.get(base),
});
const { offset, dbRows, elapsed } = await getDbRows(baseModel, view, req);
const data = XLSX.utils.json_to_sheet(dbRows);
return { offset, dbRows, elapsed, data };
}
export async function extractCsvData(view: View, req: Request) {
const base = await Base.get(view.base_id);
@ -56,11 +83,27 @@ export async function extractCsvData(view: View, req: Request) {
dbDriver: NcConnectionMgrv2.get(base),
});
const { offset, dbRows, elapsed } = await getDbRows(baseModel, view, req);
const data = papaparse.unparse(
{
fields: view.model.columns.map((c) => c.title),
data: dbRows,
},
{
escapeFormulae: true,
}
);
return { offset, dbRows, elapsed, data };
}
async function getDbRows(baseModel, view: View, req: Request) {
let offset = +req.query.offset || 0;
const limit = 100;
// const size = +process.env.NC_EXPORT_MAX_SIZE || 1024;
const timeout = +process.env.NC_EXPORT_MAX_TIMEOUT || 5000;
const csvRows = [];
const dbRows = [];
const startTime = process.hrtime();
let elapsed, temp;
@ -89,30 +132,19 @@ export async function extractCsvData(view: View, req: Request) {
}
for (const row of rows) {
const csvRow = { ...row };
const dbRow = { ...row };
for (const column of view.model.columns) {
if (isSystemColumn(column) && !view.show_system_fields) continue;
csvRow[column.title] = await serializeCellValue({
dbRow[column.title] = await serializeCellValue({
value: row[column.title],
column,
});
}
csvRows.push(csvRow);
dbRows.push(dbRow);
}
}
const data = papaparse.unparse(
{
fields: view.model.columns.map((c) => c.title),
data: csvRows,
},
{
escapeFormulae: true,
}
);
return { offset, csvRows, elapsed, data };
return { offset, dbRows, elapsed };
}
export async function serializeCellValue({

109
packages/nocodb/src/lib/meta/api/publicApis/publicDataExportApis.ts

@ -1,4 +1,5 @@
import { Request, Response, Router } from 'express';
import * as XLSX from 'xlsx';
import View from '../../../models/View';
import Model from '../../../models/Model';
import Base from '../../../models/Base';
@ -12,8 +13,38 @@ import LookupColumn from '../../../models/LookupColumn';
import catchError, { NcError } from '../../helpers/catchError';
import getAst from '../../../db/sql-data-mapper/lib/sql/helpers/getAst';
async function exportExcel(req: Request, res: Response) {
const view = await View.getByUUID(req.params.publicDataUuid);
if (!view) NcError.notFound('Not found');
if (view.type !== ViewTypes.GRID) NcError.notFound('Not found');
if (view.password && view.password !== req.headers?.['xc-password']) {
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
}
const model = await view.getModelWithInfo();
await view.getColumns();
const { offset, dbRows, elapsed } = await getDbRows(model, view, req);
const data = XLSX.utils.json_to_sheet(dbRows);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, data, view.title);
const buf = XLSX.write(wb, { type: "base64", bookType: "xlsx" });
res.set({
'Access-Control-Expose-Headers': 'nc-export-offset',
'nc-export-offset': offset,
'nc-export-elapsed-time': elapsed,
'Content-Disposition': `attachment; filename="${encodeURI(
view.title
)}-export.xlsx"`,
});
res.end(buf);
}
async function exportCsv(req: Request, res: Response) {
const view = await View.getByUUID(req.params.publicDataUuid);
const fields = req.query.fields;
if (!view) NcError.notFound('Not found');
if (view.type !== ViewTypes.GRID) NcError.notFound('Not found');
@ -25,6 +56,40 @@ async function exportCsv(req: Request, res: Response) {
const model = await view.getModelWithInfo();
await view.getColumns();
const { offset, dbRows, elapsed } = await getDbRows(model, view, req);
const data = papaparse.unparse(
{
fields: model.columns
.sort((c1, c2) =>
Array.isArray(fields)
? fields.indexOf(c1.title as any) - fields.indexOf(c2.title as any)
: 0
)
.filter(
(c) =>
!fields || !Array.isArray(fields) || fields.includes(c.title as any)
)
.map((c) => c.title),
data: dbRows,
},
{
escapeFormulae: true,
}
);
res.set({
'Access-Control-Expose-Headers': 'nc-export-offset',
'nc-export-offset': offset,
'nc-export-elapsed-time': elapsed,
'Content-Disposition': `attachment; filename="${encodeURI(
view.title
)}-export.csv"`,
});
res.send(data);
}
async function getDbRows(model, view: View, req: Request) {
view.model.columns = view.columns
.filter((c) => c.show)
.map(
@ -35,7 +100,6 @@ async function exportCsv(req: Request, res: Response) {
if (!model) NcError.notFound('Table not found');
const fields = req.query.fields;
const listArgs: any = { ...req.query };
try {
listArgs.filterArr = JSON.parse(listArgs.filterArrJson);
@ -62,7 +126,7 @@ async function exportCsv(req: Request, res: Response) {
const limit = 100;
// const size = +process.env.NC_EXPORT_MAX_SIZE || 1024;
const timeout = +process.env.NC_EXPORT_MAX_TIMEOUT || 5000;
const csvRows = [];
const dbRows = [];
const startTime = process.hrtime();
let elapsed, temp;
@ -86,47 +150,18 @@ async function exportCsv(req: Request, res: Response) {
}
for (const row of rows) {
const csvRow = { ...row };
const dbRow = { ...row };
for (const column of view.model.columns) {
csvRow[column.title] = await serializeCellValue({
dbRow[column.title] = await serializeCellValue({
value: row[column.title],
column,
});
}
csvRows.push(csvRow);
dbRows.push(dbRow);
}
}
const data = papaparse.unparse(
{
fields: model.columns
.sort((c1, c2) =>
Array.isArray(fields)
? fields.indexOf(c1.title as any) - fields.indexOf(c2.title as any)
: 0
)
.filter(
(c) =>
!fields || !Array.isArray(fields) || fields.includes(c.title as any)
)
.map((c) => c.title),
data: csvRows,
},
{
escapeFormulae: true,
}
);
res.set({
'Access-Control-Expose-Headers': 'nc-export-offset',
'nc-export-offset': offset,
'nc-export-elapsed-time': elapsed,
'Content-Disposition': `attachment; filename="${encodeURI(
view.title
)}-export.csv"`,
});
res.send(data);
return { offset, dbRows, elapsed };
}
async function serializeCellValue({
@ -198,4 +233,8 @@ router.get(
'/api/v1/db/public/shared-view/:publicDataUuid/rows/export/csv',
catchError(exportCsv)
);
router.get(
'/api/v1/db/public/shared-view/:publicDataUuid/rows/export/excel',
catchError(exportExcel)
);
export default router;

3
packages/nocodb/src/lib/utils/projectAcl.ts

@ -27,6 +27,7 @@ export default {
dataGroupBy: true,
commentsCount: true,
exportCsv: true,
exportExcel: true,
viewList: true,
columnList: true,
@ -142,6 +143,7 @@ export default {
// project
projectGet: true,
exportCsv: true,
exportExcel: true,
//table
tableGet: true,
@ -205,6 +207,7 @@ export default {
dataGroupBy: true,
commentsCount: true,
exportCsv: true,
exportExcel: true,
// sort & filter
sortList: true,

6
scripts/cypress/integration/spec/roleValidation.spec.js

@ -217,7 +217,7 @@ export function _viewMenu(roleType, previewMode, navDrawListCnt) {
// let navDrawListCnt = 2;
// Download CSV
let actionsMenuItemsCnt = 1;
let actionsMenuItemsCnt = 2;
cy.openTableTab(columnName, 25);
@ -234,10 +234,10 @@ export function _viewMenu(roleType, previewMode, navDrawListCnt) {
if (roleType == "owner" || roleType == "creator") {
navDrawListCnt = 3;
// Download CSV / Upload CSV / Shared View List / Webhook
actionsMenuItemsCnt = 4;
actionsMenuItemsCnt = 5;
} else if (roleType == "editor") {
// Download CSV / Upload CSV
actionsMenuItemsCnt = 2;
actionsMenuItemsCnt = 3;
}
cy.get(".v-navigation-drawer__content")

13
scripts/cypress/support/page_objects/mainPage.js

@ -251,22 +251,27 @@ export class _mainPage {
shareViewList = () => {
cy.get(".nc-actions-menu-btn").click();
return cy.getActiveMenu().find('[role="menuitem"]').eq(2);
return cy.getActiveMenu().find('[role="menuitem"]').contains("Shared View List");
};
downloadCsv = () => {
cy.get(".nc-actions-menu-btn").click();
return cy.getActiveMenu().find('[role="menuitem"]').eq(0);
return cy.getActiveMenu().find('[role="menuitem"]').contains("Download as CSV");
};
downloadExcel = () => {
cy.get(".nc-actions-menu-btn").click();
return cy.getActiveMenu().find('[role="menuitem"]').contains("Download as XLSX");
};
uploadCsv = () => {
cy.get(".nc-actions-menu-btn").click();
return cy.getActiveMenu().find('[role="menuitem"]').eq(1);
return cy.getActiveMenu().find('[role="menuitem"]').contains("Upload CSV");
};
automations = () => {
cy.get(".nc-actions-menu-btn").click();
return cy.getActiveMenu().find('[role="menuitem"]').eq(3);
return cy.getActiveMenu().find('[role="menuitem"]').contains("Webhooks");
};
hideField = (field) => {

Loading…
Cancel
Save