Browse Source

Merge pull request #9665 from nocodb/develop

pull/9670/head
github-actions[bot] 1 month ago committed by GitHub
parent
commit
9453ee4b28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .github/workflows/bats-test.yml
  2. 406
      .github/workflows/ci-cd.yml
  3. 2
      .github/workflows/jest-unit-test.yml
  4. 60
      .github/workflows/playwright-test-workflow.yml
  5. 5
      .github/workflows/pre-build-for-playwright.yml
  6. 4
      .github/workflows/release-docker.yml
  7. 27
      .github/workflows/release-nightly-dev.yml
  8. 10
      .github/workflows/release-npm.yml
  9. 15
      .github/workflows/release-pr.yml
  10. 11
      .github/workflows/release-secret-cli.yml
  11. 161
      .github/workflows/release-timely-docker.yml
  12. 4
      .github/workflows/sync-to-develop.yml
  13. 11
      .github/workflows/unit-test.yml
  14. 10
      .github/workflows/update-sdk-path.yml
  15. 10
      README.md
  16. 3
      docker-compose/1_Auto_Upstall/tests/expects/install/redis.sh
  17. 3
      docker-compose/1_Auto_Upstall/tests/expects/install/scale.sh
  18. 3
      docker-compose/1_Auto_Upstall/tests/expects/install/watchtower.sh
  19. 7
      packages/nc-gui/components/cell/Checkbox.vue
  20. 2
      packages/nc-gui/components/cell/MultiSelect.vue
  21. 6
      packages/nc-gui/components/cell/Rating.vue
  22. 1
      packages/nc-gui/components/cell/SingleSelect.vue
  23. 4
      packages/nc-gui/components/dlg/QuickImport.vue
  24. 23
      packages/nc-gui/components/feed/Changelog/index.vue
  25. 6
      packages/nc-gui/components/feed/Error.vue
  26. 15
      packages/nc-gui/components/feed/Recents/Card.vue
  27. 4
      packages/nc-gui/components/feed/Recents/index.vue
  28. 14
      packages/nc-gui/components/feed/View.vue
  29. 32
      packages/nc-gui/components/smartsheet/column/CheckboxOptions.vue
  30. 37
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  31. 11
      packages/nc-gui/components/smartsheet/column/RatingOptions.vue
  32. 13
      packages/nc-gui/components/smartsheet/form/field-settings/visibility.vue
  33. 4
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  34. 6
      packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
  35. 4
      packages/nc-gui/components/smartsheet/toolbar/OpenedViewAction.vue
  36. 7
      packages/nc-gui/composables/useKanbanViewStore.ts
  37. 15
      packages/nc-gui/composables/useProductFeed.ts
  38. 153
      packages/nc-gui/composables/useSharedExecutionFn.ts
  39. 2
      packages/nc-gui/context/index.ts
  40. 23
      packages/nc-gui/helpers/columnDefaultMeta.ts
  41. 286
      packages/nc-gui/lang/es.json
  42. 206
      packages/nc-gui/lang/ko.json
  43. 4
      packages/nc-gui/lang/tr.json
  44. 12
      packages/nc-gui/lang/vi.json
  45. 2
      packages/nc-gui/lib/enums.ts
  46. 2
      packages/nc-gui/lib/types.ts
  47. 4
      packages/nc-gui/package.json
  48. 23
      packages/nc-gui/store/notification.ts
  49. 95
      packages/nc-gui/utils/columnUtils.ts
  50. 1018
      packages/nc-lib-gui/package-lock.json
  51. 1
      packages/nc-mail-templates/package.json
  52. 1
      packages/nc-secret-mgr/package.json
  53. 220
      packages/noco-docs/docs/020.getting-started/050.self-hosted/020.environment-variables.md
  54. 2
      packages/noco-docs/docs/100.data-sources/050.updating-secret.md
  55. 4712
      packages/nocodb-sdk/pnpm-lock.yaml
  56. 98
      packages/nocodb/Dockerfile.timely
  57. 2
      packages/nocodb/package.json
  58. 10
      packages/nocodb/src/controllers/public-datas-export.controller.ts
  59. 27
      packages/nocodb/src/helpers/NcPluginMgrv2.ts
  60. 24
      packages/nocodb/src/models/KanbanView.ts
  61. 3
      packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts
  62. 12
      packages/nocodb/src/plugins/GenericS3/GenericS3.ts
  63. 15
      packages/nocodb/src/plugins/s3/S3.ts
  64. 4
      packages/nocodb/src/plugins/s3/index.ts
  65. 21
      packages/nocodb/src/run/timely.ts
  66. 8
      packages/nocodb/src/schema/swagger-v2.json
  67. 14
      packages/nocodb/src/schema/swagger.json
  68. 44
      packages/nocodb/src/services/columns.service.ts
  69. 48
      packages/nocodb/src/services/tables.service.ts
  70. 7
      packages/nocodb/src/services/utils.service.ts
  71. 2
      packages/nocodb/src/utils/TeleBatchProcessor.ts
  72. 1
      packages/nocodb/src/utils/tele.ts
  73. 89
      packages/nocodb/src/version-upgrader/upgraders/0225002_ncDatasourceDecrypt.ts
  74. 5
      packages/nocodb/tests/unit/rest/tests/table.test.ts
  75. 57
      packages/nocodb/webpack.timely.config.js
  76. 33213
      pnpm-lock.yaml
  77. 12
      pnpm-workspace.yaml
  78. 2
      tests/playwright/tests/db/features/language.spec.ts

5
.github/workflows/bats-test.yml

@ -4,6 +4,7 @@ on:
push: push:
paths: paths:
- 'docker-compose/1_Auto_Upstall/noco.sh' - 'docker-compose/1_Auto_Upstall/noco.sh'
- '.github/workflows/bats-test.yml'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@ -18,7 +19,7 @@ jobs:
- name: Prepare matrix for test files - name: Prepare matrix for test files
id: set-matrix id: set-matrix
run: | run: |
BATS_FILES=$(find docker-compose/setup-script/tests -name '*.bats') BATS_FILES=$(find docker-compose/1_Auto_Upstall/tests -name '*.bats')
MATRIX_JSON=$(echo $BATS_FILES | tr -d '\n' | jq -Rsc 'split(" ")' | tr '"' "'") MATRIX_JSON=$(echo $BATS_FILES | tr -d '\n' | jq -Rsc 'split(" ")' | tr '"' "'")
echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT
test: test:
@ -39,7 +40,7 @@ jobs:
- name: Get working directory - name: Get working directory
run: | run: |
WORKING_DIR="$(pwd)/docker-compose/setup-script/tests" WORKING_DIR="$(pwd)/docker-compose/1_Auto_Upstall/tests"
echo "WORKING_DIR=$WORKING_DIR" >> $GITHUB_ENV echo "WORKING_DIR=$WORKING_DIR" >> $GITHUB_ENV
- name: Run BATS test - name: Run BATS test

406
.github/workflows/ci-cd.yml

@ -4,217 +4,217 @@
name: "CI/CD" name: "CI/CD"
on: on:
push: push:
branches: [develop] branches: [develop]
paths: paths:
- "packages/nc-gui/**" - "packages/nc-gui/**"
- "packages/nocodb/**" - "packages/nocodb/**"
- ".github/workflows/ci-cd.yml" - ".github/workflows/ci-cd.yml"
- "tests/playwright/**" - "tests/playwright/**"
pull_request: pull_request:
types: [opened, reopened, synchronize, ready_for_review, labeled] types: [opened, reopened, synchronize, ready_for_review, labeled]
branches: [develop] branches: [develop]
paths: paths:
- "packages/nc-gui/**" - "packages/nc-gui/**"
- "packages/nocodb/**" - "packages/nocodb/**"
- ".github/workflows/ci-cd.yml" - ".github/workflows/ci-cd.yml"
- ".github/workflows/playwright-test-workflow.yml" - ".github/workflows/playwright-test-workflow.yml"
- "tests/playwright/**" - "tests/playwright/**"
# Triggered manually # Triggered manually
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
validate-swagger-json: validate-swagger-json:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 10 timeout-minutes: 10
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
fetch-depth: 1 fetch-depth: 1
# enable after fixing all validation errors # enable after fixing all validation errors
# - name: Validate OpenAPI definition # - name: Validate OpenAPI definition
# uses: char0n/swagger-editor-validate@v1 # uses: char0n/swagger-editor-validate@v1
# with: # with:
# swagger-editor-url: http://localhost/ # swagger-editor-url: http://localhost/
# definition-file: packages/nocodb/src/schema/swagger.json # definition-file: packages/nocodb/src/schema/swagger.json
- name: Validate Swagger JSON - name: Validate Swagger JSON
run: | run: |
if ! jq empty packages/nocodb/src/schema/swagger.json; then if ! jq empty packages/nocodb/src/schema/swagger.json; then
echo "swagger.json file is not valid JSON" echo "swagger.json file is not valid JSON"
exit 1 exit 1
fi fi
if ! jq empty packages/nocodb/src/schema/swagger-v2.json; then if ! jq empty packages/nocodb/src/schema/swagger-v2.json; then
echo "swaggerv2.json file is not valid JSON" echo "swaggerv2.json file is not valid JSON"
exit 1 exit 1
fi fi
unit-tests: unit-tests:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 40 timeout-minutes: 40
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
steps: steps:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 8 version: 9
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.19.1 node-version: 18.19.1
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: remove use-node-version from .npmrc - name: remove use-node-version from .npmrc
run: sed -i '/^use-node-version/d' .npmrc run: sed -i '/^use-node-version/d' .npmrc
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash
run: | run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3 - uses: actions/cache@v3
name: Setup pnpm cache name: Setup pnpm cache
with: with:
path: ${{ env.STORE_PATH }} path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pnpm-store- ${{ runner.os }}-pnpm-store-
- name: Install dependencies for packages - name: Install dependencies for packages
run: pnpm bootstrap run: pnpm bootstrap
- name: run unit tests - name: run unit tests
working-directory: ./packages/nocodb working-directory: ./packages/nocodb
run: pnpm run test:unit run: pnpm run test:unit
unit-tests-pg: unit-tests-pg:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 40 timeout-minutes: 40
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
steps: steps:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 8 version: 9
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.19.1 node-version: 18.19.1
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: remove use-node-version from .npmrc - name: remove use-node-version from .npmrc
run: sed -i '/^use-node-version/d' .npmrc run: sed -i '/^use-node-version/d' .npmrc
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash
run: | run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3 - uses: actions/cache@v3
name: Setup pnpm cache name: Setup pnpm cache
with: with:
path: ${{ env.STORE_PATH }} path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pnpm-store- ${{ runner.os }}-pnpm-store-
- name: Set CI env - name: Set CI env
run: export CI=true run: export CI=true
- name: setup pg - name: setup pg
working-directory: ./ working-directory: ./
run: docker-compose -f ./tests/playwright/scripts/docker-compose-playwright-pg.yml up -d & run: docker-compose -f ./tests/playwright/scripts/docker-compose-playwright-pg.yml up -d &
- name: install dependencies - name: install dependencies
run: pnpm bootstrap run: pnpm bootstrap
- name: run unit tests - name: run unit tests
working-directory: ./packages/nocodb working-directory: ./packages/nocodb
run: pnpm run test:unit:pg run: pnpm run test:unit:pg
pre-build-for-playwright: pre-build-for-playwright:
uses: ./.github/workflows/pre-build-for-playwright.yml uses: ./.github/workflows/pre-build-for-playwright.yml
playwright-mysql-1: playwright-mysql-1:
needs: pre-build-for-playwright needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml uses: ./.github/workflows/playwright-test-workflow.yml
with: with:
db: mysql db: mysql
shard: 1 shard: 1
playwright-mysql-2: playwright-mysql-2:
needs: pre-build-for-playwright needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml uses: ./.github/workflows/playwright-test-workflow.yml
with: with:
db: mysql db: mysql
shard: 2 shard: 2
playwright-mysql-3: playwright-mysql-3:
needs: pre-build-for-playwright needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml uses: ./.github/workflows/playwright-test-workflow.yml
with: with:
db: mysql db: mysql
shard: 3 shard: 3
playwright-mysql-4: playwright-mysql-4:
needs: pre-build-for-playwright needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml uses: ./.github/workflows/playwright-test-workflow.yml
with: with:
db: mysql db: mysql
shard: 4 shard: 4
playwright-sqlite-1: playwright-sqlite-1:
needs: pre-build-for-playwright needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml uses: ./.github/workflows/playwright-test-workflow.yml
with: with:
db: sqlite db: sqlite
shard: 1 shard: 1
playwright-sqlite-2: playwright-sqlite-2:
needs: pre-build-for-playwright needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml uses: ./.github/workflows/playwright-test-workflow.yml
with: with:
db: sqlite db: sqlite
shard: 2 shard: 2
playwright-sqlite-3: playwright-sqlite-3:
needs: pre-build-for-playwright needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml uses: ./.github/workflows/playwright-test-workflow.yml
with: with:
db: sqlite db: sqlite
shard: 3 shard: 3
playwright-sqlite-4: playwright-sqlite-4:
needs: pre-build-for-playwright needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml uses: ./.github/workflows/playwright-test-workflow.yml
with: with:
db: sqlite db: sqlite
shard: 4 shard: 4
playwright-pg-shard-1: playwright-pg-shard-1:
needs: pre-build-for-playwright needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml uses: ./.github/workflows/playwright-test-workflow.yml
with: with:
db: pg db: pg
shard: 1 shard: 1
playwright-pg-shard-2: playwright-pg-shard-2:
needs: pre-build-for-playwright needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml uses: ./.github/workflows/playwright-test-workflow.yml
with: with:
db: pg db: pg
shard: 2 shard: 2
playwright-pg-shard-3: playwright-pg-shard-3:
needs: pre-build-for-playwright needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml uses: ./.github/workflows/playwright-test-workflow.yml
with: with:
db: pg db: pg
shard: 3 shard: 3
playwright-pg-shard-4: playwright-pg-shard-4:
needs: pre-build-for-playwright needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }} if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml uses: ./.github/workflows/playwright-test-workflow.yml
with: with:
db: pg db: pg
shard: 4 shard: 4

2
.github/workflows/jest-unit-test.yml

@ -31,7 +31,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 8 version: 9
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash
timeout-minutes: 1 timeout-minutes: 1

60
.github/workflows/playwright-test-workflow.yml

@ -4,7 +4,7 @@ on:
workflow_call: workflow_call:
inputs: inputs:
shard: shard:
description: 'Shard number' description: "Shard number"
required: true required: true
type: string type: string
db: db:
@ -19,7 +19,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: remove use-node-version from .npmrc - name: remove use-node-version from .npmrc
run: sed -i '/^use-node-version/d' .npmrc run: sed -i '/^use-node-version/d' .npmrc
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
@ -27,7 +27,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 8 version: 9
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash
run: | run: |
@ -42,7 +42,7 @@ jobs:
- name: setup pg - name: setup pg
if: ${{ inputs.db == 'pg' || ( inputs.db == 'sqlite' && inputs.shard == '1' ) }} if: ${{ inputs.db == 'pg' || ( inputs.db == 'sqlite' && inputs.shard == '1' ) }}
working-directory: ./ working-directory: ./
run: | run: |
service postgresql start service postgresql start
cd /var/lib/postgresql/ && sudo -u postgres psql -c "SELECT 'dropdb '||datname||'' FROM pg_database WHERE datistemplate = false AND datallowconn = true And datname NOT IN ('postgres')" |grep ' dropdb ' | sudo -u postgres /bin/bash ; cd cd /var/lib/postgresql/ && sudo -u postgres psql -c "SELECT 'dropdb '||datname||'' FROM pg_database WHERE datistemplate = false AND datallowconn = true And datname NOT IN ('postgres')" |grep ' dropdb ' | sudo -u postgres /bin/bash ; cd
sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD 'password';" sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD 'password';"
@ -144,32 +144,32 @@ jobs:
working-directory: ./tests/playwright working-directory: ./tests/playwright
run: E2E_DB_TYPE=${{ inputs.db }} node ./scripts/stressTestNewlyAddedTest.js run: E2E_DB_TYPE=${{ inputs.db }} node ./scripts/stressTestNewlyAddedTest.js
# # Quick tests (pg on sqlite shard 0 and sqlite on sqlite shard 1) # # Quick tests (pg on sqlite shard 0 and sqlite on sqlite shard 1)
# - name: Run quick server and tests (pg) # - name: Run quick server and tests (pg)
# if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }} # if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }}
# working-directory: ./packages/nocodb # working-directory: ./packages/nocodb
# run: | # run: |
# kill -9 $(lsof -t -i:8080) # kill -9 $(lsof -t -i:8080)
# npm run watch:run:playwright:pg:cyquick > quick_${{ inputs.shard }}_test_backend.log & # npm run watch:run:playwright:pg:cyquick > quick_${{ inputs.shard }}_test_backend.log &
# - name: Run quick server and tests (sqlite) # - name: Run quick server and tests (sqlite)
# if: ${{ inputs.db == 'sqlite' && inputs.shard == '2' }} # if: ${{ inputs.db == 'sqlite' && inputs.shard == '2' }}
# working-directory: ./packages/nocodb # working-directory: ./packages/nocodb
# run: | # run: |
# kill -9 $(lsof -t -i:8080) # kill -9 $(lsof -t -i:8080)
# npm run watch:run:playwright:quick > quick_${{ inputs.shard }}_test_backend.log & # npm run watch:run:playwright:quick > quick_${{ inputs.shard }}_test_backend.log &
# - name: Wait for backend for sqlite-tests # - name: Wait for backend for sqlite-tests
# if: ${{ inputs.db == 'sqlite' }} # if: ${{ inputs.db == 'sqlite' }}
# working-directory: ./tests/playwright # working-directory: ./tests/playwright
# run: | # run: |
# while ! curl --output /dev/null --silent --head --fail http://localhost:8080; do # while ! curl --output /dev/null --silent --head --fail http://localhost:8080; do
# printf '.' # printf '.'
# sleep 2 # sleep 2
# done # done
# timeout-minutes: 1 # timeout-minutes: 1
# - name: Run quick tests # - name: Run quick tests
# if: ${{ inputs.db == 'sqlite' }} # if: ${{ inputs.db == 'sqlite' }}
# working-directory: ./tests/playwright # working-directory: ./tests/playwright
# run: PLAYWRIGHT_HTML_REPORT=playwright-report-quick npm run test:quick # run: PLAYWRIGHT_HTML_REPORT=playwright-report-quick npm run test:quick
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: ${{ inputs.db == 'sqlite' }} if: ${{ inputs.db == 'sqlite' }}

5
.github/workflows/pre-build-for-playwright.yml

@ -17,9 +17,9 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 8 version: 9
- name: remove use-node-version from .npmrc - name: remove use-node-version from .npmrc
run: sed -i '/^use-node-version/d' .npmrc run: sed -i '/^use-node-version/d' .npmrc
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash
run: | run: |
@ -47,4 +47,3 @@ jobs:
zip -r ${FILE} .output || echo "UI build directory does not exists" >&2 zip -r ${FILE} .output || echo "UI build directory does not exists" >&2
echo "uploading ${FILE} to http://65.21.27.147/upload/${FILE}" echo "uploading ${FILE} to http://65.21.27.147/upload/${FILE}"
time curl -T "${FILE}" http://65.21.27.147/upload/${FILE} -n time curl -T "${FILE}" http://65.21.27.147/upload/${FILE} -n

4
.github/workflows/release-docker.yml

@ -48,7 +48,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 8 version: 9
- name: Get Docker Repository - name: Get Docker Repository
id: get-docker-repository id: get-docker-repository
run: | run: |
@ -135,6 +135,8 @@ jobs:
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new cache-to: type=local,dest=/tmp/.buildx-cache-new
push: true push: true
labels: |
"service=nocodb"
tags: | tags: |
nocodb/${{ steps.get-docker-repository.outputs.DOCKER_REPOSITORY }}:${{ steps.get-docker-repository.outputs.DOCKER_BUILD_TAG }} nocodb/${{ steps.get-docker-repository.outputs.DOCKER_REPOSITORY }}:${{ steps.get-docker-repository.outputs.DOCKER_BUILD_TAG }}
nocodb/${{ steps.get-docker-repository.outputs.DOCKER_REPOSITORY }}:${{ steps.get-docker-repository.outputs.DOCKER_BUILD_LATEST_TAG }} nocodb/${{ steps.get-docker-repository.outputs.DOCKER_REPOSITORY }}:${{ steps.get-docker-repository.outputs.DOCKER_BUILD_LATEST_TAG }}

27
.github/workflows/release-nightly-dev.yml

@ -43,29 +43,20 @@ jobs:
nightly_build_tag: ${{ steps.tag-step.outputs.NIGHTLY_BUILD_TAG }} nightly_build_tag: ${{ steps.tag-step.outputs.NIGHTLY_BUILD_TAG }}
is_daily: ${{ steps.tag-step.outputs.IS_DAILY }} is_daily: ${{ steps.tag-step.outputs.IS_DAILY }}
current_version: ${{ steps.tag-step.outputs.CURRENT_VERSION }} current_version: ${{ steps.tag-step.outputs.CURRENT_VERSION }}
# Build frontend and backend and publish to npm
release-npm:
needs: set-tag
uses: ./.github/workflows/release-npm.yml
with:
tag: ${{ needs.set-tag.outputs.nightly_build_tag }}
targetEnv: 'DEV'
secrets:
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
# Build executables and publish to GitHub # Build executables and publish to GitHub
release-executables: # release-executables:
needs: [set-tag, release-npm] # needs: [set-tag, release-npm]
uses: ./.github/workflows/release-timely-executables.yml # uses: ./.github/workflows/release-timely-executables.yml
with: # with:
tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.nightly_build_tag }} # tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.nightly_build_tag }}
secrets: # secrets:
NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}" # NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# Build docker image and push to docker hub # Build docker image and push to docker hub
release-docker: release-docker:
needs: [set-tag, release-npm] needs: [set-tag]
uses: ./.github/workflows/release-docker.yml uses: ./.github/workflows/release-timely-docker.yml
with: with:
currentVersion: ${{ needs.set-tag.outputs.current_version }} currentVersion: ${{ needs.set-tag.outputs.current_version }}
tag: ${{ needs.set-tag.outputs.nightly_build_tag }} tag: ${{ needs.set-tag.outputs.nightly_build_tag }}

10
.github/workflows/release-npm.yml

@ -40,7 +40,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 8 version: 9
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
@ -51,7 +51,7 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.19.1 node-version: 18.19.1
registry-url: 'https://registry.npmjs.org' registry-url: "https://registry.npmjs.org"
- run: | - run: |
export NODE_OPTIONS="--max_old_space_size=16384" export NODE_OPTIONS="--max_old_space_size=16384"
NOCODB_SDK_PKG_NAME=nocodb-sdk NOCODB_SDK_PKG_NAME=nocodb-sdk
@ -79,10 +79,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
signoff: true signoff: true
branch: 'release/${{ github.event.inputs.tag || inputs.tag }}' branch: "release/${{ github.event.inputs.tag || inputs.tag }}"
delete-branch: true delete-branch: true
title: 'Release ${{ github.event.inputs.tag || inputs.tag }}' title: "Release ${{ github.event.inputs.tag || inputs.tag }}"
labels: 'Bot: Automerge' labels: "Bot: Automerge"
- name: Check outputs - name: Check outputs
if: ${{ github.event.inputs.targetEnv == 'PROD' || inputs.targetEnv == 'PROD' }} if: ${{ github.event.inputs.targetEnv == 'PROD' || inputs.targetEnv == 'PROD' }}
run: | run: |

15
.github/workflows/release-pr.yml

@ -50,22 +50,11 @@ jobs:
target_tag: ${{ steps.tag-step.outputs.TARGET_TAG }} target_tag: ${{ steps.tag-step.outputs.TARGET_TAG }}
current_version: ${{ steps.tag-step.outputs.CURRENT_VERSION }} current_version: ${{ steps.tag-step.outputs.CURRENT_VERSION }}
# Build, install, publish frontend and backend to npm
release-npm:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }}
needs: [set-tag]
uses: ./.github/workflows/release-npm.yml
with:
tag: ${{ needs.set-tag.outputs.target_tag }}
targetEnv: 'DEV'
secrets:
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
# Build docker image and push to docker hub # Build docker image and push to docker hub
release-docker: release-docker:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }} if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }}
needs: [release-npm, set-tag] needs: [set-tag]
uses: ./.github/workflows/release-docker.yml uses: ./.github/workflows/release-timely-docker.yml
with: with:
currentVersion: ${{ needs.set-tag.outputs.current_version }} currentVersion: ${{ needs.set-tag.outputs.current_version }}
tag: ${{ needs.set-tag.outputs.target_tag }} tag: ${{ needs.set-tag.outputs.target_tag }}

11
.github/workflows/release-secret-cli.yml

@ -19,13 +19,13 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 8 version: 9
- name: Setup Node 18.19.1 - name: Setup Node 18.19.1
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.19.1 node-version: 18.19.1
registry-url: 'https://registry.npmjs.org' registry-url: "https://registry.npmjs.org"
- name: Cache pkg modules - name: Cache pkg modules
id: cache-pkg id: cache-pkg
@ -68,7 +68,7 @@ jobs:
with: with:
node-version: 16 node-version: 16
- name : Install nocodb, other dependencies and build executables - name: Install nocodb, other dependencies and build executables
run: | run: |
cd ./packages/nc-secret-mgr cd ./packages/nc-secret-mgr
@ -117,7 +117,6 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
needs: build-and-publish needs: build-and-publish
steps: steps:
- uses: actions/download-artifact@master - uses: actions/download-artifact@master
with: with:
name: ${{ github.event.inputs.tag || inputs.tag }} name: ${{ github.event.inputs.tag || inputs.tag }}
@ -134,9 +133,8 @@ jobs:
path: packages/nc-secret-mgr/mac-dist path: packages/nc-secret-mgr/mac-dist
retention-days: 1 retention-days: 1
publish-mac-executables: publish-mac-executables:
needs: [sign-mac-executables,build-and-publish] needs: [sign-mac-executables, build-and-publish]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/download-artifact@master - uses: actions/download-artifact@master
@ -153,4 +151,3 @@ jobs:
overwrite: true overwrite: true
file_glob: true file_glob: true
repo_name: nocodb/nc-secret-mgr repo_name: nocodb/nc-secret-mgr

161
.github/workflows/release-timely-docker.yml

