Browse Source

Merge pull request #9619 from nocodb/develop

pull/9620/head 0.256.0
github-actions[bot] 2 months ago committed by GitHub
parent
commit
953971e5c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .github/workflows/bats-test.yml
  2. 62
      .github/workflows/jest-unit-test.yml
  3. 156
      .github/workflows/release-secret-cli.yml
  4. 28
      .github/workflows/validate-docs-links.yml
  5. 2
      .gitignore
  6. 139
      README.md
  7. 2
      charts/nocodb/values.yaml
  8. 15
      docker-compose/1_Auto_Upstall/README.md
  9. 1018
      docker-compose/1_Auto_Upstall/noco.sh
  10. 0
      docker-compose/1_Auto_Upstall/tests/configure/monitor.bats
  11. 0
      docker-compose/1_Auto_Upstall/tests/configure/restart.bats
  12. 0
      docker-compose/1_Auto_Upstall/tests/configure/scale.bats
  13. 0
      docker-compose/1_Auto_Upstall/tests/configure/setup.sh
  14. 0
      docker-compose/1_Auto_Upstall/tests/configure/start.bats
  15. 0
      docker-compose/1_Auto_Upstall/tests/configure/stop.bats
  16. 0
      docker-compose/1_Auto_Upstall/tests/configure/upgrade.bats
  17. 0
      docker-compose/1_Auto_Upstall/tests/expects/configure/monitor.sh
  18. 0
      docker-compose/1_Auto_Upstall/tests/expects/configure/restart.sh
  19. 0
      docker-compose/1_Auto_Upstall/tests/expects/configure/scale.sh
  20. 0
      docker-compose/1_Auto_Upstall/tests/expects/configure/start.sh
  21. 0
      docker-compose/1_Auto_Upstall/tests/expects/configure/stop.sh
  22. 0
      docker-compose/1_Auto_Upstall/tests/expects/configure/upgrade.sh
  23. 0
      docker-compose/1_Auto_Upstall/tests/expects/install/default.sh
  24. 0
      docker-compose/1_Auto_Upstall/tests/expects/install/ip.sh
  25. 0
      docker-compose/1_Auto_Upstall/tests/expects/install/redis.sh
  26. 0
      docker-compose/1_Auto_Upstall/tests/expects/install/scale.sh
  27. 0
      docker-compose/1_Auto_Upstall/tests/expects/install/ssl.sh
  28. 0
      docker-compose/1_Auto_Upstall/tests/expects/install/watchtower.sh
  29. 0
      docker-compose/1_Auto_Upstall/tests/install/default.bats
  30. 0
      docker-compose/1_Auto_Upstall/tests/install/ip.bats
  31. 0
      docker-compose/1_Auto_Upstall/tests/install/redis.bats
  32. 0
      docker-compose/1_Auto_Upstall/tests/install/scale.bats
  33. 0
      docker-compose/1_Auto_Upstall/tests/install/setup.sh
  34. 0
      docker-compose/1_Auto_Upstall/tests/install/ssl.bats
  35. 0
      docker-compose/1_Auto_Upstall/tests/install/watchtower.bats
  36. 0
      docker-compose/1_Auto_Upstall/tests/mocks/clear
  37. 0
      docker-compose/1_Auto_Upstall/tests/mocks/nproc
  38. 0
      docker-compose/2_pg/docker-compose.yml
  39. 0
      docker-compose/3_traefik/.env
  40. 0
      docker-compose/3_traefik/README.md
  41. 0
      docker-compose/3_traefik/docker-compose.yml
  42. 60
      docker-compose/letsencrypt/nc.sh
  43. 39
      docker-compose/mysql/docker-compose.yml
  44. 948
      docker-compose/setup-script/noco.sh
  45. BIN
      docker-compose/sqlite/nocodb/noco.db
  46. 2
      markdown/readme/languages/chinese.md
  47. 12
      markdown/readme/languages/french.md
  48. 2
      markdown/readme/languages/indonesian.md
  49. 81
      markdown/readme/languages/portuguese.md
  50. 2
      markdown/readme/languages/ukrainian.md
  51. 891
      packages/nc-cli/package-lock.json
  52. 8
      packages/nc-gui/app.vue
  53. 13
      packages/nc-gui/assets/nc-icons/discord.svg
  54. 7
      packages/nc-gui/assets/nc-icons/file-big.svg
  55. 5
      packages/nc-gui/assets/nc-icons/info-solid.svg
  56. 5
      packages/nc-gui/assets/nc-icons/megaphone.svg
  57. 13
      packages/nc-gui/assets/nc-icons/nocodb.svg
  58. 3
      packages/nc-gui/assets/nc-icons/placeholder-icon.svg
  59. 16
      packages/nc-gui/assets/nc-icons/reddit.svg
  60. 12
      packages/nc-gui/assets/nc-icons/refresh-cw.svg
  61. 8
      packages/nc-gui/assets/nc-icons/script.svg
  62. 3
      packages/nc-gui/assets/nc-icons/spanner.svg
  63. 2
      packages/nc-gui/assets/nc-icons/star.svg
  64. 10
      packages/nc-gui/assets/nc-icons/twitter-x-line.svg
  65. 11
      packages/nc-gui/assets/nc-icons/youtube2.svg
  66. 5
      packages/nc-gui/assets/style.scss
  67. 6
      packages/nc-gui/components/account/ResetPassword.vue
  68. 1
      packages/nc-gui/components/account/Setup.vue
  69. 3
      packages/nc-gui/components/account/Token.vue
  70. 4
      packages/nc-gui/components/cell/Currency.vue
  71. 23
      packages/nc-gui/components/cell/DatePicker.vue
  72. 23
      packages/nc-gui/components/cell/DateTimePicker.vue
  73. 2
      packages/nc-gui/components/cell/Decimal.vue
  74. 2
      packages/nc-gui/components/cell/Duration.vue
  75. 2
      packages/nc-gui/components/cell/Email.vue
  76. 2
      packages/nc-gui/components/cell/Float.vue
  77. 2
      packages/nc-gui/components/cell/Integer.vue
  78. 4
      packages/nc-gui/components/cell/Json.vue
  79. 2
      packages/nc-gui/components/cell/Percent.vue
  80. 2
      packages/nc-gui/components/cell/PhoneNumber.vue
  81. 1
      packages/nc-gui/components/cell/RichText.vue
  82. 2
      packages/nc-gui/components/cell/Text.vue
  83. 3
      packages/nc-gui/components/cell/TextArea.vue
  84. 23
      packages/nc-gui/components/cell/TimePicker.vue
  85. 2
      packages/nc-gui/components/cell/Url.vue
  86. 23
      packages/nc-gui/components/cell/YearPicker.vue
  87. 222
      packages/nc-gui/components/cmd-k/index.vue
  88. 3
      packages/nc-gui/components/dashboard/Sidebar.vue
  89. 66
      packages/nc-gui/components/dashboard/Sidebar/Feed.vue
  90. 10
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  91. 2
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  92. 46
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  93. 5
      packages/nc-gui/components/dashboard/TreeView/index.vue
  94. 2
      packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue
  95. 1
      packages/nc-gui/components/dlg/TableDescriptionUpdate.vue
  96. 15
      packages/nc-gui/components/dlg/ViewCreate.vue
  97. 86
      packages/nc-gui/components/extensions/Details.vue
  98. 238
      packages/nc-gui/components/extensions/Extension.vue
  99. 185
      packages/nc-gui/components/extensions/Extension/Header.vue
  100. 12
      packages/nc-gui/components/extensions/Extension/HeaderMenu.vue
  101. Some files were not shown because too many files have changed in this diff Show More

2
.github/workflows/bats-test.yml

@ -3,7 +3,7 @@ name: Run BATS Tests
on: on:
push: push:
paths: paths:
- 'docker-compose/setup-script/noco.sh' - 'docker-compose/1_Auto_Upstall/noco.sh'
workflow_dispatch: workflow_dispatch:
jobs: jobs:

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

@ -0,0 +1,62 @@
name: "NestJS Unit Test"
on:
push:
branches: [develop]
paths:
- "packages/nocodb/**"
- ".github/workflows/jest-unit-test.yml"
pull_request:
types: [opened, reopened, synchronize, ready_for_review, labeled]
branches: [develop]
paths:
- "packages/nocodb/**"
- ".github/workflows/jest-unit-test.yml"
workflow_call:
# Triggered manually
workflow_dispatch:
jobs:
jest-unit-test:
runs-on: [self-hosted, aws]
timeout-minutes: 20
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft || inputs.force == true }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18.19.1
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 8
- name: Get pnpm store directory
shell: bash
timeout-minutes: 1
run: |
echo "STORE_PATH=/root/setup-pnpm/node_modules/.bin/store/v3" >> $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: Set NC Edition
run: export EE=true
- name: remove use-node-version line from .npmrc
run: sed -i '/^use-node-version/d' .npmrc
- name: install dependencies
run: pnpm bootstrap
- name: build nocodb-sdk
working-directory: ./packages/nocodb-sdk
run: |
pnpm run generate:sdk
pnpm run build:main
- name: run unit tests
working-directory: ./packages/nocodb
run: pnpm run test

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

