diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 787bbe9cbf..902a7c7218 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -18,7 +18,10 @@ on: - "packages/nc-gui/**" - "packages/nocodb/**" - ".github/workflows/ci-cd.yml" + - ".github/workflows/playwright-test-workflow.yml" - "tests/playwright/**" + # Triggered manually + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -89,6 +92,10 @@ jobs: ${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build- ${{ runner.os }}- + - name: Set CI env + run: export CI=true + - name: Set NC Edition + run: export EE=true - name: setup pg working-directory: ./ run: docker-compose -f ./tests/playwright/scripts/docker-compose-playwright-pg.yml up -d & diff --git a/.github/workflows/playwright-test-workflow.yml b/.github/workflows/playwright-test-workflow.yml index eb2ec03eca..60b62f90ff 100644 --- a/.github/workflows/playwright-test-workflow.yml +++ b/.github/workflows/playwright-test-workflow.yml @@ -13,21 +13,9 @@ on: jobs: playwright: - runs-on: ubuntu-20.04 - timeout-minutes: 40 + runs-on: [self-hosted, v2] + timeout-minutes: 100 steps: - # Reference: https://github.com/pierotofy/set-swap-space/blob/master/action.yml - - name: Set 5gb swap - shell: bash - # Delete the swap file, allocate a new one, and activate it - run: | - export SWAP_FILE=$(swapon --show=NAME | tail -n 1) - sudo swapoff $SWAP_FILE - sudo rm $SWAP_FILE - sudo fallocate -l 5G $SWAP_FILE - sudo chmod 600 $SWAP_FILE - sudo mkswap $SWAP_FILE - sudo swapon $SWAP_FILE - name: Setup Node uses: actions/setup-node@v3 with: @@ -42,57 +30,87 @@ jobs: with: # npm cache files are stored in `~/.npm` on Linux/macOS path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + key: ${{ runner.os }}-v2-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- + ${{ runner.os }}-v2-build-${{ env.cache-name }}- + ${{ runner.os }}-v2-build- + ${{ runner.os }}-v2 + - name: setup pg + if: ${{ inputs.db == 'pg' || ( inputs.db == 'sqlite' && inputs.shard == '1' ) }} + working-directory: ./ + 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';" + sudo -u postgres psql -c "ALTER USER postgres WITH SUPERUSER;" + service postgresql restart + - name: Set CI env + run: export CI=true + - name: Kill stale servers + run: | + # export NODE_OPTIONS=\"--max_old_space_size=16384\"; + kill -9 $(lsof -t -i:8080) || echo "no process running on 8080" + kill -9 $(lsof -t -i:3000) || echo "no process running on 3000" + - name: Set CI env + run: export CI=true + - name: Set NC Edition + run: export EE=true - name: install dependencies nocodb-sdk working-directory: ./packages/nocodb-sdk run: npm install - - name: build nocodb-sdk + - name: Build nocodb-sdk working-directory: ./packages/nocodb-sdk run: npm run build - - name: setup mysql + - name: Setup mysql if: ${{ inputs.db == 'mysql' }} - working-directory: ./ - run: docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml up -d & - - name: setup pg - if: ${{ inputs.db == 'pg' }} - working-directory: ./ - run: docker-compose -f ./tests/playwright/scripts/docker-compose-playwright-pg.yml up -d & - - name: setup pg for quick tests + working-directory: ./packages/nocodb/tests/mysql-sakila-db + run: | + # Get a list of non-system databases and construct the DROP DATABASE statement for each + service mysql start + mysql -u'root' -p'password' -e "SHOW DATABASES" --skip-column-names | grep -Ev "(information_schema|mysql|performance_schema|sys)" | while read db; do + mysql -u'root' -p'password' -e "DROP DATABASE IF EXISTS \`$db\`"; + done + # keep sql_mode default except remove "STRICT_TRANS_TABLES" + mysql -u'root' -p'password' -e "SET GLOBAL sql_mode = 'ONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';" + # this is only needed for connecting to sakila db as its refeferred in multiple places in test code + mysql -u'root' -p'password' < 01-mysql-sakila-schema.sql + mysql -u'root' -p'password' < 02-mysql-sakila-insert-data.sql + - name: Setup pg for quick tests if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }} - working-directory: ./ - run: docker-compose -f ./tests/playwright/scripts/docker-compose-pg-pw-quick.yml up -d & + working-directory: ./packages/nocodb/tests/pg-cy-quick/ + run: | + sudo -u postgres psql -U postgres -f 01-cy-quick.sql - name: run frontend working-directory: ./packages/nc-gui run: npm run ci:run + timeout-minutes: 20 - name: Run backend if: ${{ inputs.db == 'sqlite' }} working-directory: ./packages/nocodb run: | npm install - npm run watch:run:playwright > ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & + npm run watch:run:playwright &> ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & - name: Run backend:mysql if: ${{ inputs.db == 'mysql' }} working-directory: ./packages/nocodb run: | npm install - npm run watch:run:playwright:mysql > ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & + npm run watch:run:playwright:mysql &> ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & - name: Run backend:pg if: ${{ inputs.db == 'pg' }} working-directory: ./packages/nocodb run: | npm install - npm run watch:run:playwright:pg > ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & + npm run watch:run:playwright:pg &> ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & - name: Cache playwright npm modules uses: actions/cache@v3 id: playwright-cache with: path: | **/tests/playwright/node_modules - key: cache-nc-playwright-${{ hashFiles('**/tests/playwright/package-lock.json') }} + key: cache-v2-nc-playwright-${{ hashFiles('**/tests/playwright/package-lock.json') }} + restore-keys: | + cache-v2-nc-playwright- - name: Install dependencies if: steps.playwright-cache.outputs.cache-hit != 'true' working-directory: ./tests/playwright @@ -106,11 +124,11 @@ jobs: printf '.' sleep 2 done - - - name: Run Playwright tests + timeout-minutes: 2 + - name: Run Playwright Tests working-directory: ./tests/playwright run: E2E_DB_TYPE=${{ inputs.db }} npm run ci:test:shard:${{ inputs.shard }} - + timeout-minutes: 60 # Stress test added/modified tests - name: Fetch develop branch working-directory: ./tests/playwright @@ -119,28 +137,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 & - - 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 & run quick 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 - 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' }} with: @@ -172,3 +194,9 @@ jobs: name: backend-logs-${{ inputs.db }}-${{ inputs.shard }} path: ./packages/nocodb/${{ inputs.db }}_${{ inputs.shard }}_test_backend.log retention-days: 2 + - name: stop database servers + if: always() + working-directory: ./packages/nocodb + run: | + service postgresql stop + service mysql stop \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2d333cbc36..94d7be5041 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,10 @@ # =========== .DS_Store ehthumbs.db -Icon? Thumbs.db +Icon + + # Node and related ecosystem # ========================== @@ -92,3 +94,4 @@ test_noco.db # ngrok config httpbin +.run/test-debug.run.xml diff --git a/README.md b/README.md index a545c8a23e..82b32c8ad1 100644 --- a/README.md +++ b/README.md @@ -68,27 +68,6 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart spreadshe # Quick try -## NPX - -You can run the below command if you need an interactive configuration. - -``` -npx create-nocodb-app -``` - - - -## Node Application - -We provide a simple NodeJS Application for getting started. - -```bash -git clone https://github.com/nocodb/nocodb-seed -cd nocodb-seed -npm install -npm start -``` - ## Docker ```bash @@ -186,6 +165,28 @@ docker-compose up -d > If you plan to input some special characters, you may need to change the character set and collation yourself when creating the database. Please check out the examples for [MySQL Docker Compose](https://github.com/nocodb/nocodb/issues/1313#issuecomment-1046625974). +## NPX + +You can run the below command if you need an interactive configuration. + +``` +npx create-nocodb-app +``` + + + +## Node Application + +We provide a simple NodeJS Application for getting started. + +```bash +git clone https://github.com/nocodb/nocodb-seed +cd nocodb-seed +npm install +npm start +``` + + # GUI Access Dashboard using: [http://localhost:8080/dashboard](http://localhost:8080/dashboard) diff --git a/docker-compose/traefik/docker-compose.yml b/docker-compose/traefik/docker-compose.yml index 34cd04d510..b02b03bad7 100644 --- a/docker-compose/traefik/docker-compose.yml +++ b/docker-compose/traefik/docker-compose.yml @@ -62,7 +62,7 @@ services: - "--certificatesResolvers.letsencrypt.acme.dnsChallenge.resolvers=1.1.1.1:53,1.0.0.1:53" container_name: traefik environment: - - "CF_DNS_API_TOKEN=${CLOUDFLARE_TOKEN}" + - "CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}" healthcheck: retries: 3 test: diff --git a/markdown/readme/languages/indonesian.md b/markdown/readme/languages/indonesian.md index a3fff11549..b615bc2b04 100644 --- a/markdown/readme/languages/indonesian.md +++ b/markdown/readme/languages/indonesian.md @@ -1,17 +1,22 @@ +