@ -0,0 +1,161 @@
name: "Release : Docker"
on:
# Triggered manually
workflow_dispatch:
inputs:
tag:
description: "Docker image tag"
required: true
targetEnv:
description: "Target Environment"
required: true
type: choice
options:
- DEV
- PROD
# Triggered by release-nocodb.yml / release-nightly-dev.yml / release-pr.yml
workflow_call:
inputs:
tag:
description: "Docker image tag"
required: true
type: string
targetEnv:
description: "Target Environment"
required: true
type: string
isDaily:
description: "Is it triggered by daily schedule"
required: false
type: string
currentVersion:
description: "The current NocoDB version"
required: false
type: string
secrets:
DOCKERHUB_USERNAME:
required: true
DOCKERHUB_TOKEN:
required: true
jobs:
buildx:
runs-on: ubuntu-latest
env:
working-directory: ./packages/nocodb
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Get Docker Repository
id: get-docker-repository
run: |
DOCKER_REPOSITORY=nocodb-daily
DOCKER_BUILD_TAG=${{ github.event.inputs.tag || inputs.tag }}
DOCKER_BUILD_LATEST_TAG=latest
if [[ "$DOCKER_BUILD_TAG" =~ "-beta." ]]; then
DOCKER_BUILD_LATEST_TAG=$(echo $DOCKER_BUILD_TAG | awk -F '-beta.' '{print $1}')-beta.latest
fi
if [[ ${{ github.event.inputs.targetEnv || inputs.targetEnv }} == 'DEV' ]]; then
if [[ ${{ github.event.inputs.currentVersion || inputs.currentVersion || 'N/A' }} != 'N/A' ]]; then
DOCKER_BUILD_TAG=${{ github.event.inputs.currentVersion || inputs.currentVersion }}-${{ github.event.inputs.tag || inputs.tag }}
fi
if [[ ${{ inputs.isDaily || 'N' }} == 'Y' ]]; then
DOCKER_REPOSITORY=nocodb-daily
else
DOCKER_REPOSITORY=nocodb-timely
fi
fi
echo "DOCKER_REPOSITORY=${DOCKER_REPOSITORY}" >> $GITHUB_OUTPUT
echo "DOCKER_BUILD_TAG=${DOCKER_BUILD_TAG}" >> $GITHUB_OUTPUT
echo "DOCKER_BUILD_LATEST_TAG=${DOCKER_BUILD_LATEST_TAG}" >> $GITHUB_OUTPUT
echo DOCKER_REPOSITORY: ${DOCKER_REPOSITORY}
echo DOCKER_BUILD_TAG: ${DOCKER_BUILD_TAG}
echo DOCKER_BUILD_LATEST_TAG: ${DOCKER_BUILD_LATEST_TAG}
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1
ref: ${{ github.ref }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: 18.19.1
- name: install dependencies
run: pnpm bootstrap
- name: Build gui and sdk
run: |
pnpm bootstrap &&
cd packages/nc-gui &&
pnpm run generate
# copy build to nocodb
rsync -rvzh ./dist/ ../nocodb/docker/nc-gui/
- name: build nocodb
run: |
# build nocodb ( pack nocodb-sdk and nc-gui )
cd packages/nocodb &&
EE=true pnpm exec webpack --config webpack.timely.config.js &&
# remove bundled libraries (nocodb-sdk, knex-snowflake)
pnpm uninstall --save-prod nocodb-sdk
- name: Update version in package.json
run: |
# update package.json
cd packages/nocodb &&
jq --arg VERSION "$VERSION" '.version = $VERSION' package.json > tmp.json &&
mv tmp.json package.json
env:
VERSION: ${{ steps.get-docker-repository.outputs.DOCKER_BUILD_TAG }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to DockerHub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3.2.0
with:
context: ${{ env.working-directory }}
file: ${{ env.working-directory }}/Dockerfile.timely
build-args: NC_VERSION=${{ steps.get-docker-repository.outputs.DOCKER_BUILD_TAG }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
push: true
tags: |
nocodb/${{ steps.get-docker-repository.outputs.DOCKER_REPOSITORY }}:${{ steps.get-docker-repository.outputs.DOCKER_BUILD_TAG }}
nocodb/${{ steps.get-docker-repository.outputs.DOCKER_REPOSITORY }}:${{ steps.get-docker-repository.outputs.DOCKER_BUILD_LATEST_TAG }}
# Temp fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

4
.github/workflows/sync-to-develop.yml

@ -1,4 +1,4 @@
name: 'Sync changes back to develop branch from master' name: "Sync changes back to develop branch from master"
on: on:
# Triggered manually # Triggered manually
@ -16,7 +16,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 8 version: 9
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:

11
.github/workflows/unit-test.yml

@ -5,17 +5,16 @@ name: Backend Unit Tests
on: on:
push: push:
branches: [ "develop" ] branches: ["develop"]
paths: paths:
- "packages/nocodb/**" - "packages/nocodb/**"
pull_request: pull_request:
branches: [ "develop" ] branches: ["develop"]
paths: paths:
- "packages/nocodb/**" - "packages/nocodb/**"
jobs: jobs:
unit-tests: unit-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@ -27,15 +26,15 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 8 version: 9
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: "pnpm"
- name: remove use-node-version from .npmrc - name: remove use-node-version from .npmrc
run: sed -i '/^use-node-version/d' .npmrc run: sed -i '/^use-node-version/d' .npmrc
- name: install dependencies - name: install dependencies
run: pnpm bootstrap run: pnpm bootstrap
- name: run unit tests - name: run unit tests

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

@ -12,11 +12,11 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 8 version: 9
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.19.1 node-version: 18.19.1
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
@ -32,10 +32,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
signoff: true signoff: true
branch: 'bot/update-nocodb-sdk-path' branch: "bot/update-nocodb-sdk-path"
delete-branch: true delete-branch: true
title: 'Update nocodb-sdk to local path' title: "Update nocodb-sdk to local path"
labels: 'Bot: Automerge' labels: "Bot: Automerge"
- name: Check outputs - name: Check outputs
run: | run: |
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"

10
README.md

@ -91,6 +91,10 @@ Auto-upstall does the following : 🕊
- 🔒 Automatically setups SSL and also renews it. Needs a domain or subdomain as input while installation. - 🔒 Automatically setups SSL and also renews it. Needs a domain or subdomain as input while installation.
> install.nocodb.com/noco.sh script can be found [here in our github](https://raw.githubusercontent.com/nocodb/nocodb/develop/docker-compose/1_Auto_Upstall/noco.sh) > install.nocodb.com/noco.sh script can be found [here in our github](https://raw.githubusercontent.com/nocodb/nocodb/develop/docker-compose/1_Auto_Upstall/noco.sh)
## One-Click Deployment
[![Deploy on RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploy.png)](https://repocloud.io/details/?app_id=100)
## Other Methods ## Other Methods
@ -109,6 +113,12 @@ Auto-upstall does the following : 🕊
> When running locally access nocodb by visiting: [http://localhost:8080/dashboard](http://localhost:8080/dashboard) > When running locally access nocodb by visiting: [http://localhost:8080/dashboard](http://localhost:8080/dashboard)
## Self-Hosting NocoDB
### Elestio
[![Deploy on Elestio](https://elest.io/images/logos/deploy-to-elestio-btn.png)](https://elest.io/open-source/nocodb)
# Screenshots # Screenshots
![2](https://github.com/nocodb/nocodb/assets/86527202/a127c05e-2121-4af2-a342-128e0e2d0291) ![2](https://github.com/nocodb/nocodb/assets/86527202/a127c05e-2121-4af2-a342-128e0e2d0291)
![3](https://github.com/nocodb/nocodb/assets/86527202/674da952-8a06-4848-a0e8-a7b02d5f5c88) ![3](https://github.com/nocodb/nocodb/assets/86527202/674da952-8a06-4848-a0e8-a7b02d5f5c88)

3
docker-compose/1_Auto_Upstall/tests/expects/install/redis.sh

@ -18,6 +18,9 @@ send "Y\r"
expect "Choose Community or Enterprise Edition*" expect "Choose Community or Enterprise Edition*"
send "\r" send "\r"
expect "Select PostgreSQL or SQLite as your database*"
send "P\r"
expect "Do you want to enabled Redis for caching*" expect "Do you want to enabled Redis for caching*"
send "Y\r" send "Y\r"

3
docker-compose/1_Auto_Upstall/tests/expects/install/scale.sh

@ -18,6 +18,9 @@ send "Y\r"
expect "Choose Community or Enterprise Edition*" expect "Choose Community or Enterprise Edition*"
send "\r" send "\r"
expect "Select PostgreSQL or SQLite as your database*"
send "P\r"
expect "Do you want to enabled Redis for caching*" expect "Do you want to enabled Redis for caching*"
send "Y\r" send "Y\r"

3
docker-compose/1_Auto_Upstall/tests/expects/install/watchtower.sh

@ -18,6 +18,9 @@ send "Y\r"
expect "Choose Community or Enterprise Edition*" expect "Choose Community or Enterprise Edition*"
send "\r" send "\r"
expect "Select PostgreSQL or SQLite as your database*"
send "P\r"
expect "Do you want to enabled Redis for caching*" expect "Do you want to enabled Redis for caching*"
send "\r" send "\r"

7
packages/nc-gui/components/cell/Checkbox.vue

@ -38,13 +38,12 @@ const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isGrid = inject(IsGridInj, ref(false)) const isGrid = inject(IsGridInj, ref(false))
const checkboxMeta = computed(() => { const checkboxMeta = computed(() => {
const icon = extractCheckboxIcon(column?.value?.meta)
return { return {
icon: {
checked: 'mdi-check-circle-outline',
unchecked: 'mdi-checkbox-blank-circle-outline',
},
color: 'primary', color: 'primary',
...parseProp(column?.value?.meta), ...parseProp(column?.value?.meta),
icon,
} }
}) })

2
packages/nc-gui/components/cell/MultiSelect.vue

@ -381,6 +381,7 @@ const onFocus = () => {
v-for="op of options" v-for="op of options"
:key="op.title" :key="op.title"
:value="op.title" :value="op.title"
class="gap-2"
:data-testid="`select-option-${column.title}-${location === 'filter' ? 'filter' : rowIndex}`" :data-testid="`select-option-${column.title}-${location === 'filter' ? 'filter' : rowIndex}`"
:class="`nc-select-option-${column.title}-${op.title}`" :class="`nc-select-option-${column.title}-${op.title}`"
> >
@ -487,6 +488,7 @@ const onFocus = () => {
v-for="op of options" v-for="op of options"
:key="op.id || op.title" :key="op.id || op.title"
:value="op.title" :value="op.title"
class="gap-2"
:data-testid="`select-option-${column.title}-${location === 'filter' ? 'filter' : rowIndex}`" :data-testid="`select-option-${column.title}-${location === 'filter' ? 'filter' : rowIndex}`"
:class="`nc-select-option-${column.title}-${op.title}`" :class="`nc-select-option-${column.title}-${op.title}`"
@click.stop @click.stop

6
packages/nc-gui/components/cell/Rating.vue

@ -16,14 +16,12 @@ const rowHeight = inject(RowHeightInj, ref(undefined))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const ratingMeta = computed(() => { const ratingMeta = computed(() => {
const icon = extractRatingIcon(column?.value?.meta)
return { return {
icon: {
full: 'mdi-star',
empty: 'mdi-star-outline',
},
color: '#fcb401', color: '#fcb401',
max: 5, max: 5,
...parseProp(column.value?.meta), ...parseProp(column.value?.meta),
icon,
} }
}) })

1
packages/nc-gui/components/cell/SingleSelect.vue

@ -407,6 +407,7 @@ const onFocus = () => {
v-for="op of options" v-for="op of options"
:key="op.title" :key="op.title"
:value="op.title" :value="op.title"
class="gap-2"
:data-testid="`select-option-${column.title}-${rowIndex}`" :data-testid="`select-option-${column.title}-${rowIndex}`"
:class="`nc-select-option-${column.title}-${op.title}`" :class="`nc-select-option-${column.title}-${op.title}`"
@click.stop @click.stop

4
packages/nc-gui/components/dlg/QuickImport.vue

@ -318,9 +318,9 @@ const customReqCbk = (customReqArgs: { file: any; onSuccess: () => void }) => {
/** check if the file size exceeds the limit */ /** check if the file size exceeds the limit */
const beforeUpload = (file: UploadFile) => { const beforeUpload = (file: UploadFile) => {
const exceedLimit = file.size! / 1024 / 1024 > 5 const exceedLimit = file.size! / 1024 / 1024 > 25
if (exceedLimit) { if (exceedLimit) {
message.error(`File ${file.name} is too big. The accepted file size is less than 5MB.`) message.error(`File ${file.name} is too big. The accepted file size is less than 25MB.`)
} }
return !exceedLimit || Upload.LIST_IGNORE return !exceedLimit || Upload.LIST_IGNORE
} }

23
packages/nc-gui/components/feed/Changelog/index.vue

@ -1,5 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
const { loadFeed, githubFeed, isErrorOccurred } = useProductFeed() const props = defineProps<{
type: 'github' | 'cloud'
}>()
const { loadFeed, githubFeed, isErrorOccurred, cloudFeed } = useProductFeed()
const scrollContainer = ref<HTMLElement>() const scrollContainer = ref<HTMLElement>()
@ -8,12 +12,16 @@ const { isLoading } = useInfiniteScroll(
async () => { async () => {
if (isLoading.value) return if (isLoading.value) return
await loadFeed({ await loadFeed({
type: 'github', type: props.type,
loadMore: true, loadMore: true,
}) })
}, },
{ distance: 4 }, { distance: 4 },
) )
const feeds = computed(() => {
return props.type === 'github' ? githubFeed.value : cloudFeed.value
})
</script> </script>
<template> <template>
@ -24,15 +32,18 @@ const { isLoading } = useInfiniteScroll(
}" }"
class="overflow-y-auto nc-scrollbar-md mx-auto w-full" class="overflow-y-auto nc-scrollbar-md mx-auto w-full"
> >
<div v-if="isErrorOccurred?.github && !githubFeed.length" class="h-full flex justify-center items-center"> <div
<FeedError page="github" /> v-if="(props.type === 'github' ? isErrorOccurred.github : isErrorOccurred.cloud) && !feeds.length"
class="h-full flex justify-center items-center"
>
<FeedError :page="type" />
</div> </div>
<div v-else-if="isLoading && !githubFeed.length" class="flex items-center justify-center h-full w-full"> <div v-else-if="isLoading && !feeds.length" class="flex items-center justify-center h-full w-full">
<GeneralLoader size="xlarge" /> <GeneralLoader size="xlarge" />
</div> </div>
<div v-else class="mx-auto max-w-[540px] xl:max-w-[638px] justify-around justify-items-center"> <div v-else class="mx-auto max-w-[540px] xl:max-w-[638px] justify-around justify-items-center">
<FeedChangelogItem v-for="(feed, index) in githubFeed" :key="feed.Id" :item="feed" :index="index" /> <FeedChangelogItem v-for="(item, index) in feeds" :key="item.Id" :item="item" :index="index" />
</div> </div>
</div> </div>
</template> </template>

6
packages/nc-gui/components/feed/Error.vue

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ const props = defineProps<{
page: 'all' | 'youtube' | 'github' | 'twitter' page: 'all' | 'youtube' | 'github' | 'twitter' | 'cloud'
}>() }>()
const emits = defineEmits(['reload']) const emits = defineEmits(['reload'])
const { loadFeed, socialFeed, youtubeFeed, githubFeed } = useProductFeed() const { loadFeed, socialFeed, youtubeFeed, githubFeed, cloudFeed } = useProductFeed()
const triggerReload = async () => { const triggerReload = async () => {
if (props.page === 'twitter') { if (props.page === 'twitter') {
@ -24,6 +24,8 @@ const triggerReload = async () => {
youtubeFeed.value = data youtubeFeed.value = data
} else if (props.page === 'github') { } else if (props.page === 'github') {
githubFeed.value = data githubFeed.value = data
} else if (props.page === 'cloud') {
cloudFeed.value = data
} }
} }
</script> </script>

15
packages/nc-gui/components/feed/Recents/Card.vue

@ -22,6 +22,7 @@ const feedIcon = {
Twitter: iconMap.twitter, Twitter: iconMap.twitter,
Youtube: iconMap.youtube, Youtube: iconMap.youtube,
Github: iconMap.githubSolid, Github: iconMap.githubSolid,
Cloud: iconMap.ncCloud,
} }
const truncate = ref(true) const truncate = ref(true)
@ -32,7 +33,7 @@ const expand = () => {
truncate.value = false truncate.value = false
$e('c:nocodb:feed:recents:expand', { $e('c:nocodb:feed:recents:expand', {
title: Title, title: Title,
type: 'github', type: source,
}) })
} }
@ -61,14 +62,20 @@ const renderedText = computedAsync(async () => {
}) })
const { width } = useWindowSize() const { width } = useWindowSize()
const handleOpenUrl = (url: string) => {
if (source === 'Cloud') return
openLink(url)
}
</script> </script>
<template> <template>
<div class="bg-white recent-card border-gray-200 border-1 rounded-2xl max-w-[540px] xl:max-w-[640px]"> <div class="bg-white recent-card border-gray-200 border-1 rounded-2xl max-w-[540px] xl:max-w-[640px]">
<div class="flex items-center justify-between px-5 py-4"> <div class="flex items-center justify-between px-5 py-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<component :is="feedIcon[source as any]" class="w-4 h-4 stroke-transparent" /> <component :is="feedIcon[source]" class="w-4 h-4 stroke-transparent" />
<span class="font-weight-medium text-nc-content-gray leading-5 cursor-pointer" @click="openLink(Url)"> <span class="font-weight-medium text-nc-content-gray leading-5 cursor-pointer" @click="handleOpenUrl">
{{ source }} {{ source }}
</span> </span>
</div> </div>
@ -76,7 +83,7 @@ const { width } = useWindowSize()
{{ timeAgo(CreatedAt) }} {{ timeAgo(CreatedAt) }}
</div> </div>
</div> </div>
<template v-if="source === 'Github'"> <template v-if="['Github', 'Cloud'].includes(source)">
<div class="pb-5"> <div class="pb-5">
<LazyCellAttachmentPreviewImage <LazyCellAttachmentPreviewImage
v-if="Images?.length" v-if="Images?.length"

4
packages/nc-gui/components/feed/Recents/index.vue

@ -37,7 +37,9 @@ onMounted(() => {
<GeneralLoader size="xlarge" /> <GeneralLoader size="xlarge" />
</div> </div>
<div v-else class="flex flex-col my-6 items-center gap-6"> <div v-else class="flex flex-col my-6 items-center gap-6">
<FeedRecentsCard v-for="feed in socialFeed" :key="feed.Id" :item="feed" /> <template v-for="feed in socialFeed" :key="feed.Id">
<FeedRecentsCard v-if="['Github', 'Cloud', 'Youtube'].includes(feed['Feed Source'])" :item="feed" />
</template>
</div> </div>
</div> </div>
</template> </template>

14
packages/nc-gui/components/feed/View.vue

@ -21,7 +21,13 @@ const tabs: Array<{
container: FeedRecents, container: FeedRecents,
}, },
{ {
key: 'changelog', key: 'cloud',
icon: 'ncCloud',
title: 'Cloud Changelog',
container: FeedChangelog,
},
{
key: 'github',
icon: 'ncList', icon: 'ncList',
title: 'Changelog', title: 'Changelog',
container: FeedChangelog, container: FeedChangelog,
@ -89,13 +95,13 @@ onMounted(() => {
<div class="relative"> <div class="relative">
<FeedSocial <FeedSocial
:class="{ :class="{
'normal-left': tab.key === 'recents' || tab.key === 'youtube', 'normal-left': ['recents', 'youtube', 'cloud'].includes(tab.key),
'changelog-left': tab.key === 'changelog', 'changelog-left': tab.key === 'github',
'changelog-twitter': tab.key === 'twitter', 'changelog-twitter': tab.key === 'twitter',
}" }"
class="absolute social-card" class="absolute social-card"
/> />
<component :is="tab.container" /> <component :is="tab.container" :type="tab.key" />
</div> </div>
</a-tab-pane> </a-tab-pane>
</NcTabs> </NcTabs>

32
packages/nc-gui/components/smartsheet/column/CheckboxOptions.vue

@ -10,36 +10,7 @@ const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
// cater existing v1 cases // cater existing v1 cases
const iconList = [ const iconList = checkboxIconList
{
checked: 'mdi-check-bold',
unchecked: 'mdi-crop-square',
},
{
checked: 'mdi-check-circle-outline',
unchecked: 'mdi-checkbox-blank-circle-outline',
},
{
checked: 'mdi-star',
unchecked: 'mdi-star-outline',
},
{
checked: 'mdi-heart',
unchecked: 'mdi-heart-outline',
},
{
checked: 'mdi-moon-full',
unchecked: 'mdi-moon-new',
},
{
checked: 'mdi-thumb-up',
unchecked: 'mdi-thumb-up-outline',
},
{
checked: 'mdi-flag',
unchecked: 'mdi-flag-outline',
},
]
const picked = computed({ const picked = computed({
get: () => vModel.value.meta.color, get: () => vModel.value.meta.color,
@ -54,6 +25,7 @@ const isOpenColorPicker = ref(false)
vModel.value.meta = { vModel.value.meta = {
...columnDefaultMeta[UITypes.Checkbox], ...columnDefaultMeta[UITypes.Checkbox],
...(vModel.value.meta || {}), ...(vModel.value.meta || {}),
icon: extractCheckboxIcon(vModel.value.meta || {}),
} }
// antdv doesn't support object as value // antdv doesn't support object as value

37
packages/nc-gui/components/smartsheet/column/FormulaOptions.vue

@ -85,7 +85,19 @@ if ((column.value?.colOptions as any)?.formula_raw) {
const source = computed(() => activeBase.value?.sources?.find((s) => s.id === meta.value?.source_id)) const source = computed(() => activeBase.value?.sources?.find((s) => s.id === meta.value?.source_id))
const parsedTree = computedAsync(async () => { const parsedTree = ref<any>({
dataType: FormulaDataTypes.UNKNOWN,
})
// Initialize a counter to track watcher invocations
let watcherCounter = 0
// Define the debounced async validation function
const debouncedValidate = useDebounceFn(async () => {
// Increment the counter for each invocation
watcherCounter += 1
const currentCounter = watcherCounter
try { try {
const parsed = await validateFormulaAndExtractTreeWithType({ const parsed = await validateFormulaAndExtractTreeWithType({
formula: vModel.value.formula || vModel.value.formula_raw, formula: vModel.value.formula || vModel.value.formula_raw,
@ -94,13 +106,28 @@ const parsedTree = computedAsync(async () => {
clientOrSqlUi: source.value?.type as any, clientOrSqlUi: source.value?.type as any,
getMeta: async (modelId) => await getMeta(modelId), getMeta: async (modelId) => await getMeta(modelId),
}) })
return parsed
// Update parsedTree only if this is the latest invocation
if (currentCounter === watcherCounter) {
parsedTree.value = parsed
}
} catch (e) { } catch (e) {
return { // Update parsedTree only if this is the latest invocation
dataType: FormulaDataTypes.UNKNOWN, if (currentCounter === watcherCounter) {
parsedTree.value = {
dataType: FormulaDataTypes.UNKNOWN,
}
} }
} }
}) }, 300)
// Watch the formula inputs and call the debounced function
watch(
() => vModel.value.formula || vModel.value.formula_raw,
() => {
debouncedValidate()
},
)
// set additional validations // set additional validations
setAdditionalValidations({ setAdditionalValidations({

11
packages/nc-gui/components/smartsheet/column/RatingOptions.vue

@ -22,11 +22,12 @@ const isOpenColorPicker = ref(false)
vModel.value.meta = { vModel.value.meta = {
...columnDefaultMeta[UITypes.Rating], ...columnDefaultMeta[UITypes.Rating],
...(vModel.value.meta || {}), ...(vModel.value.meta || {}),
icon: extractRatingIcon(vModel.value.meta || {}),
} }
// antdv doesn't support object as value // antdv doesn't support object as value
// use iconIdx as value and update back in watch // use iconIdx as value and update back in watch
const iconIdx = iconList.findIndex( const iconIdx = ratingIconList.findIndex(
(ele) => ele.full === vModel.value.meta.icon.full && ele.empty === vModel.value.meta.icon.empty, (ele) => ele.full === vModel.value.meta.icon.full && ele.empty === vModel.value.meta.icon.empty,
) )
@ -35,7 +36,7 @@ vModel.value.meta.iconIdx = iconIdx === -1 ? 0 : iconIdx
watch( watch(
() => vModel.value.meta.iconIdx, () => vModel.value.meta.iconIdx,
(v) => { (v) => {
vModel.value.meta.icon = iconList[v] vModel.value.meta.icon = ratingIconList[v]
}, },
) )
</script> </script>
@ -49,7 +50,7 @@ watch(
<GeneralIcon icon="arrowDown" class="text-gray-700" /> <GeneralIcon icon="arrowDown" class="text-gray-700" />
</template> </template>
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="i"> <a-select-option v-for="(icon, i) of ratingIconList" :key="i" :value="i">
<div class="flex gap-2 w-full truncate items-center"> <div class="flex gap-2 w-full truncate items-center">
<div class="flex-1 flex items-center text-gray-700 gap-2 children:(h-4 w-4)"> <div class="flex-1 flex items-center text-gray-700 gap-2 children:(h-4 w-4)">
<component :is="getMdiIcon(icon.full)" /> <component :is="getMdiIcon(icon.full)" />
@ -84,13 +85,13 @@ watch(
> >
<div class="flex-1 flex items-center gap-2 children:(h-4 w-4)"> <div class="flex-1 flex items-center gap-2 children:(h-4 w-4)">
<component <component
:is="getMdiIcon(iconList[vModel.meta.iconIdx].full)" :is="getMdiIcon(ratingIconList[vModel.meta.iconIdx].full)"
:style="{ :style="{
color: vModel.meta.color, color: vModel.meta.color,
}" }"
/> />
<component <component
:is="getMdiIcon(iconList[vModel.meta.iconIdx].empty)" :is="getMdiIcon(ratingIconList[vModel.meta.iconIdx].empty)"
:style="{ :style="{
color: vModel.meta.color, color: vModel.meta.color,
}" }"

13
packages/nc-gui/components/smartsheet/form/field-settings/visibility.vue

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
const { visibleColumns, activeField, allViewFilters, localColumnsMapByFkColumnId } = useFormViewStoreOrThrow() const { visibleColumns, activeField, allViewFilters, localColumns, localColumnsMapByFkColumnId } = useFormViewStoreOrThrow()
const isOpen = ref<boolean>(false) const isOpen = ref<boolean>(false)
@ -9,6 +9,17 @@ const allFilters = ref({})
provide(AllFiltersInj, allFilters) provide(AllFiltersInj, allFilters)
const fieldAlias = computed(() => {
return localColumns.value.reduce((acc, field) => {
if (field?.fk_column_id && field?.label?.trim()) {
acc[field.fk_column_id] = field.label
}
return acc
}, {} as Record<string, string>)
})
provide(FieldNameAlias, fieldAlias)
const visibilityError = computed(() => { const visibilityError = computed(() => {
return parseProp(activeField.value?.meta)?.visibility?.errors || {} return parseProp(activeField.value?.meta)?.visibility?.errors || {}
}) })

4
packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue

@ -17,6 +17,8 @@ const customColumns = toRef(restProps, 'columns')
const meta = toRef(restProps, 'meta') const meta = toRef(restProps, 'meta')
const fieldNameAlias = inject(FieldNameAlias, ref({} as Record<string, string>))
const { metas } = useMetas() const { metas } = useMetas()
const localValue = computed({ const localValue = computed({
@ -110,7 +112,7 @@ const options = computed<SelectProps['options']>(() =>
}) })
?.map((c: ColumnType) => ({ ?.map((c: ColumnType) => ({
value: c.id, value: c.id,
label: c.title, label: fieldNameAlias.value[c.id!] || c.title,
icon: h( icon: h(
isVirtualCol(c) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), isVirtualCol(c) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'),
{ {

6
packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue

@ -197,6 +197,10 @@ const isInputBoxOnFocus = ref(false)
// provide the following to override the default behavior and enable input fields like in form // provide the following to override the default behavior and enable input fields like in form
provide(ActiveCellInj, ref(true)) provide(ActiveCellInj, ref(true))
provide(IsFormInj, ref(true)) provide(IsFormInj, ref(true))
const isSingleOrMultiSelect = computed(() => {
return filterType.value === 'isSingleSelect' || filterType.value === 'isMultiSelect'
})
</script> </script>
<template> <template>
@ -209,7 +213,7 @@ provide(IsFormInj, ref(true))
<div <div
v-else v-else
class="bg-white border-1 flex flex-grow min-h-4 h-full px-1 items-center nc-filter-input-wrapper !rounded-lg" class="bg-white border-1 flex flex-grow min-h-4 h-full px-1 items-center nc-filter-input-wrapper !rounded-lg"
:class="{ 'px-2': hasExtraPadding, 'border-brand-500': isInputBoxOnFocus }" :class="{ 'px-2': hasExtraPadding, 'border-brand-500': isInputBoxOnFocus, '!max-w-100': isSingleOrMultiSelect }"
@mouseup.stop @mouseup.stop
> >
<component <component

4
packages/nc-gui/components/smartsheet/toolbar/OpenedViewAction.vue

@ -5,6 +5,8 @@ const { isMobileMode } = useGlobal()
const { isSharedBase, base } = storeToRefs(useBase()) const { isSharedBase, base } = storeToRefs(useBase())
const { sharedView } = useSharedView()
const { t } = useI18n() const { t } = useI18n()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
@ -188,7 +190,7 @@ function openDeleteDialog() {
</NcTooltip> </NcTooltip>
</div> </div>
<NcDropdown <NcDropdown
v-else v-else-if="!sharedView"
v-model:visible="isDropdownOpen" v-model:visible="isDropdownOpen"
class="!xs:pointer-events-none nc-actions-menu-btn nc-view-context-btn" class="!xs:pointer-events-none nc-actions-menu-btn nc-view-context-btn"
overlay-class-name="nc-dropdown-actions-menu" overlay-class-name="nc-dropdown-actions-menu"

7
packages/nc-gui/composables/useKanbanViewStore.ts

@ -220,6 +220,10 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { collapsed, ...rest } = stackMetaObj.value[fk_grp_col_id][idx] const { collapsed, ...rest } = stackMetaObj.value[fk_grp_col_id][idx]
if (!deepCompare(rest, option)) { if (!deepCompare(rest, option)) {
// Don't update stack meta if it is shared view and
// shared view meta grouping field options not matched with actual column options
if (isPublic.value) continue
// update the option in stackMetaObj // update the option in stackMetaObj
stackMetaObj.value[fk_grp_col_id][idx] = { stackMetaObj.value[fk_grp_col_id][idx] = {
...stackMetaObj.value[fk_grp_col_id][idx], ...stackMetaObj.value[fk_grp_col_id][idx],
@ -482,7 +486,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
// update to groupingField value to target value // update to groupingField value to target value
formattedData.value.set( formattedData.value.set(
stackTitle, stackTitle,
formattedData.value.get(stackTitle)!.map((o) => ({ (formattedData.value.get(stackTitle) || []).map((o) => ({
...o, ...o,
row: { row: {
...o.row, ...o.row,
@ -624,6 +628,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
} }
function removeRowFromUncategorizedStack() { function removeRowFromUncategorizedStack() {
if (isPublic.value) return
// remove the last record // remove the last record
formattedData.value.get(null)!.pop() formattedData.value.get(null)!.pop()
// decrease total count by 1 // decrease total count by 1

15
packages/nc-gui/composables/useProductFeed.ts

@ -14,13 +14,16 @@ export const useProductFeed = createSharedComposable(() => {
const socialFeed = ref<ProductFeedItem[]>([]) const socialFeed = ref<ProductFeedItem[]>([])
const cloudFeed = ref<ProductFeedItem[]>([])
const isErrorOccurred = reactive({ const isErrorOccurred = reactive({
youtube: false, youtube: false,
github: false, github: false,
social: false, social: false,
cloud: false,
}) })
const loadFeed = async ({ loadMore, type }: { loadMore: boolean; type: 'youtube' | 'github' | 'all' }) => { const loadFeed = async ({ loadMore, type }: { loadMore: boolean; type: 'youtube' | 'github' | 'all' | 'cloud' }) => {
try { try {
let page = 1 let page = 1
@ -35,6 +38,9 @@ export const useProductFeed = createSharedComposable(() => {
case 'all': case 'all':
page = Math.ceil(socialFeed.value.length / 10) + 1 page = Math.ceil(socialFeed.value.length / 10) + 1
break break
case 'cloud':
page = Math.ceil(cloudFeed.value.length / 10) + 1
break
} }
} }
@ -50,6 +56,9 @@ export const useProductFeed = createSharedComposable(() => {
case 'all': case 'all':
socialFeed.value = [...socialFeed.value, ...response] as ProductFeedItem[] socialFeed.value = [...socialFeed.value, ...response] as ProductFeedItem[]
break break
case 'cloud':
cloudFeed.value = [...cloudFeed.value, ...response] as ProductFeedItem[]
break
} }
} catch (error) { } catch (error) {
switch (type) { switch (type) {
@ -62,6 +71,9 @@ export const useProductFeed = createSharedComposable(() => {
case 'all': case 'all':
isErrorOccurred.social = true isErrorOccurred.social = true
break break
case 'cloud':
isErrorOccurred.cloud = true
break
} }
console.error(error) console.error(error)
return [] return []
@ -113,6 +125,7 @@ export const useProductFeed = createSharedComposable(() => {
youtubeFeed, youtubeFeed,
githubFeed, githubFeed,
socialFeed, socialFeed,
cloudFeed,
loadFeed, loadFeed,
isNewFeedAvailable, isNewFeedAvailable,
} }

153
packages/nc-gui/composables/useSharedExecutionFn.ts

@ -0,0 +1,153 @@
import { useStorage, useTimeoutFn } from '@vueuse/core'
interface SharedExecutionOptions {
timeout?: number // Maximum time a lock can be held before it's considered stale - default 5000ms
storageDelay?: number // Delay before reading from storage to allow for changes to propagate - default 50ms
debug?: boolean // Enable or disable debug logging
}
const tabId = `tab-${Math.random().toString(36).slice(2, 9)}`
/**
* Creates a composable that ensures a function is executed only once across all tabs
* @param key Unique key to identify the function
* @param fn Function to be executed
* @param options Optional configuration (timeout, storageDelay)
* @returns A wrapped function that ensures single execution across tabs
*/
export function useSharedExecutionFn<T>(key: string, fn: () => Promise<T> | T, options: SharedExecutionOptions = {}) {
const { timeout = 5000, storageDelay = 50, debug = false } = options
const storageResultKey = `nc-shared-execution-${key}-result`
const storageLockKey = `nc-shared-execution-${key}-lock`
const storageResultState = useStorage<{
status?: 'success' | 'error'
result?: T
error?: any
}>(storageResultKey, {})
const debugLog = (...args: any[]) => {
if (debug) console.log(`[${tabId}]`, ...args)
}
debugLog(`Tab initialized with ID: ${tabId}`)
const getLock = (): { timestamp: number; tabId: string } | null => {
try {
return JSON.parse(localStorage.getItem(storageLockKey) || 'null')
} catch (error) {
debugLog(`Error reading lock:`, error)
return null
}
}
const acquireLock = async (): Promise<boolean> => {
let currentLock = getLock()
const now = Date.now()
if (!currentLock) {
localStorage.setItem(storageLockKey, JSON.stringify({ timestamp: now, tabId }))
// Allow storage updates to propagate - which will determine strictness of lock
await new Promise((resolve) => setTimeout(resolve, storageDelay))
currentLock = getLock()
if (currentLock?.tabId === tabId) {
debugLog(`Lock acquired successfully`)
return true
}
debugLog(`Lock acquired by ${currentLock?.tabId}`)
return false
}
const lockIsStale = now - currentLock.timestamp > timeout
if (lockIsStale) {
localStorage.setItem(storageLockKey, JSON.stringify({ timestamp: now, tabId }))
// Allow storage updates to propagate - which will determine strictness of lock
await new Promise((resolve) => setTimeout(resolve, storageDelay))
currentLock = getLock()
if (currentLock?.tabId === tabId) {
debugLog(`Stale lock acquired successfully`)
return true
}
debugLog(`Stale lock acquired by ${currentLock?.tabId}`)
return false
}
debugLog(`Lock is held by ${currentLock?.tabId}`)
return false
}
const releaseLock = (): void => {
const currentLock = getLock()
if (currentLock?.tabId === tabId) {
debugLog(`Releasing lock.`)
localStorage.removeItem(storageLockKey)
}
}
const sharedExecutionFn = async (): Promise<T> => {
debugLog(`sharedExecutionFn called`)
if (!(await acquireLock())) {
const currentLock = getLock()
return new Promise((resolve, reject) => {
let timedOut = false
const { start: startTimeout, stop: stopTimeout } = useTimeoutFn(
() => {
timedOut = true
localStorage.removeItem(storageLockKey)
reject(new Error(`Timeout waiting for result on key ${key}`))
},
currentLock?.timestamp ? timeout - (Date.now() - currentLock.timestamp) : timeout,
)
startTimeout()
if (storageResultState.value.status) {
storageResultState.value = { ...storageResultState.value, status: undefined }
}
until(() => storageResultState.value)
.toMatch((v) => v.status === 'success' || v.status === 'error')
.then((res) => {
if (timedOut) return
stopTimeout()
const { result, error } = res
result ? resolve(result) : reject(error)
})
})
}
try {
storageResultState.value = { ...storageResultState.value, status: undefined }
const result = await fn()
storageResultState.value = { status: 'success', result }
return result
} catch (error) {
storageResultState.value = { status: 'error', error }
throw error
} finally {
releaseLock()
debugLog(`Function execution completed (success or failure).`)
}
}
// Make sure to release lock on page unload
onBeforeMount(() => {
window.addEventListener('beforeunload', releaseLock)
})
// Remove listener on component unmount to avoid leaks
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', releaseLock)
})
return sharedExecutionFn
}

2
packages/nc-gui/context/index.ts

@ -87,3 +87,5 @@ export const ActiveSourceInj: InjectionKey<
> = Symbol('active-source-injection') > = Symbol('active-source-injection')
export const IsToolbarIconMode: InjectionKey<ComputedRef<boolean>> = Symbol('toolbar-icon-mode-injection') export const IsToolbarIconMode: InjectionKey<ComputedRef<boolean>> = Symbol('toolbar-icon-mode-injection')
export const FieldNameAlias: InjectionKey<ComputedRef<Record<string, string>> | Ref<Record<string, string>>> =
Symbol('field-name-alias')

23
packages/nc-gui/helpers/columnDefaultMeta.ts

@ -2,29 +2,6 @@ import { UITypes, dateFormats, timeFormats } from 'nocodb-sdk'
export const precisionFormats = [1, 2, 3, 4, 5, 6, 7, 8] export const precisionFormats = [1, 2, 3, 4, 5, 6, 7, 8]
export const iconList = [
{
full: 'mdi-star',
empty: 'mdi-star-outline',
},
{
full: 'mdi-heart',
empty: 'mdi-heart-outline',
},
{
full: 'mdi-moon-full',
empty: 'mdi-moon-new',
},
{
full: 'mdi-thumb-up',
empty: 'mdi-thumb-up-outline',
},
{
full: 'mdi-flag',
empty: 'mdi-flag-outline',
},
]
export const supportedBarcodeFormats = [ export const supportedBarcodeFormats = [
{ value: 'CODE128', label: 'CODE128' }, { value: 'CODE128', label: 'CODE128' },
{ value: 'upc', label: 'UPC' }, { value: 'upc', label: 'UPC' },

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

@ -39,67 +39,67 @@
} }
}, },
"aggregation": { "aggregation": {
"sum": "", "sum": "Sum",
"count": "Cuenta", "count": "Cuenta",
"min": "Min", "min": "Mín",
"max": "Máximo", "max": "Máx.",
"avg": "Media", "avg": "Prom",
"median": "Mediana", "median": "Mediana",
"std_dev": "Desviación estándar", "std_dev": "Desviación estándar",
"histogram": "Histograma", "histogram": "Histograma",
"range": "Rango", "range": "Rango",
"percent_empty": "Vacío", "percent_empty": "Vacío",
"percent_filled": "Lleno", "percent_filled": "Rellenado",
"percent_unique": "Único", "percent_unique": "Unico",
"count_unique": "Único", "count_unique": "Unico",
"count_empty": "Vacío", "count_empty": "Vacío",
"count_filled": "Lleno", "count_filled": "Rellenado",
"earliest_date": "Fecha mínima", "earliest_date": "Fecha mínima",
"latest_date": "Fecha máxima", "latest_date": "Fecha máxima",
"date_range": "Rango de fechas", "date_range": "Rango",
"month_range": "Rango de meses", "month_range": "Rango",
"checked": "Marcado", "checked": "Comprobado",
"unchecked": "Desmarcado", "unchecked": "No seleccionado",
"percent_checked": "Marcado", "percent_checked": "Comprobado",
"percent_unchecked": "Desmarcado", "percent_unchecked": "No seleccionado",
"attachment_size": "Tamaño", "attachment_size": "Tamaño",
"none": "Ninguno" "none": "Nada"
}, },
"aggregation_type": { "aggregation_type": {
"sum": "Suma", "sum": "Suma",
"count": "Cuenta", "count": "Cuenta",
"min": "Min", "min": "Mín.",
"max": "Máximo", "max": "Máx.",
"avg": "Media", "avg": "Promedio",
"median": "Mediana", "median": "Mediana",
"std_dev": "Desviación estándar", "std_dev": "Desviación estándar",
"histogram": "Histograma", "histogram": "Histograma",
"range": "Rango", "range": "Rango",
"percent_empty": "Porcentaje vacío", "percent_empty": "Porcentaje vacío",
"percent_filled": "Porcentaje lleno", "percent_filled": "Porcentaje cubierto",
"percent_unique": "Porcentaje único", "percent_unique": "Porcentaje único",
"count_unique": "Único", "count_unique": "Unico",
"count_empty": "Vacío", "count_empty": "Vacío",
"count_filled": "Lleno", "count_filled": "Rellenado",
"earliest_date": "Fecha más temprana", "earliest_date": "Fecha más temprana",
"latest_date": "Fecha más reciente", "latest_date": "Última Fecha",
"date_range": "Rango de fechas", "date_range": "Rango de fechas",
"month_range": "Rango de meses", "month_range": "Rango de mes",
"checked": "Marcado", "checked": "Comprobado",
"unchecked": "Desmarcado", "unchecked": "No seleccionado",
"percent_checked": "Porcentaje Marcado", "percent_checked": "Porcentaje comprobado",
"percent_unchecked": "Porcentaje Desmarcado", "percent_unchecked": "Porcentaje Sin comprobar",
"attachment_size": "Tamaño del Adjunto", "attachment_size": "Tamaño del archivo adjunto",
"none": "Ninguno" "none": "Ninguno"
}, },
"general": { "general": {
"scripts": "Comandos", "scripts": "Comandos",
"configure": "Configurar", "configure": "Configurar",
"switch": "Cambiar", "switch": "Interruptor",
"on": "Encendido", "on": "Activado",
"onMultiple": "En Múltiple", "onMultiple": "En múltiples",
"manual": "Manual", "manual": "Manual",
"trigger": "Activar", "trigger": "Disparador",
"addLookupField": "Add {count} lookup fields", "addLookupField": "Add {count} lookup fields",
"style": "Estilo", "style": "Estilo",
"label": "Etiqueta", "label": "Etiqueta",
@ -152,7 +152,7 @@
"code": "Código", "code": "Código",
"duplicate": "Duplicar", "duplicate": "Duplicar",
"duplicating": "Duplicando", "duplicating": "Duplicando",
"duplicateEntity": "Duplicar {entity}", "duplicateEntity": "Duplicado {entity}",
"activate": "Activar", "activate": "Activar",
"action": "Acción", "action": "Acción",
"insert": "Insertar", "insert": "Insertar",
@ -167,7 +167,7 @@
"rename": "Renombrar", "rename": "Renombrar",
"renameEntity": "Renombrar {entity}", "renameEntity": "Renombrar {entity}",
"reload": "Recargar", "reload": "Recargar",
"refresh": "Refrescar", "refresh": "Actualizar",
"reset": "Reiniciar", "reset": "Reiniciar",
"install": "Instalar", "install": "Instalar",
"show": "Mostrar", "show": "Mostrar",
@ -225,7 +225,7 @@
"betaNote": "Esta función se encuentra actualmente en fase beta.", "betaNote": "Esta función se encuentra actualmente en fase beta.",
"moreInfo": "Puede encontrar más información aquí", "moreInfo": "Puede encontrar más información aquí",
"logs": "Registros", "logs": "Registros",
"groupingField": "Agrupar por campo", "groupingField": "Apilar por campo",
"insertAfter": "Insertar a la derecha", "insertAfter": "Insertar a la derecha",
"insertBefore": "Insertar a la izquierda", "insertBefore": "Insertar a la izquierda",
"insertAbove": "Insertar arriba", "insertAbove": "Insertar arriba",
@ -237,7 +237,7 @@
"move": "Mover", "move": "Mover",
"geoDataField": "Campo GeoDatos", "geoDataField": "Campo GeoDatos",
"type": "Tipo", "type": "Tipo",
"subType": "Subtipo", "subType": "Sub-tipo",
"name": "Nombre", "name": "Nombre",
"changes": "Cambios", "changes": "Cambios",
"new": "Nuevo", "new": "Nuevo",
@ -277,10 +277,10 @@
"appearance": "Apariencia", "appearance": "Apariencia",
"now": "Ahora", "now": "Ahora",
"set": "Establecer", "set": "Establecer",
"format": "Formato", "format": "Plantilla",
"colour": "Color", "colour": "Color",
"use": "Usar", "use": "Usar",
"stack": "Pila", "stack": "Apilado",
"ipAddress": "Dirección IP", "ipAddress": "Dirección IP",
"integration": "Integración", "integration": "Integración",
"integrations": "Integraciones", "integrations": "Integraciones",
@ -364,8 +364,8 @@
"googleCalendar": "Google Calendar", "googleCalendar": "Google Calendar",
"googleDrive": "Google Drive", "googleDrive": "Google Drive",
"googleSheets": "Google Sheets", "googleSheets": "Google Sheets",
"hubspot": "Hubspot", "hubspot": "HubSpot",
"serviceHub": "Centro de Servicio", "serviceHub": "Centro de servicios",
"jira": "Jira", "jira": "Jira",
"mailchimp": "Mailchimp", "mailchimp": "Mailchimp",
"microsoftAccess": "Microsoft Access", "microsoftAccess": "Microsoft Access",
@ -373,7 +373,7 @@
"microsoftOutlook": "Microsoft Outlook", "microsoftOutlook": "Microsoft Outlook",
"miro": "Miro", "miro": "Miro",
"salesforce": "Salesforce", "salesforce": "Salesforce",
"serviceCloud": "Nube de Servicio", "serviceCloud": "Servicios en la Nube",
"snowflake": "Snowflake", "snowflake": "Snowflake",
"stripe": "Stripe", "stripe": "Stripe",
"surveyMonkey": "SurveyMonkey", "surveyMonkey": "SurveyMonkey",
@ -386,10 +386,10 @@
"postgreSQL": "PostgreSQL", "postgreSQL": "PostgreSQL",
"sqlite": "SQLite", "sqlite": "SQLite",
"dataBricks": "DataBricks", "dataBricks": "DataBricks",
"mssqlServer": "Servidor MSSQL", "mssqlServer": "MSSQL Server",
"oracle": "Oracle", "oracle": "Oracle",
"telegram": "Telegram", "telegram": "Telegram",
"whatsapp": "Whatsapp", "whatsapp": "WhatsApp",
"gmail": "Gmail", "gmail": "Gmail",
"pipedrive": "Pipedrive", "pipedrive": "Pipedrive",
"microsoftDynamics365": "Microsoft Dynamics 365", "microsoftDynamics365": "Microsoft Dynamics 365",
@ -397,7 +397,7 @@
"greenhouse": "Greenhouse", "greenhouse": "Greenhouse",
"lever": "Lever", "lever": "Lever",
"bitbucket": "BitBucket", "bitbucket": "BitBucket",
"quickbooks": "Quickbooks", "quickbooks": "QuickBooks",
"intercom": "Intercom", "intercom": "Intercom",
"dropbox": "Dropbox", "dropbox": "Dropbox",
"openai": "OpenAI", "openai": "OpenAI",
@ -411,24 +411,24 @@
"allIntegrationsSubtitle": "", "allIntegrationsSubtitle": "",
"databaseSubtitle": "Conecte y gestione sin problemas sus bases de datos con NocoDB.", "databaseSubtitle": "Conecte y gestione sin problemas sus bases de datos con NocoDB.",
"communication": "Comunicación", "communication": "Comunicación",
"communicationSubtitle": "Reciba notificaciones de cambios y agilice la comunicación del equipo con NocoDB.", "communicationSubtitle": "Reciba notificaciones sobre los cambios y agilice la comunicación del equipo con NocoDB.",
"projectManagement": "Gestión de proyectos", "projectManagement": "Administración de proyectos",
"projectManagementSubtitle": "Mejore los flujos de trabajo de proyectos y la gestión de tareas con NocoDB.", "projectManagementSubtitle": "Mejore los flujos de trabajo de los proyectos y la gestión de tareas con NocoDB.",
"crm": "CRM", "crm": "CRM",
"crmSubtitle": "Optimice la gestión de relaciones con clientes a través de integraciones con NocoDB.", "crmSubtitle": "Optimice la gestión de las relaciones con los clientes mediante las integraciones de NocoDB.",
"marketing": "Marketing", "marketing": "Marketing",
"marketingSubtitle": "Impulse sus esfuerzos de marketing con las potentes integraciones de NocoDB.", "marketingSubtitle": "Impulse sus esfuerzos de marketing con las potentes integraciones de NocoDB.",
"ats": "ATS", "ats": "ATS",
"atsSubtitle": "Optimice su sistema de seguimiento de candidatos con NocoDB.", "atsSubtitle": "Agilice su sistema de seguimiento de candidatos con NocoDB.",
"development": "Desarrollo", "development": "Desarrollo",
"developmentSubtitle": "Acelere los procesos de desarrollo con integraciones de NocoDB.", "developmentSubtitle": "Acelere los procesos de desarrollo con las integraciones de NocoDB.",
"finance": "Finanzas", "finance": "Finanzas",
"financeSubtitle": "Simplifique las operaciones financieras y la gestión de datos con NocoDB.", "financeSubtitle": "Simplifique las operaciones financieras y la gestión de datos con NocoDB.",
"ticketing": "Ticketing", "ticketing": "Sistema de tickets",
"ticketingSubtitle": "Gestione y rastree tickets de soporte de manera eficiente con NocoDB.", "ticketingSubtitle": "Gestione y haga un seguimiento eficiente de los tickets de soporte con NocoDB.",
"storageSubtitle": "Integre y organice sus soluciones de almacenamiento sin problemas con NocoDB.", "storageSubtitle": "Integre y organice sus soluciones de almacenamiento sin problemas con NocoDB.",
"others": "Otros", "others": "Otros",
"othersSubtitle": "Descubra integraciones adicionales versátiles para mejorar su experiencia con NocoDB.", "othersSubtitle": "Descubra otras integraciones versátiles para mejorar su experiencia con NocoDB.",
"ai": "IA", "ai": "IA",
"spreadSheet": "Hoja de cálculo", "spreadSheet": "Hoja de cálculo",
"spreadSheetSubtitle": "Conecte y gestione sus hojas de cálculo con NocoDB." "spreadSheetSubtitle": "Conecte y gestione sus hojas de cálculo con NocoDB."
@ -488,13 +488,13 @@
}, },
"title": { "title": {
"searchWebhook": "Buscar webhook", "searchWebhook": "Buscar webhook",
"webcam": "Cámara Web", "webcam": "Cámara web",
"uploadViaUrl": "Subir a través de URL", "uploadViaUrl": "Subir por URL",
"localFiles": "Local Files", "localFiles": "Local Files",
"renameBase": "Renombrar Base", "renameBase": "Renombrar base",
"renameWorkspace": "Renombrar Espacio de Trabajo", "renameWorkspace": "Renombrar Espacio de trabajo",
"renamingWorkspace": "Renombrando Espacio de Trabajo", "renamingWorkspace": "Renombrando el espacio de trabajo",
"renamingBase": "Renombrando Base", "renamingBase": "Renombrando la base",
"sso": "Autenticación (SSO)", "sso": "Autenticación (SSO)",
"docs": "Documentos", "docs": "Documentos",
"forum": "Foro", "forum": "Foro",
@ -523,7 +523,7 @@
"removeFile": "Eliminar archivo", "removeFile": "Eliminar archivo",
"hasMany": "Tiene muchos", "hasMany": "Tiene muchos",
"manyToMany": "Muchos a Muchos", "manyToMany": "Muchos a Muchos",
"oneToOne": "Uno a Uno", "oneToOne": "Uno a uno",
"virtualRelation": "Relación virtual", "virtualRelation": "Relación virtual",
"linkMore": "Enlace más", "linkMore": "Enlace más",
"linkMoreRecords": "Vincular más registros", "linkMoreRecords": "Vincular más registros",
@ -617,7 +617,7 @@
"noOptionsFound": "No se encontraron opciones", "noOptionsFound": "No se encontraron opciones",
"surveyFormSubmitConfirmMsg": "¿Está seguro de que desea enviar este formulario?", "surveyFormSubmitConfirmMsg": "¿Está seguro de que desea enviar este formulario?",
"noResultsMatchedYourSearch": "Your search did not yield any matching results.", "noResultsMatchedYourSearch": "Your search did not yield any matching results.",
"looksLikeThisStackIsEmpty": "Parece que esta pila no tiene registros", "looksLikeThisStackIsEmpty": "Parece que esta pila no tiene ningún registro",
"fromScratch": "Desde cero", "fromScratch": "Desde cero",
"fromFileAndExternalSources": "Desde archivos y fuentes externas", "fromFileAndExternalSources": "Desde archivos y fuentes externas",
"directlyInRealTime": "Directamente en tiempo real", "directlyInRealTime": "Directamente en tiempo real",
@ -630,12 +630,12 @@
"configuration": "Configuración", "configuration": "Configuración",
"setup": "Configuración", "setup": "Configuración",
"configLabel": "Configurar {label}", "configLabel": "Configurar {label}",
"switchToProd": "Cambiar a una base de datos lista para producción", "switchToProd": "Cambie a una aplicación base de datos lista para producción",
"sharedBase": "Shared Base", "sharedBase": "Base compartida",
"fieldID": "ID de Campo", "fieldID": "ID de campo",
"addDescription": "Agregar descripción", "addDescription": "Añadir descripción",
"editDescription": "Editar descripción", "editDescription": "Editar descripción",
"urlFormula": "Fórmula de URL", "urlFormula": "Fórmula URL",
"selectIcon": "ninguno", "selectIcon": "ninguno",
"selectAWebhook": "--seleccionar un webhook--", "selectAWebhook": "--seleccionar un webhook--",
"openUrl": "Abrir URL", "openUrl": "Abrir URL",
@ -644,27 +644,27 @@
"defaultView": "Vista por defecto", "defaultView": "Vista por defecto",
"recordInsert": "Insertar registro", "recordInsert": "Insertar registro",
"recordUpdate": "Actualizar registro", "recordUpdate": "Actualizar registro",
"recordDelete": "Eliminar registro", "recordDelete": "Borrar registro",
"supportDocs": "Docs de soporte", "supportDocs": "Documentos de apoyo",
"addedOn": "Añadido el", "addedOn": "Añadido él",
"changeDisplayValueField": "Cambiar campo de valor de visualización", "changeDisplayValueField": "Cambiar el valor de campo a visualizar",
"selectYourNewTitleFor": "Select your new display value field for ", "selectYourNewTitleFor": "Select your new display value field for ",
"searchDisplayValue": "Seleccionar campo de valor de visualización", "searchDisplayValue": "Seleccione el valor de campo a visualizar",
"changeTitleField": "Cambiar campo de título", "changeTitleField": "Cambiar el título del campo",
"clearAll": "Borrar todo", "clearAll": "Limpiar todo",
"addNewLookupHelperText1": "Los campos de búsqueda muestran datos de registros vinculados. Seleccione campos específicos de", "addNewLookupHelperText1": "Los campos de búsqueda muestran los datos de los registros vinculados. Seleccione campos específicos de ",
"addNewLookupHelperText2": " tabla para añadirlos como campos de búsqueda en esta tabla.", "addNewLookupHelperText2": "Para agregarlos como campos de Búsqueda en esta tabla.",
"formatting": "Formato", "formatting": "Formateando",
"selectAFormatType": "- -Seleccionar un tipo de formato (opcional)- -", "selectAFormatType": "-Seleccione un tipo de formulario (opcional)- -",
"formatType": "Tipo de formato", "formatType": "Tipo de formato",
"toUpload": "para subir", "toUpload": "Para cargar",
"dragFilesHere": "arrastra los archivos aquí", "dragFilesHere": "Arrastre los archivos aquí",
"browseFiles": "examinar archivos", "browseFiles": "Examinar Archivos",
"clickTo": "Hacer clic para", "clickTo": "Haga clic para",
"allowAccessToYourCamera": "Por favor permite el acceso a tu cámara", "allowAccessToYourCamera": "Por favor, permita el acceso a su cámara",
"openFile": "Abrir archivo", "openFile": "Abrir archivo",
"enterValidUrl": "Introduce una URL válida para subir archivos", "enterValidUrl": "Introduzca una URL válida para cargar archivos",
"addFilesFromUrl": "Agregar archivos desde URL", "addFilesFromUrl": "Añadir archivos desde URL",
"uploading": "Subiendo", "uploading": "Subiendo",
"dropHere": "Soltar aquí", "dropHere": "Soltar aquí",
"addMore": "Añadir más", "addMore": "Añadir más",
@ -674,46 +674,46 @@
"allowMetaWrite": "Allow Schema Edit", "allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit", "allowDataWrite": "Allow Data Edit",
"selectView": "Seleccionar una vista", "selectView": "Seleccionar una vista",
"connectionDetails": "Detalles de conexión de la fuente", "connectionDetails": "Detalles de la conexión origen",
"metaSync": "Meta Sync", "metaSync": "Meta Sync",
"mention": "Mención", "mention": "Mención",
"today": "Hoy", "today": "Hoy",
"currentDate": "Fecha actual", "currentDate": "Fecha actual",
"workspace": "Espacio de trabajo", "workspace": "Espacio de trabajo",
"txt": "Valor de registro TXT", "txt": "Valor del registro TXT",
"transferOwnership": "Transferir Propiedad", "transferOwnership": "Transferir la Propiedad",
"recentActivity": "Actividad Reciente", "recentActivity": "Actividad reciente",
"goToMembers": "Ir a los Miembros", "goToMembers": "Ir a Miembros",
"addMember": "Añadir Miembro", "addMember": "Añadir miembro",
"numberOfMembers": "N.º de Miembros", "numberOfMembers": "No. Miembros",
"numberOfBases": "N.º de Bases", "numberOfBases": "No. Bases",
"numberOfRecords": "N.º de Registros", "numberOfRecords": "No. Registros",
"workspaceName": "Nombre del Espacio de Trabajo", "workspaceName": "Nombre del espacio de trabajo",
"workspaceWithoutOwner": "Espacio de Trabajo sin Propietarios", "workspaceWithoutOwner": "Espacio de trabajo sin propietarios",
"inviteUsersToWorkspace": "Invitar Usuarios al Espacio de Trabajo", "inviteUsersToWorkspace": "Invitar usuarios al espacio de trabajo",
"selectWorkspace": "-seleccionar espacios de trabajo a los que invitar-", "selectWorkspace": "-seleccionar espacios de trabajo a los que invitar-",
"addMembersToOrganization": "Añadir Miembros a la Organización", "addMembersToOrganization": "Añadir Miembros a la Organización",
"memberIn": "Miembro en:", "memberIn": "Miembro en:",
"assignAs": "Asignar como", "assignAs": "Asignar como",
"signOutUser": "Cerrar sesión del usuario", "signOutUser": "Cerrar sesión de usuario",
"signOutUsers": "Cerrar sesión de usuarios", "signOutUsers": "Cerrar sesión de usuarios",
"deactivateUser": "Desactivar usuario", "deactivateUser": "Desactivar usuario",
"deactivateUsers": "Desactivar usuarios", "deactivateUsers": "Desactivar usuarios",
"lastActive": "Último activo", "lastActive": "Última vez activo",
"dateAdded": "Fecha de incorporación", "dateAdded": "Fecha creación",
"uploadImage": "Subir imagen", "uploadImage": "Subir imagen",
"organizationProfile": "Perfil de la organización", "organizationProfile": "Perfil de la organización",
"organizationImage": "Imagen de la organización", "organizationImage": "Imagen de la organización",
"organizationName": "Nombre de la organización", "organizationName": "Nombre de la organización",
"activeDomains": "Dominios activos", "activeDomains": "Dominios activos",
"domains": "Dominios", "domains": "Dominios",
"disablePublicSharing": "Deshabilitar compartición pública", "disablePublicSharing": "Desactivar Compartir Público",
"shareSettings": "Configuración de compartir", "shareSettings": "Compartir ajustes",
"deleteUserAndData": "Eliminar usuario y sus datos", "deleteUserAndData": "Eliminar usuario y sus datos",
"userOptions": "Opciones de usuario", "userOptions": "Opciones de usuario",
"deleteThisOrganization": "Eliminar esta organización", "deleteThisOrganization": "Eliminar esta Organización",
"dangerZone": "Zona de peligro", "dangerZone": "Zona peligrosa",
"childView": "Vista hija", "childView": "Vista Hijo",
"selectYear": "Seleccionar Año", "selectYear": "Seleccionar Año",
"save": "Guardar", "save": "Guardar",
"cancel": "Cancelar", "cancel": "Cancelar",
@ -725,14 +725,14 @@
"newProvider": "Nuevo proveedor", "newProvider": "Nuevo proveedor",
"generalSettings": "Ajustes Generales", "generalSettings": "Ajustes Generales",
"adminPanel": "Panel de administración", "adminPanel": "Panel de administración",
"moveWorkspaceToOrg": "Mover espacio de trabajo a la organización", "moveWorkspaceToOrg": "Mover espacio de trabajo a organización",
"ssoSettings": "Ajustes SSO", "ssoSettings": "Ajustes SSO",
"addDomain": "Añadir dominio", "addDomain": "Agregar dominio",
"domain": "Dominio", "domain": "Dominio",
"settings": "Configuración", "settings": "Ajustes",
"workspaces": "Espacios de trabajo", "workspaces": "Espacios de trabajo",
"back": "Volver", "back": "Volver",
"dashboard": "Panel de control", "dashboard": "Dashboard",
"organizeBy": "Organizar por", "organizeBy": "Organizar por",
"previous": "Anterior", "previous": "Anterior",
"nextMonth": "Mes siguiente", "nextMonth": "Mes siguiente",
@ -754,12 +754,12 @@
"noToken": "Sin Token", "noToken": "Sin Token",
"tokenLimit": "Sólo se permite un token por usuario", "tokenLimit": "Sólo se permite un token por usuario",
"duplicateAttachment": "Archivo con el nombre {filename} ya adjuntado", "duplicateAttachment": "Archivo con el nombre {filename} ya adjuntado",
"tableIdColon": "ID DE TABLA: {tableId}", "tableIdColon": "ID de Tabla: {tableId}",
"viewIdColon": "VIEW ID: {viewId}", "viewIdColon": "VIEW ID: {viewId}",
"toAddress": "A Dirección", "toAddress": "A Dirección",
"subject": "Asunto", "subject": "Asunto",
"body": "Cuerpo", "body": "Cuerpo",
"commaSeparatedMobileNumber": "Número móvil separado por comas", "commaSeparatedMobileNumber": "Número Celular separado por Comas",
"headerName": "Nombre del encabezado", "headerName": "Nombre del encabezado",
"icon": "Icono", "icon": "Icono",
"max": "Máximo", "max": "Máximo",
@ -788,12 +788,12 @@
"optional": "(Opcional)", "optional": "(Opcional)",
"clickToMake": "Pulsar para hacer", "clickToMake": "Pulsar para hacer",
"visibleForRole": "visible para el rol:", "visibleForRole": "visible para el rol:",
"inUI": "en el panel de control de la interfaz de usuario", "inUI": "En él UI del Dashboard",
"projectSettings": "Ajustes básicos", "projectSettings": "Ajustes básicos",
"clickToHide": "Clic para ocultar", "clickToHide": "Clic para ocultar",
"clickToDownload": "Clic para descargar", "clickToDownload": "Clic para descargar",
"forRole": "para el rol", "forRole": "para el rol",
"clickToCopyTableID": "Clic para copiar ID de Tabla", "clickToCopyTableID": "Haga clic para copiar el ID de la tabla",
"clickToCopyViewID": "Clic para copiar View ID", "clickToCopyViewID": "Clic para copiar View ID",
"viewMode": "Modo solo lectura", "viewMode": "Modo solo lectura",
"searchUsers": "Buscar usuarios", "searchUsers": "Buscar usuarios",
@ -840,9 +840,9 @@
"databaseType": "Tipo en base de datos", "databaseType": "Tipo en base de datos",
"lengthValue": "Longitud/valor", "lengthValue": "Longitud/valor",
"dbType": "Tipo de Base de Datos", "dbType": "Tipo de Base de Datos",
"servername": "nombre del servidor / dirección del host", "servername": "Nombre del Servidor / Dirección del Host",
"sqliteFile": "Ruta del archivo SQLite", "sqliteFile": "Ruta del archivo SQLite",
"hostAddress": "Dirección del servidor", "hostAddress": "Dirección del host",
"port": "Número de puerto", "port": "Número de puerto",
"username": "Usuario", "username": "Usuario",
"password": "Contraseña", "password": "Contraseña",
@ -947,7 +947,7 @@
"hasMany": "tiene muchos", "hasMany": "tiene muchos",
"belongsTo": "pertenece a", "belongsTo": "pertenece a",
"manyToMany": "tienen una relación de muchos a muchos", "manyToMany": "tienen una relación de muchos a muchos",
"oneToOne": "tener una relación uno a uno", "oneToOne": "Tienen una relación de uno a uno",
"extraConnectionParameters": "Parámetros de conexión adicionales", "extraConnectionParameters": "Parámetros de conexión adicionales",
"commentsOnly": "Sólo comentarios", "commentsOnly": "Sólo comentarios",
"documentation": "Documentación", "documentation": "Documentación",
@ -964,7 +964,7 @@
"noAccess": "Sin acceso", "noAccess": "Sin acceso",
"restApis": "API REST", "restApis": "API REST",
"apis": "API", "apis": "API",
"apiSnippet": "Fragmentos de API", "apiSnippet": "API Snippets",
"includeData": "Incluir datos", "includeData": "Incluir datos",
"includeView": "Incluir vista", "includeView": "Incluir vista",
"includeWebhook": "Incluir Webhook", "includeWebhook": "Incluir Webhook",
@ -988,36 +988,36 @@
"appearanceSettings": "Ajustes de apariencia", "appearanceSettings": "Ajustes de apariencia",
"backgroundColor": "Color de Fondo", "backgroundColor": "Color de Fondo",
"hideNocodbBranding": "Ocultar marca NocoDB", "hideNocodbBranding": "Ocultar marca NocoDB",
"showOnConditions": "Show on conditions", "showOnConditions": "Mostrar en condiciones",
"showFieldOnConditionsMet": "Muestra el campo sólo cuando se cumplen las condiciones", "showFieldOnConditionsMet": "Muestra el campo sólo cuando se cumplen las condiciones",
"limitOptions": "Limitar opciones", "limitOptions": "Limitar las opciones",
"limitOptionsSubtext": "Limite las opciones visibles a los usuarios seleccionando las opciones disponibles", "limitOptionsSubtext": "Limitar opciones visibles para los usuarios seleccionando las opciones disponibles",
"clearSelection": "Borrar selección", "clearSelection": "Limpiar selección",
"displayAsProgress": "Mostrar como progreso", "displayAsProgress": "Mostrar como progreso",
"relationType": "Tipo de relación", "relationType": "Tipo de relación",
"showThousandsSeparator": "Mostrar separador de miles", "showThousandsSeparator": "Mostrar separador de miles",
"signUpForFree": "Regístrate gratis", "signUpForFree": "Regístrese gratis",
"coverImageField": "Cover image field", "coverImageField": "Cover image field",
"fitImage": "Ajustar imagen", "fitImage": "Ajustar imagen",
"coverImageArea": "Imagen de portada", "coverImageArea": "Imagen de portada",
"syncData": "Sincronizar datos", "syncData": "Sincronizar datos",
"syncDataModalSubtitle": "Registre los servicios en los que está interesado para recibir notificaciones cuando estén disponibles", "syncDataModalSubtitle": "Registre los servicios que le interesan para recibir una notificación cuando estén disponibles",
"redirectToUrl": "Redirigir a URL" "redirectToUrl": "Redirigir a URL"
}, },
"activity": { "activity": {
"webhookDetails": "Detalles del Webhook", "webhookDetails": "Detalles de Webhook",
"hideWeekends": "Ocultar fines de semana", "hideWeekends": "Ocultar los fines de semana",
"renameBase": "Renombrar base", "renameBase": "Renombrar base",
"renameWorkspace": "Renombrar espacio de trabajo", "renameWorkspace": "Renombrar espacio de trabajo",
"deactivate": "Desactivar", "deactivate": "Desactivar",
"manageUsers": "Gestionar usuarios", "manageUsers": "Gestionar usuarios",
"newWorkspace": "Espacio de trabajo nuevo", "newWorkspace": "Nuevo espacio de trabajo",
"addDomain": "Añadir dominio", "addDomain": "Añadir dominio",
"addMembers": "Agregar Miembros", "addMembers": "Añadir miembros",
"enterEmail": "Ingrese direcciones de correo electrónico", "enterEmail": "Ingresar direcciones de correo electrónico",
"inviteToBase": "Invitar a la Base", "inviteToBase": "Invitar a la base",
"inviteToWorkspace": "Invitar al espacio de trabajo", "inviteToWorkspace": "Invitar al espacio de trabajo",
"addMember": "Agregar Miembro a la Base", "addMember": "Añadir miembro a la base",
"noRange": "La vista del calendario requiere un rango de fechas", "noRange": "La vista del calendario requiere un rango de fechas",
"goToToday": "Ir a Hoy", "goToToday": "Ir a Hoy",
"toggleSidebar": "Alternar Barra Lateral", "toggleSidebar": "Alternar Barra Lateral",
@ -1230,7 +1230,7 @@
}, },
"kanban": { "kanban": {
"collapseStack": "Colapsar pila", "collapseStack": "Colapsar pila",
"collapseAll": "Colapsar todo", "collapseAll": "Contraer todo",
"expandAll": "Expandir todo", "expandAll": "Expandir todo",
"renameStack": "Renombrar pila", "renameStack": "Renombrar pila",
"deleteStack": "Borrar pila", "deleteStack": "Borrar pila",
@ -1251,28 +1251,28 @@
"addFieldFromFormView": "Añadir Campo", "addFieldFromFormView": "Añadir Campo",
"selectAllFields": "Seleccionar todos los campos", "selectAllFields": "Seleccionar todos los campos",
"preFilledFields": { "preFilledFields": {
"title": "Habilitar Pre-rellenar", "title": "Activar pre-rellenado",
"default": "Por defecto", "default": "Por defecto",
"locked": "Bloquear campos pre-rellenados como solo lectura", "locked": "Bloquear los campos pre-rellenados como solo lectura",
"hidden": "Ocultar campos pre-rellenados", "hidden": "Ocultar campos pre-rellenados",
"lockedFieldTooltip": "Valor pre-rellenado" "lockedFieldTooltip": "Valor pre-rellenado"
}, },
"getPreFilledLink": "Obtener enlace pre-rellenado", "getPreFilledLink": "Obtener enlace pre-rellenado",
"group": "Grupo", "group": "Grupo",
"goToDocs": "Ir a Documentos", "goToDocs": "Ir a documentos",
"addCondition": "Añadir condición", "addCondition": "Añadir condición",
"addConditionGroup": "Añadir grupo de condiciones" "addConditionGroup": "Añadir grupo de condiciones"
}, },
"tooltip": { "tooltip": {
"currentDateNotAvail": "Current date option not available for this data source / data type", "currentDateNotAvail": "Current date option not available for this data source / data type",
"privateConnection": "Habilitar para hacer esta conexión privada y oculta para otros creadores en este espacio de trabajo.", "privateConnection": "Active esta opción para que esta conexión sea privada y quede oculta a los demás creadores de este espacio de trabajo.",
"optionalDatabaseName": "Opcional. Usa la base de datos predeterminada \"{database}\" si se deja en blanco", "optionalDatabaseName": "Opcional. Utiliza la base de datos por defecto \"{database}\" si se deja en blanco.",
"optionalSchemaName": "Opcional. Usa el esquema predeterminado \"{schema}\" si se deja en blanco.", "optionalSchemaName": "Opcional. Utiliza el esquema por defecto \"{schema}\" si se deja en blanco.",
"schemaChangeDisabled": "La edición del esquema está deshabilitada para esta fuente de datos.", "schemaChangeDisabled": "La edición del esquema está desactivada para esta fuente de datos.",
"typeNotAllowed": "Este tipo de datos no está permitido.", "typeNotAllowed": "Este tipo de datos no está permitido.",
"dataWriteOptionDisabled": "Data editing can only be disabled when 'Schema editing' is also disabled.", "dataWriteOptionDisabled": "Data editing can only be disabled when 'Schema editing' is also disabled.",
"allowMetaWrite": "Esta opción permite la modificación del esquema de la base de datos, incluyendo la adición, alteración o eliminación de tablas y columnas. Úselo con precaución, ya que los cambios pueden afectar la integridad estructural de su base de datos.", "allowMetaWrite": "Esta opción permite modificar el esquema de la base de datos, incluyendo la adición, alteración o eliminación de tablas y columnas. Utilícela con precaución, ya que los cambios pueden afectar a la integridad estructural de su base de datos.",
"allowDataWrite": "Esta opción permite crear, actualizar o eliminar registros dentro de tablas de la base de datos. Ideal para administradores que necesitan cambiar datos directamente.", "allowDataWrite": "Esta opción permite crear, actualizar o eliminar registros dentro de las tablas de la base de datos. Ideal para usuarios administrativos que necesitan modificar datos directamente.",
"reachedSourceLimit": "Limitado a 10 fuentes de datos por base", "reachedSourceLimit": "Limitado a 10 fuentes de datos por base",
"saveChanges": "Guardar cambios", "saveChanges": "Guardar cambios",
"xcDB": "Crear un nuevo proyecto", "xcDB": "Crear un nuevo proyecto",
@ -1304,9 +1304,9 @@
"changeIconColour": "Cambiar el color del icono", "changeIconColour": "Cambiar el color del icono",
"preFillFormInfo": "Generate share form URL with pre-filled field data. To get a pre-filled link, make sure you’ve filled the necessary fields in the form view builder.", "preFillFormInfo": "Generate share form URL with pre-filled field data. To get a pre-filled link, make sure you’ve filled the necessary fields in the form view builder.",
"surveyFormInfo": "Modo formulario con un campo por página", "surveyFormInfo": "Modo formulario con un campo por página",
"useFieldEditMenuToConfigFieldType": "Utiliza el menú de edición de campo para conversiones de tipo después de que el archivo sea importado", "useFieldEditMenuToConfigFieldType": "Utilizar el menú de edición de campos para las conversiones de tipo después de importar el archivo",
"roleInheritedFromWorkspace": "Rol heredado del espacio de trabajo", "roleInheritedFromWorkspace": "Rol heredado del espacio de trabajo",
"comingSoonIntegration": "¡Próximamente! Haga clic para votar por la integración que necesita en NocoDB." "comingSoonIntegration": "¡Próximamente! Haz clic para votar a favor de la integración que necesitas en NocoDB."
}, },
"placeholder": { "placeholder": {
"searchIcons": "Buscar iconos", "searchIcons": "Buscar iconos",
@ -1321,7 +1321,7 @@
"projName": "Ingresa el nombre de proyecto", "projName": "Ingresa el nombre de proyecto",
"selectGroupField": "Seleccione un campo de agrupación", "selectGroupField": "Seleccione un campo de agrupación",
"selectGroupFieldNotFound": "No se encuentra ningún campo de selección único. Por favor, cree uno primero.", "selectGroupFieldNotFound": "No se encuentra ningún campo de selección único. Por favor, cree uno primero.",
"selectCoverImageField": "Seleccione un campo para la imagen de portada", "selectCoverImageField": "Seleccione un campo de imagen de portada",
"selectGeoField": "Seleccione un campo de datos geográficos", "selectGeoField": "Seleccione un campo de datos geográficos",
"notSelected": "-no seleccionado-", "notSelected": "-no seleccionado-",
"selectGeoFieldNotFound": "No se encuentra ningún campo de datos geográficos. Por favor, cree uno primero.", "selectGeoFieldNotFound": "No se encuentra ningún campo de datos geográficos. Por favor, cree uno primero.",
@ -1357,7 +1357,7 @@
"key": "Clave", "key": "Clave",
"createTable": "¡Crea tu primera tabla!", "createTable": "¡Crea tu primera tabla!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.", "createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No se crearon tokens API", "noTokenCreated": "No se han creado tokens de API",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.", "noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invita a tu equipo", "inviteYourTeam": "Invita a tu equipo",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace.", "inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace.",

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

@ -223,7 +223,7 @@
"questions": "질문", "questions": "질문",
"reachOut": "문의하기", "reachOut": "문의하기",
"betaNote": "이 기능은 현재 베타입니다.", "betaNote": "이 기능은 현재 베타입니다.",
"moreInfo": "자세한 내용은 여기를 참조하십시오.", "moreInfo": "자세한 내용은 여기를 참조하십시오",
"logs": "로그", "logs": "로그",
"groupingField": "필드별 스택", "groupingField": "필드별 스택",
"insertAfter": "오른쪽에 삽입", "insertAfter": "오른쪽에 삽입",
@ -352,7 +352,7 @@
"short": "짧음", "short": "짧음",
"medium": "중간", "medium": "중간",
"tall": "높음", "tall": "높음",
"extra": "추가" "extra": "매우 큰"
}, },
"externalDb": "외부 데이터베이스", "externalDb": "외부 데이터베이스",
"syncData": { "syncData": {
@ -374,10 +374,10 @@
"miro": "Miro", "miro": "Miro",
"salesforce": "Salesforce", "salesforce": "Salesforce",
"serviceCloud": "Service Cloud", "serviceCloud": "Service Cloud",
"snowflake": "스노우플레이크", "snowflake": "Snowflake",
"stripe": "Stripe", "stripe": "Stripe",
"surveyMonkey": "SurveyMonkey", "surveyMonkey": "SurveyMonkey",
"tableau": "타블로", "tableau": "Tableau",
"trello": "Trello", "trello": "Trello",
"typeform": "Typeform", "typeform": "Typeform",
"workday": "Workday", "workday": "Workday",
@ -536,7 +536,7 @@
"deleteWs": "작업 공간 삭제", "deleteWs": "작업 공간 삭제",
"deletingWs": "작업 공간 삭제 중", "deletingWs": "작업 공간 삭제 중",
"copyAuthToken": "인증 토큰 복사", "copyAuthToken": "인증 토큰 복사",
"copiedAuthToken": "인증 토큰이 복사되었습니다.", "copiedAuthToken": "인증 토큰이 복사되었습니다",
"copyInviteToken": "초대 토큰 복사", "copyInviteToken": "초대 토큰 복사",
"showSidebar": "사이드바 표시", "showSidebar": "사이드바 표시",
"hideSidebar": "사이드바 숨김", "hideSidebar": "사이드바 숨김",
@ -560,7 +560,7 @@
"projMeta": "프로젝트 메타 데이터", "projMeta": "프로젝트 메타 데이터",
"metaMgmt": "메타 관리", "metaMgmt": "메타 관리",
"metadata": "메타 데이터", "metadata": "메타 데이터",
"exportImportMeta": "메타 데이터 내보내기/가져오기 ", "exportImportMeta": "메타 데이터 내보내기/가져오기",
"uiACL": "UI 액세스 제어", "uiACL": "UI 액세스 제어",
"metaOperations": "메타 데이터 작업", "metaOperations": "메타 데이터 작업",
"audit": "감사", "audit": "감사",
@ -616,7 +616,7 @@
"selectFieldsFromRightPannelToAddHere": "오른쪽 패널에서 필드를 선택하여 여기에 추가", "selectFieldsFromRightPannelToAddHere": "오른쪽 패널에서 필드를 선택하여 여기에 추가",
"noOptionsFound": "옵션을 찾을 수 없습니다", "noOptionsFound": "옵션을 찾을 수 없습니다",
"surveyFormSubmitConfirmMsg": "이 양식을 제출하시겠습니까?", "surveyFormSubmitConfirmMsg": "이 양식을 제출하시겠습니까?",
"noResultsMatchedYourSearch": "Your search did not yield any matching results.", "noResultsMatchedYourSearch": "검색 결과와 일치하는 항목이 없습니다",
"looksLikeThisStackIsEmpty": "이 목록에는 현재 데이터가 없습니다", "looksLikeThisStackIsEmpty": "이 목록에는 현재 데이터가 없습니다",
"fromScratch": "처음부터", "fromScratch": "처음부터",
"fromFileAndExternalSources": "파일 & 외부 소스에서", "fromFileAndExternalSources": "파일 & 외부 소스에서",
@ -653,7 +653,7 @@
"changeTitleField": "제목 필드 변경", "changeTitleField": "제목 필드 변경",
"clearAll": "모두 지우기", "clearAll": "모두 지우기",
"addNewLookupHelperText1": "조회 필드는 연결된 레코드의 데이터를 표시합니다. 다음에서 특정 필드를 선택하세요 ", "addNewLookupHelperText1": "조회 필드는 연결된 레코드의 데이터를 표시합니다. 다음에서 특정 필드를 선택하세요 ",
"addNewLookupHelperText2": "이 테이블에 조회 필드로 추가하려면 테이블을 클릭합니다.", "addNewLookupHelperText2": "이 테이블에 조회 필드로 추가하려면 테이블을 클릭합니다",
"formatting": "서식 설정", "formatting": "서식 설정",
"selectAFormatType": "- -양식 유형을 선택하세요(선택 사항)- -", "selectAFormatType": "- -양식 유형을 선택하세요(선택 사항)- -",
"formatType": "형식 유형", "formatType": "형식 유형",
@ -663,7 +663,7 @@
"clickTo": "클릭하여", "clickTo": "클릭하여",
"allowAccessToYourCamera": "카메라에 대한 접근을 허용해주세요", "allowAccessToYourCamera": "카메라에 대한 접근을 허용해주세요",
"openFile": "파일 열기", "openFile": "파일 열기",
"enterValidUrl": "파일을 업로드하려면 유효한 URL을 입력하세요.", "enterValidUrl": "파일을 업로드하려면 유효한 URL을 입력하세요",
"addFilesFromUrl": "URL에서 파일 추가", "addFilesFromUrl": "URL에서 파일 추가",
"uploading": "업로드 중", "uploading": "업로드 중",
"dropHere": "이곳에 끌어다 놓기", "dropHere": "이곳에 끌어다 놓기",
@ -678,7 +678,7 @@
"metaSync": "메타 동기화", "metaSync": "메타 동기화",
"mention": "언급", "mention": "언급",
"today": "오늘", "today": "오늘",
"currentDate": "현재 날짜 ", "currentDate": "현재 날짜",
"workspace": "워크스페이스", "workspace": "워크스페이스",
"txt": "TXT 레코드 값", "txt": "TXT 레코드 값",
"transferOwnership": "소유권 이전", "transferOwnership": "소유권 이전",
@ -721,7 +721,7 @@
"audience-entityId": "청중/엔티티 ID", "audience-entityId": "청중/엔티티 ID",
"redirectUrl": "리디렉션 URL", "redirectUrl": "리디렉션 URL",
"oidc": "OpenID Connect (OIDC)", "oidc": "OpenID Connect (OIDC)",
"saml": "Security Assertion Markup Language (SAML)", "saml": "SAML",
"newProvider": "새 공급자", "newProvider": "새 공급자",
"generalSettings": "일반 설정", "generalSettings": "일반 설정",
"adminPanel": "관리자 패널", "adminPanel": "관리자 패널",
@ -764,8 +764,8 @@
"icon": "아이콘", "icon": "아이콘",
"max": "최대", "max": "최대",
"enableRichText": "서식있는 텍스트 활성화", "enableRichText": "서식있는 텍스트 활성화",
"idColon": "Id: {fieldId}", "idColon": "Id: {id}",
"copiedRecordURL": "레코드 URL을 복사했습니다.", "copiedRecordURL": "레코드 URL을 복사했습니다",
"copyRecordURL": "레코드 URL 복사", "copyRecordURL": "레코드 URL 복사",
"duplicateRecord": "행 복제", "duplicateRecord": "행 복제",
"binaryEncodingFormat": "바이너리 인코딩 형식", "binaryEncodingFormat": "바이너리 인코딩 형식",
@ -847,7 +847,7 @@
"username": "사용자 이름", "username": "사용자 이름",
"password": "비밀번호", "password": "비밀번호",
"schemaName": "스키마 이름", "schemaName": "스키마 이름",
"database": "데이터 베이스", "database": "데이터베이스",
"action": "동작", "action": "동작",
"actions": "행위", "actions": "행위",
"operation": "작업", "operation": "작업",
@ -898,14 +898,14 @@
}, },
"community": { "community": {
"starUs1": "Star", "starUs1": "Star",
"starUs2": "Github", "starUs2": "우리를 Github에서",
"bookDemo": "무료 데모 예약", "bookDemo": "무료 데모 예약",
"getAnswered": "디스코드", "getAnswered": "디스코드",
"joinDiscord": "디스코드 참가", "joinDiscord": "디스코드 참가",
"joinCommunity": "NocoDB 커뮤니티 참가", "joinCommunity": "NocoDB 커뮤니티 참가",
"joinReddit": "Join /r/NocoDB", "joinReddit": "Join /r/NocoDB",
"followNocodb": "NocoDB 팔로우", "followNocodb": "NocoDB 팔로우",
"communityTranslated": "(Community/AI Translated)" "communityTranslated": "(커뮤니티/AI 번역)"
}, },
"twitter": "트위터", "twitter": "트위터",
"docReference": "참조 문서", "docReference": "참조 문서",
@ -942,7 +942,7 @@
"firstRowAsHeaders": "첫 번째 행을 헤더로 사용", "firstRowAsHeaders": "첫 번째 행을 헤더로 사용",
"flattenNested": "중첩 표준화", "flattenNested": "중첩 표준화",
"downloadAllowed": "다운로드 허용", "downloadAllowed": "다운로드 허용",
"weAreHiring": "채용 중", "weAreHiring": "채용 중입니다!",
"primaryKey": "기본 키", "primaryKey": "기본 키",
"hasMany": "많이", "hasMany": "많이",
"belongsTo": "속해있다", "belongsTo": "속해있다",
@ -983,9 +983,9 @@
"deletedField": "삭제된 필드", "deletedField": "삭제된 필드",
"incompleteConfiguration": "구성 불완전", "incompleteConfiguration": "구성 불완전",
"selectField": "필드 선택", "selectField": "필드 선택",
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure." "selectFieldLabel": "목록에서 필드를 선택하여 필드 속성을 변경합니다"
}, },
"appearanceSettings": "Appearance Settings", "appearanceSettings": "모양 설정",
"backgroundColor": "배경색", "backgroundColor": "배경색",
"hideNocodbBranding": "NocoDB 브랜드 숨기기", "hideNocodbBranding": "NocoDB 브랜드 숨기기",
"showOnConditions": "조건에 따라 표시", "showOnConditions": "조건에 따라 표시",
@ -997,7 +997,7 @@
"relationType": "관계 유형", "relationType": "관계 유형",
"showThousandsSeparator": "천 단위 구분 기호 표시", "showThousandsSeparator": "천 단위 구분 기호 표시",
"signUpForFree": "무료로 가입", "signUpForFree": "무료로 가입",
"coverImageField": "Cover image field", "coverImageField": "표지 이미지",
"fitImage": "이미지 맞춤", "fitImage": "이미지 맞춤",
"coverImageArea": "표지 이미지", "coverImageArea": "표지 이미지",
"syncData": "데이터 동기화", "syncData": "데이터 동기화",
@ -1096,7 +1096,7 @@
"projInfo": "프로젝트 정보 복사", "projInfo": "프로젝트 정보 복사",
"themes": "테마" "themes": "테마"
}, },
"sort": "종류", "sort": "정렬",
"addSort": "정렬 옵션 추가", "addSort": "정렬 옵션 추가",
"filter": "필터", "filter": "필터",
"addFilter": "필터 추가", "addFilter": "필터 추가",
@ -1212,7 +1212,7 @@
"expandRecord": "레코드 확장", "expandRecord": "레코드 확장",
"deleteRecord": "레코드 삭제", "deleteRecord": "레코드 삭제",
"fullWidth": "전체 넓이", "fullWidth": "전체 넓이",
"exitFullWidth": "Exit full width", "exitFullWidth": "전체 너비 나가기",
"markAllAsRead": "모두 읽음으로 표시", "markAllAsRead": "모두 읽음으로 표시",
"column": { "column": {
"delete": "열 삭제", "delete": "열 삭제",
@ -1270,7 +1270,7 @@
"optionalSchemaName": "선택 사항. 비워두면 기본 스키마 \"{schema}\"를 사용합니다.", "optionalSchemaName": "선택 사항. 비워두면 기본 스키마 \"{schema}\"를 사용합니다.",
"schemaChangeDisabled": "이 데이터 소스에 대한 스키마 편집이 비활성화되어 있습니다.", "schemaChangeDisabled": "이 데이터 소스에 대한 스키마 편집이 비활성화되어 있습니다.",
"typeNotAllowed": "이 데이터 유형은 허용되지 않습니다.", "typeNotAllowed": "이 데이터 유형은 허용되지 않습니다.",
"dataWriteOptionDisabled": "Data editing can only be disabled when 'Schema editing' is also disabled.", "dataWriteOptionDisabled": "데이터 편집은 '스키마 편집'이 비활성화된 경우에만 비활성화되며 그렇지 않으면 활성화됩니다",
"allowMetaWrite": "이 옵션은 테이블과 열을 추가, 변경 또는 삭제하는 것을 포함하여 데이터베이스 스키마를 수정할 수 있습니다. 변경 사항이 데이터베이스의 구조적 무결성에 영향을 미칠 수 있으므로 주의해서 사용하십시오.", "allowMetaWrite": "이 옵션은 테이블과 열을 추가, 변경 또는 삭제하는 것을 포함하여 데이터베이스 스키마를 수정할 수 있습니다. 변경 사항이 데이터베이스의 구조적 무결성에 영향을 미칠 수 있으므로 주의해서 사용하십시오.",
"allowDataWrite": "이 옵션을 사용하면 데이터베이스 테이블 내에서 레코드를 생성, 업데이트 또는 삭제할 수 있습니다. 데이터를 직접 변경해야 하는 관리자에게 이상적입니다.", "allowDataWrite": "이 옵션을 사용하면 데이터베이스 테이블 내에서 레코드를 생성, 업데이트 또는 삭제할 수 있습니다. 데이터를 직접 변경해야 하는 관리자에게 이상적입니다.",
"reachedSourceLimit": "데이터 소스 제한에 도달했습니다.", "reachedSourceLimit": "데이터 소스 제한에 도달했습니다.",
@ -1302,7 +1302,7 @@
"clientCert": ".cert 파일을 선택하십시오", "clientCert": ".cert 파일을 선택하십시오",
"clientCA": "CA 파일을 선택하십시오", "clientCA": "CA 파일을 선택하십시오",
"changeIconColour": "아이콘 색상 변경", "changeIconColour": "아이콘 색상 변경",
"preFillFormInfo": "Generate share form URL with pre-filled field data. To get a pre-filled link, make sure you’ve filled the necessary fields in the form view builder.", "preFillFormInfo": "미리 작성된 링크를 생성하려면, 양식 생성기에서 필요한 필드를 모두 채웠는지 확인하세요.",
"surveyFormInfo": "페이지당 하나의 필드로 폼 모드", "surveyFormInfo": "페이지당 하나의 필드로 폼 모드",
"useFieldEditMenuToConfigFieldType": "파일을 가져온 후 유형 변환을 위해 필드 편집 메뉴 사용", "useFieldEditMenuToConfigFieldType": "파일을 가져온 후 유형 변환을 위해 필드 편집 메뉴 사용",
"roleInheritedFromWorkspace": "워크스페이스에서 상속된 역할", "roleInheritedFromWorkspace": "워크스페이스에서 상속된 역할",
@ -1356,11 +1356,11 @@
"value": "값", "value": "값",
"key": "키", "key": "키",
"createTable": "첫 번째 테이블을 만드세요!", "createTable": "첫 번째 테이블을 만드세요!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.", "createTableLabel": "처음부터 시작하거나 가져오기 또는 외부 데이터베이스에 연결하기",
"noTokenCreated": "생성된 API 토큰이 없습니다.", "noTokenCreated": "생성된 API 토큰이 없습니다.",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.", "noTokenCreatedLabel": "아직 API 토큰을 생성하지 않은 것 같습니다.",
"inviteYourTeam": "팀 초대", "inviteYourTeam": "팀 초대",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace.", "inviteYourTeamLabel": "팀과 협력하여 프로젝트를 빠르게 진행하세요!",
"searchOptions": "검색 옵션" "searchOptions": "검색 옵션"
}, },
"msg": { "msg": {
@ -1400,7 +1400,7 @@
"webhookBodyMsg3": "해당 기록을 참조하기 위해", "webhookBodyMsg3": "해당 기록을 참조하기 위해",
"formula": { "formula": {
"hintStart": "팁:{placeholder1}을(를) 사용하여 필드(예: {placeholder2})를 참조하십시오.", "hintStart": "팁:{placeholder1}을(를) 사용하여 필드(예: {placeholder2})를 참조하십시오.",
"hintEnd": "Formulas.", "hintEnd": "수식.",
"noSuggestedFormulaFound": "제안된 공식이 없습니다.", "noSuggestedFormulaFound": "제안된 공식이 없습니다.",
"noSuggestedFieldFound": "추천 필드를 찾을 수 없습니다", "noSuggestedFieldFound": "추천 필드를 찾을 수 없습니다",
"typeIsExpected": "{calleeName}에는 {position} 위치에 {type}이(가) 필요합니다", "typeIsExpected": "{calleeName}에는 {position} 위치에 {type}이(가) 필요합니다",
@ -1486,7 +1486,7 @@
"roleRequired": "역할이 필요합니다.", "roleRequired": "역할이 필요합니다.",
"warning": { "warning": {
"webhookDelete": "이 웹훅에 의존하는 버튼 필드는 영향을 받습니다", "webhookDelete": "이 웹훅에 의존하는 버튼 필드는 영향을 받습니다",
"calendarNoFields": "Calendar view requires a date or date time field to be setup. Try setting up a calendar view after adding a date / date time field!", "calendarNoFields": "캘린더 뷰에는 날짜 또는 날짜 시간 필드가 설정되어 있어야 합니다. 날짜/날짜 시간 필드를 추가한 후 캘린더 뷰를 설정해 보세요!",
"kanbanNoFields": "칸반 보기를 사용하려면 단일 선택 필드를 설정해야 합니다. 단일 선택 필드를 추가한 후 칸반 보기를 설정해 보세요!", "kanbanNoFields": "칸반 보기를 사용하려면 단일 선택 필드를 설정해야 합니다. 단일 선택 필드를 추가한 후 칸반 보기를 설정해 보세요!",
"mapNoFields": "지도 보기를 사용하려면 지리 데이터 필드를 설정해야 합니다. 지리 데이터 필드를 추가한 후 지도 보기를 설정해 보세요!", "mapNoFields": "지도 보기를 사용하려면 지리 데이터 필드를 설정해야 합니다. 지리 데이터 필드를 추가한 후 지도 보기를 설정해 보세요!",
"dbValid": "스키마 손실을 방지하기 위해 데이터베이스의 유효성을 확인하세요", "dbValid": "스키마 손실을 방지하기 위해 데이터베이스의 유효성을 확인하세요",
@ -1722,9 +1722,9 @@
"nullFilterExists": "Null 필터가 존재합니다. 제거해 주세요", "nullFilterExists": "Null 필터가 존재합니다. 제거해 주세요",
"signUpRules": { "signUpRules": {
"emailRequired": "이메일 주소는 필수입니다", "emailRequired": "이메일 주소는 필수입니다",
"emailInvalid": "이메일이 유효하지 않습니다.", "emailInvalid": "이메일이 유효하지 않습니다",
"passwdRequired": "비밀번호를 입력하십시오.", "passwdRequired": "비밀번호를 입력하십시오",
"passwdLength": "비밀번호는 8자 이상이어야 합니다.", "passwdLength": "비밀번호는 8자 이상이어야 합니다",
"passwdMismatch": "비밀번호가 일치하지 않습니다", "passwdMismatch": "비밀번호가 일치하지 않습니다",
"completeRuleSet": "대문자, 숫자 및 특수 문자가 각각 1개씩 포함된 8자 이상", "completeRuleSet": "대문자, 숫자 및 특수 문자가 각각 1개씩 포함된 8자 이상",
"atLeast8Char": "8자 이상", "atLeast8Char": "8자 이상",
@ -1739,20 +1739,20 @@
"invalidURL": "유효하지 않은 URL", "invalidURL": "유효하지 않은 URL",
"invalidEmail": "유효하지 않은 이메일", "invalidEmail": "유효하지 않은 이메일",
"internalError": "내부 오류", "internalError": "내부 오류",
"templateGeneratorNotFound": "템플릿 생성기를 찾을 수 없습니다.", "templateGeneratorNotFound": "템플릿 생성기를 찾을 수 없습니다",
"fileUploadFailed": "파일 업로드 실패", "fileUploadFailed": "파일 업로드 실패",
"primaryColumnUpdateFailed": "기본 열 업데이트 실패", "primaryColumnUpdateFailed": "기본 열 업데이트 실패",
"formDescriptionTooLong": "양식 설명이 너무 깁니다.", "formDescriptionTooLong": "양식 설명이 너무 깁니다",
"columnsRequired": "컬럼이 필요합니다.", "columnsRequired": "컬럼이 필요합니다",
"selectAtleastOneColumn": "컬럼을 하나 이상 선택하십시오.", "selectAtleastOneColumn": "컬럼을 하나 이상 선택하십시오",
"columnDescriptionNotFound": "컬럼 설명을 찾을 수 없습니다.", "columnDescriptionNotFound": "컬럼 설명을 찾을 수 없습니다",
"duplicateMappingFound": "중복 매핑이 발견되었습니다.", "duplicateMappingFound": "중복 매핑이 발견되었습니다",
"nullValueViolatesNotNull": "NULL 값이 NOT NULL 제약 조건을 위반합니다.", "nullValueViolatesNotNull": "NULL 값이 NOT NULL 제약 조건을 위반합니다",
"sourceHasInvalidNumbers": "소스 데이터에 잘못된 숫자 값이 포함되어 있습니다.", "sourceHasInvalidNumbers": "소스 데이터에 잘못된 숫자 값이 포함되어 있습니다",
"sourceHasInvalidBoolean": "소스 데이터에 잘못된 부울 값이 포함되어 있습니다.", "sourceHasInvalidBoolean": "소스 데이터에 잘못된 부울 값이 포함되어 있습니다",
"invalidForm": "유효하지 않은 양식", "invalidForm": "유효하지 않은 양식",
"formValidationFailed": "양식 유효성 검사 실패", "formValidationFailed": "양식 유효성 검사 실패",
"youHaveBeenSignedOut": "로그아웃되었습니다.", "youHaveBeenSignedOut": "로그아웃되었습니다",
"failedToLoadList": "목록을 불러오지 못했습니다.", "failedToLoadList": "목록을 불러오지 못했습니다.",
"failedToLoadChildrenList": "하위 목록을 불러오지 못했습니다.", "failedToLoadChildrenList": "하위 목록을 불러오지 못했습니다.",
"deleteFailed": "삭제 실패", "deleteFailed": "삭제 실패",
@ -1761,89 +1761,89 @@
"deleteRowFailed": "행 삭제 실패", "deleteRowFailed": "행 삭제 실패",
"setFormDataFailed": "양식 데이터 설정 실패", "setFormDataFailed": "양식 데이터 설정 실패",
"formViewUpdateFailed": "양식 뷰 업데이트 실패", "formViewUpdateFailed": "양식 뷰 업데이트 실패",
"tableNameRequired": "테이블 이름이 필요합니다.", "tableNameRequired": "테이블 이름이 필요합니다",
"nameShouldStartWithAnAlphabetOr_": "이름은 영문자 또는 _로 시작해야 합니다.", "nameShouldStartWithAnAlphabetOr_": "이름은 영문자 또는 _로 시작해야 합니다",
"followingCharactersAreNotAllowed": "다음 문자는 허용되지 않습니다.", "followingCharactersAreNotAllowed": "다음 문자는 허용되지 않습니다",
"columnNameRequired": "컬럼 이름이 필요합니다.", "columnNameRequired": "컬럼 이름이 필요합니다",
"duplicateColumnName": "중복된 필드 이름", "duplicateColumnName": "중복된 필드 이름",
"duplicateSystemColumnName": "시스템 필드에 이미 사용된 이름", "duplicateSystemColumnName": "시스템 필드에 이미 사용된 이름",
"uiDataTypeRequired": "UI 데이터 유형이 필요합니다", "uiDataTypeRequired": "UI 데이터 유형이 필요합니다",
"columnNameExceedsCharacters": "열 이름의 길이가 최대 {value}자를 초과합니다.", "columnNameExceedsCharacters": "열 이름의 길이가 최대 {value}자를 초과합니다",
"projectNameExceeds50Characters": "{title} 이름이 50자를 초과합니다", "projectNameExceeds50Characters": "{title} 이름이 50자를 초과합니다",
"projectNameCannotStartWithSpace": "{title} 이름은 공백으로 시작할 수 없습니다", "projectNameCannotStartWithSpace": "{title} 이름은 공백으로 시작할 수 없습니다",
"requiredField": "필수 필드입니다.", "requiredField": "필수 필드입니다",
"ipNotAllowed": "IP가 허용되지 않습니다.", "ipNotAllowed": "IP가 허용되지 않습니다",
"targetFileIsNotAnAcceptedFileType": "대상 파일이 허용되는 파일 유형이 아닙니다.", "targetFileIsNotAnAcceptedFileType": "대상 파일이 허용되는 파일 유형이 아닙니다",
"theAcceptedFileTypeIsCsv": "허용되는 파일 유형은 .csv입니다.", "theAcceptedFileTypeIsCsv": "허용되는 파일 유형은 .csv입니다",
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "허용되는 파일 유형은 .xls, .xlsx, .xlsm, .ods, .ots입니다", "theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "허용되는 파일 유형은 .xls, .xlsx, .xlsm, .ods, .ots입니다",
"parameterKeyCannotBeEmpty": "매개 변수 키는 비워 둘 수 없습니다.", "parameterKeyCannotBeEmpty": "매개 변수 키는 비워 둘 수 없습니다",
"duplicateParameterKeysAreNotAllowed": "중복 매개 변수 키는 허용되지 않습니다.", "duplicateParameterKeysAreNotAllowed": "중복 매개 변수 키는 허용되지 않습니다",
"fieldRequired": "{value}은(는) 비워 둘 수 없습니다.", "fieldRequired": "{value}은(는) 비워 둘 수 없습니다.",
"projectNotAccessible": "프로젝트에 액세스할 수 없습니다.", "projectNotAccessible": "프로젝트에 액세스할 수 없습니다",
"copyToClipboardError": "클립 보드에 복사할 수 없습니다.", "copyToClipboardError": "클립 보드에 복사할 수 없습니다",
"pasteFromClipboardError": "클립보드에서 붙여넣기 실패", "pasteFromClipboardError": "클립보드에서 붙여넣기 실패",
"multiFieldSaveValidation": "저장하기 전에 모든 필드의 구성을 완료하세요.", "multiFieldSaveValidation": "저장하기 전에 모든 필드의 구성을 완료하세요",
"somethingWentWrong": "문제가 발생했습니다", "somethingWentWrong": "문제가 발생했습니다",
"draggedContentIsNotTypeOfImage": "드래그된 콘텐츠가 이미지 유형이 아닙니다.", "draggedContentIsNotTypeOfImage": "드래그된 콘텐츠가 이미지 유형이 아닙니다",
"fieldToParseImageData": "이미지 데이터를 구문 분석할 필드", "fieldToParseImageData": "이미지 데이터를 구문 분석할 필드",
"someOfTheRequiredFieldsAreEmpty": "필수 필드 중 일부가 비어 있습니다." "someOfTheRequiredFieldsAreEmpty": "필수 필드 중 일부가 비어 있습니다"
}, },
"toast": { "toast": {
"exportMetadata": "프로젝트 메타 데이터를 성공적으로 내보냈습니다.", "exportMetadata": "프로젝트 메타 데이터를 성공적으로 내보냈습니다",
"importMetadata": "프로젝트 메타 데이터를 성공적으로 가져왔습니다.", "importMetadata": "프로젝트 메타 데이터를 성공적으로 가져왔습니다",
"clearMetadata": "프로젝트 메타 데이터를 성공적으로 지웠습니다.", "clearMetadata": "프로젝트 메타 데이터를 성공적으로 지웠습니다",
"stopProject": "프로젝트가 성공적으로 중지되었습니다.", "stopProject": "프로젝트가 성공적으로 중지되었습니다",
"startProject": "프로젝트가 성공적으로 시작되었습니다.", "startProject": "프로젝트가 성공적으로 시작되었습니다",
"restartProject": "프로젝트가 성공적으로 다시 시작되었습니다.", "restartProject": "프로젝트가 성공적으로 다시 시작되었습니다",
"deleteProject": "프로젝트가 성공적으로 삭제되었습니다.", "deleteProject": "프로젝트가 성공적으로 삭제되었습니다",
"authToken": "인증 토큰이 클립보드에 복사되었습니다.", "authToken": "인증 토큰이 클립보드에 복사되었습니다",
"projInfo": "프로젝트 정보가 클립보드에 복사되었습니다. ", "projInfo": "프로젝트 정보가 클립보드에 복사되었습니다",
"inviteUrlCopy": "초대 URL이 클립보드에 복사되었습니다. ", "inviteUrlCopy": "초대 URL이 클립보드에 복사되었습니다",
"createView": "뷰가 성공적으로 생성되었습니다. ", "createView": "뷰가 성공적으로 생성되었습니다",
"formEmailSMTP": "이메일 알림을 사용하려면 App Store에서 SMTP 플러그인을 활성화 하십시오.", "formEmailSMTP": "이메일 알림을 사용하려면 App Store에서 SMTP 플러그인을 활성화 하십시오",
"collabView": "공동 작업 뷰로 성공적으로 전환했습니다", "collabView": "공동 작업 뷰로 성공적으로 전환했습니다",
"lockedView": "잠긴 뷰로 성공적으로 전환되었습니다", "lockedView": "잠긴 뷰로 성공적으로 전환되었습니다",
"futureRelease": "향후 릴리스에서 사용할 수 있습니다." "futureRelease": "곧 출시!"
}, },
"success": { "success": {
"licenseKeyUpdated": "라이센스 키 업데이트됨", "licenseKeyUpdated": "라이센스 키 업데이트됨",
"columnDuplicated": "필드가 성공적으로 복제되었습니다", "columnDuplicated": "필드가 성공적으로 복제되었습니다",
"rowDuplicatedWithoutSavedYet": "행이 중복됨(저장되지 않음)", "rowDuplicatedWithoutSavedYet": "행이 중복됨(저장되지 않음)",
"updatedUIACL": "테이블에 대한 UI ACL을 업데이트했습니다.", "updatedUIACL": "테이블에 대한 UI ACL을 업데이트했습니다",
"pluginUninstalled": "플러그인이 성공적으로 제거되었습니다", "pluginUninstalled": "플러그인이 성공적으로 제거되었습니다",
"pluginSettingsSaved": "플러그인 설정이 성공적으로 저장되었습니다.", "pluginSettingsSaved": "플러그인 설정이 성공적으로 저장되었습니다",
"pluginTested": "플러그인이 성공적으로 테스트되었습니다.", "pluginTested": "플러그인이 성공적으로 테스트되었습니다",
"tableRenamed": "테이블 이름이 성공적으로 변경되었습니다.", "tableRenamed": "테이블 이름이 성공적으로 변경되었습니다",
"layoutRenamed": "레이아웃 이름이 성공적으로 변경되었습니다", "layoutRenamed": "레이아웃 이름이 성공적으로 변경되었습니다",
"viewDeleted": "뷰가 성공적으로 삭제되었습니다.", "viewDeleted": "뷰가 성공적으로 삭제되었습니다",
"primaryColumnUpdated": "기본 열이 성공적으로 업데이트되었습니다.", "primaryColumnUpdated": "기본 열이 성공적으로 업데이트되었습니다",
"tableDataExported": "테이블 데이터가 성공적으로 내보내졌습니다.", "tableDataExported": "테이블 데이터가 성공적으로 내보내졌습니다",
"updated": "성공적으로 업데이트되었습니다.", "updated": "성공적으로 업데이트되었습니다",
"sharedViewDeleted": "공유된 뷰가 성공적으로 삭제되었습니다.", "sharedViewDeleted": "공유된 뷰가 성공적으로 삭제되었습니다",
"userDeleted": "사용자가 성공적으로 삭제되었습니다.", "userDeleted": "사용자가 성공적으로 삭제되었습니다",
"viewRenamed": "뷰 이름이 성공적으로 변경되었습니다.", "viewRenamed": "뷰 이름이 성공적으로 변경되었습니다",
"tokenGenerated": "토큰이 성공적으로 생성되었습니다.", "tokenGenerated": "토큰이 성공적으로 생성되었습니다",
"tokenDeleted": "토큰이 성공적으로 삭제되었습니다.", "tokenDeleted": "토큰이 성공적으로 삭제되었습니다",
"userAddedToProject": "사용자가 성공적으로 프로젝트에 추가되었습니다.", "userAddedToProject": "사용자가 성공적으로 프로젝트에 추가되었습니다",
"userAdded": "사용자가 성공적으로 추가되었습니다.", "userAdded": "사용자가 성공적으로 추가되었습니다",
"userDeletedFromProject": "사용자가 성공적으로 프로젝트에서 삭제되었습니다.", "userDeletedFromProject": "사용자가 성공적으로 프로젝트에서 삭제되었습니다",
"inviteEmailSent": "초대 이메일이 성공적으로 전송되었습니다.", "inviteEmailSent": "초대 이메일이 성공적으로 전송되었습니다",
"inviteURLCopied": "초대 URL이 클립보드에 복사되었습니다.", "inviteURLCopied": "초대 URL이 클립보드에 복사되었습니다",
"commentCopied": "댓글이 클립보드에 복사되었습니다.", "commentCopied": "댓글이 클립보드에 복사되었습니다",
"passwordResetURLCopied": "비밀번호 재설정 URL이 클립보드에 복사되었습니다.", "passwordResetURLCopied": "비밀번호 재설정 URL이 클립보드에 복사되었습니다",
"shareableURLCopied": "공유 가능한 URL이 클립보드에 복사되었습니다.", "shareableURLCopied": "공유 가능한 URL이 클립보드에 복사되었습니다!",
"embeddableHTMLCodeCopied": "임베드 가능한 HTML 코드가 클립보드에 복사되었습니다.", "embeddableHTMLCodeCopied": "임베드 가능한 HTML 코드가 클립보드에 복사되었습니다!",
"userDetailsUpdated": "사용자 세부 정보가 성공적으로 업데이트되었습니다.", "userDetailsUpdated": "사용자 세부 정보가 성공적으로 업데이트되었습니다",
"tableDataImported": "테이블 데이터가 성공적으로 가져와졌습니다.", "tableDataImported": "테이블 데이터가 성공적으로 가져와졌습니다",
"webhookUpdated": "Webhook가 성공적으로 업데이트되었습니다.", "webhookUpdated": "Webhook가 성공적으로 업데이트되었습니다",
"webhookDeleted": "Webhook가 성공적으로 삭제되었습니다.", "webhookDeleted": "Webhook가 성공적으로 삭제되었습니다",
"webhookTested": "Webhook가 성공적으로 테스트되었습니다.", "webhookTested": "Webhook가 성공적으로 테스트되었습니다",
"columnUpdated": "열이 성공적으로 업데이트되었습니다.", "columnUpdated": "열이 성공적으로 업데이트되었습니다",
"columnCreated": "열이 성공적으로 생성되었습니다.", "columnCreated": "열이 성공적으로 생성되었습니다",
"passwordChanged": "비밀번호가 성공적으로 변경되었습니다. 다시 로그인해 주세요.", "passwordChanged": "비밀번호가 성공적으로 변경되었습니다. 다시 로그인해 주세요.",
"settingsSaved": "설정이 성공적으로 저장되었습니다.", "settingsSaved": "설정이 성공적으로 저장되었습니다",
"roleUpdated": "역할이 성공적으로 업데이트되었습니다.", "roleUpdated": "역할이 성공적으로 업데이트되었습니다",
"connectionAdded": "연동이 성공적으로 완료되었습니다.", "connectionAdded": "연동이 성공적으로 완료되었습니다",
"connectionAddedDesc": "이제 베이스 소유자와 생성자는 자격 증명을 다시 입력하지 않고도 데이터 소스를 추가할 수 있습니다." "connectionAddedDesc": "이제 베이스 소유자와 생성자는 자격 증명을 다시 입력하지 않고도 데이터 소스를 추가할 수 있습니다."
} }
} }

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

@ -40,7 +40,7 @@
}, },
"aggregation": { "aggregation": {
"sum": "Toplam", "sum": "Toplam",
"count": "Say", "count": "Miktar",
"min": "Min", "min": "Min",
"max": "Max", "max": "Max",
"avg": "Ort", "avg": "Ort",
@ -918,7 +918,7 @@
"links": "Bağlantılar", "links": "Bağlantılar",
"onUpdate": "Güncellenince", "onUpdate": "Güncellenince",
"onDelete": "Silinince", "onDelete": "Silinince",
"account": "Account", "account": "Hesap",
"language": "Language", "language": "Language",
"primaryColor": "Primary Color", "primaryColor": "Primary Color",
"accentColor": "Accent Color", "accentColor": "Accent Color",

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

@ -304,10 +304,10 @@
"workspaces": "Các không gian làm việc", "workspaces": "Các không gian làm việc",
"project": "Dự định", "project": "Dự định",
"projects": "Dự án.", "projects": "Dự án.",
"table": "Bàn", "table": "Bảng",
"tables": "Những cái bàn", "tables": "Bảng",
"field": "Cánh đồng", "field": "Trường dữ liệu",
"fields": "Lĩnh vực", "fields": "Trường dữ liệu",
"column": "Cột", "column": "Cột",
"columns": "Cột", "columns": "Cột",
"cell": "Ô", "cell": "Ô",
@ -815,7 +815,7 @@
"accountEmailID": "ID Email Tài khoản", "accountEmailID": "ID Email Tài khoản",
"backToWorkspace": "Quay lại Workspace", "backToWorkspace": "Quay lại Workspace",
"untitledToken": "Token chưa có tiêu đề", "untitledToken": "Token chưa có tiêu đề",
"tableName": "Tên bảng.", "tableName": "Tên bảng",
"dashboardName": "Tên bảng điều khiển", "dashboardName": "Tên bảng điều khiển",
"createView": "Create View", "createView": "Create View",
"creatingView": "Tạo Chế độ xem", "creatingView": "Tạo Chế độ xem",
@ -1132,7 +1132,7 @@
"createTable": "Create New Table", "createTable": "Create New Table",
"createDashboard": "Tạo bảng chính", "createDashboard": "Tạo bảng chính",
"createWorkspace": "Tạo không gian làm việc", "createWorkspace": "Tạo không gian làm việc",
"refreshTable": "Bàn làm mới", "refreshTable": "Làm mới bảng",
"renameTable": "Đổi tên bảng", "renameTable": "Đổi tên bảng",
"renameLayout": "Đổi tên bố cục", "renameLayout": "Đổi tên bố cục",
"deleteTable": "Xóa bảng", "deleteTable": "Xóa bảng",

2
packages/nc-gui/lib/enums.ts

@ -19,8 +19,10 @@ export enum Language {
id = 'Bahasa Indonesia', id = 'Bahasa Indonesia',
it = 'Italiano', it = 'Italiano',
ja = '日本語', ja = '日本語',
kn = 'ಕನನಡ',
ko = '한국어', ko = '한국어',
lv = 'Latviešu', lv = 'Latviešu',
ml = 'മലയ',
nl = 'Nederlandse', nl = 'Nederlandse',
no = 'Norsk', no = 'Norsk',
pl = 'Polski', pl = 'Polski',

2
packages/nc-gui/lib/types.ts

@ -281,7 +281,7 @@ interface ProductFeedItem {
Id: string Id: string
Title: string Title: string
Description: string Description: string
['Feed Source']: 'Youtube' | 'Github' | 'All' ['Feed Source']: 'Youtube' | 'Github' | 'All' | 'Cloud'
Url: string Url: string
Tags?: string Tags?: string
['Published Time']: string ['Published Time']: string

4
packages/nc-gui/package.json

@ -1,6 +1,7 @@
{ {
"name": "nc-gui", "name": "nc-gui",
"private": true, "private": true,
"packageManager": "pnpm@9.6.0",
"description": "NocoDB Frontend", "description": "NocoDB Frontend",
"author": { "author": {
"name": "NocoDB", "name": "NocoDB",
@ -70,6 +71,7 @@
"html-entities": "^2.5.2", "html-entities": "^2.5.2",
"httpsnippet": "^2.0.0", "httpsnippet": "^2.0.0",
"inflection": "^1.13.4", "inflection": "^1.13.4",
"isomorphic-dompurify": "^1.13.0",
"jsbarcode": "^3.11.6", "jsbarcode": "^3.11.6",
"jsep": "^1.3.8", "jsep": "^1.3.8",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
@ -79,7 +81,7 @@
"marked": "^4.3.0", "marked": "^4.3.0",
"monaco-editor": "^0.50.0", "monaco-editor": "^0.50.0",
"monaco-sql-languages": "^0.11.0", "monaco-sql-languages": "^0.11.0",
"nocodb-sdk": "0.257.0", "nocodb-sdk": "workspace:^",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"parse-github-url": "^1.0.2", "parse-github-url": "^1.0.2",
"pdfobject": "^2.3.0", "pdfobject": "^2.3.0",

23
packages/nc-gui/store/notification.ts

@ -1,9 +1,10 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { NotificationType } from 'nocodb-sdk' import type { NotificationType } from 'nocodb-sdk'
import axios, { type CancelTokenSource } from 'axios' import axios, { type CancelTokenSource } from 'axios'
import { CancelToken } from 'axios'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
const CancelToken = axios.CancelToken
export const useNotification = defineStore('notificationStore', () => { export const useNotification = defineStore('notificationStore', () => {
const isTokenRefreshInProgress = useStorage(TOKEN_REFRESH_PROGRESS_KEY, false) const isTokenRefreshInProgress = useStorage(TOKEN_REFRESH_PROGRESS_KEY, false)
@ -27,16 +28,24 @@ export const useNotification = defineStore('notificationStore', () => {
let cancelTokenSource: CancelTokenSource | null let cancelTokenSource: CancelTokenSource | null
const pollNotificationsApiCall = async () => {
// set up cancel token for polling to cancel when token changes/token is removed
cancelTokenSource = CancelToken.source()
return await api.notification.poll({
cancelToken: cancelTokenSource.token,
})
}
const sharedExecutionPollNotificationsApiCall = useSharedExecutionFn('notification', pollNotificationsApiCall, {
timeout: 30000,
})
const pollNotifications = async () => { const pollNotifications = async () => {
try { try {
if (!token.value) return if (!token.value) return
// set up cancel token for polling to cancel when token changes/token is removed const res = await sharedExecutionPollNotificationsApiCall()
cancelTokenSource = CancelToken.source()
const res = await api.notification.poll({
cancelToken: cancelTokenSource.token,
})
if (res.status === 'success') { if (res.status === 'success') {
if (notificationTab.value === 'unread') { if (notificationTab.value === 'unread') {

95
packages/nc-gui/utils/columnUtils.ts

@ -257,6 +257,97 @@ const isColumnInvalid = (col: ColumnType) => {
} }
} }
// cater existing v1 cases
const checkboxIconList = [
{
checked: 'mdi-check-bold',
unchecked: 'mdi-crop-square',
},
{
checked: 'mdi-check-circle-outline',
unchecked: 'mdi-checkbox-blank-circle-outline',
},
{
checked: 'mdi-star',
unchecked: 'mdi-star-outline',
},
{
checked: 'mdi-heart',
unchecked: 'mdi-heart-outline',
},
{
checked: 'mdi-moon-full',
unchecked: 'mdi-moon-new',
},
{
checked: 'mdi-thumb-up',
unchecked: 'mdi-thumb-up-outline',
},
{
checked: 'mdi-flag',
unchecked: 'mdi-flag-outline',
},
]
const ratingIconList = [
{
full: 'mdi-star',
empty: 'mdi-star-outline',
},
{
full: 'mdi-heart',
empty: 'mdi-heart-outline',
},
{
full: 'mdi-moon-full',
empty: 'mdi-moon-new',
},
{
full: 'mdi-thumb-up',
empty: 'mdi-thumb-up-outline',
},
{
full: 'mdi-flag',
empty: 'mdi-flag-outline',
},
]
function extractCheckboxIcon(meta: string | Record<string, any> = null) {
const parsedMeta = parseProp(meta)
const icon = {
checked: 'mdi-check-circle-outline',
unchecked: 'mdi-checkbox-blank-circle-outline',
}
if (parsedMeta.icon) {
icon.checked = parsedMeta.icon.checked || icon.checked
icon.unchecked = parsedMeta.icon.unchecked || icon.unchecked
} else if (typeof parsedMeta.iconIdx === 'number' && checkboxIconList[parsedMeta.iconIdx]) {
icon.checked = checkboxIconList[parsedMeta.iconIdx].checked
icon.unchecked = checkboxIconList[parsedMeta.iconIdx].unchecked
}
return icon
}
function extractRatingIcon(meta: string | Record<string, any> = null) {
const parsedMeta = parseProp(meta)
const icon = {
full: 'mdi-star',
empty: 'mdi-star-outline',
}
if (parsedMeta.icon) {
icon.full = parsedMeta.icon.full || icon.full
icon.empty = parsedMeta.icon.empty || icon.empty
} else if (typeof parsedMeta.iconIdx === 'number' && ratingIconList[parsedMeta.iconIdx]) {
icon.full = ratingIconList[parsedMeta.iconIdx].full
icon.empty = ratingIconList[parsedMeta.iconIdx].empty
}
return icon
}
export { export {
uiTypes, uiTypes,
isTypableInputColumn, isTypableInputColumn,
@ -267,4 +358,8 @@ export {
isColumnRequiredAndNull, isColumnRequiredAndNull,
isColumnRequired, isColumnRequired,
isVirtualColRequired, isVirtualColRequired,
checkboxIconList,
ratingIconList,
extractCheckboxIcon,
extractRatingIcon,
} }

1018
packages/nc-lib-gui/package-lock.json generated

File diff suppressed because it is too large Load Diff

1
packages/nc-mail-templates/package.json

@ -2,6 +2,7 @@
"name": "nc-mail-templates", "name": "nc-mail-templates",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"packageManager": "pnpm@9.6.0",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {

1
packages/nc-secret-mgr/package.json

@ -2,6 +2,7 @@
"name": "nc-secret-mgr", "name": "nc-secret-mgr",
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"packageManager": "pnpm@9.6.0",
"main": "dist/cli.js", "main": "dist/cli.js",
"bin": "dist/cli.js", "bin": "dist/cli.js",
"scripts": { "scripts": {

220
packages/noco-docs/docs/020.getting-started/050.self-hosted/020.environment-variables.md

@ -1,140 +1,154 @@
--- ---
title: 'Environment variables' title: 'Environment variables'
description: 'Environment Variables for NocoDB!' description: 'Environment Variables for NocoDB!'
tags: ['Open Source'] tags: [ 'Open Source' ]
keywords : ['NocoDB Environment Variables', 'NocoDB env variables', 'NocoDB envs', 'NocoDB .env'] keywords: [ 'NocoDB Environment Variables', 'NocoDB env variables', 'NocoDB envs', 'NocoDB .env' ]
--- ---
For production use cases, it is crucial to set all environment variables marked as **"Mandatory"** to ensure optimal performance, security, and functionality of NocoDB. For production use cases, it is crucial to set all environment variables marked as **"Mandatory"** to ensure optimal
performance, security, and functionality of NocoDB.
## Database ## Database
| Variable | Mandatory | Description | If Not Set |
| -------- |-----------| ----------- | ---------- | | Variable | Mandatory | Description | If Not Set |
| `NC_DB` | Yes | The primary database where all NocoDB metadata and data are stored. Example format: `pg://host.docker.internal:5432?u=username&p=password&d=database_name`. | A local SQLite database will be created in the root folder if `NC_DB` is not specified. | |-----------------------------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
| `NC_DB_JSON` | No | Allows setting the database connection using a valid [knex connection JSON string](https://knexjs.org/guide/#configuration-options) instead of `NC_DB`. | | | `NC_DB` | Yes | The primary database where all NocoDB metadata and data are stored. Example format: `pg://host.docker.internal:5432?u=username&p=password&d=database_name`. | A local SQLite database will be created in the root folder if `NC_DB` is not specified. |
| `NC_DB_JSON_FILE` | No | A path to a knex connection JSON file can be used to specify the database connection, as an alternative to `NC_DB`. | | | `NC_DB_JSON` | No | Allows setting the database connection using a valid [knex connection JSON string](https://knexjs.org/guide/#configuration-options) instead of `NC_DB`. | |
| `DATABASE_URL` | No | A [JDBC URL string](https://jdbc.postgresql.org/documentation/use/#connecting-to-the-database) can be used for the database connection instead of `NC_DB`. | | | `NC_DB_JSON_FILE` | No | A path to a knex connection JSON file can be used to specify the database connection, as an alternative to `NC_DB`. | |
| `DATABASE_URL_FILE` | No | A path to a file containing a JDBC URL can be specified for the database connection as an alternative to `NC_DB`. | | | `DATABASE_URL` | No | A [JDBC URL string](https://jdbc.postgresql.org/documentation/use/#connecting-to-the-database) can be used for the database connection instead of `NC_DB`. | |
| `NC_CONNECTION_ENCRYPT_KEY` | No | The key used to encrypt the credentials of external databases. <br/> **Warning:** Changing this variable may break the application. If you must change it, use the CLI as described in the [NocoDB Secret CLI documentation](/data-sources/updating-secret). | Keep connection credentials as plain text in the database if not set. | | `DATABASE_URL_FILE` | No | A path to a file containing a JDBC URL can be specified for the database connection as an alternative to `NC_DB`. | |
| `NC_CONNECTION_ENCRYPT_KEY` | No | The key used to encrypt the credentials of external databases. <br/> **Warning:** Changing this variable may break the application. If you must change it, use the CLI as described in the [NocoDB Secret CLI documentation](/data-sources/updating-secret). | Keep connection credentials as plain text in the database if not set. |
## Authentication ## Authentication
| Variable | Mandatory | Description | If Not Set |
| -------- |-----------| ----------- | ---------- | | Variable | Mandatory | Description | If Not Set |
| `NC_AUTH_JWT_SECRET` | Yes | This JWT secret is utilized for generating authentication tokens. | A random secret will be generated automatically. | |---------------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
| `NC_JWT_EXPIRES_IN` | No | Specifies the expiration time for JWT tokens. | Defaults to `10h`. | | `NC_AUTH_JWT_SECRET` | Yes | This JWT secret is utilized for generating authentication tokens. | A random secret will be generated automatically. |
| `NC_GOOGLE_CLIENT_ID` | No | Google client ID required to activate Google authentication. | | | `NC_JWT_EXPIRES_IN` | No | Specifies the expiration time for JWT tokens. | Defaults to `10h`. |
| `NC_GOOGLE_CLIENT_SECRET` | No | Google client secret required to activate Google authentication. | | | `NC_GOOGLE_CLIENT_ID` | No | Google client ID required to activate Google authentication. | |
| `NC_ADMIN_EMAIL` | No | Super admin email address. This is useful in case you need to recover your username and password. | An initial prompt for email and password is required when accessing the UI for the first time. | | `NC_GOOGLE_CLIENT_SECRET` | No | Google client secret required to activate Google authentication. | |
| `NC_ADMIN_PASSWORD` | No | Super admin password. Must be at least 8 characters long, including one uppercase letter, one number, and one special character from `$&+,:;=?@#'.^*()%!_-\"`. This is useful for username and password recovery. | | | `NC_ADMIN_EMAIL` | No | Super admin email address. This is useful in case you need to recover your username and password. | An initial prompt for email and password is required when accessing the UI for the first time. |
| `NC_DISABLE_EMAIL_AUTH` | No | Disables email and password-based authentication, intended for use when Google authentication variables are configured. | | | `NC_ADMIN_PASSWORD` | No | Super admin password. Must be at least 8 characters long, including one uppercase letter, one number, and one special character from `$&+,:;=?@#'.^*()%!_-\"`. This is useful for username and password recovery. | |
| `NC_DISABLE_EMAIL_AUTH` | No | Disables email and password-based authentication, intended for use when Google authentication variables are configured. | |
## Storage ## Storage
| Variable | Mandatory | Description | If Not Set |
| -------- | --------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------| | Variable | Mandatory | Description | If Not Set |
| `NC_S3_BUCKET_NAME` | No | The name of the AWS S3 bucket used for the S3 storage plugin. | | |--------------------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------|
| `NC_S3_REGION` | No | The AWS S3 region where the S3 storage plugin bucket is located. | | | `NC_S3_BUCKET_NAME` | No | The name of the AWS S3 bucket used for the S3 storage plugin. | |
| `NC_S3_ENDPOINT` | No | S3 endpoint for S3 storage plugin. | Defaults to `s3.<region>.amazonaws.com` | | `NC_S3_REGION` | No | The AWS S3 region where the S3 storage plugin bucket is located. | |
| `NC_S3_ACCESS_KEY` | No | The AWS access key ID required for the S3 storage plugin. | | | `NC_S3_ENDPOINT` | No | S3 endpoint for S3 storage plugin. | Defaults to `s3.<region>.amazonaws.com` |
| `NC_S3_ACCESS_SECRET` | No | The AWS access secret associated with the S3 storage plugin. | | | `NC_S3_ACCESS_KEY` | No | The AWS access key ID for the S3 storage plugin. Required if no role access in use. | |
| `NC_S3_FORCE_PATH_STYLE` | No | Whether to force [path-style requests](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#path-style-access) for the S3 storage plugin. | | | `NC_S3_ACCESS_SECRET` | No | The AWS access secret associated with the S3 storage plugin. Required if no role access in use. | |
| `NC_S3_ACL` | No | The [ACL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html) for the objects in S3 | | | | `NC_S3_FORCE_PATH_STYLE` | No | Whether to force [path-style requests](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#path-style-access) for the S3 storage plugin. | |
| `NC_ATTACHMENT_FIELD_SIZE` | No | Maximum file size allowed for [attachments](/fields/field-types/custom-types/attachment/) in bytes. | Defaults to `20971520` (20 MiB). | | `NC_S3_ACL` | No | The [ACL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html) for the objects in S3 | |
| `NC_MAX_ATTACHMENTS_ALLOWED` | No | Maximum number of attachments allowed per cell. | Defaults to `10`. | | `NC_ATTACHMENT_FIELD_SIZE` | No | Maximum file size allowed for [attachments](/fields/field-types/custom-types/attachment/) in bytes. | Defaults to `20971520` (20 MiB). |
| `NC_ATTACHMENT_RETENTION_DAYS` | No | Number of days to retain attachment on storage after all references deleted. (Set 0 to keep forever) | Defaults to `10`. | | `NC_MAX_ATTACHMENTS_ALLOWED` | No | Maximum number of attachments allowed per cell. | Defaults to `10`. |
| `NC_SECURE_ATTACHMENTS` | No | Enables access to attachments only through pre-signed URLs. Set to `true` to activate; all other values are treated as `false`. ⚠ Note: Enabling this will make existing links inaccessible. | Defaults to `false`. | | `NC_ATTACHMENT_RETENTION_DAYS` | No | Number of days to retain attachment on storage after all references deleted. (Set 0 to keep forever) | Defaults to `10`. |
| `NC_ATTACHMENT_EXPIRE_SECONDS` | No | Time in seconds after which pre-signed URLs for attachments start to expire. The actual expiration will occur after this time plus an additional 10 minutes. Only applicable if `NC_SECURE_ATTACHMENTS` is enabled. | Defaults to `7200` (2 hours). | | `NC_SECURE_ATTACHMENTS` | No | Enables access to attachments only through pre-signed URLs. Set to `true` to activate; all other values are treated as `false`. ⚠ Note: Enabling this will make existing links inaccessible. | Defaults to `false`. |
| `NC_ATTACHMENT_EXPIRE_SECONDS` | No | Time in seconds after which pre-signed URLs for attachments start to expire. The actual expiration will occur after this time plus an additional 10 minutes. Only applicable if `NC_SECURE_ATTACHMENTS` is enabled. | Defaults to `7200` (2 hours). |
## Email Notifications ## Email Notifications
- The following SMTP variables are used to send email notifications to users, e.g., invites. - The following SMTP variables are used to send email notifications to users, e.g., invites.
| Variable | Mandatory | Description | If Not Set | | Variable | Mandatory | Description | If Not Set |
| -------- |-----------| ----------- | ---------- | |----------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|
| `NC_SMTP_FROM` | Yes | The email address used as the sender for the SMTP plugin. | | | `NC_SMTP_FROM` | Yes | The email address used as the sender for the SMTP plugin. | |
| `NC_SMTP_HOST` | Yes | The hostname of the email server for the SMTP plugin. | | | `NC_SMTP_HOST` | Yes | The hostname of the email server for the SMTP plugin. | |
| `NC_SMTP_PORT` | Yes | The network port of the email server for the SMTP plugin. | | | `NC_SMTP_PORT` | Yes | The network port of the email server for the SMTP plugin. | |
| `NC_SMTP_USERNAME` | Yes | The username for authentication with the SMTP plugin. | | | `NC_SMTP_USERNAME` | Yes | The username for authentication with the SMTP plugin. | |
| `NC_SMTP_PASSWORD` | Yes | The password for authentication with the SMTP plugin. | | | `NC_SMTP_PASSWORD` | Yes | The password for authentication with the SMTP plugin. | |
| `NC_SMTP_SECURE` | Yes | Enables secure authentication for the SMTP plugin. Set to `true` to enable; all other values are considered `false`. | | | `NC_SMTP_SECURE` | Yes | Enables secure authentication for the SMTP plugin. Set to `true` to enable; all other values are considered `false`. | |
| `NC_SMTP_IGNORE_TLS` | Yes | Ignores TLS for the SMTP plugin. Set to `true` to ignore TLS; all other values are considered `false`. For more details, see [Nodemailer's SMTP documentation](https://nodemailer.com/smtp/). | | | `NC_SMTP_IGNORE_TLS` | Yes | Ignores TLS for the SMTP plugin. Set to `true` to ignore TLS; all other values are considered `false`. For more details, see [Nodemailer's SMTP documentation](https://nodemailer.com/smtp/). | |
## Backend ## Backend
| Variable | Mandatory | Description | If Not Set |
| -------- | --------- | ----------- | ---------- | | Variable | Mandatory | Description | If Not Set |
| `PORT` | No | Specifies the network port on which NocoDB will run. | Defaults to `8080`. | |----------------|-----------|-------------------------------------------------------------------------------------------------|---------------------|
| `NODE_OPTIONS` | No | Node.js [options](https://nodejs.org/api/cli.html#node_optionsoptions) to pass to the instance. | | | `PORT` | No | Specifies the network port on which NocoDB will run. | Defaults to `8080`. |
| `NODE_OPTIONS` | No | Node.js [options](https://nodejs.org/api/cli.html#node_optionsoptions) to pass to the instance. | |
## Frontend ## Frontend
| Variable | Mandatory | Description | If Not Set |
| -------- | --------- | ----------- | ---------- | | Variable | Mandatory | Description | If Not Set |
| `NC_PUBLIC_URL` | No | This is the base URL used for constructing URLs in email templates, generating the Swagger documentation URL, and handling backend URL requirements. It should be set to your public-facing NocoDB URL to ensure consistency across the application. | By default, it infers the URL from the incoming request on the backend. If the server is behind a proxy, this may result in incorrect URLs. | |------------------------------|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| `NC_DASHBOARD_URL` | No | Defines a custom dashboard URL path. | Defaults to `/dashboard`. | | `NC_PUBLIC_URL` | No | This is the base URL used for constructing URLs in email templates, generating the Swagger documentation URL, and handling backend URL requirements. It should be set to your public-facing NocoDB URL to ensure consistency across the application. | By default, it infers the URL from the incoming request on the backend. If the server is behind a proxy, this may result in incorrect URLs. |
| `NUXT_PUBLIC_NC_BACKEND_URL` | No | Specifies a custom backend URL. | Defaults to `http://localhost:8080`. | | `NC_DASHBOARD_URL` | No | Defines a custom dashboard URL path. | Defaults to `/dashboard`. |
| `NUXT_PUBLIC_NC_BACKEND_URL` | No | Specifies a custom backend URL. | Defaults to `http://localhost:8080`. |
## Cache ## Cache
| Variable | Mandatory | Description | If Not Set |
| -------- |-----------|--------------------------------------------------------------------------------------------------|--------------------------| | Variable | Mandatory | Description | If Not Set |
|----------------|-----------|--------------------------------------------------------------------------------------------------|--------------------------|
| `NC_REDIS_URL` | Yes | Specifies the Redis URL used for caching. <br></br> Eg: `redis://:authpassword@127.0.0.1:6380/4` | Caching layer of backend | | `NC_REDIS_URL` | Yes | Specifies the Redis URL used for caching. <br></br> Eg: `redis://:authpassword@127.0.0.1:6380/4` | Caching layer of backend |
## Product Configuration ## Product Configuration
| Variable | Mandatory | Description | If Not Set |
| -------- | --------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------|
| `DB_QUERY_LIMIT_DEFAULT` | No | Default pagination limit for data tables. | Defaults to `25`. Maximum is `100` |
| `DB_QUERY_LIMIT_GROUP_BY_GROUP` | No | Number of groups per page. | Defaults to `10`. |
| `DB_QUERY_LIMIT_GROUP_BY_RECORD` | No | Number of records per group. | Defaults to `10`. |
| `DB_QUERY_LIMIT_MAX` | No | Maximum allowable pagination limit. | Defaults to `1000`. |
| `DB_QUERY_LIMIT_MIN` | No | Minimum allowable pagination limit. | Defaults to `10` |
| `NC_CONNECT_TO_EXTERNAL_DB_DISABLED` | No | Disables the ability to create bases on external databases. | |
| `NC_INVITE_ONLY_SIGNUP` | No | Disables public signup; signup is possible only via invitations. Integrated into the [super admin settings menu](/account-settings/oss-specific-details#enable--disable-signup) as of version 0.99.0. | |
| `NC_REQUEST_BODY_SIZE` | No | Maximum bytes allowed in the request body, based on [ExpressJS limits](https://expressjs.com/en/resources/middleware/body-parser.html#limit). | Defaults to `1048576` (1 MB). |
| `NC_EXPORT_MAX_TIMEOUT` | No | Sets a timeout in milliseconds for downloading CSVs in batches if not completed within this period. | Defaults to `5000` (5 seconds). |
| `NC_ALLOW_LOCAL_HOOKS` | No | Allows webhooks to call local network links, posing potential security risks. Set to `true` to enable; all other values are considered `false`. | Defaults to `false`. |
| `NC_SANITIZE_COLUMN_NAME` | No | Enables sanitization of column names during their creation to prevent SQL injection and other security issues. | Defaults to `true`. |
| `NC_TOOL_DIR` | No | Specifies the directory to store metadata and app-related files. In Docker setups, this maps to `/usr/app/data/` for mounting volumes. | Defaults to the current working directory. |
| `NC_DISABLE_PG_DATA_REFLECTION` | No | Disables the creation of a schema for each base in PostgreSQL. [Click here for more detail](#postgres-data-reflection) | |
| `NC_MIGRATIONS_DISABLED` | No | Disables NocoDB migrations. | |
| `NC_DISABLE_AUDIT` | No | Disables the audit log feature. | Defaults to `false`. |
| `NC_AUTOMATION_LOG_LEVEL` | No | Configures logging levels for automation features. Possible values: `OFF`, `ERROR`, `ALL`. More details can be found under [Webhooks](/automation/webhook/create-webhook). | Defaults to `OFF`. |
### Postgres Data Reflection | Variable | Mandatory | Description | If Not Set |
|--------------------------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------|
| `DB_QUERY_LIMIT_DEFAULT` | No | Default pagination limit for data tables. | Defaults to `25`. Maximum is `100` |
| `DB_QUERY_LIMIT_GROUP_BY_GROUP` | No | Number of groups per page. | Defaults to `10`. |
| `DB_QUERY_LIMIT_GROUP_BY_RECORD` | No | Number of records per group. | Defaults to `10`. |
| `DB_QUERY_LIMIT_MAX` | No | Maximum allowable pagination limit. | Defaults to `1000`. |
| `DB_QUERY_LIMIT_MIN` | No | Minimum allowable pagination limit. | Defaults to `10` |
| `NC_CONNECT_TO_EXTERNAL_DB_DISABLED` | No | Disables the ability to create bases on external databases. | |
| `NC_INVITE_ONLY_SIGNUP` | No | Disables public signup; signup is possible only via invitations. Integrated into the [super admin settings menu](/account-settings/oss-specific-details#enable--disable-signup) as of version 0.99.0. | |
| `NC_REQUEST_BODY_SIZE` | No | Maximum bytes allowed in the request body, based on [ExpressJS limits](https://expressjs.com/en/resources/middleware/body-parser.html#limit). | Defaults to `1048576` (1 MB). |
| `NC_EXPORT_MAX_TIMEOUT` | No | Sets a timeout in milliseconds for downloading CSVs in batches if not completed within this period. | Defaults to `5000` (5 seconds). |
| `NC_ALLOW_LOCAL_HOOKS` | No | Allows webhooks to call local network links, posing potential security risks. Set to `true` to enable; all other values are considered `false`. | Defaults to `false`. |
| `NC_SANITIZE_COLUMN_NAME` | No | Enables sanitization of column names during their creation to prevent SQL injection and other security issues. | Defaults to `true`. |
| `NC_TOOL_DIR` | No | Specifies the directory to store metadata and app-related files. In Docker setups, this maps to `/usr/app/data/` for mounting volumes. | Defaults to the current working directory. |
| `NC_DISABLE_PG_DATA_REFLECTION` | No | Disables the creation of a schema for each base in PostgreSQL. [Click here for more detail](#postgres-data-reflection) | |
| `NC_MIGRATIONS_DISABLED` | No | Disables NocoDB migrations. | |
| `NC_DISABLE_AUDIT` | No | Disables the audit log feature. | Defaults to `false`. |
| `NC_AUTOMATION_LOG_LEVEL` | No | Configures logging levels for automation features. Possible values: `OFF`, `ERROR`, `ALL`. More details can be found under [Webhooks](/automation/webhook/create-webhook). | Defaults to `OFF`. |
NocoDB UI is exactly what's in your Postgres database schema. Same tables, same columns—everything is perfectly mirrored. This is done by creating a schema for each base in PostgreSQL. This feature is enabled by default if the user has the required permissions. To disable it, set the `NC_DISABLE_PG_DATA_REFLECTION` environment variable to `false`. ### Postgres Data Reflection
NocoDB UI is exactly what's in your Postgres database schema. Same tables, same columns—everything is perfectly
mirrored. This is done by creating a schema for each base in PostgreSQL. This feature is enabled by default if the user
has the required permissions. To disable it, set the `NC_DISABLE_PG_DATA_REFLECTION` environment variable to `false`.
## Logging & Monitoring ## Logging & Monitoring
| Variable | Mandatory | Description | If Not Set |
| -------- | --------- |---------------------------------------------------------------------------------------|------------| | Variable | Mandatory | Description | If Not Set |
| `NC_SENTRY_DSN` | No | Data Source Name (DSN) for integrating with Sentry for monitoring and error tracking. | | |--------------------------|-----------|---------------------------------------------------------------------------------------|------------|
| `NC_DISABLE_ERR_REPORTS` | No | Disable default Sentry error reporting. | TRUE | | `NC_SENTRY_DSN` | No | Data Source Name (DSN) for integrating with Sentry for monitoring and error tracking. | |
| `NC_DISABLE_ERR_REPORTS` | No | Disable default Sentry error reporting. | TRUE |
## Debugging Only ## Debugging Only
| Variable | Mandatory | Description | If Not Set |
| -------- | --------- | ----------- | ---------- | | Variable | Mandatory | Description | If Not Set |
| `NC_DISABLE_CACHE` | No | Disables caching to force metadata fetching directly from the database instead of Redis/cache. Recommended only during debugging. | Defaults to `false`. | |--------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------|----------------------|
| `NC_DISABLE_CACHE` | No | Disables caching to force metadata fetching directly from the database instead of Redis/cache. Recommended only during debugging. | Defaults to `false`. |
## Telemetry ## Telemetry
| Variable | Mandatory | Description | If Not Set |
| -------- | --------- | ----------- | ---------- | | Variable | Mandatory | Description | If Not Set |
| `NC_DISABLE_TELE` | No | Disables the telemetry to prevent sending anonymous usage data. Please keep it enabled to help us understand the usage of the product and the impact that any new breaking change can cause. | | |-------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|
| `NC_DISABLE_TELE` | No | Disables the telemetry to prevent sending anonymous usage data. Please keep it enabled to help us understand the usage of the product and the impact that any new breaking change can cause. | |
## Litestream ## Litestream
> Litestream is used **only** when `NC_DB` is set to SQLite. It backs up the SQLite database and stores it in S3. > Litestream is used **only** when `NC_DB` is set to SQLite. It backs up the SQLite database and stores it in S3.
| Variable | Mandatory | Description | If Not Set | | Variable | Mandatory | Description | If Not Set |
| -------- | --------- | ----------- | ---------- | |---------------------------------------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
| `LITESTREAM_S3_ENDPOINT` | No | URL of an S3-compatible object storage service endpoint for [Litestream](https://litestream.io/) replication of NocoDB's default SQLite database. Example: `s3.eu-central-1.amazonaws.com`. | Defaults to [AWS S3](https://aws.amazon.com/s3/). | | `LITESTREAM_S3_ENDPOINT` | No | URL of an S3-compatible object storage service endpoint for [Litestream](https://litestream.io/) replication of NocoDB's default SQLite database. Example: `s3.eu-central-1.amazonaws.com`. | Defaults to [AWS S3](https://aws.amazon.com/s3/). |
| `LITESTREAM_S3_REGION` | No | AWS region of the Litestream replication object storage bucket. Note that `LITESTREAM_S3_ENDPOINT` takes precedence if configured (the endpoint URL includes the region). | Defaults to the [default region configured in AWS](https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-plan-region.html). | | `LITESTREAM_S3_REGION` | No | AWS region of the Litestream replication object storage bucket. Note that `LITESTREAM_S3_ENDPOINT` takes precedence if configured (the endpoint URL includes the region). | Defaults to the [default region configured in AWS](https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-plan-region.html). |
| `LITESTREAM_S3_BUCKET` | No | Name of the object storage bucket to store the Litestream replication in. | *Litestream replication is disabled if this variable is not set.* | | `LITESTREAM_S3_BUCKET` | No | Name of the object storage bucket to store the Litestream replication in. | *Litestream replication is disabled if this variable is not set.* |
| `LITESTREAM_S3_PATH` | No | Directory path to use within the Litestream replication object storage bucket. | Defaults to `nocodb`. | | `LITESTREAM_S3_PATH` | No | Directory path to use within the Litestream replication object storage bucket. | Defaults to `nocodb`. |
| `LITESTREAM_S3_ACCESS_KEY_ID` | No | Authentication key ID for the Litestream replication object storage bucket. | *Litestream replication is disabled if this variable is not set.* | | `LITESTREAM_S3_ACCESS_KEY_ID` | No | Authentication key ID for the Litestream replication object storage bucket. | *Litestream replication is disabled if this variable is not set.* |
| `LITESTREAM_S3_SECRET_ACCESS_KEY` | No | Authentication secret for the Litestream replication object storage bucket. | *Litestream replication is disabled if this variable is not set.* | | `LITESTREAM_S3_SECRET_ACCESS_KEY` | No | Authentication secret for the Litestream replication object storage bucket. | *Litestream replication is disabled if this variable is not set.* |
| `LITESTREAM_S3_SKIP_VERIFY` | No | Whether to disable TLS verification for the Litestream replication object storage service. Useful when testing against a local node such as MinIO and you are using self-signed certificates. | Defaults to `false`. | | `LITESTREAM_S3_SKIP_VERIFY` | No | Whether to disable TLS verification for the Litestream replication object storage service. Useful when testing against a local node such as MinIO and you are using self-signed certificates. | Defaults to `false`. |
| `LITESTREAM_RETENTION` | No | Amount of time Litestream snapshot and WAL files are kept. After the retention period, a new snapshot is created and the old one is removed. WAL files that exist before the oldest snapshot will also be removed. | Defaults to `1440h` (60 days). | | `LITESTREAM_RETENTION` | No | Amount of time Litestream snapshot and WAL files are kept. After the retention period, a new snapshot is created and the old one is removed. WAL files that exist before the oldest snapshot will also be removed. | Defaults to `1440h` (60 days). |
| `LITESTREAM_RETENTION_CHECK_INTERVAL` | No | Frequency in which Litestream will check if retention needs to be enforced. | Defaults to `72h` (3 days). | | `LITESTREAM_RETENTION_CHECK_INTERVAL` | No | Frequency in which Litestream will check if retention needs to be enforced. | Defaults to `72h` (3 days). |
| `LITESTREAM_SNAPSHOT_INTERVAL` | No | Frequency in which new Litestream snapshots are created. A higher frequency reduces the time to restore since newer snapshots will have fewer WAL frames to apply. Retention still applies to these snapshots. | Defaults to `24h` (1 day). | | `LITESTREAM_SNAPSHOT_INTERVAL` | No | Frequency in which new Litestream snapshots are created. A higher frequency reduces the time to restore since newer snapshots will have fewer WAL frames to apply. Retention still applies to these snapshots. | Defaults to `24h` (1 day). |
| `LITESTREAM_SYNC_INTERVAL` | No | Frequency in which frames are pushed to the Litestream replica. Increasing this frequency can increase object storage costs significantly. | Defaults to `60s` (1 minute). | | `LITESTREAM_SYNC_INTERVAL` | No | Frequency in which frames are pushed to the Litestream replica. Increasing this frequency can increase object storage costs significantly. | Defaults to `60s` (1 minute). |
| `LITESTREAM_AGE_PUBLIC_KEY` | No | [age](https://age-encryption.org/) public key generated by `age-keygen` (`age1...`) or SSH public key (`ssh-ed25519 AAAA...`, `ssh-rsa AAAA...`) used to encrypt the Litestream replication for. Refer to the relevant [Litestream documentation](https://litestream.io/reference/config/#encryption) for details. | *Litestream replication is unencrypted if this variable is not set.* | | `LITESTREAM_AGE_PUBLIC_KEY` | No | [age](https://age-encryption.org/) public key generated by `age-keygen` (`age1...`) or SSH public key (`ssh-ed25519 AAAA...`, `ssh-rsa AAAA...`) used to encrypt the Litestream replication for. Refer to the relevant [Litestream documentation](https://litestream.io/reference/config/#encryption) for details. | *Litestream replication is unencrypted if this variable is not set.* |
| `LITESTREAM_AGE_SECRET_KEY` | No | [age](https://age-encryption.org/) secret key (`AGE-SECRET-KEY-1...`) used to encrypt the Litestream replication with. Refer to the relevant [Litestream documentation](https://litestream.io/reference/config/#encryption) for details. | *Litestream replication is unencrypted if this variable is not set.* | | `LITESTREAM_AGE_SECRET_KEY` | No | [age](https://age-encryption.org/) secret key (`AGE-SECRET-KEY-1...`) used to encrypt the Litestream replication with. Refer to the relevant [Litestream documentation](https://litestream.io/reference/config/#encryption) for details. | *Litestream replication is unencrypted if this variable is not set.* |
| `AWS_ACCESS_KEY_ID` | No | ***Deprecated***. Please use `LITESTREAM_S3_ACCESS_KEY_ID` instead. | | | `AWS_ACCESS_KEY_ID` | No | ***Deprecated***. Please use `LITESTREAM_S3_ACCESS_KEY_ID` instead. | |
| `AWS_SECRET_ACCESS_KEY` | No | ***Deprecated***. Please use `LITESTREAM_S3_SECRET_ACCESS_KEY` instead. | | | `AWS_SECRET_ACCESS_KEY` | No | ***Deprecated***. Please use `LITESTREAM_S3_SECRET_ACCESS_KEY` instead. | |
| `AWS_BUCKET` | No | ***Deprecated***. Please use `LITESTREAM_S3_BUCKET` instead. | | | `AWS_BUCKET` | No | ***Deprecated***. Please use `LITESTREAM_S3_BUCKET` instead. | |
| `AWS_BUCKET_PATH` | No | ***Deprecated***. Please use `LITESTREAM_S3_PATH` instead. | | | `AWS_BUCKET_PATH` | No | ***Deprecated***. Please use `LITESTREAM_S3_PATH` instead. | |

2
packages/noco-docs/docs/100.data-sources/050.updating-secret.md

@ -37,7 +37,7 @@ To update a secret in NocoDB, you can use the `nc-secret-mgr` package. Follow th
Alternatively, you can use the `nc-secret-mgr` executable to update secrets. Alternatively, you can use the `nc-secret-mgr` executable to update secrets.
1. Download the `nc-secret-mgr` executable from the [NocoDB website](https://github.com/nocodb/nc-secret-mgr/releases/latest). 1. Download the `nc-secret-mgr` executable from the [NocoDB Github](https://github.com/nocodb/nc-secret-mgr/releases/latest).
2. Run the executable using the following command: 2. Run the executable using the following command:
```bash ```bash

4712
packages/nocodb-sdk/pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

98
packages/nocodb/Dockerfile.timely

@ -0,0 +1,98 @@
# syntax=docker/dockerfile:1.5
# Use Buildx cross-compilation
###########
# Litestream Builder
###########
FROM --platform=$BUILDPLATFORM golang:alpine3.19 as lt-builder
WORKDIR /usr/src/
# Use build platform-specific tools
RUN apk --no-cache add git make musl-dev gcc
# Build litestream for the target platform
RUN git clone https://github.com/benbjohnson/litestream.git litestream \
&& cd litestream \
&& GOARCH=$(echo $TARGETPLATFORM | cut -d '/' -f 2) GOOS=$(echo $TARGETPLATFORM | cut -d '/' -f 1) go install ./cmd/litestream \
&& cp $GOPATH/bin/litestream /usr/src/lt
###########
# Builder
###########
FROM --platform=$BUILDPLATFORM node:18.19.1-alpine as builder
WORKDIR /usr/src/app
# Install node-gyp dependencies
RUN apk add --no-cache python3 make g++
# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy application dependency manifests to the container image.
COPY --link ./package.json ./package.json
COPY --link ./docker/main.js ./docker/main.js
COPY --link ./docker/start-litestream.sh /usr/src/appEntry/start.sh
COPY --link src/public/ ./docker/public/
COPY --link ./docker/nc-gui/ ./docker/nc-gui/
# For pnpm to generate a flat node_modules without symlinks
# So that modclean works as expected
RUN echo "node-linker=hoisted" > .npmrc
# Install production dependencies, reduce node_module size with modclean
# Removing sqlite deps and add execute permission to start.sh
RUN pnpm install --prod --shamefully-hoist \
&& pnpm dlx modclean --patterns="default:*" --ignore="nc-lib-gui/**,dayjs/**,express-status-monitor/**,@azure/msal-node/dist/**" --run \
&& rm -rf ./node_modules/sqlite3/deps \
&& chmod +x /usr/src/appEntry/start.sh
############
## Binary Dependencies Builder
############
FROM --platform=$TARGETPLATFORM node:18.19.1-alpine as bin-builder
WORKDIR /usr/src/app
RUN apk add --no-cache jq
# copy package.json to extract dependency versions
COPY --link ./package.json ./package-copy.json
# Install sqlite3 for the target platform to copy to the final image
RUN SQLITE3_VERSION=$(jq -r '.dependencies["sqlite3"]' /usr/src/app/package-copy.json) \
&& SHARP_VERSION=$(jq -r '.dependencies["sharp"]' /usr/src/app/package-copy.json) \
&& npm init -y && npm install sqlite3@$SQLITE3_VERSION sharp@$SHARP_VERSION
###########
# Runner
###########
FROM --platform=$TARGETPLATFORM alpine:3.19
WORKDIR /usr/src/app
ENV LITESTREAM_S3_SKIP_VERIFY=false \
LITESTREAM_RETENTION=1440h \
LITESTREAM_RETENTION_CHECK_INTERVAL=72h \
LITESTREAM_SNAPSHOT_INTERVAL=24h \
LITESTREAM_SYNC_INTERVAL=60s \
NC_DOCKER=0.6 \
NC_TOOL_DIR=/usr/app/data/ \
NODE_ENV=production \
PORT=8080
RUN apk add --update --no-cache dasel dumb-init nodejs
# Copy litestream binary and config file
COPY --link --from=lt-builder /usr/src/lt /usr/local/bin/litestream
COPY --link ./docker/litestream.yml /etc/litestream.yml
# Copy production code & main entry file
COPY --link --from=builder /usr/src/app/ /usr/src/app/
COPY --link --from=bin-builder /usr/src/app/node_modules/sqlite3/ /usr/src/app/node_modules/sqlite3/
COPY --link --from=bin-builder /usr/src/app/node_modules/sharp/ /usr/src/app/node_modules/sharp/
COPY --link --from=builder /usr/src/appEntry/ /usr/src/appEntry/
EXPOSE 8080
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
# Start Nocodb
CMD ["/usr/src/appEntry/start.sh"]

2
packages/nocodb/package.json

@ -126,7 +126,7 @@
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"nc-lib-gui": "0.257.0", "nc-lib-gui": "0.257.0",
"nestjs-throttler-storage-redis": "^0.4.4", "nestjs-throttler-storage-redis": "^0.4.4",
"nocodb-sdk": "0.257.0", "nocodb-sdk": "workspace:^",
"nodemailer": "^6.9.13", "nodemailer": "^6.9.13",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"object-sizeof": "^2.6.4", "object-sizeof": "^2.6.4",

10
packages/nocodb/src/controllers/public-datas-export.controller.ts

@ -52,6 +52,11 @@ export class PublicDatasExportController {
NcError.invalidSharedViewPassword(); NcError.invalidSharedViewPassword();
} }
// check if download is allowed, in general it's called as CSV download
if (!view.meta?.allowCSVDownload) {
NcError.forbidden('Download is not allowed for this view');
}
const model = await view.getModelWithInfo(context); const model = await view.getModelWithInfo(context);
await view.getColumns(context); await view.getColumns(context);
@ -115,6 +120,11 @@ export class PublicDatasExportController {
NcError.invalidSharedViewPassword(); NcError.invalidSharedViewPassword();
} }
// check if download is allowed
if (!view.meta?.allowCSVDownload) {
NcError.forbidden('Download is not allowed for this view');
}
const model = await view.getModelWithInfo(context); const model = await view.getModelWithInfo(context);
await view.getColumns(context); await view.getColumns(context);

27
packages/nocodb/src/helpers/NcPluginMgrv2.ts

@ -118,28 +118,27 @@ class NcPluginMgrv2 {
/* /*
* NC_S3_BUCKET_NAME * NC_S3_BUCKET_NAME
* NC_S3_REGION * NC_S3_REGION
* NC_S3_ACCESS_KEY
* NC_S3_ACCESS_SECRET
* */ * */
if ( if (
process.env.NC_S3_BUCKET_NAME && process.env.NC_S3_BUCKET_NAME &&
process.env.NC_S3_REGION && process.env.NC_S3_REGION
process.env.NC_S3_ACCESS_KEY &&
process.env.NC_S3_ACCESS_SECRET
) { ) {
const s3Plugin = await Plugin.getPluginByTitle(S3PluginConfig.title); const s3Plugin = await Plugin.getPluginByTitle(S3PluginConfig.title);
const s3CfgData: Record<string, any> = {
bucket: process.env.NC_S3_BUCKET_NAME,
region: process.env.NC_S3_REGION,
endpoint: process.env.NC_S3_ENDPOINT,
force_path_style: process.env.NC_S3_FORCE_PATH_STYLE === 'true',
acl: process.env.NC_S3_ACL,
}
if (process.env.NC_S3_ACCESS_KEY && process.env.NC_S3_ACCESS_SECRET) {
s3CfgData.access_key = process.env.NC_S3_ACCESS_KEY
s3CfgData.access_secret = process.env.NC_S3_ACCESS_SECRET
}
await Plugin.update(s3Plugin.id, { await Plugin.update(s3Plugin.id, {
active: true, active: true,
input: JSON.stringify({ input: JSON.stringify(s3CfgData),
bucket: process.env.NC_S3_BUCKET_NAME,
region: process.env.NC_S3_REGION,
endpoint: process.env.NC_S3_ENDPOINT,
access_key: process.env.NC_S3_ACCESS_KEY,
access_secret: process.env.NC_S3_ACCESS_SECRET,
force_path_style: process.env.NC_S3_FORCE_PATH_STYLE === 'true',
acl: process.env.NC_S3_ACL,
}),
}); });
} }

24
packages/nocodb/src/models/KanbanView.ts

@ -65,24 +65,20 @@ export default class KanbanView implements KanbanType {
return view && new KanbanView(view); return view && new KanbanView(view);
} }
public static async IsColumnBeingUsedAsGroupingField( public static async getViewsByGroupingColId(
context: NcContext, context: NcContext,
columnId: string, columnId: string,
ncMeta = Noco.ncMeta, ncMeta = Noco.ncMeta,
) { ) {
return ( return await ncMeta.metaList2(
( context.workspace_id,
await ncMeta.metaList2( context.base_id,
context.workspace_id, MetaTable.KANBAN_VIEW,
context.base_id, {
MetaTable.KANBAN_VIEW, condition: {
{ fk_grp_col_id: columnId,
condition: { },
fk_grp_col_id: columnId, },
},
},
)
).length > 0
); );
} }

3
packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts

@ -194,6 +194,7 @@ export class DuplicateController {
@Param('modelId') modelId?: string, @Param('modelId') modelId?: string,
@Body() @Body()
body?: { body?: {
title?: string;
options?: { options?: {
excludeData?: boolean; excludeData?: boolean;
excludeViews?: boolean; excludeViews?: boolean;
@ -226,7 +227,7 @@ export class DuplicateController {
const models = await source.getModels(context); const models = await source.getModels(context);
const uniqueTitle = generateUniqueName( const uniqueTitle = generateUniqueName(
`${model.title} copy`, body.title || `${model.title} copy`,
models.map((p) => p.title), models.map((p) => p.title),
); );

12
packages/nocodb/src/plugins/GenericS3/GenericS3.ts

@ -14,21 +14,21 @@ import type { PutObjectRequest, S3 as S3Client } from '@aws-sdk/client-s3';
import type { IStorageAdapterV2, XcFile } from '~/types/nc-plugin'; import type { IStorageAdapterV2, XcFile } from '~/types/nc-plugin';
import { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils'; import { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils';
interface GenerocObjectStorageInput { interface GenericObjectStorageInput {
bucket: string; bucket: string;
region?: string; region?: string;
access_key: string; access_key?: string;
access_secret: string; access_secret?: string;
} }
export default class GenericS3 implements IStorageAdapterV2 { export default class GenericS3 implements IStorageAdapterV2 {
public name; public name;
protected s3Client: S3Client; protected s3Client: S3Client;
protected input: GenerocObjectStorageInput; protected input: GenericObjectStorageInput;
constructor(input: unknown) { constructor(input: GenericObjectStorageInput) {
this.input = input as GenerocObjectStorageInput; this.input = input;
} }
protected get defaultParams() { protected get defaultParams() {

15
packages/nocodb/src/plugins/s3/S3.ts

@ -7,8 +7,8 @@ import GenericS3 from '~/plugins/GenericS3/GenericS3';
interface S3Input { interface S3Input {
bucket: string; bucket: string;
region: string; region: string;
access_key: string; access_key?: string;
access_secret: string; access_secret?: string;
endpoint?: string; endpoint?: string;
acl?: string; acl?: string;
force_path_style?: boolean; force_path_style?: boolean;
@ -48,13 +48,16 @@ export default class S3 extends GenericS3 implements IStorageAdapterV2 {
public async init(): Promise<any> { public async init(): Promise<any> {
const s3Options: S3ClientConfig = { const s3Options: S3ClientConfig = {
region: this.input.region, region: this.input.region,
credentials: {
accessKeyId: this.input.access_key,
secretAccessKey: this.input.access_secret,
},
forcePathStyle: this.input.force_path_style ?? false, forcePathStyle: this.input.force_path_style ?? false,
}; };
if (this.input.access_key && this.input.access_secret) {
s3Options.credentials = {
accessKeyId: this.input.access_key,
secretAccessKey: this.input.access_secret,
}
}
if (this.input.endpoint) { if (this.input.endpoint) {
s3Options.endpoint = this.input.endpoint; s3Options.endpoint = this.input.endpoint;
} }

4
packages/nocodb/src/plugins/s3/index.ts

@ -38,14 +38,14 @@ const config: XcPluginConfig = {
label: 'Access Key', label: 'Access Key',
placeholder: 'Access Key', placeholder: 'Access Key',
type: XcType.SingleLineText, type: XcType.SingleLineText,
required: true, required: false,
}, },
{ {
key: 'access_secret', key: 'access_secret',
label: 'Access Secret', label: 'Access Secret',
placeholder: 'Access Secret', placeholder: 'Access Secret',
type: XcType.Password, type: XcType.Password,
required: true, required: false,
}, },
{ {
key: 'acl', key: 'acl',

21
packages/nocodb/src/run/timely.ts

@ -0,0 +1,21 @@
import path from 'path';
import cors from 'cors';
import express from 'express';
import Noco from '~/Noco';
const server = express();
server.enable('trust proxy');
server.use(cors());
server.use(
process.env.NC_DASHBOARD_URL ?? '/dashboard',
express.static(path.join(__dirname, 'nc-gui')),
);
server.set('view engine', 'ejs');
(async () => {
const httpServer = server.listen(process.env.PORT || 8080, async () => {
console.log(`App started successfully.\nVisit -> ${Noco.dashboardUrl}`);
server.use(await Noco.init({}, httpServer, server));
});
})().catch((e) => console.log(e));

8
packages/nocodb/src/schema/swagger-v2.json

@ -3883,6 +3883,11 @@
"excludeHooks": { "excludeHooks": {
"type": "boolean", "type": "boolean",
"required": false "required": false
},
"title": {
"type": "string",
"required": false,
"description": "New table title"
} }
} }
} }
@ -20568,7 +20573,6 @@
} }
}, },
"required": [ "required": [
"table_name",
"title" "title"
], ],
"x-stoplight": { "x-stoplight": {
@ -20839,7 +20843,7 @@
}, },
"required": [ "required": [
"columns", "columns",
"table_name" "title"
], ],
"title": "Table Request Model", "title": "Table Request Model",
"type": "object", "type": "object",

14
packages/nocodb/src/schema/swagger.json

@ -4489,6 +4489,11 @@
"excludeHooks": { "excludeHooks": {
"type": "boolean", "type": "boolean",
"required": false "required": false
},
"title": {
"type": "string",
"required": false,
"description": "New table title"
} }
} }
} }
@ -16046,7 +16051,8 @@
"enum": [ "enum": [
"all", "all",
"github", "github",
"youtube" "youtube",
"cloud"
] ]
}, },
"name": "type", "name": "type",
@ -23692,7 +23698,7 @@
"title": "Normal Column Request Model", "title": "Normal Column Request Model",
"type": "object", "type": "object",
"required": [ "required": [
"column_name" "title"
], ],
"x-stoplight": { "x-stoplight": {
"id": "fn3gqmojvswv2" "id": "fn3gqmojvswv2"
@ -25624,7 +25630,7 @@
} }
}, },
"required": [ "required": [
"table_name", "title",
"title" "title"
], ],
"x-stoplight": { "x-stoplight": {
@ -25904,7 +25910,7 @@
}, },
"required": [ "required": [
"columns", "columns",
"table_name" "title"
], ],
"title": "Table Request Model", "title": "Table Request Model",
"type": "object", "type": "object",

44
packages/nocodb/src/services/columns.service.ts

@ -16,6 +16,7 @@ import {
} from 'nocodb-sdk'; } from 'nocodb-sdk';
import { pluralize, singularize } from 'inflection'; import { pluralize, singularize } from 'inflection';
import hash from 'object-hash'; import hash from 'object-hash';
import { parseMetaProp } from 'src/utils/modelUtils';
import type { import type {
ColumnReqType, ColumnReqType,
LinkToAnotherColumnReqType, LinkToAnotherColumnReqType,
@ -1243,6 +1244,27 @@ export class ColumnsService {
await Column.update(context, param.columnId, { await Column.update(context, param.columnId, {
...colBody, ...colBody,
}); });
if (colBody.uidt === UITypes.SingleSelect) {
const kanbanViewsByColId = await KanbanView.getViewsByGroupingColId(
context,
column.id,
);
for (const kanbanView of kanbanViewsByColId) {
const view = await View.get(context, kanbanView.fk_view_id);
if (!view?.uuid) continue;
// Update groupingFieldColumn from view meta which will be used in shared kanban view
view.meta = parseMetaProp(view);
await View.update(context, view.id, {
...view,
meta: {
...view.meta,
groupingFieldColumn: colBody,
},
});
}
}
} else if (colBody.uidt === UITypes.User) { } else if (colBody.uidt === UITypes.User) {
// handle default value for user column // handle default value for user column
if (typeof colBody.cdf !== 'string') { if (typeof colBody.cdf !== 'string') {
@ -1517,6 +1539,15 @@ export class ColumnsService {
baseModel.getTnPath(table.table_name), baseModel.getTnPath(table.table_name),
column.column_name, column.column_name,
]); ]);
} else if (
column.uidt === UITypes.SingleSelect &&
column.uidt !== colBody.uidt &&
(await KanbanView.getViewsByGroupingColId(context, column.id)).length >
0
) {
NcError.badRequest(
`The column '${column.column_name}' is being used in Kanban View. Please update stack by field or delete Kanban View first.`,
);
} }
colBody = await getColumnPropsFromUIDT(colBody, source); colBody = await getColumnPropsFromUIDT(colBody, source);
@ -1596,6 +1627,11 @@ export class ColumnsService {
reuse?: ReusableParams; reuse?: ReusableParams;
}, },
) { ) {
// if column_name is defined and title is not defined, set title to column_name
if (param.column.column_name && !param.column.title) {
param.column.title = param.column.column_name;
}
validatePayload('swagger.json#/components/schemas/ColumnReq', param.column); validatePayload('swagger.json#/components/schemas/ColumnReq', param.column);
const reuse = param.reuse || {}; const reuse = param.reuse || {};
@ -1643,6 +1679,11 @@ export class ColumnsService {
param.column.title = param.column.title.trim(); param.column.title = param.column.title.trim();
} }
// if column_name missing then generate it from title
if (!param.column.column_name) {
param.column.column_name = param.column.title;
}
if (param.column.column_name) { if (param.column.column_name) {
// - 5 is a buffer for suffix // - 5 is a buffer for suffix
let colName = param.column.column_name.slice(0, mxColumnLength - 5); let colName = param.column.column_name.slice(0, mxColumnLength - 5);
@ -2585,7 +2626,8 @@ export class ColumnsService {
} }
case UITypes.SingleSelect: { case UITypes.SingleSelect: {
if ( if (
await KanbanView.IsColumnBeingUsedAsGroupingField(context, column.id) (await KanbanView.getViewsByGroupingColId(context, column.id))
.length > 0
) { ) {
NcError.badRequest( NcError.badRequest(
`The column '${column.column_name}' is being used in Kanban View. Please delete Kanban View first.`, `The column '${column.column_name}' is being used in Kanban View. Please delete Kanban View first.`,

48
packages/nocodb/src/services/tables.service.ts

@ -466,6 +466,20 @@ export class TablesService {
req?: any; req?: any;
}, },
) { ) {
// before validating add title for columns if only column name is present
if (param.table.columns) {
param.table.columns.forEach((c) => {
if (!c.title && c.column_name) {
c.title = c.column_name;
}
});
}
// before validating add title for table if only table name is present
if (!param.table.title && param.table.table_name) {
param.table.title = param.table.table_name
}
validatePayload('swagger.json#/components/schemas/TableReq', param.table); validatePayload('swagger.json#/components/schemas/TableReq', param.table);
const tableCreatePayLoad: Omit<TableReqType, 'columns'> & { const tableCreatePayLoad: Omit<TableReqType, 'columns'> & {
@ -558,13 +572,22 @@ export class TablesService {
} }
} }
if (!tableCreatePayLoad.title) {
NcError.badRequest('Missing table `title` property in request body');
}
if (!tableCreatePayLoad.table_name) {
tableCreatePayLoad.table_name = tableCreatePayLoad.title;
}
if ( if (
!tableCreatePayLoad.table_name || !(await Model.checkAliasAvailable(context, {
(base.prefix && base.prefix === tableCreatePayLoad.table_name) title: tableCreatePayLoad.title,
base_id: base.id,
source_id: source.id,
}))
) { ) {
NcError.badRequest( NcError.badRequest('Duplicate table alias');
'Missing table name `table_name` property in request body',
);
} }
if (source.type === 'databricks') { if (source.type === 'databricks') {
@ -608,16 +631,6 @@ export class TablesService {
); );
} }
if (
!(await Model.checkAliasAvailable(context, {
title: tableCreatePayLoad.title,
base_id: base.id,
source_id: source.id,
}))
) {
NcError.badRequest('Duplicate table alias');
}
const sqlMgr = await ProjectMgrv2.getSqlMgr(context, base); const sqlMgr = await ProjectMgrv2.getSqlMgr(context, base);
const sqlClient = await NcConnectionMgrv2.getSqlClient(source); const sqlClient = await NcConnectionMgrv2.getSqlClient(source);
@ -652,6 +665,11 @@ export class TablesService {
) { ) {
const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType); const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType);
// set column name using title if not present
if (!column.column_name && column.title) {
column.column_name = column.title;
}
// - 5 is a buffer for suffix // - 5 is a buffer for suffix
column.column_name = sanitizeColumnName( column.column_name = sanitizeColumnName(
column.column_name.slice(0, mxColumnLength - 5), column.column_name.slice(0, mxColumnLength - 5),

7
packages/nocodb/src/services/utils.service.ts

@ -80,7 +80,7 @@ export class UtilsService {
constructor(protected readonly configService: ConfigService<AppConfig>) {} constructor(protected readonly configService: ConfigService<AppConfig>) {}
lastSyncTime = dayjs(); lastSyncTime = null;
async versionInfo() { async versionInfo() {
if ( if (
@ -526,7 +526,10 @@ export class UtilsService {
} }
let payload = null; let payload = null;
if (dayjs().isAfter(this.lastSyncTime.add(3, 'hours'))) { if (
!this.lastSyncTime ||
dayjs().isAfter(this.lastSyncTime.add(3, 'hours'))
) {
payload = await T.payload(); payload = await T.payload();
this.lastSyncTime = dayjs(); this.lastSyncTime = dayjs();
} }

2
packages/nocodb/src/utils/TeleBatchProcessor.ts

@ -43,7 +43,7 @@ class TeleBatchProcessor {
return; return;
} }
await axios.post('https://nocodb.com/api/v1/telemetry', batch); await axios.post('https://telemetry.nocodb.com/api/v1/telemetry', batch);
} }
} }

1
packages/nocodb/src/utils/tele.ts

@ -264,6 +264,7 @@ class Tele {
xc_version: process.env.NC_SERVER_UUID, xc_version: process.env.NC_SERVER_UUID,
env: process.env.NODE_ENV || 'production', env: process.env.NODE_ENV || 'production',
oneClick: !!process.env.NC_ONE_CLICK, oneClick: !!process.env.NC_ONE_CLICK,
disabled: isDisabled,
}; };
try { try {
payload.os_type = os.type(); payload.os_type = os.type();

89
packages/nocodb/src/version-upgrader/upgraders/0225002_ncDatasourceDecrypt.ts

@ -11,20 +11,49 @@ const logger = {
}, },
}; };
const decryptConfig = async (encryptedConfig: string, secret: string) => { const decryptConfigWithFallbackKey = async ({
encryptedConfig,
secret,
fallbackSecret,
fallbackToNullIfFailed = false,
}: {
encryptedConfig: string;
secret: string;
fallbackSecret?: string;
fallbackToNullIfFailed?: boolean;
}) => {
if (!encryptedConfig) return encryptedConfig; if (!encryptedConfig) return encryptedConfig;
const decryptedVal = CryptoJS.AES.decrypt(encryptedConfig, secret).toString(
CryptoJS.enc.Utf8,
);
// validate by parsing JSON
try { try {
JSON.parse(decryptedVal); const decryptedVal = CryptoJS.AES.decrypt(encryptedConfig, secret).toString(
} catch { CryptoJS.enc.Utf8,
throw new Error('Config decryption failed'); );
let parsedVal;
// validate by parsing JSON
try {
parsedVal = JSON.parse(decryptedVal);
} catch (parseError) {
throw new Error(`JSON parse failed: ${parseError.message}`);
}
// if parsed value is null, return null
return parsedVal === null ? null : decryptedVal;
} catch (e) {
if (fallbackSecret) {
logger.log('Retrying decryption with a fallback mechanism');
return decryptConfigWithFallbackKey({
encryptedConfig,
secret: fallbackSecret,
});
}
if (fallbackToNullIfFailed) {
return null;
}
throw e;
} }
return decryptedVal;
}; };
// decrypt datasource details in source table and integration table // decrypt datasource details in source table and integration table
@ -32,13 +61,18 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
logger.log('Starting decryption of sources and integrations'); logger.log('Starting decryption of sources and integrations');
let encryptionKey = process.env.NC_AUTH_JWT_SECRET; let encryptionKey = process.env.NC_AUTH_JWT_SECRET;
let fallbackEncryptionKey: string | null = null;
const encryptionKeyFromMeta = (
await ncMeta.metaGet(RootScopes.ROOT, RootScopes.ROOT, MetaTable.STORE, {
key: 'nc_auth_jwt_secret',
})
)?.value;
if (!encryptionKey) { if (!encryptionKey) {
encryptionKey = ( encryptionKey = encryptionKeyFromMeta;
await ncMeta.metaGet(RootScopes.ROOT, RootScopes.ROOT, MetaTable.STORE, { } else {
key: 'nc_auth_jwt_secret', fallbackEncryptionKey = encryptionKeyFromMeta;
})
)?.value;
} }
// if encryption key is same as previous, just update is_encrypted flag and return // if encryption key is same as previous, just update is_encrypted flag and return
@ -61,7 +95,7 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
throw Error('Encryption key not found'); throw Error('Encryption key not found');
} }
// get all external sources // get all sources
const sources = await ncMeta.knexConnection(MetaTable.SOURCES); const sources = await ncMeta.knexConnection(MetaTable.SOURCES);
const passed = []; const passed = [];
@ -70,7 +104,13 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
for (const source of sources) { for (const source of sources) {
if (source?.config) { if (source?.config) {
try { try {
const decrypted = await decryptConfig(source.config, encryptionKey); const decrypted = await decryptConfigWithFallbackKey({
encryptedConfig: source.config,
secret: encryptionKey,
fallbackSecret: fallbackEncryptionKey,
// if source is meta, fallback to null if decryption failed as it is not required and the actual value is JSON `null` string
fallbackToNullIfFailed: source.is_meta,
});
await ncMeta await ncMeta
.knexConnection(MetaTable.SOURCES) .knexConnection(MetaTable.SOURCES)
.update({ .update({
@ -78,7 +118,11 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
}) })
.where('id', source.id); .where('id', source.id);
logger.log(`Decrypted source ${source.id}`); logger.log(`Decrypted source ${source.id}`);
passed.push(true);
// skip pushing to passed if it is meta source
if (!source.is_meta) {
passed.push(true);
}
} catch (e) { } catch (e) {
logger.error(`Failed to decrypt source ${source.id}`); logger.error(`Failed to decrypt source ${source.id}`);
passed.push(false); passed.push(false);
@ -93,10 +137,11 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
for (const integration of integrations) { for (const integration of integrations) {
if (integration?.config) { if (integration?.config) {
try { try {
const decrypted = await decryptConfig( const decrypted = await decryptConfigWithFallbackKey({
integration.config, encryptedConfig: integration.config,
encryptionKey, secret: encryptionKey,
); fallbackSecret: fallbackEncryptionKey,
});
await ncMeta await ncMeta
.knexConnection(MetaTable.INTEGRATIONS) .knexConnection(MetaTable.INTEGRATIONS)
.update({ .update({

5
packages/nocodb/tests/unit/rest/tests/table.test.ts

@ -48,13 +48,12 @@ function tableStaticTests() {
expect(response.body.list).to.be.an('array').not.empty; expect(response.body.list).to.be.an('array').not.empty;
}); });
it('Create table with no table name', async function () { it('Create table with no table title', async function () {
const response = await request(context.app) const response = await request(context.app)
.post(`/api/v1/db/meta/projects/${base.id}/tables`) .post(`/api/v1/db/meta/projects/${base.id}/tables`)
.set('xc-auth', context.token) .set('xc-auth', context.token)
.send({ .send({
table_name: undefined, title: undefined,
title: 'new_title',
columns: defaultColumns(context), columns: defaultColumns(context),
}) })
.expect(400); .expect(400);

57
packages/nocodb/webpack.timely.config.js

@ -0,0 +1,57 @@
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const webpack = require('webpack');
const CopyPlugin = require('copy-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { resolveTsAliases } = require('./build-utils/resolveTsAliases');
module.exports = {
entry: './src/run/timely.ts',
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
},
],
},
optimization: {
minimize: true, //Update this to true or false
minimizer: [new TerserPlugin()],
nodeEnv: false,
},
externals: [
nodeExternals({
allowlist: ['nocodb-sdk'],
}),
],
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json'],
alias: resolveTsAliases(path.resolve('./tsconfig.json')),
},
mode: 'production',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'docker'),
library: 'libs',
libraryTarget: 'umd',
globalObject: "typeof self !== 'undefined' ? self : this",
},
node: {
__dirname: false,
},
plugins: [
new webpack.EnvironmentPlugin(['EE']),
new CopyPlugin({
patterns: [{ from: 'src/public', to: 'public' }],
}),
],
target: 'node',
};

33213
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

12
pnpm-workspace.yaml

@ -1,7 +1,7 @@
packages: packages:
- 'packages/nocodb-sdk' - "packages/nocodb-sdk"
- 'packages/nc-gui' - "packages/nc-gui"
- 'packages/nc-mail-templates' - "packages/nc-mail-templates"
- 'packages/nocodb' - "packages/nocodb"
- 'tests/playwright' - "tests/playwright"
- 'packages/nc-secret-mgr' - "packages/nc-secret-mgr"

2
tests/playwright/tests/db/features/language.spec.ts

@ -23,8 +23,10 @@ const langMenu = [
'id.json', 'id.json',
'it.json', 'it.json',
'ja.json', 'ja.json',
'kn.json',
'ko.json', 'ko.json',
'lv.json', 'lv.json',
'ml.json',
'nl.json', 'nl.json',
'no.json', 'no.json',
'pl.json', 'pl.json',

Loading…
Cancel
Save