@ -0,0 +1,156 @@
name: "Release : Secret CLI NPM & Executables"
on:
# Triggered manually
workflow_dispatch:
inputs:
tag:
description: "Tag name"
required: true
secrets:
# Replace with `NC_GITHUB_TOKEN` once replaced with a token which have access to `nocodb/nc-secret-mgr`
NC_GITHUB_TOKEN_TEMP:
required: true
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 8
- 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'
- name: Cache pkg modules
id: cache-pkg
uses: actions/cache@v3
env:
cache-name: cache-pkg
with:
# pkg cache files are stored in `~/.pkg-cache`
path: ~/.pkg-cache
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Npm package build and publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
pnpm bootstrap
cd ./packages/nocodb
pnpm run build:cli:module
cd ../nc-secret-mgr
targetVersion=${{ github.event.inputs.tag || inputs.tag }} node ../../scripts/updateCliVersion.js
pnpm run build && pnpm run npm:publish
# for building images for all platforms these libraries are required in Linux
- name: Install QEMU and ldid
run: |
sudo apt update
# Install qemu
sudo apt install qemu binfmt-support qemu-user-static -y
# install ldid
git clone https://github.com/daeken/ldid.git
cd ./ldid
./make.sh
sudo cp ./ldid /usr/local/bin
- uses: actions/setup-node@v3
with:
node-version: 16
- name : Install nocodb, other dependencies and build executables
run: |
cd ./packages/nc-secret-mgr
# install npm dependendencies
pnpm i
# Build sqlite binaries for all platforms
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --fallback-to-build --target_arch=x64 --target_libc=unknown
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --fallback-to-build --target_arch=ia32 --target_libc=unknown
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --fallback-to-build --target_arch=x64 --target_libc=unknown
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --fallback-to-build --target_arch=arm64 --target_libc=unknown
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --fallback-to-build --target_arch=x64 --target_libc=glibc
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --fallback-to-build --target_arch=arm64 --target_libc=glibc
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --fallback-to-build --target_arch=x64 --target_libc=musl
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --fallback-to-build --target_arch=arm64 --target_libc=musl
# clean up code to optimize size
npx modclean --patterns="default:*" --run
# build executables
npm run build:pkg
ls ./dist-pkg
# Move macOS executables for signing
mkdir ./mac-dist
mv ./dist-pkg/nc-secret-mgr-macos-arm64 ./mac-dist/
mv ./dist-pkg/nc-secret-mgr-macos-x64 ./mac-dist/
- name: Upload executables(except mac executables) to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.NC_GITHUB_TOKEN_TEMP }}
file: packages/nc-secret-mgr/dist-pkg/**
tag: ${{ github.event.inputs.tag || inputs.tag }}
overwrite: true
file_glob: true
repo_name: nocodb/nc-secret-mgr
- uses: actions/upload-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: packages/nc-secret-mgr/mac-dist
retention-days: 1
sign-mac-executables:
runs-on: macos-latest
needs: build-and-publish
steps:
- uses: actions/download-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
path: packages/nc-secret-mgr/mac-dist
- name: Sign macOS executables
run: |
/usr/bin/codesign --force -s - ./packages/nc-secret-mgr/mac-dist/nc-secret-mgr-macos-arm64 -v
/usr/bin/codesign --force -s - ./packages/nc-secret-mgr/mac-dist/nc-secret-mgr-macos-x64 -v
- uses: actions/upload-artifact@master
with:
name: ${{ format('{0}-signed', github.event.inputs.tag || inputs.tag) }}
path: packages/nc-secret-mgr/mac-dist
retention-days: 1
publish-mac-executables:
needs: [sign-mac-executables,build-and-publish]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@master
with:
name: ${{ format('{0}-signed', github.event.inputs.tag || inputs.tag) }}
path: packages/nc-secret-mgr/mac-dist
- name: Upload executables(except mac executables) to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.NC_GITHUB_TOKEN_TEMP }}
file: packages/nc-secret-mgr/mac-dist/**
tag: ${{ github.event.inputs.tag || inputs.tag }}
overwrite: true
file_glob: true
repo_name: nocodb/nc-secret-mgr

28
.github/workflows/validate-docs-links.yml

@ -0,0 +1,28 @@
name: "Validate: Docs"
on:
# Triggered manually
workflow_dispatch:
pull_request:
types: [opened, reopened, synchronize, ready_for_review, labeled]
branches: [develop]
paths:
- "packages/noco-docs/**"
jobs:
validate-docs:
runs-on: [self-hosted, aws]
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 18.19.1
- name: Build docs
run: |
cd packages/noco-docs
npm install
npm run generate
npm run remark:once

2
.gitignore vendored

@ -96,3 +96,5 @@ test_noco.db
httpbin httpbin
.run/test-debug.run.xml .run/test-debug.run.xml
/packages/nc-secret-mgr/dist/index.js
/packages/nc-secret-mgr/dist/index.js.map

139
README.md

@ -1,6 +1,6 @@
<h1 align="center" style="border-bottom: none"> <h1 align="center" style="border-bottom: none">
<div> <div>
<a href="https://www.nocodb.com"> <a style="color:#36f" href="https://www.nocodb.com">
<img src="/packages/nc-gui/assets/img/icons/512x512.png" width="80" /> <img src="/packages/nc-gui/assets/img/icons/512x512.png" width="80" />
<br> <br>
NocoDB NocoDB
@ -10,15 +10,9 @@
</h1> </h1>
<p align="center"> <p align="center">
Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart spreadsheet. NocoDB is the fastest and easiest way to build databases online.
</p> </p>
<div align="center">
[![Node version](https://img.shields.io/badge/node-%3E%3D%2018.19.1-brightgreen)](http://nodejs.org/download/)
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-green.svg)](https://conventionalcommits.org)
</div>
<p align="center"> <p align="center">
<a href="http://www.nocodb.com"><b>Website</b></a> <a href="http://www.nocodb.com"><b>Website</b></a>
@ -49,10 +43,6 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart spreadshe
<img src="https://static.scarf.sh/a.png?x-pxid=c12a77cc-855e-4602-8a0f-614b2d0da56a" /> <img src="https://static.scarf.sh/a.png?x-pxid=c12a77cc-855e-4602-8a0f-614b2d0da56a" />
# Join Our Team
<p align=""><a href="http://careers.nocodb.com" target="_blank"><img src="https://user-images.githubusercontent.com/61551451/169663818-45643495-e95b-48e2-be13-01d6a77dc2fd.png" width="250"/></a></p>
# Join Our Community # Join Our Community
<a href="https://discord.gg/5RgZmkW" target="_blank"> <a href="https://discord.gg/5RgZmkW" target="_blank">
@ -61,50 +51,63 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart spreadshe
[![Stargazers repo roster for @nocodb/nocodb](http://reporoster.com/stars/nocodb/nocodb)](https://github.com/nocodb/nocodb/stargazers) [![Stargazers repo roster for @nocodb/nocodb](http://reporoster.com/stars/nocodb/nocodb)](https://github.com/nocodb/nocodb/stargazers)
# Quick try
## Docker # Installation
## Docker with SQLite
```bash ```bash
# with PostgreSQL docker run -d --name noco
docker run -d --name nocodb-postgres \ -v "$(pwd)"/nocodb:/usr/app/data/
-v "$(pwd)"/nocodb:/usr/app/data/ \ -p 8080:8080
-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 nocodb/nocodb:latest
```
# with SQLite : mounting volume `/usr/app/data/` is crucial to avoid data loss. ## Docker with PG
docker run -d --name nocodb \ ```bash
-v "$(pwd)"/nocodb:/usr/app/data/ \ docker run -d --name noco
-p 8080:8080 \ -v "$(pwd)"/nocodb:/usr/app/data/
-p 8080:8080
# replace with your pg connection string
-e NC_DB="pg://host.docker.internal:5432?u=root&p=password&d=d1"
# replace with a random secret
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010"
nocodb/nocodb:latest nocodb/nocodb:latest
``` ```
## Binaries ## Auto-upstall
🚥 Binaries are intended for ONLY quick trials or testing purposes and are not recommended for production use. Auto-upstall is a single command that sets up NocoDB on a server for production usage.
| OS | Architecture | Command | Behind the scenes it auto-generates docker-compose for you.
|---------|--------------|----------------------------------------------------------------------------------------------|
| macOS | arm64 | `curl http://get.nocodb.com/macos-arm64 -o nocodb -L && chmod +x nocodb && ./nocodb` |
| macOS | x64 | `curl http://get.nocodb.com/macos-x64 -o nocodb -L && chmod +x nocodb && ./nocodb` |
| Linux | x64 | `curl http://get.nocodb.com/linux-x64 -o nocodb -L && chmod +x nocodb && ./nocodb` |
| Linux | arm64 | `curl http://get.nocodb.com/linux-arm64 -o nocodb -L && chmod +x nocodb && ./nocodb` |
| Windows | x64 | `iwr http://get.nocodb.com/win-x64.exe -o Noco-win-x64.exe &&.\Noco-win-x64.exe` |
| Windows | arm64 | `iwr http://get.nocodb.com/win-arm64.exe -o Noco-win-arm64.exe && .\Noco-win-arm64.exe` |
```bash
bash <(curl -sSL http://install.nocodb.com/noco.sh) <(mktemp)
```
## Docker Compose Auto-upstall does the following : 🕊
- 🐳 Automatically installs all pre-requisites like docker, docker-compose
- 🚀 Automatically installs NocoDB with PostgreSQL, Redis, Minio, Traefik gateway using Docker Compose. 🐘 🗄 🌐
- 🔄 Automatically upgrades NocoDB to the latest version when you run the command again.
- 🔒 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)
We provide different docker-compose.yml files under [this directory](https://github.com/nocodb/nocodb/tree/master/docker-compose). Here are some examples.
```bash
git clone https://github.com/nocodb/nocodb
cd nocodb/docker-compose/pg
```
# GUI ## Other Methods
> Binaries are only for quick testing locally.
Access Dashboard using: [http://localhost:8080/dashboard](http://localhost:8080/dashboard) | Install Method | Command to install |
|-------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 🍏 MacOS arm64 <br>(Binary) | `curl http://get.nocodb.com/macos-arm64 -o nocodb -L && chmod +x nocodb && ./nocodb` |
| 🍏 MacOS x64 <br>(Binary) | `curl http://get.nocodb.com/macos-x64 -o nocodb -L && chmod +x nocodb && ./nocodb` |
| 🐧 Linux arm64 <br>(Binary) | `curl http://get.nocodb.com/linux-arm64 -o nocodb -L && chmod +x nocodb && ./nocodb` |
| 🐧 Linux x64 <br>(Binary) | `curl http://get.nocodb.com/linux-x64 -o nocodb -L && chmod +x nocodb && ./nocodb` |
| 🪟 Windows arm64 <br>(Binary) | `iwr http://get.nocodb.com/win-arm64.exe -o Noco-win-arm64.exe && .\Noco-win-arm64.exe` |
| 🪟 Windows x64 <br>(Binary) | `iwr http://get.nocodb.com/win-x64.exe -o Noco-win-x64.exe && .\Noco-win-x64.exe` |
> When running locally access nocodb by visiting: [http://localhost:8080/dashboard](http://localhost:8080/dashboard)
# Screenshots # Screenshots
![2](https://github.com/nocodb/nocodb/assets/86527202/a127c05e-2121-4af2-a342-128e0e2d0291) ![2](https://github.com/nocodb/nocodb/assets/86527202/a127c05e-2121-4af2-a342-128e0e2d0291)
@ -122,40 +125,16 @@ Access Dashboard using: [http://localhost:8080/dashboard](http://localhost:8080/
![11](https://user-images.githubusercontent.com/35857179/194844903-c1e47f40-e782-4f5d-8dce-6449cc70b181.png) ![11](https://user-images.githubusercontent.com/35857179/194844903-c1e47f40-e782-4f5d-8dce-6449cc70b181.png)
![12](https://user-images.githubusercontent.com/35857179/194844907-09277d3e-cbbf-465c-9165-6afc4161e279.png) ![12](https://user-images.githubusercontent.com/35857179/194844907-09277d3e-cbbf-465c-9165-6afc4161e279.png)
# Table of Contents
- [Quick try](#quick-try)
- [Docker](#docker)
- [Docker Compose](#docker-compose)
- [GUI](#gui)
- [Join Our Community](#join-our-community)
- [Screenshots](#screenshots)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Rich Spreadsheet Interface](#rich-spreadsheet-interface)
- [App Store for Workflow Automations](#app-store-for-workflow-automations)
- [Programmatic Access](#programmatic-access)
- [Sync Schema](#sync-schema)
- [Audit](#audit)
- [Production Setup](#production-setup)
- [Environment variables](#environment-variables)
- [Development Setup](#development-setup)
- [Contributing](#contributing)
- [Why are we building this?](#why-are-we-building-this)
- [Our Mission](#our-mission)
- [License](#license)
- [Contributors](#contributors)
# Features # Features
### Rich Spreadsheet Interface ### Rich Spreadsheet Interface
- ⚡ &nbsp;Basic Operations: Create, Read, Update and Delete Tables, Columns, and Rows - ⚡ &nbsp;Basic Operations: Create, Read, Update and Delete Tables, Columns, and Rows
- ⚡ &nbsp;Fields Operations: Sort, Filter, Hide / Unhide Columns - ⚡ &nbsp;Fields Operations: Sort, Filter, Group, Hide / Unhide Columns
- ⚡ &nbsp;Multiple Views Types: Grid (By default), Gallery, Form View and Kanban View - ⚡ &nbsp;Multiple Views Types: Grid (By default), Gallery, Form, Kanban and Calendar View
- ⚡ &nbsp;View Permissions Types: Collaborative Views, & Locked Views - ⚡ &nbsp;View Permissions Types: Collaborative Views, & Locked Views
- ⚡ &nbsp;Share Bases / Views: either Public or Private (with Password Protected) - ⚡ &nbsp;Share Bases / Views: either Public or Private (with Password Protected)
- ⚡ &nbsp;Variant Cell Types: ID, LinkToAnotherRecord, Lookup, Rollup, SingleLineText, Attachment, Currency, Formula, etc - ⚡ &nbsp;Variant Cell Types: ID, Links, Lookup, Rollup, SingleLineText, Attachment, Currency, Formula, User, etc
- ⚡ &nbsp;Access Control with Roles: Fine-grained Access Control at different levels - ⚡ &nbsp;Access Control with Roles: Fine-grained Access Control at different levels
- ⚡ &nbsp;and more ... - ⚡ &nbsp;and more ...
@ -174,26 +153,6 @@ We provide the following ways to let users programmatically invoke actions. You
- ⚡ &nbsp;REST APIs - ⚡ &nbsp;REST APIs
- ⚡ &nbsp;NocoDB SDK - ⚡ &nbsp;NocoDB SDK
### Sync Schema
We allow you to sync schema changes if you have made changes outside NocoDB GUI. However, it has to be noted then you will have to bring your own schema migrations for moving from one environment to another. See <a href="https://docs.nocodb.com/data-sources/sync-with-data-source" target="_blank">Sync Schema</a> for details.
### Audit
We are keeping all the user operation logs in one place. See <a href="https://docs.nocodb.com/data-sources/actions-on-data-sources/#audit-logs" target="_blank">Audit</a> for details.
# Production Setup
By default, SQLite is used for storing metadata. However, you can specify your database. The connection parameters for this database can be specified in `NC_DB` environment variable. Moreover, we also provide the below environment variables for configuration.
## Environment variables
Please refer to the [Environment variables](https://docs.nocodb.com/getting-started/self-hosted/environment-variables)
# Development Setup
Please refer to [Development Setup](https://docs.nocodb.com/engineering/development-setup)
# Contributing # Contributing
Please refer to [Contribution Guide](https://github.com/nocodb/nocodb/blob/master/.github/CONTRIBUTING.md). Please refer to [Contribution Guide](https://github.com/nocodb/nocodb/blob/master/.github/CONTRIBUTING.md).
@ -212,6 +171,8 @@ Our mission is to provide the most powerful no-code interface for databases that
This project is licensed under <a href="./LICENSE">AGPLv3</a>. This project is licensed under <a href="./LICENSE">AGPLv3</a>.
</p> </p>
# Contributors # Contributors
Thank you for your contributions! We appreciate all the contributions from the community. Thank you for your contributions! We appreciate all the contributions from the community.

2
charts/nocodb/values.yaml

@ -79,7 +79,7 @@ tolerations: []
affinity: {} affinity: {}
extraEnvs: extraEnvs:
NC_PUBLIC_URL: https:/nocodb.local.org NC_PUBLIC_URL: https://nocodb.local.org
extraSecretEnvs: extraSecretEnvs:
NC_AUTH_JWT_SECRET: secretString NC_AUTH_JWT_SECRET: secretString

15
docker-compose/1_Auto_Upstall/README.md

@ -0,0 +1,15 @@
# NocoDB : Auto-upstall script
### Usage
```bash
./noco.sh
````
### Notes
This simple command : 🕊
- 🐳 Automatically installs all pre-requisites like docker, docker-compose
- 🚀 Automatically installs NocoDB with PostgreSQL, Redis, Minio, Traefik gateway using Docker Compose. 🐘 🗄 🌐
- 🔄 Automatically upgrades NocoDB to the latest version when you run the command again.
- 🔒 Automatically setups SSL and also renews it. Needs a domain or subdomain as input while installation.

1018
docker-compose/1_Auto_Upstall/noco.sh

File diff suppressed because it is too large Load Diff

0
docker-compose/setup-script/tests/configure/monitor.bats → docker-compose/1_Auto_Upstall/tests/configure/monitor.bats

0
docker-compose/setup-script/tests/configure/restart.bats → docker-compose/1_Auto_Upstall/tests/configure/restart.bats

0
docker-compose/setup-script/tests/configure/scale.bats → docker-compose/1_Auto_Upstall/tests/configure/scale.bats

0
docker-compose/setup-script/tests/configure/setup.sh → docker-compose/1_Auto_Upstall/tests/configure/setup.sh

0
docker-compose/setup-script/tests/configure/start.bats → docker-compose/1_Auto_Upstall/tests/configure/start.bats

0
docker-compose/setup-script/tests/configure/stop.bats → docker-compose/1_Auto_Upstall/tests/configure/stop.bats

0
docker-compose/setup-script/tests/configure/upgrade.bats → docker-compose/1_Auto_Upstall/tests/configure/upgrade.bats

0
docker-compose/setup-script/tests/expects/configure/monitor.sh → docker-compose/1_Auto_Upstall/tests/expects/configure/monitor.sh

0
docker-compose/setup-script/tests/expects/configure/restart.sh → docker-compose/1_Auto_Upstall/tests/expects/configure/restart.sh

0
docker-compose/setup-script/tests/expects/configure/scale.sh → docker-compose/1_Auto_Upstall/tests/expects/configure/scale.sh

0
docker-compose/setup-script/tests/expects/configure/start.sh → docker-compose/1_Auto_Upstall/tests/expects/configure/start.sh

0
docker-compose/setup-script/tests/expects/configure/stop.sh → docker-compose/1_Auto_Upstall/tests/expects/configure/stop.sh

0
docker-compose/setup-script/tests/expects/configure/upgrade.sh → docker-compose/1_Auto_Upstall/tests/expects/configure/upgrade.sh

0
docker-compose/setup-script/tests/expects/install/default.sh → docker-compose/1_Auto_Upstall/tests/expects/install/default.sh

0
docker-compose/setup-script/tests/expects/install/ip.sh → docker-compose/1_Auto_Upstall/tests/expects/install/ip.sh

0
docker-compose/setup-script/tests/expects/install/redis.sh → docker-compose/1_Auto_Upstall/tests/expects/install/redis.sh

0
docker-compose/setup-script/tests/expects/install/scale.sh → docker-compose/1_Auto_Upstall/tests/expects/install/scale.sh

0
docker-compose/setup-script/tests/expects/install/ssl.sh → docker-compose/1_Auto_Upstall/tests/expects/install/ssl.sh

0
docker-compose/setup-script/tests/expects/install/watchtower.sh → docker-compose/1_Auto_Upstall/tests/expects/install/watchtower.sh

0
docker-compose/setup-script/tests/install/default.bats → docker-compose/1_Auto_Upstall/tests/install/default.bats

0
docker-compose/setup-script/tests/install/ip.bats → docker-compose/1_Auto_Upstall/tests/install/ip.bats

0
docker-compose/setup-script/tests/install/redis.bats → docker-compose/1_Auto_Upstall/tests/install/redis.bats

0
docker-compose/setup-script/tests/install/scale.bats → docker-compose/1_Auto_Upstall/tests/install/scale.bats

0
docker-compose/setup-script/tests/install/setup.sh → docker-compose/1_Auto_Upstall/tests/install/setup.sh

0
docker-compose/setup-script/tests/install/ssl.bats → docker-compose/1_Auto_Upstall/tests/install/ssl.bats

0
docker-compose/setup-script/tests/install/watchtower.bats → docker-compose/1_Auto_Upstall/tests/install/watchtower.bats

0
docker-compose/setup-script/tests/mocks/clear → docker-compose/1_Auto_Upstall/tests/mocks/clear

0
docker-compose/setup-script/tests/mocks/nproc → docker-compose/1_Auto_Upstall/tests/mocks/nproc

0
docker-compose/pg/docker-compose.yml → docker-compose/2_pg/docker-compose.yml

0
docker-compose/traefik/.env → docker-compose/3_traefik/.env

0
docker-compose/traefik/README.md → docker-compose/3_traefik/README.md

0
docker-compose/traefik/docker-compose.yml → docker-compose/3_traefik/docker-compose.yml

60
docker-compose/letsencrypt/nc.sh

@ -1,60 +0,0 @@
#!/usr/bin/env bash
read -p "Enter your domain name: " domain
read -p "Enter your email id: " email
# Docker installation
if [ -x "$(command -v docker)" ]; then
echo "Docker already available"
else
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg2 software-properties-common
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add --
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian buster stable"
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
sudo usermod -a -G docker $USER
echo "Docker installed successfully"
fi
# Docker compose installation
if [ -x "$(command -v docker-compose)" ]; then
echo "Docker-compose already available"
else
sudo apt-get -y install wget
sudo wget https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m) -O /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
echo "Docker-compose installed successfully"
fi
#wget https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion/archive/master.zip -O master.zip
#
#unzip -n master.zip
#
#cd docker-compose-letsencrypt-nginx-proxy-companion-master
git clone https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion.git
cd docker-compose-letsencrypt-nginx-proxy-companion
OUTPUT1=$(./start.sh)
docker run -p 8080:8080 -p 8081:8081 -p 8082:8082 -d --name xc-instant \
-e VIRTUAL_HOST="$domain" \
-e LETSENCRYPT_HOST="$domain" \
-e LETSENCRYPT_EMAIL="$email" \
-e VIRTUAL_PORT=8080 \
--network=webproxy nocodb/nocodb:latest

39
docker-compose/mysql/docker-compose.yml

@ -1,39 +0,0 @@
version: "2.1"
services:
nocodb:
depends_on:
root_db:
condition: service_healthy
environment:
NC_DB: "mysql2://root_db:3306?u=noco&p=password&d=root_db"
image: "nocodb/nocodb:latest"
ports:
- "8080:8080"
restart: always
volumes:
- "nc_data:/usr/app/data"
root_db:
environment:
MYSQL_DATABASE: root_db
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
MYSQL_USER: noco
healthcheck:
retries: 10
test:
- CMD
- mysqladmin
- ping
- "-h"
- localhost
timeout: 20s
image: "mysql:8.3.0"
restart: always
volumes:
- "db_data:/var/lib/mysql"
# below line shows how to change charset and collation
# uncomment it if necessary
# command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
volumes:
db_data: {}
nc_data: {}

948
docker-compose/setup-script/noco.sh

@ -1,948 +0,0 @@
#!/bin/bash
set -e
# Constants
NOCO_HOME="./nocodb"
CURRENT_PATH=$(pwd)
REQUIRED_PORTS=(80 443)
# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
ORANGE='\033[0;33m'
BOLD='\033[1m'
NC='\033[0m'
# Global variables
CONFIG_DOMAIN_NAME=""
CONFIG_SSL_ENABLED=""
CONFIG_EDITION=""
CONFIG_LICENSE_KEY=""
CONFIG_REDIS_ENABLED=""
CONFIG_MINIO_ENABLED=""
CONFIG_MINIO_DOMAIN_NAME=""
CONFIG_MINIO_SSL_ENABLED=""
CONFIG_WATCHTOWER_ENABLED=""
CONFIG_NUM_INSTANCES=""
CONFIG_POSTGRES_PASSWORD=""
CONFIG_REDIS_PASSWORD=""
CONFIG_MINIO_ACCESS_KEY=""
CONFIG_MINIO_ACCESS_SECRET=""
CONFIG_DOCKER_COMMAND=""
declare -a message_arr
# Utility functions
print_color() { printf "${1}%s${NC}\n" "$2"; }
print_info() { print_color "$BLUE" "INFO: $1"; }
print_success() { print_color "$GREEN" "SUCCESS: $1"; }
print_warning() { print_color "$YELLOW" "WARNING: $1"; }
print_error() { print_color "$RED" "ERROR: $1"; }
print_box_message() {
local message=("$@")
local edge="======================================"
local padding=" "
echo "$edge"
for element in "${message[@]}"; do
echo "${padding}${element}"
done
echo "$edge"
}
print_note() {
local note_text="$1"
local note_color='\033[0;33m' # Yellow color
local bold='\033[1m'
local reset='\033[0m'
echo -e "${note_color}${bold}NOTE:${reset} ${note_text}"
}
command_exists() { command -v "$1" >/dev/null 2>&1; }
is_valid_domain() {
local domain_regex="^([a-zA-Z0-9]([-a-zA-Z0-9]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([-a-zA-Z0-9]{0,61}[a-zA-Z0-9])?\.[a-zA-Z]{2,}$"
[[ "$1" =~ $domain_regex ]]
}
urlencode() {
local string="$1"
local strlen=${#string}
local encoded=""
local pos c o
for (( pos=0 ; pos<strlen ; pos++ )); do
c=${string:$pos:1}
case "$c" in
[-_.~a-zA-Z0-9] ) o="$c" ;;
* ) printf -v o '%%%02X' "'$c"
esac
encoded+="$o"
done
echo "$encoded"
}
generate_password() {
openssl rand -base64 48 | tr -dc 'a-zA-Z0-9!@#$%^&*()-_+=' | head -c 32
}
get_public_ip() {
local ip
if command -v dig >/dev/null 2>&1; then
ip=$(dig +short myip.opendns.com @resolver1.opendns.com 2>/dev/null)
if [ -n "$ip" ]; then
echo "$ip"
return
fi
fi
# Method 2: Using curl
if command -v curl >/dev/null 2>&1; then
ip=$(curl -s -4 https://ifconfig.co 2>/dev/null)
if [ -n "$ip" ]; then
echo "$ip"
return
fi
fi
# Method 3: Using wget
if command -v wget >/dev/null 2>&1; then
ip=$(wget -qO- https://ifconfig.me 2>/dev/null)
if [ -n "$ip" ]; then
echo "$ip"
return
fi
fi
# Method 4: Using host
if command -v host >/dev/null 2>&1; then
ip=$(host myip.opendns.com resolver1.opendns.com 2>/dev/null | grep "myip.opendns.com has" | awk '{print $4}')
if [ -n "$ip" ]; then
echo "$ip"
return
fi
fi
# If all methods fail, return localhost
echo "localhost"
}
get_nproc() {
# Try to get the number of processors using nproc
if command -v nproc &> /dev/null; then
nproc
else
# Fallback: Check if /proc/cpuinfo exists and count the number of processors
if [[ -f /proc/cpuinfo ]]; then
grep -c ^processor /proc/cpuinfo
# Fallback for macOS or BSD systems using sysctl
elif command -v sysctl &> /dev/null; then
sysctl -n hw.ncpu
# Default to 1 processor if everything else fails
else
echo 1
fi
fi
}
prompt() {
local prompt_text="$1"
local default_value="$2"
local response
if [ -n "$default_value" ]; then
prompt_text+=" (default: $default_value)"
fi
prompt_text+=": "
read -r -p "$prompt_text" response
if [ -z "$response" ] && [ -n "$default_value" ]; then
echo "$default_value"
else
echo "$response"
fi
}
prompt_required() {
local prompt_text="$1"
local response
while true; do
read -r -p "$prompt_text: " response
if [ -n "$response" ]; then
echo "$response"
return
fi
print_error "This field is required."
done
}
prompt_number() {
local prompt_text="$1"
local min="$2"
local max="$3"
local response
while true; do
read -r -p "$prompt_text ($min-$max): " response
if [[ "$response" =~ ^[0-9]+$ ]] && [ "$response" -ge "$min" ] && [ "$response" -le "$max" ]; then
echo "$response"
return
fi
print_error "Please enter a number between $min and $max."
done
}
confirm() {
local prompt_text="$1"
local default_response="${2:-N}"
local response
if [ "$default_response" = "Y" ] || [ "$default_response" = "y" ]; then
prompt_text+=" [Y/n]: "
else
prompt_text+=" [y/N]: "
fi
read -r -p "$prompt_text" response
response="${response:-$default_response}"
if [ "$response" = "Y" ] || [ "$response" = "y" ]; then
return 0
else
return 1
fi
}
generate_contact_email() {
local domain="$1"
local email
if [ -z "$domain" ] || [ "$domain" = "localhost" ] || [[ "$domain" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
email="contact@example.com"
else
domain="${domain#http://}"
domain="${domain#https://}"
domain="${domain%%/*}"
domain="${domain%%\?*}"
if [[ "$domain" =~ [^.]+\.[^.]+$ ]]; then
main_domain="${BASH_REMATCH[0]}"
else
main_domain="$domain"
fi
email="contact@$main_domain"
fi
echo "$email"
}
install_package() {
if command_exists yum; then
sudo yum install -y "$1"
elif command_exists apt; then
sudo apt install -y "$1"
elif command_exists brew; then
brew install "$1"
else
print_error "Package manager not found. Please install $1 manually."
exit 1
fi
}
add_to_hosts() {
local IP="127.0.0.1"
local HOSTS_FILE="/etc/hosts"
local TEMP_HOSTS_FILE="/tmp/hosts.tmp"
if is_valid_domain $CONFIG_MINIO_DOMAIN_NAME; then
return 0
elif sudo grep -q "${CONFIG_MINIO_DOMAIN_NAME}" "$HOSTS_FILE"; then
return 0
else
sudo cp "$HOSTS_FILE" "$TEMP_HOSTS_FILE"
echo "$IP ${CONFIG_MINIO_DOMAIN_NAME}" | sudo tee -a "$TEMP_HOSTS_FILE" > /dev/null
if sudo mv "$TEMP_HOSTS_FILE" "$HOSTS_FILE"; then
print_info "Added ${CONFIG_MINIO_DOMAIN_NAME} to $HOSTS_FILE"
print_note "You may need to reboot your system, If the uploaded attachments are not accessible."
else
print_error "Failed to update $HOSTS_FILE. Please check your permissions."
return 1
fi
fi
}
check_for_docker_sudo() {
if docker ps >/dev/null 2>&1; then
echo "n"
else
echo "y"
fi
}
read_number() {
local prompt="$1"
local default="$2"
local number
while true; do
if [ -n "$default" ]; then
read -rp "$prompt [$default]: " number
number=${number:-$default}
else
read -rp "$prompt: " number
fi
if [ -z "$number" ]; then
echo "Input cannot be empty. Please enter a number."
elif ! [[ $number =~ ^[0-9]+$ ]]; then
echo "Invalid input. Please enter a valid number."
else
echo "$number"
return
fi
done
}
read_number_range() {
local prompt="$1"
local min="$2"
local max="$3"
local default="$4"
local number
while true; do
if [ -n "$default" ]; then
number=$(read_number "$prompt ($min-$max)" "$default")
else
number=$(read_number "$prompt ($min-$max)")
fi
if [ -z "$number" ]; then
continue
elif [ "$number" -lt "$min" ] || [ "$number" -gt "$max" ]; then
echo "Please enter a number between $min and $max."
else
echo "$number"
return
fi
done
}
check_if_docker_is_running() {
if ! $CONFIG_DOCKER_COMMAND ps >/dev/null 2>&1; then
print_warning "Docker is not running. Most of the commands will not work without Docker."
print_info "Use the following command to start Docker:"
print_color "$BLUE" " sudo systemctl start docker"
fi
}
# Main functions
check_existing_installation() {
NOCO_FOUND=false
# Check if $NOCO_HOME exists as directory
if [ -d "$NOCO_HOME" ]; then
NOCO_FOUND=true
elif $CONFIG_DOCKER_COMMAND ps --format '{{.Names}}' | grep -q "nocodb"; then
NOCO_ID=$($CONFIG_DOCKER_COMMAND ps | grep "nocodb/nocodb" | cut -d ' ' -f 1)
CUSTOM_HOME=$($CONFIG_DOCKER_COMMAND inspect --format='{{index .Mounts 0}}' "$NOCO_ID" | cut -d ' ' -f 3)
PARENT_DIR=$(dirname "$CUSTOM_HOME")
ln -s "$PARENT_DIR" "$NOCO_HOME"
basename "$PARENT_DIR" > "$NOCO_HOME/.COMPOSE_PROJECT_NAME"
NOCO_FOUND=true
else
mkdir -p "$NOCO_HOME"
fi
cd "$NOCO_HOME" || exit 1
# Check if nocodb is already installed
if [ "$NOCO_FOUND" = true ]; then
echo "NocoDB is already installed. And running."
echo "Do you want to reinstall NocoDB? [Y/N] (default: N): "
read -r REINSTALL
if [ -f "$NOCO_HOME/.COMPOSE_PROJECT_NAME" ]; then
COMPOSE_PROJECT_NAME=$(cat "$NOCO_HOME/.COMPOSE_PROJECT_NAME")
export COMPOSE_PROJECT_NAME
fi
if [ "$REINSTALL" != "Y" ] && [ "$REINSTALL" != "y" ]; then
management_menu
exit 0
else
echo "Reinstalling NocoDB..."
$CONFIG_DOCKER_COMMAND compose down
unset COMPOSE_PROJECT_NAME
cd /tmp || exit 1
rm -rf "$NOCO_HOME"
cd "$CURRENT_PATH" || exit 1
mkdir -p "$NOCO_HOME"
cd "$NOCO_HOME" || exit 1
fi
fi
}
check_system_requirements() {
print_info "Performing NocoDB system check and setup"
for tool in docker wget lsof openssl; do
if ! command_exists "$tool"; then
print_warning "$tool is not installed. Setting up for installation..."
if [ "$tool" = "docker" ]; then
wget -qO- https://get.docker.com/ | sh
else
install_package "$tool"
fi
fi
done
for port in "${REQUIRED_PORTS[@]}"; do
if lsof -Pi :"$port" -sTCP:LISTEN -t >/dev/null; then
print_warning "Port $port is in use. Please make sure it is free."
else
print_info "Port $port is free."
fi
done
print_success "System check completed successfully"
}
get_user_inputs() {
CONFIG_DOMAIN_NAME=$(prompt "Enter the IP address or domain name for the NocoDB instance" "$(get_public_ip)")
if is_valid_domain "$CONFIG_DOMAIN_NAME"; then
if confirm "Do you want to configure SSL for $CONFIG_DOMAIN_NAME?"; then
CONFIG_SSL_ENABLED="Y"
else
CONFIG_SSL_ENABLED="N"
fi
else
CONFIG_SSL_ENABLED="N"
fi
if confirm "Show Advanced Options?"; then
get_advanced_options
else
set_default_options
fi
}
get_advanced_options() {
CONFIG_EDITION=$(prompt "Choose Community or Enterprise Edition [CE/EE]" "CE")
if [ "$CONFIG_EDITION" = "EE" ] || [ "$CONFIG_EDITION" = "ee" ]; then
CONFIG_LICENSE_KEY=$(prompt_required "Enter the NocoDB license key")
fi
CONFIG_REDIS_ENABLED=$(confirm "Do you want to enable Redis for caching?" "Y" && echo "Y" || echo "N" "Y")
CONFIG_MINIO_ENABLED=$(confirm "Do you want to enable Minio for file storage?" "Y" && echo "Y" || echo "N" "Y")
if [ "$CONFIG_MINIO_ENABLED" = "Y" ] || [ "$CONFIG_MINIO_ENABLED" = "y" ]; then
CONFIG_MINIO_DOMAIN_NAME=$(prompt "Enter the MinIO domain name" "$(get_public_ip)")
if is_valid_domain "$CONFIG_MINIO_DOMAIN_NAME"; then
if confirm "Do you want to configure SSL for $CONFIG_MINIO_DOMAIN_NAME?"; then
CONFIG_MINIO_SSL_ENABLED="Y"
else
CONFIG_MINIO_SSL_ENABLED="N"
fi
else
CONFIG_MINIO_SSL_ENABLED="N"
fi
fi
CONFIG_WATCHTOWER_ENABLED=$(confirm "Do you want to enable Watchtower for automatic updates?" "Y" && echo "Y" || echo "N")
NUM_CORES=$(get_nproc)
CONFIG_NUM_INSTANCES=$(read_number_range "How many instances of NocoDB do you want to run?" 1 "$NUM_CORES" 1)
}
set_default_options() {
CONFIG_SSL_ENABLED="N"
CONFIG_EDITION="CE"
CONFIG_REDIS_ENABLED="Y"
CONFIG_MINIO_ENABLED="Y"
CONFIG_MINIO_DOMAIN_NAME=$(get_public_ip)
CONFIG_MINIO_SSL_ENABLED="N"
CONFIG_WATCHTOWER_ENABLED="Y"
CONFIG_NUM_INSTANCES=1
}
generate_credentials() {
CONFIG_POSTGRES_PASSWORD=$(generate_password)
CONFIG_REDIS_PASSWORD=$(generate_password)
CONFIG_MINIO_ACCESS_KEY=$(generate_password)
CONFIG_MINIO_ACCESS_SECRET=$(generate_password)
}
create_docker_compose_file() {
image="nocodb/nocodb:latest"
if [ "${CONFIG_EDITION}" = "EE" ] || [ "${CONFIG_EDITION}" = "ee" ]; then
image="nocodb/ee:latest"
fi
local compose_file="docker-compose.yml"
cat > "$compose_file" <<EOF
services:
nocodb:
image: ${image}
env_file: docker.env
deploy:
mode: replicated
replicas: ${CONFIG_NUM_INSTANCES}
depends_on:
- db
${CONFIG_REDIS_ENABLED:+- redis}
${CONFIG_MINIO_ENABLED:+- minio}
restart: unless-stopped
volumes:
- ./nocodb:/usr/app/data
labels:
- "com.centurylinklabs.watchtower.enable=true"
- "traefik.enable=true"
- "traefik.http.routers.nocodb.rule=Host(\`${CONFIG_DOMAIN_NAME}\`)"
EOF
# IF SSL is Enabled add the following lines
if [ "$CONFIG_SSL_ENABLED" = "Y" ]; then
cat >> "$compose_file" <<EOF
- "traefik.http.routers.nocodb.entrypoints=websecure"
- "traefik.http.routers.nocodb.tls=true"
- "traefik.http.routers.nocodb.tls.certresolver=letsencrypt"
EOF
# If no ssl just configure the web entrypoint
else
cat >> "$compose_file" <<EOF
- "traefik.http.routers.nocodb.entrypoints=web"
EOF
fi
# Continue with the compose file
cat >> "$compose_file" <<EOF
networks:
- nocodb-network
db:
image: postgres:16.1
env_file: docker.env
volumes:
- ./postgres:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- nocodb-network
traefik:
image: traefik:v3.1
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--entrypoints.web.address=:80"
- "--providers.docker.exposedByDefault=false"
EOF
# In Traefik we need to add the minio entrypoint if it is enabled
if [ "$CONFIG_MINIO_ENABLED" = "Y" ]; then
cat >> "$compose_file" <<EOF
- "--entrypoints.minio.address=:9000"
EOF
fi
# If SSL is enabled we need to add the following lines to the traefik service
if [ "$CONFIG_SSL_ENABLED" = "Y" ] || [ "$CONFIG_MINIO_SSL_ENABLED" = "Y" ]; then
cat >> "$compose_file" <<EOF
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.letsencrypt.acme.email=$(generate_contact_email $CONFIG_DOMAIN_NAME)"
- "--certificatesresolvers.letsencrypt.acme.storage=/etc/letsencrypt/acme.json"
EOF
fi
# Continue with the compose file
cat >> "$compose_file" <<EOF
ports:
- "80:80"
- "443:443"
- "9000:9000"
depends_on:
- nocodb
restart: unless-stopped
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- ./letsencrypt:/etc/letsencrypt
networks:
- nocodb-network
EOF
# If Redis is enabled add the following lines to the compose file
if [ "${CONFIG_REDIS_ENABLED}" = "Y" ]; then
cat >> "$compose_file" <<EOF
redis:
image: redis:latest
restart: unless-stopped
env_file: docker.env
command:
- /bin/sh
- -c
- redis-server --requirepass "\$\${REDIS_PASSWORD}"
healthcheck:
test: [ "CMD", "redis-cli", "-a", "\$\${REDIS_PASSWORD}", "--raw", "incr", "ping" ]
volumes:
- ./redis:/data
networks:
- nocodb-network
EOF
fi
# IF Minio is enabled add the following lines to the compose file
if [ "${CONFIG_MINIO_ENABLED}" = "Y" ]; then
cat >> "$compose_file" <<EOF
minio:
image: minio/minio:latest
restart: unless-stopped
env_file: docker.env
entrypoint: /bin/sh
volumes:
- ./minio:/export
command: -c 'mkdir -p /export/nocodb && /usr/bin/minio server /export'
labels:
- "traefik.enable=true"
- "traefik.http.services.minio.loadbalancer.server.port=9000"
- "traefik.http.routers.minio.rule=Host(\`${CONFIG_MINIO_DOMAIN_NAME}\`)"
EOF
# If minio SSL is enabled, set the entry point to websecure
fi
if [ "$CONFIG_MINIO_SSL_ENABLED" = "Y" ]; then
cat >> "$compose_file" <<EOF
- "traefik.http.routers.minio.entrypoints=websecure"
- "traefik.http.routers.minio.tls=true"
- "traefik.http.routers.minio.tls.certresolver=letsencrypt"
EOF
# If minio is enabled and the domain is valid, set the entry point to web
elif is_valid_domain "$CONFIG_MINIO_DOMAIN_NAME"; then
cat >> "$compose_file" <<EOF
- "traefik.http.routers.minio.entrypoints=web"
EOF
# If minio is enabled, valid domain name is not configured, set the entry point to Port 9000
else
cat >> "$compose_file" <<EOF
- "traefik.http.routers.minio.entrypoints=minio"
EOF
fi
cat >> "$compose_file" <<EOF
networks:
- nocodb-network
EOF
if [ "${CONFIG_WATCHTOWER_ENABLED}" = "Y" ]; then
cat >> "$compose_file" <<EOF
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --schedule "0 2 * * 6" --cleanup
restart: unless-stopped
networks:
- nocodb-network
EOF
fi
cat >> "$compose_file" <<EOF
volumes:
${CONFIG_REDIS_ENABLED:+redis:}
networks:
nocodb-network:
driver: bridge
EOF
}
create_env_file() {
local env_file="docker.env"
local encoded_password
encoded_password=$(urlencode "${CONFIG_POSTGRES_PASSWORD}")
ENCODED_REDIS_PASSWORD=$(urlencode "$CONFIG_REDIS_PASSWORD")
cat > "$env_file" <<EOF
POSTGRES_DB=nocodb
POSTGRES_USER=postgres
POSTGRES_PASSWORD=${CONFIG_POSTGRES_PASSWORD}
EOF
if [ "${CONFIG_EDITION}" = "EE" ] || [ "${CONFIG_EDITION}" = "ee" ]; then
echo "DATABASE_URL=postgres://postgres:${encoded_password}@db:5432/nocodb" >> "$env_file"
echo "NC_LICENSE_KEY=${CONFIG_LICENSE_KEY}" >> "$env_file"
else
echo "NC_DB=pg://db:5432?d=nocodb&user=postgres&password=${encoded_password}" >> "$env_file"
fi
if [ "${CONFIG_REDIS_ENABLED}" = "Y" ]; then
cat >> "$env_file" <<EOF
REDIS_PASSWORD=${ENCODED_REDIS_PASSWORD}
NC_REDIS_URL=redis://:${ENCODED_REDIS_PASSWORD}@redis:6379/0
EOF
fi
if [ "${CONFIG_MINIO_ENABLED}" = "Y" ]; then
cat >> "$env_file" <<EOF
MINIO_ROOT_USER=${CONFIG_MINIO_ACCESS_KEY}
MINIO_ROOT_PASSWORD=${CONFIG_MINIO_ACCESS_SECRET}
NC_S3_BUCKET_NAME=nocodb
NC_S3_REGION=us-east-1
NC_S3_ACCESS_KEY=${CONFIG_MINIO_ACCESS_KEY}
NC_S3_ACCESS_SECRET=${CONFIG_MINIO_ACCESS_SECRET}
NC_S3_FORCE_PATH_STYLE=true
EOF
if [ "$CONFIG_MINIO_SSL_ENABLED" = "Y" ]; then
echo "NC_S3_ENDPOINT=https://${CONFIG_MINIO_DOMAIN_NAME}" >> "$env_file"
elif is_valid_domain "$CONFIG_MINIO_DOMAIN_NAME"; then
echo "NC_S3_ENDPOINT=http://${CONFIG_MINIO_DOMAIN_NAME}" >> "$env_file"
else
echo "NC_S3_ENDPOINT=http://${CONFIG_MINIO_DOMAIN_NAME}:9000" >> "$env_file"
fi
fi
}
create_update_script() {
cat > ./update.sh <<EOF
#!/bin/bash
$CONFIG_DOCKER_COMMAND compose pull
$CONFIG_DOCKER_COMMAND compose up -d --force-recreate
$CONFIG_DOCKER_COMMAND image prune -a -f
EOF
chmod +x ./update.sh
message_arr+=("Update script: update.sh")
}
start_services() {
$CONFIG_DOCKER_COMMAND compose pull
$CONFIG_DOCKER_COMMAND compose up -d
echo 'Waiting for Traefik to start...'
sleep 5
}
display_completion_message() {
if [ -n "${CONFIG_DOMAIN_NAME}" ]; then
if [ "${CONFIG_SSL_ENABLED}" = "Y" ]; then
message_arr+=("NocoDB is now available at https://${CONFIG_DOMAIN_NAME}")
else
message_arr+=("NocoDB is now available at http://${CONFIG_DOMAIN_NAME}")
fi
else
message_arr+=("NocoDB is now available at http://localhost")
fi
print_box_message "${message_arr[@]}"
}
management_menu() {
while true; do
trap - INT
show_menu
echo "Enter your choice: "
read -r choice
case $choice in
1) start_service && MSG="NocoDB Started" ;;
2) stop_service && MSG="NocoDB Stopped" ;;
3) show_logs ;;
4) restart_service && MSG="NocoDB Restarted" ;;
5) upgrade_service && MSG="NocoDB has been upgraded to latest version" ;;
6) scale_service && MSG="NocoDB has been scaled" ;;
7) monitoring_service ;;
0) exit 0 ;;
*) MSG="\nInvalid choice. Please select a correct option." ;;
esac
done
}
show_menu() {
clear
check_if_docker_is_running
echo ""
echo "$MSG"
echo -e "\t\t${BOLD}Service Management Menu${NC}"
echo -e " ${GREEN}1. Start Service"
echo -e " ${ORANGE}2. Stop Service"
echo -e " ${CYAN}3. Logs"
echo -e " ${MAGENTA}4. Restart"
echo -e " ${BLUE}5. Upgrade"
echo -e " 6. Scale"
echo -e " 7. Monitoring"
echo -e " ${RED}0. Exit${NC}"
}
start_service() {
echo -e "\nStarting nocodb..."
$CONFIG_DOCKER_COMMAND compose up -d
}
stop_service() {
echo -e "\nStopping nocodb..."
$CONFIG_DOCKER_COMMAND compose stop
}
show_logs_sub_menu() {
clear
echo "Select a replica for $1:"
for i in $(seq 1 $2); do
echo "$i. \"$1\" replica $i"
done
echo "A. All"
echo "0. Back to Logs Menu"
echo "Enter replica number: "
read -r replica_choice
if [[ "$replica_choice" =~ ^[0-9]+$ ]] && [ "$replica_choice" -gt 0 ] && [ "$replica_choice" -le "$2" ]; then
container_id=$($CONFIG_DOCKER_COMMAND compose ps | grep "$1-$replica_choice" | cut -d " " -f 1)
$CONFIG_DOCKER_COMMAND logs -f "$container_id"
elif [ "$replica_choice" == "A" ] || [ "$replica_choice" == "a" ]; then
$CONFIG_DOCKER_COMMAND compose logs -f "$1"
elif [ "$replica_choice" == "0" ]; then
show_logs
else
show_logs_sub_menu "$1" "$2"
fi
}
# Function to show logs
show_logs() {
clear
echo "Select a container for logs:"
# Fetch the list of services
services=()
while IFS= read -r service; do
services+=("$service")
done < <($CONFIG_DOCKER_COMMAND compose ps --services)
service_replicas=()
count=0
# For each service, count the number of running instances
for service in "${services[@]}"; do
# Count the number of lines that have the service name, which corresponds to the number of replicas
replicas=$($CONFIG_DOCKER_COMMAND compose ps "$service" | grep -c "$service")
service_replicas["$count"]=$replicas
count=$((count + 1))
done
count=1
for service in "${services[@]}"; do
echo "$count. $service (${service_replicas[(($count - 1))]} replicas)"
count=$((count + 1))
done
echo "A. All"
echo "0. Back to main menu"
echo "Enter your choice: "
read -r log_choice
echo
if [[ "$log_choice" =~ ^[0-9]+$ ]] && [ "$log_choice" -gt 0 ] && [ "$log_choice" -lt "$count" ]; then
service_index=$((log_choice-1))
service="${services[$service_index]}"
num_replicas="${service_replicas[$service_index]}"
if [ "$num_replicas" -gt 1 ]; then
trap 'show_logs_sub_menu "$service" "$num_replicas"' INT
show_logs_sub_menu "$service" "$num_replicas"
trap - INT
else
trap 'show_logs' INT
$CONFIG_DOCKER_COMMAND compose logs -f "$service"
fi
elif [ "$log_choice" == "A" ] || [ "$log_choice" == "a" ]; then
trap 'show_logs' INT
$CONFIG_DOCKER_COMMAND compose logs -f
elif [ "$log_choice" == "0" ]; then
return
else
show_logs
fi
trap - INT
}
restart_service() {
echo -e "\nRestarting nocodb..."
$CONFIG_DOCKER_COMMAND compose restart
}
upgrade_service() {
echo -e "\nUpgrading nocodb..."
$CONFIG_DOCKER_COMMAND compose pull
$CONFIG_DOCKER_COMMAND compose up -d --force-recreate
$CONFIG_DOCKER_COMMAND image prune -a -f
}
scale_service() {
num_cores=$(get_nproc)
current_scale=$($CONFIG_DOCKER_COMMAND compose ps -q nocodb | wc -l)
echo -e "\nCurrent number of instances: $current_scale"
scale_num=$(read_number_range "How many instances of NocoDB do you want to run?" 1 "$num_cores" 1)
if [ "$scale_num" -eq "$current_scale" ]; then
echo "Number of instances is already set to $scale_num. Returning to main menu."
return
fi
$CONFIG_DOCKER_COMMAND compose up -d --scale nocodb="$scale_num"
}
monitoring_service() {
echo -e '\nLoading stats...'
trap ' ' INT
$CONFIG_DOCKER_COMMAND stats
}
main() {
CONFIG_DOCKER_COMMAND=$([ "$(check_for_docker_sudo)" = "y" ] && echo "sudo docker" || echo "docker")
check_existing_installation
check_system_requirements
get_user_inputs
generate_credentials
create_docker_compose_file
add_to_hosts
create_env_file
create_update_script
start_services
display_completion_message
if confirm "Do you want to start the management menu?"; then
management_menu
fi
}
main "$@"

BIN
docker-compose/sqlite/nocodb/noco.db

Binary file not shown.

2
markdown/readme/languages/chinese.md

@ -77,7 +77,7 @@ nocodb/nocodb:latest
```bash ```bash
git clone https://github.com/nocodb/nocodb git clone https://github.com/nocodb/nocodb
# 如果使用 PostgreSQL 的话 # 如果使用 PostgreSQL 的话
cd nocodb/docker-compose/pg cd nocodb/docker-compose/2_pg
``` ```
> 你可以通过在 0.10.6 以上的版本中挂载 `/usr/app/data/` 来持久化数据,否则你的数据会在重新创建容器后完全丢失。 > 你可以通过在 0.10.6 以上的版本中挂载 `/usr/app/data/` 来持久化数据,否则你的数据会在重新创建容器后完全丢失。

12
markdown/readme/languages/french.md

@ -108,9 +108,9 @@ Accès au tableau de bord en utilisant : [http://localhost:8080/dashboard](http:
# Caractéristiques # Caractéristiques
### Interface de feuille de calcul riche ### Interface de feuille de calcul riche
- ⚡ Recherche, trier, filtrer, masquer les colonnes avec Uber Facile - ⚡ Recherche, trier, filtrer, masquer les colonnes avec facilité
- ⚡ Créer des vues: grille, galerie, kanban, forme - ⚡ Créer des vues: grille, galerie, kanban, forme
- ⚡ Partager des vues: Public & Mot de passe protégé - ⚡ Partager des vues: Publique ou Protégé par mot de passe
- ⚡ Vues personnelles et verrouillées - ⚡ Vues personnelles et verrouillées
- ⚡ Télécharger des images sur les cellules (fonctionne avec S3, Minio, GCP, Azure, DigitalOcean, Linode, Ovh, Backblaze) !! - ⚡ Télécharger des images sur les cellules (fonctionne avec S3, Minio, GCP, Azure, DigitalOcean, Linode, Ovh, Backblaze) !!
- ⚡ Rôles: propriétaire, créateur, éditeur, commentateur, spectateur, commentateur, rôles personnalisés. - ⚡ Rôles: propriétaire, créateur, éditeur, commentateur, spectateur, commentateur, rôles personnalisés.
@ -158,15 +158,15 @@ docker-compose up -d
## Variables d'environnement ## Variables d'environnement
Please refer to [Environment variables](https://docs.nocodb.com/getting-started/self-hosted/environment-variables) Veuillez vous référer aux [Variables d'environnement](https://docs.nocodb.com/getting-started/self-hosted/environment-variables)
# Paramétrage du développement # Paramétrage du développement
Please refer to [Development Setup](https://docs.nocodb.com/engineering/development-setup) Veuillez vous référer au [Paramétrage du développement](https://docs.nocodb.com/engineering/development-setup)
# Contribuant # Contribuer
Please refer to [Contribution Guide](https://github.com/nocodb/nocodb/blob/master/.github/CONTRIBUTING.md). Veuillez vous référer au [Guide des contributions](https://github.com/nocodb/nocodb/blob/master/.github/CONTRIBUTING.md).
# Pourquoi construisons-nous cela? # Pourquoi construisons-nous cela?
La plupart des entreprises Internet s'équipent d'un tableur ou d'une base de données pour répondre à leurs besoins commerciaux. Les feuilles de calcul sont utilisées par plus d'un milliard d'humains en collaboration chaque jour. Cependant, nous sommes loin de travailler à des vitesses similaires sur des bases de données qui sont des outils beaucoup plus puissants en matière de calcul. Les tentatives pour résoudre ce problème avec les offres SaaS ont entraîné des contrôles d'accès horribles, le verrouillage des fournisseurs, le verrouillage des données, des changements de prix brusques et, surtout, un plafond de verre sur ce qui est possible à l'avenir. La plupart des entreprises Internet s'équipent d'un tableur ou d'une base de données pour répondre à leurs besoins commerciaux. Les feuilles de calcul sont utilisées par plus d'un milliard d'humains en collaboration chaque jour. Cependant, nous sommes loin de travailler à des vitesses similaires sur des bases de données qui sont des outils beaucoup plus puissants en matière de calcul. Les tentatives pour résoudre ce problème avec les offres SaaS ont entraîné des contrôles d'accès horribles, le verrouillage des fournisseurs, le verrouillage des données, des changements de prix brusques et, surtout, un plafond de verre sur ce qui est possible à l'avenir.

2
markdown/readme/languages/indonesian.md

@ -122,7 +122,7 @@ Kami menyediakan berbagai file docker-compose.yml di [bawah direktori](https://g
```bash ```bash
git clone https://github.com/nocodb/nocodb git clone https://github.com/nocodb/nocodb
# for PostgreSQL # for PostgreSQL
cd nocodb/docker-compose/pg cd nocodb/docker-compose/2_pg
docker-compose up -d docker-compose up -d
``` ```

81
markdown/readme/languages/portuguese.md

@ -2,8 +2,8 @@
<b> <b>
<a href="https://www.nocodb.com">NocoDB </a><br> <a href="https://www.nocodb.com">NocoDB </a><br>
</b> </b>
✨ A alternativa de opção de fonte aberta✨ <br> ✨ Alternativa do Airtable em código aberto ✨
<br>
</h1> </h1>
<p align="center"> <p align="center">
Transforma qualquer MySQL, PostgreSQL, SQL Server, Sqlite e MariaDB em uma planilha inteligente. Transforma qualquer MySQL, PostgreSQL, SQL Server, Sqlite e MariaDB em uma planilha inteligente.
@ -32,18 +32,17 @@ Transforma qualquer MySQL, PostgreSQL, SQL Server, Sqlite e MariaDB em uma plani
<a href="https://www.producthunt.com/posts/nocodb?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-nocodb" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=297536&theme=dark" alt="NocoDB - The Open Source Airtable alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/nocodb?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-nocodb" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=297536&theme=dark" alt="NocoDB - The Open Source Airtable alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p> </p>
# Experimente rápida # Comece rapidamente
### Usando o Docker. ### Usando o Docker.
```bash ```bash
docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest
``` ```
- NocoDB precisa de um banco de dados como entrada : Veja [Production Setup](https://github.com/nocodb/nocodb/blob/master/README.md#production-setup).
- Se a entrada não existir, nós voltamos para o SQLite. Para que SQLite também persista, você pode monta-lo em `/usr/app/data/`.
- NocoDB needs a database as input : See [Production Setup](https://github.com/nocodb/nocodb/blob/master/README.md#production-setup). Exemplo:
- If this input is absent, we fallback to SQLite. In order too persist sqlite, you can mount `/usr/app/data/`.
Example:
``` ```
docker run -d -p 8080:8080 --name nocodb -v "$(pwd)"/nocodb:/usr/app/data/ nocodb/nocodb:latest docker run -d -p 8080:8080 --name nocodb -v "$(pwd)"/nocodb:/usr/app/data/ nocodb/nocodb:latest
@ -52,7 +51,7 @@ docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest
### GUI ### GUI
Acessar o painel usando: [http://localhost:8080/dashboard](http://localhost:8080/dashboard) Acesse o painel usando: [http://localhost:8080/dashboard](http://localhost:8080/dashboard)
# Junte-se a nossa comunidade # Junte-se a nossa comunidade
@ -62,7 +61,7 @@ Acessar o painel usando: [http://localhost:8080/dashboard](http://localhost:8080
<br> <br>
<br> <br>
# Screenshots # Screenshots (Capturas de Tela)
![1](https://user-images.githubusercontent.com/86527202/136070349-cacc406d-9efe-406f-9aa2-1b81564332a7.png) ![1](https://user-images.githubusercontent.com/86527202/136070349-cacc406d-9efe-406f-9aa2-1b81564332a7.png)
<br> <br>
@ -99,34 +98,33 @@ Acessar o painel usando: [http://localhost:8080/dashboard](http://localhost:8080
# Recursos # Recursos
### Interface de planilha rica ### Rica Interface de Planilha
- ⚡ Pesquisar, classificar, filtrar, esconder colunas com uber facilidade - ⚡ Operações básicas: Criar, Ler, Atualizar e Deletar Tabelas, Colunas e Linhas<i>(Rows)</i>
- ⚡ Criar visualizações: Grade, Galeria, Kanban, Formulário - ⚡ Operação de campos: Sort, Filtro, Esconder / Mostrar Colunas
- ⚡ Compartilhar Visualizações: Public & Senha Protegido - ⚡ Multíplos tipos de visualização: Grade (Por padrão), Galeria, Visualização por Formulário e Visualização por Kanban
- ⚡ Vistas pessoais e bloqueadas - ⚡ Visualização por tipos de permissão: Colabarativo e Bloqueados
- ⚡ Carregar imagens para as células (funciona com S3, Minio, GCP, Azure, Digitalocean, Linodo, OVH, Backblaze) !! - ⚡ Bases de compartilhamento / Visualizaç~eos: Tantao pública, quanto privada (com proteção por senha)
- ⚡ Funções: proprietário, criador, editor, comentarista, visualizador, comentador, funções personalizadas. - ⚡ Variantes por tipos de células: D, LinkToAnotherRecord, Lookup, Rollup, SingleLineText, Attachment, Currency, Formula, etc
- ⚡ Controle de acesso: controle de acesso fino, mesmo no banco de dados, no nível da tabela e da coluna. - ⚡ Controle de Acesso por Funções: controle de acesso detalhado em diferentes níveis
- ⚡ E mais...
### App Store for Workflow Automations: ### App Store para fluxo de automoção:
Nós fornecemos difernetes tipos de integração na árvore principal de categórias. Veja [AppStore](https://docs.nocodb.com/account-settings/oss-specific-details/#app-store) para mais detalhes.
- ⚡ Bate-papo: Equipes Microsoft, folga, discórdia, material - ⚡ Bate-papo: Discord, Mattermost e outros
- ⚡ Email: SMTP, SES, MailChimp - ⚡ Email: AWS SES, SMTP, MailerSend e outros
- ⚡ SMS: Twilio - ⚡ Armazenamento: AWS S3, Google Cloud Storage, Minio e outros
- ⚡ whatsapp.
- ⚡ Qualquer APIs da 3ª parte
### Acesso programático da API via: ### Acesso Pragmático:
Nós forncemos as seguintes formas de deixar pragmaticamente seus usuários executar ações. Você pode usar um <i>token</i> (tanto JWT ou Autenticação por Rede Social) para assinar suas requisições de autorização para o NocoDB.
- ⚡ repouso APIs (Swagger) - ⚡ APIs Rest
- ⚡ APIs GraphQl. - ⚡ NocoDB SDK
- ⚡ Inclui autenticação JWT e autenticação social
- ⚡ Tokens de API para integrar com Zapier, integromat.
# Production Setup # Configuração de ambiente de Produção
O NOCODB requer um banco de dados para armazenar metadados de exibições de planilhas e bancos de dados externos. E parâmetros de conexão para este banco de dados podem ser especificados na variável de ambiente NC_DB. Por padrão, o SQLite é usado para armazenar metadados(metadata). Todavia, você pode específicar seu banco de dados. Os parametros de conexão com o banco de dados podem serem feitas usando a variável de ambiente `NC_DB`. E também disponibilizamos variáveis de ambientes para configuração.
## Docker ## Docker
@ -150,22 +148,29 @@ cd pg
docker-compose up -d docker-compose up -d
``` ```
## Environment variables ## Variáveis de Ambiente
Please refer to [Environment variables](https://docs.nocodb.com/getting-started/self-hosted/environment-variables) Por favor, consultar em [Variáveis de Ambiente](https://docs.nocodb.com/getting-started/self-hosted/environment-variables)
# Development setup # Configuração de Ambiente de Desenvolvimento
Please refer to [Development Setup](https://docs.nocodb.com/engineering/development-setup) Por favor, consultar em [Ambiente de Desenvolvimento](https://docs.nocodb.com/engineering/development-setup)
# Contributing # Guia de Contribuição
Please refer to [Contribution Guide](https://github.com/nocodb/nocodb/blob/master/.github/CONTRIBUTING.md). Por favor, consultar em [Guia de Contribuição](https://github.com/nocodb/nocodb/blob/master/.github/CONTRIBUTING.md).
# Por que estamos construindo isso? # Por que estamos construindo isso?
A maioria das empresas da Internet equipar-se com a planilha ou um banco de dados para resolver suas necessidades de negócios. Planilhas são usadas por um bilhão de seres humanos colaborativamente todos os dias. No entanto, estamos longe de trabalhar em velocidades semelhantes em bancos de dados que são muito mais poderosas ferramentas quando se trata de computação. As tentativas de resolver isso com ofertas de SaaS significam controles de acesso horríveis, lockin do fornecedor, lockin de dados, alterações abruptas de preços e mais importante, um teto de vidro no futuro. A maioria das empresas da internet equipam-se tanto com panilhas ou banco de dados para solucionar as necessidades de seus negócios.
Planilhas são usadas por mais de bilhões de humanos colaborativamente todos os dias.<br/> Contudo, nós estamos alguns passos atrás de atingir velocidades similares em bancos de dados - que são ferramentas poderosas - quando se trata de computação.
As tentaivas de solucionar isto oferecendo um SaaS vem significando controles de acesso horríveis, <i>vendor lock-in</i>, <i>data lock-in</i>, preços abruptos que mudam e o mais importante, um teto de vidro sobre o que é o possível futuro.
# Nossa missão # Nossa missão
Nossa missão é fornecer a mais poderosa interface de código para bancos de dados que é fonte aberta para cada negócio de Internet no mundo. Isso não apenas democratizaria o acesso a uma poderosa ferramenta de computação, mas também produzirá um bilhão de pessoas que terão habilidades radicais de corda e construção na Internet." Nossa missão é fornecer uma ferramenta com uma interface <i>no-code</i> poderosa e com banco de dados que é código aberto para todos os tipos de negócios no mundo.<br/>
Isto não somente para democratizar o acesso para uma computação poderosa, mas também trazer mais de quatro bilhões de pessoas que têm habilidades mais radicais em <i>"consertar e construir"</i> na internet.
# Licença
Este projeto está sobre a licença de [AGPLv3](https://github.com/nocodb/nocodb/blob/develop/LICENSE).

2
markdown/readme/languages/ukrainian.md

@ -134,7 +134,7 @@ iwr http://get.nocodb.com/win-arm64.exe -o Noco-win-arm64.exe
```bash ```bash
git clone https://github.com/nocodb/nocodb git clone https://github.com/nocodb/nocodb
# для PostgreSQL # для PostgreSQL
cd nocodb/docker-compose/pg cd nocodb/docker-compose/2_pg
docker-compose up -d docker-compose up -d
``` ```

891
packages/nc-cli/package-lock.json generated

File diff suppressed because it is too large Load Diff

8
packages/nc-gui/app.vue

@ -14,7 +14,7 @@ const disableBaseLayout = computed(() => route.value.path.startsWith('/nc/view')
useTheme() useTheme()
const { commandPalette, cmdData, cmdPlaceholder, activeScope, loadTemporaryScope, refreshCommandPalette } = useCommandPalette() const { commandPalette, cmdData, cmdPlaceholder, activeScope, loadTemporaryScope } = useCommandPalette()
applyNonSelectable() applyNonSelectable()
useEventListener(document, 'keydown', async (e: KeyboardEvent) => { useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
@ -91,12 +91,6 @@ function setActiveCmdView(cmd: CommandPaletteType) {
} }
} }
onMounted(() => {
nextTick(() => {
refreshCommandPalette()
})
})
// ref: https://github.com/vuejs/vue-cli/issues/7431#issuecomment-1793385162 // ref: https://github.com/vuejs/vue-cli/issues/7431#issuecomment-1793385162
// Stop error resizeObserver // Stop error resizeObserver
const debounce = (callback: (...args: any[]) => void, delay: number) => { const debounce = (callback: (...args: any[]) => void, delay: number) => {

13
packages/nc-gui/assets/nc-icons/discord.svg

@ -1,5 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <g id="Social Icons" clip-path="url(#clip0_304_20533)">
d="M12.8516 3.88457C11.9593 3.47514 11.0024 3.17349 10.0019 3.00072C9.98372 2.99739 9.96552 3.00572 9.95613 3.02239C9.83307 3.24126 9.69676 3.52681 9.6013 3.75124C8.52524 3.59014 7.45469 3.59014 6.40069 3.75124C6.30521 3.52182 6.16395 3.24126 6.04033 3.02239C6.03095 3.00628 6.01275 2.99795 5.99453 3.00072C4.99461 3.17294 4.03774 3.47459 3.14488 3.88457C3.13715 3.88791 3.13052 3.89347 3.12613 3.90068C1.31115 6.61223 0.813946 9.25712 1.05786 11.8692C1.05896 11.882 1.06613 11.8942 1.07607 11.902C2.27354 12.7814 3.4335 13.3153 4.57191 13.6691C4.59013 13.6747 4.60944 13.668 4.62103 13.653C4.89032 13.2853 5.13037 12.8975 5.33619 12.4897C5.34834 12.4659 5.33675 12.4375 5.31192 12.4281C4.93116 12.2836 4.5686 12.1075 4.21984 11.9076C4.19226 11.8915 4.19005 11.852 4.21543 11.8331C4.28882 11.7781 4.36223 11.7209 4.43231 11.6631C4.44499 11.6526 4.46265 11.6503 4.47756 11.657C6.76875 12.7031 9.24923 12.7031 11.5134 11.657C11.5283 11.6498 11.546 11.652 11.5592 11.6626C11.6293 11.7203 11.7027 11.7781 11.7766 11.8331C11.802 11.852 11.8003 11.8915 11.7728 11.9076C11.424 12.1114 11.0614 12.2836 10.6801 12.4275C10.6553 12.437 10.6443 12.4659 10.6564 12.4897C10.8666 12.8969 11.1067 13.2847 11.371 13.6525C11.3821 13.668 11.4019 13.6747 11.4201 13.6691C12.5641 13.3153 13.724 12.7814 14.9215 11.902C14.932 11.8942 14.9386 11.8826 14.9397 11.8698C15.2316 8.8499 14.4508 6.2267 12.8698 3.90124C12.8659 3.89347 12.8593 3.88791 12.8516 3.88457ZM5.67835 10.2787C4.98854 10.2787 4.42016 9.64544 4.42016 8.86769C4.42016 8.08994 4.97752 7.45665 5.67835 7.45665C6.38468 7.45665 6.94755 8.0955 6.93651 8.86769C6.93651 9.64544 6.37915 10.2787 5.67835 10.2787ZM10.3303 10.2787C9.64048 10.2787 9.0721 9.64544 9.0721 8.86769C9.0721 8.08994 9.62944 7.45665 10.3303 7.45665C11.0366 7.45665 11.5995 8.0955 11.5885 8.86769C11.5885 9.64544 11.0366 10.2787 10.3303 10.2787Z" <path id="Vector" d="M16.9308 3.4629C15.6561 2.87799 14.2892 2.44707 12.8599 2.20025C12.8339 2.19549 12.8079 2.20739 12.7945 2.2312C12.6187 2.54388 12.4239 2.9518 12.2876 3.27242C10.7503 3.04228 9.22099 3.04228 7.71527 3.27242C7.57887 2.94467 7.37707 2.54388 7.20048 2.2312C7.18707 2.20819 7.16107 2.19629 7.13504 2.20025C5.70659 2.44628 4.33963 2.87721 3.06411 3.4629C3.05307 3.46766 3.04361 3.4756 3.03732 3.48591C0.444493 7.35954 -0.265792 11.138 0.0826501 14.8695C0.0842267 14.8878 0.0944749 14.9053 0.108665 14.9164C1.81934 16.1726 3.47642 16.9353 5.10273 17.4408C5.12876 17.4488 5.15634 17.4393 5.1729 17.4178C5.55761 16.8925 5.90054 16.3385 6.19456 15.756C6.21192 15.7219 6.19535 15.6814 6.15989 15.6679C5.61594 15.4616 5.098 15.21 4.59977 14.9243C4.56037 14.9013 4.55721 14.8449 4.59346 14.8179C4.69831 14.7394 4.80318 14.6576 4.9033 14.5751C4.92141 14.56 4.94665 14.5568 4.96794 14.5664C8.24107 16.0608 11.7846 16.0608 15.0191 14.5664C15.0404 14.5561 15.0657 14.5592 15.0846 14.5743C15.1847 14.6568 15.2895 14.7394 15.3952 14.8179C15.4314 14.8449 15.4291 14.9013 15.3897 14.9243C14.8914 15.2155 14.3735 15.4616 13.8288 15.6671C13.7933 15.6806 13.7775 15.7219 13.7949 15.756C14.0952 16.3377 14.4381 16.8917 14.8157 17.417C14.8315 17.4393 14.8599 17.4488 14.8859 17.4408C16.5201 16.9353 18.1772 16.1726 19.8879 14.9164C19.9028 14.9053 19.9123 14.8886 19.9139 14.8703C20.3309 10.5562 19.2154 6.80878 16.9568 3.4867C16.9513 3.4756 16.9419 3.46766 16.9308 3.4629ZM6.68335 12.5974C5.69792 12.5974 4.88594 11.6927 4.88594 10.5816C4.88594 9.47056 5.68217 8.56585 6.68335 8.56585C7.69239 8.56585 8.49651 9.4785 8.48073 10.5816C8.48073 11.6927 7.68451 12.5974 6.68335 12.5974ZM13.329 12.5974C12.3435 12.5974 11.5316 11.6927 11.5316 10.5816C11.5316 9.47056 12.3278 8.56585 13.329 8.56585C14.338 8.56585 15.1421 9.4785 15.1264 10.5816C15.1264 11.6927 14.338 12.5974 13.329 12.5974Z" fill="#5865F2"/>
fill="#5865F2" /> </g>
<defs>
<clipPath id="clip0_304_20533">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

7
packages/nc-gui/assets/nc-icons/file-big.svg

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<path d="M18.6667 2.6665H8C7.29276 2.6665 6.61448 2.94746 6.11438 3.44755C5.61428 3.94765 5.33333 4.62593 5.33333 5.33317V26.6665C5.33333 27.3737 5.61428 28.052 6.11438 28.5521C6.61448 29.0522 7.29276 29.3332 8 29.3332H24C24.7072 29.3332 25.3855 29.0522 25.8856 28.5521C26.3857 28.052 26.6667 27.3737 26.6667 26.6665V10.6665L18.6667 2.6665Z" fill="white" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.3333 22.6665H10.6667" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.3333 17.3335H10.6667" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.3333 12H12H10.6667" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.6667 2.6665V10.6665H26.6667" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

5
packages/nc-gui/assets/nc-icons/info-solid.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 16V12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 8H12.01" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 544 B

5
packages/nc-gui/assets/nc-icons/megaphone.svg

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="megaphone">
<path id="Vector" d="M7.19333 5.66732L13.5 1.33398L13.5 14.6673L7.19333 10.334M7.19333 5.66732L2.50003 5.66732L2.5 10.334H4.84667M7.19333 5.66732L7.19333 10.334M7.19333 10.334H4.84667M7.19333 10.334L7.19333 14.0006H4.84667L4.84667 10.334" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 460 B

13
packages/nc-gui/assets/nc-icons/nocodb.svg

@ -0,0 +1,13 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame">
<path id="Vector" d="M16.6797 1H3.32031C2.03884 1 1 2.03884 1 3.32031V16.6797C1 17.9612 2.03884 19 3.32031 19H16.6797C17.9612 19 19 17.9612 19 16.6797V3.32031C19 2.03884 17.9612 1 16.6797 1Z" fill="white"/>
<path id="Vector_2" d="M16.6797 1H3.32031C2.03884 1 1 2.03884 1 3.32031V16.6797C1 17.9612 2.03884 19 3.32031 19H16.6797C17.9612 19 19 17.9612 19 16.6797V3.32031C19 2.03884 17.9612 1 16.6797 1Z" fill="url(#paint0_linear_304_21100)"/>
<path id="Vector_3" d="M4.86719 9.50131L7.31271 11.9484V15.8224H4.86719V9.50131ZM15.139 4.29891V15.4234C15.139 15.6518 14.9527 15.8365 14.7244 15.8365C14.6148 15.8365 14.51 15.7942 14.4318 15.716L4.86719 7.0902V4.64938C4.86719 4.42094 5.05182 4.23633 5.28025 4.23633H5.30217C5.41169 4.23633 5.51807 4.28014 5.59474 4.35681L12.6919 10.5151V4.29891H15.139Z" fill="white"/>
</g>
<defs>
<linearGradient id="paint0_linear_304_21100" x1="9.99859" y1="25.0599" x2="9.99859" y2="-4.04602" gradientUnits="userSpaceOnUse">
<stop stop-color="#4351E8"/>
<stop offset="1" stop-color="#2A1EA5"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

3
packages/nc-gui/assets/nc-icons/placeholder-icon.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16" fill="none">
<path d="M13.1667 2H3.83333C3.09695 2 2.5 2.59695 2.5 3.33333V12.6667C2.5 13.403 3.09695 14 3.83333 14H13.1667C13.903 14 14.5 13.403 14.5 12.6667V3.33333C14.5 2.59695 13.903 2 13.1667 2Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 386 B

16
packages/nc-gui/assets/nc-icons/reddit.svg

@ -1,7 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15Z" <g id="Social Icons" clip-path="url(#clip0_304_20536)">
fill="#FF4500" /> <path id="Vector" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20Z" fill="#FF4500"/>
<path <path id="Vector_2" d="M16.6666 9.99991C16.6666 9.19289 16.0117 8.53792 15.2046 8.53792C14.807 8.53792 14.4561 8.68997 14.1988 8.94728C13.2046 8.23383 11.8245 7.76599 10.3041 7.70751L10.9707 4.5847L13.1345 5.04084C13.1579 5.59055 13.614 6.035 14.1754 6.035C14.7485 6.035 15.2163 5.56716 15.2163 4.99406C15.2163 4.42096 14.7485 3.95312 14.1754 3.95312C13.766 3.95312 13.4152 4.18704 13.2514 4.53792L10.8304 4.0233C10.7602 4.01161 10.69 4.0233 10.6315 4.05839C10.5731 4.09348 10.538 4.15196 10.5146 4.22213L9.77774 7.70751C8.22219 7.7543 6.83037 8.21043 5.82453 8.94728C5.56722 8.70166 5.20464 8.53792 4.81868 8.53792C4.01166 8.53792 3.35669 9.19289 3.35669 9.99991C3.35669 10.5964 3.70757 11.0993 4.22219 11.3332C4.19879 11.4736 4.1871 11.6256 4.1871 11.7777C4.1871 14.0233 6.79529 15.8362 10.0234 15.8362C13.2514 15.8362 15.8596 14.0233 15.8596 11.7777C15.8596 11.6256 15.8479 11.4853 15.8245 11.3449C16.3041 11.111 16.6666 10.5964 16.6666 9.99991ZM6.66663 11.0408C6.66663 10.4677 7.13447 9.99991 7.70757 9.99991C8.28067 9.99991 8.7485 10.4677 8.7485 11.0408C8.7485 11.6139 8.28067 12.0818 7.70757 12.0818C7.13447 12.0818 6.66663 11.6139 6.66663 11.0408ZM12.4795 13.7894C11.766 14.5028 10.4093 14.5496 10.0117 14.5496C9.614 14.5496 8.24558 14.4911 7.54382 13.7894C7.43856 13.6841 7.43856 13.5087 7.54382 13.4034C7.64909 13.2982 7.82453 13.2982 7.92979 13.4034C8.37423 13.8479 9.3333 14.0116 10.0234 14.0116C10.7134 14.0116 11.6608 13.8479 12.1169 13.4034C12.2222 13.2982 12.3976 13.2982 12.5029 13.4034C12.5848 13.5204 12.5848 13.6841 12.4795 13.7894ZM12.2924 12.0818C11.7193 12.0818 11.2514 11.6139 11.2514 11.0408C11.2514 10.4677 11.7193 9.99991 12.2924 9.99991C12.8655 9.99991 13.3333 10.4677 13.3333 11.0408C13.3333 11.6139 12.8655 12.0818 12.2924 12.0818Z" fill="white"/>
d="M12.6667 8.00008C12.6667 7.43517 12.2082 6.97669 11.6433 6.97669C11.3649 6.97669 11.1193 7.08312 10.9392 7.26324C10.2433 6.76382 9.27722 6.43634 8.21289 6.3954L8.67956 4.20944L10.1942 4.52874C10.2106 4.91353 10.5298 5.22464 10.9228 5.22464C11.324 5.22464 11.6515 4.89716 11.6515 4.49599C11.6515 4.09482 11.324 3.76733 10.9228 3.76733C10.6363 3.76733 10.3907 3.93108 10.276 4.17669L8.58131 3.81646C8.53219 3.80827 8.48307 3.81646 8.44213 3.84102C8.40119 3.86558 8.37663 3.90652 8.36026 3.95564L7.84447 6.3954C6.75558 6.42815 5.78131 6.74745 5.07722 7.26324C4.8971 7.09131 4.6433 6.97669 4.37312 6.97669C3.80821 6.97669 3.34973 7.43517 3.34973 8.00008C3.34973 8.41763 3.59535 8.76967 3.95558 8.93342C3.93921 9.03166 3.93102 9.1381 3.93102 9.24453C3.93102 10.8165 5.75675 12.0855 8.0164 12.0855C10.276 12.0855 12.1018 10.8165 12.1018 9.24453C12.1018 9.1381 12.0936 9.03985 12.0772 8.9416C12.4129 8.77786 12.6667 8.41763 12.6667 8.00008ZM5.66669 8.72874C5.66669 8.32757 5.99418 8.00008 6.39535 8.00008C6.79652 8.00008 7.124 8.32757 7.124 8.72874C7.124 9.12991 6.79652 9.45739 6.39535 9.45739C5.99418 9.45739 5.66669 9.12991 5.66669 8.72874ZM9.7357 10.6527C9.23628 11.1521 8.28657 11.1849 8.00821 11.1849C7.72985 11.1849 6.77195 11.1439 6.28073 10.6527C6.20704 10.579 6.20704 10.4562 6.28073 10.3825C6.35441 10.3089 6.47722 10.3089 6.5509 10.3825C6.86201 10.6936 7.53336 10.8083 8.0164 10.8083C8.49944 10.8083 9.1626 10.6936 9.4819 10.3825C9.55558 10.3089 9.67839 10.3089 9.75207 10.3825C9.80938 10.4644 9.80938 10.579 9.7357 10.6527ZM9.6047 9.45739C9.20353 9.45739 8.87605 9.12991 8.87605 8.72874C8.87605 8.32757 9.20353 8.00008 9.6047 8.00008C10.0059 8.00008 10.3334 8.32757 10.3334 8.72874C10.3334 9.12991 10.0059 9.45739 9.6047 9.45739Z" </g>
fill="white" /> <defs>
<clipPath id="clip0_304_20536">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

12
packages/nc-gui/assets/nc-icons/refresh-cw.svg

@ -0,0 +1,12 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="refresh-cw" clip-path="url(#clip0_195_4453)">
<path id="Vector" d="M1.16699 13.3335V9.3335H5.16699" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M15.833 2.6665V6.6665H11.833" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M2.84033 5.99989C3.17844 5.04441 3.75308 4.19016 4.51064 3.51683C5.26819 2.84351 6.18397 2.37306 7.17252 2.14939C8.16106 1.92572 9.19016 1.95612 10.1638 2.23774C11.1374 2.51936 12.0238 3.04303 12.7403 3.75989L15.8337 6.66655M1.16699 9.33322L4.26033 12.2399C4.97682 12.9567 5.86324 13.4804 6.83687 13.762C7.81049 14.0437 8.83959 14.0741 9.82813 13.8504C10.8167 13.6267 11.7325 13.1563 12.49 12.4829C13.2476 11.8096 13.8222 10.9554 14.1603 9.99989" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_195_4453">
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

8
packages/nc-gui/assets/nc-icons/script.svg

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M2.5 14C1.67157 14 1 13.3284 1 12.5M4 11.5V3.5" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 14C11.3284 14 12 13.3284 12 12.5V8.5V4.5H13.5H15V3.5M10.5 14C9.67157 14 9 13.3284 9 12.5V11.5H1V12.5M10.5 14H2.5M5.5 2H13.5" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 3.5C4 2.67157 4.67157 2 5.5 2" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 3.5C15 2.67157 14.3284 2 13.5 2C12.6716 2 12 2.67157 12 3.5V4.5" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.00011 5.97858L6.00011 6.97858L7.00011 7.97858" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.99989 8.02142L9.99989 7.02142L8.99989 6.02142" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

3
packages/nc-gui/assets/nc-icons/spanner.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16" fill="none">
<path d="M9.83427 4.73872C9.71212 4.86334 9.64369 5.03089 9.64369 5.20539C9.64369 5.37989 9.71212 5.54744 9.83427 5.67206L10.9009 6.73872C11.0256 6.86088 11.1931 6.9293 11.3676 6.9293C11.5421 6.9293 11.7096 6.86088 11.8343 6.73872L14.3476 4.22539C14.6828 4.96618 14.7843 5.79155 14.6386 6.59149C14.4928 7.39143 14.1067 8.12795 13.5318 8.70291C12.9568 9.27787 12.2203 9.66394 11.4204 9.8097C10.6204 9.95545 9.79506 9.85395 9.05427 9.51872L4.4476 14.1254C4.18238 14.3906 3.82267 14.5396 3.4476 14.5396C3.07253 14.5396 2.71282 14.3906 2.4476 14.1254C2.18238 13.8602 2.03339 13.5005 2.03339 13.1254C2.03339 12.7503 2.18238 12.3906 2.4476 12.1254L7.05427 7.51872C6.71904 6.77793 6.61754 5.95257 6.76329 5.15263C6.90905 4.35269 7.29512 3.61616 7.87008 3.0412C8.44504 2.46625 9.18156 2.08017 9.9815 1.93442C10.7814 1.78867 11.6068 1.89017 12.3476 2.22539L9.84093 4.73206L9.83427 4.73872Z" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

2
packages/nc-gui/assets/nc-icons/star.svg

@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="star"> <g id="star">
<path id="Vector" d="M7.99992 1.33333L10.0599 5.50666L14.6666 6.18L11.3333 9.42666L12.1199 14.0133L7.99992 11.8467L3.87992 14.0133L4.66659 9.42666L1.33325 6.18L5.93992 5.50666L7.99992 1.33333Z" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path id="Vector" d="M7.99992 1.33333L10.0599 5.50666L14.6666 6.18L11.3333 9.42666L12.1199 14.0133L7.99992 11.8467L3.87992 14.0133L4.66659 9.42666L1.33325 6.18L5.93992 5.50666L7.99992 1.33333Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 410 B

10
packages/nc-gui/assets/nc-icons/twitter-x-line.svg

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Social Icons" clip-path="url(#clip0_304_20527)">
<path id="Vector" d="M0.0390097 0.666016L6.21643 8.92581L0 15.6414H1.39907L6.84153 9.76182L11.2389 15.6414H16L9.475 6.91698L15.2612 0.666016H13.8621L8.8499 6.08098L4.8001 0.666016H0.0390097ZM2.09644 1.69657H4.2837L13.9422 14.6106H11.755L2.09644 1.69657Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_304_20527">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 537 B

11
packages/nc-gui/assets/nc-icons/youtube2.svg

@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Social Icons" clip-path="url(#clip0_304_20530)">
<path id="Vector" d="M19.6016 5.15508C19.4885 4.72959 19.2657 4.34126 18.9554 4.02896C18.6451 3.71665 18.2582 3.49133 17.8334 3.37553C16.2698 2.95508 10.0198 2.95508 10.0198 2.95508C10.0198 2.95508 3.76978 2.95508 2.20614 3.37553C1.78138 3.49133 1.39449 3.71665 1.08418 4.02896C0.773872 4.34126 0.55103 4.72959 0.437957 5.15508C0.0197754 6.72553 0.0197754 10.0005 0.0197754 10.0005C0.0197754 10.0005 0.0197754 13.2755 0.437957 14.846C0.55103 15.2715 0.773872 15.6598 1.08418 15.9721C1.39449 16.2844 1.78138 16.5097 2.20614 16.6255C3.76978 17.046 10.0198 17.046 10.0198 17.046C10.0198 17.046 16.2698 17.046 17.8334 16.6255C18.2582 16.5097 18.6451 16.2844 18.9554 15.9721C19.2657 15.6598 19.4885 15.2715 19.6016 14.846C20.0198 13.2755 20.0198 10.0005 20.0198 10.0005C20.0198 10.0005 20.0198 6.72553 19.6016 5.15508Z" fill="#FF0302"/>
<path id="Vector_2" d="M7.97437 12.9731V7.02539L13.2016 9.99925L7.97437 12.9731Z" fill="#FEFEFE"/>
</g>
<defs>
<clipPath id="clip0_304_20530">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

5
packages/nc-gui/assets/style.scss

@ -16,6 +16,11 @@
} }
} }
* {
font-synthesis: none;
}
html { html {
overflow: hidden; overflow: hidden;
} }

6
packages/nc-gui/components/account/ResetPassword.vue

@ -46,9 +46,9 @@ const passwordChange = async () => {
message.success(t('msg.success.passwordChanged')) message.success(t('msg.success.passwordChanged'))
await signOut() await signOut({
redirectToSignin: true,
await navigateTo('/signin') })
} }
const resetError = () => { const resetError = () => {

1
packages/nc-gui/components/account/Setup.vue

@ -73,6 +73,7 @@ onMounted(async () => {
<div class="flex flex-col gap-6 w-150"> <div class="flex flex-col gap-6 w-150">
<div <div
v-for="config of configs" v-for="config of configs"
:key="config.key"
class="flex flex-col border-1 rounded-2xl border-gray-200 p-6 gap-2 hover:(shadow bg-gray-10)" class="flex flex-col border-1 rounded-2xl border-gray-200 p-6 gap-2 hover:(shadow bg-gray-10)"
:class="{ :class="{
'cursor-pointer': config.itemClick, 'cursor-pointer': config.itemClick,

3
packages/nc-gui/components/account/Token.vue

@ -140,7 +140,8 @@ const isValidTokenName = ref(false)
const deleteToken = async (token: string): Promise<void> => { const deleteToken = async (token: string): Promise<void> => {
try { try {
await api.orgTokens.delete(token) const id = allTokens.value.find((t) => t.token === token)?.id
await api.orgTokens.delete(id)
// message.success(t('msg.success.tokenDeleted')) // message.success(t('msg.success.tokenDeleted'))
await loadTokens() await loadTokens()

4
packages/nc-gui/components/cell/Currency.vue

@ -101,13 +101,14 @@ onMounted(() => {
{{ currencyMeta.currency_code }} {{ currencyMeta.currency_code }}
</span> </span>
</div> </div>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="(!readOnly && editEnabled) || (isForm && !isEditColumn && editEnabled)" v-if="(!readOnly && editEnabled) || (isForm && !isEditColumn && editEnabled)"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
type="number" type="number"
class="nc-cell-field h-full border-none rounded-md py-1 outline-none focus:outline-none focus:ring-0" class="nc-cell-field h-full border-none rounded-md py-1 outline-none focus:outline-none focus:ring-0"
:class="isForm && !isEditColumn ? 'flex flex-1' : 'w-full'" :class="isForm && !isEditColumn && !hidePrefix ? 'flex flex-1' : 'w-full'"
:placeholder="placeholder" :placeholder="placeholder"
:disabled="readOnly" :disabled="readOnly"
@blur="onBlur" @blur="onBlur"
@ -117,6 +118,7 @@ onMounted(() => {
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
@contextmenu.stop @contextmenu.stop

23
packages/nc-gui/components/cell/DatePicker.vue

@ -242,32 +242,25 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
// To prevent event listener on non active cell // To prevent event listener on non active cell
if (!active.value) return if (!active.value) return
if ( if (e.altKey || e.shiftKey || !isGrid.value || isExpandedForm.value || isEditColumn.value || isExpandedFormOpenExist()) {
e.altKey ||
e.ctrlKey ||
e.shiftKey ||
e.metaKey ||
!isGrid.value ||
isExpandedForm.value ||
isEditColumn.value ||
isExpandedFormOpenExist()
) {
return return
} }
switch (e.key) { if (e.metaKey || e.ctrlKey) {
case ';': if (e.key === ';') {
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
localState.value = dayjs(new Date()) localState.value = dayjs(new Date())
e.preventDefault() e.preventDefault()
break }
default: } else return
}
if (!isOpen.value && datePickerRef.value && /^[0-9a-z]$/i.test(e.key)) { if (!isOpen.value && datePickerRef.value && /^[0-9a-z]$/i.test(e.key)) {
isClearedInputMode.value = true isClearedInputMode.value = true
datePickerRef.value.focus() datePickerRef.value.focus()
editable.value = true editable.value = true
open.value = true open.value = true
} }
}
}) })
const handleUpdateValue = (e: Event) => { const handleUpdateValue = (e: Event) => {

23
packages/nc-gui/components/cell/DateTimePicker.vue

@ -296,32 +296,25 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
// To prevent event listener on non active cell // To prevent event listener on non active cell
if (!active.value) return if (!active.value) return
if ( if (e.altKey || e.shiftKey || !isGrid.value || isExpandedForm.value || isEditColumn.value || isExpandedFormOpenExist()) {
e.altKey ||
e.ctrlKey ||
e.shiftKey ||
e.metaKey ||
!isGrid.value ||
isExpandedForm.value ||
isEditColumn.value ||
isExpandedFormOpenExist()
) {
return return
} }
switch (e.key) { if (e.metaKey || e.ctrlKey) {
case ';': if (e.key === ';') {
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
localState.value = dayjs(new Date()) localState.value = dayjs(new Date())
e.preventDefault() e.preventDefault()
break }
default: } else return
}
if (!isOpen.value && (datePickerRef.value || timePickerRef.value) && /^[0-9a-z]$/i.test(e.key)) { if (!isOpen.value && (datePickerRef.value || timePickerRef.value) && /^[0-9a-z]$/i.test(e.key)) {
isClearedInputMode.value = true isClearedInputMode.value = true
isDatePicker.value ? datePickerRef.value?.focus() : timePickerRef.value?.focus() isDatePicker.value ? datePickerRef.value?.focus() : timePickerRef.value?.focus()
editable.value = true editable.value = true
open.value = true open.value = true
} }
}
}) })
watch(editable, (nextValue) => { watch(editable, (nextValue) => {

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

@ -98,6 +98,7 @@ watch(isExpandedFormOpen, () => {
</script> </script>
<template> <template>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"
@ -113,6 +114,7 @@ watch(isExpandedFormOpen, () => {
@keydown.right.stop @keydown.right.stop
@keydown.up.stop="onKeyDown" @keydown.up.stop="onKeyDown"
@keydown.delete.stop @keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

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

@ -76,6 +76,7 @@ const focus: VNodeRef = (el) =>
<template> <template>
<div class="duration-cell-wrapper"> <div class="duration-cell-wrapper">
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"
@ -90,6 +91,7 @@ const focus: VNodeRef = (el) =>
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

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

@ -76,6 +76,7 @@ watch(
</script> </script>
<template> <template>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"
@ -87,6 +88,7 @@ watch(
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
@paste.prevent="onPaste" @paste.prevent="onPaste"

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

@ -46,6 +46,7 @@ const focus: VNodeRef = (el) =>
</script> </script>
<template> <template>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="editEnabled" v-if="editEnabled"
:ref="focus" :ref="focus"
@ -59,6 +60,7 @@ const focus: VNodeRef = (el) =>
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

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

@ -93,6 +93,7 @@ function onKeyDown(e: any) {
</script> </script>
<template> <template>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"
@ -107,6 +108,7 @@ function onKeyDown(e: any) {
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

4
packages/nc-gui/components/cell/Json.vue

@ -86,6 +86,9 @@ const onSave = () => {
editEnabled.value = false editEnabled.value = false
// avoid saving if error exists or value is same as previous
if (error.value || localValue.value === vModel.value) return false
vModel.value = formatValue(localValue.value) === null ? null : formatJson(localValue.value as string) vModel.value = formatValue(localValue.value) === null ? null : formatJson(localValue.value as string)
} }
@ -211,6 +214,7 @@ watch(inputWrapperRef, () => {
:auto-focus="!isForm && !isEditColumn" :auto-focus="!isForm && !isEditColumn"
@update:model-value="localValue = $event" @update:model-value="localValue = $event"
@keydown.enter.stop @keydown.enter.stop
@keydown.alt.stop
/> />
<span v-if="error" class="nc-cell-field text-xs w-full py-1 text-red-500"> <span v-if="error" class="nc-cell-field text-xs w-full py-1 text-red-500">

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

@ -139,6 +139,7 @@ const onTabPress = (e: KeyboardEvent) => {
@mouseleave="onMouseleave" @mouseleave="onMouseleave"
@focus="onWrapperFocus" @focus="onWrapperFocus"
> >
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled && (isExpandedFormOpen ? expandedEditEnabled || !percentMeta.is_progress : true)" v-if="!readOnly && editEnabled && (isExpandedFormOpen ? expandedEditEnabled || !percentMeta.is_progress : true)"
:ref="focus" :ref="focus"
@ -154,6 +155,7 @@ const onTabPress = (e: KeyboardEvent) => {
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.tab="onTabPress" @keydown.tab="onTabPress"
@keydown.alt.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

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

@ -60,6 +60,7 @@ watch(
</script> </script>
<template> <template>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"
@ -71,6 +72,7 @@ watch(
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

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

@ -335,6 +335,7 @@ onClickOutside(editorDom, (e) => {
[`!overflow-hidden nc-truncate nc-line-clamp-${rowHeightTruncateLines(localRowHeight)}`]: [`!overflow-hidden nc-truncate nc-line-clamp-${rowHeightTruncateLines(localRowHeight)}`]:
!fullMode && readOnly && localRowHeight && !isExpandedFormOpen && !isForm, !fullMode && readOnly && localRowHeight && !isExpandedFormOpen && !isForm,
}" }"
@keydown.alt.stop
@keydown.alt.enter.stop @keydown.alt.enter.stop
@keydown.shift.enter.stop @keydown.shift.enter.stop
/> />

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

@ -30,6 +30,7 @@ const focus: VNodeRef = (el) =>
</script> </script>
<template> <template>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"
@ -41,6 +42,7 @@ const focus: VNodeRef = (el) =>
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

3
packages/nc-gui/components/cell/TextArea.vue

@ -242,6 +242,7 @@ watch(inputWrapperRef, () => {
> >
<LazyCellRichText v-model:value="vModel" sync-value-change read-only /> <LazyCellRichText v-model:value="vModel" sync-value-change read-only />
</div> </div>
<!-- eslint-disable vue/use-v-on-exact -->
<textarea <textarea
v-else-if="(editEnabled && !isVisible) || isForm" v-else-if="(editEnabled && !isVisible) || isForm"
:ref="focus" :ref="focus"
@ -259,6 +260,7 @@ watch(inputWrapperRef, () => {
}" }"
:disabled="readOnly" :disabled="readOnly"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown.alt.stop
@keydown.alt.enter.stop @keydown.alt.enter.stop
@keydown.shift.enter.stop @keydown.shift.enter.stop
@keydown.down.stop @keydown.down.stop
@ -359,6 +361,7 @@ watch(inputWrapperRef, () => {
:style="{ resize: 'both' }" :style="{ resize: 'both' }"
:disabled="readOnly" :disabled="readOnly"
@keydown.escape="isVisible = false" @keydown.escape="isVisible = false"
@keydown.alt.stop
/> />
</div> </div>

23
packages/nc-gui/components/cell/TimePicker.vue

@ -225,32 +225,25 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
// To prevent event listener on non active cell // To prevent event listener on non active cell
if (!active.value) return if (!active.value) return
if ( if (e.altKey || e.shiftKey || !isGrid.value || isExpandedForm.value || isEditColumn.value || isExpandedFormOpenExist()) {
e.altKey ||
e.ctrlKey ||
e.shiftKey ||
e.metaKey ||
!isGrid.value ||
isExpandedForm.value ||
isEditColumn.value ||
isExpandedFormOpenExist()
) {
return return
} }
switch (e.key) { if (e.metaKey || e.ctrlKey) {
case ';': if (e.key === ';') {
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
localState.value = dayjs(new Date()) localState.value = dayjs(new Date())
e.preventDefault() e.preventDefault()
break }
default: } else return
}
if (!isOpen.value && datePickerRef.value && /^[0-9a-z]$/i.test(e.key)) { if (!isOpen.value && datePickerRef.value && /^[0-9a-z]$/i.test(e.key)) {
isClearedInputMode.value = true isClearedInputMode.value = true
datePickerRef.value.focus() datePickerRef.value.focus()
editable.value = true editable.value = true
open.value = true open.value = true
} }
}
}) })
const handleUpdateValue = (e: Event) => { const handleUpdateValue = (e: Event) => {

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

@ -81,6 +81,7 @@ watch(
<template> <template>
<div class="flex flex-row items-center justify-between w-full h-full"> <div class="flex flex-row items-center justify-between w-full h-full">
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"
@ -92,6 +93,7 @@ watch(
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.alt.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

23
packages/nc-gui/components/cell/YearPicker.vue

@ -206,32 +206,25 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
// To prevent event listener on non active cell // To prevent event listener on non active cell
if (!active.value) return if (!active.value) return
if ( if (e.altKey || e.shiftKey || !isGrid.value || isExpandedForm.value || isEditColumn.value || isExpandedFormOpenExist()) {
e.altKey ||
e.ctrlKey ||
e.shiftKey ||
e.metaKey ||
!isGrid.value ||
isExpandedForm.value ||
isEditColumn.value ||
isExpandedFormOpenExist()
) {
return return
} }
switch (e.key) { if (e.metaKey || e.ctrlKey) {
case ';': if (e.key === ';') {
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
localState.value = dayjs(new Date()) localState.value = dayjs(new Date())
e.preventDefault() e.preventDefault()
break }
default: } else return
}
if (!isOpen.value && datePickerRef.value && /^[0-9a-z]$/i.test(e.key)) { if (!isOpen.value && datePickerRef.value && /^[0-9a-z]$/i.test(e.key)) {
isClearedInputMode.value = true isClearedInputMode.value = true
datePickerRef.value.focus() datePickerRef.value.focus()
editable.value = true editable.value = true
open.value = true open.value = true
} }
}
}) })
const handleUpdateValue = (e: Event) => { const handleUpdateValue = (e: Event) => {

222
packages/nc-gui/components/cmd-k/index.vue

@ -40,11 +40,23 @@ const cmdInputEl = ref<HTMLInputElement>()
const cmdInput = ref('') const cmdInput = ref('')
const debouncedCmdInput = ref('')
const { user } = useGlobal() const { user } = useGlobal()
const selected = ref<string>() const selected = ref<string>()
const { cmdPlaceholder } = useCommandPalette() const cmdkActionsRef = ref<HTMLElement>()
const cmdkActionSelectedRef = ref<HTMLElement>()
const ACTION_HEIGHT = 48
const WRAPPER_HEIGHT = 300
const SCROLL_MARGIN = ACTION_HEIGHT / 2
const { cmdPlaceholder, loadScope, cmdLoading } = useCommandPalette()
const formattedData: ComputedRef<(CmdAction & { weight: number })[]> = computed(() => { const formattedData: ComputedRef<(CmdAction & { weight: number })[]> = computed(() => {
const rt: (CmdAction & { weight: number })[] = [] const rt: (CmdAction & { weight: number })[] = []
@ -56,7 +68,7 @@ const formattedData: ComputedRef<(CmdAction & { weight: number })[]> = computed(
parent: el.parent || 'root', parent: el.parent || 'root',
weight: commandScore( weight: commandScore(
`${el.section}${el?.section === 'Views' && el?.is_default ? t('title.defaultView') : el.title}${el.keywords?.join()}`, `${el.section}${el?.section === 'Views' && el?.is_default ? t('title.defaultView') : el.title}${el.keywords?.join()}`,
cmdInput.value, debouncedCmdInput.value,
), ),
}) })
} }
@ -117,7 +129,7 @@ const actionList = computed(() => {
return 0 return 0
}) })
return formattedData.value.filter((el) => { return formattedData.value.filter((el) => {
if (cmdInput.value === '') { if (debouncedCmdInput.value === '') {
if (el.parent === activeScope.value) { if (el.parent === activeScope.value) {
if (!el.handler) { if (!el.handler) {
return isThereAnyActionInScope(el.id) return isThereAnyActionInScope(el.id)
@ -138,7 +150,7 @@ const actionList = computed(() => {
}) })
const searchedActionList = computed(() => { const searchedActionList = computed(() => {
if (cmdInput.value === '') return actionList.value if (debouncedCmdInput.value === '') return actionList.value
actionList.value.sort((a, b) => { actionList.value.sort((a, b) => {
if (a.weight > b.weight) return -1 if (a.weight > b.weight) return -1
if (a.weight < b.weight) return 1 if (a.weight < b.weight) return 1
@ -149,48 +161,91 @@ const searchedActionList = computed(() => {
.sort((a, b) => b.section?.toLowerCase().localeCompare(a.section?.toLowerCase() as string) || 0) .sort((a, b) => b.section?.toLowerCase().localeCompare(a.section?.toLowerCase() as string) || 0)
}) })
const actionListGroupedBySection = computed(() => { const visibleSections = computed(() => {
const rt: { [key: string]: CmdAction[] } = {} const sections: string[] = []
searchedActionList.value.forEach((el) => { searchedActionList.value.forEach((el) => {
if (el.section === 'hidden') return if (el.section && !sections.includes(el.section)) {
if (el.section) { sections.push(el.section)
if (!rt[el.section]) rt[el.section] = []
rt[el.section].push(el)
} else {
if (!rt.default) rt.default = []
rt.default.push(el)
} }
}) })
return sections
})
const actionListNormalized = computed(() => {
const rt: (CmdAction | { sectionTitle: string })[] = []
visibleSections.value.forEach((el) => {
rt.push({ sectionTitle: el || 'default' })
rt.push(...searchedActionList.value.filter((el2) => el2.section === el))
})
return rt return rt
}) })
const { list, containerProps, wrapperProps } = useVirtualList(actionListNormalized, {
itemHeight: ACTION_HEIGHT,
})
const keys = useMagicKeys() const keys = useMagicKeys()
const shiftModifier = keys.shift
const setAction = (action: string) => { const setAction = (action: string) => {
const oldActionIndex = searchedActionList.value.findIndex((el) => el.id === selected.value)
selected.value = action selected.value = action
nextTick(() => { nextTick(() => {
const actionIndex = searchedActionList.value.findIndex((el) => el.id === action) const actionIndex = searchedActionList.value.findIndex((el) => el.id === action)
if (actionIndex === -1) return if (actionIndex === -1) return
if (actionIndex === 0) { if (actionIndex === 0) {
document.querySelector('.cmdk-actions')?.scrollTo({ top: 0, behavior: 'smooth' }) containerProps.ref.value?.scrollTo({ top: 0, behavior: 'smooth' })
return
} else if (actionIndex === searchedActionList.value.length - 1) { } else if (actionIndex === searchedActionList.value.length - 1) {
document.querySelector('.cmdk-actions')?.scrollTo({ top: 999999, behavior: 'smooth' }) containerProps.ref.value?.scrollTo({
} else { top: actionIndex * ACTION_HEIGHT,
document.querySelector('.cmdk-action.selected')?.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'nearest',
}) })
return
} }
// check if selected action rendered
const actionEl = Array.isArray(cmdkActionSelectedRef.value) ? cmdkActionSelectedRef.value[0] : cmdkActionSelectedRef.value
if (!actionEl || !actionEl.classList?.contains('selected')) {
// if above the old selected action
if (actionIndex < oldActionIndex) {
containerProps.ref.value?.scrollTo({ top: (actionIndex + 1) * ACTION_HEIGHT - SCROLL_MARGIN, behavior: 'smooth' })
} else {
containerProps.ref.value?.scrollTo({
top: (actionIndex + 2) * ACTION_HEIGHT - WRAPPER_HEIGHT + SCROLL_MARGIN,
behavior: 'smooth',
}) })
} }
return
}
const selectFirstAction = () => { // count sections before the selected action
if (searchedActionList.value.length > 0) { const sectionBefore = visibleSections.value.findIndex((el) => el === searchedActionList.value[actionIndex].section) + 1
setAction(searchedActionList.value[0].id)
// check if selected action is visible in the list
const actionRect = actionEl?.getBoundingClientRect()
const listRect = cmdkActionsRef.value?.getBoundingClientRect()
if (actionRect && listRect) {
if (actionRect.top < listRect.top || actionRect.bottom > listRect.bottom) {
// if above the old selected action
if (actionIndex < oldActionIndex) {
containerProps.ref.value?.scrollTo({
top: (actionIndex + sectionBefore) * ACTION_HEIGHT - SCROLL_MARGIN,
behavior: 'smooth',
})
} else { } else {
selected.value = undefined containerProps.ref.value?.scrollTo({
top: (actionIndex + 1 + sectionBefore) * ACTION_HEIGHT - WRAPPER_HEIGHT + SCROLL_MARGIN,
behavior: 'smooth',
})
}
} }
} }
})
}
const setScope = (scope: string) => { const setScope = (scope: string) => {
activeScope.value = scope activeScope.value = scope
@ -199,13 +254,16 @@ const setScope = (scope: string) => {
nextTick(() => { nextTick(() => {
cmdInputEl.value?.focus() cmdInputEl.value?.focus()
selectFirstAction()
}) })
} }
const show = () => { const show = () => {
if (!user.value) return if (!user.value) return
if (props.scope === 'disabled') return if (props.scope === 'disabled') return
if (!vOpen.value) {
loadScope()
}
vOpen.value = true vOpen.value = true
cmdInput.value = '' cmdInput.value = ''
nextTick(() => { nextTick(() => {
@ -235,6 +293,22 @@ const fireAction = (action: CmdAction, preview = false) => {
} }
} }
const updateDebouncedInput = useDebounceFn(() => {
debouncedCmdInput.value = cmdInput.value
nextTick(() => {
cmdInputEl.value?.focus()
})
}, 100)
watch(cmdInput, () => {
if (cmdInput.value === '') {
debouncedCmdInput.value = ''
} else {
updateDebouncedInput()
}
})
whenever(keys.ctrl_k, () => { whenever(keys.ctrl_k, () => {
show() show()
}) })
@ -290,7 +364,7 @@ whenever(keys.Enter, () => {
const selectedEl = formattedData.value.find((el) => el.id === selected.value) const selectedEl = formattedData.value.find((el) => el.id === selected.value)
cmdInput.value = '' cmdInput.value = ''
if (selectedEl) { if (selectedEl) {
fireAction(selectedEl, keys.shift.value) fireAction(selectedEl, shiftModifier.value)
} }
} }
}) })
@ -306,6 +380,12 @@ onClickOutside(modalEl, () => {
if (vOpen.value) hide() if (vOpen.value) hide()
}) })
watch(searchedActionList, () => {
if (searchedActionList.value.length > 0) {
setAction(searchedActionList.value[0].id)
}
})
defineExpose({ defineExpose({
open: show, open: show,
close: hide, close: hide,
@ -329,6 +409,8 @@ defineExpose({
<div <div
class="text-gray-600 text-sm cursor-pointer flex gap-2 px-2 py-1 items-center justify-center font-medium capitalize" class="text-gray-600 text-sm cursor-pointer flex gap-2 px-2 py-1 items-center justify-center font-medium capitalize"
> >
<GeneralLoader v-if="cmdLoading && !el.label" />
<template v-else>
<GeneralWorkspaceIcon <GeneralWorkspaceIcon
v-if="el.icon && el.id.startsWith('ws')" v-if="el.icon && el.id.startsWith('ws')"
:workspace="{ :workspace="{
@ -369,83 +451,96 @@ defineExpose({
</span> </span>
</NcTooltip> </NcTooltip>
</span> </span>
</template>
</div> </div>
<span class="text-gray-700 text-sm pl-1 font-medium">/</span> <span class="text-gray-700 text-sm pl-1 font-medium">/</span>
</div> </div>
<input <input ref="cmdInputEl" v-model="cmdInput" class="cmdk-input" type="text" :placeholder="cmdPlaceholder" />
ref="cmdInputEl"
v-model="cmdInput"
class="cmdk-input"
type="text"
:placeholder="cmdPlaceholder"
@input="selectFirstAction"
/>
</div> </div>
</div> </div>
<div class="cmdk-body"> <div class="cmdk-body">
<div class="cmdk-actions nc-scrollbar-md"> <div ref="cmdkActionsRef" class="cmdk-actions nc-scrollbar-md">
<div v-if="searchedActionList.length === 0"> <div v-if="searchedActionList.length === 0 && cmdLoading" class="w-full h-[250px] flex justify-center items-center">
<GeneralLoader :size="30" />
</div>
<div v-else-if="searchedActionList.length === 0">
<div class="cmdk-action"> <div class="cmdk-action">
<div class="cmdk-action-content">No action found.</div> <div class="cmdk-action-content">No action found.</div>
</div> </div>
</div> </div>
<template v-else> <template v-else>
<div class="cmdk-action-list border-t-1 border-gray-200">
<div v-bind="containerProps" :style="`height: ${WRAPPER_HEIGHT}px`">
<div v-bind="wrapperProps">
<div v-for="item in list" :key="item.index" :style="`height: ${ACTION_HEIGHT}px`">
<template v-if="'sectionTitle' in item.data">
<div <div
v-for="[title, section] of Object.entries(actionListGroupedBySection)" class="cmdk-action-section-header capitalize"
:key="`cmdk-section-${title}`" :style="{
class="cmdk-action-section border-t-1 border-gray-200" height: `${ACTION_HEIGHT}px`,
}"
> >
<div v-if="title !== 'default'" class="cmdk-action-section-header capitalize">{{ title }}</div> {{ item.data.sectionTitle }}
<div class="cmdk-action-section-body"> </div>
</template>
<template v-else>
<div <div
v-for="act of section" :ref="item.data.id === selected ? 'cmdkActionSelectedRef' : undefined"
:key="act.id" :key="`${item.data.id}-${item.data.id === selected}`"
v-e="['a:cmdk:action']" v-e="['a:cmdk:action']"
class="cmdk-action group" class="cmdk-action group flex items-center"
:class="{ selected: selected === act.id }" :style="{
@mouseenter="setAction(act.id)" height: `${ACTION_HEIGHT}px`,
@click="fireAction(act)" }"
:class="{ selected: selected === item.data.id }"
@mouseenter="setAction(item.data.id)"
@click="fireAction(item.data)"
> >
<div class="cmdk-action-content w-full"> <div class="cmdk-action-content w-full">
<GeneralWorkspaceIcon <GeneralWorkspaceIcon
v-if="act.icon && act.id.startsWith('ws')" v-if="item.data.icon && item.data.id.startsWith('ws')"
:workspace="{ :workspace="{
id: act.id.split('-')[2], id: item.data.id.split('-')[2],
meta: { meta: {
color: act?.iconColor, color: item.data?.iconColor,
}, },
}" }"
class="mr-2" class="mr-2"
size="small" size="small"
/> />
<template v-else-if="title === 'Bases' || act.icon === 'project'"> <template v-else-if="item.data.section === 'Bases' || item.data.icon === 'project'">
<GeneralBaseIconColorPicker :key="act.iconColor" :model-value="act.iconColor" type="database" readonly> <GeneralBaseIconColorPicker
:key="item.data.iconColor"
:model-value="item.data.iconColor"
type="database"
readonly
>
</GeneralBaseIconColorPicker> </GeneralBaseIconColorPicker>
</template> </template>
<template v-else> <template v-else>
<component <component
:is="(iconMap as any)[act.icon]" :is="(iconMap as any)[item.data.icon]"
v-if="act.icon && typeof act.icon === 'string' && (iconMap as any)[act.icon]" v-if="item.data.icon && typeof item.data.icon === 'string' && (iconMap as any)[item.data.icon]"
:class="{ :class="{
'!text-blue-500': act.icon === 'grid', '!text-blue-500': item.data.icon === 'grid',
'!text-purple-500': act.icon === 'form', '!text-purple-500': item.data.icon === 'form',
'!text-[#FF9052]': act.icon === 'kanban', '!text-[#FF9052]': item.data.icon === 'kanban',
'!text-pink-500': act.icon === 'gallery', '!text-pink-500': item.data.icon === 'gallery',
'!text-maroon-500 w-4 h-4': act.icon === 'calendar', '!text-maroon-500 w-4 h-4': item.data.icon === 'calendar',
}" }"
class="cmdk-action-icon" class="cmdk-action-icon"
/> />
<div v-else-if="act.icon" class="cmdk-action-icon max-w-4 flex items-center justify-center"> <div v-else-if="item.data.icon" class="cmdk-action-icon max-w-4 flex items-center justify-center">
<LazyGeneralEmojiPicker class="!text-sm !h-4 !w-4" size="small" :emoji="act.icon" readonly /> <LazyGeneralEmojiPicker class="!text-sm !h-4 !w-4" size="small" :emoji="item.data.icon" readonly />
</div> </div>
</template> </template>
<a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg"> <a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg">
<template #title> <template #title>
{{ act.title }} {{ item.data.title }}
</template> </template>
<span class="truncate capitalize mr-4 py-0.5"> <span class="truncate capitalize mr-4 py-0.5">
{{ act.title }} {{ item.data.title }}
</span> </span>
</a-tooltip> </a-tooltip>
<div <div
@ -460,6 +555,9 @@ defineExpose({
</div> </div>
</div> </div>
</div> </div>
</template>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -618,7 +716,7 @@ defineExpose({
} }
} }
.cmdk-action-section { .cmdk-action-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;

3
packages/nc-gui/components/dashboard/Sidebar.vue

@ -5,7 +5,7 @@ const { isWorkspaceLoading } = storeToRefs(workspaceStore)
const { isSharedBase } = storeToRefs(useBase()) const { isSharedBase } = storeToRefs(useBase())
const { isMobileMode } = useGlobal() const { isMobileMode, appInfo } = useGlobal()
const treeViewDom = ref<HTMLElement>() const treeViewDom = ref<HTMLElement>()
@ -60,6 +60,7 @@ onUnmounted(() => {
<GeneralGift v-if="!isEeUI" /> <GeneralGift v-if="!isEeUI" />
<div class="border-t-1 w-full"></div> <div class="border-t-1 w-full"></div>
<DashboardSidebarBeforeUserInfo /> <DashboardSidebarBeforeUserInfo />
<DashboardSidebarFeed v-if="appInfo.feedEnabled" />
<DashboardSidebarUserInfo /> <DashboardSidebarUserInfo />
</div> </div>
</div> </div>

66
packages/nc-gui/components/dashboard/Sidebar/Feed.vue

@ -0,0 +1,66 @@
<script setup lang="ts">
const workspaceStore = useWorkspace()
const { navigateToFeed } = workspaceStore
const { isFeedPageOpened } = storeToRefs(workspaceStore)
const { isNewFeedAvailable } = useProductFeed()
const gotoFeed = () => navigateToFeed()
</script>
<template>
<NcButton
v-e="['c:nocodb:feed']"
type="text"
full-width
size="xsmall"
class="n!xs:hidden my-0.5 w-full !h-7 !rounded-md !font-normal !pl-4.5 !pr-5"
data-testid="nc-sidebar-product-feed"
:centered="false"
:class="{
'!text-brand-600 !bg-brand-50 !hover:bg-brand-50': isFeedPageOpened,
'!hover:(bg-gray-200 text-gray-700)': !isFeedPageOpened,
}"
@click="gotoFeed"
>
<div
class="flex !w-full items-center gap-2"
:class="{
'font-semibold': isFeedPageOpened,
}"
>
<div class="flex flex-1 w-full items-center gap-3">
<GeneralIcon icon="megaPhone" class="!h-4" />
<span class="">Whats New!</span>
</div>
<div v-if="isNewFeedAvailable" class="w-3 h-3 pulsing-dot bg-nc-fill-red-medium border-2 border-white rounded-full"></div>
</div>
</NcButton>
</template>
<style scoped lang="scss">
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.7;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.pulsing-dot {
animation: pulse 1.5s infinite ease-in-out;
}
:deep(.nc-btn-inner) {
@apply !w-full;
}
</style>

10
packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue

@ -22,12 +22,10 @@ const logout = async () => {
try { try {
const isSsoUser = !!(user?.value as any)?.sso_client_id const isSsoUser = !!(user?.value as any)?.sso_client_id
await signOut(false) await signOut({
redirectToSignin: true,
// No need as all stores are cleared on signout signinUrl: isSsoUser ? '/sso' : '/signin',
// await clearWorkspaces() })
await navigateTo(isSsoUser ? '/sso' : '/signin')
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} finally { } finally {

2
packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue

@ -147,7 +147,7 @@ async function onOpenModal({
</div> </div>
</NcMenuItem> </NcMenuItem>
<NcMenuItem v-if="!source.is_schema_readonly" @click="onOpenModal({ type: ViewTypes.FORM })"> <NcMenuItem v-if="!source.is_data_readonly" @click="onOpenModal({ type: ViewTypes.FORM })">
<div class="item" data-testid="sidebar-view-create-form"> <div class="item" data-testid="sidebar-view-create-form">
<div class="item-inner"> <div class="item-inner">
<GeneralViewIcon :meta="{ type: ViewTypes.FORM }" /> <GeneralViewIcon :meta="{ type: ViewTypes.FORM }" />

46
packages/nc-gui/components/dashboard/TreeView/TableNode.vue

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type BaseType, type TableType, type ViewType, ViewTypes } from 'nocodb-sdk' import { type BaseType, type TableType, ViewTypes } from 'nocodb-sdk'
import { toRef } from '@vue/reactivity' import { toRef } from '@vue/reactivity'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
@ -52,7 +52,7 @@ const {
duplicateTable: _duplicateTable, duplicateTable: _duplicateTable,
} = inject(TreeViewInj)! } = inject(TreeViewInj)!
const { loadViews: _loadViews, navigateToView } = useViewsStore() const { loadViews: _loadViews, navigateToView, duplicateView } = useViewsStore()
const { activeView, activeViewTitleOrId, viewsByTable } = storeToRefs(useViewsStore()) const { activeView, activeViewTitleOrId, viewsByTable } = storeToRefs(useViewsStore())
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore()) const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
@ -218,28 +218,21 @@ const deleteTable = () => {
isOptionsOpen.value = false isOptionsOpen.value = false
isTableDeleteDialogVisible.value = true isTableDeleteDialogVisible.value = true
} }
const isOnDuplicateLoading = ref<boolean>(false)
function onDuplicate() { async function onDuplicate() {
isOptionsOpen.value = false isOnDuplicateLoading.value = true
// Load views if not loaded
if (!viewsByTable.value.get(table.value.id as string)) {
await _openTable(table.value, undefined, false)
}
const views = viewsByTable.value.get(table.value.id as string) const views = viewsByTable.value.get(table.value.id as string)
const defaultView = views?.find((v) => v.is_default) || views?.[0] const defaultView = views?.find((v) => v.is_default) || views?.[0]
const isOpen = ref(true) if (defaultView) {
const view = await duplicateView(defaultView)
const { close } = useDialog(resolveComponent('DlgViewCreate'), {
'modelValue': isOpen,
'title': defaultView!.title,
'type': defaultView!.type as ViewTypes,
'tableId': table.value!.id,
'selectedViewId': defaultView!.id,
'groupingFieldColumnId': defaultView!.view!.fk_grp_col_id,
'views': views,
'calendarRange': defaultView!.view!.calendar_range,
'coverImageColumnId': defaultView!.view!.fk_cover_image_col_id,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()
refreshCommandPalette() refreshCommandPalette()
@ -248,6 +241,7 @@ function onDuplicate() {
tableId: table.value!.id!, tableId: table.value!.id!,
}) })
if (view) {
navigateToView({ navigateToView({
view, view,
tableId: table.value!.id!, tableId: table.value!.id!,
@ -256,16 +250,13 @@ function onDuplicate() {
}) })
$e('a:view:create', { view: view.type, sidebar: true }) $e('a:view:create', { view: view.type, sidebar: true })
},
})
function closeDialog() {
isOpen.value = false
close(1000)
} }
} }
isOnDuplicateLoading.value = false
isOptionsOpen.value = false
}
// TODO: Should find a way to render the components without using the `nextTick` function // TODO: Should find a way to render the components without using the `nextTick` function
const refreshViews = async () => { const refreshViews = async () => {
isExpanded.value = false isExpanded.value = false
@ -469,7 +460,8 @@ const source = computed(() => {
<NcDivider /> <NcDivider />
<NcMenuItem class="!text-gray-700" @click="onDuplicate"> <NcMenuItem class="!text-gray-700" @click="onDuplicate">
<GeneralIcon class="nc-view-copy-icon" icon="duplicate" /> <GeneralLoader v-if="isOnDuplicateLoading" size="regular" />
<GeneralIcon v-else class="nc-view-copy-icon" icon="duplicate" />
{{ {{
$t('general.duplicateEntity', { $t('general.duplicateEntity', {
entity: $t('title.defaultView').toLowerCase(), entity: $t('title.defaultView').toLowerCase(),

5
packages/nc-gui/components/dashboard/TreeView/index.vue

@ -151,6 +151,11 @@ const isCreateTableAllowed = computed(
useEventListener(document, 'keydown', async (e: KeyboardEvent) => { useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
if (isActiveInputElementExist()) {
return
}
if (e.altKey && !e.shiftKey && !cmdOrCtrl) { if (e.altKey && !e.shiftKey && !cmdOrCtrl) {
switch (e.keyCode) { switch (e.keyCode) {
case 84: { case 84: {

2
packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue

@ -219,6 +219,7 @@ onMounted(async () => {
</tr> </tr>
</tbody> </tbody>
<tfoot>
<tr> <tr>
<td :colspan="plugin.formDetails.items.length" class="text-center"> <td :colspan="plugin.formDetails.items.length" class="text-center">
<a-button type="default" class="!bg-gray-100 rounded-md border-none mr-1" @click="addSetting"> <a-button type="default" class="!bg-gray-100 rounded-md border-none mr-1" @click="addSetting">
@ -228,6 +229,7 @@ onMounted(async () => {
</a-button> </a-button>
</td> </td>
</tr> </tr>
</tfoot>
</table> </table>
</div> </div>

1
packages/nc-gui/components/dlg/TableDescriptionUpdate.vue

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import type { ComponentPublicInstance } from '@vue/runtime-core'
interface Props { interface Props {
modelValue?: boolean modelValue?: boolean

15
packages/nc-gui/components/dlg/ViewCreate.vue

@ -77,6 +77,8 @@ const meta = ref<TableType | undefined>()
const inputEl = ref<ComponentPublicInstance>() const inputEl = ref<ComponentPublicInstance>()
const descriptionInputEl = ref<ComponentPublicInstance>()
const formValidator = ref<typeof AntForm>() const formValidator = ref<typeof AntForm>()
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)
@ -151,12 +153,17 @@ watch(
function init() { function init() {
form.title = `${capitalize(typeAlias.value)}` form.title = `${capitalize(typeAlias.value)}`
if (selectedViewId.value) {
form.copy_from_id = selectedViewId?.value
const selectedViewName = views.value.find((v) => v.id === selectedViewId.value)?.title || form.title
form.title = generateUniqueTitle(`${selectedViewName} copy`, views.value, 'title', '_', true)
} else {
const repeatCount = views.value.filter((v) => v.title.startsWith(form.title)).length const repeatCount = views.value.filter((v) => v.title.startsWith(form.title)).length
if (repeatCount) { if (repeatCount) {
form.title = `${form.title}-${repeatCount}` form.title = `${form.title}-${repeatCount}`
} }
if (selectedViewId.value) {
form.copy_from_id = selectedViewId?.value
} }
nextTick(() => { nextTick(() => {
@ -259,7 +266,7 @@ const toggleDescription = () => {
} else { } else {
enableDescription.value = true enableDescription.value = true
setTimeout(() => { setTimeout(() => {
inputEl.value?.focus() descriptionInputEl.value?.focus()
}, 100) }, 100)
} }
} }
@ -766,7 +773,7 @@ const isCalendarReadonly = (calendarRange?: Array<{ fk_from_column_id: string; f
</div> </div>
<a-textarea <a-textarea
ref="inputEl" ref="descriptionInputEl"
v-model:value="form.description" v-model:value="form.description"
class="nc-input-sm nc-input-text-area nc-input-shadow px-3 !text-gray-800 max-h-[150px] min-h-[100px]" class="nc-input-sm nc-input-text-area nc-input-shadow px-3 !text-gray-800 max-h-[150px] min-h-[100px]"
hide-details hide-details

86
packages/nc-gui/components/extensions/Details.vue

@ -67,8 +67,7 @@ const detailsBody = computed(() => {
v-model:visible="vModel" v-model:visible="vModel"
:class="{ active: vModel }" :class="{ active: vModel }"
:footer="null" :footer="null"
:width="1154" size="lg"
size="medium"
wrap-class-name="nc-modal-extension-details" wrap-class-name="nc-modal-extension-details"
> >
<div v-if="activeExtension" class="flex flex-col w-full h-full"> <div v-if="activeExtension" class="flex flex-col w-full h-full">
@ -85,7 +84,7 @@ const detailsBody = computed(() => {
<div class="self-start flex items-center gap-2.5"> <div class="self-start flex items-center gap-2.5">
<NcButton size="small" class="w-full" @click="onAddExtension(activeExtension)"> <NcButton size="small" class="w-full" @click="onAddExtension(activeExtension)">
<div class="flex items-center justify-center gap-1 -ml-3px"> <div class="flex items-center justify-center gap-1 -ml-3px">
<GeneralIcon icon="plus" /> {{ $t('general.install') }} <GeneralIcon icon="plus" /> {{ $t('general.add') }} {{ $t('general.extension') }}
</div> </div>
</NcButton> </NcButton>
<NcButton size="small" type="text" @click="vModel = false"> <NcButton size="small" type="text" @click="vModel = false">
@ -103,32 +102,66 @@ const detailsBody = computed(() => {
<div class="extension-details-right-title">Version</div> <div class="extension-details-right-title">Version</div>
<div class="extension-details-right-subtitle">{{ activeExtension.version }}</div> <div class="extension-details-right-subtitle">{{ activeExtension.version }}</div>
</div> </div>
<NcDivider /> <NcDivider />
<div class="extension-details-right-section"> <div v-if="activeExtension.publisher" class="extension-details-right-section">
<div v-if="activeExtension.publisherName" class="extension-details-right-title">Publisher</div> <div class="extension-details-right-title">Publisher</div>
<div class="extension-details-right-subtitle">{{ activeExtension.publisherName }}</div> <div class="flex items-center gap-2">
<img
v-if="activeExtension.publisher?.icon?.src"
:src="getExtensionAssetsUrl(activeExtension.publisher.icon.src)"
alt="Publisher icon"
class="object-contain flex-none"
:style="{
width: activeExtension.publisher?.icon?.width ? `${activeExtension.publisher?.icon?.width}px` : '24px',
height: activeExtension.publisher?.icon?.height ? `${activeExtension.publisher?.icon?.height}px` : '24px',
}"
/>
<div class="extension-details-right-subtitle">{{ activeExtension.publisher.name }}</div>
</div> </div>
<template v-if="activeExtension.publisherEmail"> <div class="flex items-center gap-3 text-sm font-semibold text-nc-content-brand">
<NcDivider /> <a
<div class="extension-details-right-section"> v-if="activeExtension.publisher?.url"
<div class="extension-details-right-title">Publisher Email</div> :href="activeExtension.publisher.url"
<div class="extension-details-right-subtitle"> target="_blank"
<a :href="`mailto:${activeExtension.publisherEmail}`" target="_blank" rel="noopener noreferrer"> rel="noopener noreferrer"
{{ activeExtension.publisherEmail }} class="!no-underline !hover:underline"
>
Website
</a>
<template v-if="activeExtension.publisher?.email">
<div class="border-l-1 border-nc-border-gray-medium h-5"></div>
<a
:href="`mailto:${activeExtension.publisher.email}`"
target="_blank"
rel="noopener noreferrer"
class="!no-underline !hover:underline"
>
Contact
</a> </a>
</template>
</div> </div>
</div> </div>
</template> <template v-if="activeExtension.links && activeExtension.links.length">
<template v-if="activeExtension.publisherUrl">
<NcDivider /> <NcDivider />
<div class="extension-details-right-section"> <div class="extension-details-right-section">
<div class="extension-details-right-title">Publisher Website</div> <div class="extension-details-right-title">Links</div>
<div class="extension-details-right-subtitle"> <div>
<a :href="activeExtension.publisherUrl" target="_blank" rel="noopener noreferrer"> <div v-for="(doc, idx) of activeExtension.links" :key="idx" class="flex items-center gap-1">
{{ activeExtension.publisherUrl }} <div class="h-7 w-7 flex items-center justify-center">
<GeneralIcon icon="bookOpen" class="flex-none w-4 h-4 text-gray-600" />
</div>
<a
:href="doc.href"
target="_blank"
rel="noopener noreferrer"
class="!text-gray-700 text-sm !no-underline !hover:underline"
>
{{ doc.title }}
</a> </a>
</div> </div>
</div> </div>
</div>
</template> </template>
</div> </div>
</div> </div>
@ -138,7 +171,7 @@ const detailsBody = computed(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.extension-details { .extension-details {
@apply flex w-full h-[calc(100%_-_65px)]; @apply flex w-full h-[calc(100%_-_83px)];
.extension-details-left { .extension-details-left {
@apply p-6 flex-1 flex flex-col gap-6 nc-scrollbar-thin; @apply p-6 flex-1 flex flex-col gap-6 nc-scrollbar-thin;
@ -148,7 +181,7 @@ const detailsBody = computed(() => {
@apply p-5 w-[320px] flex flex-col space-y-4 border-l-1 border-gray-200 bg-gray-50 nc-scrollbar-thin; @apply p-5 w-[320px] flex flex-col space-y-4 border-l-1 border-gray-200 bg-gray-50 nc-scrollbar-thin;
.extension-details-right-section { .extension-details-right-section {
@apply flex flex-col gap-2; @apply flex flex-col gap-3;
} }
.extension-details-right-title { .extension-details-right-title {
@ -168,18 +201,11 @@ const detailsBody = computed(() => {
} }
.nc-modal { .nc-modal {
@apply !p-0; @apply !p-0;
height: min(calc(100vh - 100px), 1024px);
max-height: min(calc(100vh - 100px), 1024px) !important;
.nc-edit-or-add-integration-left-panel {
@apply w-full p-6 flex-1 flex justify-center;
}
.nc-edit-or-add-integration-right-panel {
@apply p-5 w-[320px] border-l-1 border-gray-200 flex flex-col gap-4 bg-gray-50 rounded-br-2xl;
}
} }
.nc-extension-details-body { .nc-extension-details-body {
@apply max-w-[768px] mx-auto;
p { p {
@apply !m-0 !leading-5; @apply !m-0 !leading-5;
} }

238
packages/nc-gui/components/extensions/Extension.vue

@ -6,15 +6,9 @@ interface Prop {
const { extensionId, error } = defineProps<Prop>() const { extensionId, error } = defineProps<Prop>()
const { const { extensionList, extensionsLoaded, availableExtensions, eventBus } = useExtensions()
extensionList,
extensionsLoaded, const isLoadedExtension = ref<boolean>(true)
availableExtensions,
eventBus,
getExtensionAssetsUrl,
duplicateExtension,
showExtensionDetails,
} = useExtensions()
const activeError = ref(error) const activeError = ref(error)
@ -32,63 +26,50 @@ const extension = computed(() => {
return ext return ext
}) })
const titleInput = ref<HTMLInputElement | null>(null) const extensionManifest = computed<ExtensionManifest | undefined>(() => {
return availableExtensions.value.find((ext) => ext.id === extension.value?.extensionId)
const titleEditMode = ref<boolean>(false)
const tempTitle = ref<string>(extension.value.title)
const enableEditMode = () => {
titleEditMode.value = true
tempTitle.value = extension.value.title
nextTick(() => {
titleInput.value?.focus()
titleInput.value?.select()
}) })
}
const updateExtensionTitle = async () => { const {
await extension.value.setTitle(tempTitle.value) fullscreen,
titleEditMode.value = false fullscreenModalSize: currentExtensionModalSize,
} collapsed,
} = useProvideExtensionHelper(extension, extensionManifest, activeError)
const { fullscreen, collapsed } = useProvideExtensionHelper(extension) const { height } = useElementSize(extensionRef)
const component = ref<any>(null) const component = ref<any>(null)
const extensionManifest = ref<ExtensionManifest | undefined>() const extensionHeight = computed(() => {
const heigthInInt = parseInt(extensionManifest.value?.config?.contentMinHeight || '') || undefined
const fullscreenModalMaxWidth = computed(() => { if (!heigthInInt || height.value > heigthInInt) return `${height.value}px`
const modalMaxWidth = {
xs: 'min(calc(100vw - 32px), 448px)',
sm: 'min(calc(100vw - 32px), 640px)',
md: 'min(calc(100vw - 48px), 900px)',
lg: 'min(calc(100vw - 48px), 1280px)',
}
return extensionManifest.value?.config?.modalMaxWith return extensionManifest.value?.config?.contentMinHeight
? modalMaxWidth[extensionManifest.value?.config?.modalMaxWith] || modalMaxWidth.lg
: modalMaxWidth.lg
}) })
const expandExtension = () => { const fullscreenModalSize = computed(() => {
if (!collapsed.value) return return currentExtensionModalSize.value ? modalSizes[currentExtensionModalSize.value] || modalSizes.lg : modalSizes.lg
})
collapsed.value = false // close fullscreen on clicking extensionModalRef directly
const closeFullscreen = (e: MouseEvent) => {
if (e.target === extensionModalRef.value) {
fullscreen.value = false
}
} }
onMounted(() => { onMounted(() => {
until(extensionsLoaded) until(extensionsLoaded)
.toMatch((v) => v) .toMatch((v) => v)
.then(() => { .then(() => {
extensionManifest.value = availableExtensions.value.find((ext) => ext.id === extension.value.extensionId)
if (!extensionManifest.value) { if (!extensionManifest.value) {
return return
} }
import(`../../extensions/${extensionManifest.value.entry}/index.vue`).then((mod) => { import(`../../extensions/${extensionManifest.value.entry}/index.vue`).then((mod) => {
component.value = markRaw(mod.default) component.value = markRaw(mod.default)
isLoadedExtension.value = false
}) })
}) })
.catch((err) => { .catch((err) => {
@ -97,9 +78,12 @@ onMounted(() => {
return return
} }
activeError.value = err activeError.value = err
isLoadedExtension.value = false
}) })
}) })
// #Listeners
// close fullscreen on escape key press // close fullscreen on escape key press
useEventListener('keydown', (e) => { useEventListener('keydown', (e) => {
// Check if the event target or its closest parent is an input, select, or textarea // Check if the event target or its closest parent is an input, select, or textarea
@ -111,23 +95,6 @@ useEventListener('keydown', (e) => {
} }
}) })
// close fullscreen on clicking extensionModalRef directly
const closeFullscreen = (e: MouseEvent) => {
if (e.target === extensionModalRef.value) {
fullscreen.value = false
}
}
const handleDuplicateExtension = async (id: string, open: boolean = false) => {
const duplicatedExt = await duplicateExtension(id)
if (duplicatedExt?.id && open) {
fullscreen.value = false
eventBus.emit(ExtensionsEvents.DUPLICATE, duplicatedExt.id)
}
}
// #Listeners
eventBus.on((event, payload) => { eventBus.on((event, payload) => {
if (event === ExtensionsEvents.DUPLICATE && extension.value.id === payload) { if (event === ExtensionsEvents.DUPLICATE && extension.value.id === payload) {
setTimeout(() => { setTimeout(() => {
@ -140,7 +107,7 @@ eventBus.on((event, payload) => {
</script> </script>
<template> <template>
<div ref="extensionRef" class="w-full px-4" :data-testid="extension.id"> <div ref="extensionRef" class="w-full px-4" :class="`nc-${extensionManifest?.id}`" :data-testid="extension.id">
<div <div
class="extension-wrapper" class="extension-wrapper"
:class="[ :class="[
@ -153,7 +120,7 @@ eventBus.on((event, payload) => {
:style=" :style="
!collapsed !collapsed
? { ? {
height: extensionManifest?.config?.contentMinHeight, height: extensionHeight,
minHeight: extensionManifest?.config?.contentMinHeight, minHeight: extensionManifest?.config?.contentMinHeight,
} }
: {} : {}
@ -161,67 +128,7 @@ eventBus.on((event, payload) => {
@mousedown="isMouseDown = true" @mousedown="isMouseDown = true"
@mouseup="isMouseDown = false" @mouseup="isMouseDown = false"
> >
<div <ExtensionsExtensionHeader :is-fullscreen="false" />
class="extension-header px-3 py-2"
:class="{
'border-b-1 border-gray-200 h-[49px]': !collapsed,
'collapsed border-transparent h-[48px]': collapsed,
}"
@click="expandExtension"
>
<div class="extension-header-left max-w-[calc(100%_-_100px)]">
<!-- Todo: enable later when we support extension reordering -->
<!-- eslint-disable vue/no-constant-condition -->
<NcButton size="xs" type="text" class="nc-extension-drag-handler !px-1" @click.stop>
<GeneralIcon icon="ncDrag" class="flex-none text-gray-500" />
</NcButton>
<img
v-if="extensionManifest"
:src="getExtensionAssetsUrl(extensionManifest.iconUrl)"
alt="icon"
class="h-8 w-8 object-contain"
/>
<a-input
v-if="titleEditMode && !fullscreen"
ref="titleInput"
v-model:value="tempTitle"
type="text"
class="flex-grow !h-8 !px-1 !py-1 !-ml-1 !rounded-lg w-4/5 extension-title"
@click.stop
@keyup.enter="updateExtensionTitle"
@keyup.esc="updateExtensionTitle"
@blur="updateExtensionTitle"
>
</a-input>
<NcTooltip v-else show-on-truncate-only class="truncate">
<template #title>
{{ extension.title }}
</template>
<span class="extension-title cursor-pointer" @dblclick.stop="enableEditMode" @click.stop>
{{ extension.title }}
</span>
</NcTooltip>
</div>
<div class="extension-header-right" @click.stop>
<ExtensionsExtensionMenu
:active-error="activeError"
class="nc-extension-menu"
@rename="enableEditMode"
@duplicate="handleDuplicateExtension(extension.id, true)"
@show-details="showExtensionDetails(extension.extensionId, 'extension')"
@clear-data="extension.clear()"
@delete="extension.delete()"
/>
<NcButton v-if="!activeError" type="text" size="xs" class="nc-extension-expand-btn !px-1" @click="fullscreen = true">
<GeneralIcon icon="ncMaximize2" class="h-3.5 w-3.5" />
</NcButton>
<NcButton size="xs" type="text" class="!px-1" @click="collapsed = !collapsed">
<GeneralIcon :icon="collapsed ? 'arrowDown' : 'arrowUp'" class="flex-none" />
</NcButton>
</div>
</div>
<template v-if="activeError"> <template v-if="activeError">
<div <div
@ -262,60 +169,16 @@ eventBus.on((event, payload) => {
:style=" :style="
fullscreen fullscreen
? { ? {
maxWidth: fullscreenModalMaxWidth, maxWidth: fullscreenModalSize.width,
maxHeight: fullscreenModalSize.height,
} }
: {} : {}
" "
> >
<div v-if="fullscreen" class="flex items-center justify-between cursor-default">
<div class="flex-1 max-w-[calc(100%_-_96px)] flex items-center gap-2 text-gray-800 font-semibold">
<img
v-if="extensionManifest"
:src="getExtensionAssetsUrl(extensionManifest.iconUrl)"
alt="icon"
class="flex-none w-8 h-8"
/>
<a-input
v-if="titleEditMode"
ref="titleInput"
v-model:value="tempTitle"
type="text"
class="flex-grow !h-8 !px-1 !py-1 !-ml-1 !rounded-lg !text-lg font-semibold extension-title max-w-[420px]"
@click.stop
@keyup.enter.stop="updateExtensionTitle"
@keyup.esc.stop="updateExtensionTitle"
@blur="updateExtensionTitle"
>
</a-input>
<NcTooltip v-else show-on-truncate-only class="extension-title truncate text-lg">
<template #title>
{{ extension.title }}
</template>
<span class="cursor-pointer" @dblclick="enableEditMode">
{{ extension.title }}
</span>
</NcTooltip>
</div>
<div class="flex items-center gap-4">
<ExtensionsExtensionMenu
:active-error="activeError"
:fullscreen="fullscreen"
@rename="enableEditMode"
@duplicate="handleDuplicateExtension(extension.id, true)"
@show-details="showExtensionDetails(extension.extensionId, 'extension')"
@clear-data="extension.clear()"
@delete="extension.delete()"
/>
<NcButton size="small" type="text" class="flex-none" @click="fullscreen = false">
<GeneralIcon icon="close" />
</NcButton>
</div>
</div>
<div <div
v-show="fullscreen || !collapsed" v-show="fullscreen || !collapsed"
class="extension-content" class="extension-content h-full"
:class="{ 'fullscreen h-[calc(100%-40px)]': fullscreen, 'h-full': !fullscreen }" :class="{ 'fullscreen': fullscreen, 'h-full nc-scrollbar-thin': !fullscreen }"
> >
<component :is="component" :key="extension.uiKey" class="h-full" /> <component :is="component" :key="extension.uiKey" class="h-full" />
</div> </div>
@ -323,6 +186,12 @@ eventBus.on((event, payload) => {
</div> </div>
</Teleport> </Teleport>
</template> </template>
<general-overlay :model-value="isLoadedExtension" inline transition class="!bg-opacity-15 rounded-xl overflow-hidden">
<div class="flex flex-col items-center justify-center h-full w-full !bg-white !bg-opacity-80">
<a-spin size="large" />
</div>
</general-overlay>
</div> </div>
</div> </div>
</template> </template>
@ -341,29 +210,6 @@ eventBus.on((event, payload) => {
} }
} }
.extension-header {
@apply flex justify-between;
&.collapsed:not(:hover) {
.nc-extension-expand-btn,
.nc-extension-menu {
@apply hidden;
}
}
.extension-header-left {
@apply flex-1 flex items-center gap-2;
}
.extension-header-right {
@apply flex items-center gap-1;
}
.extension-title {
@apply font-weight-600;
}
}
.extension-content { .extension-content {
@apply rounded-lg; @apply rounded-lg;
@ -373,10 +219,10 @@ eventBus.on((event, payload) => {
} }
.extension-modal { .extension-modal {
@apply absolute top-0 left-0 z-1000 w-full h-full bg-black bg-opacity-50; @apply absolute top-0 left-0 z-1000 w-full h-full bg-black bg-opacity-50 flex items-center justify-center;
.extension-modal-content { .extension-modal-content {
@apply bg-white rounded-2xl w-[90%] h-[90vh] mt-[5vh] mx-auto p-6 flex flex-col gap-3; @apply bg-white rounded-2xl w-[90%] h-[90vh] mx-auto flex flex-col;
} }
} }

185
packages/nc-gui/components/extensions/Extension/Header.vue

@ -0,0 +1,185 @@
<script lang="ts" setup>
/**
* ExtensionHeader component.
*
* @slot prefix - Slot for custom content to be displayed at the start of the header when in fullscreen mode.
* @slot extra - Slot for additional custom content to be displayed before the options and close button in fullscreen mode.
*/
interface Props {
isFullscreen?: boolean
}
withDefaults(defineProps<Props>(), {
isFullscreen: true,
})
const { eventBus, getExtensionAssetsUrl, duplicateExtension, showExtensionDetails } = useExtensions()
const { fullscreen, collapsed, extension, extensionManifest, activeError, showExpandBtn } = useExtensionHelperOrThrow()
const titleInput = ref<HTMLInputElement | null>(null)
const titleEditMode = ref<boolean>(false)
const tempTitle = ref<string>(extension.value.title)
const showExpandButton = computed(() => {
return showExpandBtn.value && !activeError.value
})
const enableEditMode = () => {
titleEditMode.value = true
tempTitle.value = extension.value.title
nextTick(() => {
titleInput.value?.focus()
titleInput.value?.select()
})
}
const updateExtensionTitle = async () => {
await extension.value.setTitle(tempTitle.value)
titleEditMode.value = false
}
const expandExtension = () => {
if (!collapsed.value) return
collapsed.value = false
}
/**
* Handles the duplication of an extension.
*
* @param id - The ID of the extension to duplicate.
* @param open - Optional. If true, the duplicated extension will be opened.
*/
const handleDuplicateExtension = async (id: string, open: boolean = false) => {
const duplicatedExt = await duplicateExtension(id)
if (duplicatedExt?.id && open) {
fullscreen.value = false
eventBus.emit(ExtensionsEvents.DUPLICATE, duplicatedExt.id)
}
}
</script>
<template>
<div
v-if="(isFullscreen && fullscreen) || !isFullscreen"
class="extension-header flex items-center"
:class="{
'border-b-1 border-nc-border-gray-medium h-[49px]': !collapsed && !isFullscreen,
'collapsed border-transparent h-[48px]': collapsed && !isFullscreen,
'px-3 py-2 gap-1': !isFullscreen,
'gap-3 px-4 pt-4 pb-[15px] border-b-1 border-nc-border-gray-medium': isFullscreen,
}"
@click="expandExtension"
>
<slot v-if="isFullscreen" name="prefix"></slot>
<NcButton v-if="!isFullscreen" size="xs" type="text" class="nc-extension-drag-handler !px-1" @click.stop>
<GeneralIcon icon="ncDrag" class="flex-none text-gray-500" />
</NcButton>
<img
v-if="extensionManifest"
:src="getExtensionAssetsUrl(extensionManifest.iconUrl)"
alt="icon"
class="h-8 w-8 object-contain flex-none"
:class="{
'mx-1': !isFullscreen,
}"
/>
<div
v-if="titleEditMode"
class="flex-1"
:class="{
'mr-1': !isFullscreen,
}"
>
<a-input
ref="titleInput"
v-model:value="tempTitle"
type="text"
class="!h-8 !px-1 !py-1 !-ml-1 !rounded-lg extension-title"
:class="{
'w-4/5': !isFullscreen,
'!text-lg !font-semibold max-w-[420px]': isFullscreen,
}"
@click.stop
@keyup.enter="updateExtensionTitle"
@keyup.esc="updateExtensionTitle"
@blur="updateExtensionTitle"
>
</a-input>
</div>
<NcTooltip v-else show-on-truncate-only class="truncate flex-1">
<template #title>
{{ extension.title }}
</template>
<span
class="extension-title cursor-pointer"
:class="{
'text-lg font-semibold ': isFullscreen,
'mr-1': !isFullscreen,
}"
@dblclick.stop="enableEditMode"
@click.stop
>
{{ extension.title }}
</span>
</NcTooltip>
<slot v-if="isFullscreen" name="extra"></slot>
<ExtensionsExtensionHeaderMenu
:is-fullscreen="isFullscreen"
class="nc-extension-menu"
@rename="enableEditMode"
@duplicate="handleDuplicateExtension(extension.id, true)"
@show-details="showExtensionDetails(extension.extensionId, 'extension')"
@clear-data="extension.clear()"
@delete="extension.delete()"
/>
<template v-if="!isFullscreen">
<NcButton
v-if="showExpandButton"
size="xs"
type="text"
class="nc-extension-expand-btn !px-1"
@click.stop="fullscreen = true"
>
<GeneralIcon icon="ncMaximize2" class="h-3.5 w-3.5" />
</NcButton>
<NcButton size="xs" type="text" class="!px-1" @click.stop="collapsed = !collapsed">
<GeneralIcon :icon="collapsed ? 'arrowDown' : 'arrowUp'" class="flex-none" />
</NcButton>
</template>
<NcButton v-else :size="isFullscreen ? 'small' : 'xs'" type="text" class="flex-none !px-1" @click="fullscreen = false">
<GeneralIcon icon="close" />
</NcButton>
</div>
</template>
<style lang="scss" scoped>
.extension-header {
&.collapsed:not(:hover) {
.nc-extension-expand-btn,
.nc-extension-menu {
@apply hidden;
}
}
.extension-header-left {
@apply flex-1 flex items-center gap-2;
}
.extension-header-right {
@apply flex items-center gap-1;
}
.extension-title {
@apply font-weight-600;
}
}
</style>

12
packages/nc-gui/components/extensions/ExtensionMenu.vue → packages/nc-gui/components/extensions/Extension/HeaderMenu.vue

@ -1,17 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
interface Props { interface Props {
fullscreen?: boolean isFullscreen?: boolean
activeError?: boolean
} }
const { fullscreen, activeError } = defineProps<Props>() defineProps<Props>()
const emits = defineEmits(['rename', 'duplicate', 'showDetails', 'clearData', 'delete']) const emits = defineEmits(['rename', 'duplicate', 'showDetails', 'clearData', 'delete'])
const { activeError } = useExtensionHelperOrThrow()
</script> </script>
<template> <template>
<div class="flex items-center"> <div class="flex items-center" @click.stop>
<NcDropdown :trigger="['click']" placement="bottomRight"> <NcDropdown :trigger="['click']" placement="bottomRight">
<NcButton type="text" :size="fullscreen ? 'small' : 'xs'" class="!px-1"> <NcButton type="text" :size="isFullscreen ? 'small' : 'xs'" class="!px-1">
<GeneralIcon icon="threeDotVertical" /> <GeneralIcon icon="threeDotVertical" />
</NcButton> </NcButton>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save