- - NocoDB
-
+
+ + +
+ NocoDB +
+
✨ Sebuah Alternatif AirTable Open Source ✨

Mengubah MySQL, PostgreSQL, SQL Server, SQLite & MariaDB apapun menjadi spreadsheet pintar.

-
-[![Build Status](https://travis-ci.org/dwyl/esta.svg?branch=master)](https://travis-ci.com/github/NocoDB/NocoDB) -[![Node version](https://img.shields.io/badge/node-%3E%3D%2014.18.0-brightgreen)](http://nodejs.org/download/) +
+ +[![Node version](https://img.shields.io/badge/node-%3E%3D%2016.14.0-brightgreen)](http://nodejs.org/download/) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-green.svg)](https://conventionalcommits.org)
@@ -19,12 +24,13 @@ Mengubah MySQL, PostgreSQL, SQL Server, SQLite & MariaDB apapun menjadi spreadsh

WebsiteDiscord • + KomunitasTwitterReddit • - Documentation + Dokumentasi

-![OpenSourceAirtableAlternative](https://user-images.githubusercontent.com/5435402/133762127-e94da292-a1c3-4458-b09a-02cd5b57be53.png) +![All Views](https://user-images.githubusercontent.com/35857179/194825053-3aa3373d-3e0f-4b42-b3f1-42928332054a.gif) @@ -32,49 +38,134 @@ Mengubah MySQL, PostgreSQL, SQL Server, SQLite & MariaDB apapun menjadi spreadsh NocoDB - The Open Source Airtable alternative | Product Hunt

-# Mulai Cepat +# Join Tim Kami + +

+ +# Join Komunitas Kami + + + + + + + +[![Stargazers repo roster for @nocodb/nocodb](https://reporoster.com/stars/nocodb/nocodb)](https://github.com/nocodb/nocodb/stargazers) + +# Coba singkat + +## Docker + +```bash +# for SQLite +docker run -d --name nocodb \ +-v "$(pwd)"/nocodb:/usr/app/data/ \ +-p 8080:8080 \ +nocodb/nocodb:latest + +# for MySQL +docker run -d --name nocodb-mysql \ +-v "$(pwd)"/nocodb:/usr/app/data/ \ +-p 8080:8080 \ +-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \ +-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \ +nocodb/nocodb:latest + +# for PostgreSQL +docker run -d --name nocodb-postgres \ +-v "$(pwd)"/nocodb:/usr/app/data/ \ +-p 8080:8080 \ +-e NC_DB="pg://host.docker.internal:5432?u=root&p=password&d=d1" \ +-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \ +nocodb/nocodb:latest + +# for MSSQL +docker run -d --name nocodb-mssql \ +-v "$(pwd)"/nocodb:/usr/app/data/ \ +-p 8080:8080 \ +-e NC_DB="mssql://host.docker.internal:1433?u=root&p=password&d=d1" \ +-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \ +nocodb/nocodb:latest +``` + +> Untuk menyimpan data di dalam Docker, Anda dapat melakukan mount volume di direktori /usr/app/data/ mulai dari versi 0.10.6. Jika tidak, data Anda akan hilang setelah mengulang pembuatan kontainer. -### Menggunakan Docker +> Jika Anda berencana untuk memasukkan beberapa karakter khusus, Anda perlu mengubah set karakter dan kolasi sendiri saat membuat basis data. Silakan lihat contoh-contoh untuk [MySQL Docker](https://github.com/nocodb/nocodb/issues/1340#issuecomment-1049481043). + +## Binaries + +##### MacOS (x64) ```bash -docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest +curl http://get.nocodb.com/macos-x64 -o nocodb -L && chmod +x nocodb && ./nocodb ``` -- NocoDB needs a database as input : See [Production Setup](https://github.com/nocodb/nocodb/blob/master/README.md#production-setup). -- If this input is absent, we fallback to SQLite. In order too persist sqlite, you can mount `/usr/app/data/`. +##### MacOS (arm64) + +```bash +curl http://get.nocodb.com/macos-arm64 -o nocodb -L && chmod +x nocodb && ./nocodb +``` + +##### Linux (x64) + +```bash +curl http://get.nocodb.com/linux-x64 -o nocodb -L && chmod +x nocodb && ./nocodb +``` - Example: +##### Linux (arm64) - ``` - docker run -d -p 8080:8080 --name nocodb -v "$(pwd)"/nocodb:/usr/app/data/ nocodb/nocodb:latest - ``` +```bash +curl http://get.nocodb.com/linux-arm64 -o nocodb -L && chmod +x nocodb && ./nocodb +``` -### Menggunakan NPM +##### Windows (x64) +```bash +iwr http://get.nocodb.com/win-x64.exe -o Noco-win-x64.exe +.\Noco-win-x64.exe ``` -npx create-nocodb-app + +##### Windows (arm64) + +```bash +iwr http://get.nocodb.com/win-arm64.exe -o Noco-win-arm64.exe +.\Noco-win-arm64.exe ``` -### Menggunakan git +## Docker Compose + +Kami menyediakan berbagai file docker-compose.yml di [bawah direktori](https://github.com/nocodb/nocodb/tree/master/docker-compose) ini. Berikut beberapa contohnya: +```bash +git clone https://github.com/nocodb/nocodb +# for MySQL +cd nocodb/docker-compose/mysql +# for PostgreSQL +cd nocodb/docker-compose/pg +# for MSSQL +cd nocodb/docker-compose/mssql +docker-compose up -d ``` + +> Untuk menyimpan data dalam Docker, Anda dapat melakukan mount volume pada direktori /usr/app/data/ mulai dari versi 0.10.6. Jika tidak, data Anda akan hilang setelah mengulang pembuatan kontainer. + +> Jika Anda berencana untuk memasukkan beberapa karakter khusus, Anda perlu mengubah set karakter dan kolasi sendiri saat membuat basis data. Silakan lihat contoh-contoh untuk [MySQL Docker Compose](https://github.com/nocodb/nocodb/issues/1313#issuecomment-1046625974). + + +```bash git clone https://github.com/nocodb/nocodb-seed cd nocodb-seed npm install npm start ``` -### GUI +# GUI Akses dasbor menggunakan : [http://localhost:8080/dashboard](http://localhost:8080/dashboard) -# Bergabunglah dengan komunitas kami - - - - -
- # Tangkapan Layar ![1](https://user-images.githubusercontent.com/86527202/136074228-f52e181b-e65d-44ce-afca-0447eb506e90.png) @@ -110,6 +201,32 @@ Akses dasbor menggunakan : [http://localhost:8080/dashboard](http://localhost:80 ![11](https://user-images.githubusercontent.com/86527202/136074285-b5d1dc5c-fac3-43af-b9fc-1a5d1d41d071.png)
+# Table of Contents + +# Daftar Isi + +- [Coba Singkat](#coba-singkat) + - [Docker](#docker) + - [Docker Compose](#docker-compose) +- [Antarmuka Grafis Pengguna (GUI)](#gui) +- [Bergabunglah dengan Komunitas Kami](#join-komunitas-kami) +- [Screenshots](#tangkapan-layar) +- [Daftar Isi](#daftar-isi) +- [Fitur-fitur](#fitur) + - [Antarmuka Lembar Kerja yang Kaya](#antarmuka-spreadsheet-yang-kaya) + - [Toko Aplikasi untuk Otomasi Alur Kerja](#app-store-untuk-automasi-alur-kerja) + - [Akses Programatik](#akses-api-programmatik-melalui) + - [Sinkronisasi Skema](#sinkronisasi-skema) + - [Audit](#audit) +- [Pengaturan Produksi](#pengaturan-produksi) + - [Variabel Lingkungan](#environment-variables) +- [Pengaturan Pengembangan](#pengaturan-pengembangan) +- [Berkontribusi](#berkontribusi) +- [Mengapa kami membangun ini?](#kenapa-kita-membangun-ini) +- [Misi Kami](#misi-kami) +- [Lisensi](#lisensi) + + # Fitur ### Antarmuka spreadsheet yang kaya @@ -137,65 +254,38 @@ Akses dasbor menggunakan : [http://localhost:8080/dashboard](http://localhost:80 - ⚡ Termasuk Otentikasi JWT & Auth Sosial - ⚡ Token API untuk berintegrasi dengan Zapier, Integromat. -# Pengaturan Produksi - -NOCODB membutuhkan database untuk menyimpan metadata tampilan spreadsheet dan database eksternal. Dan params koneksi untuk basis data ini dapat ditentukan dalam variabel lingkungan NC_DB. - -## Docker - -#### Contoh MySQL +### Sinkronisasi Skema -``` -docker run -d -p 8080:8080 \ - -e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \ - -e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \ - nocodb/nocodb:latest -``` +Kami memungkinkan Anda untuk menyinkronkan perubahan skema jika Anda telah melakukan perubahan di luar antarmuka NocoDB GUI. Namun, perlu diperhatikan bahwa Anda harus menyediakan migrasi skema sendiri untuk berpindah dari satu lingkungan ke lingkungan lainnya. Lihat [Sinkronisasi Skema](https://docs.nocodb.com/setup-and-usages/sync-schema/) untuk detail lebih lanjut. -#### Contoh Postgres +### Audit -``` -docker run -d -p 8080:8080 \ - -e NC_DB="pg://host:port?u=user&p=password&d=database" \ - -e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \ - nocodb/nocodb:latest -``` - -#### Contoh SQL Server - -``` -docker run -d -p 8080:8080 \ - -e NC_DB="mssql://host:port?u=user&p=password&d=database" \ - -e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \ - nocodb/nocodb:latest -``` +Kami menyimpan semua log operasi pengguna di satu tempat. Lihat [Audit](https://docs.nocodb.com/setup-and-usages/audit) untuk detail lebih lanjut. -## Docker Compose +# Pengaturan Produksi -``` -git clone https://github.com/nocodb/nocodb -cd nocodb -cd docker-compose -cd mysql or pg or mssql -docker-compose up -d -``` +Secara default, SQLite digunakan untuk menyimpan metadata. Namun, Anda dapat menentukan basis data Anda sendiri. Parameter koneksi untuk basis data ini dapat ditentukan dalam variabel lingkungan `NC_DB`. Selain itu, kami juga menyediakan ## Environment variables -Please refer to [Environment variables](https://docs.nocodb.com/getting-started/environment-variables) +Silakan lihat [Environment Variables](https://docs.nocodb.com/getting-started/environment-variables) untuk informasi lebih lanjut. # Pengaturan Pengembangan -Please refer to [Development Setup](https://docs.nocodb.com/engineering/development-setup) +Silakan lihat [Pengaturan Development](https://docs.nocodb.com/engineering/development-setup) untuk informasi lebih lanjut. # Berkontribusi - -Please refer to [Contribution Guide](https://github.com/nocodb/nocodb/blob/master/.github/CONTRIBUTING.md). +Silakan lihat [Panduan Kontribusi](https://github.com/nocodb/nocodb/blob/master/.github/CONTRIBUTING.md) untuk informasi lebih lanjut. # Kenapa kita membangun ini? -Sebagian besar bisnis internet melengkapi diri mereka dengan spreadsheet atau database untuk menyelesaikan kebutuhan bisnis mereka. Spreadsheet digunakan oleh satu miliar + manusia secara kolaboratif setiap hari. Namun, kami jauh bekerja dengan kecepatan yang sama pada basis data yang merupakan alat yang lebih kuat ketika datang ke komputasi. Upaya untuk menyelesaikan ini dengan persembahan SaaS berarti kontrol akses yang mengerikan, vendor lockin, data lockin, perubahan harga mendadak & paling penting plafon kaca pada apa yang mungkin di masa depan. +Sebagian besar bisnis internet melengkapi diri mereka dengan spreadsheet atau database untuk menyelesaikan kebutuhan bisnis mereka. Spreadsheet digunakan oleh satu miliar+ manusia secara kolaboratif setiap hari. Namun, kami jauh bekerja dengan kecepatan yang sama pada basis data yang merupakan alat yang lebih kuat ketika datang ke komputasi. Upaya untuk menyelesaikan ini dengan persembahan SaaS berarti kontrol akses yang mengerikan, vendor lockin, data lockin, perubahan harga mendadak & paling penting plafon kaca pada apa yang mungkin di masa depan. -# Misi kita +# Misi Kami -Misi kami adalah menyediakan antarmuka tanpa kode yang paling kuat untuk basis data yang merupakan sumber terbuka untuk setiap bisnis internet di dunia. Ini tidak hanya akan mendemokratisasi akses ke alat komputasi yang kuat tetapi juga memunculkan satu miliar + orang yang akan memiliki kemampuan mengotori-dan membangun radikal di Internet. +Misi kami adalah menyediakan antarmuka no-code yang paling kuat untuk basis data yang merupakan sumber terbuka untuk setiap bisnis internet di dunia. Ini tidak hanya akan mendemokratisasi akses ke alat komputasi yang kuat tetapi juga memunculkan satu miliar+ orang yang akan memiliki kemampuan membangun di Internet. + +# Lisensi +

+Proyek ini dilisensikan di bawah AGPLv3. +

diff --git a/packages/nc-gui/app.vue b/packages/nc-gui/app.vue index dfd4c809a6..4c57910a95 100644 --- a/packages/nc-gui/app.vue +++ b/packages/nc-gui/app.vue @@ -1,12 +1,15 @@ + + diff --git a/packages/nc-gui/components/api-client/Params.vue b/packages/nc-gui/components/api-client/Params.vue index 35da74a0a5..61aa3f1f2c 100644 --- a/packages/nc-gui/components/api-client/Params.vue +++ b/packages/nc-gui/components/api-client/Params.vue @@ -11,69 +11,77 @@ const vModel = useVModel(props, 'modelValue', emits) const addParamRow = () => vModel.value.push({}) -const deleteParamRow = (i: number) => vModel.value.splice(i, 1) +const deleteParamRow = (i: number) => { + if (vModel.value.length === 1) return + + vModel.value.splice(i, 1) +} + + diff --git a/packages/nc-gui/components/cell/Checkbox.vue b/packages/nc-gui/components/cell/Checkbox.vue index 94a87c5a0c..c95b66cd1e 100644 --- a/packages/nc-gui/components/cell/Checkbox.vue +++ b/packages/nc-gui/components/cell/Checkbox.vue @@ -35,7 +35,7 @@ const isForm = inject(IsFormInj) const readOnly = inject(ReadonlyInj) -const checkboxMeta = $computed(() => { +const checkboxMeta = computed(() => { return { icon: { checked: 'mdi-check-circle-outline', @@ -46,7 +46,7 @@ const checkboxMeta = $computed(() => { } }) -let vModel = $computed({ +const vModel = computed({ get: () => !!props.modelValue && props.modelValue !== '0' && props.modelValue !== 0, set: (val: any) => emits('update:modelValue', isMssql(column?.value?.base_id) ? +val : val), }) @@ -59,7 +59,7 @@ function onClick(force?: boolean, event?: MouseEvent) { return } if (!readOnly?.value && (force || active.value)) { - vModel = !vModel + vModel.value = !vModel.value } } diff --git a/packages/nc-gui/components/cell/ClampedText.vue b/packages/nc-gui/components/cell/ClampedText.vue index 88b4ab046d..d01c6fb0f4 100644 --- a/packages/nc-gui/components/cell/ClampedText.vue +++ b/packages/nc-gui/components/cell/ClampedText.vue @@ -13,7 +13,7 @@ const props = defineProps<{ '-webkit-line-clamp': props.lines || 1, '-webkit-box-orient': 'vertical', 'overflow': 'hidden', - 'white-space': 'pre', + 'word-break': 'break-all', }" > {{ props.value || '' }} diff --git a/packages/nc-gui/components/cell/Currency.vue b/packages/nc-gui/components/cell/Currency.vue index 51aa023d4a..799e90592b 100644 --- a/packages/nc-gui/components/cell/Currency.vue +++ b/packages/nc-gui/components/cell/Currency.vue @@ -59,7 +59,7 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputEle const submitCurrency = () => { if (lastSaved.value !== vModel.value) { - lastSaved.value = vModel.value + vModel.value = lastSaved.value = vModel.value ?? null emit('save') } editEnabled.value = false diff --git a/packages/nc-gui/components/cell/DatePicker.vue b/packages/nc-gui/components/cell/DatePicker.vue index 5cde5ec01f..ca3dac123e 100644 --- a/packages/nc-gui/components/cell/DatePicker.vue +++ b/packages/nc-gui/components/cell/DatePicker.vue @@ -30,22 +30,24 @@ const columnMeta = inject(ColumnInj, null)! const readOnly = inject(ReadonlyInj, ref(false)) +const isLockedMode = inject(IsLockedInj, ref(false)) + const active = inject(ActiveCellInj, ref(false)) const editable = inject(EditModeInj, ref(false)) -let isDateInvalid = $ref(false) +const isDateInvalid = ref(false) -const dateFormat = $computed(() => parseProp(columnMeta?.value?.meta)?.date_format ?? 'YYYY-MM-DD') +const dateFormat = computed(() => parseProp(columnMeta?.value?.meta)?.date_format ?? 'YYYY-MM-DD') -let localState = $computed({ +const localState = computed({ get() { if (!modelValue) { return undefined } if (!dayjs(modelValue).isValid()) { - isDateInvalid = true + isDateInvalid.value = true return undefined } @@ -77,7 +79,7 @@ watch( { flush: 'post' }, ) -const placeholder = computed(() => (modelValue === null && showNull.value ? 'NULL' : isDateInvalid ? 'Invalid date' : '')) +const placeholder = computed(() => (modelValue === null && showNull.value ? 'NULL' : isDateInvalid.value ? 'Invalid date' : '')) useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { switch (e.key) { @@ -110,7 +112,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { } break case 'ArrowLeft': - if (!localState) { + if (!localState.value) { ;(document.querySelector('.nc-picker-date.active .ant-picker-header-prev-btn') as HTMLButtonElement)?.click() } else { const prevEl = document.querySelector('.nc-picker-date.active .ant-picker-cell-selected') @@ -133,7 +135,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { } break case 'ArrowRight': - if (!localState) { + if (!localState.value) { ;(document.querySelector('.nc-picker-date.active .ant-picker-header-next-btn') as HTMLButtonElement)?.click() } else { const nextEl = document.querySelector('.nc-picker-date.active .ant-picker-cell-selected') @@ -156,15 +158,15 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { } break case 'ArrowUp': - if (!localState) + if (!localState.value) (document.querySelector('.nc-picker-date.active .ant-picker-header-super-prev-btn') as HTMLButtonElement)?.click() break case 'ArrowDown': - if (!localState) + if (!localState.value) (document.querySelector('.nc-picker-date.active .ant-picker-header-super-next-btn') as HTMLButtonElement)?.click() break case ';': - localState = dayjs(new Date()) + localState.value = dayjs(new Date()) break } }) @@ -206,7 +208,7 @@ const clickHandler = () => { :allow-clear="!readOnly && !localState && !isPk" :input-read-only="true" :dropdown-class-name="`${randomClass} nc-picker-date ${open ? 'active' : ''}`" - :open="(readOnly || (localState && isPk)) && !active && !editable ? false : open" + :open="((readOnly || (localState && isPk)) && !active && !editable) || isLockedMode ? false : open" @click="clickHandler" @update:open="updateOpen" > diff --git a/packages/nc-gui/components/cell/DateTimePicker.vue b/packages/nc-gui/components/cell/DateTimePicker.vue index 397ceda16f..38c0d1417d 100644 --- a/packages/nc-gui/components/cell/DateTimePicker.vue +++ b/packages/nc-gui/components/cell/DateTimePicker.vue @@ -36,11 +36,13 @@ const active = inject(ActiveCellInj, ref(false)) const editable = inject(EditModeInj, ref(false)) +const isLockedMode = inject(IsLockedInj, ref(false)) + const column = inject(ColumnInj)! -let isDateInvalid = $ref(false) +const isDateInvalid = ref(false) -const dateTimeFormat = $computed(() => { +const dateTimeFormat = computed(() => { const dateFormat = parseProp(column?.value?.meta)?.date_format ?? dateFormats[0] const timeFormat = parseProp(column?.value?.meta)?.time_format ?? timeFormats[0] return `${dateFormat} ${timeFormat}` @@ -48,14 +50,14 @@ const dateTimeFormat = $computed(() => { let localModelValue = modelValue ? dayjs(modelValue).utc().local() : undefined -let localState = $computed({ +const localState = computed({ get() { if (!modelValue) { return undefined } if (!dayjs(modelValue).isValid()) { - isDateInvalid = true + isDateInvalid.value = true return undefined } @@ -129,7 +131,7 @@ watch( { flush: 'post' }, ) -const placeholder = computed(() => (modelValue === null && showNull.value ? 'NULL' : isDateInvalid ? 'Invalid date' : '')) +const placeholder = computed(() => (modelValue === null && showNull.value ? 'NULL' : isDateInvalid.value ? 'Invalid date' : '')) useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { switch (e.key) { @@ -158,7 +160,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { } break case 'ArrowLeft': - if (!localState) { + if (!localState.value) { ;(document.querySelector('.nc-picker-datetime.active .ant-picker-header-prev-btn') as HTMLButtonElement)?.click() } else { const prevEl = document.querySelector('.nc-picker-datetime.active .ant-picker-cell-selected') @@ -181,7 +183,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { } break case 'ArrowRight': - if (!localState) { + if (!localState.value) { ;(document.querySelector('.nc-picker-datetime.active .ant-picker-header-next-btn') as HTMLButtonElement)?.click() } else { const nextEl = document.querySelector('.nc-picker-datetime.active .ant-picker-cell-selected') @@ -204,15 +206,15 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { } break case 'ArrowUp': - if (!localState) + if (!localState.value) (document.querySelector('.nc-picker-datetime.active .ant-picker-header-super-prev-btn') as HTMLButtonElement)?.click() break case 'ArrowDown': - if (!localState) + if (!localState.value) (document.querySelector('.nc-picker-datetime.active .ant-picker-header-super-next-btn') as HTMLButtonElement)?.click() break case ';': - localState = dayjs(new Date()) + localState.value = dayjs(new Date()) break } }) @@ -248,7 +250,7 @@ const clickHandler = () => { :allow-clear="!readOnly && !localState && !isPk" :input-read-only="true" :dropdown-class-name="`${randomClass} nc-picker-datetime ${open ? 'active' : ''}`" - :open="readOnly || (localState && isPk) ? false : open && (active || editable)" + :open="readOnly || (localState && isPk) || isLockedMode ? false : open && (active || editable)" :disabled="readOnly || (localState && isPk)" @click="clickHandler" @ok="open = !open" diff --git a/packages/nc-gui/components/cell/Decimal.vue b/packages/nc-gui/components/cell/Decimal.vue index 562a4ac858..87b2170325 100644 --- a/packages/nc-gui/components/cell/Decimal.vue +++ b/packages/nc-gui/components/cell/Decimal.vue @@ -21,8 +21,24 @@ const { showNull } = useGlobal() const editEnabled = inject(EditModeInj) +const column = inject(ColumnInj, null)! + +const domRef = ref() + +const meta = computed(() => { + return typeof column?.value.meta === 'string' ? JSON.parse(column.value.meta) : column?.value.meta ?? {} +}) + const _vModel = useVModel(props, 'modelValue', emits) +const displayValue = computed(() => { + if (_vModel.value === null) return null + + if (isNaN(Number(_vModel.value))) return null + + return Number(_vModel.value).toFixed(meta.value.precision ?? 1) +}) + const vModel = computed({ get: () => _vModel.value, set: (value) => { @@ -36,9 +52,39 @@ const vModel = computed({ }, }) +const precision = computed(() => { + const meta = typeof column?.value.meta === 'string' ? JSON.parse(column.value.meta) : column?.value.meta ?? {} + const _precision = meta.precision ?? 1 + + return Number(0.1 ** _precision).toFixed(_precision) +}) + const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! +// Handle the arrow keys as its default behavior is to increment/decrement the value +const onKeyDown = (e: any) => { + if (e.key === 'ArrowDown') { + e.preventDefault() + // Move the cursor to the end of the input + e.target.type = 'text' + e.target?.setSelectionRange(e.target.value.length, e.target.value.length) + e.target.type = 'number' + } else if (e.key === 'ArrowUp') { + e.preventDefault() + + e.target.type = 'text' + e.target?.setSelectionRange(0, 0) + e.target.type = 'number' + } +} + const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() + +watch(isExpandedFormOpen, () => { + if (!isExpandedFormOpen.value) { + domRef.value?.focus() + } +}) diff --git a/packages/nc-gui/components/cell/Duration.vue b/packages/nc-gui/components/cell/Duration.vue index f9c9f80eb1..6c579311a9 100644 --- a/packages/nc-gui/components/cell/Duration.vue +++ b/packages/nc-gui/components/cell/Duration.vue @@ -15,7 +15,7 @@ import { interface Props { modelValue: number | string | null | undefined - showValidationError: boolean + showValidationError?: boolean } const { modelValue, showValidationError = true } = defineProps() diff --git a/packages/nc-gui/components/cell/Email.vue b/packages/nc-gui/components/cell/Email.vue index b15b6d4e52..2c4669d118 100644 --- a/packages/nc-gui/components/cell/Email.vue +++ b/packages/nc-gui/components/cell/Email.vue @@ -10,6 +10,8 @@ const { modelValue: value } = defineProps() const emit = defineEmits(['update:modelValue']) +const rowHeight = inject(RowHeightInj, ref(undefined)) + const { t } = useI18n() const { showNull } = useGlobal() @@ -73,8 +75,8 @@ watch( NULL - {{ vModel }} + - {{ vModel }} + diff --git a/packages/nc-gui/components/cell/GeoData.vue b/packages/nc-gui/components/cell/GeoData.vue index 68a3cdcf11..042f5bd838 100644 --- a/packages/nc-gui/components/cell/GeoData.vue +++ b/packages/nc-gui/components/cell/GeoData.vue @@ -16,17 +16,17 @@ const emits = defineEmits() const vModel = useVModel(props, 'modelValue', emits) -let isExpanded = $ref(false) +const isExpanded = ref(false) -let isLoading = $ref(false) +const isLoading = ref(false) -let isLocationSet = $ref(false) +const isLocationSet = ref(false) const [latitude, longitude] = (vModel.value || '').split(';') const latLongStr = computed(() => { const [latitude, longitude] = (vModel.value || '').split(';') - if (latitude) isLocationSet = true + if (latitude) isLocationSet.value = true return latitude && longitude ? `${latitude}; ${longitude}` : 'Set location' }) @@ -37,28 +37,28 @@ const formState = reactive({ const handleFinish = () => { vModel.value = latLongToJoinedString(parseFloat(formState.latitude), parseFloat(formState.longitude)) - isExpanded = false + isExpanded.value = false } const clear = () => { - isExpanded = false + isExpanded.value = false formState.latitude = latitude formState.longitude = longitude } const onClickSetCurrentLocation = () => { - isLoading = true + isLoading.value = true const onSuccess: PositionCallback = (position: GeolocationPosition) => { const crd = position.coords formState.latitude = `${crd.latitude}` formState.longitude = `${crd.longitude}` - isLoading = false + isLoading.value = false } const onError: PositionErrorCallback = (err: GeolocationPositionError) => { console.error(`ERROR(${err.code}): ${err.message}`) - isLoading = false + isLoading.value = false } const options = { diff --git a/packages/nc-gui/components/cell/Integer.vue b/packages/nc-gui/components/cell/Integer.vue index 0800c09823..793d9ed5c3 100644 --- a/packages/nc-gui/components/cell/Integer.vue +++ b/packages/nc-gui/components/cell/Integer.vue @@ -23,6 +23,14 @@ const editEnabled = inject(EditModeInj) const _vModel = useVModel(props, 'modelValue', emits) +const displayValue = computed(() => { + if (_vModel.value === null) return null + + if (isNaN(Number(_vModel.value))) return null + + return Number(_vModel.value) +}) + const vModel = computed({ get: () => _vModel.value, set: (value) => { @@ -40,17 +48,33 @@ const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() -function onKeyDown(evt: KeyboardEvent) { - const cmdOrCtrl = isMac() ? evt.metaKey : evt.ctrlKey - if (cmdOrCtrl && !evt.altKey) { - switch (evt.keyCode) { +function onKeyDown(e: any) { + const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey + if (cmdOrCtrl && !e.altKey) { + switch (e.keyCode) { case 90: { - evt.stopPropagation() + e.stopPropagation() break } } } - return evt.key === '.' && evt.preventDefault() + if (e.key === '.') { + return e.preventDefault() + } + + if (e.key === 'ArrowDown') { + e.preventDefault() + // Move the cursor to the end of the input + e.target.type = 'text' + e.target?.setSelectionRange(e.target.value.length, e.target.value.length) + e.target.type = 'number' + } else if (e.key === 'ArrowUp') { + e.preventDefault() + + e.target.type = 'text' + e.target?.setSelectionRange(0, 0) + e.target.type = 'number' + } } @@ -61,6 +85,7 @@ function onKeyDown(evt: KeyboardEvent) { v-model="vModel" class="outline-none p-0 border-none w-full h-full text-sm" type="number" + style="letter-spacing: 0.06rem" @blur="editEnabled = false" @keydown="onKeyDown" @keydown.down.stop @@ -72,11 +97,23 @@ function onKeyDown(evt: KeyboardEvent) { @mousedown.stop /> NULL - {{ vModel }} + {{ displayValue }} diff --git a/packages/nc-gui/components/cell/Json.vue b/packages/nc-gui/components/cell/Json.vue index 3042946ff4..1206b4b5b2 100644 --- a/packages/nc-gui/components/cell/Json.vue +++ b/packages/nc-gui/components/cell/Json.vue @@ -39,25 +39,25 @@ const vModel = useVModel(props, 'modelValue', emits) const localValueState = ref() -let error = $ref() +const error = ref() -let isExpanded = $ref(false) +const isExpanded = ref(false) const localValue = computed | undefined>({ get: () => localValueState.value, set: (val: undefined | string | Record) => { localValueState.value = typeof val === 'object' ? JSON.stringify(val, null, 2) : val /** if form and not expanded then sync directly */ - if (isForm.value && !isExpanded) { + if (isForm.value && !isExpanded.value) { vModel.value = val } }, }) const clear = () => { - error = undefined + error.value = undefined - isExpanded = false + isExpanded.value = false editEnabled.value = false @@ -66,44 +66,59 @@ const clear = () => { const formatJson = (json: string) => { try { - return JSON.stringify(JSON.parse(json), null, 2) + json = json + .trim() + .replace(/^\{\s*|\s*\}$/g, '') + .replace(/\n\s*/g, '') + json = `{${json}}` + + return json } catch (e) { + console.log(e) return json } } const onSave = () => { - isExpanded = false + isExpanded.value = false editEnabled.value = false - localValue.value = localValue ? formatJson(localValue.value as string) : localValue + vModel.value = localValue ? formatJson(localValue.value as string) : localValue +} - vModel.value = localValue.value +const setLocalValue = (val: any) => { + try { + localValue.value = typeof val === 'string' ? JSON.stringify(JSON.parse(val), null, 2) : val + } catch (e) { + localValue.value = val + } } watch( vModel, (val) => { - localValue.value = val + setLocalValue(val) }, { immediate: true }, ) -watch(localValue, (val) => { +watch([localValue, editEnabled], () => { try { - JSON.parse(val as string) + JSON.parse(localValue.value as string) - error = undefined + error.value = undefined } catch (e: any) { - error = e + if (localValue.value === undefined) return + + error.value = e } }) watch(editEnabled, () => { - isExpanded = false + isExpanded.value = false - localValue.value = vModel.value + setLocalValue(vModel.value) }) useSelectedCellKeyupListener(active, (e) => { diff --git a/packages/nc-gui/components/cell/MultiSelect.vue b/packages/nc-gui/components/cell/MultiSelect.vue index 7a22788276..69d7628c6b 100644 --- a/packages/nc-gui/components/cell/MultiSelect.vue +++ b/packages/nc-gui/components/cell/MultiSelect.vue @@ -4,6 +4,7 @@ import { message } from 'ant-design-vue' import tinycolor from 'tinycolor2' import type { Select as AntSelect } from 'ant-design-vue' import type { SelectOptionType, SelectOptionsType } from 'nocodb-sdk' +import { WorkspaceUserRoles } from 'nocodb-sdk' import { ActiveCellInj, CellClickHookInj, @@ -46,6 +47,8 @@ const column = inject(ColumnInj)! const readOnly = inject(ReadonlyInj)! +const isLockedMode = inject(IsLockedInj, ref(false)) + const isEditable = inject(EditModeInj, ref(false)) const activeCell = inject(ActiveCellInj, ref(false)) @@ -99,7 +102,15 @@ const isOptionMissing = computed(() => { return (options.value ?? []).every((op) => op.title !== searchVal.value) }) -const hasEditRoles = computed(() => hasRole('owner', true) || hasRole('creator', true) || hasRole('editor', true)) +const hasEditRoles = computed( + () => + hasRole('owner', true) || + hasRole('creator', true) || + hasRole('editor', true) || + hasRole(WorkspaceUserRoles.OWNER, true) || + hasRole(WorkspaceUserRoles.CREATOR, true) || + hasRole(WorkspaceUserRoles.EDITOR, true), +) const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && active.value) @@ -334,7 +345,11 @@ const selectedOpts = computed(() => {