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:
paths:
- 'docker-compose/1_Auto_Upstall/noco.sh'
- '.github/workflows/bats-test.yml'
workflow_dispatch:
jobs:
@ -18,7 +19,7 @@ jobs:
- name: Prepare matrix for test files
id: set-matrix
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 '"' "'")
echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT
test:
@ -39,7 +40,7 @@ jobs:
- name: Get working directory
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
- name: Run BATS test

406
.github/workflows/ci-cd.yml

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

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

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

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

@ -4,7 +4,7 @@ on:
workflow_call:
inputs:
shard:
description: 'Shard number'
description: "Shard number"
required: true
type: string
db:
@ -19,7 +19,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- 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
uses: actions/setup-node@v3
with:
@ -27,7 +27,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 8
version: 9
- name: Get pnpm store directory
shell: bash
run: |
@ -42,7 +42,7 @@ jobs:
- name: setup pg
if: ${{ inputs.db == 'pg' || ( inputs.db == 'sqlite' && inputs.shard == '1' ) }}
working-directory: ./
run: |
run: |
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
sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD 'password';"
@ -144,32 +144,32 @@ jobs:
working-directory: ./tests/playwright
run: E2E_DB_TYPE=${{ inputs.db }} node ./scripts/stressTestNewlyAddedTest.js
# # Quick tests (pg on sqlite shard 0 and sqlite on sqlite shard 1)
# - name: Run quick server and tests (pg)
# if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }}
# working-directory: ./packages/nocodb
# run: |
# kill -9 $(lsof -t -i:8080)
# npm run watch:run:playwright:pg:cyquick > quick_${{ inputs.shard }}_test_backend.log &
# - name: Run quick server and tests (sqlite)
# if: ${{ inputs.db == 'sqlite' && inputs.shard == '2' }}
# working-directory: ./packages/nocodb
# run: |
# kill -9 $(lsof -t -i:8080)
# npm run watch:run:playwright:quick > quick_${{ inputs.shard }}_test_backend.log &
# - name: Wait for backend for sqlite-tests
# if: ${{ inputs.db == 'sqlite' }}
# working-directory: ./tests/playwright
# run: |
# while ! curl --output /dev/null --silent --head --fail http://localhost:8080; do
# printf '.'
# sleep 2
# done
# timeout-minutes: 1
# - name: Run quick tests
# if: ${{ inputs.db == 'sqlite' }}
# working-directory: ./tests/playwright
# run: PLAYWRIGHT_HTML_REPORT=playwright-report-quick npm run test:quick
# # Quick tests (pg on sqlite shard 0 and sqlite on sqlite shard 1)
# - name: Run quick server and tests (pg)
# if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }}
# working-directory: ./packages/nocodb
# run: |
# kill -9 $(lsof -t -i:8080)
# npm run watch:run:playwright:pg:cyquick > quick_${{ inputs.shard }}_test_backend.log &
# - name: Run quick server and tests (sqlite)
# if: ${{ inputs.db == 'sqlite' && inputs.shard == '2' }}
# working-directory: ./packages/nocodb
# run: |
# kill -9 $(lsof -t -i:8080)
# npm run watch:run:playwright:quick > quick_${{ inputs.shard }}_test_backend.log &
# - name: Wait for backend for sqlite-tests
# if: ${{ inputs.db == 'sqlite' }}
# working-directory: ./tests/playwright
# run: |
# while ! curl --output /dev/null --silent --head --fail http://localhost:8080; do
# printf '.'
# sleep 2
# done
# timeout-minutes: 1
# - name: Run quick tests
# if: ${{ inputs.db == 'sqlite' }}
# working-directory: ./tests/playwright
# run: PLAYWRIGHT_HTML_REPORT=playwright-report-quick npm run test:quick
- uses: actions/upload-artifact@v3
if: ${{ inputs.db == 'sqlite' }}

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

@ -17,9 +17,9 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 8
version: 9
- 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
shell: bash
run: |
@ -47,4 +47,3 @@ jobs:
zip -r ${FILE} .output || echo "UI build directory does not exists" >&2
echo "uploading ${FILE} to http://65.21.27.147/upload/${FILE}"
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
uses: pnpm/action-setup@v4
with:
version: 8
version: 9
- name: Get Docker Repository
id: get-docker-repository
run: |
@ -135,6 +135,8 @@ jobs:
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
push: true
labels: |
"service=nocodb"
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 }}

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

@ -43,29 +43,20 @@ jobs:
nightly_build_tag: ${{ steps.tag-step.outputs.NIGHTLY_BUILD_TAG }}
is_daily: ${{ steps.tag-step.outputs.IS_DAILY }}
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
release-executables:
needs: [set-tag, release-npm]
uses: ./.github/workflows/release-timely-executables.yml
with:
tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.nightly_build_tag }}
secrets:
NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# release-executables:
# needs: [set-tag, release-npm]
# uses: ./.github/workflows/release-timely-executables.yml
# with:
# tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.nightly_build_tag }}
# secrets:
# NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# Build docker image and push to docker hub
release-docker:
needs: [set-tag, release-npm]
uses: ./.github/workflows/release-docker.yml
needs: [set-tag]
uses: ./.github/workflows/release-timely-docker.yml
with:
currentVersion: ${{ needs.set-tag.outputs.current_version }}
tag: ${{ needs.set-tag.outputs.nightly_build_tag }}

10
.github/workflows/release-npm.yml

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

15
.github/workflows/release-pr.yml

@ -50,22 +50,11 @@ jobs:
target_tag: ${{ steps.tag-step.outputs.TARGET_TAG }}
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
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' }}
needs: [release-npm, set-tag]
uses: ./.github/workflows/release-docker.yml
needs: [set-tag]
uses: ./.github/workflows/release-timely-docker.yml
with:
currentVersion: ${{ needs.set-tag.outputs.current_version }}
tag: ${{ needs.set-tag.outputs.target_tag }}

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

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

11
.github/workflows/unit-test.yml

@ -5,17 +5,16 @@ name: Backend Unit Tests
on:
push:
branches: [ "develop" ]
branches: ["develop"]
paths:
- "packages/nocodb/**"
pull_request:
branches: [ "develop" ]
branches: ["develop"]
paths:
- "packages/nocodb/**"
jobs:
unit-tests:
runs-on: ubuntu-latest
strategy:
@ -27,15 +26,15 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 8
version: 9
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache: "pnpm"
- 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
run: pnpm bootstrap
- name: run unit tests

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

