Browse Source

Merge branch 'develop' into fix/knex-binding

pull/2424/head
Wing-Kam Wong 2 years ago
parent
commit
653cc09c68
  1. 12
      .github/workflows/ci-cd.yml
  2. 12
      package.json
  3. 27
      packages/nc-gui/components/ProjectTabs.vue
  4. 20
      packages/nc-gui/components/import/ImportFromAirtable.vue
  5. 484
      packages/nc-gui/components/import/JSONImport.vue
  6. 1
      packages/nc-gui/components/import/templateParsers/CSVTemplateAdapter.js
  7. 150
      packages/nc-gui/components/import/templateParsers/JSONTemplateAdapter.js
  8. 21
      packages/nc-gui/components/import/templateParsers/JSONUrlTemplateAdapter.js
  9. 58
      packages/nc-gui/components/import/templateParsers/parserHelpers.js
  10. 3
      packages/nc-gui/components/monaco/MonacoJsonEditor.js
  11. 16
      packages/nc-plugin/package-lock.json
  12. 14
      packages/noco-blog/package-lock.json
  13. 28
      packages/noco-docs-prev/package-lock.json
  14. 3
      packages/noco-docs/content/en/developer-resources/rest-apis.md
  15. 28
      packages/noco-docs/package-lock.json
  16. 6
      packages/noco-i18n/package-lock.json
  17. 3
      packages/nocodb-sdk/src/index.ts
  18. 20
      packages/nocodb-sdk/src/lib/Api.ts
  19. 30
      packages/nocodb-sdk/src/lib/TemplateGenerator.ts
  20. 10
      packages/nocodb/src/__tests__/restv2.test.ts
  21. 8
      packages/nocodb/src/lib/meta/api/columnApis.ts
  22. 8
      packages/nocodb/src/lib/meta/api/metaDiffApis.ts
  23. 4
      packages/nocodb/src/lib/meta/api/projectApis.ts
  24. 55
      packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts
  25. 8
      packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts
  26. 7
      packages/nocodb/src/lib/meta/api/userApi/userApis.ts
  27. 11
      packages/nocodb/src/lib/meta/api/utilApis.ts
  28. 3
      packages/nocodb/src/lib/models/User.ts
  29. 6
      scripts/cypress/integration/common/1a_table_operations.js
  30. 2
      scripts/cypress/integration/common/1d_pg_table_view_drag_drop_reorder.js
  31. 2
      scripts/cypress/integration/common/1d_table_view_drag_drop_reorder.js
  32. 4
      scripts/cypress/integration/common/2a_table_with_belongs_to_colulmn.js
  33. 4
      scripts/cypress/integration/common/2b_table_with_m2m_column.js
  34. 2
      scripts/cypress/integration/common/4c_form_view_detailed.js
  35. 6
      scripts/cypress/integration/common/4d_table_view_grid_locked.js
  36. 6
      scripts/cypress/integration/common/4e_form_view_share.js
  37. 30
      scripts/cypress/integration/common/4f_grid_view_share.js
  38. 28
      scripts/cypress/integration/common/4f_pg_grid_view_share.js
  39. 2
      scripts/cypress/integration/common/5a_user_role.js
  40. 4
      scripts/cypress/integration/common/6b_downloadCsv.js
  41. 2
      scripts/cypress/integration/common/6f_attachments.js
  42. 2
      scripts/cypress/integration/spec/roleValidation.spec.js
  43. 23
      scripts/sdk/swagger.json

12
.github/workflows/ci-cd.yml

@ -49,7 +49,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -94,7 +93,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -139,7 +137,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -184,7 +181,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
@ -229,7 +225,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -274,7 +269,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -319,7 +313,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -364,7 +357,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -409,7 +401,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -454,7 +445,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -499,7 +489,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
@ -544,7 +533,6 @@ jobs:
uses: cypress-io/github-action@v2
with:
start: |
npm run build:common
npm run start:xcdb-api:cache
npm run start:web
docker-compose -f ./scripts/docker-compose-cypress.yml up -d

12
package.json

