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. 18
      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. 669
      packages/nocodb/src/lib/noco/meta/api/sync/helpers/job.ts
  71. 55
      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": [ "contributions": [
"code" "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, "contributorsPerLine": 7,

24
.github/workflows/ci-cd.yml

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

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

@ -14,6 +14,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Pushes swagger file to src - name: Pushes swagger file to src
uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658 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 - uses: actions/setup-node@v2
with: with:
node-version: 14 node-version: 16
- name: Build blogs - name: Build blogs
run: | run: |
cd packages/noco-blog cd packages/noco-blog

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

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

12
.github/workflows/publish-docs.yml

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

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

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

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

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

2
.github/workflows/release-docker.yml

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

29
.github/workflows/release-draft.yml

@ -26,23 +26,36 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - name: Create tag
uses: actions/github-script@v3 uses: actions/github-script@v3
with: 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: | script: |
github.git.createRef({ github.git.createRef({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
ref: "refs/tags/${{ github.event.inputs.tag || inputs.tag }}", 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 - uses: actions/setup-node@v2
with: 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 }}" - 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_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}"
DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}" 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 # Change nocodb-sdk back to local path
update-sdk-path: update-sdk-path:
needs: release-docker needs: publish-docs
uses: ./.github/workflows/update-sdk-path.yml uses: ./.github/workflows/update-sdk-path.yml
# Sync changes to develop # Sync changes to develop

2
.github/workflows/release-npm.yml

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

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

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

10
README.md

@ -230,6 +230,7 @@ Access Dashboard using : [http://localhost:8080/dashboard](http://localhost:8080
* [Environment variables](#environment-variables) * [Environment variables](#environment-variables)
- [Development Setup](#development-setup) - [Development Setup](#development-setup)
* [Cloning the Project](#cloning-the-project) * [Cloning the Project](#cloning-the-project)
* [Build SDK](#build-sdk)
* [Running Backend locally](#running-backend-locally) * [Running Backend locally](#running-backend-locally)
* [Running Frontend locally](#running-frontend-locally) * [Running Frontend locally](#running-frontend-locally)
* [Running Cypress tests locally](#running-cypress-tests-locally) * [Running Cypress tests locally](#running-cypress-tests-locally)
@ -291,6 +292,14 @@ git clone https://github.com/nocodb/nocodb
cd nocodb cd nocodb
``` ```
## Build SDK
```shell
cd packages/nocodb-sdk
npm install
npm run build
```
## Running Backend locally ## Running Backend locally
```shell ```shell
@ -463,6 +472,7 @@ Our mission is to provide the most powerful no-code interface for databases whic
</tr> </tr>
<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://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> </tr>
</table> </table>

2
Run.md

@ -4,7 +4,7 @@
- Clone `nocodb/nocodb` GitHub repo and checkout to `feat/v2` branch - Clone `nocodb/nocodb` GitHub repo and checkout to `feat/v2` branch
```sh ```sh
git clone https://github.com/nocodb/nc git clone https://github.com/nocodb/nocodb
cd 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", "updated:xc-migrator": "lerna run publish --scope xc-migrator && lerna run xc && lerna publish && npm install -f xc-cli",
"doc": "lerna run doc", "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: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": { "dependencies": {
"mysql2": "^2.3.3", "mysql2": "^2.3.3",

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

@ -37,6 +37,10 @@ body {
Apply Vazirmatn for rtl Apply Vazirmatn for rtl
*/ */
.rtl .v-application .v-application--wrap * { .rtl .v-application *:not(.material-icons) {
font-family: Vazirmatn !important; 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> <template>
<div> <div>
<v-menu offset-y> <v-menu
offset-y
transition="slide-y-transition"
>
<template #activator="{ on }"> <template #activator="{ on }">
<v-btn <v-btn
v-show="isDashboard && _isUIAllowed('previewAs')" v-show="isDashboard && _isUIAllowed('previewAs')"

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

@ -1,14 +1,22 @@
<template> <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-card class="nc-import-card h-100">
<v-toolbar class="elevation-0 align-center" height="68"> <v-toolbar class="elevation-0 align-center" height="68">
<h3 class="mt-2"> <h3 class="mt-2">
{{ $t('title.importFromAirtable') }} {{ $t('title.importFromAirtable') }}
</h3> </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> </div>
<v-spacer /> <v-spacer />
<v-icon color="warning" class="" @click="airtableModal = false">
mdi-close
</v-icon>
</v-toolbar> </v-toolbar>
<v-divider /> <v-divider />
@ -21,11 +29,18 @@
> >
<template v-if="step === 1"> <template v-if="step === 1">
<div class="d-flex flex-column justify-center align-center pt-2 pb-6"> <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 Credentials
</span> </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> </div>
<v-form v-model="valid"> <v-form v-model="valid">
@ -55,7 +70,63 @@
:rules="[(v) => !!v || 'Shared Base ID / URL is required']" :rules="[(v) => !!v || 'Shared Base ID / URL is required']"
/> />
</div> </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-btn
v-t="['c:sync-airtable:save-and-sync']" v-t="['c:sync-airtable:save-and-sync']"
class="nc-btn-airtable-import" class="nc-btn-airtable-import"
@ -114,9 +185,14 @@
</div> </div>
</template> </template>
<div class="text-center pa-4 pb-0"> <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> <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> </div>
</v-card> </v-card>
</div> </div>
@ -134,12 +210,30 @@ export default {
value: Boolean value: Boolean
}, },
data: () => ({ data: () => ({
advanceOptionsPanel: false,
isPasswordVisible: false, isPasswordVisible: false,
valid: false, valid: false,
socket: null, socket: null,
step: 1, step: 1,
progress: [], 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: '' syncSourceUrlOrId: ''
}), }),
computed: { computed: {
@ -221,7 +315,7 @@ export default {
const { data: { list: srcs } } = await this.$axios.get(`/api/v1/db/meta/projects/${this.projectId}/syncs`) const { data: { list: srcs } } = await this.$axios.get(`/api/v1/db/meta/projects/${this.projectId}/syncs`)
if (srcs && srcs[0]) { if (srcs && srcs[0]) {
srcs[0].details = srcs[0].details || {} srcs[0].details = srcs[0].details || {}
this.syncSource = srcs[0] this.syncSource = this.migrateSync(srcs[0])
this.syncSourceUrlOrId = srcs[0].details.shareId this.syncSourceUrlOrId = srcs[0].details.shareId
} else { } else {
this.syncSource = { this.syncSource = {
@ -230,11 +324,16 @@ export default {
syncInterval: '15mins', syncInterval: '15mins',
syncDirection: 'Airtable to NocoDB', syncDirection: 'Airtable to NocoDB',
syncRetryCount: 1, syncRetryCount: 1,
syncViews: false,
apiKey: '', apiKey: '',
shareId: '' shareId: '',
options: {
syncViews: false,
syncData: true,
syncRollup: false,
syncLookup: true,
syncFormula: false,
syncAttachment: true
}
} }
} }
} }
@ -252,8 +351,23 @@ export default {
} }
}, },
enableTurbo() { 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) 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" :key="col.column_name"
@click="searchField = col.title" @click="searchField = col.title"
> >
<v-icon color="grey darken-4" small class="mr-1">
{{ col.icon }}
</v-icon>
<span class="caption">{{ col.title }}</span> <span class="caption">{{ col.title }}</span>
</v-list-item> </v-list-item>
</v-list> </v-list>
@ -333,8 +336,13 @@
:style="{ height: isForm ? '100%' : 'calc(100% - 36px)' }" :style="{ height: isForm ? '100%' : 'calc(100% - 36px)' }"
style="overflow: auto; width: 100%" 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 <template
v-if="selectedViewId && selectedView" v-else-if="selectedViewId && selectedView"
> >
<!-- <v-skeleton-loader v-if="!dataLoaded && loadingData || !meta" type="table" />--> <!-- <v-skeleton-loader v-if="!dataLoaded && loadingData || !meta" type="table" />-->
<template v-if="selectedView.type === viewTypes.GRID"> <template v-if="selectedView.type === viewTypes.GRID">
@ -504,7 +512,7 @@
@rerender="viewKey++" @rerender="viewKey++"
@generateNewViewKey="generateNewViewKey" @generateNewViewKey="generateNewViewKey"
@mapFieldsAndShowFields="mapFieldsAndShowFields" @mapFieldsAndShowFields="mapFieldsAndShowFields"
@loadTableData="loadTableData" @loadTableData="loadTableData(false)"
@showAdditionalFeatOverlay="showAdditionalFeatOverlay($event)" @showAdditionalFeatOverlay="showAdditionalFeatOverlay($event)"
> >
<!-- <v-tooltip bottom> <!-- <v-tooltip bottom>
@ -610,7 +618,7 @@
</v-list-item> </v-list-item>
</template> </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> <v-tooltip bottom>
<template #activator="{ on }"> <template #activator="{ on }">
<v-list-item <v-list-item
@ -787,8 +795,8 @@ export default {
syncDataDebounce: debounce(async function(self) { syncDataDebounce: debounce(async function(self) {
await self.syncData() await self.syncData()
}, 500), }, 500),
loadTableDataDeb: debounce(async function(self) { loadTableDataDeb: debounce(async function(self, ignoreLoader) {
await self.loadTableDataFn() await self.loadTableDataFn(ignoreLoader)
}, 200), }, 200),
viewKey: 0, viewKey: 0,
extraViewParams: {}, extraViewParams: {},
@ -886,7 +894,7 @@ export default {
watch: { watch: {
isActive(n, o) { isActive(n, o) {
if (!o && n) { if (!o && n) {
this.reload() this.reload(true)
} }
}, },
page(p) { page(p) {
@ -975,7 +983,7 @@ export default {
await this.reload() await this.reload()
this.$e('a:table:reload:navbar') this.$e('a:table:reload:navbar')
}, },
async reload() { async reload(ignoreLoader = false) {
this.$store.dispatch('meta/ActLoadMeta', { this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env, env: this.nodes.env,
dbAlias: this.nodes.dbAlias, dbAlias: this.nodes.dbAlias,
@ -985,7 +993,7 @@ export default {
if (this.selectedView && this.selectedView.show_as === 'kanban') { if (this.selectedView && this.selectedView.show_as === 'kanban') {
await this.loadKanbanData() await this.loadKanbanData()
} else { } else {
await this.loadTableData() await this.loadTableData(ignoreLoader)
} }
this.key = Math.random() this.key = Math.random()
}, },
@ -1396,17 +1404,17 @@ export default {
}) })
}, },
clickPagination() { clickPagination() {
this.loadTableData() this.loadTableData(false)
this.$e('a:grid:pagination') this.$e('a:grid:pagination')
}, },
loadTableData() { loadTableData(ignoreLoader = true) {
this.loadTableDataDeb(this) this.loadTableDataDeb(this, ignoreLoader)
}, },
async loadTableDataFn() { async loadTableDataFn(ignoreLoader = true) {
if (this.isForm || !this.selectedView || !this.selectedView.title) { if (this.isForm || !this.selectedView || !this.selectedView.title) {
return return
} }
this.loadingData = true this.loadingData = !ignoreLoader
try { try {
// if (this.api) { // if (this.api) {
// const { list, count } = await this.api.paginatedList(this.queryParams) // 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" /> <time-cell v-else-if="isTime" :value="value" />
<boolean-cell v-else-if="isBoolean" :value="value" read-only /> <boolean-cell v-else-if="isBoolean" :value="value" read-only />
<rating-cell v-else-if="isRating" :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> <span v-else :class="{'long-text-cell' : isTextArea}" :title="title">{{ value }}</span>
</template> </template>
@ -35,10 +36,11 @@ import EditableAttachmentCell from '~/components/project/spreadsheet/components/
import BooleanCell from '~/components/project/spreadsheet/components/cell/BooleanCell' import BooleanCell from '~/components/project/spreadsheet/components/cell/BooleanCell'
import EmailCell from '~/components/project/spreadsheet/components/cell/EmailCell' import EmailCell from '~/components/project/spreadsheet/components/cell/EmailCell'
import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell' import RatingCell from '~/components/project/spreadsheet/components/editableCell/RatingCell'
import CurrencyCell from '@/components/project/spreadsheet/components/cell/CurrencyCell'
export default { export default {
name: 'TableCell', 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], mixins: [cell],
props: ['value', 'dbAlias', 'isLocked', 'selected', 'column'], props: ['value', 'dbAlias', 'isLocked', 'selected', 'column'],
computed: { 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)" @change="saveOrUpdate(filter, i)"
> >
<template #item="{ item }"> <template #item="{ item }">
<span <span :class="`caption font-weight-regular nc-filter-fld-${item.title}`">
:class="`caption font-weight-regular nc-filter-fld-${item.title}`" <v-icon small class="mr-1">
> {{ item.icon }}
</v-icon>
{{ item.title }} {{ item.title }}
</span> </span>
</template> </template>
<template #selection="{item}">
<v-icon small class="mr-1">
{{ item.icon }}
</v-icon> {{ item.title }}
</template>
</v-select> </v-select>
<v-select <v-select
:key="'k' + i" :key="'k' + i"
@ -182,7 +188,7 @@
</template> </template>
<script> <script>
import { UITypes } from '~/components/project/spreadsheet/helpers/uiTypes' import { getUIDTIcon, UITypes } from '~/components/project/spreadsheet/helpers/uiTypes'
export default { export default {
name: 'ColumnFilter', name: 'ColumnFilter',
@ -267,6 +273,11 @@ export default {
] ]
}), }),
computed: { computed: {
columnIcon() {
return this.meta.columns.reduce((iconsObj, c) => {
return { ...iconsObj, [c.title]: getUIDTIcon(c.uidt) }
}, {})
},
columnsById() { columnsById() {
return (this.columns || []).reduce((o, c) => ({ ...o, [c.id]: c }), {}) return (this.columns || []).reduce((o, c) => ({ ...o, [c.id]: c }), {})
}, },
@ -276,7 +287,10 @@ export default {
columns() { columns() {
return ( return (
this.meta && 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() { types() {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -49,6 +49,7 @@
offset-y offset-y
open-on-hover open-on-hover
left left
transition="slide-y-transition"
> >
<template #activator="{on}"> <template #activator="{on}">
<v-icon v-if="!isLocked && !isForm" small v-on="on"> <v-icon v-if="!isLocked && !isForm" small v-on="on">
@ -116,7 +117,13 @@
</v-card> </v-card>
</v-dialog> </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}"> <template #activator="{on}">
<span v-on="on" /> <span v-on="on" />
</template> </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-select
v-model="colMeta.icon" v-model="colMeta.icon"
label="Icon" label="Icon"
:menu-props="{ bottom: true, offsetY: true }"
:items="icons" :items="icons"
dense dense
outlined outlined
@ -29,18 +30,22 @@
</template> </template>
</v-select> </v-select>
</div> </div>
<v-color-picker <color-picker
v-model="colMeta.color" v-model="colMeta.color"
class="mx-auto" row-size="8"
hide-inputs :colors="['#fcb401', '#faa307', '#f48c06', '#e85d04', '#dc2f02', '#d00000', '#9d0208', '#777']"
/> />
</div> </div>
</template> </template>
<script> <script>
import ColorPicker from '@/components/project/spreadsheet/components/ColorPicker.vue'
export default { export default {
name: 'CheckboxOptions', name: 'CheckboxOptions',
components: {
ColorPicker
},
props: ['column', 'meta', 'value'], props: ['column', 'meta', 'value'],
data: () => ({ data: () => ({
colMeta: { colMeta: {
@ -82,7 +87,7 @@ export default {
} }
}, },
created() { created() {
this.colMeta = this.value || { ...this.colMeta } this.colMeta = this.value ? { ...this.value } : { ...this.colMeta }
} }
} }
</script> </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 <span
class="caption primary--text text--lighten-2 font-weight-bold" 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 }} {{ it.text }}
</span> </span>
</v-list-item-content> </v-list-item-content>
@ -70,10 +73,12 @@
<span <span
class="caption text--darken-3 font-weight-bold" class="caption text--darken-3 font-weight-bold"
> >
<v-icon color="grey darken-4" small class="mr-1">
{{ it.icon }}
</v-icon>
{{ it.text }} {{ it.text }}
</span> </span>
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<span class="caption"> <span class="caption">
Column Column
@ -87,6 +92,9 @@
<span <span
class="caption indigo--text text--darken-3 font-weight-bold" 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 }} {{ it.text }}
</span> </span>
</v-list-item-content> </v-list-item-content>
@ -109,7 +117,8 @@
import debounce from 'debounce' import debounce from 'debounce'
import jsep from 'jsep' import jsep from 'jsep'
import { UITypes, jsepCurlyHook } from 'nocodb-sdk' 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 { getWordUntilCaret, insertAtCursor } from '@/helpers'
import NcAutocompleteTree from '@/helpers/NcAutocompleteTree' 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 => ({ ...this.meta.columns.filter(c => !this.column || this.column.id !== c.id).map(c => ({
text: c.title, text: c.title,
type: 'column', type: 'column',
icon: getUIDTIcon(c.uidt),
c c
})), })),
...this.availableBinOps.map(op => ({ ...this.availableBinOps.map(op => ({
@ -490,7 +500,7 @@ export default {
case UITypes.PhoneNumber: case UITypes.PhoneNumber:
case UITypes.Email: case UITypes.Email:
case UITypes.URL: case UITypes.URL:
return 'string' return formulaTypes.STRING
// numeric // numeric
case UITypes.Year: case UITypes.Year:
@ -499,14 +509,14 @@ export default {
case UITypes.Rating: case UITypes.Rating:
case UITypes.Count: case UITypes.Count:
case UITypes.AutoNumber: case UITypes.AutoNumber:
return 'number' return formulaTypes.NUMERIC
// date // date
case UITypes.Date: case UITypes.Date:
case UITypes.DateTime: case UITypes.DateTime:
case UITypes.CreateTime: case UITypes.CreateTime:
case UITypes.LastModifiedTime: case UITypes.LastModifiedTime:
return 'date' return formulaTypes.DATE
// not supported // not supported
case UITypes.ForeignKey: case UITypes.ForeignKey:
@ -527,20 +537,25 @@ export default {
} }
} }
} else if (parsedTree.type === jsep.BINARY_EXP || parsedTree.type === jsep.UNARY_EXP) { } else if (parsedTree.type === jsep.BINARY_EXP || parsedTree.type === jsep.UNARY_EXP) {
return 'number' return formulaTypes.NUMERIC
} else if (parsedTree.type === jsep.LITERAL) { } else if (parsedTree.type === jsep.LITERAL) {
return typeof parsedTree.value return typeof parsedTree.value
} else { } else {
return 'N/A' 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) { appendText(it) {
const text = it.text const text = it.text
const len = this.wordToComplete.length const len = this.wordToComplete.length
if (it.type === 'function') { if (it.type === 'function') {
this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len, 1)) this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len, 1))
} else if (it.type === 'column') { } 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 { } else {
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))
} }
@ -560,6 +575,9 @@ export default {
const parts = query.split(/\W+/) const parts = query.split(/\W+/)
this.wordToComplete = parts.pop() this.wordToComplete = parts.pop()
this.suggestion = this.acTree.complete(this.wordToComplete)?.sort((x, y) => this.sortOrder[x.type] - this.sortOrder[y.type]) 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 this.autocomplete = !!this.suggestion.length
}, },
selectText() { selectText() {

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

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

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

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

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

@ -66,7 +66,7 @@ export default {
return this.uiDatatype === UITypes.Rating return this.uiDatatype === UITypes.Rating
}, },
isCurrency() { 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 { isVirtualCol, filterOutSystemColumns } from 'nocodb-sdk'
import { getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'
export default { export default {
data: () => ({ data: () => ({
@ -91,7 +92,10 @@ export default {
}, []) }, [])
}, },
availableRealColumns() { 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() { allColumns() {

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

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

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

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

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

@ -90,8 +90,11 @@ export default {
}, },
methods: { methods: {
applyDirection() { applyDirection() {
document.body.classList.add(this.isRtlLang() ? "rtl" : "ltr"); const targetDirection = this.isRtlLang() ? 'rtl' : 'ltr'
document.body.style.direction = 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() { isRtlLang() {
return ['fa'].includes(this.language) 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": "مدیریت کاربران", "userMgmt": "مدیریت کاربران",
"apiTokenMgmt": "مدیریت توکنهای API", "apiTokenMgmt": "مدیریت توکنهای API",
"rolesMgmt": "مدیریت نقشها", "rolesMgmt": "مدیریت نقشها",
"projMeta": "فردادههای پروژه", "projMeta": "فرادادههای پروژه",
"metaMgmt": "فرا مدیریت", "metaMgmt": "فرا مدیریت",
"metadata": "فراداده", "metadata": "فراداده",
"exportImportMeta": "ورود و خروج فراداده", "exportImportMeta": "ورود و خروج فراداده",
@ -460,7 +460,7 @@
"loginMsg": "ورود به NocoDB", "loginMsg": "ورود به NocoDB",
"passwordRecovery": { "passwordRecovery": {
"message_1": "لطفا پست الکترونیکی خود را که در هنگام ثبت نام استفاده کردید وارد کنید.", "message_1": "لطفا پست الکترونیکی خود را که در هنگام ثبت نام استفاده کردید وارد کنید.",
"message_2": "ما یک لینک جهت تغییر کلمه عبور به پست الکترونیکی شما ارسال خواهیمکرد.", "message_2": "ما یک لینک جهت تغییر کلمه عبور به پست الکترونیکی شما ارسال خواهیم کرد.",
"success": "لطفا برای تغییر کلمه عبور به پست الکترونیکی خود مراجعه کنید" "success": "لطفا برای تغییر کلمه عبور به پست الکترونیکی خود مراجعه کنید"
}, },
"signUp": { "signUp": {

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

@ -58,7 +58,6 @@
<div style="flex: 1" class="d-flex justify-end"> <div style="flex: 1" class="d-flex justify-end">
<v-toolbar-items class="hidden-sm-and-down nc-topright-menu"> <v-toolbar-items class="hidden-sm-and-down nc-topright-menu">
<important-announcement />
<release-info /> <release-info />
<language class="mr-3" /> <language class="mr-3" />
@ -341,7 +340,6 @@ import Language from '~/components/utils/Language'
import Loader from '~/components/Loader' import Loader from '~/components/Loader'
import PreviewAs from '~/components/PreviewAs' import PreviewAs from '~/components/PreviewAs'
import ShareOrInviteModal from '~/components/auth/ShareOrInviteModal' import ShareOrInviteModal from '~/components/auth/ShareOrInviteModal'
import ImportantAnnouncement from '../components/ImportantAnnouncement.vue'
import weAreHiring from '~/helpers/weAreHiring' import weAreHiring from '~/helpers/weAreHiring'
export default { export default {
@ -353,8 +351,7 @@ export default {
Language, Language,
XBtn, XBtn,
dlgUnexpectedError, dlgUnexpectedError,
settings, settings
ImportantAnnouncement
}, },
data: () => ({ data: () => ({
clickCount: true, 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()) const browserLan = (navigator.languages || [navigator.language || navigator.userLanguage || 'en']).map(v => v.toLowerCase())
return zhLan.some(l => browserLan.includes(l)) return zhLan.some(l => browserLan.includes(l))
}, },
_isRtl() {
const rtl = ['fa']
return rtl.includes(this.$store.state.settings.language)
},
...mapGetters({ ...mapGetters({
_isUIAllowed: 'users/GtrIsUIAllowed', _isUIAllowed: 'users/GtrIsUIAllowed',
projectName: 'project/GtrProjectName', 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", "httpsnippet": "^2.0.0",
"inflection": "^1.12.0", "inflection": "^1.12.0",
"jsep": "^1.3.6", "jsep": "^1.3.6",
"locale-codes": "^1.3.1",
"material-design-icons-iconfont": "^5.0.1", "material-design-icons-iconfont": "^5.0.1",
"monaco-editor": "^0.19.3", "monaco-editor": "^0.19.3",
"monaco-themes": "^0.2.5", "monaco-themes": "^0.2.5",
"nano-assign": "^1.0.1", "nano-assign": "^1.0.1",
"nocodb-sdk": "0.91.0", "nocodb-sdk": "file:../nocodb-sdk",
"nuxt": "^2.14.0", "nuxt": "^2.14.0",
"odometer": "^0.4.8", "odometer": "^0.4.8",
"papaparse": "^5.3.1", "papaparse": "^5.3.1",

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

@ -436,7 +436,16 @@
mdi-github mdi-github
</v-icon> </v-icon>
</v-list-item-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 --> <!-- Star -->
{{ $t("labels.community.starUs1") }} {{ $t("labels.community.starUs1") }}
<v-icon small> <v-icon small>

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

@ -1,8 +1,7 @@
export const state = () => ({ export const state = () => ({
releaseVersion: null, releaseVersion: null,
hiddenRelease: null, hiddenRelease: null,
latestRelease: null, latestRelease: null
hiddenAnnouncement: null
}) })
export const mutations = { export const mutations = {
@ -14,9 +13,6 @@ export const mutations = {
}, },
MutLatestRelease(state, latestRelease) { MutLatestRelease(state, latestRelease) {
state.latestRelease = 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_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_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_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) ### 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. 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`. 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 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`. 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. 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. 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 ## 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. 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 ## UI Access Control
Go to `Project Metadata`, under ``UI Access Control``, you can control the access to each table by roles. 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` ### 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 ### 3. Changes carried outside GUI, identified by NocoDB are listed under `Sync state`
![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`
![image](https://user-images.githubusercontent.com/35857179/161957119-f66f22ad-9d37-45ed-84ca-35c99726078c.png) ![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 #### 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, ...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 * No description
* *
* @tags DB view row * @tags DB view row
* @name GroupBy * @name GroupBy
* @summary Table view row Group by * @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 * @response `200` `any` OK
*/ */
groupBy: ( groupBy: (
@ -2714,7 +2739,7 @@ export class Api<
params: RequestParams = {} params: RequestParams = {}
) => ) =>
this.request<any, any>({ 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', method: 'GET',
query: query, query: query,
format: 'json', format: 'json',

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

@ -84,9 +84,6 @@ export function substituteColumnIdWithAliasInFormula(
c.title === colNameOrId c.title === colNameOrId
); );
pt.name = column?.title || ptRaw?.name || pt?.name; 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') { } else if (pt.type === 'BinaryExpression') {
substituteId(pt.left, ptRaw?.left); substituteId(pt.left, ptRaw?.left);
substituteId(pt.right, ptRaw?.right); substituteId(pt.right, ptRaw?.right);

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

@ -1657,7 +1657,7 @@ export class PgUi {
}; };
break; break;
case 'Percent': case 'Percent':
colProp.dt = 'double'; colProp.dt = 'double precision';
break; break;
case 'Duration': case 'Duration':
colProp.dt = 'int8'; 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", "mysql2": "^2.2.5",
"nanoid": "^3.1.20", "nanoid": "^3.1.20",
"nc-common": "0.0.6", "nc-common": "0.0.6",
"nc-help": "0.2.56", "nc-help": "0.2.59",
"nc-lib-gui": "0.91.0", "nc-lib-gui": "0.91.0",
"nc-plugin": "^0.1.1", "nc-plugin": "^0.1.1",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nocodb-sdk": "0.91.0", "nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"ora": "^4.0.4", "ora": "^4.0.4",

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

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

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

@ -284,6 +284,12 @@ const parseConditionV2 = async (
case 'notnull': case 'notnull':
qb = qb.whereNotNull(customWhereClause || field); qb = qb.whereNotNull(customWhereClause || field);
break; 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, model,
{ ...aliasToColumn, [col.id]: null } { ...aliasToColumn, [col.id]: null }
); );
builder.sql = '(' + builder.sql + ')';
aliasToColumn[col.id] = builder; aliasToColumn[col.id] = builder;
} }
break; break;
@ -611,7 +612,21 @@ export default async function formulaQueryBuilderv2(
return knex.raw( return knex.raw(
`${pt.callee.name}(${pt.arguments `${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}` .join()})${colAlias}`
); );
} else if (pt.type === 'Literal') { } else if (pt.type === 'Literal') {
@ -636,13 +651,16 @@ export default async function formulaQueryBuilderv2(
pt.left.fnName = pt.left.fnName || 'ARITH'; pt.left.fnName = pt.left.fnName || 'ARITH';
pt.right.fnName = pt.right.fnName || 'ARITH'; pt.right.fnName = pt.right.fnName || 'ARITH';
const query = knex.raw( const left = fn(pt.left, null, pt.operator).toQuery();
`${fn(pt.left, null, pt.operator).toQuery()} ${pt.operator} ${fn( const right = fn(pt.right, null, pt.operator).toQuery();
pt.right, let sql = `${left} ${pt.operator} ${right}${colAlias}`;
null,
pt.operator // handle NULL values when calling CONCAT for sqlite3
).toQuery()}${colAlias}` 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) { if (prevBinaryOp && pt.operator !== prevBinaryOp) {
query.wrap('(', ')'); 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) { 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( protected async invokeFailureCbks(
jobName: string, jobName: string,
@ -50,7 +52,7 @@ export default abstract class JobsMgr {
error?: Error error?: Error
) { ) {
await Promise.all( await Promise.all(
this.failureCbks?.[jobName]?.map(cb => cb(payload, error)) this.failureCbks?.[jobName]?.map(cb => cb(payload, error)) || []
); );
} }
protected async invokeProgressCbks( protected async invokeProgressCbks(
@ -59,7 +61,7 @@ export default abstract class JobsMgr {
data?: any data?: any
) { ) {
await Promise.all( 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 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' | 'le'
| 'in' | 'in'
| 'isnot' | 'isnot'
| 'is'; | 'is'
| 'btw'
| 'nbtw';
value?: string; value?: string;
logical_op?: 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 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( await createHmAndBtColumn(
child, child,
@ -450,6 +463,27 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
fk_mm_parent_column_id: childCol.id, fk_mm_parent_column_id: childCol.id,
fk_related_model_id: child.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' }); Tele.emit('evt', { evt_type: 'relation:created' });
@ -649,7 +683,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
formula, formula,
[new_column] [new_column]
); );
await FormulaColumn.update(f.id, { await FormulaColumn.update(c.id, {
formula_raw: new_formula_raw formula_raw: new_formula_raw
}); });
} }
@ -995,6 +1029,29 @@ const deleteHmOrBtRelation = async (
await Column.delete(childColumn.id, ncMeta); 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 }); const router = Router({ mergeParams: true });
router.post( router.post(
'/api/v1/db/meta/tables/:tableId/columns/', '/api/v1/db/meta/tables/:tableId/columns/',

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

File diff suppressed because it is too large Load Diff

55
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 Noco from '../../../Noco';
import * as jwt from 'jsonwebtoken'; import * as jwt from 'jsonwebtoken';
const AIRTABLE_IMPORT_JOB = 'AIRTABLE_IMPORT_JOB'; const AIRTABLE_IMPORT_JOB = 'AIRTABLE_IMPORT_JOB';
const AIRTABLE_PROGRESS_JOB = 'AIRTABLE_PROGRESS_JOB';
enum SyncStatus { enum SyncStatus {
PROGRESS = 'PROGRESS', PROGRESS = 'PROGRESS',
@ -17,24 +18,45 @@ enum SyncStatus {
} }
export default (router: Router, clients: { [id: string]: Socket }) => { 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.addJobWorker(AIRTABLE_IMPORT_JOB, job);
NocoJobs.jobsMgr.addJobWorker(
AIRTABLE_PROGRESS_JOB,
({ payload, progress }) => {
clients?.[payload?.id]?.emit('progress', {
msg: progress?.msg,
level: progress?.level,
status: progress?.status
});
}
);
NocoJobs.jobsMgr.addProgressCbk(AIRTABLE_IMPORT_JOB, (payload, progress) => { NocoJobs.jobsMgr.addProgressCbk(AIRTABLE_IMPORT_JOB, (payload, progress) => {
clients?.[payload?.id]?.emit('progress', { NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
msg: progress?.msg, payload,
level: progress?.level, progress: {
status: SyncStatus.PROGRESS msg: progress?.msg,
level: progress?.level,
status: progress?.status
}
}); });
}); });
NocoJobs.jobsMgr.addSuccessCbk(AIRTABLE_IMPORT_JOB, payload => { NocoJobs.jobsMgr.addSuccessCbk(AIRTABLE_IMPORT_JOB, payload => {
clients?.[payload?.id]?.emit('progress', { NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
msg: 'Complete!', payload,
status: SyncStatus.COMPLETED progress: {
msg: 'Complete!',
status: SyncStatus.COMPLETED
}
}); });
}); });
NocoJobs.jobsMgr.addFailureCbk(AIRTABLE_IMPORT_JOB, (payload, error: any) => { NocoJobs.jobsMgr.addFailureCbk(AIRTABLE_IMPORT_JOB, (payload, error: any) => {
clients?.[payload?.id]?.emit('progress', { NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
msg: error?.message || 'Failed due to some internal error', payload,
status: SyncStatus.FAILED 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 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, { NocoJobs.jobsMgr.add<AirtableSyncConfig>(AIRTABLE_IMPORT_JOB, {
id: req.query.id, id: req.query.id,
...(syncSource?.details || {}), ...(syncSource?.details || {}),
projectId: syncSource.project_id, projectId: syncSource.project_id,
authToken: token, authToken: token,
baseURL: (req as any).ncSiteUrl baseURL
}); });
res.json({}); res.json({});
}) })

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

@ -49,6 +49,8 @@ export const genTest = (apiType, dbType) => {
// verify // verify
mainPage.getPagination(5).click(); mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000)
mainPage mainPage
.getCell("Country", 10) .getCell("Country", 10)
.contains("Test Country") .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 // clean up newly added rows into Country table operations
// this auto verifies successfull addition of rows to table as well // this auto verifies successfull addition of rows to table as well
mainPage.getPagination(5).click(); mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000)
cy.get(".nc-grid-row").should("have.length", 13); cy.get(".nc-grid-row").should("have.length", 13);
mainPage mainPage
.getRow(10) .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 // clean up newly added rows into Country table operations
// this auto verifies successfull addition of rows to table as well // this auto verifies successfull addition of rows to table as well
mainPage.getPagination(25).click(); mainPage.getPagination(25).click();
// kludge: flicker on load
cy.wait(3000)
cy.get(".nc-grid-row").should("have.length", 1); cy.get(".nc-grid-row").should("have.length", 1);
mainPage mainPage
.getRow(1) .getRow(1)

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

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

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

@ -390,6 +390,9 @@ export const genTest = (apiType, dbType) => {
// delete row // delete row
mainPage.getPagination(5).click(); mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000)
// wait for page rendering to complete // wait for page rendering to complete
cy.get(".nc-grid-row").should("have.length", 10); cy.get(".nc-grid-row").should("have.length", 10);
mainPage 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 // clean up newly added rows into Country table operations
// this auto verifies successfull addition of rows to table as well // this auto verifies successfull addition of rows to table as well
mainPage.getPagination(5).click(); mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000)
// wait for page rendering to complete // wait for page rendering to complete
cy.get(".nc-grid-row").should("have.length", 10); cy.get(".nc-grid-row").should("have.length", 10);
mainPage 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); cy.get(".nc-grid-row").should("have.length", rc);
} }
// kludge: add delay to skip flicker
cy.wait(3000)
cy.snip(`GridView_${tn}`); 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": [ "parameters": [
{ {
"schema": { "schema": {

Loading…
Cancel
Save