Browse Source

Merge pull request #2178 from nocodb/develop

pull/2179/head 0.91.1
github-actions[bot] 3 years ago committed by GitHub
parent
commit
00b6082d88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      .all-contributorsrc
  2. 24
      .github/workflows/ci-cd.yml
  3. 2
      .github/workflows/publish-api-docs.yml
  4. 2
      .github/workflows/publish-blog.yml
  5. 2
      .github/workflows/publish-dev-docs.yml
  6. 12
      .github/workflows/publish-docs.yml
  7. 2
      .github/workflows/publish-noco-i18n.yml
  8. 2
      .github/workflows/publish-prev-docs.yml
  9. 2
      .github/workflows/release-docker.yml
  10. 29
      .github/workflows/release-draft.yml
  11. 7
      .github/workflows/release-nocodb.yml
  12. 2
      .github/workflows/release-npm.yml
  13. 4
      .github/workflows/update-sdk-path.yml
  14. 10
      README.md
  15. 2
      Run.md
  16. 4
      package.json
  17. 6
      packages/nc-gui/assets/css/global.css
  18. 94
      packages/nc-gui/components/ImportantAnnouncement.vue
  19. 5
      packages/nc-gui/components/PreviewAs.vue
  20. 142
      packages/nc-gui/components/import/ImportFromAirtable.vue
  21. 34
      packages/nc-gui/components/project/spreadsheet/RowsXcDataTable.vue
  22. 4
      packages/nc-gui/components/project/spreadsheet/components/Cell.vue
  23. 124
      packages/nc-gui/components/project/spreadsheet/components/ColorPicker.vue
  24. 24
      packages/nc-gui/components/project/spreadsheet/components/ColumnFilter.vue
  25. 6
      packages/nc-gui/components/project/spreadsheet/components/ColumnFilterMenu.vue
  26. 42
      packages/nc-gui/components/project/spreadsheet/components/EditColumn.vue
  27. 12
      packages/nc-gui/components/project/spreadsheet/components/FieldsMenu.vue
  28. 10
      packages/nc-gui/components/project/spreadsheet/components/HeaderCell.vue
  29. 1
      packages/nc-gui/components/project/spreadsheet/components/MoreActions.vue
  30. 103
      packages/nc-gui/components/project/spreadsheet/components/SortListMenu.vue
  31. 3
      packages/nc-gui/components/project/spreadsheet/components/SpreadsheetNavDrawer.vue
  32. 9
      packages/nc-gui/components/project/spreadsheet/components/VirtualHeaderCell.vue
  33. 37
      packages/nc-gui/components/project/spreadsheet/components/cell/CurrencyCell.vue
  34. 13
      packages/nc-gui/components/project/spreadsheet/components/editColumn/CheckboxOptions.vue
  35. 73
      packages/nc-gui/components/project/spreadsheet/components/editColumn/CurrencyOptions.vue
  36. 32
      packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue
  37. 14
      packages/nc-gui/components/project/spreadsheet/components/editColumn/RatingOptions.vue
  38. 14
      packages/nc-gui/components/project/spreadsheet/helpers/uiTypes.js
  39. 2
      packages/nc-gui/components/project/spreadsheet/mixins/cell.js
  40. 6
      packages/nc-gui/components/project/spreadsheet/mixins/spreadsheet.js
  41. 3
      packages/nc-gui/components/project/spreadsheet/public/XcTable.vue
  42. 3
      packages/nc-gui/components/project/spreadsheet/views/GridView.vue
  43. 7
      packages/nc-gui/components/utils/Language.vue
  44. 82
      packages/nc-gui/helpers/currencyHelper.js
  45. 4
      packages/nc-gui/lang/fa.json
  46. 5
      packages/nc-gui/layouts/default.vue
  47. 4
      packages/nc-gui/mixins/device.js
  48. 17461
      packages/nc-gui/package-lock.json
  49. 3
      packages/nc-gui/package.json
  50. 11
      packages/nc-gui/pages/projects/index.vue
  51. 6
      packages/nc-gui/store/app.js
  52. 1
      packages/noco-docs/content/en/getting-started/installation.md
  53. 4
      packages/noco-docs/content/en/setup-and-usages/audit.md
  54. 13
      packages/noco-docs/content/en/setup-and-usages/meta-management.md
  55. 16
      packages/noco-docs/content/en/setup-and-usages/sync-schema.md
  56. 10531
      packages/nocodb-sdk/package-lock.json
  57. 29
      packages/nocodb-sdk/src/lib/Api.ts
  58. 3
      packages/nocodb-sdk/src/lib/formulaHelpers.ts
  59. 2
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  60. 25210
      packages/nocodb/package-lock.json
  61. 4
      packages/nocodb/package.json
  62. 3
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSqlv2.ts
  63. 6
      packages/nocodb/src/lib/dataMapper/lib/sql/conditionV2.ts
  64. 5
      packages/nocodb/src/lib/dataMapper/lib/sql/customValidators.ts
  65. 34
      packages/nocodb/src/lib/dataMapper/lib/sql/formulav2/formulaQueryBuilderv2.ts
  66. 8
      packages/nocodb/src/lib/noco-jobs/JobsMgr.ts
  67. 2
      packages/nocodb/src/lib/noco-models/Column.ts
  68. 4
      packages/nocodb/src/lib/noco-models/Filter.ts
  69. 59
      packages/nocodb/src/lib/noco/meta/api/columnApis.ts
  70. 455
      packages/nocodb/src/lib/noco/meta/api/sync/helpers/job.ts
  71. 43
      packages/nocodb/src/lib/noco/meta/api/sync/importApis.ts
  72. 2
      scripts/cypress/integration/common/3a_filter_sort_fields_operations.js
  73. 3
      scripts/cypress/integration/common/4c_form_view_detailed.js
  74. 3
      scripts/cypress/integration/common/4e_form_view_share.js
  75. 3
      scripts/cypress/integration/common/4f_grid_view_share.js
  76. 3
      scripts/cypress/integration/common/4f_pg_grid_view_share.js
  77. 3
      scripts/cypress/integration/common/6f_attachments.js
  78. 2
      scripts/cypress/support/commands.js
  79. 2
      scripts/sdk/swagger.json

9
.all-contributorsrc

@ -774,6 +774,15 @@
"contributions": [
"code"
]
},
{
"login": "RK311y",
"name": "River Kelly",
"avatar_url": "https://avatars.githubusercontent.com/u/65210753?v=4",
"profile": "https://github.com/RK311y",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

24
.github/workflows/ci-cd.yml

@ -25,7 +25,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 16
- name: Checkout
uses: actions/checkout@v2
with:
@ -70,7 +70,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 16
- name: Checkout
uses: actions/checkout@v2
with:
@ -115,7 +115,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 16
- name: Checkout
uses: actions/checkout@v2
with:
@ -160,7 +160,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 16
- name: Checkout
uses: actions/checkout@v2
with:
@ -205,7 +205,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 16
- name: Checkout
uses: actions/checkout@v2
with:
@ -250,7 +250,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 16
- name: Checkout
uses: actions/checkout@v2
with:
@ -295,7 +295,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 16
- name: Checkout
uses: actions/checkout@v2
with:
@ -340,7 +340,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 16
- name: Checkout
uses: actions/checkout@v2
with:
@ -385,7 +385,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 16
- name: Checkout
uses: actions/checkout@v2
with:
@ -430,7 +430,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 16
- name: Checkout
uses: actions/checkout@v2
with:
@ -475,7 +475,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 16
- name: Checkout
uses: actions/checkout@v2
with:
@ -520,7 +520,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
node-version: 16
- name: Checkout
uses: actions/checkout@v2
with:

2
.github/workflows/publish-api-docs.yml

@ -14,6 +14,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Pushes swagger file to src
uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658

2
.github/workflows/publish-blog.yml

@ -17,7 +17,7 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
- name: Build blogs
run: |
cd packages/noco-blog

2
.github/workflows/publish-dev-docs.yml

@ -18,7 +18,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
- name: Build docs
run: |
cd packages/noco-docs

12
.github/workflows/publish-docs.yml

@ -1,14 +1,10 @@
name: "Publish : Docs"
on:
push:
branches: [ master ]
paths:
- "packages/noco-docs/**"
release:
types: [ published ]
# Triggered manually
workflow_dispatch:
# Triggered by release-nocodb.yml
workflow_call:
jobs:
copy-file:
@ -20,14 +16,12 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
- name: Build docs
run: |
cd packages/noco-docs
npm install
npm run generate
- name: Pushes generated output
uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658
env:

2
.github/workflows/publish-noco-i18n.yml

@ -19,7 +19,7 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
- name: Build noco-i18n
run: |
cd packages/noco-i18n

2
.github/workflows/publish-prev-docs.yml

@ -19,7 +19,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
- name: Build prev docs
run: |
cd packages/noco-docs-prev

2
.github/workflows/release-docker.yml

@ -46,7 +46,7 @@ jobs:
working-directory: ./packages/nocodb
strategy:
matrix:
node-version: [14]
node-version: [16]
steps:
- name: Get Docker Repository
id: get-docker-repository

29
.github/workflows/release-draft.yml

@ -26,23 +26,36 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
ref: ${{ github.ref }}
- name: Get SHA
id: get-sha
# If it's triggered by release-nocodb.yml,
# the SHA from the third commit (i.e. Auto PR from master to develop) will be taken
# else HEAD will be taken
run: |
TARGET_SHA=$(git rev-parse HEAD~2)
if [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then
TARGET_SHA=$(git rev-parse HEAD)
fi
echo "::set-output name=TARGET_SHA::${TARGET_SHA}"
echo "Setting TARGET_SHA: ${{steps.get-sha.outputs.TARGET_SHA}}"
- name: Create tag
uses: actions/github-script@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# need workflows permission but it's not in GITHUB_TOKEN scope
# need a custom PAT with workflows permission here
github-token: ${{ secrets.NC_GITHUB_TOKEN }}
script: |
github.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: "refs/tags/${{ github.event.inputs.tag || inputs.tag }}",
sha: context.sha
sha: "${{steps.get-sha.outputs.TARGET_SHA}}"
})
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
- run: "npx github-release-notes@0.17.2 release --token ${{ secrets.GITHUB_TOKEN }} --draft --tags ${{ github.event.inputs.tag || inputs.tag }}..${{ github.event.inputs.prev_tag || inputs.prev_tag }}"

7
.github/workflows/release-nocodb.yml

@ -105,9 +105,14 @@ jobs:
DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}"
DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}"
# Publish Docs
publish-docs:
needs: release-docker
uses: ./.github/workflows/publish-docs.yml
# Change nocodb-sdk back to local path
update-sdk-path:
needs: release-docker
needs: publish-docs
uses: ./.github/workflows/update-sdk-path.yml
# Sync changes to develop

2
.github/workflows/release-npm.yml

@ -37,7 +37,7 @@ jobs:
working-directory: ./packages/nocodb
strategy:
matrix:
node-version: [14]
node-version: [16]
steps:
- name: Checkout
uses: actions/checkout@v2

4
.github/workflows/update-sdk-path.yml

@ -10,10 +10,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14]
node-version: [16]
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- run: |
cd packages/nocodb

10
README.md