@ -15,12 +15,12 @@
"scripts": {
"build:common": "cd ./packages/nocodb-sdk; npm install; npm run build",
"install:common": "cd ./packages/nocodb; npm install ../nocodb-sdk; cd ../nc-gui; npm install ../nocodb-sdk",
"start:api": "cd ./packages/nocodb; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:xcdb-api": "cd ./packages/nocodb; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:api:cache": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:api:cache:pg": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress:pg",
"start:xcdb-api:cache": "cd ./packages/nocodb; npm install; NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:web": "cd ./packages/nc-gui; npm install; npm run dev",
"start:api": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:xcdb-api": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk;npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:api:cache": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk;npm install; NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:api:cache:pg": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress:pg",
"start:xcdb-api:cache": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:web": "npm run build:common ; cd ./packages/nc-gui; npm install ../nocodb-sdk; npm install; npm run dev",
"cypress:run": "cypress run --config-file ./scripts/cypress/cypress.json",
"cypress:open": "cypress open --config-file ./scripts/cypress/cypress.json",
"cypress:clear": "cypress cache clear",

27
packages/nc-gui/components/ProjectTabs.vue

@ -315,6 +315,21 @@
</span>
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="_isUIAllowed('jsonImport')"
v-t="['a:actions:import-json']"
@click="jsonImportModal = true"
>
<v-list-item-title>
<v-icon small>
mdi-code-json
</v-icon>
<span class="caption">
<!-- TODO: i18n -->
JSON file
</span>
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="_isUIAllowed('excelQuickImport')"
v-t="['a:actions:import-excel']"
@ -370,6 +385,13 @@
@closeModal="quickImportModal = false"
/>
<!-- Import From JSON string / file -->
<json-import
v-model="jsonImportModal"
hide-label
@closeModal="jsonImportModal = false"
/>
<import-from-airtable v-if="airtableImportModal" v-model="airtableImportModal" />
</v-container>
</template>
@ -404,9 +426,11 @@ import GlobalAcl from '~/components/GlobalAcl'
import AuditTab from '~/components/project/AuditTab'
import QuickImport from '~/components/import/QuickImport'
import ImportFromAirtable from '~/components/import/ImportFromAirtable'
import JsonImport from '~/components/import/JSONImport'
export default {
components: {
JsonImport,
ImportFromAirtable,
SwaggerClient,
// Screensaver,
@ -447,7 +471,8 @@ export default {
showScreensaver: false,
quickImportModal: false,
quickImportType: '',
airtableImportModal: false
airtableImportModal: false,
jsonImportModal: false
}
},
methods: {

20
packages/nc-gui/components/import/ImportFromAirtable.vue

@ -8,7 +8,6 @@
<div
v-t="['c:airtable-import:turbo-mode']"
class="ml-2 mt-3 title pointer nc-btn-enable-turbo"
@click="enableTurbo"
>
🚀
</div>
@ -86,6 +85,13 @@
hide-details
dense
/>
<v-checkbox
v-model="syncSource.details.options.syncViews"
class="caption"
label="Import Secondary Views"
hide-details
dense
/>
<v-checkbox
v-model="syncSource.details.options.syncRollup"
class="caption"
@ -225,7 +231,7 @@ export default {
apiKey: '',
shareId: '',
options: {
syncViews: false,
syncViews: true,
syncData: true,
syncRollup: false,
syncLookup: true,
@ -327,7 +333,7 @@ export default {
apiKey: '',
shareId: '',
options: {
syncViews: false,
syncViews: true,
syncData: true,
syncRollup: false,
syncLookup: true,
@ -350,10 +356,10 @@ export default {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
}
},
enableTurbo() {
this.$set(this.syncSource.details.options, 'syncViews', true)
this.$toast.success('🚀🚀 Ludicrous mode activated! Let\'s go! 🚀🚀').goAway(3000)
},
// enableTurbo() {
// this.$set(this.syncSource.details.options, 'syncViews', true)
// this.$toast.success('🚀🚀 Ludicrous mode activated! Let\'s go! 🚀🚀').goAway(3000)
// },
migrateSync(src) {
if (!src.details?.options) {
src.details.options = {

484
packages/nc-gui/components/import/JSONImport.vue

@ -0,0 +1,484 @@
<template>
<div :class="{'pt-10':!hideLabel}">
<v-dialog v-model="dropOrUpload" max-width="600">
<v-card max-width="600">
<v-tabs height="30">
<v-tab>
<v-icon small class="mr-1">
mdi-file-upload-outline
</v-icon>
<span class="caption text-capitalize">Upload</span>
</v-tab>
<!-- <v-tab>-->
<!-- <v-icon small class="mr-1">
mdi-link-variant
</v-icon>
<span class="caption text-capitalize">URL</span>
</v-tab>-->
<v-tab>
<v-icon small class="mr-1">
mdi-link-variant
</v-icon>
<span class="caption text-capitalize">String</span>
</v-tab>
<v-tab-item>
<div class="nc-json-import-tab-item ">
<div
class="nc-droppable d-flex align-center justify-center flex-column"
:style="{
background : dragOver ? '#7772' : ''
}"
@click="$refs.file.click()"
@drop.prevent="dropHandler"
@dragover.prevent="dragOver = true"
@dragenter.prevent="dragOver = true"
@dragexit="dragOver = false"
@dragleave="dragOver = false"
@dragend="dragOver = false"
>
<x-icon :color="['primary','grey']" size="50">
mdi-file-plus-outline
</x-icon>
<p class="title mb-1 mt-2">
<!-- Select File to Upload-->
{{ $t('msg.info.upload') }}
</p>
<p class="grey--text mb-1">
<!-- or drag and drop file-->
{{ $t('msg.info.upload_sub') }}
</p>
<p v-if="quickImportType == 'excel'" class="caption grey--text">
<!-- Supported: .xls, .xlsx, .xlsm, .ods, .ots -->
{{ $t('msg.info.excelSupport') }}
</p>
</div>
</div>
</v-tab-item>
<!-- <v-tab-item>
<div class="nc-json-import-tab-item align-center">
<div class="pa-4 d-100 h-100">
<v-form ref="form" v-model="valid">
<div class="d-flex">
&lt;!&ndash; todo: i18n label&ndash;&gt;
<v-text-field
v-model="url"
hide-details="auto"
type="url"
label="Enter JSON file url"
class="caption"
outlined
dense
:rules="
[
v => !!v || $t('general.required'),
v => !(/(10)(\.([2]([0-5][0-5]|[01234][6-9])|[1][0-9][0-9]|[1-9][0-9]|[0-9])){3}|(172)\.(1[6-9]|2[0-9]|3[0-1])(\.(2[0-4][0-9]|25[0-5]|[1][0-9][0-9]|[1-9][0-9]|[0-9])){2}|(192)\.(168)(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){2}|(0.0.0.0)|localhost?/g).test(v) || errorMessages.ipBlockList
]"
/>
<v-btn v-t="['c:project:create:json:load-url']" class="ml-3" color="primary" @click="loadUrl">
&lt;!&ndash;Load&ndash;&gt;
{{ $t('general.load') }}
</v-btn>
</div>
</v-form>
</div>
</div>
</v-tab-item>-->
<v-tab-item>
<div class="nc-json-import-tab-item align-center">
<div class="pa-4 d-100 h-100">
<v-form ref="form" v-model="valid">
<div class="nc-json-editor-wrapper">
<v-btn small class="nc-json-format-btn" @click="formatJson">
Format
</v-btn>
<!--label="Enter excel file url"-->
<monaco-json-editor
ref="editor"
v-model="jsonString"
style="height:320px"
/>
<div class="text-center mt-4">
<v-btn v-t="['c:project:create:excel:load-url']" class="ml-3" color="primary" @click="loadJsonString">
<!--Load-->
{{ $t('general.load') }}
</v-btn>
</div>
</div>
</v-form>
</div>
</div>
</v-tab-item>
</v-tabs>
<div class="px-4 pb-2">
<div class="d-flex">
<v-spacer />
<span class="caption pointer grey--text" @click="showMore = !showMore">
{{ showMore ? $t('general.hideAll') : $t('general.showMore') }}
<v-icon small color="grey lighten-1">mdi-menu-{{ showMore ? 'up' : 'down' }}</v-icon>
</span>
</div>
<div class="mb-2 pt-2 nc-json-import-options" :style="{ maxHeight: showMore ? '200px' : '0'}">
<p />
<!--hint="# of rows to parse to infer data type"-->
<v-text-field
v-model="parserConfig.maxRowsToParse"
style="max-width: 250px"
class="caption mx-auto"
dense
persistent-hint
:hint="$t('msg.info.footMsg')"
outlined
type="number"
/>
<v-checkbox
v-model="parserConfig.normalizeNested"
style="width: 250px"
class="mx-auto mb-2"
dense
hide-details
>
<template #label>
<span class="caption">Flatten nested</span>
<v-tooltip bottom position-y="">
<template #activator="{ on }">
<v-icon small class="ml-1" v-on="on">
mdi-information-outline
</v-icon>
</template>
<div class="caption" style="width: 260px">
If flatten nested option is set it will flatten nested object as root level property. In normal case nested object will treat as JSON column.
<br>
<br>
For example the following input: <code class="caption font-weight-bold">{
"prop1": {
"prop2": "value"
},
"prop3": "value",
"prop4": 1
}</code> will treat as:
<code class="caption font-weight-bold">{
"prop1_prop2": "value",
"prop3": "value",
"prop4": 1
}</code>
</div>
</v-tooltip>
</template>
</v-checkbox>
<v-checkbox
v-model="parserConfig.importData"
style="width: 250px"
class="mx-auto mb-2"
dense
hide-details
>
<template #label>
<span class="caption">Import data</span>
</template>
</v-checkbox>
</div>
</div>
</v-card>
</v-dialog>
<v-tooltip bottom>
<template #activator="{on}">
<input
ref="file"
class="nc-json-import-input"
type="file"
style="display: none"
accept=".json"
@change="_change($event)"
>
<v-btn
v-if="!hideLabel"
small
outlined
v-on="on"
@click="$refs.file.click()"
>
<v-icon small class="mr-1">
mdi-file-excel-outline
</v-icon>
<!--Import-->
{{ $t('activity.import') }}
</v-btn>
</template>
<span class="caption">Create template from JSON</span>
</v-tooltip>
<v-dialog v-if="templateData" v-model="templateEditorModal" max-width="1000">
<v-card class="pa-6" min-width="500">
<template-editor :project-template.sync="templateData" json-import :quick-import-type="quickImportType">
<template #toolbar="{valid}">
<h3 class="mt-2 grey--text">
<span>
JSON Import
</span>
</h3>
<v-spacer />
<v-spacer />
<create-project-from-template-btn
:template-data="templateData"
:import-data="importData"
:import-to-project="importToProject"
json-import
:valid="valid"
create-gql-text="Import as GQL Project"
create-rest-text="Import as REST Project"
@closeModal="$emit('closeModal'),templateEditorModal = false"
>
<!--Import Excel-->
<span v-if="quickImportType === 'excel'">
{{ $t('activity.importExcel') }}
</span>
<!--Import CSV-->
<span v-if="quickImportType === 'csv'">
{{ $t('activity.importCSV') }}
</span>
</create-project-from-template-btn>
</template>
</template-editor>
</v-card>
</v-dialog>
</div>
</template>
<script>
import TemplateEditor from '~/components/templates/Editor'
import CreateProjectFromTemplateBtn from '~/components/templates/CreateProjectFromTemplateBtn'
import MonacoJsonEditor from '~/components/monaco/MonacoJsonEditor'
import JSONTemplateAdapter from '~/components/import/templateParsers/JSONTemplateAdapter'
import JSONUrlTemplateAdapter from '~/components/import/templateParsers/JSONUrlTemplateAdapter'
export default {
name: 'JsonImport',
components: { MonacoJsonEditor, CreateProjectFromTemplateBtn, TemplateEditor },
props: {
hideLabel: Boolean,
value: Boolean,
importToProject: Boolean,
quickImportType: String
},
data() {
return {
templateEditorModal: false,
valid: null,
templateData: null,
importData: null,
dragOver: false,
url: '',
showMore: false,
parserConfig: {
maxRowsToParse: 500,
normalizeNested: true,
importData: true
},
filename: '',
jsonString: '',
errorMessages: {
ipBlockList: 'IP Not allowed!',
importJSON: 'Target file is not an accepted file type. The accepted file type is .json!'
}
}
},
computed: {
dropOrUpload: {
set(v) {
this.$emit('input', v)
},
get() {
return this.value
}
},
tables() {
return this.$store.state.project.tables || []
}
},
mounted() {
if (this.$route && this.$route.query && this.$route.query.excelUrl) {
this.url = this.$route.query.excelUrl
this.loadUrl()
}
},
methods: {
formatJson() {
console.log(this.$refs.editor)
this.$refs.editor.format()
},
selectFile() {
this.$refs.file.files = null
this.$refs.file.click()
},
_change(event) {
const files = event.target.files
if (files && files[0]) {
this._file(files[0])
event.target.value = ''
}
},
async _file(file) {
this.templateData = null
this.importData = null
this.$store.commit('loader/MutMessage', 'Loading excel file')
let i = 0
const int = setInterval(() => {
this.$store.commit('loader/MutMessage', `Loading excel file${'.'.repeat(++i % 4)}`)
}, 1000)
this.dropOrUpload = false
const reader = new FileReader()
this.filename = file.name
reader.onload = async(e) => {
const ab = e.target.result
await this.parseAndExtractData('file', ab, file.name)
this.$store.commit('loader/MutMessage', null)
clearInterval(int)
}
const handleEvent = (event) => {
this.$store.commit('loader/MutMessage', `${event.type}: ${event.loaded} bytes transferred`)
}
reader.addEventListener('progress', handleEvent)
reader.onerror = (e) => {
console.log('error', e)
this.$store.commit('loader/MutClear')
}
reader.readAsText(file)
},
async parseAndExtractData(type, val, name) {
try {
let templateGenerator
this.templateData = null
this.importData = null
switch (type) {
case 'file':
templateGenerator = new JSONTemplateAdapter(name, val, this.parserConfig)
break
case 'url':
templateGenerator = new JSONUrlTemplateAdapter(val, this.$store, this.parserConfig, this.$api)
break
case 'string':
templateGenerator = new JSONTemplateAdapter(name, val, this.parserConfig)
break
}
await templateGenerator.init()
templateGenerator.parse()
this.templateData = templateGenerator.getTemplate()
this.templateData.tables[0].table_name = this.populateUniqueTableName()
this.importData = templateGenerator.getData()
this.templateEditorModal = true
} catch (e) {
console.log(e)
this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000)
}
},
dropHandler(ev) {
this.dragOver = false
let file
if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s)
if (ev.dataTransfer.items.length && ev.dataTransfer.items[0].kind === 'file') {
file = ev.dataTransfer.items[0].getAsFile()
}
} else if (ev.dataTransfer.files.length) {
file = ev.dataTransfer.files[0]
}
if (!file) {
return
}
if (!/.*\.json/.test(file.name)) {
return this.$toast.error(this.errorMessages.importJSON).goAway(3000)
}
this._file(file)
},
dragOverHandler(ev) {
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault()
},
populateUniqueTableName() {
let c = 1
while (this.tables.some(t => t.title === `Sheet${c}`)) { c++ }
return `Sheet${c}`
},
async loadUrl() {
if ((this.$refs.form && !this.$refs.form.validate()) || !this.url) {
return
}
this.$store.commit('loader/MutMessage', 'Loading json file from url')
let i = 0
const int = setInterval(() => {
this.$store.commit('loader/MutMessage', `Loading json file${'.'.repeat(++i % 4)}`)
}, 1000)
this.dropOrUpload = false
await this.parseAndExtractData('url', this.url, '')
clearInterval(int)
this.$store.commit('loader/MutClear')
},
async loadJsonString() {
await this.parseAndExtractData('string', this.jsonString)
this.$store.commit('loader/MutClear')
}
}
}
</script>
<style scoped>
.nc-droppable {
width: 100%;
min-height: 200px;
border-radius: 4px;
border: 2px dashed #ddd;
}
.nc-json-import-tab-item {
min-height: 400px;
padding: 20px;
display: flex;
align-items: stretch;
width: 100%;
}
.nc-json-import-options {
transition: .4s max-height;
overflow: hidden;
}
.nc-json-editor-wrapper{
position: relative;
}
.nc-json-format-btn{
position:absolute;
right:4px;
top:4px;
z-index:9;
}
</style>

1
packages/nc-gui/components/import/templateParsers/CSVTemplateAdapter.js

@ -1,6 +1,5 @@
import Papaparse from 'papaparse'
import TemplateGenerator from '~/components/import/templateParsers/TemplateGenerator'
export default class CSVTemplateAdapter extends TemplateGenerator {
constructor(name, data) {
super()

150
packages/nc-gui/components/import/templateParsers/JSONTemplateAdapter.js

@ -0,0 +1,150 @@
import { TemplateGenerator, UITypes } from 'nocodb-sdk'
import {
extractMultiOrSingleSelectProps,
getCheckboxValue,
isCheckboxType, isDecimalType, isEmailType,
isMultiLineTextType, isUrlType
} from '~/components/import/templateParsers/parserHelpers'
const jsonTypeToUidt = {
number: UITypes.Number,
string: UITypes.SingleLineText,
date: UITypes.DateTime,
boolean: UITypes.Checkbox,
object: UITypes.JSON
}
const extractNestedData = (obj, path) => path.reduce((val, key) => val && val[key], obj)
export default class JSONTemplateAdapter extends TemplateGenerator {
constructor(name = 'test', data, parserConfig = {}) {
super()
this.config = {
maxRowsToParse: 500,
...parserConfig
}
this.name = name
this._jsonData = typeof data === 'string' ? JSON.parse(data) : data
this.project = {
title: this.name,
tables: []
}
this.data = {}
}
async init() {
}
parseData() {
this.columns = this.csv.meta.fields
this.data = this.csv.data
}
getColumns() {
return this.columns
}
getData() {
return this.data
}
get jsonData() {
return Array.isArray(this._jsonData) ? this._jsonData : [this._jsonData]
}
parse() {
const jsonData = this.jsonData
const tn = 'table'
const table = { table_name: tn, ref_table_name: tn, columns: [] }
this.data[tn] = []
for (const col of Object.keys(jsonData[0])) {
const columns = this._parseColumn([col], jsonData)
table.columns.push(...columns)
}
if (this.config.importData) { this._parseTableData(table) }
this.project.tables.push(table)
}
getTemplate() {
return this.project
}
_parseColumn(path = [], jsonData = this.jsonData, firstRowVal = path.reduce((val, k) => val && val[k], this.jsonData[0])) {
const columns = []
// parse nested
if (firstRowVal && typeof firstRowVal === 'object' && !Array.isArray(firstRowVal) && this.config.normalizeNested) {
for (const key of Object.keys(firstRowVal)) {
const normalizedNestedColumns = this._parseColumn([...path, key], this.jsonData, firstRowVal[key])
columns.push(...normalizedNestedColumns)
}
} else {
const cn = path.join('_').replace(/\W/g, '_').trim()
const column = {
column_name: cn,
ref_column_name: cn,
path
}
column.uidt = jsonTypeToUidt[typeof firstRowVal] || UITypes.SingleLineText
const colData = jsonData.map(r => extractNestedData(r, path))
Object.assign(column, this._getColumnUIDTAndMetas(colData, column.uidt))
columns.push(column)
}
return columns
}
_getColumnUIDTAndMetas(colData, defaultType) {
const colProps = { uidt: defaultType }
// todo: optimize
if (colProps.uidt === UITypes.SingleLineText) {
// check for long text
if (isMultiLineTextType(colData)) {
colProps.uidt = UITypes.LongText
} if (isEmailType(colData)) {
colProps.uidt = UITypes.Email
} if (isUrlType(colData)) {
colProps.uidt = UITypes.URL
} else {
const checkboxType = isCheckboxType(colData)
if (checkboxType.length === 1) {
colProps.uidt = UITypes.Checkbox
} else {
Object.assign(colProps, extractMultiOrSingleSelectProps(colData))
}
}
} else if (colProps.uidt === UITypes.Number) {
if (isDecimalType(colData)) {
colProps.uidt = UITypes.Decimal
}
}
return colProps
}
_parseTableData(tableMeta) {
for (const row of this.jsonData) {
const rowData = {}
for (let i = 0; i < tableMeta.columns.length; i++) {
const value = extractNestedData(row, tableMeta.columns[i].path || [])
if (tableMeta.columns[i].uidt === UITypes.Checkbox) {
rowData[tableMeta.columns[i].ref_column_name] = getCheckboxValue(value)
} else if (tableMeta.columns[i].uidt === UITypes.SingleSelect || tableMeta.columns[i].uidt === UITypes.MultiSelect) {
rowData[tableMeta.columns[i].ref_column_name] = (value || '').toString().trim() || null
} else if (tableMeta.columns[i].uidt === UITypes.JSON) {
rowData[tableMeta.columns[i].ref_column_name] = JSON.stringify(value)
} else {
// toto: do parsing if necessary based on type
rowData[tableMeta.columns[i].column_name] = value
}
}
this.data[tableMeta.ref_table_name].push(rowData)
// rowIndex++
}
}
}

21
packages/nc-gui/components/import/templateParsers/JSONUrlTemplateAdapter.js

@ -0,0 +1,21 @@
import JSONTemplateAdapter from '~/components/import/templateParsers/JSONTemplateAdapter'
export default class JSONUrlTemplateAdapter extends JSONTemplateAdapter {
constructor(url, $store, parserConfig, $api) {
const name = url.split('/').pop()
super(name, null, parserConfig)
this.url = url
this.$api = $api
this.$store = $store
}
async init() {
const data = await this.$api.utils.axiosRequestMake({
apiMeta: {
url: this.url
}
})
this._jsonData = data
await super.init()
}
}

58
packages/nc-gui/components/import/templateParsers/parserHelpers.js

@ -1,3 +1,6 @@
import { UITypes } from 'nocodb-sdk'
import { isEmail, isValidURL } from '~/helpers'
const booleanOptions = [
{ checked: true, unchecked: false },
{ x: true, '': false },
@ -11,14 +14,24 @@ const booleanOptions = [
{ '✔': true, '': false },
{ enabled: true, disabled: false },
{ on: true, off: false },
{ done: true, '': false }
{ done: true, '': false },
{ true: true, false: false }
]
const aggBooleanOptions = booleanOptions.reduce((obj, o) => ({ ...obj, ...o }), {})
export const isCheckboxType = (values, col = '') => {
const getColVal = (row, col = null) => {
return row && col ? row[col] : row
}
export const isCheckboxType = (values, col = null) => {
let options = booleanOptions
for (let i = 0; i < values.length; i++) {
let val = col ? values[i][col] : values[i]
val = val === null || val === undefined ? '' : val
const val = getColVal(values[i], col)
if (val === null || val === undefined || val.toString().trim() === '') {
continue
}
options = options.filter(v => val in v)
if (!options.length) {
return false
@ -29,3 +42,40 @@ export const isCheckboxType = (values, col = '') => {
export const getCheckboxValue = (value) => {
return value && aggBooleanOptions[value]
}
export const isMultiLineTextType = (values, col = null) => {
return values.some(r =>
(getColVal(r, col) || '').toString().match(/[\r\n]/) ||
(getColVal(r, col) || '').toString().length > 255)
}
export const extractMultiOrSingleSelectProps = (colData) => {
const colProps = {}
if (colData.some(v => v && (v || '').toString().includes(','))) {
let flattenedVals = colData.flatMap(v => v ? v.toString().trim().split(/\s*,\s*/) : [])
const uniqueVals = flattenedVals = flattenedVals
.filter((v, i, arr) => i === arr.findIndex(v1 => v.toLowerCase() === v1.toLowerCase()))
if (flattenedVals.length > uniqueVals.length && uniqueVals.length <= Math.ceil(flattenedVals.length / 2)) {
colProps.uidt = UITypes.MultiSelect
colProps.dtxp = `'${uniqueVals.join("','")}'`
}
} else {
const uniqueVals = colData.map(v => (v || '').toString().trim()).filter((v, i, arr) => i === arr.findIndex(v1 => v.toLowerCase() === v1.toLowerCase()))
if (colData.length > uniqueVals.length && uniqueVals.length <= Math.ceil(colData.length / 2)) {
colProps.uidt = UITypes.SingleSelect
colProps.dtxp = `'${uniqueVals.join("','")}'`
}
}
return colProps
}
export const isDecimalType = colData => colData.some((v) => {
return v && parseInt(+v) !== +v
})
export const isEmailType = colData => !colData.some((v) => {
return v && !isEmail(v)
})
export const isUrlType = colData => !colData.some((v) => {
return v && !isValidURL(v)
})

3
packages/nc-gui/components/monaco/MonacoJsonEditor.js

@ -83,6 +83,9 @@ export default {
},
methods: {
format() {
this.editor.getAction('editor.action.formatDocument').run()
},
resizeLayout() {
this.editor.layout();
},

16
packages/nc-plugin/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nc-plugin",
"version": "0.1.1",
"version": "0.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nc-plugin",
"version": "0.1.1",
"version": "0.1.3",
"license": "MIT",
"dependencies": {
"@bitauth/libauth": "^1.17.1",
@ -10298,9 +10298,9 @@
}
},
"node_modules/shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true
},
"node_modules/shelljs": {
@ -19881,9 +19881,9 @@
"dev": true
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true
},
"shelljs": {

14
packages/noco-blog/package-lock.json generated

@ -5836,16 +5836,16 @@
}
},
"got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"requires": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -10797,9 +10797,9 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"side-channel": {
"version": "1.0.4",

28
packages/noco-docs-prev/package-lock.json generated

@ -7390,16 +7390,16 @@
}
},
"node_modules/got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -13804,9 +13804,9 @@
}
},
"node_modules/shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"node_modules/side-channel": {
"version": "1.0.4",
@ -22650,16 +22650,16 @@
}
},
"got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"requires": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -27611,9 +27611,9 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"side-channel": {
"version": "1.0.4",

3
packages/noco-docs/content/en/developer-resources/rest-apis.md

@ -166,7 +166,8 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Meta | Delete| utils | cacheDelete | /api/v1/db/meta/cache |
| Meta | Post | utils | testConnection | /api/v1/db/meta/projects/connection/test |
| Meta | Get | utils | appInfo | /api/v1/db/meta/nocodb/info |
| Meta | Get | utils | appVersion | /api/v1/db/meta/nocodb/version |
| Meta | Get | utils | appVersion | /api/v1/version |
| Meta | Get | utils | appHealth | /api/v1/health |
## Query params

28
packages/noco-docs/package-lock.json generated

@ -7390,16 +7390,16 @@
}
},
"node_modules/got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -13804,9 +13804,9 @@
}
},
"node_modules/shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"node_modules/side-channel": {
"version": "1.0.4",
@ -22650,16 +22650,16 @@
}
},
"got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"requires": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@ -27611,9 +27611,9 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"side-channel": {
"version": "1.0.4",