@ -12,11 +12,11 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 8
version: 9
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18.19.1
node-version: 18.19.1
- name: Checkout
uses: actions/checkout@v3
with:
@ -32,10 +32,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
signoff: true
branch: 'bot/update-nocodb-sdk-path'
branch: "bot/update-nocodb-sdk-path"
delete-branch: true
title: 'Update nocodb-sdk to local path'
labels: 'Bot: Automerge'
title: "Update nocodb-sdk to local path"
labels: "Bot: Automerge"
- name: Check outputs
run: |
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.
> 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
@ -109,6 +113,12 @@ Auto-upstall does the following : 🕊
> 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
![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
docker-compose/1_Auto_Upstall/tests/expects/install/redis.sh

@ -18,6 +18,9 @@ send "Y\r"
expect "Choose Community or Enterprise Edition*"
send "\r"
expect "Select PostgreSQL or SQLite as your database*"
send "P\r"
expect "Do you want to enabled Redis for caching*"
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*"
send "\r"
expect "Select PostgreSQL or SQLite as your database*"
send "P\r"
expect "Do you want to enabled Redis for caching*"
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*"
send "\r"
expect "Select PostgreSQL or SQLite as your database*"
send "P\r"
expect "Do you want to enabled Redis for caching*"
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 checkboxMeta = computed(() => {
const icon = extractCheckboxIcon(column?.value?.meta)
return {
icon: {
checked: 'mdi-check-circle-outline',
unchecked: 'mdi-checkbox-blank-circle-outline',
},
color: 'primary',
...parseProp(column?.value?.meta),
icon,
}
})

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

@ -381,6 +381,7 @@ const onFocus = () => {
v-for="op of options"
:key="op.title"
:value="op.title"
class="gap-2"
:data-testid="`select-option-${column.title}-${location === 'filter' ? 'filter' : rowIndex}`"
:class="`nc-select-option-${column.title}-${op.title}`"
>
@ -487,6 +488,7 @@ const onFocus = () => {
v-for="op of options"
:key="op.id || op.title"
:value="op.title"
class="gap-2"
:data-testid="`select-option-${column.title}-${location === 'filter' ? 'filter' : rowIndex}`"
:class="`nc-select-option-${column.title}-${op.title}`"
@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 ratingMeta = computed(() => {
const icon = extractRatingIcon(column?.value?.meta)
return {
icon: {
full: 'mdi-star',
empty: 'mdi-star-outline',
},
color: '#fcb401',
max: 5,
...parseProp(column.value?.meta),
icon,
}
})

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

@ -407,6 +407,7 @@ const onFocus = () => {
v-for="op of options"
:key="op.title"
:value="op.title"
class="gap-2"
:data-testid="`select-option-${column.title}-${rowIndex}`"
:class="`nc-select-option-${column.title}-${op.title}`"
@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 */
const beforeUpload = (file: UploadFile) => {
const exceedLimit = file.size! / 1024 / 1024 > 5
const exceedLimit = file.size! / 1024 / 1024 > 25
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
}

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

@ -1,5 +1,9 @@
<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>()
@ -8,12 +12,16 @@ const { isLoading } = useInfiniteScroll(
async () => {
if (isLoading.value) return
await loadFeed({
type: 'github',
type: props.type,
loadMore: true,
})
},
{ distance: 4 },
)
const feeds = computed(() => {
return props.type === 'github' ? githubFeed.value : cloudFeed.value
})
</script>
<template>
@ -24,15 +32,18 @@ const { isLoading } = useInfiniteScroll(
}"
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">
<FeedError page="github" />
<div
v-if="(props.type === 'github' ? isErrorOccurred.github : isErrorOccurred.cloud) && !feeds.length"
class="h-full flex justify-center items-center"
>
<FeedError :page="type" />
</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" />
</div>
<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>
</template>

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

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

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

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

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

@ -37,7 +37,9 @@ onMounted(() => {
<GeneralLoader size="xlarge" />
</div>
<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>
</template>

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

@ -21,7 +21,13 @@ const tabs: Array<{
container: FeedRecents,
},
{
key: 'changelog',
key: 'cloud',
icon: 'ncCloud',
title: 'Cloud Changelog',
container: FeedChangelog,
},
{
key: 'github',
icon: 'ncList',
title: 'Changelog',
container: FeedChangelog,
@ -89,13 +95,13 @@ onMounted(() => {
<div class="relative">
<FeedSocial
:class="{
'normal-left': tab.key === 'recents' || tab.key === 'youtube',
'changelog-left': tab.key === 'changelog',
'normal-left': ['recents', 'youtube', 'cloud'].includes(tab.key),
'changelog-left': tab.key === 'github',
'changelog-twitter': tab.key === 'twitter',
}"
class="absolute social-card"
/>
<component :is="tab.container" />
<component :is="tab.container" :type="tab.key" />
</div>
</a-tab-pane>
</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)
// cater existing v1 cases
const iconList = [
{
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 iconList = checkboxIconList
const picked = computed({
get: () => vModel.value.meta.color,
@ -54,6 +25,7 @@ const isOpenColorPicker = ref(false)
vModel.value.meta = {
...columnDefaultMeta[UITypes.Checkbox],
...(vModel.value.meta || {}),
icon: extractCheckboxIcon(vModel.value.meta || {}),
}
// 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 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 {
const parsed = await validateFormulaAndExtractTreeWithType({
formula: vModel.value.formula || vModel.value.formula_raw,
@ -94,13 +106,28 @@ const parsedTree = computedAsync(async () => {
clientOrSqlUi: source.value?.type as any,
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) {
return {
dataType: FormulaDataTypes.UNKNOWN,
// Update parsedTree only if this is the latest invocation
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
setAdditionalValidations({

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

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

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

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk'
const { visibleColumns, activeField, allViewFilters, localColumnsMapByFkColumnId } = useFormViewStoreOrThrow()
const { visibleColumns, activeField, allViewFilters, localColumns, localColumnsMapByFkColumnId } = useFormViewStoreOrThrow()
const isOpen = ref<boolean>(false)
@ -9,6 +9,17 @@ const allFilters = ref({})
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(() => {
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 fieldNameAlias = inject(FieldNameAlias, ref({} as Record<string, string>))
const { metas } = useMetas()
const localValue = computed({
@ -110,7 +112,7 @@ const options = computed<SelectProps['options']>(() =>
})
?.map((c: ColumnType) => ({
value: c.id,
label: c.title,
label: fieldNameAlias.value[c.id!] || c.title,
icon: h(
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(ActiveCellInj, ref(true))
provide(IsFormInj, ref(true))
const isSingleOrMultiSelect = computed(() => {
return filterType.value === 'isSingleSelect' || filterType.value === 'isMultiSelect'
})
</script>
<template>
@ -209,7 +213,7 @@ provide(IsFormInj, ref(true))
<div
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="{ 'px-2': hasExtraPadding, 'border-brand-500': isInputBoxOnFocus }"
:class="{ 'px-2': hasExtraPadding, 'border-brand-500': isInputBoxOnFocus, '!max-w-100': isSingleOrMultiSelect }"
@mouseup.stop
>
<component

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

@ -5,6 +5,8 @@ const { isMobileMode } = useGlobal()
const { isSharedBase, base } = storeToRefs(useBase())
const { sharedView } = useSharedView()
const { t } = useI18n()
const { $api, $e } = useNuxtApp()
@ -188,7 +190,7 @@ function openDeleteDialog() {
</NcTooltip>
</div>
<NcDropdown
v-else
v-else-if="!sharedView"
v-model:visible="isDropdownOpen"
class="!xs:pointer-events-none nc-actions-menu-btn nc-view-context-btn"
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
const { collapsed, ...rest } = stackMetaObj.value[fk_grp_col_id][idx]
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
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
formattedData.value.set(
stackTitle,
formattedData.value.get(stackTitle)!.map((o) => ({
(formattedData.value.get(stackTitle) || []).map((o) => ({
...o,
row: {
...o.row,
@ -624,6 +628,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
}
function removeRowFromUncategorizedStack() {
if (isPublic.value) return
// remove the last record
formattedData.value.get(null)!.pop()
// 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 cloudFeed = ref<ProductFeedItem[]>([])
const isErrorOccurred = reactive({
youtube: false,
github: 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 {
let page = 1
@ -35,6 +38,9 @@ export const useProductFeed = createSharedComposable(() => {
case 'all':
page = Math.ceil(socialFeed.value.length / 10) + 1
break
case 'cloud':
page = Math.ceil(cloudFeed.value.length / 10) + 1
break
}
}
@ -50,6 +56,9 @@ export const useProductFeed = createSharedComposable(() => {
case 'all':
socialFeed.value = [...socialFeed.value, ...response] as ProductFeedItem[]
break
case 'cloud':
cloudFeed.value = [...cloudFeed.value, ...response] as ProductFeedItem[]
break
}
} catch (error) {
switch (type) {
@ -62,6 +71,9 @@ export const useProductFeed = createSharedComposable(() => {
case 'all':
isErrorOccurred.social = true
break
case 'cloud':
isErrorOccurred.cloud = true
break
}
console.error(error)
return []
@ -113,6 +125,7 @@ export const useProductFeed = createSharedComposable(() => {
youtubeFeed,
githubFeed,
socialFeed,
cloudFeed,
loadFeed,
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')
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 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 = [
{ value: 'CODE128', label: 'CODE128' },
{ value: 'upc', label: 'UPC' },

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

@ -39,67 +39,67 @@
}
},
"aggregation": {
"sum": "",
"sum": "Sum",
"count": "Cuenta",
"min": "Min",
"max": "Máximo",
"avg": "Media",
"min": "Mín",
"max": "Máx.",
"avg": "Prom",
"median": "Mediana",
"std_dev": "Desviación estándar",
"histogram": "Histograma",
"range": "Rango",
"percent_empty": "Vacío",
"percent_filled": "Lleno",
"percent_unique": "Único",
"count_unique": "Único",
"percent_filled": "Rellenado",
"percent_unique": "Unico",
"count_unique": "Unico",
"count_empty": "Vacío",
"count_filled": "Lleno",
"count_filled": "Rellenado",
"earliest_date": "Fecha mínima",
"latest_date": "Fecha máxima",
"date_range": "Rango de fechas",
"month_range": "Rango de meses",
"checked": "Marcado",
"unchecked": "Desmarcado",
"percent_checked": "Marcado",
"percent_unchecked": "Desmarcado",
"date_range": "Rango",
"month_range": "Rango",
"checked": "Comprobado",
"unchecked": "No seleccionado",
"percent_checked": "Comprobado",
"percent_unchecked": "No seleccionado",
"attachment_size": "Tamaño",
"none": "Ninguno"
"none": "Nada"
},
"aggregation_type": {
"sum": "Suma",
"count": "Cuenta",
"min": "Min",
"max": "Máximo",
"avg": "Media",
"min": "Mín.",
"max": "Máx.",
"avg": "Promedio",
"median": "Mediana",
"std_dev": "Desviación estándar",
"histogram": "Histograma",
"range": "Rango",
"percent_empty": "Porcentaje vacío",
"percent_filled": "Porcentaje lleno",
"percent_filled": "Porcentaje cubierto",
"percent_unique": "Porcentaje único",
"count_unique": "Único",
"count_unique": "Unico",
"count_empty": "Vacío",
"count_filled": "Lleno",
"count_filled": "Rellenado",
"earliest_date": "Fecha más temprana",
"latest_date": "Fecha más reciente",
"latest_date": "Última Fecha",
"date_range": "Rango de fechas",
"month_range": "Rango de meses",
"checked": "Marcado",
"unchecked": "Desmarcado",
"percent_checked": "Porcentaje Marcado",
"percent_unchecked": "Porcentaje Desmarcado",
"attachment_size": "Tamaño del Adjunto",
"month_range": "Rango de mes",
"checked": "Comprobado",
"unchecked": "No seleccionado",
"percent_checked": "Porcentaje comprobado",
"percent_unchecked": "Porcentaje Sin comprobar",
"attachment_size": "Tamaño del archivo adjunto",
"none": "Ninguno"
},
"general": {
"scripts": "Comandos",
"configure": "Configurar",
"switch": "Cambiar",
"on": "Encendido",
"onMultiple": "En Múltiple",
"switch": "Interruptor",
"on": "Activado",
"onMultiple": "En múltiples",
"manual": "Manual",
"trigger": "Activar",
"trigger": "Disparador",
"addLookupField": "Add {count} lookup fields",
"style": "Estilo",
"label": "Etiqueta",
@ -152,7 +152,7 @@
"code": "Código",
"duplicate": "Duplicar",
"duplicating": "Duplicando",
"duplicateEntity": "Duplicar {entity}",
"duplicateEntity": "Duplicado {entity}",
"activate": "Activar",
"action": "Acción",
"insert": "Insertar",
@ -167,7 +167,7 @@
"rename": "Renombrar",
"renameEntity": "Renombrar {entity}",
"reload": "Recargar",
"refresh": "Refrescar",
"refresh": "Actualizar",
"reset": "Reiniciar",
"install": "Instalar",
"show": "Mostrar",
@ -225,7 +225,7 @@
"betaNote": "Esta función se encuentra actualmente en fase beta.",
"moreInfo": "Puede encontrar más información aquí",
"logs": "Registros",
"groupingField": "Agrupar por campo",
"groupingField": "Apilar por campo",
"insertAfter": "Insertar a la derecha",
"insertBefore": "Insertar a la izquierda",
"insertAbove": "Insertar arriba",
@ -237,7 +237,7 @@
"move": "Mover",
"geoDataField": "Campo GeoDatos",
"type": "Tipo",
"subType": "Subtipo",
"subType": "Sub-tipo",
"name": "Nombre",
"changes": "Cambios",
"new": "Nuevo",
@ -277,10 +277,10 @@
"appearance": "Apariencia",
"now": "Ahora",
"set": "Establecer",
"format": "Formato",
"format": "Plantilla",
"colour": "Color",
"use": "Usar",
"stack": "Pila",
"stack": "Apilado",
"ipAddress": "Dirección IP",
"integration": "Integración",
"integrations": "Integraciones",
@ -364,8 +364,8 @@
"googleCalendar": "Google Calendar",
"googleDrive": "Google Drive",
"googleSheets": "Google Sheets",
"hubspot": "Hubspot",
"serviceHub": "Centro de Servicio",
"hubspot": "HubSpot",
"serviceHub": "Centro de servicios",
"jira": "Jira",
"mailchimp": "Mailchimp",
"microsoftAccess": "Microsoft Access",
@ -373,7 +373,7 @@
"microsoftOutlook": "Microsoft Outlook",
"miro": "Miro",
"salesforce": "Salesforce",
"serviceCloud": "Nube de Servicio",
"serviceCloud": "Servicios en la Nube",
"snowflake": "Snowflake",
"stripe": "Stripe",
"surveyMonkey": "SurveyMonkey",
@ -386,10 +386,10 @@
"postgreSQL": "PostgreSQL",
"sqlite": "SQLite",
"dataBricks": "DataBricks",
"mssqlServer": "Servidor MSSQL",
"mssqlServer": "MSSQL Server",
"oracle": "Oracle",
"telegram": "Telegram",
"whatsapp": "Whatsapp",
"whatsapp": "WhatsApp",
"gmail": "Gmail",
"pipedrive": "Pipedrive",
"microsoftDynamics365": "Microsoft Dynamics 365",
@ -397,7 +397,7 @@
"greenhouse": "Greenhouse",
"lever": "Lever",
"bitbucket": "BitBucket",
"quickbooks": "Quickbooks",
"quickbooks": "QuickBooks",
"intercom": "Intercom",
"dropbox": "Dropbox",
"openai": "OpenAI",
@ -411,24 +411,24 @@
"allIntegrationsSubtitle": "",
"databaseSubtitle": "Conecte y gestione sin problemas sus bases de datos con NocoDB.",
"communication": "Comunicación",
"communicationSubtitle": "Reciba notificaciones de cambios y agilice la comunicación del equipo con NocoDB.",
"projectManagement": "Gestión de proyectos",
"projectManagementSubtitle": "Mejore los flujos de trabajo de proyectos y la gestión de tareas con NocoDB.",
"communicationSubtitle": "Reciba notificaciones sobre los cambios y agilice la comunicación del equipo con NocoDB.",
"projectManagement": "Administración de proyectos",
"projectManagementSubtitle": "Mejore los flujos de trabajo de los proyectos y la gestión de tareas con NocoDB.",
"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",
"marketingSubtitle": "Impulse sus esfuerzos de marketing con las potentes integraciones de NocoDB.",
"ats": "ATS",
"atsSubtitle": "Optimice su sistema de seguimiento de candidatos con NocoDB.",
"atsSubtitle": "Agilice su sistema de seguimiento de candidatos con NocoDB.",
"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",
"financeSubtitle": "Simplifique las operaciones financieras y la gestión de datos con NocoDB.",
"ticketing": "Ticketing",
"ticketingSubtitle": "Gestione y rastree tickets de soporte de manera eficiente con NocoDB.",
"ticketing": "Sistema de tickets",
"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.",
"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",
"spreadSheet": "Hoja de cálculo",
"spreadSheetSubtitle": "Conecte y gestione sus hojas de cálculo con NocoDB."
@ -488,13 +488,13 @@
},
"title": {
"searchWebhook": "Buscar webhook",
"webcam": "Cámara Web",
"uploadViaUrl": "Subir a través de URL",
"webcam": "Cámara web",
"uploadViaUrl": "Subir por URL",
"localFiles": "Local Files",
"renameBase": "Renombrar Base",
"renameWorkspace": "Renombrar Espacio de Trabajo",
"renamingWorkspace": "Renombrando Espacio de Trabajo",
"renamingBase": "Renombrando Base",
"renameBase": "Renombrar base",
"renameWorkspace": "Renombrar Espacio de trabajo",
"renamingWorkspace": "Renombrando el espacio de trabajo",
"renamingBase": "Renombrando la base",
"sso": "Autenticación (SSO)",
"docs": "Documentos",
"forum": "Foro",
@ -523,7 +523,7 @@
"removeFile": "Eliminar archivo",
"hasMany": "Tiene muchos",
"manyToMany": "Muchos a Muchos",
"oneToOne": "Uno a Uno",
"oneToOne": "Uno a uno",
"virtualRelation": "Relación virtual",
"linkMore": "Enlace más",
"linkMoreRecords": "Vincular más registros",
@ -617,7 +617,7 @@
"noOptionsFound": "No se encontraron opciones",
"surveyFormSubmitConfirmMsg": "¿Está seguro de que desea enviar este formulario?",
"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",
"fromFileAndExternalSources": "Desde archivos y fuentes externas",
"directlyInRealTime": "Directamente en tiempo real",
@ -630,12 +630,12 @@
"configuration": "Configuración",
"setup": "Configuración",
"configLabel": "Configurar {label}",
"switchToProd": "Cambiar a una base de datos lista para producción",
"sharedBase": "Shared Base",
"fieldID": "ID de Campo",
"addDescription": "Agregar descripción",
"switchToProd": "Cambie a una aplicación base de datos lista para producción",
"sharedBase": "Base compartida",
"fieldID": "ID de campo",
"addDescription": "Añadir descripción",
"editDescription": "Editar descripción",
"urlFormula": "Fórmula de URL",
"urlFormula": "Fórmula URL",
"selectIcon": "ninguno",
"selectAWebhook": "--seleccionar un webhook--",
"openUrl": "Abrir URL",
@ -644,27 +644,27 @@
"defaultView": "Vista por defecto",
"recordInsert": "Insertar registro",
"recordUpdate": "Actualizar registro",
"recordDelete": "Eliminar registro",
"supportDocs": "Docs de soporte",
"addedOn": "Añadido el",
"changeDisplayValueField": "Cambiar campo de valor de visualización",
"recordDelete": "Borrar registro",
"supportDocs": "Documentos de apoyo",
"addedOn": "Añadido él",
"changeDisplayValueField": "Cambiar el valor de campo a visualizar",
"selectYourNewTitleFor": "Select your new display value field for ",
"searchDisplayValue": "Seleccionar campo de valor de visualización",
"changeTitleField": "Cambiar campo de título",
"clearAll": "Borrar todo",
"addNewLookupHelperText1": "Los campos de búsqueda muestran datos de registros vinculados. Seleccione campos específicos de",
"addNewLookupHelperText2": " tabla para añadirlos como campos de búsqueda en esta tabla.",
"formatting": "Formato",
"selectAFormatType": "- -Seleccionar un tipo de formato (opcional)- -",
"searchDisplayValue": "Seleccione el valor de campo a visualizar",
"changeTitleField": "Cambiar el título del campo",
"clearAll": "Limpiar todo",
"addNewLookupHelperText1": "Los campos de búsqueda muestran los datos de los registros vinculados. Seleccione campos específicos de ",
"addNewLookupHelperText2": "Para agregarlos como campos de Búsqueda en esta tabla.",
"formatting": "Formateando",
"selectAFormatType": "-Seleccione un tipo de formulario (opcional)- -",
"formatType": "Tipo de formato",
"toUpload": "para subir",
"dragFilesHere": "arrastra los archivos aquí",
"browseFiles": "examinar archivos",
"clickTo": "Hacer clic para",
"allowAccessToYourCamera": "Por favor permite el acceso a tu cámara",
"toUpload": "Para cargar",
"dragFilesHere": "Arrastre los archivos aquí",
"browseFiles": "Examinar Archivos",
"clickTo": "Haga clic para",
"allowAccessToYourCamera": "Por favor, permita el acceso a su cámara",
"openFile": "Abrir archivo",
"enterValidUrl": "Introduce una URL válida para subir archivos",
"addFilesFromUrl": "Agregar archivos desde URL",
"enterValidUrl": "Introduzca una URL válida para cargar archivos",
"addFilesFromUrl": "Añadir archivos desde URL",
"uploading": "Subiendo",
"dropHere": "Soltar aquí",
"addMore": "Añadir más",
@ -674,46 +674,46 @@
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
"selectView": "Seleccionar una vista",
"connectionDetails": "Detalles de conexión de la fuente",
"connectionDetails": "Detalles de la conexión origen",
"metaSync": "Meta Sync",
"mention": "Mención",
"today": "Hoy",
"currentDate": "Fecha actual",
"workspace": "Espacio de trabajo",
"txt": "Valor de registro TXT",
"transferOwnership": "Transferir Propiedad",
"recentActivity": "Actividad Reciente",
"goToMembers": "Ir a los Miembros",
"addMember": "Añadir Miembro",
"numberOfMembers": "N.º de Miembros",
"numberOfBases": "N.º de Bases",
"numberOfRecords": "N.º de Registros",
"workspaceName": "Nombre del Espacio de Trabajo",
"workspaceWithoutOwner": "Espacio de Trabajo sin Propietarios",
"inviteUsersToWorkspace": "Invitar Usuarios al Espacio de Trabajo",
"txt": "Valor del registro TXT",
"transferOwnership": "Transferir la Propiedad",
"recentActivity": "Actividad reciente",
"goToMembers": "Ir a Miembros",
"addMember": "Añadir miembro",
"numberOfMembers": "No. Miembros",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Registros",
"workspaceName": "Nombre del espacio de trabajo",
"workspaceWithoutOwner": "Espacio de trabajo sin propietarios",
"inviteUsersToWorkspace": "Invitar usuarios al espacio de trabajo",
"selectWorkspace": "-seleccionar espacios de trabajo a los que invitar-",
"addMembersToOrganization": "Añadir Miembros a la Organización",
"memberIn": "Miembro en:",
"assignAs": "Asignar como",
"signOutUser": "Cerrar sesión del usuario",
"signOutUser": "Cerrar sesión de usuario",
"signOutUsers": "Cerrar sesión de usuarios",
"deactivateUser": "Desactivar usuario",
"deactivateUsers": "Desactivar usuarios",
"lastActive": "Último activo",
"dateAdded": "Fecha de incorporación",
"lastActive": "Última vez activo",
"dateAdded": "Fecha creación",
"uploadImage": "Subir imagen",
"organizationProfile": "Perfil de la organización",
"organizationImage": "Imagen de la organización",
"organizationName": "Nombre de la organización",
"activeDomains": "Dominios activos",
"domains": "Dominios",
"disablePublicSharing": "Deshabilitar compartición pública",
"shareSettings": "Configuración de compartir",
"disablePublicSharing": "Desactivar Compartir Público",
"shareSettings": "Compartir ajustes",
"deleteUserAndData": "Eliminar usuario y sus datos",
"userOptions": "Opciones de usuario",
"deleteThisOrganization": "Eliminar esta organización",
"dangerZone": "Zona de peligro",
"childView": "Vista hija",
"deleteThisOrganization": "Eliminar esta Organización",
"dangerZone": "Zona peligrosa",
"childView": "Vista Hijo",
"selectYear": "Seleccionar Año",
"save": "Guardar",
"cancel": "Cancelar",
@ -725,14 +725,14 @@
"newProvider": "Nuevo proveedor",
"generalSettings": "Ajustes Generales",
"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",
"addDomain": "Añadir dominio",
"addDomain": "Agregar dominio",
"domain": "Dominio",
"settings": "Configuración",
"settings": "Ajustes",
"workspaces": "Espacios de trabajo",
"back": "Volver",
"dashboard": "Panel de control",
"dashboard": "Dashboard",
"organizeBy": "Organizar por",
"previous": "Anterior",
"nextMonth": "Mes siguiente",
@ -754,12 +754,12 @@
"noToken": "Sin Token",
"tokenLimit": "Sólo se permite un token por usuario",
"duplicateAttachment": "Archivo con el nombre {filename} ya adjuntado",
"tableIdColon": "ID DE TABLA: {tableId}",
"tableIdColon": "ID de Tabla: {tableId}",
"viewIdColon": "VIEW ID: {viewId}",
"toAddress": "A Dirección",
"subject": "Asunto",
"body": "Cuerpo",
"commaSeparatedMobileNumber": "Número móvil separado por comas",
"commaSeparatedMobileNumber": "Número Celular separado por Comas",
"headerName": "Nombre del encabezado",
"icon": "Icono",
"max": "Máximo",
@ -788,12 +788,12 @@
"optional": "(Opcional)",
"clickToMake": "Pulsar para hacer",
"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",
"clickToHide": "Clic para ocultar",
"clickToDownload": "Clic para descargar",
"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",
"viewMode": "Modo solo lectura",
"searchUsers": "Buscar usuarios",
@ -840,9 +840,9 @@
"databaseType": "Tipo en base de datos",
"lengthValue": "Longitud/valor",
"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",
"hostAddress": "Dirección del servidor",
"hostAddress": "Dirección del host",
"port": "Número de puerto",
"username": "Usuario",
"password": "Contraseña",
@ -947,7 +947,7 @@
"hasMany": "tiene muchos",
"belongsTo": "pertenece a",
"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",
"commentsOnly": "Sólo comentarios",
"documentation": "Documentación",
@ -964,7 +964,7 @@
"noAccess": "Sin acceso",
"restApis": "API REST",
"apis": "API",
"apiSnippet": "Fragmentos de API",
"apiSnippet": "API Snippets",
"includeData": "Incluir datos",
"includeView": "Incluir vista",
"includeWebhook": "Incluir Webhook",
@ -988,36 +988,36 @@
"appearanceSettings": "Ajustes de apariencia",
"backgroundColor": "Color de Fondo",
"hideNocodbBranding": "Ocultar marca NocoDB",
"showOnConditions": "Show on conditions",
"showOnConditions": "Mostrar en condiciones",
"showFieldOnConditionsMet": "Muestra el campo sólo cuando se cumplen las condiciones",
"limitOptions": "Limitar opciones",
"limitOptionsSubtext": "Limite las opciones visibles a los usuarios seleccionando las opciones disponibles",
"clearSelection": "Borrar selección",
"limitOptions": "Limitar las opciones",
"limitOptionsSubtext": "Limitar opciones visibles para los usuarios seleccionando las opciones disponibles",
"clearSelection": "Limpiar selección",
"displayAsProgress": "Mostrar como progreso",
"relationType": "Tipo de relación",
"showThousandsSeparator": "Mostrar separador de miles",
"signUpForFree": "Regístrate gratis",
"signUpForFree": "Regístrese gratis",
"coverImageField": "Cover image field",
"fitImage": "Ajustar imagen",
"coverImageArea": "Imagen de portada",
"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"
},
"activity": {
"webhookDetails": "Detalles del Webhook",
"hideWeekends": "Ocultar fines de semana",
"webhookDetails": "Detalles de Webhook",
"hideWeekends": "Ocultar los fines de semana",
"renameBase": "Renombrar base",
"renameWorkspace": "Renombrar espacio de trabajo",
"deactivate": "Desactivar",
"manageUsers": "Gestionar usuarios",
"newWorkspace": "Espacio de trabajo nuevo",
"newWorkspace": "Nuevo espacio de trabajo",
"addDomain": "Añadir dominio",
"addMembers": "Agregar Miembros",
"enterEmail": "Ingrese direcciones de correo electrónico",
"inviteToBase": "Invitar a la Base",
"addMembers": "Añadir miembros",
"enterEmail": "Ingresar direcciones de correo electrónico",
"inviteToBase": "Invitar a la base",
"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",
"goToToday": "Ir a Hoy",
"toggleSidebar": "Alternar Barra Lateral",
@ -1230,7 +1230,7 @@
},
"kanban": {
"collapseStack": "Colapsar pila",
"collapseAll": "Colapsar todo",
"collapseAll": "Contraer todo",
"expandAll": "Expandir todo",
"renameStack": "Renombrar pila",
"deleteStack": "Borrar pila",
@ -1251,28 +1251,28 @@
"addFieldFromFormView": "Añadir Campo",
"selectAllFields": "Seleccionar todos los campos",
"preFilledFields": {
"title": "Habilitar Pre-rellenar",
"title": "Activar pre-rellenado",
"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",
"lockedFieldTooltip": "Valor pre-rellenado"
},
"getPreFilledLink": "Obtener enlace pre-rellenado",
"group": "Grupo",
"goToDocs": "Ir a Documentos",
"goToDocs": "Ir a documentos",
"addCondition": "Añadir condición",
"addConditionGroup": "Añadir grupo de condiciones"
},
"tooltip": {
"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.",
"optionalDatabaseName": "Opcional. Usa la base de datos predeterminada \"{database}\" si se deja en blanco",
"optionalSchemaName": "Opcional. Usa el esquema predeterminado \"{schema}\" si se deja en blanco.",
"schemaChangeDisabled": "La edición del esquema está deshabilitada para esta fuente de datos.",
"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. Utiliza la base de datos por defecto \"{database}\" 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á desactivada para esta fuente de datos.",
"typeNotAllowed": "Este tipo de datos no está permitido.",
"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.",
"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.",
"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 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",
"saveChanges": "Guardar cambios",
"xcDB": "Crear un nuevo proyecto",
@ -1304,9 +1304,9 @@
"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.",
"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",
"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": {
"searchIcons": "Buscar iconos",
@ -1321,7 +1321,7 @@
"projName": "Ingresa el nombre de proyecto",
"selectGroupField": "Seleccione un campo de agrupación",
"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",
"notSelected": "-no seleccionado-",
"selectGeoFieldNotFound": "No se encuentra ningún campo de datos geográficos. Por favor, cree uno primero.",
@ -1357,7 +1357,7 @@
"key": "Clave",
"createTable": "¡Crea tu primera tabla!",
"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.",
"inviteYourTeam": "Invita a tu equipo",
"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": "질문",
"reachOut": "문의하기",
"betaNote": "이 기능은 현재 베타입니다.",
"moreInfo": "자세한 내용은 여기를 참조하십시오.",
"moreInfo": "자세한 내용은 여기를 참조하십시오",
"logs": "로그",
"groupingField": "필드별 스택",
"insertAfter": "오른쪽에 삽입",
@ -352,7 +352,7 @@
"short": "짧음",
"medium": "중간",
"tall": "높음",
"extra": "추가"
"extra": "매우 큰"
},
"externalDb": "외부 데이터베이스",
"syncData": {
@ -374,10 +374,10 @@
"miro": "Miro",
"salesforce": "Salesforce",
"serviceCloud": "Service Cloud",
"snowflake": "스노우플레이크",
"snowflake": "Snowflake",
"stripe": "Stripe",
"surveyMonkey": "SurveyMonkey",
"tableau": "타블로",
"tableau": "Tableau",
"trello": "Trello",
"typeform": "Typeform",
"workday": "Workday",
@ -536,7 +536,7 @@
"deleteWs": "작업 공간 삭제",
"deletingWs": "작업 공간 삭제 중",
"copyAuthToken": "인증 토큰 복사",
"copiedAuthToken": "인증 토큰이 복사되었습니다.",
"copiedAuthToken": "인증 토큰이 복사되었습니다",
"copyInviteToken": "초대 토큰 복사",
"showSidebar": "사이드바 표시",
"hideSidebar": "사이드바 숨김",
@ -560,7 +560,7 @@
"projMeta": "프로젝트 메타 데이터",
"metaMgmt": "메타 관리",
"metadata": "메타 데이터",
"exportImportMeta": "메타 데이터 내보내기/가져오기 ",
"exportImportMeta": "메타 데이터 내보내기/가져오기",
"uiACL": "UI 액세스 제어",
"metaOperations": "메타 데이터 작업",
"audit": "감사",
@ -616,7 +616,7 @@
"selectFieldsFromRightPannelToAddHere": "오른쪽 패널에서 필드를 선택하여 여기에 추가",
"noOptionsFound": "옵션을 찾을 수 없습니다",
"surveyFormSubmitConfirmMsg": "이 양식을 제출하시겠습니까?",
"noResultsMatchedYourSearch": "Your search did not yield any matching results.",
"noResultsMatchedYourSearch": "검색 결과와 일치하는 항목이 없습니다",
"looksLikeThisStackIsEmpty": "이 목록에는 현재 데이터가 없습니다",
"fromScratch": "처음부터",
"fromFileAndExternalSources": "파일 & 외부 소스에서",
@ -653,7 +653,7 @@
"changeTitleField": "제목 필드 변경",
"clearAll": "모두 지우기",
"addNewLookupHelperText1": "조회 필드는 연결된 레코드의 데이터를 표시합니다. 다음에서 특정 필드를 선택하세요 ",
"addNewLookupHelperText2": "이 테이블에 조회 필드로 추가하려면 테이블을 클릭합니다.",
"addNewLookupHelperText2": "이 테이블에 조회 필드로 추가하려면 테이블을 클릭합니다",
"formatting": "서식 설정",
"selectAFormatType": "- -양식 유형을 선택하세요(선택 사항)- -",
"formatType": "형식 유형",
@ -663,7 +663,7 @@
"clickTo": "클릭하여",
"allowAccessToYourCamera": "카메라에 대한 접근을 허용해주세요",
"openFile": "파일 열기",
"enterValidUrl": "파일을 업로드하려면 유효한 URL을 입력하세요.",
"enterValidUrl": "파일을 업로드하려면 유효한 URL을 입력하세요",
"addFilesFromUrl": "URL에서 파일 추가",
"uploading": "업로드 중",
"dropHere": "이곳에 끌어다 놓기",
@ -678,7 +678,7 @@
"metaSync": "메타 동기화",
"mention": "언급",
"today": "오늘",
"currentDate": "현재 날짜 ",
"currentDate": "현재 날짜",
"workspace": "워크스페이스",
"txt": "TXT 레코드 값",
"transferOwnership": "소유권 이전",
@ -721,7 +721,7 @@
"audience-entityId": "청중/엔티티 ID",
"redirectUrl": "리디렉션 URL",
"oidc": "OpenID Connect (OIDC)",
"saml": "Security Assertion Markup Language (SAML)",
"saml": "SAML",
"newProvider": "새 공급자",
"generalSettings": "일반 설정",
"adminPanel": "관리자 패널",
@ -764,8 +764,8 @@
"icon": "아이콘",
"max": "최대",
"enableRichText": "서식있는 텍스트 활성화",
"idColon": "Id: {fieldId}",
"copiedRecordURL": "레코드 URL을 복사했습니다.",
"idColon": "Id: {id}",
"copiedRecordURL": "레코드 URL을 복사했습니다",
"copyRecordURL": "레코드 URL 복사",
"duplicateRecord": "행 복제",
"binaryEncodingFormat": "바이너리 인코딩 형식",
@ -847,7 +847,7 @@
"username": "사용자 이름",
"password": "비밀번호",
"schemaName": "스키마 이름",
"database": "데이터 베이스",
"database": "데이터베이스",
"action": "동작",
"actions": "행위",
"operation": "작업",
@ -898,14 +898,14 @@
},
"community": {
"starUs1": "Star",
"starUs2": "Github",
"starUs2": "우리를 Github에서",
"bookDemo": "무료 데모 예약",
"getAnswered": "디스코드",
"joinDiscord": "디스코드 참가",
"joinCommunity": "NocoDB 커뮤니티 참가",
"joinReddit": "Join /r/NocoDB",
"followNocodb": "NocoDB 팔로우",
"communityTranslated": "(Community/AI Translated)"
"communityTranslated": "(커뮤니티/AI 번역)"
},
"twitter": "트위터",
"docReference": "참조 문서",
@ -942,7 +942,7 @@
"firstRowAsHeaders": "첫 번째 행을 헤더로 사용",
"flattenNested": "중첩 표준화",
"downloadAllowed": "다운로드 허용",
"weAreHiring": "채용 중",
"weAreHiring": "채용 중입니다!",
"primaryKey": "기본 키",
"hasMany": "많이",
"belongsTo": "속해있다",
@ -983,9 +983,9 @@
"deletedField": "삭제된 필드",
"incompleteConfiguration": "구성 불완전",
"selectField": "필드 선택",
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
"selectFieldLabel": "목록에서 필드를 선택하여 필드 속성을 변경합니다"
},
"appearanceSettings": "Appearance Settings",
"appearanceSettings": "모양 설정",
"backgroundColor": "배경색",
"hideNocodbBranding": "NocoDB 브랜드 숨기기",
"showOnConditions": "조건에 따라 표시",
@ -997,7 +997,7 @@
"relationType": "관계 유형",
"showThousandsSeparator": "천 단위 구분 기호 표시",
"signUpForFree": "무료로 가입",
"coverImageField": "Cover image field",
"coverImageField": "표지 이미지",
"fitImage": "이미지 맞춤",
"coverImageArea": "표지 이미지",
"syncData": "데이터 동기화",
@ -1096,7 +1096,7 @@
"projInfo": "프로젝트 정보 복사",
"themes": "테마"
},
"sort": "종류",
"sort": "정렬",
"addSort": "정렬 옵션 추가",
"filter": "필터",
"addFilter": "필터 추가",
@ -1212,7 +1212,7 @@
"expandRecord": "레코드 확장",
"deleteRecord": "레코드 삭제",
"fullWidth": "전체 넓이",
"exitFullWidth": "Exit full width",
"exitFullWidth": "전체 너비 나가기",
"markAllAsRead": "모두 읽음으로 표시",
"column": {
"delete": "열 삭제",
@ -1270,7 +1270,7 @@
"optionalSchemaName": "선택 사항. 비워두면 기본 스키마 \"{schema}\"를 사용합니다.",
"schemaChangeDisabled": "이 데이터 소스에 대한 스키마 편집이 비활성화되어 있습니다.",
"typeNotAllowed": "이 데이터 유형은 허용되지 않습니다.",
"dataWriteOptionDisabled": "Data editing can only be disabled when 'Schema editing' is also disabled.",
"dataWriteOptionDisabled": "데이터 편집은 '스키마 편집'이 비활성화된 경우에만 비활성화되며 그렇지 않으면 활성화됩니다",
"allowMetaWrite": "이 옵션은 테이블과 열을 추가, 변경 또는 삭제하는 것을 포함하여 데이터베이스 스키마를 수정할 수 있습니다. 변경 사항이 데이터베이스의 구조적 무결성에 영향을 미칠 수 있으므로 주의해서 사용하십시오.",
"allowDataWrite": "이 옵션을 사용하면 데이터베이스 테이블 내에서 레코드를 생성, 업데이트 또는 삭제할 수 있습니다. 데이터를 직접 변경해야 하는 관리자에게 이상적입니다.",
"reachedSourceLimit": "데이터 소스 제한에 도달했습니다.",
@ -1302,7 +1302,7 @@
"clientCert": ".cert 파일을 선택하십시오",
"clientCA": "CA 파일을 선택하십시오",
"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": "페이지당 하나의 필드로 폼 모드",
"useFieldEditMenuToConfigFieldType": "파일을 가져온 후 유형 변환을 위해 필드 편집 메뉴 사용",
"roleInheritedFromWorkspace": "워크스페이스에서 상속된 역할",
@ -1356,11 +1356,11 @@
"value": "값",
"key": "키",
"createTable": "첫 번째 테이블을 만드세요!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"createTableLabel": "처음부터 시작하거나 가져오기 또는 외부 데이터베이스에 연결하기",
"noTokenCreated": "생성된 API 토큰이 없습니다.",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"noTokenCreatedLabel": "아직 API 토큰을 생성하지 않은 것 같습니다.",
"inviteYourTeam": "팀 초대",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace.",
"inviteYourTeamLabel": "팀과 협력하여 프로젝트를 빠르게 진행하세요!",
"searchOptions": "검색 옵션"
},
"msg": {
@ -1400,7 +1400,7 @@
"webhookBodyMsg3": "해당 기록을 참조하기 위해",
"formula": {
"hintStart": "팁:{placeholder1}을(를) 사용하여 필드(예: {placeholder2})를 참조하십시오.",
"hintEnd": "Formulas.",
"hintEnd": "수식.",
"noSuggestedFormulaFound": "제안된 공식이 없습니다.",
"noSuggestedFieldFound": "추천 필드를 찾을 수 없습니다",
"typeIsExpected": "{calleeName}에는 {position} 위치에 {type}이(가) 필요합니다",
@ -1486,7 +1486,7 @@
"roleRequired": "역할이 필요합니다.",
"warning": {
"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": "칸반 보기를 사용하려면 단일 선택 필드를 설정해야 합니다. 단일 선택 필드를 추가한 후 칸반 보기를 설정해 보세요!",
"mapNoFields": "지도 보기를 사용하려면 지리 데이터 필드를 설정해야 합니다. 지리 데이터 필드를 추가한 후 지도 보기를 설정해 보세요!",
"dbValid": "스키마 손실을 방지하기 위해 데이터베이스의 유효성을 확인하세요",
@ -1722,9 +1722,9 @@
"nullFilterExists": "Null 필터가 존재합니다. 제거해 주세요",
"signUpRules": {
"emailRequired": "이메일 주소는 필수입니다",
"emailInvalid": "이메일이 유효하지 않습니다.",
"passwdRequired": "비밀번호를 입력하십시오.",
"passwdLength": "비밀번호는 8자 이상이어야 합니다.",
"emailInvalid": "이메일이 유효하지 않습니다",
"passwdRequired": "비밀번호를 입력하십시오",
"passwdLength": "비밀번호는 8자 이상이어야 합니다",
"passwdMismatch": "비밀번호가 일치하지 않습니다",
"completeRuleSet": "대문자, 숫자 및 특수 문자가 각각 1개씩 포함된 8자 이상",
"atLeast8Char": "8자 이상",
@ -1739,20 +1739,20 @@
"invalidURL": "유효하지 않은 URL",
"invalidEmail": "유효하지 않은 이메일",
"internalError": "내부 오류",
"templateGeneratorNotFound": "템플릿 생성기를 찾을 수 없습니다.",
"templateGeneratorNotFound": "템플릿 생성기를 찾을 수 없습니다",
"fileUploadFailed": "파일 업로드 실패",
"primaryColumnUpdateFailed": "기본 열 업데이트 실패",
"formDescriptionTooLong": "양식 설명이 너무 깁니다.",
"columnsRequired": "컬럼이 필요합니다.",
"selectAtleastOneColumn": "컬럼을 하나 이상 선택하십시오.",
"columnDescriptionNotFound": "컬럼 설명을 찾을 수 없습니다.",
"duplicateMappingFound": "중복 매핑이 발견되었습니다.",
"nullValueViolatesNotNull": "NULL 값이 NOT NULL 제약 조건을 위반합니다.",
"sourceHasInvalidNumbers": "소스 데이터에 잘못된 숫자 값이 포함되어 있습니다.",
"sourceHasInvalidBoolean": "소스 데이터에 잘못된 부울 값이 포함되어 있습니다.",
"formDescriptionTooLong": "양식 설명이 너무 깁니다",
"columnsRequired": "컬럼이 필요합니다",
"selectAtleastOneColumn": "컬럼을 하나 이상 선택하십시오",
"columnDescriptionNotFound": "컬럼 설명을 찾을 수 없습니다",
"duplicateMappingFound": "중복 매핑이 발견되었습니다",
"nullValueViolatesNotNull": "NULL 값이 NOT NULL 제약 조건을 위반합니다",
"sourceHasInvalidNumbers": "소스 데이터에 잘못된 숫자 값이 포함되어 있습니다",
"sourceHasInvalidBoolean": "소스 데이터에 잘못된 부울 값이 포함되어 있습니다",
"invalidForm": "유효하지 않은 양식",
"formValidationFailed": "양식 유효성 검사 실패",
"youHaveBeenSignedOut": "로그아웃되었습니다.",
"youHaveBeenSignedOut": "로그아웃되었습니다",
"failedToLoadList": "목록을 불러오지 못했습니다.",
"failedToLoadChildrenList": "하위 목록을 불러오지 못했습니다.",
"deleteFailed": "삭제 실패",
@ -1761,89 +1761,89 @@
"deleteRowFailed": "행 삭제 실패",
"setFormDataFailed": "양식 데이터 설정 실패",
"formViewUpdateFailed": "양식 뷰 업데이트 실패",
"tableNameRequired": "테이블 이름이 필요합니다.",
"nameShouldStartWithAnAlphabetOr_": "이름은 영문자 또는 _로 시작해야 합니다.",
"followingCharactersAreNotAllowed": "다음 문자는 허용되지 않습니다.",
"columnNameRequired": "컬럼 이름이 필요합니다.",
"tableNameRequired": "테이블 이름이 필요합니다",
"nameShouldStartWithAnAlphabetOr_": "이름은 영문자 또는 _로 시작해야 합니다",
"followingCharactersAreNotAllowed": "다음 문자는 허용되지 않습니다",
"columnNameRequired": "컬럼 이름이 필요합니다",
"duplicateColumnName": "중복된 필드 이름",
"duplicateSystemColumnName": "시스템 필드에 이미 사용된 이름",
"uiDataTypeRequired": "UI 데이터 유형이 필요합니다",
"columnNameExceedsCharacters": "열 이름의 길이가 최대 {value}자를 초과합니다.",
"columnNameExceedsCharacters": "열 이름의 길이가 최대 {value}자를 초과합니다",
"projectNameExceeds50Characters": "{title} 이름이 50자를 초과합니다",
"projectNameCannotStartWithSpace": "{title} 이름은 공백으로 시작할 수 없습니다",
"requiredField": "필수 필드입니다.",
"ipNotAllowed": "IP가 허용되지 않습니다.",
"targetFileIsNotAnAcceptedFileType": "대상 파일이 허용되는 파일 유형이 아닙니다.",
"theAcceptedFileTypeIsCsv": "허용되는 파일 유형은 .csv입니다.",
"requiredField": "필수 필드입니다",
"ipNotAllowed": "IP가 허용되지 않습니다",
"targetFileIsNotAnAcceptedFileType": "대상 파일이 허용되는 파일 유형이 아닙니다",
"theAcceptedFileTypeIsCsv": "허용되는 파일 유형은 .csv입니다",
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "허용되는 파일 유형은 .xls, .xlsx, .xlsm, .ods, .ots입니다",
"parameterKeyCannotBeEmpty": "매개 변수 키는 비워 둘 수 없습니다.",
"duplicateParameterKeysAreNotAllowed": "중복 매개 변수 키는 허용되지 않습니다.",
"parameterKeyCannotBeEmpty": "매개 변수 키는 비워 둘 수 없습니다",
"duplicateParameterKeysAreNotAllowed": "중복 매개 변수 키는 허용되지 않습니다",
"fieldRequired": "{value}은(는) 비워 둘 수 없습니다.",
"projectNotAccessible": "프로젝트에 액세스할 수 없습니다.",
"copyToClipboardError": "클립 보드에 복사할 수 없습니다.",
"projectNotAccessible": "프로젝트에 액세스할 수 없습니다",
"copyToClipboardError": "클립 보드에 복사할 수 없습니다",
"pasteFromClipboardError": "클립보드에서 붙여넣기 실패",
"multiFieldSaveValidation": "저장하기 전에 모든 필드의 구성을 완료하세요.",
"multiFieldSaveValidation": "저장하기 전에 모든 필드의 구성을 완료하세요",
"somethingWentWrong": "문제가 발생했습니다",
"draggedContentIsNotTypeOfImage": "드래그된 콘텐츠가 이미지 유형이 아닙니다.",
"draggedContentIsNotTypeOfImage": "드래그된 콘텐츠가 이미지 유형이 아닙니다",
"fieldToParseImageData": "이미지 데이터를 구문 분석할 필드",
"someOfTheRequiredFieldsAreEmpty": "필수 필드 중 일부가 비어 있습니다."
"someOfTheRequiredFieldsAreEmpty": "필수 필드 중 일부가 비어 있습니다"
},
"toast": {
"exportMetadata": "프로젝트 메타 데이터를 성공적으로 내보냈습니다.",
"importMetadata": "프로젝트 메타 데이터를 성공적으로 가져왔습니다.",
"clearMetadata": "프로젝트 메타 데이터를 성공적으로 지웠습니다.",
"stopProject": "프로젝트가 성공적으로 중지되었습니다.",
"startProject": "프로젝트가 성공적으로 시작되었습니다.",
"restartProject": "프로젝트가 성공적으로 다시 시작되었습니다.",
"deleteProject": "프로젝트가 성공적으로 삭제되었습니다.",
"authToken": "인증 토큰이 클립보드에 복사되었습니다.",
"projInfo": "프로젝트 정보가 클립보드에 복사되었습니다. ",
"inviteUrlCopy": "초대 URL이 클립보드에 복사되었습니다. ",
"createView": "뷰가 성공적으로 생성되었습니다. ",
"formEmailSMTP": "이메일 알림을 사용하려면 App Store에서 SMTP 플러그인을 활성화 하십시오.",
"exportMetadata": "프로젝트 메타 데이터를 성공적으로 내보냈습니다",
"importMetadata": "프로젝트 메타 데이터를 성공적으로 가져왔습니다",
"clearMetadata": "프로젝트 메타 데이터를 성공적으로 지웠습니다",
"stopProject": "프로젝트가 성공적으로 중지되었습니다",
"startProject": "프로젝트가 성공적으로 시작되었습니다",
"restartProject": "프로젝트가 성공적으로 다시 시작되었습니다",
"deleteProject": "프로젝트가 성공적으로 삭제되었습니다",
"authToken": "인증 토큰이 클립보드에 복사되었습니다",
"projInfo": "프로젝트 정보가 클립보드에 복사되었습니다",
"inviteUrlCopy": "초대 URL이 클립보드에 복사되었습니다",
"createView": "뷰가 성공적으로 생성되었습니다",
"formEmailSMTP": "이메일 알림을 사용하려면 App Store에서 SMTP 플러그인을 활성화 하십시오",
"collabView": "공동 작업 뷰로 성공적으로 전환했습니다",
"lockedView": "잠긴 뷰로 성공적으로 전환되었습니다",
"futureRelease": "향후 릴리스에서 사용할 수 있습니다."
"futureRelease": "곧 출시!"
},
"success": {
"licenseKeyUpdated": "라이센스 키 업데이트됨",
"columnDuplicated": "필드가 성공적으로 복제되었습니다",
"rowDuplicatedWithoutSavedYet": "행이 중복됨(저장되지 않음)",
"updatedUIACL": "테이블에 대한 UI ACL을 업데이트했습니다.",
"updatedUIACL": "테이블에 대한 UI ACL을 업데이트했습니다",
"pluginUninstalled": "플러그인이 성공적으로 제거되었습니다",
"pluginSettingsSaved": "플러그인 설정이 성공적으로 저장되었습니다.",
"pluginTested": "플러그인이 성공적으로 테스트되었습니다.",
"tableRenamed": "테이블 이름이 성공적으로 변경되었습니다.",
"pluginSettingsSaved": "플러그인 설정이 성공적으로 저장되었습니다",
"pluginTested": "플러그인이 성공적으로 테스트되었습니다",
"tableRenamed": "테이블 이름이 성공적으로 변경되었습니다",
"layoutRenamed": "레이아웃 이름이 성공적으로 변경되었습니다",
"viewDeleted": "뷰가 성공적으로 삭제되었습니다.",
"primaryColumnUpdated": "기본 열이 성공적으로 업데이트되었습니다.",
"tableDataExported": "테이블 데이터가 성공적으로 내보내졌습니다.",
"updated": "성공적으로 업데이트되었습니다.",
"sharedViewDeleted": "공유된 뷰가 성공적으로 삭제되었습니다.",
"userDeleted": "사용자가 성공적으로 삭제되었습니다.",
"viewRenamed": "뷰 이름이 성공적으로 변경되었습니다.",
"tokenGenerated": "토큰이 성공적으로 생성되었습니다.",
"tokenDeleted": "토큰이 성공적으로 삭제되었습니다.",
"userAddedToProject": "사용자가 성공적으로 프로젝트에 추가되었습니다.",
"userAdded": "사용자가 성공적으로 추가되었습니다.",
"userDeletedFromProject": "사용자가 성공적으로 프로젝트에서 삭제되었습니다.",
"inviteEmailSent": "초대 이메일이 성공적으로 전송되었습니다.",
"inviteURLCopied": "초대 URL이 클립보드에 복사되었습니다.",
"commentCopied": "댓글이 클립보드에 복사되었습니다.",
"passwordResetURLCopied": "비밀번호 재설정 URL이 클립보드에 복사되었습니다.",
"shareableURLCopied": "공유 가능한 URL이 클립보드에 복사되었습니다.",
"embeddableHTMLCodeCopied": "임베드 가능한 HTML 코드가 클립보드에 복사되었습니다.",
"userDetailsUpdated": "사용자 세부 정보가 성공적으로 업데이트되었습니다.",
"tableDataImported": "테이블 데이터가 성공적으로 가져와졌습니다.",
"webhookUpdated": "Webhook가 성공적으로 업데이트되었습니다.",
"webhookDeleted": "Webhook가 성공적으로 삭제되었습니다.",
"webhookTested": "Webhook가 성공적으로 테스트되었습니다.",
"columnUpdated": "열이 성공적으로 업데이트되었습니다.",
"columnCreated": "열이 성공적으로 생성되었습니다.",
"viewDeleted": "뷰가 성공적으로 삭제되었습니다",
"primaryColumnUpdated": "기본 열이 성공적으로 업데이트되었습니다",
"tableDataExported": "테이블 데이터가 성공적으로 내보내졌습니다",
"updated": "성공적으로 업데이트되었습니다",
"sharedViewDeleted": "공유된 뷰가 성공적으로 삭제되었습니다",
"userDeleted": "사용자가 성공적으로 삭제되었습니다",
"viewRenamed": "뷰 이름이 성공적으로 변경되었습니다",
"tokenGenerated": "토큰이 성공적으로 생성되었습니다",
"tokenDeleted": "토큰이 성공적으로 삭제되었습니다",
"userAddedToProject": "사용자가 성공적으로 프로젝트에 추가되었습니다",
"userAdded": "사용자가 성공적으로 추가되었습니다",
"userDeletedFromProject": "사용자가 성공적으로 프로젝트에서 삭제되었습니다",
"inviteEmailSent": "초대 이메일이 성공적으로 전송되었습니다",
"inviteURLCopied": "초대 URL이 클립보드에 복사되었습니다",
"commentCopied": "댓글이 클립보드에 복사되었습니다",
"passwordResetURLCopied": "비밀번호 재설정 URL이 클립보드에 복사되었습니다",
"shareableURLCopied": "공유 가능한 URL이 클립보드에 복사되었습니다!",
"embeddableHTMLCodeCopied": "임베드 가능한 HTML 코드가 클립보드에 복사되었습니다!",
"userDetailsUpdated": "사용자 세부 정보가 성공적으로 업데이트되었습니다",
"tableDataImported": "테이블 데이터가 성공적으로 가져와졌습니다",
"webhookUpdated": "Webhook가 성공적으로 업데이트되었습니다",
"webhookDeleted": "Webhook가 성공적으로 삭제되었습니다",
"webhookTested": "Webhook가 성공적으로 테스트되었습니다",
"columnUpdated": "열이 성공적으로 업데이트되었습니다",
"columnCreated": "열이 성공적으로 생성되었습니다",
"passwordChanged": "비밀번호가 성공적으로 변경되었습니다. 다시 로그인해 주세요.",
"settingsSaved": "설정이 성공적으로 저장되었습니다.",
"roleUpdated": "역할이 성공적으로 업데이트되었습니다.",
"connectionAdded": "연동이 성공적으로 완료되었습니다.",
"settingsSaved": "설정이 성공적으로 저장되었습니다",
"roleUpdated": "역할이 성공적으로 업데이트되었습니다",
"connectionAdded": "연동이 성공적으로 완료되었습니다",
"connectionAddedDesc": "이제 베이스 소유자와 생성자는 자격 증명을 다시 입력하지 않고도 데이터 소스를 추가할 수 있습니다."
}
}

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

@ -40,7 +40,7 @@
},
"aggregation": {
"sum": "Toplam",
"count": "Say",
"count": "Miktar",
"min": "Min",
"max": "Max",
"avg": "Ort",
@ -918,7 +918,7 @@
"links": "Bağlantılar",
"onUpdate": "Güncellenince",
"onDelete": "Silinince",
"account": "Account",
"account": "Hesap",
"language": "Language",
"primaryColor": "Primary 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",
"project": "Dự định",
"projects": "Dự án.",
"table": "Bàn",
"tables": "Những cái bàn",
"field": "Cánh đồng",
"fields": "Lĩnh vực",
"table": "Bảng",
"tables": "Bảng",
"field": "Trường dữ liệu",
"fields": "Trường dữ liệu",
"column": "Cột",
"columns": "Cột",
"cell": "Ô",
@ -815,7 +815,7 @@
"accountEmailID": "ID Email Tài khoản",
"backToWorkspace": "Quay lại Workspace",
"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",
"createView": "Create View",
"creatingView": "Tạo Chế độ xem",
@ -1132,7 +1132,7 @@
"createTable": "Create New Table",
"createDashboard": "Tạo bảng chính",
"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",
"renameLayout": "Đổi tên bố cục",
"deleteTable": "Xóa bảng",

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

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

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

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

4
packages/nc-gui/package.json

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

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

@ -1,9 +1,10 @@
import { defineStore } from 'pinia'
import type { NotificationType } from 'nocodb-sdk'
import axios, { type CancelTokenSource } from 'axios'
import { CancelToken } from 'axios'
import { useStorage } from '@vueuse/core'
const CancelToken = axios.CancelToken
export const useNotification = defineStore('notificationStore', () => {
const isTokenRefreshInProgress = useStorage(TOKEN_REFRESH_PROGRESS_KEY, false)
@ -27,16 +28,24 @@ export const useNotification = defineStore('notificationStore', () => {
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 () => {
try {
if (!token.value) return
// set up cancel token for polling to cancel when token changes/token is removed
cancelTokenSource = CancelToken.source()
const res = await api.notification.poll({
cancelToken: cancelTokenSource.token,
})
const res = await sharedExecutionPollNotificationsApiCall()
if (res.status === 'success') {
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 {
uiTypes,
isTypableInputColumn,
@ -267,4 +358,8 @@ export {
isColumnRequiredAndNull,
isColumnRequired,
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",
"version": "1.0.0",
"description": "",
"packageManager": "pnpm@9.6.0",
"main": "index.js",
"type": "module",
"scripts": {

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

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

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

@ -1,140 +1,154 @@
---
title: 'Environment variables'
description: 'Environment Variables for NocoDB!'
tags: ['Open Source']
keywords : ['NocoDB Environment Variables', 'NocoDB env variables', 'NocoDB envs', 'NocoDB .env']
tags: [ 'Open Source' ]
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
| 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_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` | 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`. | |
| `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. |
| 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_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` | 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`. | |
| `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
| 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_GOOGLE_CLIENT_ID` | No | Google client ID required to activate Google authentication. | |
| `NC_GOOGLE_CLIENT_SECRET` | No | Google client secret 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_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. | |
| 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_GOOGLE_CLIENT_ID` | No | Google client ID required to activate Google authentication. | |
| `NC_GOOGLE_CLIENT_SECRET` | No | Google client secret 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_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
| 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_ENDPOINT` | No | S3 endpoint for S3 storage plugin. | Defaults to `s3.<region>.amazonaws.com` |
| `NC_S3_ACCESS_KEY` | No | The AWS access key ID required for the S3 storage plugin. | |
| `NC_S3_ACCESS_SECRET` | No | The AWS access secret associated with the S3 storage plugin. | |
| `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_ACL` | No | The [ACL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html) for the objects in S3 | | |
| `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_MAX_ATTACHMENTS_ALLOWED` | No | Maximum number of attachments allowed per cell. | Defaults to `10`. |
| `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_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). |
| 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_ENDPOINT` | No | S3 endpoint for S3 storage plugin. | Defaults to `s3.<region>.amazonaws.com` |
| `NC_S3_ACCESS_KEY` | No | The AWS access key ID for the S3 storage plugin. Required if no role access in use. | |
| `NC_S3_ACCESS_SECRET` | No | The AWS access secret associated with 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_ACL` | No | The [ACL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html) for the objects in S3 | |
| `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_MAX_ATTACHMENTS_ALLOWED` | No | Maximum number of attachments allowed per cell. | Defaults to `10`. |
| `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_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
- The following SMTP variables are used to send email notifications to users, e.g., invites.
| Variable | Mandatory | Description | If Not Set |
| -------- |-----------| ----------- | ---------- |
| `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_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_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_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/). | |
| Variable | Mandatory | Description | If Not Set |
|----------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|
| `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_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_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_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
| 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. | |
| 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. | |
## Frontend
| 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`. |
| `NUXT_PUBLIC_NC_BACKEND_URL` | No | Specifies a custom backend URL. | Defaults to `http://localhost:8080`. |
| 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`. |
| `NUXT_PUBLIC_NC_BACKEND_URL` | No | Specifies a custom backend URL. | Defaults to `http://localhost:8080`. |
## 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 |
## 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
| 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 |
| 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 |
## Debugging Only
| 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`. |
| 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`. |
## Telemetry
| 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. | |
| 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. | |
## Litestream
> 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 |
| -------- | --------- | ----------- | ---------- |
| `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_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_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_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_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_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_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_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_PATH` | No | ***Deprecated***. Please use `LITESTREAM_S3_PATH` instead. | |
| 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_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_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_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_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_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_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.* |
| `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_BUCKET` | No | ***Deprecated***. Please use `LITESTREAM_S3_BUCKET` 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.
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:
```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",
"nc-lib-gui": "0.257.0",
"nestjs-throttler-storage-redis": "^0.4.4",
"nocodb-sdk": "0.257.0",
"nocodb-sdk": "workspace:^",
"nodemailer": "^6.9.13",
"object-hash": "^3.0.0",
"object-sizeof": "^2.6.4",

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

@ -52,6 +52,11 @@ export class PublicDatasExportController {
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);
await view.getColumns(context);
@ -115,6 +120,11 @@ export class PublicDatasExportController {
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);
await view.getColumns(context);

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

@ -118,28 +118,27 @@ class NcPluginMgrv2 {
/*
* NC_S3_BUCKET_NAME
* NC_S3_REGION
* NC_S3_ACCESS_KEY
* NC_S3_ACCESS_SECRET
* */
if (
process.env.NC_S3_BUCKET_NAME &&
process.env.NC_S3_REGION &&
process.env.NC_S3_ACCESS_KEY &&
process.env.NC_S3_ACCESS_SECRET
process.env.NC_S3_REGION
) {
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, {
active: true,
input: JSON.stringify({
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,
}),
input: JSON.stringify(s3CfgData),
});
}

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

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

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

@ -194,6 +194,7 @@ export class DuplicateController {
@Param('modelId') modelId?: string,
@Body()
body?: {
title?: string;
options?: {
excludeData?: boolean;
excludeViews?: boolean;
@ -226,7 +227,7 @@ export class DuplicateController {
const models = await source.getModels(context);
const uniqueTitle = generateUniqueName(
`${model.title} copy`,
body.title || `${model.title} copy`,
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 { generateTempFilePath, waitForStreamClose } from '~/utils/pluginUtils';
interface GenerocObjectStorageInput {
interface GenericObjectStorageInput {
bucket: string;
region?: string;
access_key: string;
access_secret: string;
access_key?: string;
access_secret?: string;
}
export default class GenericS3 implements IStorageAdapterV2 {
public name;
protected s3Client: S3Client;
protected input: GenerocObjectStorageInput;
protected input: GenericObjectStorageInput;
constructor(input: unknown) {
this.input = input as GenerocObjectStorageInput;
constructor(input: GenericObjectStorageInput) {
this.input = input;
}
protected get defaultParams() {

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

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

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

@ -38,14 +38,14 @@ const config: XcPluginConfig = {
label: 'Access Key',
placeholder: 'Access Key',
type: XcType.SingleLineText,
required: true,
required: false,
},
{
key: 'access_secret',
label: 'Access Secret',
placeholder: 'Access Secret',
type: XcType.Password,
required: true,
required: false,
},
{
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": {
"type": "boolean",
"required": false
},
"title": {
"type": "string",
"required": false,
"description": "New table title"
}
}
}
@ -20568,7 +20573,6 @@
}
},
"required": [
"table_name",
"title"
],
"x-stoplight": {
@ -20839,7 +20843,7 @@
},
"required": [
"columns",
"table_name"
"title"
],
"title": "Table Request Model",
"type": "object",

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

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

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

@ -16,6 +16,7 @@ import {
} from 'nocodb-sdk';
import { pluralize, singularize } from 'inflection';
import hash from 'object-hash';
import { parseMetaProp } from 'src/utils/modelUtils';
import type {
ColumnReqType,
LinkToAnotherColumnReqType,
@ -1243,6 +1244,27 @@ export class ColumnsService {
await Column.update(context, param.columnId, {
...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) {
// handle default value for user column
if (typeof colBody.cdf !== 'string') {
@ -1517,6 +1539,15 @@ export class ColumnsService {
baseModel.getTnPath(table.table_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);
@ -1596,6 +1627,11 @@ export class ColumnsService {
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);
const reuse = param.reuse || {};
@ -1643,6 +1679,11 @@ export class ColumnsService {
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) {
// - 5 is a buffer for suffix
let colName = param.column.column_name.slice(0, mxColumnLength - 5);
@ -2585,7 +2626,8 @@ export class ColumnsService {
}
case UITypes.SingleSelect: {
if (
await KanbanView.IsColumnBeingUsedAsGroupingField(context, column.id)
(await KanbanView.getViewsByGroupingColId(context, column.id))
.length > 0
) {
NcError.badRequest(
`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;
},
) {
// 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);
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 (
!tableCreatePayLoad.table_name ||
(base.prefix && base.prefix === tableCreatePayLoad.table_name)
!(await Model.checkAliasAvailable(context, {
title: tableCreatePayLoad.title,
base_id: base.id,
source_id: source.id,
}))
) {
NcError.badRequest(
'Missing table name `table_name` property in request body',
);
NcError.badRequest('Duplicate table alias');
}
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 sqlClient = await NcConnectionMgrv2.getSqlClient(source);
@ -652,6 +665,11 @@ export class TablesService {
) {
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
column.column_name = sanitizeColumnName(
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>) {}
lastSyncTime = dayjs();
lastSyncTime = null;
async versionInfo() {
if (
@ -526,7 +526,10 @@ export class UtilsService {
}
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();
this.lastSyncTime = dayjs();
}

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

@ -43,7 +43,7 @@ class TeleBatchProcessor {
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,
env: process.env.NODE_ENV || 'production',
oneClick: !!process.env.NC_ONE_CLICK,
disabled: isDisabled,
};
try {
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;
const decryptedVal = CryptoJS.AES.decrypt(encryptedConfig, secret).toString(
CryptoJS.enc.Utf8,
);
// validate by parsing JSON
try {
JSON.parse(decryptedVal);
} catch {
throw new Error('Config decryption failed');
const decryptedVal = CryptoJS.AES.decrypt(encryptedConfig, secret).toString(
CryptoJS.enc.Utf8,
);
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
@ -32,13 +61,18 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
logger.log('Starting decryption of sources and integrations');
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) {
encryptionKey = (
await ncMeta.metaGet(RootScopes.ROOT, RootScopes.ROOT, MetaTable.STORE, {
key: 'nc_auth_jwt_secret',
})
)?.value;
encryptionKey = encryptionKeyFromMeta;
} else {
fallbackEncryptionKey = encryptionKeyFromMeta;
}
// 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');
}
// get all external sources
// get all sources
const sources = await ncMeta.knexConnection(MetaTable.SOURCES);
const passed = [];
@ -70,7 +104,13 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
for (const source of sources) {
if (source?.config) {
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
.knexConnection(MetaTable.SOURCES)
.update({
@ -78,7 +118,11 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
})
.where('id', 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) {
logger.error(`Failed to decrypt source ${source.id}`);
passed.push(false);
@ -93,10 +137,11 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
for (const integration of integrations) {
if (integration?.config) {
try {
const decrypted = await decryptConfig(
integration.config,
encryptionKey,
);
const decrypted = await decryptConfigWithFallbackKey({
encryptedConfig: integration.config,
secret: encryptionKey,
fallbackSecret: fallbackEncryptionKey,
});
await ncMeta
.knexConnection(MetaTable.INTEGRATIONS)
.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;
});
it('Create table with no table name', async function () {
it('Create table with no table title', async function () {
const response = await request(context.app)
.post(`/api/v1/db/meta/projects/${base.id}/tables`)
.set('xc-auth', context.token)
.send({
table_name: undefined,
title: 'new_title',
title: undefined,
columns: defaultColumns(context),
})
.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/nocodb-sdk'
- 'packages/nc-gui'
- 'packages/nc-mail-templates'
- 'packages/nocodb'
- 'tests/playwright'
- 'packages/nc-secret-mgr'
- "packages/nocodb-sdk"
- "packages/nc-gui"
- "packages/nc-mail-templates"
- "packages/nocodb"
- "tests/playwright"
- "packages/nc-secret-mgr"

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

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

Loading…
Cancel
Save