@ -230,6 +230,7 @@ Access Dashboard using : [http://localhost:8080/dashboard](http://localhost:8080
* [Environment variables](#environment-variables)
- [Development Setup](#development-setup)
* [Cloning the Project](#cloning-the-project)
* [Build SDK](#build-sdk)
* [Running Backend locally](#running-backend-locally)
* [Running Frontend locally](#running-frontend-locally)
* [Running Cypress tests locally](#running-cypress-tests-locally)
@ -291,6 +292,14 @@ git clone https://github.com/nocodb/nocodb
cd nocodb
```
## Build SDK
```shell
cd packages/nocodb-sdk
npm install
npm run build
```
## Running Backend locally
```shell
@ -463,6 +472,7 @@ Our mission is to provide the most powerful no-code interface for databases whic
</tr>
<tr>
<td align="center"><a href="https://www.youyi.io"><img src="https://avatars.githubusercontent.com/u/49471274?v=4?s=100" width="100px;" alt=""/><br /><sub><b>youyiio</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=youyiio" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/RK311y"><img src="https://avatars.githubusercontent.com/u/65210753?v=4?s=100" width="100px;" alt=""/><br /><sub><b>River Kelly</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=RK311y" title="Code">💻</a></td>
</tr>
</table>

2
Run.md

@ -4,7 +4,7 @@
- Clone `nocodb/nocodb` GitHub repo and checkout to `feat/v2` branch
```sh
git clone https://github.com/nocodb/nc
git clone https://github.com/nocodb/nocodb
cd nocodb
```

4
package.json

@ -28,7 +28,9 @@
"updated:xc-migrator": "lerna run publish --scope xc-migrator && lerna run xc && lerna publish && npm install -f xc-cli",
"doc": "lerna run doc",
"install:local:dep": "cd packages/nc-lib-gui;npm uninstall -S xc-lib;rm package-lock.json; npm i ../../../xc-lib-private; cd ../xc-instant;npm uninstall -S xc-lib xc-lib-gui;npm i ../../../xc-lib-private;npm i ../xc-lib-gui",
"install:npm:dep": "cd packages/nc-lib-gui;npm uninstall -S xc-lib; npm i -S xc-lib@latest; cd ../xc-instant;npm uninstall -S xc-lib xc-lib-gui;npm i -S xc-lib@latest xc-lib-gui@latest;npm i ../xc-lib-gui"
"install:npm:dep": "cd packages/nc-lib-gui;npm uninstall -S xc-lib; npm i -S xc-lib@latest; cd ../xc-instant;npm uninstall -S xc-lib xc-lib-gui;npm i -S xc-lib@latest xc-lib-gui@latest;npm i ../xc-lib-gui",
"start:pg": "docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d",
"stop:pg": "docker-compose -f ./scripts/cypress/docker-compose-pg.yml down"
},
"dependencies": {
"mysql2": "^2.3.3",

6
packages/nc-gui/assets/css/global.css

@ -37,6 +37,10 @@ body {
Apply Vazirmatn for rtl
*/
.rtl .v-application .v-application--wrap * {
.rtl .v-application *:not(.material-icons) {
font-family: Vazirmatn !important;
}
.rtl .v-application .ml-n1 {
margin-left: 0px !important;
}

94
packages/nc-gui/components/ImportantAnnouncement.vue

@ -1,94 +0,0 @@
<template>
<v-menu bottom offset-y>
<template #activator="{on}">
<transition name="announcement">
<v-btn
v-show="announcementAlert"
text
small
class="mb-0 mr-2 py-0 "
v-on="on"
>
Announcement
<v-icon small>
mdi-menu-down
</v-icon>
</v-btn>
</transition>
</template>
<v-list dense>
<v-list-item dense>
<span class="message">
Starting from v0.90, <br/>
our API will undergo changes <br/>
and we are discontinuing GraphQL
</span>
</v-list-item>
<v-list-item dense href="https://github.com/nocodb/nocodb/issues/1564" target="_blank">
<v-icon small class="mr-2">
mdi-script-text-outline
</v-icon>
<span class="caption">
v0.90.0 API Changes
</span>
</v-list-item>
<v-list-item dense href="https://github.com/nocodb/nocodb/releases/tag/0.90.0" target="_blank">
<v-icon small class="mr-2">
mdi-script-text-outline
</v-icon>
<span class="caption">
v0.90.0 Release Note
</span>
</v-list-item>
<v-list-item @click="announcementAlert = false">
<v-icon small class="mr-2">
mdi-close
</v-icon>
<span class="caption">
<!--Hide menu-->
{{ $t('general.hideMenu') }}
</span>
</v-list-item>
</v-list>
</v-menu>
</template>
<script>
export default {
name: 'ImportantAnnouncement',
data: () => ({
loading: true
}),
computed: {
announcementAlert: {
get() {
return !this.loading && !this.$store.state.app.hiddenAnnouncement
},
set(val) {
return this.$store.commit('app/MutHiddenAnnouncement', val ? null : true)
}
}
},
mounted() {
setTimeout(() => {
this.loading = false
}, 1000)
}
}
</script>
<style scoped>
.announcement-enter-active, .announcement-leave-active {
transition: opacity .5s;
}
.announcement-enter, .announcement-leave-to {
opacity: 0;
}
.message {
font-size: 0.80rem !important;
font-weight: bold;
margin: 10px;
}
</style>

5
packages/nc-gui/components/PreviewAs.vue

@ -1,6 +1,9 @@
<template>
<div>
<v-menu offset-y>
<v-menu
offset-y
transition="slide-y-transition"
>
<template #activator="{ on }">
<v-btn
v-show="isDashboard && _isUIAllowed('previewAs')"

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

@ -1,14 +1,22 @@
<template>
<v-dialog v-model="airtableModal" max-width="min(600px, 90%)">
<v-dialog v-model="airtableModal" max-width="min(600px, 90%)" persistent>
<v-card class="nc-import-card h-100">
<v-toolbar class="elevation-0 align-center" height="68">
<h3 class="mt-2">
{{ $t('title.importFromAirtable') }}
</h3>
<div v-t="['c:airtable-import:turbo-mode']" class="ml-2 mt-3 title pointer nc-btn-enable-turbo" @click="enableTurbo">
<div
v-t="['c:airtable-import:turbo-mode']"
class="ml-2 mt-3 title pointer nc-btn-enable-turbo"
@click="enableTurbo"
>
🚀
</div>
<v-spacer />
<v-icon color="warning" class="" @click="airtableModal = false">
mdi-close
</v-icon>
</v-toolbar>
<v-divider />
@ -21,11 +29,18 @@
>
<template v-if="step === 1">
<div class="d-flex flex-column justify-center align-center pt-2 pb-6">
<span class="subtitle-1 font-weight-medium" @dblclick="$set(syncSource.details,'syncViews',true)">
<span
class="subtitle-1 font-weight-medium"
@dblclick="$set(syncSource.details.options,'syncViews',true)"
>
Credentials
</span>
<a href="https://docs.nocodb.com/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free/#get-airtable-credentials" class="caption grey--text" target="_blank">Where to find this?</a>
<a
href="https://docs.nocodb.com/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free/#get-airtable-credentials"
class="caption grey--text"
target="_blank"
>Where to find this?</a>
</div>
<v-form v-model="valid">
@ -55,7 +70,63 @@
:rules="[(v) => !!v || 'Shared Base ID / URL is required']"
/>
</div>
</v-form> <v-card-actions class="justify-center pb-6">
<v-expansion-panels v-model="advanceOptionsPanel" class="mx-auto" style="width: 50%;" flat>
<v-expansion-panel>
<v-expansion-panel-header hide-actions>
<v-spacer />
<span class="grey--text caption">More Options <v-icon color="grey" small>
mdi-chevron-{{ advanceOptionsPanel === 0 ? 'up' : 'down' }}
</v-icon></span>
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-checkbox
v-model="syncSource.details.options.syncData"
class="caption mt-n4"
label="Import Data"
hide-details
dense
/>
<v-checkbox
v-model="syncSource.details.options.syncRollup"
class="caption"
label="Import Rollup Columns"
hide-details
dense
/>
<v-checkbox
v-model="syncSource.details.options.syncLookup"
class="caption"
label="Import Lookup Columns"
hide-details
dense
/>
<v-checkbox
v-model="syncSource.details.options.syncAttachment"
class="caption"
label="Import Attachment Columns"
hide-details
dense
/>
<v-tooltip bottom>
<template #activator="{ on }">
<div v-on="on">
<v-checkbox
v-model="syncSource.details.options.syncFormula"
class="caption"
label="Import Formula Columns"
hide-details
dense
disabled
/>
</div>
</template>
<span>Coming Soon!</span>
</v-tooltip>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-form>
<v-card-actions class="justify-center pb-6 pt-6">
<v-btn
v-t="['c:sync-airtable:save-and-sync']"
class="nc-btn-airtable-import"
@ -114,9 +185,14 @@
</div>
</template>
<div class="text-center pa-4 pb-0">
<a class="caption grey--text" href="https://github.com/nocodb/nocodb/issues/2052" target="_blank">Questions / Help - reach out here</a>
<a class="caption grey--text" href="https://github.com/nocodb/nocodb/issues/2052" target="_blank">Questions
/ Help - reach out here</a>
<br>
<span class="caption grey--text"> This feature is currently in beta and more information can be found <a href="https://github.com/nocodb/nocodb/discussions/2122" class="caption grey--text" target="_blank">here</a>.</span>
<span class="caption grey--text"> This feature is currently in beta and more information can be found <a
href="https://github.com/nocodb/nocodb/discussions/2122"
class="caption grey--text"
target="_blank"
>here</a>.</span>
</div>
</v-card>
</div>
@ -134,12 +210,30 @@ export default {
value: Boolean
},
data: () => ({
advanceOptionsPanel: false,
isPasswordVisible: false,
valid: false,
socket: null,
step: 1,
progress: [],
syncSource: null,
syncSource: {
type: 'Airtable',
details: {
syncInterval: '15mins',
syncDirection: 'Airtable to NocoDB',
syncRetryCount: 1,
apiKey: '',
shareId: '',
options: {
syncViews: false,
syncData: true,
syncRollup: false,
syncLookup: true,
syncFormula: false,
syncAttachment: true
}
}
},
syncSourceUrlOrId: ''
}),
computed: {
@ -221,7 +315,7 @@ export default {
const { data: { list: srcs } } = await this.$axios.get(`/api/v1/db/meta/projects/${this.projectId}/syncs`)
if (srcs && srcs[0]) {
srcs[0].details = srcs[0].details || {}
this.syncSource = srcs[0]
this.syncSource = this.migrateSync(srcs[0])
this.syncSourceUrlOrId = srcs[0].details.shareId
} else {
this.syncSource = {
@ -230,11 +324,16 @@ export default {
syncInterval: '15mins',
syncDirection: 'Airtable to NocoDB',
syncRetryCount: 1,
syncViews: false,
apiKey: '',
shareId: ''
shareId: '',
options: {
syncViews: false,
syncData: true,
syncRollup: false,
syncLookup: true,
syncFormula: false,
syncAttachment: true
}
}
}
}
@ -252,8 +351,23 @@ export default {
}
},
enableTurbo() {
this.$set(this.syncSource.details, 'syncViews', true)
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 = {
syncViews: false,
syncData: true,
syncRollup: false,
syncLookup: true,
syncFormula: false,
syncAttachment: true
}
src.details.options.syncViews = src.syncViews
delete src.syncViews
}
return src
}
}
}

34
packages/nc-gui/components/project/spreadsheet/RowsXcDataTable.vue

@ -29,6 +29,9 @@
:key="col.column_name"
@click="searchField = col.title"
>
<v-icon color="grey darken-4" small class="mr-1">
{{ col.icon }}
</v-icon>
<span class="caption">{{ col.title }}</span>
</v-list-item>
</v-list>
@ -333,8 +336,13 @@
:style="{ height: isForm ? '100%' : 'calc(100% - 36px)' }"
style="overflow: auto; width: 100%"
>
<div v-if="loadingData && (isGallery || isGrid)" class="d-100 h-100 align-center justify-center d-flex flex-column">
<v-progress-circular size="40" color="grey" width="2" indeterminate class="mb-4" />
<span v-if="selectedView" class="caption grey--text">Loading view data... </span>
</div>
<template
v-if="selectedViewId && selectedView"
v-else-if="selectedViewId && selectedView"
>
<!-- <v-skeleton-loader v-if="!dataLoaded && loadingData || !meta" type="table" />-->
<template v-if="selectedView.type === viewTypes.GRID">
@ -504,7 +512,7 @@
@rerender="viewKey++"
@generateNewViewKey="generateNewViewKey"
@mapFieldsAndShowFields="mapFieldsAndShowFields"
@loadTableData="loadTableData"
@loadTableData="loadTableData(false)"
@showAdditionalFeatOverlay="showAdditionalFeatOverlay($event)"
>
<!-- <v-tooltip bottom>
@ -610,7 +618,7 @@
</v-list-item>
</template>
<template v-if="isEditable && !isLocked && rowContextMenu.col && !rowContextMenu.col.rqd && !rowContextMenu.col.virtual">
<template v-if="isEditable && !isLocked && rowContextMenu.col && !rowContextMenu.col.rqd && !rowContextMenu.col.virtual && rowContextMenu.col.uidt !== 'Formula'">
<v-tooltip bottom>
<template #activator="{ on }">
<v-list-item
@ -787,8 +795,8 @@ export default {
syncDataDebounce: debounce(async function(self) {
await self.syncData()
}, 500),
loadTableDataDeb: debounce(async function(self) {
await self.loadTableDataFn()
loadTableDataDeb: debounce(async function(self, ignoreLoader) {
await self.loadTableDataFn(ignoreLoader)
}, 200),
viewKey: 0,
extraViewParams: {},
@ -886,7 +894,7 @@ export default {
watch: {
isActive(n, o) {
if (!o && n) {
this.reload()
this.reload(true)
}
},
page(p) {
@ -975,7 +983,7 @@ export default {
await this.reload()
this.$e('a:table:reload:navbar')
},
async reload() {
async reload(ignoreLoader = false) {
this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
@ -985,7 +993,7 @@ export default {
if (this.selectedView && this.selectedView.show_as === 'kanban') {
await this.loadKanbanData()
} else {
await this.loadTableData()
await this.loadTableData(ignoreLoader)
}
this.key = Math.random()
},
@ -1396,17 +1404,17 @@ export default {
})
},
clickPagination() {
this.loadTableData()
this.loadTableData(false)
this.$e('a:grid:pagination')
},
loadTableData() {
this.loadTableDataDeb(this)
loadTableData(ignoreLoader = true) {
this.loadTableDataDeb(this, ignoreLoader)
},
async loadTableDataFn() {
async loadTableDataFn(ignoreLoader = true) {
if (this.isForm || !this.selectedView || !this.selectedView.title) {
return
}
this.loadingData = true
this.loadingData = !ignoreLoader
try {
// if (this.api) {
// const { list, count } = await this.api.paginatedList(this.queryParams)

4
packages/nc-gui/components/project/spreadsheet/components/Cell.vue

@ -18,6 +18,7 @@
<time-cell v-else-if="isTime" :value="value" />
<boolean-cell v-else-if="isBoolean" :value="value" read-only />
<rating-cell v-else-if="isRating" :value="value" read-only />
<currency-cell v-else-if="isCurrency" :value="value" :column="column" />
<span v-else :class="{'long-text-cell' : isTextArea}" :title="title">{{ value }}</span>
</template>
@ -35,10 +36,11 @@ import EditableAttachmentCell from '~/components/project/spreadsheet/components/
import BooleanCell from '~/components/project/spreadsheet/components/cell/BooleanCell'
import EmailCell from '~/components/project/spreadsheet/components/cell/EmailCell'
import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell'
import CurrencyCell from '@/components/project/spreadsheet/components/cell/CurrencyCell'
export default {
name: 'TableCell',
components: { RatingCell, EmailCell, TimeCell, DateTimeCell, DateCell, JsonCell, UrlCell, EditableAttachmentCell, EnumCell, SetListCell, BooleanCell },
components: { RatingCell, EmailCell, TimeCell, DateTimeCell, DateCell, JsonCell, UrlCell, EditableAttachmentCell, EnumCell, SetListCell, BooleanCell, CurrencyCell },
mixins: [cell],
props: ['value', 'dbAlias', 'isLocked', 'selected', 'column'],
computed: {

124
packages/nc-gui/components/project/spreadsheet/components/ColorPicker.vue

@ -0,0 +1,124 @@
<template>
<div class="color-picker">
<div
v-for="colId in Math.ceil(colors.length / rowSize)"
:key="colId"
class="color-picker-row"
>
<button
v-for="(color, i) in colors.slice((colId - 1) * rowSize, (colId) * rowSize)"
:key="`color-${colId}-${i}`"
class="color-selector"
:class="compare(picked, color) ? 'selected':''"
:style="{ 'background-color': color }"
@click="select(color)"
>
{{ compare(picked, color) ? '&#10003;':'' }}
</button>
</div>
<v-expansion-panels v-if="advanced">
<v-expansion-panel>
<v-expansion-panel-header>Advanced</v-expansion-panel-header>
<v-expansion-panel-content>
<v-container class="d-flex flex-column">
<v-btn class="primary lighten-2" @click="select(picked)">
Pick Color
</v-btn>
<v-color-picker
v-model="picked"
class="align-self-center ma-2"
canvas-height="100px"
mode="hexa"
/>
</v-container>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</div>
</template>
<script>
import { enumColor } from '@/components/project/spreadsheet/helpers/colors'
export default {
name: 'ColorPicker',
props: {
value: {
type: String,
default: () => enumColor.light[0]
},
colors: {
type: Array,
default: () => enumColor.light.concat(enumColor.dark)
},
rowSize: {
type: Number,
default: () => 10
},
advanced: {
type: Boolean,
default: () => true
}
},
data: () => ({
picked: ''
}),
created() {
this.picked = this.value || ''
},
methods: {
select(color) {
this.picked = color
this.$emit('input', color)
},
compare(colorA, colorB) {
if ((typeof colorA === 'string' || colorA instanceof String) &&
(typeof colorB === 'string' || colorB instanceof String)) {
return colorA.toLowerCase() === colorB.toLowerCase()
}
return false
}
}
}
</script>
<style scoped>
.color-picker {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: white;
padding: 10px;
}
.color-picker-row {
display: flex;
flex-direction: row;
}
.color-selector {
position: relative;
height: 32px;
width: 32px;
margin: 10px 5px;
border-radius: 5px;
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: white;
}
.color-selector:hover {
filter: brightness(90%);
-webkit-filter: brightness(90%);
}
.color-selector.selected {
filter: brightness(90%);
-webkit-filter: brightness(90%);
}
/deep/ .v-input__control {
height: auto!important;
}
</style>

24
packages/nc-gui/components/project/spreadsheet/components/ColumnFilter.vue

@ -109,12 +109,18 @@
@change="saveOrUpdate(filter, i)"
>
<template #item="{ item }">
<span
:class="`caption font-weight-regular nc-filter-fld-${item.title}`"
>
<span :class="`caption font-weight-regular nc-filter-fld-${item.title}`">
<v-icon small class="mr-1">
{{ item.icon }}
</v-icon>
{{ item.title }}
</span>
</template>
<template #selection="{item}">
<v-icon small class="mr-1">
{{ item.icon }}
</v-icon> {{ item.title }}
</template>
</v-select>
<v-select
:key="'k' + i"
@ -182,7 +188,7 @@
</template>
<script>
import { UITypes } from '~/components/project/spreadsheet/helpers/uiTypes'
import { getUIDTIcon, UITypes } from '~/components/project/spreadsheet/helpers/uiTypes'
export default {
name: 'ColumnFilter',
@ -267,6 +273,11 @@ export default {
]
}),
computed: {
columnIcon() {
return this.meta.columns.reduce((iconsObj, c) => {
return { ...iconsObj, [c.title]: getUIDTIcon(c.uidt) }
}, {})
},
columnsById() {
return (this.columns || []).reduce((o, c) => ({ ...o, [c.id]: c }), {})
},
@ -276,7 +287,10 @@ export default {
columns() {
return (
this.meta &&
this.meta.columns.filter(c => c && (!c.colOptions || !c.system))
this.meta.columns.filter(c => c && (!c.colOptions || !c.system)).map(c => ({
...c,
icon: getUIDTIcon(c.uidt)
}))
)
},
types() {

6
packages/nc-gui/components/project/spreadsheet/components/ColumnFilterMenu.vue

@ -1,5 +1,9 @@
<template>
<v-menu offset-y eager>
<v-menu
offset-y
eager
transition="slide-y-transition"
>
<template #activator="{ on }">
<v-badge :value="filters.length" color="primary" dot overlap>
<v-btn

42
packages/nc-gui/components/project/spreadsheet/components/EditColumn.vue

@ -166,6 +166,12 @@
:meta="meta"
/>
</v-col>
<currency-options
v-else-if="isCurrency"
v-model="newColumn.meta"
:column="newColumn"
:meta="meta"
/>
<v-col
v-if="accordion"
@ -428,14 +434,7 @@
@change="onDataTypeChange"
/>
</v-col>
<v-col
:cols="
sqlUi.showScale(newColumn) && !isSelect
? 6
: 12
"
>
<v-col :cols="sqlUi.showScale(newColumn) && !isSelect ? 6 : 12">
<!--label="Length / Values"-->
<v-text-field
v-if="!isSelect"
@ -572,6 +571,7 @@ import LinkedToAnotherOptions from '~/components/project/spreadsheet/components/
import { validateColumnName } from '~/helpers'
import RatingOptions from '~/components/project/spreadsheet/components/editColumn/RatingOptions'
import CheckboxOptions from '~/components/project/spreadsheet/components/editColumn/CheckboxOptions'
import CurrencyOptions from '@/components/project/spreadsheet/components/editColumn/CurrencyOptions'
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
@ -586,7 +586,8 @@ export default {
LinkedToAnotherOptions,
DlgLabelSubmitCancel,
RelationOptions,
CustomSelectOptions
CustomSelectOptions,
CurrencyOptions
},
props: {
nodes: Object,
@ -678,6 +679,9 @@ export default {
},
isVirtual() {
return this.isLinkToAnotherRecord || this.isLookup || this.isRollup
},
isCurrency() {
return this.newColumn && this.newColumn.uidt === UITypes.Currency
}
},
watch: {
@ -803,6 +807,16 @@ export default {
this.newColumn.dtxp = this.column.dtxp
}
if (this.isCurrency) {
if (this.column?.uidt === UITypes.Currency) {
this.newColumn.dtxp = this.column.dtxp
this.newColumn.dtxs = this.column.dtxs
} else {
this.newColumn.dtxp = 19
this.newColumn.dtxs = 2
}
}
// this.$set(this.newColumn, 'uidt', this.sqlUi.getUIType(this.newColumn));
this.newColumn.altered = this.newColumn.altered || 2
@ -843,6 +857,16 @@ export default {
}
}
if (this.isCurrency) {
if (this.column?.uidt === UITypes.Currency) {
this.newColumn.dtxp = this.column.dtxp
this.newColumn.dtxs = this.column.dtxs
} else {
this.newColumn.dtxp = 19
this.newColumn.dtxs = 2
}
}
this.newColumn.altered = this.newColumn.altered || 2
},
focusInput() {

12
packages/nc-gui/components/project/spreadsheet/components/FieldsMenu.vue

@ -1,5 +1,8 @@
<template>
<v-menu offset-y>
<v-menu
offset-y
transition="slide-y-transition"
>
<template #activator="{ on }">
<v-badge :value="isAnyFieldHidden" color="primary" dot overlap>
<v-btn
@ -123,6 +126,9 @@
@change="saveOrUpdate(field, i)"
>
<template #label>
<v-icon small class="mr-1">
{{ field.icon }}
</v-icon>
<span class="caption">{{ field.title }}</span>
</template>
</v-checkbox>
@ -172,6 +178,7 @@
<script>
import draggable from 'vuedraggable'
import { getSystemColumnsIds } from 'nocodb-sdk'
import { getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'
export default {
name: 'FieldsMenu',
@ -346,7 +353,8 @@ export default {
title: c.title,
fk_column_id: c.id,
...(fieldById[c.id] ? fieldById[c.id] : {}),
order: (fieldById[c.id] && fieldById[c.id].order) || order++
order: (fieldById[c.id] && fieldById[c.id].order) || order++,
icon: getUIDTIcon(c.uidt)
}))
.sort((a, b) => a.order - b.order)
} else if (this.isPublic) {

10
packages/nc-gui/components/project/spreadsheet/components/HeaderCell.vue

@ -51,6 +51,7 @@
open-on-hover
left
z-index="999"
transition="slide-y-transition"
>
<template #activator="{on}">
<v-icon v-if="!isLocked && !isVirtual" small v-on="on">
@ -104,7 +105,14 @@
</v-list>
</v-menu>
<v-menu v-model="editColumnMenu" z-index="999" offset-y content-class="" left>
<v-menu
v-model="editColumnMenu"
z-index="999"
offset-y
content-class=""
left
transition="slide-y-transition"
>
<template #activator="{on}">
<span v-on="on" />
</template>

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

@ -4,6 +4,7 @@
open-on-hover
bottom
offset-y
transition="slide-y-transition"
>
<template #activator="{on}">
<v-btn

103
packages/nc-gui/components/project/spreadsheet/components/SortListMenu.vue

@ -1,5 +1,8 @@
<template>
<v-menu offset-y>
<v-menu
offset-y
transition="slide-y-transition"
>
<template #activator="{ on }">
<v-badge :value="sortList && sortList.length" color="primary" dot overlap>
<v-btn
@ -15,10 +18,14 @@
}"
v-on="on"
>
<v-icon small class="mr-1" color="#777"> mdi-sort </v-icon>
<v-icon small class="mr-1" color="#777">
mdi-sort
</v-icon>
<!-- Sort -->
{{ $t("activity.sort") }}
<v-icon small color="#777"> mdi-menu-down </v-icon>
<v-icon small color="#777">
mdi-menu-down
</v-icon>
</v-btn>
</v-badge>
</template>
@ -49,10 +56,18 @@
@click.stop
@change="saveOrUpdate(sort, i)"
>
<template #selection="{item}">
<v-icon small class="mr-1">
{{ item.icon }}
</v-icon> {{ item.title }}
</template>
<template #item="{ item }">
<span
:class="`caption font-weight-regular nc-sort-fld-${item.title}`"
>
<v-icon color="grey darken-4" small class="mr-1">
{{ item.icon }}
</v-icon>
{{ item.title }}
</span>
</template>
@ -80,7 +95,9 @@
</template>
</div>
<v-btn small class="elevation-0 grey--text my-3" @click.stop="addSort">
<v-icon small color="grey"> mdi-plus </v-icon>
<v-icon small color="grey">
mdi-plus
</v-icon>
<!-- Add Sort Option -->
{{ $t("activity.addSort") }}
</v-btn>
@ -89,102 +106,106 @@
</template>
<script>
import { RelationTypes, UITypes } from "nocodb-sdk";
import { RelationTypes, UITypes } from 'nocodb-sdk'
import { getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'
export default {
name: "SortListMenu",
name: 'SortListMenu',
props: {
fieldList: Array,
value: [Array, Object],
isLocked: Boolean,
meta: [Object],
viewId: String,
shared: Boolean,
shared: Boolean
},
data: () => ({
sortList: [],
sortList: []
}),
computed: {
columns() {
if (!this.meta || !this.meta.columns) {
return [];
return []
}
return this.meta.columns.filter(
(c) =>
c =>
!(
c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions.type !== RelationTypes.BELONGS_TO
)
);
},
).map(c => ({
...c,
icon: getUIDTIcon(c.uidt)
}))
}
},
watch: {
value(v) {
this.sortList = v || [];
this.sortList = v || []
},
async viewId(v) {
if (v) {
await this.loadSortList();
await this.loadSortList()
}
}
},
},
async created() {
this.sortList = this.value || [];
this.loadSortList();
this.sortList = this.value || []
this.loadSortList()
},
methods: {
addSort() {
this.sortList.push({
fk_column_id: null,
direction: "asc",
});
this.sortList = this.sortList.slice();
this.$e("a:sort:add", { length: this.sortList.length });
direction: 'asc'
})
this.sortList = this.sortList.slice()
this.$e('a:sort:add', { length: this.sortList.length })
},
async loadSortList() {
if (!this.shared) {
// && !this._isUIAllowed('sortSync')) {
let sortList = [];
let sortList = []
if (this.viewId) {
const data = await this.$api.dbTableSort.list(this.viewId);
sortList = data.sorts.list;
const data = await this.$api.dbTableSort.list(this.viewId)
sortList = data.sorts.list
}
this.sortList = sortList;
this.sortList = sortList
}
},
async saveOrUpdate(sort, i) {
if (!this.shared && this._isUIAllowed("sortSync")) {
if (!this.shared && this._isUIAllowed('sortSync')) {
if (sort.id) {
await this.$api.dbTableSort.update(sort.id, sort);
await this.$api.dbTableSort.update(sort.id, sort)
} else {
this.$set(
this.sortList,
i,
await this.$api.dbTableSort.create(this.viewId, sort)
);
)
}
} else {
this.$emit("input", this.sortList);
this.$emit('input', this.sortList)
}
this.$emit("updated");
this.$emit('updated')
this.$e("a:sort:dir", { direction: sort.direction });
this.$e('a:sort:dir', { direction: sort.direction })
},
async deleteSort(sort, i) {
if (!this.shared && sort.id && this._isUIAllowed("sortSync")) {
await this.$api.dbTableSort.delete(sort.id);
await this.loadSortList();
if (!this.shared && sort.id && this._isUIAllowed('sortSync')) {
await this.$api.dbTableSort.delete(sort.id)
await this.loadSortList()
} else {
this.sortList.splice(i, 1);
this.$emit("input", this.sortList);
this.sortList.splice(i, 1)
this.$emit('input', this.sortList)
}
this.$emit("updated");
this.$e("a:sort:delete");
},
},
};
this.$emit('updated')
this.$e('a:sort:delete')
}
}
}
</script>
<style scoped>

3
packages/nc-gui/components/project/spreadsheet/components/SpreadsheetNavDrawer.vue

@ -678,7 +678,8 @@ export default {
'URL',
'DateTime',
'CreateTime',
'LastModifiedTime'
'LastModifiedTime',
'Currency'
].includes(col.uidt)
},
onPasswordProtectChange() {

9
packages/nc-gui/components/project/spreadsheet/components/VirtualHeaderCell.vue

@ -49,6 +49,7 @@
offset-y
open-on-hover
left
transition="slide-y-transition"
>
<template #activator="{on}">
<v-icon v-if="!isLocked && !isForm" small v-on="on">
@ -116,7 +117,13 @@
</v-card>
</v-dialog>
<v-menu v-model="editColumnMenu" offset-y content-class="" left>
<v-menu
v-model="editColumnMenu"
offset-y
content-class=""
left
transition="slide-y-transition"
>
<template #activator="{on}">
<span v-on="on" />
</template>

37
packages/nc-gui/components/project/spreadsheet/components/cell/CurrencyCell.vue

@ -0,0 +1,37 @@
<template>
<a v-if="value">{{ currency }}</a>
<span v-else />
</template>
<script>
export default {
name: 'CurrencyCell',
props: {
column: Object,
value: [String, Number]
},
computed: {
currency() {
try {
return new Intl.NumberFormat(this.currencyMeta.currency_locale || 'en-US',
{ style: 'currency', currency: this.currencyMeta.currency_code || 'USD' }).format(this.value)
} catch (e) {
return this.value
}
},
currencyMeta() {
return {
currency_locale: 'en-US',
currency_code: 'USD',
...(this.column && this.column.meta
? this.column.meta
: {})
}
}
}
}
</script>
<style scoped>
</style>

13
packages/nc-gui/components/project/spreadsheet/components/editColumn/CheckboxOptions.vue

@ -4,6 +4,7 @@
<v-select
v-model="colMeta.icon"
label="Icon"
:menu-props="{ bottom: true, offsetY: true }"
:items="icons"
dense
outlined
@ -29,18 +30,22 @@
</template>
</v-select>
</div>
<v-color-picker
<color-picker
v-model="colMeta.color"
class="mx-auto"
hide-inputs
row-size="8"
:colors="['#fcb401', '#faa307', '#f48c06', '#e85d04', '#dc2f02', '#d00000', '#9d0208', '#777']"
/>
</div>
</template>
<script>
import ColorPicker from '@/components/project/spreadsheet/components/ColorPicker.vue'
export default {
name: 'CheckboxOptions',
components: {
ColorPicker
},
props: ['column', 'meta', 'value'],
data: () => ({
colMeta: {
@ -82,7 +87,7 @@ export default {
}
},
created() {
this.colMeta = this.value || { ...this.colMeta }
this.colMeta = this.value ? { ...this.value } : { ...this.colMeta }
}
}
</script>

73
packages/nc-gui/components/project/spreadsheet/components/editColumn/CurrencyOptions.vue

@ -0,0 +1,73 @@
<template>
<v-row class="currency-wrapper">
<v-col cols="6">
<!--label="Format Locale"-->
<v-autocomplete
v-model="colMeta.currency_locale"
dense
class="caption"
label="Currency Locale"
:rules="[isValidCurrencyLocale]"
:items="currencyLocaleList"
outlined
hide-details
/>
</v-col>
<v-col cols="6">
<!--label="Currency Code"-->
<v-autocomplete
v-model="colMeta.currency_code"
dense
class="caption"
label="Currency Code"
:rules="[isValidCurrencyCode]"
:items="currencyList"
outlined
hide-details
/>
</v-col>
</v-row>
</template>
<script>
import { currencyCodes, currencyLocales, validateCurrencyCode, validateCurrencyLocale } from '~/helpers/currencyHelper'
export default {
name: 'CurrencyOptions',
props: ['column', 'meta', 'value'],
data: () => ({
colMeta: {
currency_locale: 'en-US',
currency_code: 'USD'
},
currencyList: currencyCodes,
currencyLocaleList: currencyLocales(),
isValidCurrencyLocale: (value) => {
return validateCurrencyLocale(value) || 'Invalid locale'
},
isValidCurrencyCode: (value) => {
return validateCurrencyCode(value) || 'Invalid Currency Code'
}
}),
watch: {
value() {
this.colMeta = this.value || {}
},
colMeta(v) {
this.$emit('input', v)
}
},
created() {
this.colMeta = this.value ? { ...this.value } : { ...this.colMeta }
}
}
</script>
<style scoped>
.currency-wrapper {
margin: 0;
}
/deep/ .v-input__append-inner {
margin-top: 4px !important;
}
</style>

32
packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue

@ -43,6 +43,9 @@
<span
class="caption primary--text text--lighten-2 font-weight-bold"
>
<v-icon color="primary lighten-2" small class="mr-1">
mdi-function
</v-icon>
{{ it.text }}
</span>
</v-list-item-content>
@ -70,10 +73,12 @@
<span
class="caption text--darken-3 font-weight-bold"
>
<v-icon color="grey darken-4" small class="mr-1">
{{ it.icon }}
</v-icon>
{{ it.text }}
</span>
</v-list-item-content>
<v-list-item-action>
<span class="caption">
Column
@ -87,6 +92,9 @@
<span
class="caption indigo--text text--darken-3 font-weight-bold"
>
<v-icon color="indigo darken-3" small class="mr-1">
mdi-calculator-variant
</v-icon>
{{ it.text }}
</span>
</v-list-item-content>
@ -109,7 +117,8 @@
import debounce from 'debounce'
import jsep from 'jsep'
import { UITypes, jsepCurlyHook } from 'nocodb-sdk'
import formulaList, { formulas, formulaTypes } from '../../../../../helpers/formulaList'
import { getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'
import formulaList, { formulas, formulaTypes } from '@/helpers/formulaList'
import { getWordUntilCaret, insertAtCursor } from '@/helpers'
import NcAutocompleteTree from '@/helpers/NcAutocompleteTree'
@ -146,6 +155,7 @@ export default {
...this.meta.columns.filter(c => !this.column || this.column.id !== c.id).map(c => ({
text: c.title,
type: 'column',
icon: getUIDTIcon(c.uidt),
c
})),
...this.availableBinOps.map(op => ({
@ -490,7 +500,7 @@ export default {
case UITypes.PhoneNumber:
case UITypes.Email:
case UITypes.URL:
return 'string'
return formulaTypes.STRING
// numeric
case UITypes.Year:
@ -499,14 +509,14 @@ export default {
case UITypes.Rating:
case UITypes.Count:
case UITypes.AutoNumber:
return 'number'
return formulaTypes.NUMERIC
// date
case UITypes.Date:
case UITypes.DateTime:
case UITypes.CreateTime:
case UITypes.LastModifiedTime:
return 'date'
return formulaTypes.DATE
// not supported
case UITypes.ForeignKey:
@ -527,20 +537,25 @@ export default {
}
}
} else if (parsedTree.type === jsep.BINARY_EXP || parsedTree.type === jsep.UNARY_EXP) {
return 'number'
return formulaTypes.NUMERIC
} else if (parsedTree.type === jsep.LITERAL) {
return typeof parsedTree.value
} else {
return 'N/A'
}
},
isCurlyBracketBalanced() {
// count number of opening curly brackets and closing curly brackets
const cntCurlyBrackets = (this.$refs.input.$el.querySelector('input').value.match(/\{|}/g) || []).reduce((acc, cur) => (acc[cur] = (acc[cur] || 0) + 1, acc), {})
return (cntCurlyBrackets['{'] || 0) === (cntCurlyBrackets['}'] || 0)
},
appendText(it) {
const text = it.text
const len = this.wordToComplete.length
if (it.type === 'function') {
this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len, 1))
} else if (it.type === 'column') {
this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), '{' + text + '}', len))
this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), '{' + text + '}', len + (!this.isCurlyBracketBalanced())))
} else {
this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len))
}
@ -560,6 +575,9 @@ export default {
const parts = query.split(/\W+/)
this.wordToComplete = parts.pop()
this.suggestion = this.acTree.complete(this.wordToComplete)?.sort((x, y) => this.sortOrder[x.type] - this.sortOrder[y.type])
if (!this.isCurlyBracketBalanced()) {
this.suggestion = this.suggestion.filter(v => v.type === 'column')
}
this.autocomplete = !!this.suggestion.length
},
selectText() {

14
packages/nc-gui/components/project/spreadsheet/components/editColumn/RatingOptions.vue

@ -4,6 +4,7 @@
<v-select
v-model="colMeta.icon"
label="Icon"
:menu-props="{ bottom: true, offsetY: true }"
:items="icons"
dense
outlined
@ -31,24 +32,29 @@
<v-select
v-model="colMeta.max"
label="Max"
:menu-props="{ bottom: true, offsetY: true }"
:items="[1,2,3,4,5,6,7,8,9,10]"
dense
outlined
class="caption"
/>
</div>
<v-color-picker
<color-picker
v-model="colMeta.color"
class="mx-auto"
hide-inputs
row-size="8"
:colors="['#fcb401', '#faa307', '#f48c06', '#e85d04', '#dc2f02', '#d00000', '#9d0208', '#777']"
/>
</div>
</template>
<script>
import ColorPicker from '@/components/project/spreadsheet/components/ColorPicker.vue'
export default {
name: 'RatingOptions',
components: {
ColorPicker
},
props: ['column', 'meta', 'value'],
data: () => ({
colMeta: {
@ -85,7 +91,7 @@ export default {
}
},
created() {
this.colMeta = this.value || { ...this.colMeta }
this.colMeta = this.value ? { ...this.value } : { ...this.colMeta }
}
}
</script>

14
packages/nc-gui/components/project/spreadsheet/helpers/uiTypes.js

@ -151,10 +151,20 @@ const uiTypes = [
]
const getUIDTIcon = (uidt) => {
return ([...uiTypes, {
return ([...uiTypes,
{
name: 'CreateTime',
icon: 'mdi-calendar-clock'
}].find(t => t.name === uidt) || {}).icon
},
{
name: 'ID',
icon: 'mdi-identifier'
},
{
name: 'ForeignKey',
icon: 'mdi-link-variant'
}
].find(t => t.name === uidt) || {}).icon
}
export {

2
packages/nc-gui/components/project/spreadsheet/mixins/cell.js

@ -66,7 +66,7 @@ export default {
return this.uiDatatype === UITypes.Rating
},
isCurrency() {
return this.column.uidt == 'Currency'
return this.uiDatatype === 'Currency'
}
}

6
packages/nc-gui/components/project/spreadsheet/mixins/spreadsheet.js

@ -1,4 +1,5 @@
import { isVirtualCol, filterOutSystemColumns } from 'nocodb-sdk'
import { getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'
export default {
data: () => ({
@ -91,7 +92,10 @@ export default {
}, [])
},
availableRealColumns() {
return this.availableColumns && this.availableColumns.filter(c => !isVirtualCol(c))
return this.availableColumns && this.availableColumns.filter(c => !isVirtualCol(c)).map(c => ({
...c,
icon: getUIDTIcon(c.uidt)
}))
},
allColumns() {

3
packages/nc-gui/components/project/spreadsheet/public/XcTable.vue

@ -77,7 +77,7 @@
:data="data"
:available-columns="availableColumns"
:show-fields="showFields"
:nodes="{dbAlias:''}"
:nodes="{...nodes, dbConnection:{client}}"
:sql-ui="sqlUi"
:columns-width="columnsWidth"
:password="password"
@ -401,6 +401,7 @@ export default {
this.sorts = this.viewMeta.sorts
this.viewName = this.viewMeta.title
this.client = this.viewMeta.client
} catch (e) {
if (e.response && e.response.status === 404) {
this.notFound = true

3
packages/nc-gui/components/project/spreadsheet/views/GridView.vue

@ -571,7 +571,8 @@ export default {
'URL',
'DateTime',
'CreateTime',
'LastModifiedTime'
'LastModifiedTime',
'Currency'
].includes(col.uidt)
},
async xcAuditModelCommentsCount() {

7
packages/nc-gui/components/utils/Language.vue

@ -90,8 +90,11 @@ export default {
},
methods: {
applyDirection() {
document.body.classList.add(this.isRtlLang() ? "rtl" : "ltr");
document.body.style.direction = this.isRtlLang() ? "rtl" : "ltr";
const targetDirection = this.isRtlLang() ? 'rtl' : 'ltr'
const oppositeDirection = targetDirection == 'ltr' ? 'rtl' : 'ltr'
document.body.classList.remove(oppositeDirection)
document.body.classList.add(targetDirection)
document.body.style.direction = targetDirection
},
isRtlLang() {
return ['fa'].includes(this.language)

82
packages/nc-gui/helpers/currencyHelper.js

@ -0,0 +1,82 @@
import locale from 'locale-codes'
export const currencyCodes = [
'AED', 'AFN', 'ALL', 'AMD', 'ANG', 'AOA', 'ARS', 'AUD',
'AWG', 'AZN', 'BAM', 'BBD', 'BDT', 'BGN', 'BHD', 'BIF',
'BMD', 'BND', 'BOB', 'BOV', 'BRL', 'BSD', 'BTN', 'BWP',
'BYR', 'BZD', 'CAD', 'CDF', 'CHE', 'CHF', 'CHW', 'CLF',
'CLP', 'CNY', 'COP', 'COU', 'CRC', 'CUP', 'CVE', 'CYP',
'CZK', 'DJF', 'DKK', 'DOP', 'DZD', 'EEK', 'EGP', 'ERN',
'ETB', 'EUR', 'FJD', 'FKP', 'GBP', 'GEL', 'GHC', 'GIP',
'GMD', 'GNF', 'GTQ', 'GYD', 'HKD', 'HNL', 'HRK', 'HTG',
'HUF', 'IDR', 'ILS', 'INR', 'IQD', 'IRR', 'ISK', 'JMD',
'JOD', 'JPY', 'KES', 'KGS', 'KHR', 'KMF', 'KPW', 'KRW',
'KWD', 'KYD', 'KZT', 'LAK', 'LBP', 'LKR', 'LRD', 'LSL',
'LTL', 'LVL', 'LYD', 'MAD', 'MDL', 'MGA', 'MKD', 'MMK',
'MNT', 'MOP', 'MRO', 'MTL', 'MUR', 'MVR', 'MWK', 'MXN',
'MXV', 'MYR', 'MZN', 'NAD', 'NGN', 'NIO', 'NOK', 'NPR',
'NZD', 'OMR', 'PAB', 'PEN', 'PGK', 'PHP', 'PKR', 'PLN',
'PYG', 'QAR', 'ROL', 'RON', 'RSD', 'RUB', 'RWF', 'SAR',
'SBD', 'SCR', 'SDD', 'SEK', 'SGD', 'SHP', 'SIT', 'SKK',
'SLL', 'SOS', 'SRD', 'STD', 'SYP', 'SZL', 'THB', 'TJS',
'TMM', 'TND', 'TOP', 'TRY', 'TTD', 'TWD', 'TZS', 'UAH',
'UGX', 'USD', 'USN', 'USS', 'UYU', 'UZS', 'VEB', 'VND',
'VUV', 'WST', 'XAF', 'XAG', 'XAU', 'XBA', 'XBB', 'XBC',
'XBD', 'XCD', 'XDR', 'XFO', 'XFU', 'XOF', 'XPD', 'XPF',
'XPT', 'XTS', 'XXX', 'YER', 'ZAR', 'ZMK', 'ZWD'
]
export function validateCurrencyCode(v) {
return currencyCodes.includes(v)
}
export function currencyLocales() {
const localeList = locale.all
.filter((l) => {
try {
if (Intl.NumberFormat.supportedLocalesOf(l.tag).length > 0) {
return true
}
return false
} catch (e) {
return false
}
})
.map((l) => {
return {
text: l.name + ' (' + l.tag + ')',
value: l.tag
}
})
return localeList
}
export function validateCurrencyLocale(v) {
try {
return Intl.NumberFormat.supportedLocalesOf(v).length > 0
} catch (e) {
return false
}
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Mert Ersoy <mertmit99@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

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

@ -156,7 +156,7 @@
"userMgmt": "مدیریت کاربران",
"apiTokenMgmt": "مدیریت توکنهای API",
"rolesMgmt": "مدیریت نقشها",
"projMeta": "فردادههای پروژه",
"projMeta": "فرادادههای پروژه",
"metaMgmt": "فرا مدیریت",
"metadata": "فراداده",
"exportImportMeta": "ورود و خروج فراداده",
@ -460,7 +460,7 @@
"loginMsg": "ورود به NocoDB",
"passwordRecovery": {
"message_1": "لطفا پست الکترونیکی خود را که در هنگام ثبت نام استفاده کردید وارد کنید.",
"message_2": "ما یک لینک جهت تغییر کلمه عبور به پست الکترونیکی شما ارسال خواهیمکرد.",
"message_2": "ما یک لینک جهت تغییر کلمه عبور به پست الکترونیکی شما ارسال خواهیم کرد.",
"success": "لطفا برای تغییر کلمه عبور به پست الکترونیکی خود مراجعه کنید"
},
"signUp": {

5
packages/nc-gui/layouts/default.vue

@ -58,7 +58,6 @@
<div style="flex: 1" class="d-flex justify-end">
<v-toolbar-items class="hidden-sm-and-down nc-topright-menu">
<important-announcement />
<release-info />
<language class="mr-3" />
@ -341,7 +340,6 @@ import Language from '~/components/utils/Language'
import Loader from '~/components/Loader'
import PreviewAs from '~/components/PreviewAs'
import ShareOrInviteModal from '~/components/auth/ShareOrInviteModal'
import ImportantAnnouncement from '../components/ImportantAnnouncement.vue'
import weAreHiring from '~/helpers/weAreHiring'
export default {
@ -353,8 +351,7 @@ export default {
Language,
XBtn,
dlgUnexpectedError,
settings,
ImportantAnnouncement
settings
},
data: () => ({
clickCount: true,

4
packages/nc-gui/mixins/device.js

@ -51,6 +51,10 @@ export default {
const browserLan = (navigator.languages || [navigator.language || navigator.userLanguage || 'en']).map(v => v.toLowerCase())
return zhLan.some(l => browserLan.includes(l))
},
_isRtl() {
const rtl = ['fa']
return rtl.includes(this.$store.state.settings.language)
},
...mapGetters({
_isUIAllowed: 'users/GtrIsUIAllowed',
projectName: 'project/GtrProjectName',

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

File diff suppressed because it is too large Load Diff

3
packages/nc-gui/package.json

@ -26,11 +26,12 @@
"httpsnippet": "^2.0.0",
"inflection": "^1.12.0",
"jsep": "^1.3.6",
"locale-codes": "^1.3.1",
"material-design-icons-iconfont": "^5.0.1",
"monaco-editor": "^0.19.3",
"monaco-themes": "^0.2.5",
"nano-assign": "^1.0.1",
"nocodb-sdk": "0.91.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nuxt": "^2.14.0",
"odometer": "^0.4.8",
"papaparse": "^5.3.1",

11
packages/nc-gui/pages/projects/index.vue

@ -436,7 +436,16 @@
mdi-github
</v-icon>
</v-list-item-icon>
<v-list-item-title>
<v-list-item-title v-if="_isRtl">
<!-- us on Github -->
{{ $t("labels.community.starUs2") }}
<!-- Star -->
{{ $t("labels.community.starUs1") }}
<v-icon small>
mdi-star-outline
</v-icon>
</v-list-item-title>
<v-list-item-title v-else>
<!-- Star -->
{{ $t("labels.community.starUs1") }}
<v-icon small>

6
packages/nc-gui/store/app.js

@ -1,8 +1,7 @@
export const state = () => ({
releaseVersion: null,
hiddenRelease: null,
latestRelease: null,
hiddenAnnouncement: null
latestRelease: null
})
export const mutations = {
@ -14,9 +13,6 @@ export const mutations = {
},
MutLatestRelease(state, latestRelease) {
state.latestRelease = latestRelease
},
MutHiddenAnnouncement(state, hiddenAnnouncement) {
state.hiddenAnnouncement = hiddenAnnouncement
}
}

1
packages/noco-docs/content/en/getting-started/installation.md

@ -207,6 +207,7 @@ By default, SQLite is used for storing meta data. However, you can specify your
| NC_DISABLE_ERR_REPORT | No | Disable error reporting | | |
| NC_REDIS_URL | No | Custom Redis URL. Example: `redis://:authpassword@127.0.0.1:6380/4` | Meta data will be stored in memory | |
| NC_DISABLE_CACHE | No | To be used only while debugging. On setting this to `true` - meta data be fetched from db instead of redis/cache. | `false` | |
| NC_BASEURL_INTERNAL | No | Used as base url for internal(server) API calls | Default value in docker will be `http://localhost:$PORT` and in all other case it's populated from request object | |
### AWS ECS (Fargate)

4
packages/noco-docs/content/en/setup-and-usages/audit.md

@ -8,8 +8,8 @@ menuTitle: 'Audit'
We are keeping all the user operation logs under Audit. Audits logs can be accessed by clicking `Team & Settings` from the left navigation drawer.
![image](https://user-images.githubusercontent.com/35857179/161902474-fd06678c-a171-4237-b171-dc028b3753de.png)
<img width="367" alt="image" src="https://user-images.githubusercontent.com/35857179/170426881-ba645392-24a2-4446-b501-0595a0887724.png">
Then, under SETTINGS, click `Audit`.
![image](https://user-images.githubusercontent.com/35857179/161956444-d1a28568-2adb-4e4e-9aca-4032b4a2f7c2.png)
<img width="1335" alt="image" src="https://user-images.githubusercontent.com/35857179/170428570-627a3763-26ae-4b8f-b5a8-0b8b42638464.png">

13
packages/noco-docs/content/en/setup-and-usages/meta-management.md

@ -8,13 +8,13 @@ menuTitle: 'Metadata'
Project Metadata can be found by clicking `Team & Settings` from the left navigation drawer
![image](https://user-images.githubusercontent.com/35857179/161902474-fd06678c-a171-4237-b171-dc028b3753de.png)
<img width="367" alt="image" src="https://user-images.githubusercontent.com/35857179/170426881-ba645392-24a2-4446-b501-0595a0887724.png">
and clicking `Project Metadata`.
![image](https://user-images.githubusercontent.com/35857179/161905030-6c5deef7-3a3d-4e71-8763-88e57586e5b4.png)
![image](https://user-images.githubusercontent.com/35857179/170427133-09faf93f-a41c-428b-b51c-fefe3fb45d9d.png)
## Project Metadata
<!-- ## Project Metadata
The metadata is stored in meta directory in project level, database level, and API level.
@ -47,17 +47,16 @@ From the source project, go to `Project Metadata`. Under ``Export / Import Metad
From the destination project, go to `Project Metadata`. Under ``Export / Import Metadata`` tab, select ``Import zip``, select ``meta.zip`` file stored in previous step. This step imports project metadata from compressed file (zip) selected and restarts the project.
![image](https://user-images.githubusercontent.com/35857179/161904452-da0ac683-1715-438a-9c9c-91b34f8f45ba.png)
![image](https://user-images.githubusercontent.com/35857179/161904452-da0ac683-1715-438a-9c9c-91b34f8f45ba.png) -->
## Database Metadata
Go to `Project Metadata`, under ``Metadata``, you can see your metadata sync status. If it is out of sync, you can sync the schema. See <a href="./sync-schema">Sync Schema</a> for more.
![image](https://user-images.githubusercontent.com/35857179/161904869-e6c8fe74-3156-49bc-be66-09f8d676aa83.png)
<img width="1339" alt="image" src="https://user-images.githubusercontent.com/35857179/170427543-07dfdc30-b8f9-4e4f-bd5b-96f93a16b2fe.png">
## UI Access Control
Go to `Project Metadata`, under ``UI Access Control``, you can control the access to each table by roles.
![image](https://user-images.githubusercontent.com/35857179/161904939-6869e36d-0612-4ae5-a123-fee371472ede.png)
<img width="1335" alt="image" src="https://user-images.githubusercontent.com/35857179/170427529-8bb403bc-0c1f-43ff-868a-c17c8ce9b778.png">

16
packages/noco-docs/content/en/setup-and-usages/sync-schema.md

@ -14,23 +14,19 @@ Below are the steps to sync schema changes.
### 1. From the menu bar, click `Team & Settings`
![image](https://user-images.githubusercontent.com/35857179/161902474-fd06678c-a171-4237-b171-dc028b3753de.png)
<img width="367" alt="image" src="https://user-images.githubusercontent.com/35857179/170426881-ba645392-24a2-4446-b501-0595a0887724.png">
### 2. Click `Project Metadata` under SETTINGS
### 2. Click `Project Metadata` under SETTINGS and click `Metadata`
![image](https://user-images.githubusercontent.com/35857179/161905030-6c5deef7-3a3d-4e71-8763-88e57586e5b4.png)
![image](https://user-images.githubusercontent.com/35857179/170427133-09faf93f-a41c-428b-b51c-fefe3fb45d9d.png)
### 3. Under `Metadata` tab, click on `Metadata` sub tab
![image](https://user-images.githubusercontent.com/35857179/161956928-95d3646c-5ae4-4562-8a65-5e36a63e3c2a.png)
### 4. Changes carried outside GUI, identified by NocoDB are listed under `Sync state`
### 3. Changes carried outside GUI, identified by NocoDB are listed under `Sync state`
![image](https://user-images.githubusercontent.com/35857179/161957119-f66f22ad-9d37-45ed-84ca-35c99726078c.png)
### 5. Click `Sync Now` to complete Schema sync procedure
### 4. Click `Sync Now` to complete Schema sync procedure
![image](https://user-images.githubusercontent.com/35857179/161957228-de6b0a50-0a0f-4d3d-8624-585a28851ad7.png)
<img width="1352" alt="image" src="https://user-images.githubusercontent.com/35857179/170428004-022dd436-0c58-41c5-b5e6-89d1d3ac87b0.png">
#### Notes

10531
packages/nocodb-sdk/package-lock.json generated

File diff suppressed because it is too large Load Diff

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

@ -2690,13 +2690,38 @@ export class Api<
...params,
}),
/**
* No description
*
* @tags DB view row
* @name FindOne
* @summary Table view row FindOne
* @request GET:/api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/find-one
* @response `200` `any` OK
*/
findOne: (
orgs: string,
projectName: string,
tableName: string,
viewName: string,
query?: { fields?: any[]; sort?: any[]; where?: string; nested?: any },
params: RequestParams = {}
) =>
this.request<any, any>({
path: `/api/v1/db/data/${orgs}/${projectName}/${tableName}/views/${viewName}/find-one`,
method: 'GET',
query: query,
format: 'json',
...params,
}),
/**
* No description
*
* @tags DB view row
* @name GroupBy
* @summary Table view row Group by
* @request GET:/api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/find-one
* @request GET:/api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/groupby
* @response `200` `any` OK
*/
groupBy: (
@ -2714,7 +2739,7 @@ export class Api<
params: RequestParams = {}
) =>
this.request<any, any>({
path: `/api/v1/db/data/${orgs}/${projectName}/${tableName}/views/${viewName}/find-one`,
path: `/api/v1/db/data/${orgs}/${projectName}/${tableName}/views/${viewName}/groupby`,
method: 'GET',
query: query,
format: 'json',

3
packages/nocodb-sdk/src/lib/formulaHelpers.ts

@ -84,9 +84,6 @@ export function substituteColumnIdWithAliasInFormula(
c.title === colNameOrId
);
pt.name = column?.title || ptRaw?.name || pt?.name;
if (pt.name[0] != '$' && pt.name[pt.name.length - 1] != '$') {
pt.name = '$' + pt.name + '$';
}
} else if (pt.type === 'BinaryExpression') {
substituteId(pt.left, ptRaw?.left);
substituteId(pt.right, ptRaw?.right);

2
packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts

@ -1657,7 +1657,7 @@ export class PgUi {
};
break;
case 'Percent':
colProp.dt = 'double';
colProp.dt = 'double precision';
break;
case 'Duration':
colProp.dt = 'int8';

25210
packages/nocodb/package-lock.json generated

File diff suppressed because it is too large Load Diff

4
packages/nocodb/package.json

@ -152,11 +152,11 @@
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-common": "0.0.6",
"nc-help": "0.2.56",
"nc-help": "0.2.59",
"nc-lib-gui": "0.91.0",
"nc-plugin": "^0.1.1",
"ncp": "^2.0.0",
"nocodb-sdk": "0.91.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"ora": "^4.0.4",

3
packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSqlv2.ts

@ -38,6 +38,7 @@ import {
parseBody
} from '../../../noco/meta/helpers/webhookHelpers';
import Validator from 'validator';
import { customValidators } from './customValidators';
import { NcError } from '../../../noco/meta/helpers/catchError';
import { customAlphabet } from 'nanoid';
@ -1833,7 +1834,7 @@ class BaseModelSqlv2 {
if (!validate) continue;
const { func, msg } = validate;
for (let j = 0; j < func.length; ++j) {
const fn = typeof func[j] === 'string' ? Validator[func[j]] : func[j];
const fn = typeof func[j] === 'string' ? (customValidators[func[j]] ? customValidators[func[j]] : Validator[func[j]]) : func[j];
const arg =
typeof func[j] === 'string' ? columns[cn] + '' : columns[cn];
if (

6
packages/nocodb/src/lib/dataMapper/lib/sql/conditionV2.ts

@ -284,6 +284,12 @@ const parseConditionV2 = async (
case 'notnull':
qb = qb.whereNotNull(customWhereClause || field);
break;
case 'btw':
qb = qb.whereBetween(field, val.split(','));
break;
case 'nbtw':
qb = qb.whereNotBetween(field, val.split(','));
break;
}
};
}

5
packages/nocodb/src/lib/dataMapper/lib/sql/customValidators.ts

@ -0,0 +1,5 @@
import Validator from 'validator';
export const customValidators = {
isCurrency: Validator['isFloat']
}

34
packages/nocodb/src/lib/dataMapper/lib/sql/formulav2/formulaQueryBuilderv2.ts

@ -69,6 +69,7 @@ export default async function formulaQueryBuilderv2(
model,
{ ...aliasToColumn, [col.id]: null }
);
builder.sql = '(' + builder.sql + ')';
aliasToColumn[col.id] = builder;
}
break;
@ -611,7 +612,21 @@ export default async function formulaQueryBuilderv2(
return knex.raw(
`${pt.callee.name}(${pt.arguments
.map(arg => fn(arg).toQuery())
.map(arg => {
const query = fn(arg).toQuery();
if (pt.callee.name === 'CONCAT') {
if (knex.clientType() === 'mysql2') {
// mysql2: CONCAT() returns NULL if any argument is NULL.
// adding IFNULL to convert NULL values to empty strings
return `IFNULL(${query}, '')`;
} else {
// do nothing
// pg / mssql: Concatenate all arguments. NULL arguments are ignored.
// sqlite3: special handling - See BinaryExpression
}
}
return query;
})
.join()})${colAlias}`
);
} else if (pt.type === 'Literal') {
@ -636,13 +651,16 @@ export default async function formulaQueryBuilderv2(
pt.left.fnName = pt.left.fnName || 'ARITH';
pt.right.fnName = pt.right.fnName || 'ARITH';
const query = knex.raw(
`${fn(pt.left, null, pt.operator).toQuery()} ${pt.operator} ${fn(
pt.right,
null,
pt.operator
).toQuery()}${colAlias}`
);
const left = fn(pt.left, null, pt.operator).toQuery();
const right = fn(pt.right, null, pt.operator).toQuery();
let sql = `${left} ${pt.operator} ${right}${colAlias}`;
// handle NULL values when calling CONCAT for sqlite3
if (pt.left.fnName === 'CONCAT' && knex.clientType() === 'sqlite3') {
sql = `COALESCE(${left}, '') ${pt.operator} COALESCE(${right},'')${colAlias}`;
}
const query = knex.raw(sql);
if (prevBinaryOp && pt.operator !== prevBinaryOp) {
query.wrap('(', ')');
}

8
packages/nocodb/src/lib/noco-jobs/JobsMgr.ts

@ -42,7 +42,9 @@ export default abstract class JobsMgr {
}
protected async invokeSuccessCbks(jobName: string, payload: any) {
await Promise.all(this.successCbks?.[jobName]?.map(cb => cb(payload)));
await Promise.all(
this.successCbks?.[jobName]?.map(cb => cb(payload)) || []
);
}
protected async invokeFailureCbks(
jobName: string,
@ -50,7 +52,7 @@ export default abstract class JobsMgr {
error?: Error
) {
await Promise.all(
this.failureCbks?.[jobName]?.map(cb => cb(payload, error))
this.failureCbks?.[jobName]?.map(cb => cb(payload, error)) || []
);
}
protected async invokeProgressCbks(
@ -59,7 +61,7 @@ export default abstract class JobsMgr {
data?: any
) {
await Promise.all(
this.progressCbks?.[jobName]?.map(cb => cb(payload, data))
this.progressCbks?.[jobName]?.map(cb => cb(payload, data)) || []
);
}
}

2
packages/nocodb/src/lib/noco-models/Column.ts

@ -541,7 +541,7 @@ export default class Column<T = any> implements ColumnType {
title: col?.title
})
)
await FormulaColumn.update(formula.id, formula, ncMeta);
await FormulaColumn.update(formulaCol.id, formula, ncMeta);
}
}

4
packages/nocodb/src/lib/noco-models/Filter.ts

@ -37,7 +37,9 @@ export default class Filter {
| 'le'
| 'in'
| 'isnot'
| 'is';
| 'is'
| 'btw'
| 'nbtw';
value?: string;
logical_op?: string;

59
packages/nocodb/src/lib/noco/meta/api/columnApis.ts

@ -296,6 +296,19 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
parentColumn: parent.primaryKey.column_name
});
}
// todo: create index for virtual relations as well
// create index for foreign key in pg
if (base.type === 'pg') {
await createColumnIndex({
column: new Column({
...newColumn,
fk_model_id: child.id
}),
base,
sqlMgr
});
}
}
await createHmAndBtColumn(
child,
@ -450,6 +463,27 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
fk_mm_parent_column_id: childCol.id,
fk_related_model_id: child.id
});
// todo: create index for virtual relations as well
// create index for foreign key in pg
if (base.type === 'pg') {
await createColumnIndex({
column: new Column({
...associateTableCols[0],
fk_model_id: assocModel.id
}),
base,
sqlMgr
});
await createColumnIndex({
column: new Column({
...associateTableCols[1],
fk_model_id: assocModel.id
}),
base,
sqlMgr
});
}
}
}
Tele.emit('evt', { evt_type: 'relation:created' });
@ -649,7 +683,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
formula,
[new_column]
);
await FormulaColumn.update(f.id, {
await FormulaColumn.update(c.id, {
formula_raw: new_formula_raw
});
}
@ -995,6 +1029,29 @@ const deleteHmOrBtRelation = async (
await Column.delete(childColumn.id, ncMeta);
};
async function createColumnIndex({
column,
sqlMgr,
base,
indexName = null,
nonUnique = true
}: {
column: Column;
sqlMgr: SqlMgrv2;
base: Base;
indexName?: string;
nonUnique?: boolean;
}) {
const model = await column.getModel();
const indexArgs = {
columns: [column.column_name],
tn: model.table_name,
non_unique: nonUnique,
indexName
};
sqlMgr.sqlOpPlus(base, 'indexCreate', indexArgs);
}
const router = Router({ mergeParams: true });
router.post(
'/api/v1/db/meta/tables/:tableId/columns/',

455
packages/nocodb/src/lib/noco/meta/api/sync/helpers/job.ts

@ -1,5 +1,6 @@
import FetchAT from './fetchAT';
import { UITypes } from 'nocodb-sdk';
import { Tele } from 'nc-help';
// import * as sMap from './syncMap';
import FormData from 'form-data';
@ -56,10 +57,20 @@ export default async (
if (debugMode) progress({ level: 1, msg: log });
}
const perfStats = [];
function recordPerfStart() {
if (!debugMode) return 0;
return Date.now();
}
function recordPerfStats(start, event) {
if (!debugMode) return;
const duration = Date.now() - start;
perfStats.push({ d: duration, e: event });
}
let base, baseId;
const start = Date.now();
const enableErrorLogs = false;
const process_aTblData = true;
const generate_migrationStats = true;
const debugMode = false;
let api: Api<any>;
@ -69,6 +80,9 @@ export default async (
const nestedLookupTbl: any[] = [];
const nestedRollupTbl: any[] = [];
const ncSysFields = { id: 'ncRecordId', hash: 'ncRecordHash' };
const storeLinks = false;
const skipAttachments = false;
const ncLinkDataStore: any = {};
const uniqueTableNameGen = getUniqueNameGenerator('sheet');
@ -173,7 +187,11 @@ export default async (
}
function nc_getSanitizedColumnName(table, name) {
const col_name = nc_sanitizeName(name);
let col_name = nc_sanitizeName(name);
// truncate to 60 chars if character if exceeds above 60
col_name = col_name?.slice(0, 60);
// for knex, replace . with _
const col_alias = name.trim().replace(/\./g, '_');
@ -350,7 +368,8 @@ export default async (
);
}
// const csvOpt = "'" + opt.join("','") + "'";
const csvOpt = opt
const optSansDuplicate = [...new Set(opt)];
const csvOpt = optSansDuplicate
.map(v => `'${v.replace(/'/g, "\\'").replace(/,/g, '.')}'`)
.join(',');
return { type: 'select', data: csvOpt };
@ -368,7 +387,7 @@ export default async (
for (let i = 0; i < tblSchema.length; ++i) {
const table: any = {};
if (syncDB.syncViews) {
if (syncDB.options.syncViews) {
rtc.view.total += tblSchema[i].views.reduce(
(acc, cur) =>
['grid', 'form', 'gallery'].includes(cur.type) ? ++acc : acc,
@ -380,7 +399,15 @@ export default async (
// Enable to use aTbl identifiers as is: table.id = tblSchema[i].id;
table.title = tblSchema[i].name;
table.table_name = uniqueTableNameGen(nc_sanitizeName(tblSchema[i].name));
let sanitizedName = nc_sanitizeName(tblSchema[i].name);
// truncate to 50 chars if character if exceeds above 50
// upto 64 should be fine but we are keeping it to 50 since
// meta project adds prefix as well
sanitizedName = sanitizedName?.slice(0, 50);
// check for duplicate and populate a unique name if already exist
table.table_name = uniqueTableNameGen(sanitizedName);
const uniqueColNameGen = getUniqueNameGenerator('field');
table.columns = [];
@ -471,10 +498,14 @@ export default async (
logBasic(`:: [${idx + 1}/${tables.length}] ${tables[idx].title}`);
logDetailed(`NC API: dbTable.create ${tables[idx].title}`);
let _perfStart = recordPerfStart();
const table: any = await api.dbTable.create(
ncCreatedProjectSchema.id,
tables[idx]
);
recordPerfStats(_perfStart, 'dbTable.create');
updateNcTblSchema(table);
// update mapping table
@ -494,13 +525,18 @@ export default async (
// update default view name- to match it to airtable view name
logDetailed(`NC API: dbView.list ${table.id}`);
_perfStart = recordPerfStart();
const view = await api.dbView.list(table.id);
recordPerfStats(_perfStart, 'dbView.list');
const aTbl_grid = aTblSchema[idx].views.find(x => x.type === 'grid');
logDetailed(`NC API: dbView.update ${view.list[0].id} ${aTbl_grid.name}`);
_perfStart = recordPerfStart();
await api.dbView.update(view.list[0].id, {
title: aTbl_grid.name
});
recordPerfStats(_perfStart, 'dbView.update');
await updateNcTblSchemaById(table.id);
await sMap.addToMappingTbl(
@ -563,7 +599,9 @@ export default async (
}
// check if already a column exists with this name?
let _perfStart = recordPerfStart();
const srcTbl: any = await api.dbTable.read(srcTableId);
recordPerfStats(_perfStart, 'dbTable.read');
// create link
const ncName = nc_getSanitizedColumnName(
@ -574,6 +612,7 @@ export default async (
logDetailed(
`NC API: dbTableColumn.create LinkToAnotherRecord ${ncName.title}`
);
_perfStart = recordPerfStart();
const ncTbl: any = await api.dbTableColumn.create(srcTableId, {
uidt: UITypes.LinkToAnotherRecord,
title: ncName.title,
@ -585,6 +624,8 @@ export default async (
// ? 'mm'
// : 'hm'
});
recordPerfStats(_perfStart, 'dbTableColumn.create');
updateNcTblSchema(ncTbl);
const ncId = ncTbl.columns.find(x => x.title === ncName.title)?.id;
@ -599,7 +640,7 @@ export default async (
// this information will be helpful in identifying relation pair
const link = {
nc: {
title: aTblLinkColumns[i].name,
title: ncName.title,
parentId: srcTableId,
childId: childTableId,
type: 'mm'
@ -626,12 +667,17 @@ export default async (
x.aTbl.id === aTblLinkColumns[i].typeOptions.symmetricColumnId
);
let _perfStart = recordPerfStart();
const childTblSchema: any = await api.dbTable.read(
ncLinkMappingTable[x].nc.childId
);
recordPerfStats(_perfStart, 'dbTable.read');
_perfStart = recordPerfStart();
const parentTblSchema: any = await api.dbTable.read(
ncLinkMappingTable[x].nc.parentId
);
recordPerfStats(_perfStart, 'dbTable.read');
// fix me
// let childTblSchema = ncSchema.tablesById[ncLinkMappingTable[x].nc.childId]
@ -641,6 +687,16 @@ export default async (
col => col.title === ncLinkMappingTable[x].nc.title
);
if (parentLinkColumn === undefined) {
updateMigrationSkipLog(
parentTblSchema?.title,
ncLinkMappingTable[x].nc.title,
UITypes.LinkToAnotherRecord,
'Link error'
);
continue;
}
// hack // fix me
if (parentLinkColumn.uidt !== 'LinkToAnotherRecord') {
parentLinkColumn = parentTblSchema.columns.find(
@ -698,6 +754,7 @@ export default async (
logDetailed(
`NC API: dbTableColumn.update rename symmetric column ${ncName.title}`
);
_perfStart = recordPerfStart();
const ncTbl: any = await api.dbTableColumn.update(
childLinkColumn.id,
{
@ -706,6 +763,8 @@ export default async (
column_name: ncName.column_name
}
);
recordPerfStats(_perfStart, 'dbTableColumn.update');
updateNcTblSchema(ncTbl);
const ncId = ncTbl.columns.find(
@ -768,7 +827,10 @@ export default async (
aTblColumns[i].typeOptions.foreignTableRollupColumnId
);
if (ncLookupColumnId === undefined) {
if (
ncLookupColumnId === undefined ||
ncRelationColumnId === undefined
) {
aTblColumns[i]['srcTableId'] = srcTableId;
nestedLookupTbl.push(aTblColumns[i]);
continue;
@ -780,6 +842,7 @@ export default async (
);
logDetailed(`NC API: dbTableColumn.create LOOKUP ${ncName.title}`);
const _perfStart = recordPerfStart();
const ncTbl: any = await api.dbTableColumn.create(srcTableId, {
uidt: UITypes.Lookup,
title: ncName.title,
@ -787,6 +850,8 @@ export default async (
fk_relation_column_id: ncRelationColumnId,
fk_lookup_column_id: ncLookupColumnId
});
recordPerfStats(_perfStart, 'dbTableColumn.create');
updateNcTblSchema(ncTbl);
const ncId = ncTbl.columns.find(x => x.title === aTblColumns[i].name)
@ -853,6 +918,7 @@ export default async (
);
logDetailed(`NC API: dbTableColumn.create LOOKUP ${ncName.title}`);
const _perfStart = recordPerfStart();
const ncTbl: any = await api.dbTableColumn.create(srcTableId, {
uidt: UITypes.Lookup,
title: ncName.title,
@ -860,6 +926,8 @@ export default async (
fk_relation_column_id: ncRelationColumnId,
fk_lookup_column_id: ncLookupColumnId
});
recordPerfStats(_perfStart, 'dbTableColumn.create');
updateNcTblSchema(ncTbl);
const ncId = ncTbl.columns.find(
@ -900,6 +968,7 @@ export default async (
return aTbl_ncRollUp[fn];
}
//@ts-ignore
async function nocoCreateRollup(aTblSchema) {
// Rollup
for (let idx = 0; idx < aTblSchema.length; idx++) {
@ -925,8 +994,9 @@ export default async (
const ncRollupFn = getRollupNcFunction(
aTblColumns[i].typeOptions.formulaTextParsed
);
// const ncRollupFn = '';
if (ncRollupFn === '') {
if (ncRollupFn === '' || ncRollupFn === undefined) {
updateMigrationSkipLog(
srcTableSchema.title,
aTblColumns[i].name,
@ -990,6 +1060,7 @@ export default async (
);
logDetailed(`NC API: dbTableColumn.create ROLLUP ${ncName.title}`);
const _perfStart = recordPerfStart();
const ncTbl: any = await api.dbTableColumn.create(srcTableId, {
uidt: UITypes.Rollup,
title: ncName.title,
@ -998,6 +1069,8 @@ export default async (
fk_rollup_column_id: ncRollupColumnId,
rollup_function: ncRollupFn
});
recordPerfStats(_perfStart, 'dbTableColumn.create');
updateNcTblSchema(ncTbl);
const ncId = ncTbl.columns.find(x => x.title === aTblColumns[i].name)
@ -1014,6 +1087,7 @@ export default async (
logDetailed(`Nested rollup: ${nestedRollupTbl.length}`);
}
//@ts-ignore
async function nocoLookupForRollup() {
const nestedCnt = nestedLookupTbl.length;
for (let i = 0; i < nestedLookupTbl.length; i++) {
@ -1043,6 +1117,7 @@ export default async (
);
logDetailed(`NC API: dbTableColumn.create LOOKUP ${ncName.title}`);
const _perfStart = recordPerfStart();
const ncTbl: any = await api.dbTableColumn.create(srcTableId, {
uidt: UITypes.Lookup,
title: ncName.title,
@ -1050,6 +1125,8 @@ export default async (
fk_relation_column_id: ncRelationColumnId,
fk_lookup_column_id: ncLookupColumnId
});
recordPerfStats(_perfStart, 'dbTableColumn.create');
updateNcTblSchema(ncTbl);
const ncId = ncTbl.columns.find(x => x.title === nestedLookupTbl[0].name)
@ -1080,7 +1157,9 @@ export default async (
// skip primary column configuration if we field not migrated
if (ncColId) {
logDetailed(`NC API: dbTableColumn.primaryColumnSet`);
const _perfStart = recordPerfStart();
await api.dbTableColumn.primaryColumnSet(ncColId);
recordPerfStats(_perfStart, 'dbTableColumn.primaryColumnSet');
// update schema
const ncTblId = sMap.getNcIdFromAtId(aTblSchema[idx].id);
@ -1094,11 +1173,17 @@ export default async (
// retrieve view Info
let viewDetails;
if (viewType === 'form')
const _perfStart = recordPerfStart();
if (viewType === 'form') {
viewDetails = (await api.dbView.formRead(viewId)).columns;
else if (viewType === 'gallery')
recordPerfStats(_perfStart, 'dbView.formRead');
} else if (viewType === 'gallery') {
viewDetails = (await api.dbView.galleryRead(viewId)).columns;
else viewDetails = await api.dbView.gridColumnsList(viewId);
recordPerfStats(_perfStart, 'dbView.galleryRead');
} else {
viewDetails = await api.dbView.gridColumnsList(viewId);
recordPerfStats(_perfStart, 'dbView.gridColumnsList');
}
return viewDetails.find(x => x.fk_column_id === ncColumnId)?.id;
}
@ -1107,29 +1192,34 @@ export default async (
async function nocoLinkProcessing(projName, table, record, _field) {
const rec = record.fields;
const refRowIdList: any = Object.values(rec);
const referenceColumnName = Object.keys(rec)[0];
for (const [key, value] of Object.entries(rec)) {
const refRowIdList: any = value;
const referenceColumnName = key;
if (refRowIdList.length) {
for (let i = 0; i < refRowIdList[0].length; i++) {
for (let i = 0; i < refRowIdList.length; i++) {
logDetailed(
`NC API: dbTableRow.nestedAdd ${record.id}/mm/${referenceColumnName}/${refRowIdList[0][i]}`
);
const _perfStart = recordPerfStart();
await api.dbTableRow.nestedAdd(
'noco',
projName,
table.id,
`${record.id}`,
'mm', // fix me
'mm',
encodeURIComponent(referenceColumnName),
`${refRowIdList[0][i]}`
`${refRowIdList[i]}`
);
recordPerfStats(_perfStart, 'dbTableRow.nestedAdd');
}
}
}
}
async function nocoBaseDataProcessing(sDB, table, record) {
async function nocoBaseDataProcessing_v2(sDB, table, record) {
const recordHash = hash(record);
const rec = record.fields;
@ -1149,22 +1239,35 @@ export default async (
// retrieve datatype
const dt = table.columns.find(x => x.title === key)?.uidt;
switch (dt) {
// https://www.npmjs.com/package/validator
// default value: digits_after_decimal: [2]
// if currency, set decimal place to 2
//
if (dt === UITypes.Currency) rec[key] = (+value).toFixed(2);
case UITypes.Currency:
rec[key] = (+value).toFixed(2);
break;
// we will pick up LTAR once all table data's are in place
if (dt === UITypes.LinkToAnotherRecord) {
delete rec[key];
case UITypes.LinkToAnotherRecord:
if (storeLinks) {
if (ncLinkDataStore[table.title][record.id] === undefined)
ncLinkDataStore[table.title][record.id] = {
id: record.id,
fields: {}
};
ncLinkDataStore[table.title][record.id]['fields'][key] = value;
}
delete rec[key];
break;
// these will be automatically populated depending on schema configuration
if (dt === UITypes.Lookup) delete rec[key];
if (dt === UITypes.Rollup) delete rec[key];
case UITypes.Lookup:
case UITypes.Rollup:
delete rec[key];
break;
if (dt === UITypes.Collaborator) {
case UITypes.Collaborator:
// in case of multi-collaborator, this will be an array
if (Array.isArray(value)) {
let collaborators = '';
@ -1173,26 +1276,47 @@ export default async (
rec[key] = collaborators;
}
} else rec[key] = `${value?.name} <${value?.email}>`;
}
break;
if (dt === UITypes.Barcode) rec[key] = value.text;
if (dt === UITypes.Button) rec[key] = `${value?.label} <${value?.url}>`;
case UITypes.Barcode:
rec[key] = value.text;
break;
if (
dt === UITypes.DateTime ||
dt === UITypes.CreateTime ||
dt === UITypes.LastModifiedTime
) {
const atDateField = dayjs(value);
rec[key] = atDateField.utc().format('YYYY-MM-DD HH:mm');
case UITypes.Button:
rec[key] = `${value?.label} <${value?.url}>`;
break;
case UITypes.DateTime:
case UITypes.CreateTime:
case UITypes.LastModifiedTime:
rec[key] = dayjs(value)
.utc()
.format('YYYY-MM-DD HH:mm');
break;
case UITypes.Date:
if (/\d{5,}/.test(value)) {
// skip
rec[key] = null;
logBasic(`:: Invalid date ${value}`);
} else {
rec[key] = dayjs(value)
.utc()
.format('YYYY-MM-DD');
}
break;
if (dt === UITypes.SingleSelect) rec[key] = value.replace(/,/g, '.');
case UITypes.SingleSelect:
rec[key] = value.replace(/,/g, '.');
break;
if (dt === UITypes.MultiSelect)
case UITypes.MultiSelect:
rec[key] = value.map(v => `${v.replace(/,/g, '.')}`).join(',');
break;
if (dt === UITypes.Attachment) {
case UITypes.Attachment:
if (skipAttachments) rec[key] = null;
else {
const tempArr = [];
for (const v of value) {
const binaryImage = await axios
@ -1238,23 +1362,24 @@ export default async (
}
rec[key] = JSON.stringify(tempArr);
}
break;
default:
break;
}
}
// insert airtable record ID explicitly into each records
rec[ncSysFields.id] = record.id;
rec[ncSysFields.hash] = recordHash;
// bulk Insert
logDetailed(`NC API: dbTableRow.bulkCreate ${table.title} [${rec}]`);
await api.dbTableRow.bulkCreate(
'nc',
sDB.projectName,
table.id, // encodeURIComponent(table.title),
[rec]
);
return rec;
}
async function nocoReadData(sDB, table, callback) {
async function nocoReadData(sDB, table) {
ncLinkDataStore[table.title] = {};
const insertJobs: Promise<any>[] = [];
return new Promise((resolve, reject) => {
base(table.title)
.select({
@ -1270,20 +1395,46 @@ export default async (
`:: ${table.title} : ${recordCnt + 1} ~ ${(recordCnt += 100)}`
);
await Promise.all(
records.map(record => callback(sDB, table, record))
// await Promise.all(
// records.map(record => _callback(sDB, table, record))
// );
const ncRecords = [];
for (let i = 0; i < records.length; i++) {
const r = await nocoBaseDataProcessing_v2(sDB, table, records[i]);
ncRecords.push(r);
}
// wait for previous job's to finish
await Promise.all(insertJobs);
const _perfStart = recordPerfStart();
insertJobs.push(
api.dbTableRow.bulkCreate(
'nc',
sDB.projectName,
table.id, // encodeURIComponent(table.title),
ncRecords
)
);
recordPerfStats(_perfStart, 'dbTableRow.bulkCreate');
// To fetch the next page of records, call `fetchNextPage`.
// If there are more records, `page` will get called again.
// If there are no more records, `done` will get called.
// logBasic(
// `:: ${Date.now()} Awaiting response from Airtable Data API ...`
// );
fetchNextPage();
},
function done(err) {
async function done(err) {
if (err) {
console.error(err);
reject(err);
}
// wait for all jobs to be completed
await Promise.all(insertJobs);
resolve(null);
}
);
@ -1296,7 +1447,7 @@ export default async (
.select({
pageSize: 100,
// maxRecords: 100,
fields: [fields]
fields: fields
})
.eachPage(
async function page(records, fetchNextPage) {
@ -1339,19 +1490,23 @@ export default async (
async function nocoCreateProject(projName) {
// create empty project (XC-DB)
logDetailed(`Create Project: ${projName}`);
const _perfStart = recordPerfStart();
ncCreatedProjectSchema = await api.project.create({
title: projName
});
recordPerfStats(_perfStart, 'project.create');
}
async function nocoGetProject(projId) {
// create empty project (XC-DB)
logDetailed(`Getting project meta: ${projId}`);
const _perfStart = recordPerfStart();
ncCreatedProjectSchema = await api.project.read(projId);
recordPerfStats(_perfStart, 'project.read');
}
async function nocoConfigureGalleryView(sDB, aTblSchema) {
if (!sDB.syncViews) return;
if (!sDB.options.syncViews) return;
for (let idx = 0; idx < aTblSchema.length; idx++) {
const tblId = (await nc_getTableSchema(aTblSchema[idx].name)).id;
const galleryViews = aTblSchema[idx].views.filter(
@ -1377,7 +1532,10 @@ export default async (
);
logDetailed(`NC API dbView.galleryCreate :: ${viewName}`);
const _perfStart = recordPerfStart();
await api.dbView.galleryCreate(tblId, { title: viewName });
recordPerfStats(_perfStart, 'dbView.galleryCreate');
await updateNcTblSchemaById(tblId);
// syncLog(`[${idx+1}/${aTblSchema.length}][Gallery View][${i+1}/${galleryViews.length}] Create ${viewName}`)
@ -1387,7 +1545,7 @@ export default async (
}
async function nocoConfigureFormView(sDB, aTblSchema) {
if (!sDB.syncViews) return;
if (!sDB.options.syncViews) return;
for (let idx = 0; idx < aTblSchema.length; idx++) {
const tblId = sMap.getNcIdFromAtId(aTblSchema[idx].id);
const formViews = aTblSchema[idx].views.filter(x => x.type === 'form');
@ -1435,7 +1593,10 @@ export default async (
};
logDetailed(`NC API dbView.formCreate :: ${viewName}`);
const _perfStart = recordPerfStart();
const f = await api.dbView.formCreate(tblId, formData);
recordPerfStats(_perfStart, 'dbView.formCreate');
logDetailed(
`[${idx + 1}/${aTblSchema.length}][Form View][${i + 1}/${
formViews.length
@ -1462,11 +1623,11 @@ export default async (
const gridViews = aTblSchema[idx].views.filter(x => x.type === 'grid');
let viewCnt = idx;
if (syncDB.syncViews)
if (syncDB.options.syncViews)
viewCnt = rtc.view.grid + rtc.view.gallery + rtc.view.form;
rtc.view.grid += gridViews.length;
for (let i = 0; i < (sDB.syncViews ? gridViews.length : 1); i++) {
for (let i = 0; i < (sDB.options.syncViews ? gridViews.length : 1); i++) {
logDetailed(` Axios fetch view-data`);
// fetch viewData JSON
const vData = await getViewData(gridViews[i].id);
@ -1475,7 +1636,10 @@ export default async (
const viewName = aTblSchema[idx].views.find(
x => x.id === gridViews[i].id
)?.name;
const _perfStart = recordPerfStart();
const viewList: any = await api.dbView.list(tblId);
recordPerfStats(_perfStart, 'dbView.list');
let ncViewId = viewList?.list?.find(x => x.tn === viewName)?.id;
logBasic(
@ -1487,9 +1651,12 @@ export default async (
// create view (default already created)
if (i > 0) {
logDetailed(`NC API dbView.gridCreate :: ${viewName}`);
const _perfStart = recordPerfStart();
const viewCreated = await api.dbView.gridCreate(tblId, {
title: viewName
});
recordPerfStats(_perfStart, 'dbView.gridCreate');
await updateNcTblSchemaById(tblId);
await sMap.addToMappingTbl(
gridViews[i].id,
@ -1544,6 +1711,7 @@ export default async (
const userList = aTblSchema.appBlanket.userInfoById;
const totalUsers = Object.keys(userList).length;
let cnt = 0;
const insertJobs: Promise<any>[] = [];
for (const [, value] of Object.entries(
userList as { [key: string]: any }
@ -1551,11 +1719,16 @@ export default async (
logDetailed(
`[${++cnt}/${totalUsers}] NC API auth.projectUserAdd :: ${value.email}`
);
await api.auth.projectUserAdd(ncCreatedProjectSchema.id, {
const _perfStart = recordPerfStart();
insertJobs.push(
api.auth.projectUserAdd(ncCreatedProjectSchema.id, {
email: value.email,
roles: userRoles[value.permissionLevel]
});
})
);
recordPerfStats(_perfStart, 'auth.projectUserAdd');
}
await Promise.all(insertJobs);
}
function updateNcTblSchema(tblSchema) {
@ -1571,7 +1744,10 @@ export default async (
}
async function updateNcTblSchemaById(tblId) {
const _perfStart = recordPerfStart();
const ncTbl = await api.dbTable.read(tblId);
recordPerfStats(_perfStart, 'dbTable.read');
updateNcTblSchema(ncTbl);
}
@ -1665,23 +1841,61 @@ export default async (
return accumulator + object.nc.rollup;
}, 0);
logDetailed(`Quick Summary:`);
logDetailed(`:: Total Tables: ${aTblSchema.length}`);
logDetailed(`:: Total Columns: ${columnSum}`);
logDetailed(`:: Links: ${linkSum}`);
logDetailed(`:: Lookup: ${lookupSum}`);
logDetailed(`:: Rollup: ${rollupSum}`);
logDetailed(`:: Total Filters: ${rtc.filter}`);
logDetailed(`:: Total Sort: ${rtc.sort}`);
logDetailed(`:: Total Views: ${rtc.view.total}`);
logDetailed(`:: Grid: ${rtc.view.grid}`);
logDetailed(`:: Gallery: ${rtc.view.gallery}`);
logDetailed(`:: Form: ${rtc.view.form}`);
logBasic(`Quick Summary:`);
logBasic(`:: Total Tables: ${aTblSchema.length}`);
logBasic(`:: Total Columns: ${columnSum}`);
logBasic(`:: Links: ${linkSum}`);
logBasic(`:: Lookup: ${lookupSum}`);
logBasic(`:: Rollup: ${rollupSum}`);
logBasic(`:: Total Filters: ${rtc.filter}`);
logBasic(`:: Total Sort: ${rtc.sort}`);
logBasic(`:: Total Views: ${rtc.view.total}`);
logBasic(`:: Grid: ${rtc.view.grid}`);
logBasic(`:: Gallery: ${rtc.view.gallery}`);
logBasic(`:: Form: ${rtc.view.form}`);
const duration = Date.now() - start;
logDetailed(`:: Migration time: ${duration}`);
logDetailed(`:: Axios fetch count: ${rtc.fetchAt.count}`);
logDetailed(`:: Axios fetch time: ${rtc.fetchAt.time}`);
logBasic(`:: Migration time: ${duration}`);
logBasic(`:: Axios fetch count: ${rtc.fetchAt.count}`);
logBasic(`:: Axios fetch time: ${rtc.fetchAt.time}`);
if (debugMode) {
jsonfile.writeFileSync('stats.json', perfStats, { spaces: 2 });
const perflog = [];
for (let i = 0; i < perfStats.length; i++) {
perflog.push(`${perfStats[i].e}, ${perfStats[i].d}`);
}
jsonfile.writeFileSync('stats.csv', perflog, { spaces: 2 });
jsonfile.writeFileSync('skip.txt', rtc.migrationSkipLog.log, {
spaces: 2
});
}
Tele.event({
event: 'a:airtable-import:success',
data: {
stats: {
migrationTime: duration,
totalTables: aTblSchema.length,
totalColumns: columnSum,
links: linkSum,
lookup: lookupSum,
rollup: rollupSum,
totalFilters: rtc.filter,
totalSort: rtc.sort,
view: {
total: rtc.view.total,
grid: rtc.view.grid,
gallery: rtc.view.gallery,
form: rtc.view.form
},
axios: {
count: rtc.fetchAt.count,
time: rtc.fetchAt.time
}
}
}
});
}
//////////////////////////////
@ -1776,9 +1990,12 @@ export default async (
// insert filters
for (let i = 0; i < ncFilters.length; i++) {
const _perfStart = recordPerfStart();
await api.dbTableFilter.create(viewId, {
...ncFilters[i]
});
recordPerfStats(_perfStart, 'dbTableFilter.create');
rtc.filter++;
}
}
@ -1788,11 +2005,14 @@ export default async (
for (let i = 0; i < s.sortSet.length; i++) {
const columnId = (await nc_getColumnSchema(s.sortSet[i].columnId))?.id;
if (columnId)
if (columnId) {
const _perfStart = recordPerfStart();
await api.dbTableSort.create(viewId, {
fk_column_id: columnId,
direction: s.sortSet[i].ascending ? 'asc' : 'dsc'
});
recordPerfStats(_perfStart, 'dbTableSort.create');
}
rtc.sort++;
}
}
@ -1807,23 +2027,41 @@ export default async (
const ncTbl = await nc_getTableSchema(tblName);
// retrieve view ID
const viewId = ncTbl.views.find(x => x.title === viewName).id;
let viewDetails;
const _perfStart = recordPerfStart();
if (viewType === 'form') {
viewDetails = (await api.dbView.formRead(viewId)).columns;
recordPerfStats(_perfStart, 'dbView.formRead');
} else if (viewType === 'gallery') {
viewDetails = (await api.dbView.galleryRead(viewId)).columns;
recordPerfStats(_perfStart, 'dbView.galleryRead');
} else {
viewDetails = await api.dbView.gridColumnsList(viewId);
recordPerfStats(_perfStart, 'dbView.gridColumnsList');
}
// nc-specific columns; default hide.
for (let j = 0; j < hiddenColumns.length; j++) {
const ncColumnId = ncTbl.columns.find(x => x.title === hiddenColumns[j])
.id;
const ncViewColumnId = await nc_getViewColumnId(
viewId,
viewType,
ncColumnId
);
const ncViewColumnId = viewDetails.find(
x => x.fk_column_id === ncColumnId
)?.id;
// const ncViewColumnId = await nc_getViewColumnId(
// viewId,
// viewType,
// ncColumnId
// );
if (ncViewColumnId === undefined) continue;
// first two positions held by record id & record hash
const _perfStart = recordPerfStart();
await api.dbViewColumn.update(viewId, ncViewColumnId, {
show: false,
order: j + 1 + c.length
});
recordPerfStats(_perfStart, 'dbViewColumn.update');
}
// rest of the columns from airtable- retain order & visibility property
@ -1845,10 +2083,14 @@ export default async (
if (x?.title) formData[`label`] = x.title;
if (x?.required) formData[`required`] = x.required;
if (x?.description) formData[`description`] = x.description;
const _perfStart = recordPerfStart();
await api.dbView.formColumnUpdate(ncViewColumnId, formData);
recordPerfStats(_perfStart, 'dbView.formColumnUpdate');
}
}
const _perfStart = recordPerfStart();
await api.dbViewColumn.update(viewId, ncViewColumnId, configData);
recordPerfStats(_perfStart, 'dbViewColumn.update');
}
}
@ -1897,21 +2139,26 @@ export default async (
await nocoCreateLinkToAnotherRecord(aTblSchema);
logDetailed('Migrating LTAR columns completed');
if (syncDB.options.syncLookup) {
logDetailed(`Configuring Lookup`);
// add look-ups
await nocoCreateLookups(aTblSchema);
logDetailed('Migrating Lookup columns completed');
}
if (syncDB.options.syncRollup) {
logDetailed('Configuring Rollup');
// add roll-ups
await nocoCreateRollup(aTblSchema);
logDetailed('Migrating Rollup columns completed');
if (syncDB.options.syncLookup) {
logDetailed('Migrating Lookup form Rollup columns');
// lookups for rollup
await nocoLookupForRollup();
logDetailed('Migrating Lookup form Rollup columns completed');
}
}
logDetailed('Configuring Primary value column');
// configure primary values
await nocoSetPrimary(aTblSchema);
@ -1932,33 +2179,56 @@ export default async (
await nocoConfigureGalleryView(syncDB, aTblSchema);
logDetailed('Syncing views completed');
if (process_aTblData) {
if (syncDB.options.syncData) {
try {
// await nc_DumpTableSchema();
const _perfStart = recordPerfStart();
const ncTblList = await api.dbTable.list(ncCreatedProjectSchema.id);
recordPerfStats(_perfStart, 'dbTable.list');
logBasic('Reading Records...');
for (let i = 0; i < ncTblList.list.length; i++) {
const _perfStart = recordPerfStart();
const ncTbl = await api.dbTable.read(ncTblList.list[i].id);
recordPerfStats(_perfStart, 'dbTable.read');
// not a migrated table, skip
if (undefined === aTblSchema.find(x => x.name === ncTbl.title))
continue;
recordCnt = 0;
await nocoReadData(syncDB, ncTbl, async (sDB, table, record) => {
await nocoBaseDataProcessing(sDB, table, record);
});
await nocoReadData(syncDB, ncTbl);
logDetailed(`Data inserted from ${ncTbl.title}`);
}
logBasic('Configuring Record Links...');
// Configure link @ Data row's
if (storeLinks) {
// const insertJobs: Promise<any>[] = [];
for (const [pTitle, v] of Object.entries(ncLinkDataStore)) {
logBasic(`:: ${pTitle}`);
for (const [, record] of Object.entries(v)) {
const tbl = ncTblList.list.find(a => a.title === pTitle);
await nocoLinkProcessing(syncDB.projectName, tbl, record, 0);
// insertJobs.push(
// nocoLinkProcessing(syncDB.projectName, tbl, record, 0)
// );
}
}
// await Promise.all(insertJobs);
// await nocoLinkProcessing(syncDB.projectName, 0, 0, 0);
} else {
// create link groups (table: link fields)
const tblLinkGroup = {};
for (let idx = 0; idx < ncLinkMappingTable.length; idx++) {
const x = ncLinkMappingTable[idx];
const ncTbl = await nc_getTableSchema(
aTbl_getTableName(x.aTbl.tblId).tn
);
if (tblLinkGroup[x.aTbl.tblId] === undefined)
tblLinkGroup[x.aTbl.tblId] = [x.aTbl.name];
else tblLinkGroup[x.aTbl.tblId].push(x.aTbl.name);
}
for (const [k, v] of Object.entries(tblLinkGroup)) {
const ncTbl = await nc_getTableSchema(aTbl_getTableName(k).tn);
// not a migrated table, skip
if (undefined === aTblSchema.find(x => x.name === ncTbl.title))
@ -1971,9 +2241,9 @@ export default async (
async (projName, table, record, _field) => {
await nocoLinkProcessing(projName, table, record, _field);
},
x.aTbl.name
v
);
logDetailed(`Linked data to ${ncTbl.title}`);
}
}
} catch (error) {
logDetailed(
@ -1987,6 +2257,10 @@ export default async (
}
} catch (e) {
if (e.response?.data?.msg) {
Tele.event({
event: 'a:airtable-import:error',
data: { error: e.response.data.msg }
});
throw new Error(e.response.data.msg);
}
throw e;
@ -2015,5 +2289,12 @@ export interface AirtableSyncConfig {
projectId?: string;
apiKey: string;
shareId: string;
options: {
syncViews: boolean;
syncData: boolean;
syncRollup: boolean;
syncLookup: boolean;
syncFormula: boolean;
syncAttachment: boolean;
};
}

43
packages/nocodb/src/lib/noco/meta/api/sync/importApis.ts

@ -9,6 +9,7 @@ import SyncSource from '../../../../noco-models/SyncSource';
import Noco from '../../../Noco';
import * as jwt from 'jsonwebtoken';
const AIRTABLE_IMPORT_JOB = 'AIRTABLE_IMPORT_JOB';
const AIRTABLE_PROGRESS_JOB = 'AIRTABLE_PROGRESS_JOB';
enum SyncStatus {
PROGRESS = 'PROGRESS',
@ -17,24 +18,45 @@ enum SyncStatus {
}
export default (router: Router, clients: { [id: string]: Socket }) => {
// add importer job handler and progress notification job handler
NocoJobs.jobsMgr.addJobWorker(AIRTABLE_IMPORT_JOB, job);
NocoJobs.jobsMgr.addProgressCbk(AIRTABLE_IMPORT_JOB, (payload, progress) => {
NocoJobs.jobsMgr.addJobWorker(
AIRTABLE_PROGRESS_JOB,
({ payload, progress }) => {
clients?.[payload?.id]?.emit('progress', {
msg: progress?.msg,
level: progress?.level,
status: SyncStatus.PROGRESS
status: progress?.status
});
}
);
NocoJobs.jobsMgr.addProgressCbk(AIRTABLE_IMPORT_JOB, (payload, progress) => {
NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
payload,
progress: {
msg: progress?.msg,
level: progress?.level,
status: progress?.status
}
});
});
NocoJobs.jobsMgr.addSuccessCbk(AIRTABLE_IMPORT_JOB, payload => {
clients?.[payload?.id]?.emit('progress', {
NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
payload,
progress: {
msg: 'Complete!',
status: SyncStatus.COMPLETED
}
});
});
NocoJobs.jobsMgr.addFailureCbk(AIRTABLE_IMPORT_JOB, (payload, error: any) => {
clients?.[payload?.id]?.emit('progress', {
NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
payload,
progress: {
msg: error?.message || 'Failed due to some internal error',
status: SyncStatus.FAILED
}
});
});
@ -67,12 +89,23 @@ export default (router: Router, clients: { [id: string]: Socket }) => {
Noco.getConfig().auth.jwt.options
);
// Treat default baseUrl as siteUrl from req object
let baseURL = (req as any).ncSiteUrl;
// if environment value avail use it
// or if it's docker construct using `PORT`
if (process.env.NC_BASEURL_INTERNAL) {
baseURL = process.env.NC_BASEURL_INTERNAL;
} else if (process.env.NC_DOCKER) {
baseURL = `http://localhost:${process.env.PORT || 8080}`;
}
NocoJobs.jobsMgr.add<AirtableSyncConfig>(AIRTABLE_IMPORT_JOB, {
id: req.query.id,
...(syncSource?.details || {}),
projectId: syncSource.project_id,
authToken: token,
baseURL: (req as any).ncSiteUrl
baseURL
});
res.json({});
})

2
scripts/cypress/integration/common/3a_filter_sort_fields_operations.js

@ -49,6 +49,8 @@ export const genTest = (apiType, dbType) => {
// verify
mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000)
mainPage
.getCell("Country", 10)
.contains("Test Country")

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

@ -517,6 +517,9 @@ export const genTest = (apiType, dbType) => {
// clean up newly added rows into Country table operations
// this auto verifies successfull addition of rows to table as well
mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000)
cy.get(".nc-grid-row").should("have.length", 13);
mainPage
.getRow(10)

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

@ -208,6 +208,9 @@ export const genTest = (apiType, dbType) => {
// clean up newly added rows into Country table operations
// this auto verifies successfull addition of rows to table as well
mainPage.getPagination(25).click();
// kludge: flicker on load
cy.wait(3000)
cy.get(".nc-grid-row").should("have.length", 1);
mainPage
.getRow(1)

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

@ -396,6 +396,9 @@ export const genTest = (apiType, dbType) => {
// delete row
mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000)
// wait for page rendering to complete
cy.get(".nc-grid-row").should("have.length", 10);
mainPage

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

@ -390,6 +390,9 @@ export const genTest = (apiType, dbType) => {
// delete row
mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000)
// wait for page rendering to complete
cy.get(".nc-grid-row").should("have.length", 10);
mainPage

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

@ -17,6 +17,9 @@ export const genTest = (apiType, dbType) => {
// clean up newly added rows into Country table operations
// this auto verifies successfull addition of rows to table as well
mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000)
// wait for page rendering to complete
cy.get(".nc-grid-row").should("have.length", 10);
mainPage

2
scripts/cypress/support/commands.js

@ -180,6 +180,8 @@ Cypress.Commands.add("openTableTab", (tn, rc) => {
cy.get(".nc-grid-row").should("have.length", rc);
}
// kludge: add delay to skip flicker
cy.wait(3000)
cy.snip(`GridView_${tn}`);
});

2
scripts/sdk/swagger.json

@ -2874,7 +2874,7 @@
}
}
},
"/api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/find-one": {
"/api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/groupby": {
"parameters": [
{
"schema": {

Loading…
Cancel
Save