6
packages/noco-i18n/package-lock.json generated

@ -9793,9 +9793,9 @@
"dev": true
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true
},
"side-channel": {

3
packages/nocodb-sdk/src/index.ts

@ -5,6 +5,7 @@ export * from './lib/sqlUi';
export * from './lib/globals';
export * from './lib/helperFunctions';
export * from './lib/formulaHelpers';
export * from './lib/passwordHelpers';
export { default as UITypes, isVirtualCol } from './lib/UITypes';
export { default as CustomAPI } from './lib/CustomAPI';
export { default as TemplateGenerator } from './lib/TemplateGenerator';
export * from './lib/passwordHelpers';

20
packages/nocodb-sdk/src/lib/Api.ts

@ -3197,12 +3197,28 @@ export class Api<
*
* @tags Utils
* @name AppVersion
* @request GET:/api/v1/db/meta/nocodb/version
* @request GET:/api/v1/version
* @response `200` `any` OK
*/
appVersion: (params: RequestParams = {}) =>
this.request<any, any>({
path: `/api/v1/db/meta/nocodb/version`,
path: `/api/v1/version`,
method: 'GET',
format: 'json',
...params,
}),
/**
* No description
*
* @tags Utils
* @name AppHealth
* @request GET:/api/v1/health
* @response `200` `any` OK
*/
appHealth: (params: RequestParams = {}) =>
this.request<any, any>({
path: `/api/v1/health`,
method: 'GET',
format: 'json',
...params,

30
packages/nocodb-sdk/src/lib/TemplateGenerator.ts

@ -0,0 +1,30 @@
import UITypes from './UITypes';
export interface Column {
column_name: string;
ref_column_name: string;
uidt?: UITypes;
dtxp?: any;
dt?: any;
}
export interface Table {
table_name: string;
ref_table_name: string;
columns: Array<Column>;
}
export interface Template {
title: string;
tables: Array<Table>;
}
export default abstract class TemplateGenerator {
abstract parse(): Promise<any>;
abstract parseTemplate(): Promise<Template>;
abstract getColumns(): Promise<any>;
abstract parseData(): Promise<any>;
abstract getData(): Promise<{
[table_name: string]: Array<{
[key: string]: any;
}>;
}>;
}

10
packages/nocodb/src/__tests__/restv2.test.ts

@ -209,7 +209,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Rollup,
alias: 'filmCount',
rollupColumn: 'FilmId',
relationColumn: 'FilmMMList',
relationColumn: 'Film List',
rollupFunction: 'count'
}
];
@ -413,7 +413,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Lookup,
alias: 'filmNames',
lookupColumn: 'Title',
relationColumn: 'FilmMMList'
relationColumn: 'Film List'
};
request(app)
.post(`/nc/${projectId}/generate`)
@ -1335,7 +1335,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Lookup,
alias: 'filmIds',
lookupColumn: 'FilmId',
relationColumn: 'FilmMMList'
relationColumn: 'Film List'
},
{
table: 'actor',
@ -1398,7 +1398,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Rollup,
alias: 'actorsCount',
rollupColumn: 'ActorId',
relationColumn: 'ActorMMList',
relationColumn: 'ActorList',
rollupFunction: 'count'
},
{
@ -1406,7 +1406,7 @@ describe('Noco v2 Tests', () => {
type: UITypes.Lookup,
alias: 'actorsCountList',
lookupColumn: 'actorsCount',
relationColumn: 'FilmMMList'
relationColumn: 'Film List'
},
{
table: 'actor',

8
packages/nocodb/src/lib/meta/api/columnApis.ts

@ -57,7 +57,7 @@ async function createHmAndBtColumn(
{
const title = getUniqueColumnAliasName(
await child.getColumns(),
type === 'bt' ? alias : `${parent.title}Read`
type === 'bt' ? alias : `${parent.title}`
);
await Column.insert<LinkToAnotherRecordColumn>({
title,
@ -79,7 +79,7 @@ async function createHmAndBtColumn(
{
const title = getUniqueColumnAliasName(
await parent.getColumns(),
type === 'hm' ? alias : `${child.title}List`
type === 'hm' ? alias : `${child.title} List`
);
await Column.insert({
title,
@ -427,7 +427,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
await Column.insert({
title: getUniqueColumnAliasName(
await child.getColumns(),
`${child.title}MMList`
`${parent.title} List`
),
uidt: UITypes.LinkToAnotherRecord,
type: 'mm',
@ -447,7 +447,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
await Column.insert({
title: getUniqueColumnAliasName(
await parent.getColumns(),
req.body.title ?? `${parent.title}MMList`
req.body.title ?? `${child.title} List`
),
uidt: UITypes.LinkToAnotherRecord,

8
packages/nocodb/src/lib/meta/api/metaDiffApis.ts

@ -671,7 +671,7 @@ export async function metaDiffSync(req, res) {
if (change.relationType === RelationTypes.BELONGS_TO) {
const title = getUniqueColumnAliasName(
childModel.columns,
`${parentModel.title || parentModel.table_name}Read`
`${parentModel.title || parentModel.table_name}`
);
await Column.insert<LinkToAnotherRecordColumn>({
uidt: UITypes.LinkToAnotherRecord,
@ -686,7 +686,7 @@ export async function metaDiffSync(req, res) {
} else if (change.relationType === RelationTypes.HAS_MANY) {
const title = getUniqueColumnAliasName(
childModel.columns,
`${childModel.title || childModel.table_name}List`
`${childModel.title || childModel.table_name} List`
);
await Column.insert<LinkToAnotherRecordColumn>({
uidt: UITypes.LinkToAnotherRecord,
@ -785,7 +785,7 @@ export async function extractAndGenerateManyToManyRelations(
await Column.insert<LinkToAnotherRecordColumn>({
title: getUniqueColumnAliasName(
modelA.columns,
`${modelB.title}MMList`
`${modelB.title} List`
),
fk_model_id: modelA.id,
fk_related_model_id: modelB.id,
@ -803,7 +803,7 @@ export async function extractAndGenerateManyToManyRelations(
await Column.insert<LinkToAnotherRecordColumn>({
title: getUniqueColumnAliasName(
modelB.columns,
`${modelA.title}MMList`
`${modelA.title} List`
),
fk_model_id: modelB.id,
fk_related_model_id: modelA.id,

4
packages/nocodb/src/lib/meta/api/projectApis.ts

@ -215,7 +215,7 @@ async function populateMeta(base: Base, project: Project): Promise<any> {
uidt: UITypes.LinkToAnotherRecord,
type: 'hm',
hm,
title: `${hm.title}List`
title: `${hm.title} List`
};
}),
...belongsTo.map(bt => {
@ -230,7 +230,7 @@ async function populateMeta(base: Base, project: Project): Promise<any> {
uidt: UITypes.LinkToAnotherRecord,
type: 'bt',
bt,
title: `${bt.rtitle}Read`
title: `${bt.rtitle}`
};
})
];

55
packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts

@ -5,10 +5,11 @@ import { Tele } from 'nc-help';
import bcrypt from 'bcryptjs';
import Noco from '../../../Noco';
import { MetaTable } from '../../../utils/globals';
import { CacheScope, MetaTable } from '../../../utils/globals';
import ProjectUser from '../../../models/ProjectUser';
import { validatePassword } from 'nocodb-sdk';
import boxen from 'boxen';
import NocoCache from '../../../cache/NocoCache';
const { isEmail } = require('validator');
const rolesLevel = { owner: 0, creator: 1, editor: 2, commenter: 3, viewer: 4 };
@ -103,7 +104,7 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
// check user account already present with the new admin email
const existingUserWithNewEmail = await User.getByEmail(email, ncMeta);
if (existingUserWithNewEmail) {
if (existingUserWithNewEmail?.id) {
// get all project access belongs to the existing account
// and migrate to the admin account
const existingUserProjects = await ncMeta.metaList2(
@ -155,13 +156,25 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
}
// delete existing user
ncMeta.metaDelete(
await ncMeta.metaDelete(
null,
null,
MetaTable.USERS,
existingUserWithNewEmail.id
);
// clear cache
await NocoCache.delAll(
CacheScope.USER,
`${existingUserWithNewEmail.email}___*`
);
await NocoCache.del(
`${CacheScope.USER}:${existingUserWithNewEmail.id}`
);
await NocoCache.del(
`${CacheScope.USER}:${existingUserWithNewEmail.email}`
);
// Update email and password of super admin account
await User.update(
superUser.id,
@ -169,7 +182,9 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
salt,
email,
password,
email_verification_token
email_verification_token,
token_version: null,
refresh_token: null
},
ncMeta
);
@ -181,22 +196,34 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
salt,
email,
password,
email_verification_token
email_verification_token,
token_version: null,
refresh_token: null
},
ncMeta
);
}
} else {
// if email's are not different update the password and hash
await User.update(
superUser.id,
{
salt,
password,
email_verification_token
},
ncMeta
const newPasswordHash = await promisify(bcrypt.hash)(
process.env.NC_ADMIN_PASSWORD,
superUser.salt
);
if (newPasswordHash !== superUser.password) {
// if email's are same and passwords are different
// then update the password and token version
await User.update(
superUser.id,
{
salt,
password,
email_verification_token,
token_version: null,
refresh_token: null
},
ncMeta
);
}
}
}
await ncMeta.commit();

8
packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts

@ -103,8 +103,8 @@ export function initStrategies(router): void {
if (cachedVal) {
if (
cachedVal.token_version &&
jwtPayload.token_version &&
!cachedVal.token_version ||
!jwtPayload.token_version ||
cachedVal.token_version !== jwtPayload.token_version
) {
return done(new Error('Token Expired. Please login again.'));
@ -115,8 +115,8 @@ export function initStrategies(router): void {
User.getByEmail(jwtPayload?.email)
.then(async user => {
if (
user.token_version &&
jwtPayload.token_version &&
!user.token_version ||
!jwtPayload.token_version ||
user.token_version !== jwtPayload.token_version
) {
return done(new Error('Token Expired. Please login again.'));

7
packages/nocodb/src/lib/meta/api/userApi/userApis.ts

@ -179,15 +179,14 @@ async function successfulSignIn({
await promisify((req as any).login.bind(req))(user);
const refreshToken = randomTokenString();
let token_version = user.token_version;
if (!token_version) {
token_version = randomTokenString();
if (!user.token_version) {
user.token_version = randomTokenString();
}
await User.update(user.id, {
refresh_token: refreshToken,
email: user.email,
token_version
token_version: user.token_version
});
setTokenCookie(res, refreshToken);

11
packages/nocodb/src/lib/meta/api/utilApis.ts

@ -60,6 +60,14 @@ export async function releaseVersion(_req: Request, res: Response) {
res.json(result);
}
export async function appHealth(_: Request, res: Response) {
res.json({
message: 'OK',
timestamp: Date.now(),
uptime: process.uptime()
});
}
async function _axiosRequestMake(req: Request, res: Response) {
const { apiMeta } = req.body;
@ -132,6 +140,7 @@ export default router => {
ncMetaAclMw(testConnection, 'testConnection')
);
router.get('/api/v1/db/meta/nocodb/info', catchError(appInfo));
router.get('/api/v1/db/meta/nocodb/version', catchError(releaseVersion));
router.post('/api/v1/db/meta/axiosRequestMake', catchError(axiosRequestMake));
router.get('/api/v1/version', catchError(releaseVersion));
router.get('/api/v1/health', catchError(appHealth));
};

3
packages/nocodb/src/lib/models/User.ts

@ -84,6 +84,9 @@ export default class User implements UserType {
if (updateObj.email) {
updateObj.email = updateObj.email.toLowerCase();
} else {
// set email prop to avoid generation of invalid cache key
updateObj.email = (await this.get(id, ncMeta))?.email?.toLowerCase();
}
// get existing cache
const keys = [

6
scripts/cypress/integration/common/1a_table_operations.js

@ -80,9 +80,9 @@ export const genTest = (apiType, dbType) => {
// 4a. Address table, has many field
cy.openTableTab("Address", 25);
mainPage.getCell("CityRead", 1).scrollIntoView();
mainPage.getCell("City", 1).scrollIntoView();
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.find(".name")
.contains("Lethbridge")
.should("exist");
@ -92,7 +92,7 @@ export const genTest = (apiType, dbType) => {
cy.openTableTab("Country", 25);
mainPage
.getCell("CityList", 1)
.getCell("City List", 1)
.find(".name")
.contains("Kabul")
.should("exist");

2
scripts/cypress/integration/common/1d_pg_table_view_drag_drop_reorder.js

@ -23,7 +23,7 @@ export const genTest = (apiType, dbType) => {
/*
Original order of list items
Actor, Address, Category, City, Country, Customer, FIlm, FilmText, Language, Payment, Rental Staff
ActorInfo, CustomerList, FilmList, NiceButSlowerFilmList, SalesByFilmCategory, SalesByStore, StaffList
ActorInfo, Customer List, Film List, NiceButSlowerFilm List, SalesByFilmCategory, SalesByStore, Staff List
*/
it(`Table & SQL View list, Drag/drop`, () => {

2
scripts/cypress/integration/common/1d_table_view_drag_drop_reorder.js

@ -18,7 +18,7 @@ export const genTest = (apiType, dbType) => {
/*
Original order of list items
Actor, Address, Category, City, Country, Customer, FIlm, FilmText, Language, Payment, Rental Staff
ActorInfo, CustomerList, FilmList, NiceButSlowerFilmList, SalesByFilmCategory, SalesByStore, StaffList
ActorInfo, Customer List, Film List, NiceButSlowerFilm List, SalesByFilmCategory, SalesByStore, Staff List
*/
before(() => {

4
scripts/cypress/integration/common/2a_table_with_belongs_to_colulmn.js

@ -23,12 +23,12 @@ export const genTest = (apiType, dbType) => {
it("Expand belongs-to column", () => {
// expand first row
cy.get('td[data-col="CityList"] div:visible', {
cy.get('td[data-col="City List"] div:visible', {
timeout: 12000,
})
.first()
.click();
cy.get('td[data-col="CityList"] div .mdi-arrow-expand:visible')
cy.get('td[data-col="City List"] div .mdi-arrow-expand:visible')
.first()
.click();

4
scripts/cypress/integration/common/2b_table_with_m2m_column.js

@ -23,10 +23,10 @@ export const genTest = (apiType, dbType) => {
it("Expand m2m column", () => {
// expand first row
cy.get('td[data-col="FilmMMList"] div', { timeout: 12000 })
cy.get('td[data-col="Film List"] div', { timeout: 12000 })
.first()
.click({ force: true });
cy.get('td[data-col="FilmMMList"] div .mdi-arrow-expand')
cy.get('td[data-col="Film List"] div .mdi-arrow-expand')
.first()
.click({ force: true });

2
scripts/cypress/integration/common/4c_form_view_detailed.js

@ -119,7 +119,7 @@ export const genTest = (apiType, dbType) => {
.should("exist");
cy.get(".nc-field-wrapper")
.eq(1)
.contains("CityList")
.contains("City List")
.should("exist");
cy.get(".nc-field-wrapper")
.eq(2)

6
scripts/cypress/integration/common/4d_table_view_grid_locked.js

@ -77,18 +77,18 @@ export const genTest = (apiType, dbType) => {
// check if add/ expand options available for 'has many' column type
mainPage
.getCell("CityList", 1)
.getCell("City List", 1)
.click()
.find("button.mdi-plus")
.should(`${vString}exist`);
mainPage
.getCell("CityList", 1)
.getCell("City List", 1)
.click()
.find("button.mdi-arrow-expand")
.should(`${vString}exist`);
// update row option (right click) - should not be available for Lock view
mainPage.getCell("CityList", 1).rightclick();
mainPage.getCell("City List", 1).rightclick();
cy.get(".menuable__content__active").should(
`${vString}be.visible`
);

6
scripts/cypress/integration/common/4e_form_view_share.js

@ -72,7 +72,7 @@ export const genTest = (apiType, dbType) => {
// "#data-table-form-City"
// );
cy.get('[title="AddressList"]').drag(".nc-drag-n-drop-to-hide");
cy.get('[title="Address List"]').drag(".nc-drag-n-drop-to-hide");
cy.get(".nc-form > .mx-auto")
.find('[type="checkbox"]')
@ -131,8 +131,8 @@ export const genTest = (apiType, dbType) => {
// all fields, barring removed field should exist
cy.get('[title="City"]').should("exist");
cy.get('[title="LastUpdate"]').should("exist");
cy.get('[title="CountryRead"]').should("exist");
cy.get('[title="AddressList"]').should("not.exist");
cy.get('[title="Country"]').should("exist");
cy.get('[title="Address List"]').should("not.exist");
// order of LastUpdate & City field is retained
cy.get(".nc-field-wrapper")

30
scripts/cypress/integration/common/4f_grid_view_share.js

@ -170,7 +170,7 @@ export const genTest = (apiType, dbType) => {
const verifyCsv = (retrievedRecords) => {
// expected output, statically configured
let storedRecords = [
`Address,District,PostalCode,Phone,Location,CustomerList,StaffList,CityRead,StaffMMList`,
`Address,District,PostalCode,Phone,Location,Customer List,Staff List,City,Staff List`,
`1013 Tabuk Boulevard,West Bengali,96203,158399646978,[object Object],2,,Kanchrapara,`,
`1892 Nabereznyje Telny Lane,Tutuila,28396,478229987054,[object Object],2,,Tafuna,`,
`1993 Tabuk Lane,Tamil Nadu,64221,648482415405,[object Object],2,,Tambaram,`,
@ -231,7 +231,7 @@ export const genTest = (apiType, dbType) => {
const verifyCsv = (retrievedRecords) => {
// expected output, statically configured
let storedRecords = [
`Address,District,PostalCode,Phone,Location,CustomerList,StaffList,CityRead,StaffMMList`,
`Address,District,PostalCode,Phone,Location,Customer List,Staff List,City,Staff List`,
`1993 Tabuk Lane,Tamil Nadu,64221,648482415405,[object Object],2,,Tambaram,`,
`1661 Abha Drive,Tamil Nadu,14400,270456873752,[object Object],1,,Pudukkottai,`,
];
@ -267,24 +267,24 @@ export const genTest = (apiType, dbType) => {
it(`Share GRID view : Virtual column validation > has many`, () => {
// verify column headers
cy.get('[data-col="CustomerList"]').should("exist");
cy.get('[data-col="StaffList"]').should("exist");
cy.get('[data-col="CityRead"]').should("exist");
cy.get('[data-col="StaffMMList"]').should("exist");
cy.get('[data-col="Customer List"]').should("exist");
cy.get('[data-col="Staff List"]').should("exist");
cy.get('[data-col="City"]').should("exist");
cy.get('[data-col="Staff List"]').should("exist");
// has many field validation
mainPage
.getCell("CustomerList", 3)
.getCell("Customer List", 3)
.click()
.find("button.mdi-close-thick")
.should("not.exist");
mainPage
.getCell("CustomerList", 3)
.getCell("Customer List", 3)
.click()
.find("button.mdi-plus")
.should("not.exist");
mainPage
.getCell("CustomerList", 3)
.getCell("Customer List", 3)
.click()
.find("button.mdi-arrow-expand")
.click();
@ -308,17 +308,17 @@ export const genTest = (apiType, dbType) => {
it(`Share GRID view : Virtual column validation > belongs to`, () => {
// belongs to field validation
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.click()
.find("button.mdi-close-thick")
.should("not.exist");
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.click()
.find("button.mdi-arrow-expand")
.should("not.exist");
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.find(".v-chip")
.contains("Kanchrapara")
.should("exist");
@ -327,17 +327,17 @@ export const genTest = (apiType, dbType) => {
it(`Share GRID view : Virtual column validation > many to many`, () => {
// many-to-many field validation
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-close-thick")
.should("not.exist");
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-plus")
.should("not.exist");
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-arrow-expand")
.click();

28
scripts/cypress/integration/common/4f_pg_grid_view_share.js

@ -228,7 +228,7 @@ export const genTest = (apiType, dbType) => {
const verifyCsv = (retrievedRecords) => {
// expected output, statically configured
let storedRecords = [
`Address,District,PostalCode,Phone,Location,CustomerList,StaffList,CityRead,StaffMMList`,
`Address,District,PostalCode,Phone,Location,Customer List,Staff List,City,Staff List`,
`1888 Kabul Drive,,20936,,1,,Ife,,`,
`1661 Abha Drive,,14400,,1,,Pudukkottai,,`,
];
@ -262,24 +262,24 @@ export const genTest = (apiType, dbType) => {
it(`Share GRID view : Virtual column validation > has many`, () => {
// verify column headers
cy.get('[data-col="CustomerList"]').should("exist");
cy.get('[data-col="StaffList"]').should("exist");
cy.get('[data-col="CityRead"]').should("exist");
cy.get('[data-col="StaffMMList"]').should("exist");
cy.get('[data-col="Customer List"]').should("exist");
cy.get('[data-col="Staff List"]').should("exist");
cy.get('[data-col="City"]').should("exist");
cy.get('[data-col="Staff List"]').should("exist");
// has many field validation
mainPage
.getCell("CustomerList", 3)
.getCell("Customer List", 3)
.click()
.find("button.mdi-close-thick")
.should("not.exist");
mainPage
.getCell("CustomerList", 3)
.getCell("Customer List", 3)
.click()
.find("button.mdi-plus")
.should("not.exist");
mainPage
.getCell("CustomerList", 3)
.getCell("Customer List", 3)
.click()
.find("button.mdi-arrow-expand")
.click();
@ -303,17 +303,17 @@ export const genTest = (apiType, dbType) => {
it(`Share GRID view : Virtual column validation > belongs to`, () => {
// belongs to field validation
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.click()
.find("button.mdi-close-thick")
.should("not.exist");
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.click()
.find("button.mdi-arrow-expand")
.should("not.exist");
mainPage
.getCell("CityRead", 1)
.getCell("City", 1)
.find(".v-chip")
.contains("al-Ayn")
.should("exist");
@ -322,17 +322,17 @@ export const genTest = (apiType, dbType) => {
it(`Share GRID view : Virtual column validation > many to many`, () => {
// many-to-many field validation
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-close-thick")
.should("not.exist");
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-plus")
.should("not.exist");
mainPage
.getCell("StaffMMList", 1)
.getCell("Staff List", 1)
.click()
.find("button.mdi-arrow-expand")
.click();

2
scripts/cypress/integration/common/5a_user_role.js

@ -209,7 +209,7 @@ export const genTest = (apiType, dbType) => {
const verifyCsv = (retrievedRecords) => {
// expected output, statically configured
let storedRecords = [
`City,AddressList,CountryRead`,
`City,Address List,Country`,
`A Corua (La Corua),939 Probolinggo Loop,Spain`,
`Abha,733 Mandaluyong Place,Saudi Arabia`,
`Abu Dhabi,535 Ahmadnagar Manor,United Arab Emirates`,

4
scripts/cypress/integration/common/6b_downloadCsv.js

@ -31,7 +31,7 @@ export const genTest = (apiType, dbType) => {
// `Angola,"Benguela, Namibe"`,
// ];
let storedRecords = [
['Country','CityList'],
['Country','City List'],
['Afghanistan','Kabul'],
['Algeria','Skikda', 'Bchar', 'Batna'],
['American Samoa','Tafuna'],
@ -41,7 +41,7 @@ export const genTest = (apiType, dbType) => {
// if (isPostgres()) {
// // order of second entry is different
// storedRecords = [
// `Country,CityList`,
// `Country,City List`,
// `Afghanistan,Kabul`,
// `Algeria,"Skikda, Bchar, Batna"`,
// `American Samoa,Tafuna`,

2
scripts/cypress/integration/common/6f_attachments.js

@ -113,7 +113,7 @@ export const genTest = (apiType, dbType) => {
const verifyCsv = (retrievedRecords) => {
let storedRecords = [
`Country,CityList,testAttach`,
`Country,City List,testAttach`,
`Afghanistan,Kabul,1.json(http://localhost:8080/download/p_h0wxjx5kgoq3w4/vw_skyvc7hsp9i34a/2HvU8R.json)`,
];

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

@ -236,7 +236,7 @@ export function _viewMenu(roleType, previewMode, navDrawListCnt) {
// Download CSV / Upload CSV / Shared View List / Webhook
actionsMenuItemsCnt = 4;
} else if (roleType == "editor") {
// Download CSV / Upload CSV
// Download CSV / Upload CSV
actionsMenuItemsCnt = 2;
}

23
scripts/sdk/swagger.json

@ -5107,7 +5107,7 @@
]
}
},
"/api/v1/db/meta/nocodb/version": {
"/api/v1/version": {
"parameters": [],
"get": {
"summary": "",
@ -5128,6 +5128,27 @@
"description": ""
}
},
"/api/v1/health": {
"parameters": [],
"get": {
"summary": "",
"operationId": "utils-app-health",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {}
}
}
}
},
"tags": [
"Utils"
],
"description": ""
}
},
"/api/v1/db/meta/cache": {
"get": {
"summary": "Your GET endpoint",

Loading…
Cancel
Save