Browse Source

Merge pull request #8852 from nocodb/develop

pull/8853/head 0.251.0
github-actions[bot] 6 months ago committed by GitHub
parent
commit
b3eb8ecd76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 112
      README.md
  2. 31
      docker-compose/mssql/docker-compose.yml
  3. 27
      markdown/readme/languages/chinese.md
  4. 30
      markdown/readme/languages/dutch.md
  5. 26
      markdown/readme/languages/french.md
  6. 32
      markdown/readme/languages/german.md
  7. 19
      markdown/readme/languages/indonesian.md
  8. 32
      markdown/readme/languages/italian.md
  9. 32
      markdown/readme/languages/japanese.md
  10. 32
      markdown/readme/languages/korean.md
  11. 33
      markdown/readme/languages/portuguese.md
  12. 32
      markdown/readme/languages/russian.md
  13. 32
      markdown/readme/languages/spanish.md
  14. 29
      markdown/readme/languages/ukrainian.md
  15. 6
      packages/nc-gui/assets/nc-icons/chevron-up-down.svg
  16. 2
      packages/nc-gui/assets/nc-icons/maximize-all.svg
  17. 8
      packages/nc-gui/assets/nc-icons/minimize-2.svg
  18. 2
      packages/nc-gui/assets/nc-icons/minimize.svg
  19. 9
      packages/nc-gui/assets/nc-icons/refresh.svg
  20. 78
      packages/nc-gui/assets/style.scss
  21. 4
      packages/nc-gui/components/cell/Currency.vue
  22. 1
      packages/nc-gui/components/cell/DatePicker.vue
  23. 29
      packages/nc-gui/components/cell/Json.vue
  24. 10
      packages/nc-gui/components/cell/MultiSelect.vue
  25. 2
      packages/nc-gui/components/cell/RichText.vue
  26. 6
      packages/nc-gui/components/cell/SingleSelect.vue
  27. 28
      packages/nc-gui/components/cell/attachment/RenameFile.vue
  28. 4
      packages/nc-gui/components/cell/attachment/index.vue
  29. 8
      packages/nc-gui/components/dashboard/TreeView/AddNewTableNode.vue
  30. 16
      packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue
  31. 8
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  32. 138
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  33. 19
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  34. 12
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  35. 5
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  36. 11
      packages/nc-gui/components/dashboard/TreeView/index.vue
  37. 10
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  38. 58
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  39. 57
      packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue
  40. 64
      packages/nc-gui/components/dashboard/settings/data-sources/SourceRestrictions.vue
  41. 14
      packages/nc-gui/components/dlg/ProjectAudit.vue
  42. 2
      packages/nc-gui/components/dlg/ProjectDelete.vue
  43. 11
      packages/nc-gui/components/dlg/ProjectDuplicate.vue
  44. 60
      packages/nc-gui/components/dlg/TableCreate.vue
  45. 2
      packages/nc-gui/components/dlg/TableDelete.vue
  46. 11
      packages/nc-gui/components/dlg/TableDuplicate.vue
  47. 13
      packages/nc-gui/components/dlg/TableRename.vue
  48. 169
      packages/nc-gui/components/dlg/ViewCreate.vue
  49. 2
      packages/nc-gui/components/dlg/ViewDelete.vue
  50. 2
      packages/nc-gui/components/dlg/WorkspaceDelete.vue
  51. 4
      packages/nc-gui/components/general/BaseLogo.vue
  52. 7
      packages/nc-gui/components/general/DeleteModal.vue
  53. 39
      packages/nc-gui/components/general/SourceRestrictionTooltip.vue
  54. 3
      packages/nc-gui/components/nc/Dropdown.vue
  55. 34
      packages/nc-gui/components/nc/Pagination.vue
  56. 241
      packages/nc-gui/components/nc/PaginationV2.vue
  57. 2
      packages/nc-gui/components/nc/Select.vue
  58. 8
      packages/nc-gui/components/nc/SubMenu.vue
  59. 6
      packages/nc-gui/components/nc/Tooltip.vue
  60. 1
      packages/nc-gui/components/project/AccessSettings.vue
  61. 4
      packages/nc-gui/components/project/AllTables.vue
  62. 4
      packages/nc-gui/components/project/View.vue
  63. 4
      packages/nc-gui/components/smartsheet/Details.vue
  64. 2
      packages/nc-gui/components/smartsheet/Form.vue
  65. 33
      packages/nc-gui/components/smartsheet/Gallery.vue
  66. 2
      packages/nc-gui/components/smartsheet/Kanban.vue
  67. 8
      packages/nc-gui/components/smartsheet/Pagination.vue
  68. 2
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  69. 2
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  70. 5
      packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue
  71. 3
      packages/nc-gui/components/smartsheet/column/DecimalOptions.vue
  72. 72
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  73. 88
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  74. 5
      packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue
  75. 9
      packages/nc-gui/components/smartsheet/column/RatingOptions.vue
  76. 7
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  77. 52
      packages/nc-gui/components/smartsheet/column/UITypesOptionsWithSearch.vue
  78. 27
      packages/nc-gui/components/smartsheet/details/Fields.vue
  79. 6
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  80. 500
      packages/nc-gui/components/smartsheet/grid/GroupBy.vue
  81. 13
      packages/nc-gui/components/smartsheet/grid/GroupByTable.vue
  82. 291
      packages/nc-gui/components/smartsheet/grid/PaginationV2.vue
  83. 313
      packages/nc-gui/components/smartsheet/grid/Table.vue
  84. 47
      packages/nc-gui/components/smartsheet/grid/index.vue
  85. 16
      packages/nc-gui/components/smartsheet/header/Cell.vue
  86. 127
      packages/nc-gui/components/smartsheet/header/Menu.vue
  87. 10
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  88. 2
      packages/nc-gui/components/smartsheet/toolbar/Calendar/Range.vue
  89. 19
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  90. 23
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  91. 174
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  92. 15
      packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue
  93. 5
      packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue
  94. 16
      packages/nc-gui/components/tabs/Smartsheet.vue
  95. 2
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  96. 4
      packages/nc-gui/components/virtual-cell/HasMany.vue
  97. 2
      packages/nc-gui/components/virtual-cell/Links.vue
  98. 11
      packages/nc-gui/components/virtual-cell/Lookup.vue
  99. 4
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  100. 4
      packages/nc-gui/components/virtual-cell/OneToOne.vue
  101. Some files were not shown because too many files have changed in this diff Show More

112
README.md

@ -71,82 +71,32 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart spreadshe
## Docker
```bash
# for SQLite
docker run -d --name nocodb \
-v "$(pwd)"/nocodb:/usr/app/data/ \
-p 8080:8080 \
nocodb/nocodb:latest
# for MySQL
docker run -d --name nocodb-mysql \
-v "$(pwd)"/nocodb:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
# for PostgreSQL
# with PostgreSQL
docker run -d --name nocodb-postgres \
-v "$(pwd)"/nocodb:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="pg://host.docker.internal:5432?u=root&p=password&d=d1" \
-e NC_DB="pg://host.docker.internal:5432?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
# for MSSQL
docker run -d --name nocodb-mssql \
# with SQLite : mounting volume `/usr/app/data/` is crucial to avoid data loss.
docker run -d --name nocodb \
-v "$(pwd)"/nocodb:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="mssql://host.docker.internal:1433?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
> To persist data in docker you can mount volume at `/usr/app/data/` since 0.10.6. Otherwise your data will be lost after recreating the container.
> If you plan to input some special characters, you may need to change the character set and collation yourself when creating the database. Please check out the examples for [MySQL Docker](https://github.com/nocodb/nocodb/issues/1340#issuecomment-1049481043).
> Different commands just indicate the database that NocoDB will use internally for metadata storage, but that doesn't influence the ability to connect to a different database type.
## Binaries
##### MacOS (x64)
```bash
curl http://get.nocodb.com/macos-x64 -o nocodb -L && chmod +x nocodb && ./nocodb
```
##### MacOS (arm64)
```bash
curl http://get.nocodb.com/macos-arm64 -o nocodb -L && chmod +x nocodb && ./nocodb
```
##### Linux (x64)
```bash
curl http://get.nocodb.com/linux-x64 -o nocodb -L && chmod +x nocodb && ./nocodb
```
##### Linux (arm64)
```bash
curl http://get.nocodb.com/linux-arm64 -o nocodb -L && chmod +x nocodb && ./nocodb
```
##### Windows (x64)
```bash
iwr http://get.nocodb.com/win-x64.exe -o Noco-win-x64.exe
.\Noco-win-x64.exe
```
##### Windows (arm64)
```bash
iwr http://get.nocodb.com/win-arm64.exe -o Noco-win-arm64.exe
.\Noco-win-arm64.exe
```
🚥 Binaries are intended for ONLY quick trials or testing purposes and are not recommended for production use.
| OS | Architecture | Command |
|---------|--------------|----------------------------------------------------------------------------------------------|
| 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` |
## Docker Compose
@ -154,41 +104,9 @@ We provide different docker-compose.yml files under [this directory](https://git
```bash
git clone https://github.com/nocodb/nocodb
# for MySQL
cd nocodb/docker-compose/mysql
# for PostgreSQL
cd nocodb/docker-compose/pg
# for MSSQL
cd nocodb/docker-compose/mssql
docker-compose up -d
```
> To persist data in docker, you can mount volume at `/usr/app/data/` since 0.10.6. Otherwise your data will be lost after recreating the container.
> If you plan to input some special characters, you may need to change the character set and collation yourself when creating the database. Please check out the examples for [MySQL Docker Compose](https://github.com/nocodb/nocodb/issues/1313#issuecomment-1046625974).
## NPX
You can run the below command if you need an interactive configuration.
```
npx create-nocodb-app
```
<img src="https://user-images.githubusercontent.com/35857179/163672964-00ef5d62-0434-447d-ac01-3ebb780099b9.png" width="520px"/>
## Node Application
We provide a simple NodeJS Application for getting started.
```bash
git clone https://github.com/nocodb/nocodb-seed
cd nocodb-seed
npm install
npm start
```
# GUI
Access Dashboard using: [http://localhost:8080/dashboard](http://localhost:8080/dashboard)
@ -212,8 +130,6 @@ Access Dashboard using: [http://localhost:8080/dashboard](http://localhost:8080/
# Table of Contents
- [Quick try](#quick-try)
- [NPX](#npx)
- [Node Application](#node-application)
- [Docker](#docker)
- [Docker Compose](#docker-compose)
- [GUI](#gui)

31
docker-compose/mssql/docker-compose.yml

@ -1,31 +0,0 @@
version: "2.4"
services:
nocodb:
depends_on:
root_db:
condition: service_healthy
environment:
NC_DB: "mssql://root_db:1433?u=sa&p=Password123.&d=root_db"
image: "nocodb/nocodb:latest"
ports:
- "8080:8080"
restart: always
volumes:
- "nc_data:/usr/app/data"
root_db:
environment:
ACCEPT_EULA: "Y"
SA_PASSWORD: Password123.
healthcheck:
interval: 10s
retries: 10
start_period: 10s
test: "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P \"$$SA_PASSWORD\" -Q \"SELECT 1\" || exit 1"
timeout: 3s
image: "mcr.microsoft.com/mssql/server:2017-latest"
restart: always
volumes:
- "db_data:/var/opt/mssql"
volumes:
db_data: {}
nc_data: {}

27
markdown/readme/languages/chinese.md

@ -33,13 +33,6 @@
# 快速尝试
## NPX
如果你需要一个交互式的配置,你可以运行下面的命令。
```
npx create-nocodb-app
```
<img src="https://user-images.githubusercontent.com/35857179/163672964-00ef5d62-0434-447d-ac01-3ebb780099b9.png" width="520px"/>
@ -63,13 +56,6 @@ docker run -d --name nocodb \
-p 8080:8080 \
nocodb/nocodb:latest
# 如果使用 MySQL 的话
docker run -d --name nocodb-mysql \
-v "$(pwd)"/nocodb:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
# 如果使用 PostgreSQL 的话
docker run -d --name nocodb-postgres \
@ -79,14 +65,6 @@ docker run -d --name nocodb-postgres \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
# 如果使用 MSSQL 的话
docker run -d --name nocodb-mssql \
-v "$(pwd)"/nocodb:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="mssql://host.docker.internal:1433?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
> 你可以通过在 0.10.6 以上的版本中挂载 `/usr/app/data/` 来持久化数据,否则你的数据会在重新创建容器后完全丢失。
@ -98,13 +76,8 @@ nocodb/nocodb:latest
```bash
git clone https://github.com/nocodb/nocodb
# 如果使用 MySQL 的话
cd nocodb/docker-compose/mysql
# 如果使用 PostgreSQL 的话
cd nocodb/docker-compose/pg
# 如果使用 MSSQL 的话
cd nocodb/docker-compose/mssql
docker-compose up -d
```
> 你可以通过在 0.10.6 以上的版本中挂载 `/usr/app/data/` 来持久化数据,否则你的数据会在重新创建容器后完全丢失。

30
markdown/readme/languages/dutch.md

@ -49,20 +49,6 @@ docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest
docker run -d -p 8080:8080 --name nocodb -v "$(pwd)"/nocodb:/usr/app/data/ nocodb/nocodb:latest
```
### Gebruik van NPM
```
npx create-nocodb-app
```
### Git gebruiken
```
git clone https://github.com/nocodb/nocodb-seed
cd nocodb-seed
npm install
npm start
```
### GUI
@ -142,14 +128,6 @@ NOCODB vereist een database om metadata van spreadsheets weergaven en externe da
## Docker
#### Example MySQL
```
docker run -d -p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
#### Example Postgres
@ -162,12 +140,6 @@ docker run -d -p 8080:8080 \
#### Example SQL Server
```
docker run -d -p 8080:8080 \
-e NC_DB="mssql://host:port?u=user&p=password&d=database" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
## Docker Compose
@ -175,7 +147,7 @@ docker run -d -p 8080:8080 \
git clone https://github.com/nocodb/nocodb
cd nocodb
cd docker-compose
cd mysql or pg or mssql
cd pg
docker-compose up -d
```

26
markdown/readme/languages/french.md

@ -50,16 +50,6 @@ docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest
```
> Pour conserver les données, vous pouvez installer le volume dans `/usr/app/data/`.
### NPX
Vous pouvez exécuter la commande ci-dessous pour passer par la configuration interactive.
```
npx create-nocodb-app
```
<img src="https://user-images.githubusercontent.com/35857179/163672964-00ef5d62-0434-447d-ac01-3ebb780099b9.png" width="520px"/>
### En utilisant git
```
git clone https://github.com/nocodb/nocodb-seed
@ -147,13 +137,6 @@ NocoDB nécessite une base de données pour stocker les métadonnées des vues d
## Docker
#### Exemple MySQL
```
docker run -d -p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
#### Exemple Postgres
```
@ -163,20 +146,13 @@ docker run -d -p 8080:8080 \
nocodb/nocodb:latest
```
#### Exemple SQL Server
```
docker run -d -p 8080:8080 \
-e NC_DB="mssql://host:port?u=user&p=password&d=database" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
## Docker Compose
```
git clone https://github.com/nocodb/nocodb
cd nocodb
cd docker-compose
cd mysql or pg or mssql
cd pg
docker-compose up -d
```

32
markdown/readme/languages/german.md

@ -55,20 +55,6 @@ docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest
npm install create-nocodb-app
```
### Verwenden von NPX
```
npx create-nocodb-app
```
### Verwenden von Git
```
git clone https://github.com/nocodb/nocodb-seed
cd nocodb-seed
npm install
npm start
```
### GUI
@ -151,14 +137,6 @@ NocoDB erfordert eine Datenbank, um Metadaten von Tabellenansichten und externen
## Docker
#### Beispiel MySQL / MariaDB
```
docker run -d -p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
#### Beispiel PostgreSQL
@ -169,14 +147,6 @@ docker run -d -p 8080:8080 \
nocodb/nocodb:latest
```
#### Beispiel MS SQL Server
```
docker run -d -p 8080:8080 \
-e NC_DB="mssql://host:port?u=user&p=password&d=database" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
## Docker Compose
@ -184,7 +154,7 @@ docker run -d -p 8080:8080 \
git clone https://github.com/nocodb/nocodb
cd nocodb
cd docker-compose
cd mysql or pg or mssql
cd pg
docker-compose up -d
```

19
markdown/readme/languages/indonesian.md

@ -66,13 +66,6 @@ docker run -d --name nocodb \
-p 8080:8080 \
nocodb/nocodb:latest
# for MySQL
docker run -d --name nocodb-mysql \
-v "$(pwd)"/nocodb:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
# for PostgreSQL
docker run -d --name nocodb-postgres \
@ -82,14 +75,6 @@ docker run -d --name nocodb-postgres \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
# for MSSQL
docker run -d --name nocodb-mssql \
-v "$(pwd)"/nocodb:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="mssql://host.docker.internal:1433?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
> Untuk menyimpan data di dalam Docker, Anda dapat melakukan mount volume di direktori /usr/app/data/ mulai dari versi 0.10.6. Jika tidak, data Anda akan hilang setelah mengulang pembuatan kontainer.
@ -141,12 +126,8 @@ Kami menyediakan berbagai file docker-compose.yml di [bawah direktori](https://g
```bash
git clone https://github.com/nocodb/nocodb
# for MySQL
cd nocodb/docker-compose/mysql
# for PostgreSQL
cd nocodb/docker-compose/pg
# for MSSQL
cd nocodb/docker-compose/mssql
docker-compose up -d
```

32
markdown/readme/languages/italian.md

@ -49,20 +49,6 @@ docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest
docker run -d -p 8080:8080 --name nocodb -v "$(pwd)"/nocodb:/usr/app/data/ nocodb/nocodb:latest
```
### Con NPM
```
npx create-nocodb-app
```
### Con git
```
git clone https://github.com/nocodb/nocodb-seed
cd nocodb-seed
npm install
npm start
```
### GUI
@ -146,14 +132,6 @@ NOCODB richiede un database per memorizzare i metadati delle viste dei fogli di
## Docker
#### Esempio con MySQL
```
docker run -d -p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
#### Esempio con Postgres
@ -164,14 +142,6 @@ docker run -d -p 8080:8080 \
nocodb/nocodb:latest
```
#### Esempio con SQL Server
```
docker run -d -p 8080:8080 \
-e NC_DB="mssql://host:port?u=user&p=password&d=database" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
## Docker Compose
@ -179,7 +149,7 @@ docker run -d -p 8080:8080 \
git clone https://github.com/nocodb/nocodb
cd nocodb
cd docker-compose
cd mysql or pg or mssql
cd pg
docker-compose up -d
```

32
markdown/readme/languages/japanese.md

@ -49,20 +49,6 @@ docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest
docker run -d -p 8080:8080 --name nocodb -v "$(pwd)"/nocodb:/usr/app/data/ nocodb/nocodb:latest
```
### NPM を使用して初期化を行う
```
npx create-nocodb-app
```
### git を使う
```
git clone https://github.com/nocodb/nocodb-seed
cd nocodb-seed
npm install
npm start
```
### GUI
@ -145,14 +131,6 @@ NoCodb には、スプレッドシートビューと外部データベースの
## Docker
#### MySQLの例
```
docker run -d -p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
#### Postgresの例
@ -163,14 +141,6 @@ docker run -d -p 8080:8080 \
nocodb/nocodb:latest
```
#### SQL Serverの例
```
docker run -d -p 8080:8080 \
-e NC_DB="mssql://host:port?u=user&p=password&d=database" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
## Docker Compose
@ -178,7 +148,7 @@ docker run -d -p 8080:8080 \
git clone https://github.com/nocodb/nocodb
cd nocodb
cd docker-compose
cd mysql or pg or mssql
cd pg
docker-compose up -d
```

32
markdown/readme/languages/korean.md

@ -48,20 +48,6 @@ docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest
```
docker run -d -p 8080:8080 --name nocodb -v "$(pwd)"/nocodb:/usr/app/data/ nocodb/nocodb:
### npm 사용
```
npx create-nocodb-app
```
### Git 사용
```
git clone https://github.com/nocodb/nocodb-seed
cd nocodb-seed
npm install
npm start
```
### GUI
@ -145,14 +131,6 @@ NocoDB는 스프레드시트 뷰 메타데이터와 외부 데이터베이스
## Docker
#### MySQL 예제
```
docker run -d -p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
#### PostgreSQL 예제
@ -163,14 +141,6 @@ docker run -d -p 8080:8080 \
nocodb/nocodb:latest
```
#### SQL Server 예제
```
docker run -d -p 8080:8080 \
-e NC_DB="mssql://host:port?u=user&p=password&d=database" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
## Docker Compose
@ -178,7 +148,7 @@ docker run -d -p 8080:8080 \
git clone https://github.com/nocodb/nocodb
cd nocodb
cd docker-compose
cd mysql or pg or mssql
cd pg
docker-compose up -d
```

33
markdown/readme/languages/portuguese.md

@ -49,20 +49,6 @@ docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest
docker run -d -p 8080:8080 --name nocodb -v "$(pwd)"/nocodb:/usr/app/data/ nocodb/nocodb:latest
```
### Usando npx.
```
npx create-nocodb-app
```
### Usando o git.
```
git clone https://github.com/nocodb/nocodb-seed
cd nocodb-seed
npm install
npm start
```
### GUI
@ -144,15 +130,6 @@ O NOCODB requer um banco de dados para armazenar metadados de exibições de pla
## Docker
#### Example MySQL
```
docker run -d -p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
#### Example Postgres
```
@ -162,14 +139,6 @@ docker run -d -p 8080:8080 \
nocodb/nocodb:latest
```
#### Example SQL Server
```
docker run -d -p 8080:8080 \
-e NC_DB="mssql://host:port?u=user&p=password&d=database" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
## Docker Compose
@ -177,7 +146,7 @@ docker run -d -p 8080:8080 \
git clone https://github.com/nocodb/nocodb
cd nocodb
cd docker-compose
cd mysql or pg or mssql
cd pg
docker-compose up -d
```

32
markdown/readme/languages/russian.md

@ -49,20 +49,7 @@ docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest
docker run -d -p 8080:8080 --name nocodb -v "$(pwd)"/nocodb:/usr/app/data/ nocodb/nocodb:latest
```
### Используя NPM
```
npx create-nocodb-app
```
### Используя git.
```
git clone https://github.com/nocodb/nocodb-seed
cd nocodb-seed
npm install
npm start
```
### GUI
@ -146,15 +133,6 @@ NocoDB требует базу данных для хранения метада
## Docker
#### Пример MySQL
```
docker run -d -p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
#### Пример Postgres
```
@ -164,14 +142,6 @@ docker run -d -p 8080:8080 \
nocodb/nocodb:latest
```
#### Пример SQL Server
```
docker run -d -p 8080:8080 \
-e NC_DB="mssql://host:port?u=user&p=password&d=database" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
## Docker Compose
@ -179,7 +149,7 @@ docker run -d -p 8080:8080 \
git clone https://github.com/nocodb/nocodb
cd nocodb
cd docker-compose
cd mysql or pg or mssql
cd pg
docker-compose up -d
```

32
markdown/readme/languages/spanish.md

@ -49,20 +49,6 @@ docker run -d --name nocodb -p 8080:8080 nocodb/nocodb:latest
docker run -d -p 8080:8080 --name nocodb -v "$(pwd)"/nocodb:/usr/app/data/ nocodb/nocodb:latest
```
### Usando npx.
```
npx create-nocodb-app
```
### Usando git.
```
git clone https://github.com/nocodb/nocodb-seed
cd nocodb-seed
npm install
npm start
```
### GUI
@ -143,14 +129,6 @@ Nocodb requiere una base de datos para almacenar metadatos de vistas a las hojas
## Docker
#### Ejemplo MySQL
```
docker run -d -p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
#### Ejemplo Postgres
@ -161,14 +139,6 @@ docker run -d -p 8080:8080 \
nocodb/nocodb:latest
```
#### Ejemplo SQL Server
```
docker run -d -p 8080:8080 \
-e NC_DB="mssql://host:port?u=user&p=password&d=database" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
## Docker Compose
@ -176,7 +146,7 @@ docker run -d -p 8080:8080 \
git clone https://github.com/nocodb/nocodb
cd nocodb
cd docker-compose
cd mysql or pg or mssql
cd pg
docker-compose up -d
```

29
markdown/readme/languages/ukrainian.md

@ -76,13 +76,6 @@ docker run -d --name nocodb \
-p 8080:8080 \
nocodb/nocodb:latest
# для MySQL
docker run -d --name nocodb-mysql \
-v "$(pwd)"/nocodb:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="mysql2://host.docker.internal:3306?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
# для PostgreSQL
docker run -d --name nocodb-postgres \
@ -92,14 +85,6 @@ docker run -d --name nocodb-postgres \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
# для MSSQL
docker run -d --name nocodb-mssql \
-v "$(pwd)"/nocodb:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="mssql://host.docker.internal:1433?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
```
> Щоб зберегти дані в Docker, ви можете змонтувати том в /usr/app/data/ з версії 0.10.6. В іншому випадку ваші дані будуть втрачені після перестворення контейнера.
@ -153,12 +138,8 @@ iwr http://get.nocodb.com/win-arm64.exe -o Noco-win-arm64.exe
```bash
git clone https://github.com/nocodb/nocodb
# для MySQL
cd nocodb/docker-compose/mysql
# для PostgreSQL
cd nocodb/docker-compose/pg
# для MSSQL
cd nocodb/docker-compose/mssql
docker-compose up -d
```
@ -166,16 +147,6 @@ docker-compose up -d
> Якщо ви плануєте вводити будь-які спеціальні символи, вам може знадобитися змінити набір символів та порівняння при створенні бази даних. Будь ласка, перегляньте приклади для [MySQL Docker](https://github.com/nocodb/nocodb/issues/1340#issuecomment-1049481043).
## NPX
Ви можете запустити нижченаведену команду, якщо вам потрібна інтерактивна конфігурація.
```
npx create-nocodb-app
```
<img src="https://user-images.githubusercontent.com/35857179/163672964-00ef5d62-0434-447d-ac01-3ebb780099b9.png" width="520px"/>
## Node Application
Для початку ви можете використати простий Node.js застосунок.

6
packages/nc-gui/assets/nc-icons/chevron-up-down.svg

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" class="flex-none">
<path d="M12 6L8 2L4 6" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"
class="up" />
<path d=" M4 10L8 14L12 10" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" class="down" />
</svg>

After

Width:  |  Height:  |  Size: 410 B

2
packages/nc-gui/assets/nc-icons/maximize-all.svg

@ -2,4 +2,4 @@
<path
d="M5.33333 2H3.33333C2.97971 2 2.64057 2.14048 2.39052 2.39052C2.14048 2.64057 2 2.97971 2 3.33333V5.33333M14 5.33333V3.33333C14 2.97971 13.8595 2.64057 13.6095 2.39052C13.3594 2.14048 13.0203 2 12.6667 2H10.6667M10.6667 14H12.6667C13.0203 14 13.3594 13.8595 13.6095 13.6095C13.8595 13.3594 14 13.0203 14 12.6667V10.6667M2 10.6667V12.6667C2 13.0203 2.14048 13.3594 2.39052 13.6095C2.64057 13.8595 2.97971 14 3.33333 14H5.33333"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 652 B

After

Width:  |  Height:  |  Size: 653 B

8
packages/nc-gui/assets/nc-icons/minimize-2.svg

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="minimize-2">
<path id="Vector" d="M2.66666 9.3335H6.66666V13.3335" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M2 14.0002L6.66667 9.3335" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M13.3333 6.6665H9.33334V2.6665" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_4" d="M9.33334 6.66667L14 2" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 709 B

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

@ -7,4 +7,4 @@
stroke-linejoin="round" />
<path d="M9.3335 6.66667L14.0002 2" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 682 B

After

Width:  |  Height:  |  Size: 683 B

9
packages/nc-gui/assets/nc-icons/refresh.svg

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M0.666504 13.333V9.33301H4.6665" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M15.3335 2.66699V6.66699H11.3335" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M2.33984 6.00038C2.67795 5.0449 3.25259 4.19064 4.01015 3.51732C4.7677 2.844 5.68348 2.37355 6.67203 2.14988C7.66058 1.92621 8.68967 1.9566 9.6633 2.23823C10.6369 2.51985 11.5233 3.04352 12.2398 3.76038L15.3332 6.66704M0.666504 9.33371L3.75984 12.2404C4.47634 12.9572 5.36275 13.4809 6.33638 13.7625C7.31 14.0441 8.3391 14.0745 9.32765 13.8509C10.3162 13.6272 11.232 13.1568 11.9895 12.4834C12.7471 11.8101 13.3217 10.9559 13.6598 10.0004"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 965 B

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

@ -2,6 +2,20 @@
@import '@vue-flow/core/dist/style.css';
@import '@vue-flow/core/dist/theme-default.css';
@layer utilities {
@variants responsive {
/* Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
}
html {
overflow: hidden;
}
@ -60,6 +74,55 @@ main {
@apply !rounded-lg !py-2 !px-3 mb-1;
}
.nc-input-sm {
@apply !rounded-lg !py-1 !px-3;
}
.nc-input-shadow {
&.ant-input {
&:not(:hover):not(:focus):not(:disabled) {
@apply shadow-default border-gray-200;
}
&:hover:not(:focus):not(:disabled) {
@apply border-gray-200 shadow-hover;
}
&:focus {
@apply shadow-selected ring-0;
}
}
}
.ant-form-item-explain {
@apply !min-h-5;
.ant-form-item-explain-error {
@apply text-sm;
&:first-child {
@apply mt-1;
}
}
}
.ant-form-item-has-error :not(.ant-input-affix-wrapper-disabled):not(.ant-input-affix-wrapper-borderless).ant-input-affix-wrapper,
.ant-form-item-has-error
:not(.ant-input-affix-wrapper-disabled):not(.ant-input-affix-wrapper-borderless).ant-input-affix-wrapper:hover,
.ant-form-item-has-error :not(.ant-input-disabled):not(.ant-input-borderless).ant-input,
.ant-form-item-has-error :not(.ant-input-disabled):not(.ant-input-borderless).ant-input:hover,
.ant-form-item-has-error
:not(.ant-input-number-affix-wrapper-disabled):not(.ant-input-number-affix-wrapper-borderless).ant-input-number-affix-wrapper,
.ant-form-item-has-error
:not(.ant-input-number-affix-wrapper-disabled):not(
.ant-input-number-affix-wrapper-borderless
).ant-input-number-affix-wrapper:hover {
border-color: var(--ant-error-color) !important;
}
ant-form-item-explain-error {
&:first-child {
@apply mt-2;
}
}
.mobile {
.nc-scrollbar-md,
.nc-scrollbar-lg,
@ -266,6 +329,21 @@ a {
}
}
.ant-form-item:not(.ant-form-item-has-error)
.nc-select-shadow.ant-select-focused:not(.ant-select-disabled).ant-select:not(.ant-select-customize-input)
.ant-select-selector {
@apply !shadow-selected;
}
.ant-form-item.ant-form-item-has-error
.nc-select-shadow.ant-select:not(.ant-select-disabled):not(.ant-select-customize-input).ant-select-focused
.ant-select-selector,
.ant-form-item.ant-form-item-has-error
.nc-select-shadow.ant-select:not(.ant-select-disabled):not(.ant-select-customize-input).ant-select-open
.ant-select-selector {
@apply !shadow-error;
}
// select dropdown border style
.ant-select-dropdown {
@apply border-1 border-gray-200 rounded-lg;

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

@ -94,7 +94,7 @@ onMounted(() => {
<template>
<div
v-if="isForm && !isEditColumn && !hidePrefix"
v-if="isForm && !isEditColumn && editEnabled && !hidePrefix"
class="nc-currency-code h-full !bg-gray-100 border-r border-gray-200 px-3 mr-1 flex items-center"
>
<span>
@ -102,7 +102,7 @@ onMounted(() => {
</span>
</div>
<input
v-if="(!readOnly && editEnabled) || (isForm && !isEditColumn)"
v-if="(!readOnly && editEnabled) || (isForm && !isEditColumn && editEnabled)"
:ref="focus"
v-model="vModel"
type="number"

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

@ -342,7 +342,6 @@ function handleSelectDate(value?: dayjs.Dayjs) {
v-model:page-date="tempDate"
:is-open="isOpen"
:selected-date="localState"
:is-monday-first="false"
type="date"
size="medium"
@update:selected-date="handleSelectDate"

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

@ -1,12 +1,14 @@
<script setup lang="ts">
import NcModal from '../nc/Modal.vue'
type ModelValueType = string | Record<string, any> | undefined | null
interface Props {
modelValue: string | Record<string, any> | undefined
modelValue: ModelValueType
}
interface Emits {
(event: 'update:modelValue', model: string): void
(event: 'update:modelValue', model: string | null): void
}
const props = defineProps<Props>()
@ -27,7 +29,7 @@ const readOnly = inject(ReadonlyInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits)
const localValueState = ref<string | undefined>()
const localValueState = ref<string | undefined | null>()
const error = ref<string | undefined>()
@ -37,13 +39,17 @@ const isExpanded = ref(false)
const rowHeight = inject(RowHeightInj, ref(undefined))
const localValue = computed<string | Record<string, any> | undefined>({
const formatValue = (val: ModelValueType) => {
return !val || val === 'null' ? null : val
}
const localValue = computed<ModelValueType>({
get: () => localValueState.value,
set: (val: undefined | string | Record<string, any>) => {
localValueState.value = typeof val === 'object' ? JSON.stringify(val, null, 2) : val
set: (val: ModelValueType) => {
localValueState.value = formatValue(val) === null ? null : typeof val === 'object' ? JSON.stringify(val, null, 2) : val
/** if form and not expanded then sync directly */
if (isForm.value && !isExpanded.value) {
vModel.value = val
vModel.value = formatValue(val) === null ? null : val
}
},
})
@ -72,14 +78,15 @@ const onSave = () => {
editEnabled.value = false
vModel.value = localValue ? formatJson(localValue.value as string) : localValue
vModel.value = formatValue(localValue.value) === null ? null : formatJson(localValue.value as string)
}
const setLocalValue = (val: any) => {
try {
localValue.value = typeof val === 'string' ? JSON.stringify(JSON.parse(val), null, 2) : val
localValue.value =
formatValue(localValue.value) === null ? null : typeof val === 'string' ? JSON.stringify(JSON.parse(val), null, 2) : val
} catch (e) {
localValue.value = val
localValue.value = formatValue(localValue.value) === null ? null : val
}
}
@ -97,7 +104,7 @@ watch([localValue, editEnabled], () => {
error.value = undefined
} catch (e: any) {
if (localValue.value === undefined) return
if (localValue.value === undefined || localValue.value === null) return
error.value = e
}

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

@ -55,9 +55,7 @@ const searchVal = ref<string | null>()
const { $api } = useNuxtApp()
const { getMeta } = useMetas()
const { isUIAllowed } = useRoles()
const { isUIAllowed, isMetaReadOnly } = useRoles()
const { isPg, isMysql } = useBase()
@ -284,8 +282,8 @@ async function addIfMissingAndSave() {
activeOptCreateInProgress.value--
if (!activeOptCreateInProgress.value) {
await getMeta(column.value.fk_model_id!, true)
vModel.value = [...vModel.value]
// await getMeta(column.value.fk_model_id!, true)
tempSelectedOptsState.splice(0, tempSelectedOptsState.length)
}
} else {
@ -522,7 +520,9 @@ const onFocus = () => {
</a-select-option>
<a-select-option
v-if="searchVal && isOptionMissing && !isPublic && !disableOptionCreation && isUIAllowed('fieldEdit')"
v-if="
!isMetaReadOnly && searchVal && isOptionMissing && !isPublic && !disableOptionCreation && isUIAllowed('fieldEdit')
"
:key="searchVal"
:value="searchVal"
>

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

@ -217,7 +217,7 @@ const onFocusWrapper = () => {
if (props.syncValueChange) {
watch([vModel, editor], () => {
setEditorContent(vModel.value)
setEditorContent(isFormField.value ? (vModel.value || '')?.replace(/(<br \/>)+$/g, '') : vModel.value)
})
}

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

@ -49,7 +49,7 @@ const searchVal = ref()
const { getMeta } = useMetas()
const { isUIAllowed } = useRoles()
const { isUIAllowed, isMetaReadOnly } = useRoles()
const { isPg, isMysql } = useBase()
@ -59,7 +59,9 @@ const tempSelectedOptState = ref<string>()
const isFocusing = ref(false)
const isNewOptionCreateEnabled = computed(() => !isPublic.value && !disableOptionCreation && isUIAllowed('fieldEdit'))
const isNewOptionCreateEnabled = computed(
() => !isPublic.value && !disableOptionCreation && isUIAllowed('fieldEdit') && !isMetaReadOnly.value,
)
const options = computed<(SelectOptionType & { value: string })[]>(() => {
if (column?.value.colOptions) {

28
packages/nc-gui/components/cell/attachment/RenameFile.vue

@ -46,28 +46,24 @@ onMounted(() => {
</script>
<template>
<GeneralModal v-model:visible="visible" class="nc-attachment-rename-modal !w-[30rem]">
<div class="flex flex-col items-center justify-center h-full p-8">
<div class="text-lg font-semibold self-start mb-4">{{ $t('title.renameFile') }}</div>
<GeneralModal v-model:visible="visible" class="nc-attachment-rename-modal" size="small">
<div class="flex flex-col items-center justify-center h-full p-6">
<div class="text-lg font-semibold self-start mb-5">{{ $t('title.renameFile') }}</div>
<a-form class="w-full h-full" no-style :model="form" @finish="renameFile(form.title)">
<a-form-item class="w-full" name="title" :rules="rules.title">
<a-input ref="inputEl" v-model:value="form.title" class="w-full" :placeholder="$t('general.rename')" />
<a-form-item class="w-full !mb-0" name="title" :rules="rules.title">
<a-input
ref="inputEl"
v-model:value="form.title"
class="w-full nc-input-sm nc-input-shadow"
:placeholder="$t('general.rename')"
/>
</a-form-item>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton key="back" html-type="back" type="secondary">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" html-type="submit" type="primary">{{ $t('general.confirm') }}</NcButton>
<NcButton key="back" html-type="back" size="small" type="secondary">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" html-type="submit" size="small" type="primary">{{ $t('general.confirm') }}</NcButton>
</div>
</a-form>
</div>
</GeneralModal>
</template>
<style scoped lang="scss">
.nc-attachment-rename-modal {
.ant-input-affix-wrapper,
.ant-input {
@apply !appearance-none my-1 border-1 border-solid border-primary border-opacity-50 rounded;
}
}
</style>

4
packages/nc-gui/components/cell/attachment/index.vue

@ -270,7 +270,7 @@ const handleFileDelete = (i: number) => {
'py-1': rowHeight === 1 && !isForm && !isExpandedForm,
'py-1.5': rowHeight !== 1 || isForm || isExpandedForm,
}"
class="nc-attachment-wrapper flex cursor-pointer w-full items-center flex-wrap gap-2 scrollbar-thin-dull overflow-hidden mt-0 items-start"
class="nc-attachment-wrapper flex cursor-pointer w-full items-center flex-wrap gap-2 nc-scrollbar-thin mt-0 items-start px-[1px]"
:style="{
maxHeight: isForm || isExpandedForm ? undefined : `max(100%, ${isGrid ? '22px' : '32px'})`,
}"
@ -318,7 +318,7 @@ const handleFileDelete = (i: number) => {
<IcOutlineInsertDriveFile v-else :class="{ 'h-13 w-13': isForm || isExpandedForm }" />
</div>
<a-tooltip v-if="isForm || isExpandedForm">
<a-tooltip v-if="!isReadonly && (isForm || isExpandedForm)">
<template #title> {{ $t('title.removeFile') }} </template>
<component
:is="iconMap.closeCircle"

8
packages/nc-gui/components/dashboard/TreeView/AddNewTableNode.vue

@ -112,11 +112,15 @@ function openTableCreateMagicDialog(sourceId?: string) {
close(1000)
}
}
const source = computed(() => {
return base.value?.sources?.[props.sourceIndex]
})
</script>
<template>
<div
v-if="isUIAllowed('tableCreate', { roles: baseRole })"
v-if="isUIAllowed('tableCreate', { roles: baseRole, source })"
class="group flex items-center gap-2 pl-2 pr-4.75 py-1 text-primary/70 hover:(text-primary/100) cursor-pointer select-none"
@click="emit('openTableCreateDialog')"
>
@ -191,7 +195,7 @@ function openTableCreateMagicDialog(sourceId?: string) {
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('excelImport', { roles: baseRole })"
v-if="isUIAllowed('excelImport', { roles: baseRole, source: base.sources[sourceIndex] })"
key="quick-import-excel"
@click="openQuickImportDialog('excel', base.sources[sourceIndex].id)"
>

16
packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue

@ -8,9 +8,11 @@ const props = defineProps<{
const source = toRef(props, 'source')
const base = toRef(props, 'base')
const { isUIAllowed } = useRoles()
const baseRole = inject(ProjectRoleInj)
const baseRole = computed(() => base.value.project_role || base.value.workspace_role)
const { $e } = useNuxtApp()
@ -68,7 +70,7 @@ function openQuickImportDialog(type: string) {
<template #expandIcon></template>
<NcMenuItem
v-if="isUIAllowed('airtableImport', { roles: baseRole })"
v-if="isUIAllowed('airtableImport', { roles: baseRole, source })"
key="quick-import-airtable"
@click="openAirtableImportDialog(source.base_id, source.id)"
>
@ -78,7 +80,11 @@ function openQuickImportDialog(type: string) {
</div>
</NcMenuItem>
<NcMenuItem v-if="isUIAllowed('csvImport', { roles: baseRole })" key="quick-import-csv" @click="openQuickImportDialog('csv')">
<NcMenuItem
v-if="isUIAllowed('csvImport', { roles: baseRole, source })"
key="quick-import-csv"
@click="openQuickImportDialog('csv')"
>
<div v-e="['c:import:csv']" class="flex gap-2 items-center">
<GeneralIcon icon="csv" class="w-4 group-hover:text-black" />
{{ $t('labels.csvFile') }}
@ -86,7 +92,7 @@ function openQuickImportDialog(type: string) {
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('jsonImport', { roles: baseRole })"
v-if="isUIAllowed('jsonImport', { roles: baseRole, source })"
key="quick-import-json"
@click="openQuickImportDialog('json')"
>
@ -97,7 +103,7 @@ function openQuickImportDialog(type: string) {
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('excelImport', { roles: baseRole })"
v-if="isUIAllowed('excelImport', { roles: baseRole, source })"
key="quick-import-excel"
@click="openQuickImportDialog('excel')"
>

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

@ -1,10 +1,11 @@
<script lang="ts" setup>
import type { ViewType } from 'nocodb-sdk'
import { type ViewType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk'
const props = defineProps<{
// Prop used to align the dropdown to the left in sidebar
alignLeftLevel: number | undefined
source: Source
}>()
const { $e } = useNuxtApp()
@ -37,6 +38,7 @@ async function onOpenModal({
copyViewId,
groupingFieldColumnId,
calendarRange,
coverImageColumnId,
}: {
title?: string
type: ViewTypes
@ -46,6 +48,7 @@ async function onOpenModal({
fk_from_column_id: string
fk_to_column_id: string | null // for ee only
}>
coverImageColumnId?: string
}) {
if (isViewListLoading.value) return
@ -69,6 +72,7 @@ async function onOpenModal({
'selectedViewId': copyViewId,
calendarRange,
groupingFieldColumnId,
coverImageColumnId,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()
@ -122,7 +126,7 @@ async function onOpenModal({
</div>
</NcMenuItem>
<NcMenuItem @click="onOpenModal({ type: ViewTypes.FORM })">
<NcMenuItem v-if="!source.is_schema_readonly" @click="onOpenModal({ type: ViewTypes.FORM })">
<div class="item" data-testid="sidebar-view-create-form">
<div class="item-inner">
<GeneralViewIcon :meta="{ type: ViewTypes.FORM }" />

138
packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -29,6 +29,8 @@ const { isMobileMode } = useGlobal()
const { api } = useApi()
const { auditLogsQuery, auditCurrentPage } = storeToRefs(useWorkspace())
const { createProject: _createProject, updateProject, getProjectMetaInfo, loadProject } = basesStore
const { bases } = storeToRefs(basesStore)
@ -69,7 +71,7 @@ const { t } = useI18n()
const input = ref<HTMLInputElement>()
const baseRole = inject(ProjectRoleInj)
const baseRole = computed(() => base.value.project_role || base.value.workspace_role)
const { activeProjectId } = storeToRefs(useBases())
@ -100,9 +102,9 @@ const baseViewOpen = computed(() => {
return routeNameAfterProjectView.split('-').length === 2 || routeNameAfterProjectView.split('-').length === 1
})
const showBaseOption = computed(() => {
return ['airtableImport', 'csvImport', 'jsonImport', 'excelImport'].some((permission) => isUIAllowed(permission))
})
const showBaseOption = (source: SourceType) => {
return ['airtableImport', 'csvImport', 'jsonImport', 'excelImport'].some((permission) => isUIAllowed(permission, { source }))
}
const enableEditMode = () => {
editMode.value = true
@ -444,6 +446,40 @@ const onTableIdCopy = async () => {
message.error(e.message)
}
}
const getSource = (sourceId: string) => {
return base.value.sources?.find((s) => s.id === sourceId)
}
async function openAudit(source: SourceType) {
$e('c:project:audit')
auditCurrentPage.value = 1
auditLogsQuery.value = {
...auditLogsQuery.value,
orderBy: {
created_at: 'desc',
user: undefined,
},
}
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgProjectAudit'), {
'modelValue': isOpen,
'sourceId': source!.id,
'onUpdate:modelValue': () => closeDialog(),
'baseId': base.value!.id,
'bordered': true,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
</script>
<template>
@ -577,6 +613,17 @@ const onTableIdCopy = async () => {
</div>
</NcMenuItem>
<!-- Audit -->
<NcMenuItem
v-if="isUIAllowed('baseAuditList') && base?.sources?.[0]?.enabled"
key="audit"
data-testid="nc-sidebar-base-audit"
@click="openAudit(base?.sources?.[0])"
>
<GeneralIcon icon="audit" class="group-hover:text-black" />
{{ $t('title.audit') }}
</NcMenuItem>
<!-- Swagger: Rest APIs -->
<NcMenuItem
v-if="isUIAllowed('apiDocs')"
@ -596,7 +643,7 @@ const onTableIdCopy = async () => {
</NcMenuItem>
</template>
<template v-if="base?.sources?.[0]?.enabled && showBaseOption">
<template v-if="base?.sources?.[0]?.enabled && showBaseOption(base?.sources?.[0])">
<NcDivider />
<DashboardTreeViewBaseOptions v-model:base="base" :source="base.sources[0]" />
</template>
@ -631,7 +678,7 @@ const onTableIdCopy = async () => {
</NcDropdown>
<NcButton
v-if="isUIAllowed('tableCreate', { roles: baseRole })"
v-if="isUIAllowed('tableCreate', { roles: baseRole, source: base?.sources?.[0] })"
v-e="['c:base:create-table']"
:disabled="!base?.sources?.[0]?.enabled"
class="nc-sidebar-node-btn"
@ -652,7 +699,7 @@ const onTableIdCopy = async () => {
v-e="['c:base:expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand !xs:opacity-100"
class="nc-sidebar-node-btn nc-sidebar-expand !xs:opacity-100 !mr-0 mt-0.5"
:class="{
'!opacity-100': isOptionsOpen,
}"
@ -701,7 +748,7 @@ const onTableIdCopy = async () => {
v-e="['c:external:base:expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand !xs:opacity-100"
class="nc-sidebar-node-btn nc-sidebar-expand !xs:opacity-100 !mr-0 mt-0.5"
:class="{ '!opacity-100 !inline-block': isBasesOptionsOpen[source!.id!] }"
>
<GeneralIcon
@ -727,17 +774,27 @@ const onTableIdCopy = async () => {
class="source-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full"
@contextmenu="setMenuContext('source', source)"
>
<GeneralBaseLogo
class="flex-none min-w-4 !xs:(min-w-4.25 w-4.25 text-sm) !text-gray-600 !group-hover:text-gray-800"
/>
<NcTooltip
:tooltip-style="{ 'min-width': 'max-content' }"
:overlay-inner-style="{ 'min-width': 'max-content' }"
:mouse-leave-delay="0.3"
placement="topLeft"
trigger="hover"
class="flex items-center"
>
<template #title>
<component :is="getSourceTooltip(source)" />
</template>
<GeneralBaseLogo
:color="getSourceIconColor(source)"
class="flex-none min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)"
/>
</NcTooltip>
<input
v-if="source.id && sourceRenameHelpers[source.id]?.editMode"
ref="input"
v-model="sourceRenameHelpers[source.id].tempTitle"
class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent flex-1 mr-4"
:class="
activeProjectId === base.id && baseViewOpen ? '!text-brand-600 !font-semibold' : '!text-gray-700'
"
class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent flex-1 mr-4 !text-gray-700"
:data-source-rename-input-id="source.id"
@click.stop
@keydown.enter.stop.prevent
@ -747,13 +804,8 @@ const onTableIdCopy = async () => {
/>
<NcTooltip
v-else
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none"
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none text-gray-700"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
:class="
activeProjectId === base.id && baseViewOpen && !isMobileMode
? 'text-brand-600 font-semibold'
: 'text-gray-700'
"
show-on-truncate-only
>
<template #title> {{ source.alias || '' }}</template>
@ -761,17 +813,6 @@ const onTableIdCopy = async () => {
{{ source.alias || '' }}
</span>
</NcTooltip>
<NcTooltip class="xs:(hidden) flex items-center mr-1">
<template #title>{{ $t('objects.externalDb') }}</template>
<GeneralIcon
icon="info"
class="flex-none text-gray-400 hover:text-gray-700 nc-sidebar-node-btn"
:class="{
'!hidden': !isBasesOptionsOpen[source!.id!],
}"
/>
</NcTooltip>
</div>
<div class="flex flex-row items-center gap-x-0.25">
<NcDropdown
@ -817,13 +858,17 @@ const onTableIdCopy = async () => {
</div>
</NcMenuItem>
<DashboardTreeViewBaseOptions v-if="showBaseOption" v-model:base="base" :source="source" />
<DashboardTreeViewBaseOptions
v-if="showBaseOption(source)"
v-model:base="base"
:source="source"
/>
</NcMenu>
</template>
</NcDropdown>
<NcButton
v-if="isUIAllowed('tableCreate', { roles: baseRole })"
v-if="isUIAllowed('tableCreate', { roles: baseRole, source })"
v-e="['c:source:add-table']"
type="text"
size="xxsmall"
@ -865,7 +910,7 @@ const onTableIdCopy = async () => {
<template v-else-if="contextMenuTarget.type === 'table'">
<NcTooltip>
<template #title> {{ $t('labels.clickToCopyTableID') }} </template>
<template #title> {{ $t('labels.clickToCopyTableID') }}</template>
<div
class="flex items-center justify-between p-2 mx-1.5 rounded-md cursor-pointer hover:bg-gray-100 group"
@click.stop="onTableIdCopy"
@ -884,9 +929,17 @@ const onTableIdCopy = async () => {
</div>
</NcTooltip>
<template v-if="isUIAllowed('tableRename') || isUIAllowed('tableDelete')">
<template
v-if="
isUIAllowed('tableRename', { source: getSource(contextMenuTarget.value?.source_id) }) ||
isUIAllowed('tableDelete', { source: getSource(contextMenuTarget.value?.source_id) })
"
>
<NcDivider />
<NcMenuItem v-if="isUIAllowed('tableRename')" @click="openRenameTableDialog(contextMenuTarget.value, true)">
<NcMenuItem
v-if="isUIAllowed('tableRename', { source: getSource(contextMenuTarget.value?.source_id) })"
@click="openRenameTableDialog(contextMenuTarget.value, true)"
>
<div v-e="['c:table:rename']" class="nc-base-option-item flex gap-2 items-center">
<GeneralIcon icon="rename" class="text-gray-700" />
{{ $t('general.rename') }} {{ $t('objects.table') }}
@ -894,7 +947,10 @@ const onTableIdCopy = async () => {
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('tableDuplicate') && (contextMenuBase?.is_meta || contextMenuBase?.is_local)"
v-if="
isUIAllowed('tableDuplicate', { source: getSource(contextMenuTarget.value?.source_id) }) &&
(contextMenuBase?.is_meta || contextMenuBase?.is_local)
"
@click="duplicateTable(contextMenuTarget.value)"
>
<div v-e="['c:table:duplicate']" class="nc-base-option-item flex gap-2 items-center">
@ -903,7 +959,11 @@ const onTableIdCopy = async () => {
</div>
</NcMenuItem>
<NcDivider />
<NcMenuItem v-if="isUIAllowed('table-delete')" class="!hover:bg-red-50" @click="tableDelete">
<NcMenuItem
v-if="isUIAllowed('tableDelete', { source: getSource(contextMenuTarget.value?.source_id) })"
class="!hover:bg-red-50"
@click="tableDelete"
>
<div class="nc-base-option-item flex gap-2 items-center text-red-600">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('objects.table') }}

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

@ -213,6 +213,10 @@ const refreshViews = async () => {
await nextTick()
isExpanded.value = true
}
const source = computed(() => {
return base.value?.sources?.[sourceIndex.value]
})
</script>
<template>
@ -331,15 +335,16 @@ const refreshViews = async () => {
<template
v-if="
!isSharedBase &&
(isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
(isUIAllowed('tableRename', { roles: baseRole, source }) ||
isUIAllowed('tableDelete', { roles: baseRole, source }))
"
>
<NcDivider />
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: baseRole })"
v-if="isUIAllowed('tableRename', { roles: baseRole, source })"
:data-testid="`sidebar-table-rename-${table.title}`"
class="nc-table-rename"
@click="openRenameTableDialog(table, base.sources[sourceIndex].id)"
@click="openRenameTableDialog(table, source.id)"
>
<div v-e="['c:table:rename']" class="flex gap-2 items-center">
<GeneralIcon icon="rename" class="text-gray-700" />
@ -349,9 +354,11 @@ const refreshViews = async () => {
<NcMenuItem
v-if="
isUIAllowed('tableDuplicate') &&
isUIAllowed('tableDuplicate', {
source,
}) &&
base.sources?.[sourceIndex] &&
(base.sources[sourceIndex].is_meta || base.sources[sourceIndex].is_local)
(source.is_meta || source.is_local)
"
:data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)"
@ -364,7 +371,7 @@ const refreshViews = async () => {
<NcDivider />
<NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: baseRole })"
v-if="isUIAllowed('tableDelete', { roles: baseRole, source })"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50 nc-table-delete"
@click="deleteTable"

12
packages/nc-gui/components/dashboard/TreeView/ViewsList.vue

@ -13,6 +13,7 @@ interface Emits {
title?: string
copyViewId?: string
groupingFieldColumnId?: string
coverImageColumnId?: string
},
): void
@ -74,13 +75,14 @@ function markItem(id: string) {
}, 300)
}
const source = computed(() => base.value?.sources?.find((b) => b.id === table.value.source_id))
const isDefaultSource = computed(() => {
if (base.value?.sources?.length === 1) return true
const source = base.value?.sources?.find((b) => b.id === table.value.source_id)
if (!source) return false
if (!source.value) return false
return isDefaultBase(source)
return isDefaultBase(source.value)
})
/** validate view title */
@ -337,6 +339,7 @@ function onOpenModal({
copyViewId,
groupingFieldColumnId,
calendarRange,
coverImageColumnId,
}: {
title?: string
type: ViewTypes
@ -346,6 +349,7 @@ function onOpenModal({
fk_from_column_id: string
fk_to_column_id: string | null // for ee only
}>
coverImageColumnId?: string
}) {
const isOpen = ref(true)
@ -358,6 +362,7 @@ function onOpenModal({
groupingFieldColumnId,
'views': views,
calendarRange,
coverImageColumnId,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()
@ -402,6 +407,7 @@ function onOpenModal({
'!pl-13.3 !xs:(pl-13.5)': isDefaultSource,
'!pl-18.6 !xs:(pl-20)': !isDefaultSource,
}"
:source="source"
>
<div
:class="{

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

@ -21,7 +21,10 @@ interface Emits {
(event: 'delete', view: ViewType): void
(event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string; groupingFieldColumnId?: string }): void
(
event: 'openModal',
data: { type: ViewTypes; title?: string; copyViewId?: string; groupingFieldColumnId?: string; coverImageColumnId?: string },
): void
}
const props = defineProps<Props>()

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

@ -23,7 +23,7 @@ const baseCreateDlg = ref(false)
const baseStore = useBase()
const { isSharedBase } = storeToRefs(baseStore)
const { isSharedBase, base } = storeToRefs(baseStore)
const { activeTable: _activeTable } = storeToRefs(useTablesStore())
@ -100,7 +100,8 @@ const duplicateTable = async (table: TableType) => {
const isCreateTableAllowed = computed(
() =>
isUIAllowed('tableCreate') &&
base.value?.sources?.[0] &&
isUIAllowed('tableCreate', { source: base.value?.sources?.[0] }) &&
route.value.name !== 'index' &&
route.value.name !== 'index-index' &&
route.value.name !== 'index-index-create' &&
@ -248,9 +249,9 @@ watch(
ghost-class="ghost"
@change="onMove($event)"
>
<template #item="{ element: base }">
<div :key="base.id">
<ProjectWrapper :base-role="base.project_role" :base="base">
<template #item="{ element: baseItem }">
<div :key="baseItem.id">
<ProjectWrapper :base-role="baseItem.project_role" :base="baseItem">
<DashboardTreeViewProjectNode />
</ProjectWrapper>
</div>

10
packages/nc-gui/components/dashboard/settings/DataSources.vue

@ -260,7 +260,7 @@ const openedTab = ref('erd')
</script>
<template>
<div class="flex flex-col h-full">
<div class="flex flex-col h-full" data-testid="nc-settings-datasources-tab">
<div class="px-4 py-2 flex justify-between">
<a-breadcrumb separator=">" class="w-full cursor-pointer font-weight-bold">
<a-breadcrumb-item @click="activeSource = null">
@ -307,7 +307,7 @@ const openedTab = ref('erd')
<LazyDashboardSettingsBaseAudit :source-id="activeSource.id" />
</div>
</a-tab-pane>
<a-tab-pane v-if="!activeSource.is_meta && !activeSource.is_local" key="audit">
<a-tab-pane v-if="!activeSource.is_meta && !activeSource.is_local" key="edit">
<template #tab>
<div class="tab" data-testid="nc-connection-tab">
<div>{{ $t('labels.connectionDetails') }}</div>
@ -315,7 +315,7 @@ const openedTab = ref('erd')
</template>
<div class="p-6 mt-4 h-full overflow-auto">
<LazyDashboardSettingsDataSourcesEditBase
class="w-600px"
class="w-760px pr-5"
:source-id="activeSource.id"
@source-updated="loadBases(true)"
@close="activeSource = null"
@ -397,7 +397,7 @@ const openedTab = ref('erd')
<NcButton
v-if="!sources[0].is_meta && !sources[0].is_local"
size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
class="nc-action-btn nc-edit-base cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text"
@click.stop="baseAction(sources[0].id, DataSourcesSubTab.Edit)"
>
@ -446,7 +446,7 @@ const openedTab = ref('erd')
<NcButton
v-if="!source.is_meta && !source.is_local"
size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
class="nc-action-btn nc-delete-base cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text"
@click.stop="openDeleteBase(source)"
>

58
packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue

@ -55,6 +55,8 @@ const formState = ref<ProjectCreateForm>({
},
sslUse: SSLUsage.No,
extraParameters: [],
is_schema_readonly: true,
is_data_readonly: false,
})
const customFormState = ref<ProjectCreateForm>({
@ -68,9 +70,20 @@ const customFormState = ref<ProjectCreateForm>({
extraParameters: [],
})
const easterEgg = ref(false)
const easterEggCount = ref(0)
const onEasterEgg = () => {
easterEggCount.value += 1
if (easterEggCount.value >= 2) {
easterEgg.value = true
}
}
const clientTypes = computed(() => {
return _clientTypes.filter((type) => {
return ![ClientType.SNOWFLAKE, ClientType.DATABRICKS].includes(type.value)
return ![ClientType.SNOWFLAKE, ClientType.DATABRICKS, ...(easterEgg.value ? [] : [ClientType.MSSQL])].includes(type.value)
})
})
@ -244,6 +257,8 @@ const createSource = async () => {
config,
inflection_column: formState.value.inflection.inflectionColumn,
inflection_table: formState.value.inflection.inflectionTable,
is_schema_readonly: formState.value.is_schema_readonly,
is_data_readonly: formState.value.is_data_readonly,
})
$poller.subscribe(
@ -393,6 +408,26 @@ watch(
const toggleModal = (val: boolean) => {
vOpen.value = val
}
const allowMetaWrite = computed({
get: () => !formState.value.is_schema_readonly,
set: (v) => {
formState.value.is_schema_readonly = !v
// if schema write is allowed, data write should be allowed too
if (v) {
formState.value.is_data_readonly = false
}
$e('c:source:schema-write-toggle', { allowed: !v, edit: true })
},
})
const allowDataWrite = computed({
get: () => !formState.value.is_data_readonly,
set: (v) => {
formState.value.is_data_readonly = !v
$e('c:source:data-write-toggle', { allowed: !v })
},
})
</script>
<template>
@ -401,7 +436,7 @@ const toggleModal = (val: boolean) => {
:closable="!creatingSource"
:keyboard="!creatingSource"
:mask-closable="false"
size="medium"
:width="750"
@update:visible="toggleModal"
>
<div class="py-6 px-8">
@ -418,7 +453,7 @@ const toggleModal = (val: boolean) => {
name="external-base-create-form"
layout="horizontal"
no-style
:label-col="{ span: 8 }"
:label-col="{ span: 5 }"
>
<div
class="nc-scrollbar-md"
@ -508,6 +543,18 @@ const toggleModal = (val: boolean) => {
>
<a-input v-model:value="formState.dataSource.searchPath[0]" />
</a-form-item>
</template>
<DashboardSettingsDataSourcesSourceRestrictions
v-model:allowMetaWrite="allowMetaWrite"
v-model:allowDataWrite="allowDataWrite"
/>
<template
v-if="
formState.dataSource.client !== ClientType.SQLITE &&
formState.dataSource.client !== ClientType.DATABRICKS &&
formState.dataSource.client !== ClientType.SNOWFLAKE
"
>
<div class="flex items-right justify-end gap-2">
<!-- Use Connection URL -->
<NcButton type="ghost" size="small" class="nc-extdb-btn-import-url !rounded-md" @click.stop="importURLDlg = true">
@ -639,8 +686,8 @@ const toggleModal = (val: boolean) => {
v-model:value="formState.inflection.inflectionColumn"
dropdown-class-name="nc-dropdown-inflection-column-name"
>
<a-select-option v-for="tp in inflectionTypes" :key="tp" :value="tp"
><div class="flex items-center gap-2 justify-between">
<a-select-option v-for="tp in inflectionTypes" :key="tp" :value="tp">
<div class="flex items-center gap-2 justify-between">
<div>{{ tp }}</div>
<component
:is="iconMap.check"
@ -666,6 +713,7 @@ const toggleModal = (val: boolean) => {
<a-form-item class="flex justify-end !mt-5">
<div class="flex justify-end gap-2">
<div class="w-[15px] h-[15px] cursor-pointer" @dblclick="onEasterEgg"></div>
<NcButton
:type="testSuccess ? 'ghost' : 'primary'"
size="small"

57
packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue

@ -45,9 +45,20 @@ const { t } = useI18n()
const editingSource = ref(false)
const easterEgg = ref(false)
const easterEggCount = ref(0)
const onEasterEgg = () => {
easterEggCount.value += 1
if (easterEggCount.value >= 2) {
easterEgg.value = true
}
}
const clientTypes = computed(() => {
return _clientTypes.filter((type) => {
return ![ClientType.SNOWFLAKE, ClientType.DATABRICKS].includes(type.value)
return ![ClientType.SNOWFLAKE, ClientType.DATABRICKS, ...(easterEgg.value ? [] : [ClientType.MSSQL])].includes(type.value)
})
})
@ -60,6 +71,8 @@ const formState = ref<ProjectCreateForm>({
},
sslUse: SSLUsage.No,
extraParameters: [],
is_schema_readonly: true,
is_data_readonly: false,
})
const customFormState = ref<ProjectCreateForm>({
@ -71,6 +84,8 @@ const customFormState = ref<ProjectCreateForm>({
},
sslUse: SSLUsage.No,
extraParameters: [],
is_schema_readonly: true,
is_data_readonly: false,
})
const validators = computed(() => {
@ -228,6 +243,8 @@ const editBase = async () => {
config,
inflection_column: formState.value.inflection.inflectionColumn,
inflection_table: formState.value.inflection.inflectionTable,
is_schema_readonly: formState.value.is_schema_readonly,
is_data_readonly: formState.value.is_data_readonly,
})
$e('a:source:edit:extdb')
@ -339,6 +356,8 @@ onMounted(async () => {
},
extraParameters: tempParameters,
sslUse: SSLUsage.No,
is_schema_readonly: activeBase.is_schema_readonly,
is_data_readonly: activeBase.is_data_readonly,
}
updateSSLUse()
}
@ -356,13 +375,33 @@ watch(
immediate: true,
},
)
const allowMetaWrite = computed({
get: () => !formState.value.is_schema_readonly,
set: (v) => {
formState.value.is_schema_readonly = !v
// if schema write is allowed, data write should be allowed too
if (v) {
formState.value.is_data_readonly = false
}
$e('c:source:schema-write-toggle', { allowed: !v, edit: true })
},
})
const allowDataWrite = computed({
get: () => !formState.value.is_data_readonly,
set: (v) => {
formState.value.is_data_readonly = !v
$e('c:source:data-write-toggle', { allowed: !v, edit: true })
},
})
</script>
<template>
<div class="edit-source bg-white relative flex flex-col justify-start gap-2 w-full p-2">
<h1 class="prose-2xl font-bold self-start">{{ $t('activity.editSource') }}</h1>
<a-form ref="form" :model="formState" name="external-base-create-form" layout="horizontal" no-style :label-col="{ span: 8 }">
<a-form ref="form" :model="formState" name="external-base-create-form" layout="horizontal" no-style :label-col="{ span: 5 }">
<div
class="nc-scrollbar-md"
:style="{
@ -372,7 +411,6 @@ watch(
<a-form-item label="Source Name" v-bind="validateInfos.title">
<a-input v-model:value="formState.title" class="nc-extdb-proj-name" />
</a-form-item>
<a-form-item :label="$t('labels.dbType')" v-bind="validateInfos['dataSource.client']">
<a-select
v-model:value="formState.dataSource.client"
@ -524,6 +562,18 @@ watch(
>
<a-input v-model:value="formState.dataSource.searchPath[0]" />
</a-form-item>
</template>
<DashboardSettingsDataSourcesSourceRestrictions
v-model:allowMetaWrite="allowMetaWrite"
v-model:allowDataWrite="allowDataWrite"
/>
<template
v-if="
formState.dataSource.client !== ClientType.SQLITE &&
formState.dataSource.client !== ClientType.DATABRICKS &&
formState.dataSource.client !== ClientType.SNOWFLAKE
"
>
<!-- Use Connection URL -->
<div class="flex justify-end gap-2">
<NcButton size="small" type="ghost" class="nc-extdb-btn-import-url !rounded-md" @click.stop="importURLDlg = true">
@ -644,6 +694,7 @@ watch(
<a-form-item class="flex justify-end !mt-5">
<div class="flex justify-end gap-2">
<div class="w-[15px] h-[15px] cursor-pointer" @dblclick="onEasterEgg"></div>
<NcButton
:type="testSuccess ? 'ghost' : 'primary'"
size="small"

64
packages/nc-gui/components/dashboard/settings/data-sources/SourceRestrictions.vue

@ -0,0 +1,64 @@
<script setup lang="ts">
const props = defineProps<{
allowMetaWrite: boolean
allowDataWrite: boolean
}>()
const emits = defineEmits(['update:allowMetaWrite', 'update:allowDataWrite'])
const dataWrite = useVModel(props, 'allowDataWrite', emits)
const metaWrite = useVModel(props, 'allowMetaWrite', emits)
</script>
<template>
<a-form-item>
<template #help>
<span class="text-small">
{{ $t('tooltip.allowDataWrite') }}
</span>
</template>
<template #label>
<div class="flex gap-1 justify-end">
<span>
{{ $t('labels.allowDataWrite') }}
</span>
</div>
</template>
<div class="flex justify-start">
<NcTooltip :disabled="!metaWrite" placement="topLeft">
<template #title>
{{ $t('tooltip.dataWriteOptionDisabled') }}
</template>
<a-switch v-model:checked="dataWrite" :disabled="metaWrite" data-testid="nc-allow-data-write" size="small"></a-switch>
</NcTooltip>
</div>
</a-form-item>
<a-form-item>
<template #help>
<span class="text-small">
<span class="font-weight-medium" :class="{ 'nc-allow-meta-write-help': metaWrite }">
{{ $t('labels.notRecommended') }}:
</span>
{{ $t('tooltip.allowMetaWrite') }}
</span>
</template>
<template #label>
<div class="flex gap-1 justify-end">
<span>
{{ $t('labels.allowMetaWrite') }}
</span>
</div>
</template>
<a-switch v-model:checked="metaWrite" data-testid="nc-allow-meta-write" class="nc-allow-meta-write" size="small"></a-switch>
</a-form-item>
</template>
<style scoped>
.nc-allow-meta-write.ant-switch-checked {
background: #b33870;
}
.nc-allow-meta-write-help {
color: #b33870;
}
</style>

14
packages/nc-gui/components/dlg/ProjectAudit.vue

@ -1,15 +1,17 @@
<script lang="ts" setup>
const props = defineProps<{
workspaceId?: string
baseId: string
sourceId: string
modelValue: boolean
bordered?: boolean
}>()
const emit = defineEmits(['update:modelValue'])
const isOpen = useVModel(props, 'modelValue', emit)
const activeSourceId = computed(() => props.sourceId)
const { workspaceId, sourceId, bordered } = toRefs(props)
const { openedProject: base } = storeToRefs(useBases())
@ -45,9 +47,15 @@ onMounted(async () => {
</script>
<template>
<GeneralModal v-model:visible="isOpen" size="xl" class="!w-[70rem] !top-[5vh]">
<GeneralModal v-model:visible="isOpen" size="xl" class="!top-[5vh] lg:!max-w-[calc(100vw_-_64px)]" width="96.95rem">
<div class="p-6 h-full">
<DashboardSettingsBaseAudit v-if="!isLoading" :source-id="activeSourceId" :base-id="baseId" :show-all-columns="false" />
<WorkspaceAuditLogs
v-if="!isLoading"
:workspace-id="workspaceId"
:source-id="sourceId"
:base-id="baseId"
:bordered="bordered"
/>
</div>
</GeneralModal>
</template>

2
packages/nc-gui/components/dlg/ProjectDelete.vue

@ -51,7 +51,7 @@ const onDelete = async () => {
<template>
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.project')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="base" class="flex flex-row items-center py-2 px-2.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
<div v-if="base" class="flex flex-row items-center py-2 px-2.25 bg-gray-50 rounded-lg text-gray-700">
<GeneralProjectIcon :color="parseProp(base.meta).iconColor" :type="base.type" class="nc-view-icon w-6 h-6 mx-1" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"

11
packages/nc-gui/components/dlg/ProjectDuplicate.vue

@ -128,18 +128,17 @@ const isEaster = ref(false)
<GeneralModal
v-if="base"
v-model:visible="dialogShow"
:closable="!isLoading"
:mask-closable="!isLoading"
:keyboard="!isLoading"
class="!w-[30rem]"
wrap-class-name="nc-modal-base-duplicate"
>
<div>
<div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster">
<div class="font-medium text-lg text-gray-800 self-center" @dblclick="isEaster = !isEaster">
{{ $t('general.duplicate') }} {{ $t('objects.project') }}
</div>
<div class="mt-4">{{ $t('msg.warning.duplicateProject') }}</div>
<div class="mt-5">{{ $t('msg.warning.duplicateProject') }}</div>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>
@ -154,8 +153,10 @@ const isEaster = ref(false)
</div>
</div>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton v-if="!isLoading" key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" v-e="['a:base:duplicate']" :loading="isLoading" @click="_duplicate"
<NcButton v-if="!isLoading" key="back" type="secondary" size="small" @click="dialogShow = false">{{
$t('general.cancel')
}}</NcButton>
<NcButton key="submit" v-e="['a:base:duplicate']" size="small" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>
</div>

60
packages/nc-gui/components/dlg/TableCreate.vue

@ -121,29 +121,46 @@ onMounted(() => {
</script>
<template>
<NcModal v-model:visible="dialogShow" :header="$t('activity.createTable')" size="small" @keydown.esc="dialogShow = false">
<NcModal
v-model:visible="dialogShow"
:show-separator="false"
:header="$t('activity.createTable')"
size="small"
@keydown.esc="dialogShow = false"
>
<template #header>
<div class="flex flex-row items-center gap-x-2">
<GeneralIcon icon="table" class="!text-gray-600/75" />
<div class="flex flex-row items-center gap-x-2 text-base text-gray-800">
<GeneralIcon icon="table" class="!text-gray-600 w-5 h-5" />
{{ $t('activity.createTable') }}
</div>
</template>
<div class="flex flex-col mt-2">
<a-form :model="table" name="create-new-table-form" @keydown.enter="_createTable" @keydown.esc="dialogShow = false">
<a-form-item v-bind="validateInfos.title" :class="{ '!mb-1': isSnowflake(props.sourceId) }">
<a-input
ref="inputEl"
v-model:value="table.title"
class="nc-input-md"
hide-details
data-testid="create-table-title-input"
:placeholder="$t('msg.info.enterTableName')"
/>
</a-form-item>
<template v-if="isSnowflake(props.sourceId)">
<a-checkbox v-model:checked="table.is_hybrid" class="!flex flex-row items-center"> Hybrid Table </a-checkbox>
</template>
<div class="nc-table-advanced-options" :class="{ active: isAdvanceOptVisible }">
<div class="flex flex-col mt-1">
<a-form
:model="table"
name="create-new-table-form"
class="flex flex-col gap-5"
@keydown.enter="_createTable"
@keydown.esc="dialogShow = false"
>
<div>
<a-form-item
v-bind="validateInfos.title"
:class="{ '!mb-1': isSnowflake(props.sourceId), '!mb-0': !isSnowflake(props.sourceId) }"
>
<a-input
ref="inputEl"
v-model:value="table.title"
class="nc-input-sm nc-input-shadow"
hide-details
data-testid="create-table-title-input"
:placeholder="$t('msg.info.enterTableName')"
/>
</a-form-item>
<template v-if="isSnowflake(props.sourceId)">
<a-checkbox v-model:checked="table.is_hybrid" class="!flex flex-row items-center"> Hybrid Table </a-checkbox>
</template>
</div>
<div v-if="isAdvanceOptVisible" class="nc-table-advanced-options" :class="{ active: isAdvanceOptVisible }">
<div>
<div class="mb-1">
<!-- Add Default Columns -->
@ -171,12 +188,13 @@ onMounted(() => {
</a-row>
</div>
</div>
<div class="flex flex-row justify-end gap-x-2 mt-2">
<NcButton type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<div class="flex flex-row justify-end gap-x-2">
<NcButton type="secondary" size="small" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton
v-e="['a:table:create']"
type="primary"
size="small"
:disabled="validateInfos.title.validateStatus === 'error'"
:loading="creating"
@click="_createTable"

2
packages/nc-gui/components/dlg/TableDelete.vue

@ -110,7 +110,7 @@ const onDelete = async () => {
<template>
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.table')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="table" class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<div v-if="table" class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700">
<GeneralTableIcon :meta="table" class="nc-view-icon" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"

11
packages/nc-gui/components/dlg/TableDuplicate.vue

@ -129,7 +129,6 @@ const isEaster = ref(false)
<GeneralModal
v-model:visible="dialogShow"
:class="{ active: dialogShow }"
:closable="!isLoading"
:mask-closable="!isLoading"
:keyboard="!isLoading"
centered
@ -139,11 +138,11 @@ const isEaster = ref(false)
@keydown.esc="dialogShow = false"
>
<div>
<div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster">
<div class="font-medium text-lg text-gray-800 self-center" @dblclick="isEaster = !isEaster">
{{ $t('general.duplicate') }} {{ $t('objects.table') }}
</div>
<div class="mt-4">{{ $t('msg.warning.duplicateTable') }}</div>
<div class="mt-5">{{ $t('msg.warning.duplicateTable') }}</div>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>
@ -158,8 +157,10 @@ const isEaster = ref(false)
</div>
</div>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton v-if="!isLoading" key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" v-e="['a:table:duplicate']" type="primary" :loading="isLoading" @click="_duplicate"
<NcButton v-if="!isLoading" key="back" type="secondary" size="small" @click="dialogShow = false">{{
$t('general.cancel')
}}</NcButton>
<NcButton key="submit" v-e="['a:table:duplicate']" type="primary" size="small" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>
</div>

13
packages/nc-gui/components/dlg/TableRename.vue

@ -174,33 +174,34 @@ const renameTable = async (undo = false, disableTitleDiffCheck?: boolean | undef
</script>
<template>
<NcModal v-model:visible="dialogShow" size="small">
<NcModal v-model:visible="dialogShow" size="small" :show-separator="false">
<template #header>
<div class="flex flex-row items-center gap-x-2">
<GeneralIcon icon="rename" />
{{ $t('activity.renameTable') }}
</div>
</template>
<div class="mt-2">
<div class="mt-1">
<a-form :model="formState" name="create-new-table-form">
<a-form-item v-bind="validateInfos.title">
<a-input
ref="inputEl"
v-model:value="formState.title"
class="nc-input-md"
class="nc-input-sm nc-input-shadow"
hide-details
size="large"
size="small"
:placeholder="$t('msg.info.enterTableName')"
@keydown.enter="() => renameTable()"
/>
</a-form-item>
</a-form>
<div class="flex flex-row justify-end gap-x-2 mt-6">
<NcButton type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<div class="flex flex-row justify-end gap-x-2 mt-5">
<NcButton type="secondary" size="small" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton
key="submit"
type="primary"
size="small"
:disabled="validateInfos.title.validateStatus === 'error' || formState.title?.trim() === tableMeta.title"
label="Rename Table"
loading-label="Renaming Table"

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

@ -17,6 +17,7 @@ interface Props {
fk_from_column_id: string
fk_to_column_id: string | null // for ee only
}>
coverImageColumnId?: string
}
interface Emits {
@ -38,6 +39,7 @@ interface Form {
fk_from_column_id: string
fk_to_column_id: string | null // for ee only
}>
fk_cover_image_col_id: string | null
}
const props = withDefaults(defineProps<Props>(), {
@ -45,6 +47,7 @@ const props = withDefaults(defineProps<Props>(), {
groupingFieldColumnId: undefined,
geoDataFieldColumnId: undefined,
calendarRange: undefined,
coverImageColumnId: undefined,
})
const emits = defineEmits<Emits>()
@ -55,7 +58,7 @@ const { viewsByTable } = storeToRefs(useViewsStore())
const { refreshCommandPalette } = useCommandPalette()
const { selectedViewId, groupingFieldColumnId, geoDataFieldColumnId, tableId } = toRefs(props)
const { selectedViewId, groupingFieldColumnId, geoDataFieldColumnId, tableId, coverImageColumnId } = toRefs(props)
const meta = ref<TableType | undefined>()
@ -88,13 +91,14 @@ const form = reactive<Form>({
fk_grp_col_id: null,
fk_geo_data_col_id: null,
calendar_range: props.calendarRange || [],
fk_cover_image_col_id: null,
})
const viewSelectFieldOptions = ref<SelectProps['options']>([])
const viewNameRules = [
// name is required
{ required: true, message: `${t('labels.viewName')} ${t('general.required')}` },
{ required: true, message: `${t('labels.viewName')} ${t('general.required').toLowerCase()}` },
// name is unique
{
validator: (_: unknown, v: string) =>
@ -231,7 +235,7 @@ const addCalendarRange = async () => {
const isMetaLoading = ref(false)
onMounted(async () => {
if (props.type === ViewTypes.KANBAN || props.type === ViewTypes.MAP || props.type === ViewTypes.CALENDAR) {
if ([ViewTypes.GALLERY, ViewTypes.KANBAN, ViewTypes.MAP, ViewTypes.CALENDAR].includes(props.type)) {
isMetaLoading.value = true
try {
meta.value = (await getMeta(tableId.value))!
@ -258,6 +262,30 @@ onMounted(async () => {
}
}
// preset the cover image field
if (props.type === ViewTypes.GALLERY) {
viewSelectFieldOptions.value = [
{ value: null, label: 'No Image' },
...meta.value
.columns!.filter((el) => el.uidt === UITypes.Attachment)
.map((field) => {
return {
value: field.id,
label: field.title,
uidt: field.uidt,
}
}),
]
if (coverImageColumnId.value) {
form.fk_cover_image_col_id = coverImageColumnId.value
} else if (viewSelectFieldOptions.value.length > 1 && !form.copy_from_id) {
form.fk_cover_image_col_id = viewSelectFieldOptions.value[1].value as string
} else {
form.fk_cover_image_col_id = null
}
}
// preset the grouping field column
if (props.type === ViewTypes.KANBAN) {
viewSelectFieldOptions.value = meta.value
@ -266,6 +294,7 @@ onMounted(async () => {
return {
value: field.id,
label: field.title,
uidt: field.uidt,
}
})
@ -279,6 +308,14 @@ onMounted(async () => {
// if there is no grouping field column, disable the create button
isNecessaryColumnsPresent.value = false
}
if (coverImageColumnId.value) {
form.fk_cover_image_col_id = coverImageColumnId.value
} else if (viewSelectFieldOptions.value.length > 1 && !form.copy_from_id) {
form.fk_cover_image_col_id = viewSelectFieldOptions.value[1].value as string
} else {
form.fk_cover_image_col_id = null
}
}
if (props.type === ViewTypes.CALENDAR) {
@ -319,12 +356,14 @@ onMounted(async () => {
<template>
<NcModal
v-model:visible="vModel"
:size="[ViewTypes.KANBAN, ViewTypes.MAP, ViewTypes.CALENDAR].includes(form.type) ? 'medium' : 'small'"
class="nc-view-create-modal"
:show-separator="false"
:size="[ViewTypes.MAP].includes(form.type) ? 'medium' : 'small'"
>
<template #header>
<div class="flex w-full flex-row justify-between items-center">
<div class="flex font-bold text-base gap-x-3 items-center">
<GeneralViewIcon :meta="{ type: form.type }" class="nc-view-icon !text-xl" />
<GeneralViewIcon :meta="{ type: form.type }" class="nc-view-icon !text-[24px] !leading-6 max-h-6 max-w-6" />
<template v-if="form.type === ViewTypes.GRID">
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateGridView') }}
@ -380,24 +419,60 @@ onMounted(async () => {
:href="`https://docs.nocodb.com/views/view-types/${typeAlias}`"
target="_blank"
>
Go to Docs
Docs
</a>
</div>
</template>
<div class="mt-2">
<a-form v-if="isNecessaryColumnsPresent" ref="formValidator" :model="form" layout="vertical">
<div class="mt-1">
<a-form v-if="isNecessaryColumnsPresent" ref="formValidator" :model="form" layout="vertical" class="flex flex-col gap-y-5">
<a-form-item :rules="viewNameRules" name="title">
<a-input
ref="inputEl"
v-model:value="form.title"
:placeholder="$t('labels.viewName')"
autofocus
class="nc-input-md h-10"
class="nc-input-sm nc-input-shadow"
@keydown.enter="onSubmit"
/>
</a-form-item>
<a-form-item
v-if="form.type === ViewTypes.KANBAN"
v-if="form.type === ViewTypes.GALLERY && !form.copy_from_id"
:label="`${$t('labels.coverImageField')}`"
name="fk_cover_image_col_id"
>
<NcSelect
v-model:value="form.fk_cover_image_col_id"
:disabled="isMetaLoading"
:loading="isMetaLoading"
dropdown-match-select-width
:not-found-content="$t('placeholder.selectGroupFieldNotFound')"
:placeholder="$t('placeholder.selectCoverImageField')"
class="nc-select-shadow w-full nc-gallery-cover-image-field-select"
>
<a-select-option v-for="option of viewSelectFieldOptions" :key="option.value" :value="option.value">
<div class="w-full flex gap-2 items-center justify-between" :title="option.label">
<div class="flex-1 flex items-center gap-1 max-w-[calc(100%_-_24px)]">
<SmartsheetHeaderIcon v-if="option.value" :column="option" class="!ml-0" />
<NcTooltip class="flex-1 max-w-[calc(100%_-_20px)] truncate" show-on-truncate-only>
<template #title>
{{ option.label }}
</template>
<template #default>{{ option.label }}</template>
</NcTooltip>
</div>
<GeneralIcon
v-if="form.fk_cover_image_col_id === option.value"
id="nc-selected-item-icon"
icon="check"
class="flex-none text-primary w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
</a-form-item>
<a-form-item
v-if="form.type === ViewTypes.KANBAN && !form.copy_from_id"
:label="$t('general.groupingField')"
:rules="groupingFieldColumnRules"
name="fk_grp_col_id"
@ -406,11 +481,32 @@ onMounted(async () => {
v-model:value="form.fk_grp_col_id"
:disabled="isMetaLoading"
:loading="isMetaLoading"
dropdown-match-select-width
:not-found-content="$t('placeholder.selectGroupFieldNotFound')"
:options="viewSelectFieldOptions"
:placeholder="$t('placeholder.selectGroupField')"
class="w-full nc-kanban-grouping-field-select"
/>
class="nc-select-shadow w-full nc-kanban-grouping-field-select"
>
<a-select-option v-for="option of viewSelectFieldOptions" :key="option.value" :value="option.value">
<div class="w-full flex gap-2 items-center justify-between" :title="option.label">
<div class="flex-1 flex items-center gap-1 max-w-[calc(100%_-_24px)]">
<SmartsheetHeaderIcon :column="option" class="!ml-0" />
<NcTooltip class="flex-1 max-w-[calc(100%_-_20px)] truncate" show-on-truncate-only>
<template #title>
{{ option.label }}
</template>
<template #default>{{ option.label }}</template>
</NcTooltip>
</div>
<GeneralIcon
v-if="form.fk_grp_col_id === option.value"
id="nc-selected-item-icon"
icon="check"
class="flex-none text-primary w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
</a-form-item>
<a-form-item
v-if="form.type === ViewTypes.MAP"
@ -425,19 +521,19 @@ onMounted(async () => {
:not-found-content="$t('placeholder.selectGeoFieldNotFound')"
:options="viewSelectFieldOptions"
:placeholder="$t('placeholder.selectGeoField')"
class="w-full"
class="nc-select-shadow w-full"
/>
</a-form-item>
<template v-if="form.type === ViewTypes.CALENDAR">
<div v-for="(range, index) in form.calendar_range" :key="`range-${index}`" class="flex w-full mb-2 items-center gap-2">
<span>
<template v-if="form.type === ViewTypes.CALENDAR && !form.copy_from_id">
<div v-for="(range, index) in form.calendar_range" :key="`range-${index}`" class="flex w-full items-center gap-2">
<span class="text-gray-800">
{{ $t('labels.organiseBy') }}
</span>
<NcSelect
v-model:value="range.fk_from_column_id"
:disabled="isMetaLoading"
:loading="isMetaLoading"
class="nc-from-select"
class="nc-select-shadow nc-from-select"
>
<a-select-option
v-for="(option, id) in [...viewSelectFieldOptions!].filter((f) => {
@ -453,7 +549,7 @@ onMounted(async () => {
>
<div class="flex w-full gap-2 justify-between items-center">
<div class="flex gap-2 items-center">
<SmartsheetHeaderIcon :column="option" />
<SmartsheetHeaderIcon :column="option" class="!ml-0" />
<NcTooltip class="truncate flex-1 max-w-18" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
{{ option.label }}
@ -538,16 +634,16 @@ onMounted(async () => {
</a-form>
<div v-else-if="!isNecessaryColumnsPresent" class="flex flex-row p-4 border-gray-200 border-1 gap-x-4 rounded-lg w-full">
<div class="text-gray-500 flex gap-4">
<GeneralIcon class="min-w-6 h-6 text-orange-500" icon="warning" />
<GeneralIcon class="min-w-6 h-6 text-orange-500" icon="alertTriangle" />
<div class="flex flex-col gap-1">
<h2 class="font-semibold text-sm mb-0 text-gray-800">Suitable fields not present</h2>
<span class="text-gray-500 font-default"> {{ errorMessages[form.type] }}</span>
<span class="text-gray-500 font-default text-sm"> {{ errorMessages[form.type] }}</span>
</div>
</div>
</div>
<div class="flex flex-row w-full justify-end gap-x-2 mt-7">
<NcButton type="secondary" @click="vModel = false">
<div class="flex flex-row w-full justify-end gap-x-2 mt-5">
<NcButton type="secondary" size="small" @click="vModel = false">
{{ $t('general.cancel') }}
</NcButton>
@ -556,6 +652,7 @@ onMounted(async () => {
:disabled="!isNecessaryColumnsPresent"
:loading="isViewCreating"
type="primary"
size="small"
@click="onSubmit"
>
{{ $t('labels.createView') }}
@ -582,11 +679,29 @@ onMounted(async () => {
@apply !rounded-r-none;
}
.ant-input {
@apply border-gray-200;
.ant-form-item {
@apply !mb-0;
}
.ant-form-item {
@apply !mb-6;
.nc-input-sm {
@apply !mb-0;
}
.nc-view-create-modal {
:deep(.nc-modal) {
}
}
:deep(.ant-form-item-label > label) {
@apply !text-sm text-gray-800 flex;
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
@apply content-[''] m-0;
}
}
:deep(.ant-select) {
.ant-select-selector {
@apply !rounded-lg;
}
}
</style>

2
packages/nc-gui/components/dlg/ViewDelete.vue

@ -46,7 +46,7 @@ async function onDelete() {
<template>
<GeneralDeleteModal v-model:visible="vModel" :entity-name="$t('objects.view')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="view" class="flex flex-row items-center py-2 px-3 bg-gray-50 rounded-lg text-gray-700 mb-4">
<div v-if="view" class="flex flex-row items-center py-2 px-3 bg-gray-50 rounded-lg text-gray-700">
<GeneralViewIcon :meta="props.view" class="nc-view-icon w-4 min-h-4"></GeneralViewIcon>
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-3"

2
packages/nc-gui/components/dlg/WorkspaceDelete.vue

@ -50,7 +50,7 @@ const onDelete = async () => {
<template>
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.workspace')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="workspace" class="flex flex-row items-center py-2.25 px-2.75 bg-gray-50 rounded-lg text-gray-700 mb-4">
<div v-if="workspace" class="flex flex-row items-center py-2.25 px-2.75 bg-gray-50 rounded-lg text-gray-700">
<GeneralIcon icon="workspace" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-2.25"

4
packages/nc-gui/components/general/BaseLogo.vue

@ -6,7 +6,7 @@ import SimpleIconsMicrosoftsqlserver from '~icons/simple-icons/microsoftsqlserve
import LogosSnowflakeIcon from '~icons/logos/snowflake-icon'
import MdiDatabaseOutline from '~icons/mdi/database-outline'
const { sourceType } = defineProps<{ sourceType?: string }>()
const { sourceType } = defineProps<{ sourceType?: string; color?: string }>()
const baseIcon = computed(() => {
switch (sourceType) {
@ -27,5 +27,5 @@ const baseIcon = computed(() => {
</script>
<template>
<component :is="baseIcon" />
<component :is="baseIcon" :style="color ? { color } : {}" />
</template>

7
packages/nc-gui/components/general/DeleteModal.vue

@ -45,9 +45,7 @@ onKeyStroke('Enter', () => {
<template>
<GeneralModal v-model:visible="visible" size="small" centered>
<div class="flex flex-col p-6">
<div class="flex flex-row pb-2 mb-4 font-medium text-lg border-b-1 border-gray-50 text-gray-800">
{{ deleteLabel }} {{ props.entityName }}
</div>
<div class="flex flex-row pb-2 mb-3 font-medium text-lg text-gray-800">{{ deleteLabel }} {{ props.entityName }}</div>
<div class="mb-3 text-gray-800">
{{
@ -60,13 +58,14 @@ onKeyStroke('Enter', () => {
<slot name="entity-preview"></slot>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton type="secondary" @click="visible = false">
<NcButton type="secondary" size="small" @click="visible = false">
{{ $t('general.cancel') }}
</NcButton>
<NcButton
key="submit"
type="danger"
size="small"
html-type="submit"
:loading="isLoading"
data-testid="nc-delete-modal-delete-btn"

39
packages/nc-gui/components/general/SourceRestrictionTooltip.vue

@ -0,0 +1,39 @@
<script setup lang="ts">
import type { TooltipPlacement } from 'ant-design-vue/es/tooltip'
import type { CSSProperties } from '@vue/runtime-dom'
defineProps<{
tooltipStyle?: CSSProperties
overlayInnerStyle?: CSSProperties
mouseLeaveDelay?: number
placement?: TooltipPlacement
trigger?: 'hover' | 'click'
message?: string
enabled?: boolean
}>()
</script>
<template>
<NcTooltip
:disabled="!enabled"
:tooltip-style="{ 'min-width': 'max-content' }"
:overlay-inner-style="{ 'min-width': 'max-content' }"
:mouse-leave-delay="0.3"
placement="left"
trigger="hover"
>
<template #title>
{{ $t('tooltip.schemaChangeDisabled') }} <br />
{{ message }}
<br v-if="message" />
<a
class="!text-current"
href="https://docs.nocodb.com/data-sources/connect-to-data-source#configuring-permissions"
target="_blank"
>
Learn more
</a>
</template>
<slot />
</NcTooltip>
</template>

3
packages/nc-gui/components/nc/Dropdown.vue

@ -4,6 +4,7 @@ const props = withDefaults(
trigger?: Array<'click' | 'hover' | 'contextmenu'>
visible?: boolean | undefined
overlayClassName?: string | undefined
disabled?: boolean
placement?: 'bottom' | 'top' | 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight' | 'topCenter' | 'bottomCenter'
autoClose?: boolean
}>(),
@ -11,6 +12,7 @@ const props = withDefaults(
trigger: () => ['click'],
visible: undefined,
placement: 'bottomLeft',
disabled: false,
overlayClassName: undefined,
autoClose: true,
},
@ -61,6 +63,7 @@ const onVisibleUpdate = (event: any) => {
<template>
<a-dropdown
:disabled="disabled"
:visible="visible"
:placement="placement"
:trigger="trigger"

34
packages/nc-gui/components/nc/Pagination.vue

@ -1,22 +1,28 @@
<script setup lang="ts">
import NcTooltip from '~/components/nc/Tooltip.vue'
const props = defineProps<{
current: number
total: number
pageSize: number
entityName?: string
mode?: 'simple' | 'full'
prevPageTooltip?: string
nextPageTooltip?: string
firstPageTooltip?: string
lastPageTooltip?: string
showSizeChanger?: boolean
}>()
const props = withDefaults(
defineProps<{
current: number
total: number
pageSize: number
entityName?: string
mode?: 'simple' | 'full'
prevPageTooltip?: string
nextPageTooltip?: string
firstPageTooltip?: string
lastPageTooltip?: string
showSizeChanger?: boolean
useStoredPageSize?: boolean
}>(),
{
useStoredPageSize: true,
},
)
const emits = defineEmits(['update:current', 'update:pageSize'])
const { total, showSizeChanger } = toRefs(props)
const { total, showSizeChanger, useStoredPageSize } = toRefs(props)
const current = useVModel(props, 'current', emits)
@ -26,7 +32,7 @@ const { gridViewPageSize, setGridViewPageSize } = useGlobal()
const localPageSize = computed({
get: () => {
if (!showSizeChanger.value) return pageSize.value
if (!showSizeChanger.value || (showSizeChanger.value && !useStoredPageSize.value)) return pageSize.value
const storedPageSize = gridViewPageSize.value || 25

241
packages/nc-gui/components/nc/PaginationV2.vue

@ -0,0 +1,241 @@
<script setup lang="ts">
import NcTooltip from '~/components/nc/Tooltip.vue'
const props = defineProps<{
current: number
total: number
pageSize: number
entityName?: string
mode?: 'simple' | 'full'
prevPageTooltip?: string
nextPageTooltip?: string
firstPageTooltip?: string
lastPageTooltip?: string
showSizeChanger?: boolean
}>()
const emits = defineEmits(['update:current', 'update:pageSize'])
const { total, showSizeChanger } = toRefs(props)
const current = useVModel(props, 'current', emits)
const pageSize = useVModel(props, 'pageSize', emits)
const { gridViewPageSize, setGridViewPageSize } = useGlobal()
const localPageSize = computed({
get: () => {
if (!showSizeChanger.value) return pageSize.value
const storedPageSize = gridViewPageSize.value || 25
if (pageSize.value !== storedPageSize) {
pageSize.value = storedPageSize
}
return pageSize.value
},
set: (val) => {
setGridViewPageSize(val)
pageSize.value = val
},
})
const entityName = computed(() => props.entityName || 'item')
const totalPages = computed(() => Math.max(Math.ceil(total.value / localPageSize.value), 1))
const { isMobileMode } = useGlobal()
const mode = computed(() => props.mode || (isMobileMode.value ? 'simple' : 'full'))
const changePage = ({ increase, set }: { increase?: boolean; set?: number }) => {
if (set) {
current.value = set
} else if (increase && current.value < totalPages.value) {
current.value = current.value + 1
} else if (current.value > 0) {
current.value = current.value - 1
}
}
const goToLastPage = () => {
current.value = totalPages.value
}
const goToFirstPage = () => {
current.value = 1
}
const pagesList = computed(() => {
return Array.from({ length: totalPages.value }, (_, i) => ({
value: i + 1,
label: i + 1,
}))
})
const pageSizeOptions = [
{
value: 25,
label: '25 / page',
},
{
value: 50,
label: '50 / page',
},
{
value: 75,
label: '75 / page',
},
{
value: 100,
label: '100 / page',
},
]
</script>
<template>
<div class="nc-pagination flex flex-row items-center gap-x-0.25">
<template v-if="totalPages > 1">
<component :is="props.firstPageTooltip && mode === 'full' ? NcTooltip : 'div'" v-if="mode === 'full'">
<template v-if="props.firstPageTooltip" #title>
{{ props.firstPageTooltip }}
</template>
<NcButton
v-e="[`a:pagination:${entityName}:first-page`]"
class="first-page !border-0"
type="text"
size="xsmall"
:disabled="current === 1"
@click="goToFirstPage"
>
<GeneralIcon icon="doubleLeftArrow" class="nc-pagination-icon" />
</NcButton>
</component>
<component :is="props.prevPageTooltip && mode === 'full' ? NcTooltip : 'div'">
<template v-if="props.prevPageTooltip" #title>
{{ props.prevPageTooltip }}
</template>
<NcButton
v-e="[`a:pagination:${entityName}:prev-page`]"
class="prev-page !border-0"
type="secondary"
size="xsmall"
:disabled="current === 1"
@click="changePage({ increase: false })"
>
<GeneralIcon icon="arrowLeft" class="nc-pagination-icon" />
</NcButton>
</component>
<div v-if="!isMobileMode" class="text-gray-500">
<NcDropdown placement="top" overlay-class-name="!shadow-none">
<NcButton class="!border-0 nc-select-page" type="secondary" size="xsmall">
<div class="flex gap-1 items-center px-2">
<span class="nc-current-page">
{{ current }}
</span>
<GeneralIcon icon="arrowDown" class="text-gray-800 mt-0.5 nc-select-expand-btn" />
</div>
</NcButton>
<template #overlay>
<NcMenu class="nc-scrollbar-md nc-pagination-menu max-h-54">
<NcSubMenu :key="`${localPageSize}page`" class="bg-gray-100 z-20 top-0 !sticky">
<template #title>
<div class="rounded-lg text-[13px] font-medium w-full">{{ localPageSize }} / page</div>
</template>
<NcMenuItem v-for="option in pageSizeOptions" :key="option.value" @click="localPageSize = option.value">
<span
class="text-[13px]"
:class="{
'!text-brand-500': option.value === localPageSize,
}"
>
{{ option.value }} / page
</span>
</NcMenuItem>
</NcSubMenu>
<div :key="localPageSize" class="flex flex-col mt-1 max-h-48 overflow-hidden nc-scrollbar-md gap-1">
<NcMenuItem
v-for="x in pagesList"
:key="`${localPageSize}${x.value}`"
@click.stop="
changePage({
set: x.value,
})
"
>
<div
:class="{
'text-brand-500': x.value === current,
}"
class="flex text-[13px] !w-full text-gray-800 items-center justify-between"
>
{{ x.label }}
</div>
</NcMenuItem>
</div>
</NcMenu>
</template>
</NcDropdown>
</div>
<component :is="props.nextPageTooltip && mode === 'full' ? NcTooltip : 'div'">
<template v-if="props.nextPageTooltip" #title>
{{ props.nextPageTooltip }}
</template>
<NcButton
v-e="[`a:pagination:${entityName}:next-page`]"
class="next-page !border-0"
type="secondary"
size="xsmall"
:disabled="current === totalPages"
@click="changePage({ increase: true })"
>
<GeneralIcon icon="arrowRight" class="nc-pagination-icon" />
</NcButton>
</component>
<component :is="props.lastPageTooltip && mode === 'full' ? NcTooltip : 'div'" v-if="mode === 'full'">
<template v-if="props.lastPageTooltip" #title>
{{ props.lastPageTooltip }}
</template>
<NcButton
v-e="[`a:pagination:${entityName}:last-page`]"
class="last-page !border-0"
type="secondary"
size="xsmall"
:disabled="current === totalPages"
@click="goToLastPage"
>
<GeneralIcon icon="doubleRightArrow" class="nc-pagination-icon" />
</NcButton>
</component>
</template>
<div v-if="showSizeChanger && !isMobileMode" class="text-gray-500"></div>
</div>
</template>
<style lang="scss" scoped>
.nc-pagination-icon {
@apply w-4 h-4;
}
:deep(.ant-dropdown-menu-title-content) {
@apply justify-center;
}
:deep(.nc-button:not(:disabled)) {
.nc-pagination-icon {
@apply !text-gray-500;
}
}
</style>
<style lang="scss"></style>

2
packages/nc-gui/components/nc/Select.vue

@ -84,7 +84,7 @@ const onChange = (value: string) => {
height: fit-content;
.ant-select-selector {
box-shadow: 0px 5px 3px -2px rgba(0, 0, 0, 0.02), 0px 3px 1px -2px rgba(0, 0, 0, 0.06);
@apply border-1 border-gray-200 rounded-lg;
@apply border-1 border-gray-200 rounded-lg shadow-default;
}
.ant-select-selection-item {

8
packages/nc-gui/components/nc/SubMenu.vue

@ -1,5 +1,11 @@
<script lang="ts" setup>
const props = defineProps<{
popupOffset?: number[]
}>()
</script>
<template>
<a-sub-menu class="nc-sub-menu" popup-class-name="nc-submenu-popup">
<a-sub-menu :popup-offset="props.popupOffset" class="nc-sub-menu" popup-class-name="nc-submenu-popup">
<template #title>
<div class="flex flex-row items-center gap-x-1.5 py-1.75 justify-between group hover:text-gray-800">
<div class="flex flex-row items-center gap-x-2">

6
packages/nc-gui/components/nc/Tooltip.vue

@ -14,6 +14,8 @@ interface Props {
hideOnClick?: boolean
overlayClassName?: string
wrapChild?: keyof HTMLElementTagNameMap
mouseLeaveDelay?: number
overlayInnerStyle?: object
}
const props = defineProps<Props>()
@ -77,7 +79,7 @@ watch([isHovering, () => modifierKey.value, () => disabled.value], ([hovering, k
}
}
if (!hovering || isDisabled) {
if ((!hovering || isDisabled) && !props.mouseLeaveDelay) {
showTooltip.value = false
return
}
@ -117,9 +119,11 @@ const onClick = () => {
v-model:visible="showTooltip"
:overlay-class-name="`nc-tooltip ${showTooltip ? 'visible' : 'hidden'} ${overlayClassName}`"
:overlay-style="tooltipStyle"
:overlay-inner-style="overlayInnerStyle"
arrow-point-at-center
:trigger="[]"
:placement="placement"
:mouse-leave-delay="mouseLeaveDelay"
>
<template #title>
<slot name="title" />

1
packages/nc-gui/components/project/AccessSettings.vue

@ -118,6 +118,7 @@ const updateCollaborator = async (collab: any, roles: ProjectRoles) => {
} else {
currentCollaborator.roles = ProjectRoles.NO_ACCESS
}
currentCollaborator.base_roles = null
} else if (currentCollaborator.base_roles) {
currentCollaborator.roles = roles
await updateProjectUser(currentBase.value.id!, currentCollaborator as unknown as User)

4
packages/nc-gui/components/project/AllTables.vue

@ -76,7 +76,7 @@ function openTableCreateDialog(baseIndex?: number | undefined) {
}"
>
<div
v-if="isUIAllowed('tableCreate')"
v-if="isUIAllowed('tableCreate', { source: base?.sources?.[0] })"
role="button"
class="nc-base-view-all-table-btn"
data-testid="proj-view-btn__add-new-table"
@ -86,7 +86,7 @@ function openTableCreateDialog(baseIndex?: number | undefined) {
<div class="label">{{ $t('general.new') }} {{ $t('objects.table') }}</div>
</div>
<div
v-if="isUIAllowed('tableCreate')"
v-if="isUIAllowed('tableCreate', { source: base?.sources?.[0] })"
v-e="['c:table:import']"
role="button"
class="nc-base-view-all-table-btn"

4
packages/nc-gui/components/project/View.vue

@ -39,7 +39,9 @@ const { projectPageTab } = storeToRefs(useConfigStore())
const { isMobileMode } = useGlobal()
const userCount = computed(() => (activeProjectId.value ? basesUser.value.get(activeProjectId.value)?.length : 0))
const userCount = computed(() =>
activeProjectId.value ? basesUser.value.get(activeProjectId.value)?.filter((user) => !user?.deleted)?.length : 0,
)
watch(
() => route.value.query?.page,

4
packages/nc-gui/components/smartsheet/Details.vue

@ -8,7 +8,7 @@ const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const { $e } = useNuxtApp()
const { isUIAllowed } = useRoles()
const { isUIAllowed, isDataReadOnly } = useRoles()
const { base } = storeToRefs(useBase())
const meta = inject(MetaInj, ref())
@ -85,7 +85,7 @@ watch(openedSubTab, () => {
</div>
</a-tab-pane>
<a-tab-pane v-if="isUIAllowed('hookList')" key="webhook">
<a-tab-pane v-if="isUIAllowed('hookList') && !isDataReadOnly" key="webhook">
<template #tab>
<div class="tab" data-testid="nc-webhooks-tab">
<GeneralIcon icon="webhook" class="tab-icon" :class="{}" />

2
packages/nc-gui/components/smartsheet/Form.vue

@ -361,7 +361,7 @@ async function handleAddOrRemoveAllColumns<T>(value: T) {
}
async function checkSMTPStatus() {
if (emailMe.value) {
if (emailMe.value && !isEeUI) {
const emailPluginActive = await $api.plugin.status('SMTP')
if (!emailPluginActive) {
emailMe.value = false

33
packages/nc-gui/components/smartsheet/Gallery.vue

@ -26,7 +26,6 @@ const {
loadGalleryData,
galleryData,
changePage,
addEmptyRow,
deleteRow,
navigateToSiblingRow,
} = useViewData(meta, view, xWhere)
@ -123,10 +122,19 @@ const expandFormClick = async (e: MouseEvent, row: RowType) => {
expandForm(row)
}
openNewRecordFormHook?.on(async () => {
const newRow = await addEmptyRow()
expandForm(newRow)
})
const openNewRecordFormHookHandler = async () => {
expandForm({
row: { ...rowDefaultData(meta.value?.columns) },
oldRow: {},
rowMeta: { new: true },
})
}
openNewRecordFormHook?.on(openNewRecordFormHookHandler)
// remove openNewRecordFormHookHandler before unmounting
// so that it won't be triggered multiple times
onBeforeUnmount(() => openNewRecordFormHook.off(openNewRecordFormHookHandler))
const expandedFormOnRowIdDlg = computed({
get() {
@ -223,7 +231,7 @@ watch(
<div
class="flex flex-col w-full nc-gallery nc-scrollbar-md bg-gray-50"
data-testid="nc-gallery-wrapper"
:style="{ height: isMobileMode ? 'calc(100% - var(--topbar-height))' : 'calc(100% - var(--topbar-height) + 0.7rem)' }"
:style="{ height: isMobileMode ? 'calc(100% - var(--topbar-height))' : 'calc(100% - var(--topbar-height) + 0.6rem)' }"
:class="{
'!overflow-hidden': isViewDataLoading,
}"
@ -370,7 +378,18 @@ watch(
align-count-on-right
show-api-timing
:change-page="changePage"
/>
class=""
>
<template #add-record>
<NcButton v-if="isUIAllowed('dataInsert')" size="xs" type="secondary" class="ml-2" @click="openNewRecordFormHook.trigger">
<div class="flex items-center gap-2">
<component :is="iconMap.plus" class="" />
{{ $t('activity.newRecord') }}
</div>
</NcButton>
</template>
</LazySmartsheetPagination>
<Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"

2
packages/nc-gui/components/smartsheet/Kanban.vue

@ -714,7 +714,7 @@ const handleSubmitRenameOrNewStack = async (loadMeta: boolean, stack?: any, stac
item-key="row.Id"
draggable=".nc-kanban-item"
group="kanban-card"
class="flex flex-col h-full mb-2"
class="flex flex-col h-full"
filter=".not-draggable"
@start="(e) => e.target.classList.add('grabbing')"
@end="(e) => e.target.classList.remove('grabbing')"

8
packages/nc-gui/components/smartsheet/Pagination.vue

@ -92,13 +92,14 @@ const tempPageVal = ref(page.value)
class="flex items-center bg-white border-gray-200 nc-grid-pagination-wrapper"
:class="{ 'border-t-1': !isGroupBy, 'h-13': isMobileMode, 'h-10': !isMobileMode }"
:style="`${fixedSize ? `width: ${fixedSize}px;` : ''}${
isGroupBy ? 'margin-top:1px; border-radius: 0 0 12px 12px !important;' : ''
}${extraStyle}`"
isGroupBy ? 'margin-top:1px; border-radius: 0 0 8px 8px !important;' : ''
} ${extraStyle}`"
>
<div
class="flex items-center"
:class="{
'flex-1': !alignLeft,
'sticky left-0': isGroupBy,
}"
>
<slot name="add-record" />
@ -117,6 +118,9 @@ const tempPageVal = ref(page.value)
:class="{
'-ml-17': isLeftSidebarOpen && !alignLeft,
'ml-8': alignLeft,
'sticky': isGroupBy,
'left-[159px]': isGroupBy && $slots['add-record'],
'left-[32px]': isGroupBy && !$slots['add-record'],
}"
>
<div v-if="isViewDataLoading" class="nc-pagination-skeleton flex flex-row justify-center item-center min-h-10 min-w-42">

2
packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue

@ -996,7 +996,7 @@ watch(
</template>
</NcDropdown>
<NcButton
v-else-if="!isPublic"
v-else-if="!isPublic && isUIAllowed('dataEdit')"
:class="{
'!block': hour.isSame(selectedTime),
'!hidden': !hour.isSame(selectedTime),

2
packages/nc-gui/components/smartsheet/calendar/SideMenu.vue

@ -554,7 +554,7 @@ onClickOutside(searchRef, toggleSearch)
<template v-if="coverImageColumns" #image>
<a-carousel
v-if="attachments(record).length"
class="gallery-carousel rounded-md !border-1 !border-gray-200"
class="gallery-carousel rounded-md !border-1 !border-gray-200 w-10 h-10"
arrows
>
<template #customPaging>

5
packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue

@ -12,7 +12,7 @@ const { fields, metaColumnById } = useViewColumnsOrThrow()
const vModel = useVModel(props, 'modelValue', emit)
const { setAdditionalValidations, validateInfos, column } = useColumnCreateStoreOrThrow()
const { setAdditionalValidations, validateInfos, column, isEdit } = useColumnCreateStoreOrThrow()
const { t } = useI18n()
@ -36,7 +36,8 @@ onMounted(() => {
...(vModel.value.meta || {}),
}
vModel.value.fk_barcode_value_column_id =
(column?.value?.colOptions as Record<string, any>)?.fk_barcode_value_column_id || columnsAllowedAsBarcodeValue.value?.[0]?.id
(column?.value?.colOptions as Record<string, any>)?.fk_barcode_value_column_id ||
(!isEdit ? columnsAllowedAsBarcodeValue.value?.[0]?.id : null)
})
watch(columnsAllowedAsBarcodeValue, (newColumnsAllowedAsBarcodeValue) => {

3
packages/nc-gui/components/smartsheet/column/DecimalOptions.vue

@ -36,6 +36,8 @@ vModel.value.meta = {
const onPrecisionChange = (value: number) => {
vModel.value.dtxs = Math.max(value, vModel.value.dtxs)
}
const { isMetaReadOnly } = useRoles()
</script>
<template>
@ -43,6 +45,7 @@ const onPrecisionChange = (value: number) => {
<a-select
v-if="vModel.meta?.precision"
v-model:value="vModel.meta.precision"
:disabled="isMetaReadOnly"
dropdown-class-name="nc-dropdown-decimal-format"
@change="onPrecisionChange"
>

72
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -1,7 +1,14 @@
<script lang="ts" setup>
import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { UITypes, UITypesName, isLinksOrLTAR, isSelfReferencingTableColumn, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { type ColumnReqType, type ColumnType } from 'nocodb-sdk'
import {
UITypes,
UITypesName,
isLinksOrLTAR,
isSelfReferencingTableColumn,
isSystemColumn,
isVirtualCol,
readonlyMetaAllowedTypes,
} from 'nocodb-sdk'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
import MdiIdentifierIcon from '~icons/mdi/identifier'
@ -39,6 +46,8 @@ const { getMeta } = useMetas()
const { t } = useI18n()
const { isMetaReadOnly } = useRoles()
const columnLabel = computed(() => props.columnLabel || t('objects.field'))
const { $e } = useNuxtApp()
@ -125,7 +134,7 @@ const uiFilters = (t: { name: UITypes; virtual?: number; deprecated?: boolean })
}
const uiTypesOptions = computed<typeof uiTypes>(() => {
return [
const types = [
...uiTypes.filter(uiFilters),
...(!isEdit.value && meta?.value?.columns?.every((c) => !c.pk)
? [
@ -137,6 +146,21 @@ const uiTypesOptions = computed<typeof uiTypes>(() => {
]
: []),
]
// if meta is readonly, move disabled types to the end
if (isMetaReadOnly.value) {
types.sort((a, b) => {
const aDisabled = readonlyMetaAllowedTypes.includes(a.name)
const bDisabled = readonlyMetaAllowedTypes.includes(b.name)
if (aDisabled && !bDisabled) return -1
if (!aDisabled && bDisabled) return 1
return 0
})
}
return types
})
const onSelectType = (uidt: UITypes) => {
@ -319,6 +343,14 @@ const filterOption = (input: string, option: { value: UITypes }) => {
(UITypesName[option.value] && UITypesName[option.value].toLowerCase().includes(input.toLowerCase()))
)
}
const isFullUpdateAllowed = computed(() => {
if (isMetaReadOnly.value && !readonlyMetaAllowedTypes.includes(formState.value?.uidt) && !isVirtualCol(formState.value)) {
return false
}
return true
})
</script>
<template>
@ -328,8 +360,8 @@ const filterOption = (input: string, option: { value: UITypes }) => {
:class="{
'bg-white': !props.fromTableExplorer,
'w-[384px]': !props.embedMode,
'min-w-500px': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links,
'!w-116 overflow-visible': formState.uidt === UITypes.Formula && !props.embedMode,
'min-w-[500px]': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links,
'overflow-visible': formState.uidt === UITypes.Formula,
'!w-[600px]': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links,
'shadow-lg border-1 border-gray-200 shadow-gray-300 rounded-xl p-5': !embedMode,
}"
@ -352,7 +384,7 @@ const filterOption = (input: string, option: { value: UITypes }) => {
<input
ref="antInput"
v-model="formState.title"
:disabled="readOnly"
:disabled="readOnly || !isFullUpdateAllowed"
:placeholder="`${$t('objects.field')} ${$t('general.name').toLowerCase()} ${isEdit ? '' : $t('labels.optional')}`"
class="flex flex-grow nc-fields-input text-sm font-semibold outline-none bg-inherit min-h-6"
:contenteditable="true"
@ -366,7 +398,7 @@ const filterOption = (input: string, option: { value: UITypes }) => {
v-model:value="formState.title"
class="nc-column-name-input !rounded-lg"
:placeholder="`${$t('objects.field')} ${$t('general.name').toLowerCase()} ${isEdit ? '' : $t('labels.optional')}`"
:disabled="isKanban || readOnly"
:disabled="isKanban || readOnly || !isFullUpdateAllowed"
@input="onAlter(8)"
/>
</a-form-item>
@ -376,12 +408,23 @@ const filterOption = (input: string, option: { value: UITypes }) => {
<SmartsheetColumnUITypesOptionsWithSearch :options="uiTypesOptions" @selected="onSelectType" />
</template>
<a-form-item v-else-if="!props.hideType" class="flex-1">
<a-form-item
v-else-if="!props.hideType"
class="flex-1"
@keydown.up.stop="handleResetHoverEffect"
@keydown.down.stop="handleResetHoverEffect"
>
<a-select
v-model:value="formState.uidt"
show-search
class="nc-column-type-input !rounded-lg"
:disabled="isKanban || readOnly || (isEdit && !!onlyNameUpdateOnEditColumns.includes(column?.uidt))"
:disabled="
(isEdit && isMetaReadOnly && !readonlyMetaAllowedTypes.includes(formState.uidt)) ||
isKanban ||
readOnly ||
(isEdit && !!onlyNameUpdateOnEditColumns.includes(column?.uidt)) ||
(isEdit && !isFullUpdateAllowed)
"
dropdown-class-name="nc-dropdown-column-type border-1 !rounded-lg border-gray-200"
:filter-option="filterOption"
@dropdown-visible-change="onDropdownChange"
@ -395,6 +438,7 @@ const filterOption = (input: string, option: { value: UITypes }) => {
v-for="opt of uiTypesOptions"
:key="opt.name"
:value="opt.name"
:disabled="isMetaReadOnly && !readonlyMetaAllowedTypes.includes(opt.name)"
v-bind="validateInfos.uidt"
:class="{
'ant-select-item-option-active-selected': showHoverEffectOnSelectedType && formState.uidt === opt.name,
@ -403,7 +447,11 @@ const filterOption = (input: string, option: { value: UITypes }) => {
>
<div class="w-full flex gap-2 items-center justify-between" :data-testid="opt.name">
<div class="flex gap-2 items-center">
<component :is="opt.icon" class="text-gray-700 w-4 h-4" />
<component
:is="opt.icon"
class="w-4 h-4"
:class="isMetaReadOnly && !readonlyMetaAllowedTypes.includes(opt.name) ? 'text-gray-300' : 'text-gray-700'"
/>
<div class="flex-1">{{ UITypesName[opt.name] }}</div>
<span v-if="opt.deprecated" class="!text-xs !text-gray-300">({{ $t('general.deprecated') }})</span>
</div>
@ -472,7 +520,7 @@ const filterOption = (input: string, option: { value: UITypes }) => {
</NcSwitch>
</div>
<template v-if="!readOnly">
<template v-if="!readOnly && isFullUpdateAllowed">
<div class="nc-column-options-wrapper flex flex-col gap-4">
<!--
Default Value for JSON & LongText is not supported in MySQL

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

@ -22,7 +22,7 @@ const uiTypesNotSupportedInFormulas = [UITypes.QrCode, UITypes.Barcode]
const vModel = useVModel(props, 'value', emit)
const { setAdditionalValidations, validateInfos, sqlUi, column } = useColumnCreateStoreOrThrow()
const { setAdditionalValidations, validateInfos, sqlUi, column, fromTableExplorer } = useColumnCreateStoreOrThrow()
const { t } = useI18n()
@ -48,6 +48,8 @@ const { getMeta } = useMetas()
const suggestionPreviewed = ref<Record<any, string> | undefined>()
const showFunctionList = ref<boolean>(true)
const validators = {
formula_raw: [
{
@ -84,8 +86,6 @@ const autocomplete = ref(false)
const formulaRef = ref()
const sugListRef = ref()
const variableListRef = ref<(typeof AntListItem)[]>([])
const sugOptionsRef = ref<(typeof AntListItem)[]>([])
@ -124,7 +124,7 @@ const suggestionsList = computed(() => {
.map((c: any) => ({
text: c.title,
type: 'column',
icon: getUIDTIcon(c.uidt),
icon: getUIDTIcon(c.uidt) ? markRaw(getUIDTIcon(c.uidt)!) : undefined,
uidt: c.uidt,
})),
...availableBinOps.map((op: string) => ({
@ -218,7 +218,11 @@ function handleInput() {
if (!isCurlyBracketBalanced()) {
suggestion.value = suggestion.value.filter((v) => v.type === 'column')
showFunctionList.value = false
} else if (!showFunctionList.value) {
showFunctionList.value = true
}
autocomplete.value = !!suggestion.value.length
}
@ -284,27 +288,62 @@ onMounted(() => {
jsep.plugins.register(jsepCurlyHook)
})
const suggestionPreviewLeft = ref('-left-85')
watch(sugListRef, () => {
nextTick(() => {
setTimeout(() => {
const fieldModal = document.querySelector('.nc-dropdown-edit-column.active') as HTMLDivElement
const suggestionPreviewPostion = ref({
top: '0px',
left: '-344px',
})
if (fieldModal && fieldModal.getBoundingClientRect().left < 364) {
suggestionPreviewLeft.value = '-right-85'
}
}, 500)
})
onMounted(() => {
// wait until MFE field modal transition complete
setTimeout(() => {
const textAreaPosition = formulaRef.value?.$el?.getBoundingClientRect()
if (!textAreaPosition) return
if (fromTableExplorer?.value) {
suggestionPreviewPostion.value.left = `${textAreaPosition.left - 344}px`
suggestionPreviewPostion.value.top = `${textAreaPosition.top}px`
} else {
suggestionPreviewPostion.value.left = textAreaPosition.left < 352 ? '350px' : '-344px'
suggestionPreviewPostion.value.top = `0px`
}
}, 250)
})
const handleKeydown = (e: KeyboardEvent) => {
e.stopPropagation()
switch (e.key) {
case 'ArrowUp': {
e.preventDefault()
suggestionListUp()
break
}
case 'ArrowDown': {
e.preventDefault()
suggestionListDown()
break
}
case 'Enter': {
e.preventDefault()
selectText()
break
}
}
}
</script>
<template>
<div class="formula-wrapper relative">
<div
v-if="suggestionPreviewed && !suggestionPreviewed.unsupported && suggestionPreviewed.type === 'function'"
class="absolute w-84 top-0 bg-white z-10 pl-3 pt-3 border-1 shadow-md rounded-xl"
:class="suggestionPreviewLeft"
class="w-84 bg-white z-10 pl-3 pt-3 border-1 shadow-md rounded-xl"
:class="{
'fixed': fromTableExplorer,
'absolute top-0': !fromTableExplorer,
}"
:style="{
left: suggestionPreviewPostion.left,
top: suggestionPreviewPostion.top,
}"
>
<div class="pr-3">
<div class="flex flex-row w-full justify-between pb-2 border-b-1">
@ -358,15 +397,13 @@ watch(sugListRef, () => {
ref="formulaRef"
v-model:value="vModel.formula_raw"
class="nc-formula-input !rounded-md"
@keydown.down.prevent="suggestionListDown"
@keydown.up.prevent="suggestionListUp"
@keydown.enter.prevent="selectText"
@keydown="handleKeydown"
@change="handleInputDeb"
/>
</a-form-item>
<div ref="sugListRef" class="h-[250px] overflow-auto nc-scrollbar-thin border-1 border-gray-200 rounded-lg mt-4">
<template v-if="suggestedFormulas.length > 0">
<div class="h-[250px] overflow-auto nc-scrollbar-thin border-1 border-gray-200 rounded-lg mt-4">
<template v-if="suggestedFormulas && showFunctionList">
<div class="border-b-1 bg-gray-50 px-3 py-1 uppercase text-gray-600 text-xs font-semibold sticky top-0 z-10">
Formulas
</div>
@ -405,13 +442,13 @@ watch(sugListRef, () => {
</a-list>
</template>
<template v-if="variableList.length > 0">
<template v-if="variableList">
<div class="border-b-1 bg-gray-50 px-3 py-1 uppercase text-gray-600 text-xs font-semibold sticky top-0 z-10">Fields</div>
<a-list
ref="variableListRef"
:data-source="variableList"
:locale="{ emptyText: $t('msg.formula.noSuggestedFormulaFound') }"
:locale="{ emptyText: $t('msg.formula.noSuggestedFieldFound') }"
class="!overflow-hidden"
>
<template #renderItem="{ item, index }">
@ -450,9 +487,6 @@ watch(sugListRef, () => {
</template>
</a-list>
</template>
<div v-if="suggestion.length === 0">
<span class="text-gray-500">Empty</span>
</div>
</div>
</div>
</template>

5
packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue

@ -14,7 +14,7 @@ const { fields, metaColumnById } = useViewColumnsOrThrow()
const vModel = useVModel(props, 'modelValue', emit)
const { setAdditionalValidations, validateInfos, column } = useColumnCreateStoreOrThrow()
const { setAdditionalValidations, validateInfos, column, isEdit } = useColumnCreateStoreOrThrow()
const columnsAllowedAsQrValue = computed<ColumnType[]>(() => {
return (
@ -32,7 +32,8 @@ const columnsAllowedAsQrValue = computed<ColumnType[]>(() => {
onMounted(() => {
// set default value
vModel.value.fk_qr_value_column_id =
(column?.value?.colOptions as Record<string, any>)?.fk_qr_value_column_id || columnsAllowedAsQrValue.value?.[0]?.id
(column?.value?.colOptions as Record<string, any>)?.fk_qr_value_column_id ||
(!isEdit ? columnsAllowedAsQrValue.value?.[0]?.id : null)
})
setAdditionalValidations({

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

@ -113,13 +113,18 @@ watch(
</a-col>
<a-col :span="8">
<a-form-item :label="$t('labels.max')">
<a-select v-model:value="vModel.meta.max" class="w-52" dropdown-class-name="nc-dropdown-rating-color">
<a-select
v-model:value="vModel.meta.max"
data-testid="nc-dropdown-rating-max"
class="w-52"
dropdown-class-name="nc-dropdown-rating-color"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(v, i) in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" :key="i" :value="v">
<div class="flex gap-2 w-full justify-between items-center">
<div class="flex gap-2 w-full justify-between items-center nc-dropdown-rating-max-option">
{{ v }}
<component
:is="iconMap.check"

7
packages/nc-gui/components/smartsheet/column/SelectOptions.vue

@ -342,7 +342,7 @@ onMounted(() => {
options.value = vModel.value.colOptions.options
let indexCounter = 0
options.value.map((el) => {
options.value = options.value.map((el) => {
el.index = indexCounter++
return el
})
@ -522,7 +522,10 @@ if (isKanbanStack.value) {
<LazyGeneralAdvanceColorPicker
v-model="element.color"
:is-open="colorMenus[index]"
@input="(el:string) => (element.color = el)"
@input="(el:string) => {
element.color = el
optionChanged(element)
}"
></LazyGeneralAdvanceColorPicker>
</div>
</template>

52
packages/nc-gui/components/smartsheet/column/UITypesOptionsWithSearch.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { UITypes, UITypesName } from 'nocodb-sdk'
import { UITypes, UITypesName, readonlyMetaAllowedTypes } from 'nocodb-sdk'
const props = defineProps<{
options: typeof uiTypes
@ -11,6 +11,8 @@ const { options } = toRefs(props)
const searchQuery = ref('')
const { isMetaReadOnly } = useRoles()
const filteredOptions = computed(
() =>
options.value?.filter(
@ -24,8 +26,12 @@ const inputRef = ref()
const activeFieldIndex = ref(-1)
const isDisabledUIType = (type: UITypes) => {
return isMetaReadOnly.value && !readonlyMetaAllowedTypes.includes(type)
}
const onClick = (uidt: UITypes) => {
if (!uidt) return
if (!uidt || isDisabledUIType(uidt)) return
emits('selected', uidt)
}
@ -94,26 +100,36 @@ onMounted(() => {
{{ options.length ? $t('title.noResultsMatchedYourSearch') : 'The list is empty' }}
</div>
<div
<GeneralSourceRestrictionTooltip
v-for="(option, index) in filteredOptions"
:key="index"
class="flex w-full py-2 items-center justify-between px-2 hover:bg-gray-100 cursor-pointer rounded-md"
:class="[
`nc-column-list-option-${index}`,
{
'bg-gray-100 nc-column-list-option-active': activeFieldIndex === index,
},
]"
:data-testid="option.name"
@click="onClick(option.name)"
:message="$t('tooltip.typeNotAllowed')"
:enabled="isDisabledUIType(option.name)"
>
<div class="flex gap-2 items-center">
<component :is="option.icon" class="text-gray-700 w-4 h-4" />
<div class="flex-1 text-sm">{{ UITypesName[option.name] }}</div>
<span v-if="option.deprecated" class="!text-xs !text-gray-300">({{ $t('general.deprecated') }})</span>
<div
class="flex w-full py-2 items-center justify-between px-2 rounded-md"
:class="[
`nc-column-list-option-${index}`,
{
'hover:bg-gray-100 cursor-pointer': !isDisabledUIType(option.name),
'bg-gray-100 nc-column-list-option-active': activeFieldIndex === index && !isDisabledUIType(option.name),
'!text-gray-400 cursor-not-allowed': isDisabledUIType(option.name),
},
]"
:data-testid="option.name"
@click="onClick(option.name)"
>
<div class="flex gap-2 items-center">
<component
:is="option.icon"
class="w-4 h-4"
:class="isDisabledUIType(option.name) ? '!text-gray-400' : 'text-gray-700'"
/>
<div class="flex-1 text-sm">{{ UITypesName[option.name] }}</div>
<span v-if="option.deprecated" class="!text-xs !text-gray-300">({{ $t('general.deprecated') }})</span>
</div>
</div>
</div>
</GeneralSourceRestrictionTooltip>
</div>
</div>
</template>

27
packages/nc-gui/components/smartsheet/details/Fields.vue

@ -1,7 +1,14 @@
<script setup lang="ts">
import { diff } from 'deep-object-diff'
import { message } from 'ant-design-vue'
import { UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import {
UITypes,
isLinksOrLTAR,
isSystemColumn,
isVirtualCol,
partialUpdateAllowedTypes,
readonlyMetaAllowedTypes,
} from 'nocodb-sdk'
import type { ColumnType, FilterType, SelectOptionsType } from 'nocodb-sdk'
import Draggable from 'vuedraggable'
import { onKeyDown, useMagicKeys } from '@vueuse/core'
@ -182,7 +189,23 @@ const setFieldMoveHook = (field: TableExplorerColumn, before = false) => {
}
}
const { isMetaReadOnly } = useRoles()
const isColumnUpdateAllowed = (column: ColumnType) => {
if (
isMetaReadOnly.value &&
!readonlyMetaAllowedTypes.includes(column?.uidt) &&
!partialUpdateAllowedTypes.includes(column?.uidt)
)
return false
return true
}
const changeField = (field?: TableExplorerColumn, event?: MouseEvent) => {
if (field?.id && field?.uidt && !isColumnUpdateAllowed(field)) {
return message.info(t('msg.info.schemaReadOnly'))
}
if (field && field?.pk) {
// Editing primary key not supported
message.info(t('msg.info.editingPKnotSupported'))
@ -1003,7 +1026,7 @@ watch(
<div
v-if="field.title.toLowerCase().includes(searchQuery.toLowerCase()) && !field.pv"
class="flex px-2 hover:bg-gray-100 first:rounded-t-lg border-b-1 last:rounded-b-none border-gray-200 pl-5 group"
:class="` ${compareCols(field, activeField) ? 'selected' : ''}`"
:class="{ 'selected': compareCols(field, activeField), 'cursor-not-allowed': !isColumnUpdateAllowed(field) }"
:data-testid="`nc-field-item-${fieldState(field)?.title || field.title}`"
@click="changeField(field, $event)"
>

6
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -959,7 +959,7 @@ export default {
<GeneralDeleteModal v-model:visible="showDeleteRowModal" entity-name="Record" :on-delete="() => onConfirmDeleteRowClick()">
<template #entity-preview>
<span>
<div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700">
<div class="text-ellipsis overflow-hidden select-none w-full pl-1.75 break-keep whitespace-nowrap">
<LazySmartsheetPlainCell v-model="displayValue" :column="displayField" />
</div>
@ -978,9 +978,9 @@ export default {
{{ $t('activity.doYouWantToSaveTheChanges') }}
</div>
<div class="flex flex-row justify-end gap-x-2 mt-5">
<NcButton type="secondary" @click="discardPreventModal">{{ $t('labels.discard') }}</NcButton>
<NcButton type="secondary" size="small" @click="discardPreventModal">{{ $t('labels.discard') }}</NcButton>
<NcButton key="submit" type="primary" :loading="isSaving" @click="saveChanges">
<NcButton key="submit" type="primary" size="small" :loading="isSaving" @click="saveChanges">
{{ $t('tooltip.saveChanges') }}
</NcButton>
</div>

500
packages/nc-gui/components/smartsheet/grid/GroupBy.vue

@ -31,8 +31,26 @@ const emits = defineEmits(['update:paginationData'])
const vGroup = useVModel(props, 'group', emits)
const meta = inject(MetaInj, ref())
const scrollLeft = toRef(props, 'scrollLeft')
const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore())
const { gridViewCols } = useViewColumnsOrThrow()
const displayField = computed(() => {
return meta.value?.columns?.find((c) => c.pv)
})
const viewDisplayField = computed(() => {
if (!displayField.value || !displayField.value.id)
return {
width: '100px',
}
return gridViewCols.value[displayField.value.id]
})
const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
const _loadGroupData = async (group: Group, force?: boolean, params?: any) => {
@ -131,8 +149,8 @@ const scrollBump = computed<number>(() => {
} else {
if (props.scrollLeft && props.viewWidth && scrollable.value) {
const scrollWidth = scrollable.value.scrollWidth + 12 + 12
if (props.scrollLeft + props.viewWidth > scrollWidth) {
return scrollWidth - props.viewWidth
if (props.scrollLeft + props.viewWidth + 20 > scrollWidth) {
return scrollWidth - props.viewWidth - 20
}
return Math.max(Math.min(scrollWidth - props.viewWidth, (props.scrollLeft ?? 0) - 12), 0)
}
@ -203,215 +221,359 @@ const shouldRenderCell = (column) =>
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(column?.uidt)
const expandGroup = (key: string) => {
if (Array.isArray(_activeGroupKeys.value)) {
_activeGroupKeys.value.push(`group-panel-${key}`)
} else {
_activeGroupKeys.value = [`group-panel-${key}`]
}
findAndLoadSubGroup(`group-panel-${key}`)
}
const collapseGroup = (key: string) => {
if (Array.isArray(_activeGroupKeys.value)) {
_activeGroupKeys.value = _activeGroupKeys.value.filter((k) => k !== `group-panel-${key}`)
} else {
_activeGroupKeys.value = []
}
}
const collapseAllGroup = () => {
_activeGroupKeys.value = []
}
const expandAllGroup = () => {
_activeGroupKeys.value = vGroup.value.children?.map((g) => `group-panel-${g.key}`) ?? []
if (vGroup.value.children) {
vGroup.value.children.forEach((g) => {
findAndLoadSubGroup(`group-panel-${g.key}`)
})
}
}
const computedWidth = computed(() => {
// 55 is padding and margin of column header. 9 is padding of each level of nesting
const baseValue = Number((viewDisplayField.value?.width ?? '').replace('px', '')) + 55 + props.maxDepth * 9
const maxDepth = props.maxDepth ?? 1
// The _scrollLeft is calculated only on root and passed down to nested groups
const tempScrollLeft = vGroup.value.root ? _scrollLeft.value ?? 0 : scrollLeft.value ?? 0
const getSubGroupWidth = (depth: number) => {
switch (depth) {
case 3:
return `${baseValue - 26}px`
case 2:
return `${baseValue - 17}px`
case 1:
return `${baseValue - 8}px`
default:
return `${baseValue}px`
}
}
if (_depth === 0) {
if (tempScrollLeft < 29) {
// The equation is calculated on trial and error basis
return `${baseValue + tempScrollLeft - (53 / 29) * tempScrollLeft}px`
}
return getSubGroupWidth(maxDepth)
}
if (_depth === 1) {
if (tempScrollLeft < 30) {
// The equation is calculated on trial and error basis
return `${baseValue + tempScrollLeft - 9 - (23 / 15) * tempScrollLeft}px`
}
return getSubGroupWidth(maxDepth)
}
if (_depth === 2) {
if (tempScrollLeft < 15) {
// The equation is calculated on trial and error basis
return `${baseValue + tempScrollLeft - 18 - (19 / 15) * tempScrollLeft}px`
}
return getSubGroupWidth(maxDepth)
}
// TODO: We only allow 3 levels of nesting for now
// We only allow 3 levels of nesting for now
// If we add support for more levels, we need to adjust the width calculation
// for each level
return `${baseValue}px`
})
const bgColor = computed(() => {
if (props.maxDepth === 3) {
switch (_depth) {
case 2:
return '#F9F9FA'
case 1:
return '#F4F4F5'
default:
return '#F1F1F1'
}
}
if (props.maxDepth === 2) {
switch (_depth) {
case 1:
return '#F9F9FA'
default:
return '#F4F4F5'
}
}
if (props.maxDepth === 1) {
return '#F9F9FA'
}
return '#F9F9FA'
})
</script>
<template>
<div class="flex flex-col h-full w-full">
<div
ref="wrapper"
class="flex flex-col h-full w-full scrollbar-thin-dull overflow-auto"
:style="`${!vGroup.root && vGroup.nested ? 'padding-left: 12px; padding-right: 12px;' : ''}`"
@scroll="onScroll"
>
<div
ref="scrollable"
class="flex flex-col"
:class="{ 'my-2': vGroup.root !== true }"
:style="`${vGroup.root === true ? 'width: fit-content' : 'width: 100%'}`"
>
<div v-if="vGroup.root === true" class="flex sticky top-0 z-5">
<div
class="bumper mb-2"
style="background-color: #f9f9fa; border-color: #e7e7e9; border-bottom-width: 1px"
:style="{ 'padding-left': `${(maxDepth || 1) * 13}px` }"
></div>
<Table ref="tableHeader" class="mb-2" :data="[]" :header-only="true" />
</div>
<div :class="{ 'px-[12px]': vGroup.root === true }">
<a-collapse
v-model:activeKey="_activeGroupKeys"
class="!bg-transparent w-full nc-group-wrapper"
:bordered="false"
destroy-inactive-panel
@change="findAndLoadSubGroup"
<div
ref="wrapper"
:class="{ 'overflow-y-auto': vGroup.root === true }"
class="h-full"
:style="`${!vGroup.root && vGroup.nested ? 'padding-left: 8px; padding-right: 8px;' : ''}`"
@scroll="onScroll"
>
<div ref="scrollable" :style="`${vGroup.root === true ? 'width: fit-content' : 'width: 100%'}`">
<div v-if="vGroup.root === true" class="flex sticky top-0 z-5">
<div
class="border-b-1 border-gray-200 mb-2"
style="background-color: #f4f4f5"
:style="{ 'padding-left': `${(maxDepth || 1) * 9}px` }"
></div>
<Table ref="tableHeader" class="mb-2" :data="[]" :hide-checkbox="true" :header-only="true" />
</div>
<div :class="{ 'pl-2': vGroup.root === true }">
<a-collapse
v-model:activeKey="_activeGroupKeys"
class="nc-group-wrapper !rounded-lg"
:bordered="false"
@change="findAndLoadSubGroup"
>
<a-collapse-panel
v-for="[i, grp] of Object.entries(vGroup?.children ?? [])"
:key="`group-panel-${grp.key}`"
class="!border-1 border-gray-300 nc-group rounded-[8px] mb-2"
:style="`background: ${bgColor};`"
:show-arrow="false"
>
<a-collapse-panel
v-for="[i, grp] of Object.entries(vGroup?.children ?? [])"
:key="`group-panel-${grp.key}`"
class="!border-1 nc-group rounded-[12px]"
:class="{ 'mb-4': vGroup.children && +i !== vGroup.children.length - 1 }"
:style="`background: rgb(${245 - _depth * 10}, ${245 - _depth * 10}, ${245 - _depth * 10})`"
:show-arrow="false"
>
<template #header>
<div class="flex !sticky left-[15px]">
<template #header>
<div
:class="{
'!rounded-b-none': activeGroups.includes(grp.key),
'border-b-1': _depth === (maxDepth ?? 1) - 1 && activeGroups.includes(grp.key),
}"
class="flex !sticky w-full items-center rounded-b-lg group select-none transition-all !rounded-t-[8px] !h-10"
>
<div
:style="`width:${computedWidth};`"
class="!sticky flex justify-between !h-10 border-r-1 pr-2 border-gray-300 overflow-clip items-center !left-2"
>
<div class="flex items-center">
<span role="img" aria-label="right" class="anticon anticon-right ant-collapse-arrow">
<NcButton class="!border-0 !shadow-none !bg-transparent !hover:bg-transparent" type="secondary" size="small">
<GeneralIcon
icon="chevronDown"
class="transition-all"
:style="`${activeGroups.includes(grp.key) ? 'transform: rotate(360deg)' : 'transform: rotate(270deg)'}`"
></GeneralIcon>
</span>
</div>
<div class="flex items-center">
<div class="flex flex-col">
<div class="flex gap-2">
<div class="text-xs nc-group-column-title">
{{ grp.column.title }}
</div>
<div class="text-xs text-gray-400 nc-group-row-count">({{ $t('datatype.Count') }}: {{ grp.count }})</div>
</div>
<div class="flex mt-1">
<template v-if="grp.column.uidt === 'MultiSelect'">
<a-tag
v-for="[tagIndex, tag] of Object.entries(grp.key.split(','))"
:key="`panel-tag-${grp.column.id}-${tag}`"
class="!py-0 !px-[12px] !rounded-[12px]"
:color="grp.color.split(',')[+tagIndex]"
>
<span
class="nc-group-value"
:style="{
'color': tinycolor.isReadable(grp.color.split(',')[+tagIndex] || '#ccc', '#fff', {
level: 'AA',
size: 'large',
})
? '#fff'
: tinycolor
.mostReadable(grp.color.split(',')[+tagIndex] || '#ccc', ['#0b1d05', '#fff'])
.toHex8String(),
'font-size': '14px',
'font-weight': 500,
}"
>
{{ tag in GROUP_BY_VARS.VAR_TITLES ? GROUP_BY_VARS.VAR_TITLES[tag] : tag }}
</span>
</a-tag>
</template>
<div
v-else-if="!(grp.key in GROUP_BY_VARS.VAR_TITLES) && shouldRenderCell(grp.column)"
class="flex min-w-[100px] flex-wrap"
>
<template v-for="(val, ind) of parseKey(grp)" :key="ind">
<GroupByLabel v-if="val" :column="grp.column" :model-value="val" />
<span v-else class="text-gray-400">No mapped value</span>
</template>
</div>
/>
</NcButton>
<div class="flex">
<template v-if="grp.column.uidt === 'MultiSelect'">
<a-tag
v-else
:key="`panel-tag-${grp.column.id}-${grp.key}`"
class="!py-0 !px-[12px]"
:class="`${grp.column.uidt === 'SingleSelect' ? '!rounded-[12px]' : '!rounded-[6px]'}`"
:color="grp.color"
v-for="[tagIndex, tag] of Object.entries(grp.key.split(','))"
:key="`panel-tag-${grp.column.id}-${tag}`"
class="!py-0 !px-[12px] !rounded-[12px]"
:color="grp.color.split(',')[+tagIndex]"
>
<span
class="nc-group-value"
:style="{
'color': tinycolor.isReadable(grp.color || '#ccc', '#fff', {
'color': tinycolor.isReadable(grp.color.split(',')[+tagIndex] || '#ccc', '#fff', {
level: 'AA',
size: 'large',
})
? '#fff'
: tinycolor.mostReadable(grp.color || '#ccc', ['#0b1d05', '#fff']).toHex8String(),
: tinycolor
.mostReadable(grp.color.split(',')[+tagIndex] || '#ccc', ['#0b1d05', '#fff'])
.toHex8String(),
'font-size': '14px',
'font-weight': 500,
}"
>
<template v-if="grp.key in GROUP_BY_VARS.VAR_TITLES">{{
GROUP_BY_VARS.VAR_TITLES[grp.key]
}}</template>
<template v-else>
{{ parseKey(grp)?.join(', ') }}
</template>
{{ tag in GROUP_BY_VARS.VAR_TITLES ? GROUP_BY_VARS.VAR_TITLES[tag] : tag }}
</span>
</a-tag>
</template>
<div
v-else-if="!(grp.key in GROUP_BY_VARS.VAR_TITLES) && shouldRenderCell(grp.column)"
class="flex min-w-[100px] flex-wrap"
>
<template v-for="(val, ind) of parseKey(grp)" :key="ind">
<GroupByLabel v-if="val" :column="grp.column" :model-value="val" />
<span v-else class="text-gray-400">No mapped value</span>
</template>
</div>
<a-tag
v-else
:key="`panel-tag-${grp.column.id}-${grp.key}`"
class="!py-0 !px-[12px]"
:class="`${grp.column.uidt === 'SingleSelect' ? '!rounded-[12px]' : '!rounded-[6px]'}`"
:color="grp.color"
>
<span
class="nc-group-value font-semibold text-[13px]"
:style="{
color: tinycolor.isReadable(grp.color || '#ccc', '#fff', {
level: 'AA',
size: 'large',
})
? '#fff'
: tinycolor.mostReadable(grp.color || '#ccc', ['#0b1d05', '#fff']).toHex8String(),
}"
>
<template v-if="grp.key in GROUP_BY_VARS.VAR_TITLES">{{ GROUP_BY_VARS.VAR_TITLES[grp.key] }}</template>
<template v-else>
{{ parseKey(grp)?.join(', ') }}
</template>
</span>
</a-tag>
</div>
</div>
<div class="flex items-center">
<div class="text-xs group-hover:hidden text-gray-500 nc-group-row-count">
<span>
{{ $t('datatype.Count') }}
</span>
<span class="text-[#374151] ml-2"> {{ grp.count }} </span>
</div>
<NcDropdown class="!hidden !group-hover:block">
<NcButton size="small" type="text" @click.stop>
<GeneralIcon icon="threeDotVertical" />
</NcButton>
<template #overlay>
<NcMenu>
<NcMenuItem v-if="activeGroups.includes(grp.key)" @click="collapseGroup(grp.key)">
<GeneralIcon icon="minimize" />
Collapse group
</NcMenuItem>
<NcMenuItem v-else @click="expandGroup(grp.key)">
<GeneralIcon icon="maximize" />
Expand group
</NcMenuItem>
<NcMenuItem @click="expandAllGroup">
<GeneralIcon icon="maximizeAll" />
Expand all
</NcMenuItem>
<NcMenuItem @click="collapseAllGroup">
<GeneralIcon icon="minimizeAll" />
Collapse all
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</div>
</div>
</template>
<GroupByTable
v-if="!grp.nested && grp.rows"
:group="grp"
:load-groups="loadGroups"
:load-group-data="_loadGroupData"
:load-group-page="loadGroupPage"
:group-wrapper-change-page="groupWrapperChangePage"
:row-height="rowHeight"
:redistribute-rows="redistributeRows"
:expand-form="expandForm"
:pagination-fixed-size="fullPage ? props.viewWidth : undefined"
:pagination-hide-sidebars="true"
:scroll-left="props.scrollLeft || _scrollLeft"
:view-width="viewWidth"
:scrollable="scrollable"
:full-page="fullPage"
/>
<GroupBy
v-else
:group="grp"
:load-groups="loadGroups"
:load-group-data="_loadGroupData"
:load-group-page="loadGroupPage"
:group-wrapper-change-page="groupWrapperChangePage"
:row-height="rowHeight"
:redistribute-rows="redistributeRows"
:expand-form="expandForm"
:view-width="viewWidth"
:depth="_depth + 1"
:scroll-left="scrollBump"
:full-page="fullPage"
/>
</a-collapse-panel>
</a-collapse>
</div>
</div>
</template>
<GroupByTable
v-if="!grp.nested && grp.rows"
:group="grp"
:max-depth="maxDepth"
:depth="depth"
:load-groups="loadGroups"
:load-group-data="_loadGroupData"
:load-group-page="loadGroupPage"
:group-wrapper-change-page="groupWrapperChangePage"
:row-height="rowHeight"
:redistribute-rows="redistributeRows"
:expand-form="expandForm"
:pagination-fixed-size="fullPage ? props.viewWidth : undefined"
:pagination-hide-sidebars="true"
:scroll-left="props.scrollLeft || _scrollLeft"
:view-width="viewWidth"
:scrollable="scrollable"
:full-page="fullPage"
/>
<GroupBy
v-else
:group="grp"
:load-groups="loadGroups"
:load-group-data="_loadGroupData"
:load-group-page="loadGroupPage"
:group-wrapper-change-page="groupWrapperChangePage"
:row-height="rowHeight"
:redistribute-rows="redistributeRows"
:expand-form="expandForm"
:view-width="viewWidth"
:depth="_depth + 1"
:max-depth="maxDepth"
:scroll-left="scrollBump"
:full-page="fullPage"
/>
</a-collapse-panel>
</a-collapse>
</div>
</div>
<LazySmartsheetPagination
v-if="vGroup.root"
v-model:pagination-data="vGroup.paginationData"
align-count-on-right
custom-label="groups"
show-api-timing
:change-page="(p: number) => groupWrapperChangePage(p, vGroup)"
:style="`${props.depth && props.depth > 0 ? 'border-radius: 0 0 12px 12px !important;' : ''}`"
></LazySmartsheetPagination>
<LazySmartsheetPagination
v-else
v-model:pagination-data="vGroup.paginationData"
align-count-on-right
custom-label="groups"
show-api-timing
:change-page="(p: number) => groupWrapperChangePage(p, vGroup)"
:hide-sidebars="true"
:style="`${props.depth && props.depth > 0 ? 'border-radius: 0 0 12px 12px !important;' : ''}margin-left: ${scrollBump}px;`"
:fixed-size="fullPage ? props.viewWidth : undefined"
></LazySmartsheetPagination>
</div>
<LazySmartsheetPagination
v-if="vGroup.root"
v-model:pagination-data="vGroup.paginationData"
align-count-on-right
custom-label="groups"
show-api-timing
:change-page="(p: number) => groupWrapperChangePage(p, vGroup)"
:style="`${props.depth && props.depth > 0 ? 'border-radius: 0 0 8px 8px !important;' : ''}`"
></LazySmartsheetPagination>
<LazySmartsheetPagination
v-else
v-model:pagination-data="vGroup.paginationData"
align-count-on-right
custom-label="groups"
show-api-timing
:change-page="(p: number) => groupWrapperChangePage(p, vGroup)"
:hide-sidebars="true"
:style="`${
props.depth && props.depth > 0
? 'border-radius: 0 0 8px 8px !important; background: transparent; border-top: 0px; height: 24px'
: ''
}`"
:fixed-size="undefined"
></LazySmartsheetPagination>
</template>
<style scoped lang="scss">
:deep(.ant-collapse-content > .ant-collapse-content-box) {
padding: 0px !important;
border-radius: 0 0 12px 12px !important;
border-radius: 0 0 8px 8px !important;
}
:deep(.ant-collapse) {
@apply !border-gray-300 !bg-transparent;
}
:deep(.ant-collapse-item > .ant-collapse-header) {
border-radius: 12px !important;
background: white;
:deep(.ant-collapse-item) {
@apply !border-gray-300;
}
:deep(.ant-collapse-header) {
@apply !p-0 !border-gray-300 !rounded-lg;
}
:deep(.ant-collapse-item-active > .ant-collapse-header) {
border-radius: 12px 12px 0 0 !important;
background: white;
border-bottom: solid 1px lightgray;
border-radius: 8px 8px 0 0 !important;
}
:deep(.ant-collapse-borderless > .ant-collapse-item:last-child) {
border-radius: 12px !important;
}
:deep(.ant-collapse > .ant-collapse-item:last-child) {
border-radius: 12px !important;
border-radius: 8px !important;
}
</style>

13
packages/nc-gui/components/smartsheet/grid/GroupByTable.vue

@ -118,22 +118,11 @@ reloadViewDataHook?.on(reloadTableData)
provide(IsGroupByInj, ref(true))
const scrollBump = computed<number>(() => {
if (props.scrollLeft && props.viewWidth && props.scrollable) {
const scrollWidth = props.scrollable.scrollWidth
if (props.scrollLeft + props.viewWidth > scrollWidth) {
return scrollWidth - props.viewWidth
}
return Math.max(Math.min(scrollWidth - props.viewWidth, (props.scrollLeft ?? 0) - 24), 0)
}
return 0
})
const pagination = computed(() => {
return {
fixedSize: props.paginationFixedSize ? props.paginationFixedSize - 2 : undefined,
hideSidebars: props.paginationHideSidebars,
extraStyle: `margin-left: ${scrollBump.value}px;`,
extraStyle: 'background: transparent !important; border-top: 0px;',
}
})

291
packages/nc-gui/components/smartsheet/grid/PaginationV2.vue

@ -0,0 +1,291 @@
<script setup lang="ts">
import axios from 'axios'
import { type PaginatedType, UITypes } from 'nocodb-sdk'
const props = defineProps<{
scrollLeft?: number
paginationData: PaginatedType
changePage: (page: number) => void
}>()
const emits = defineEmits(['update:paginationData'])
const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore())
const isLocked = inject(IsLockedInj, ref(false))
const { changePage } = props
const vPaginationData = useVModel(props, 'paginationData', emits)
const { loadViewAggregate, updateAggregate, getAggregations, visibleFieldsComputed, displayFieldComputed } =
useViewAggregateOrThrow()
const scrollLeft = toRef(props, 'scrollLeft')
const reloadViewDataHook = inject(ReloadViewDataHookInj)
const containerElement = ref()
watch(
scrollLeft,
(value) => {
if (containerElement.value) {
containerElement.value.scrollLeft = value
}
},
{
immediate: true,
},
)
reloadViewDataHook?.on(async () => {
await loadViewAggregate()
})
const count = computed(() => vPaginationData.value?.totalRows ?? Infinity)
const page = computed({
get: () => vPaginationData?.value?.page ?? 1,
set: async (p) => {
isPaginationLoading.value = true
try {
await changePage?.(p)
isPaginationLoading.value = false
} catch (e) {
if (axios.isCancel(e)) {
return
}
isPaginationLoading.value = false
}
},
})
const size = computed({
get: () => vPaginationData.value?.pageSize ?? 25,
set: (size: number) => {
if (vPaginationData.value) {
// if there is no change in size then return
if (vPaginationData.value?.pageSize && vPaginationData.value?.pageSize === size) {
return
}
vPaginationData.value.pageSize = size
if (vPaginationData.value.totalRows && page.value * size < vPaginationData.value.totalRows) {
changePage?.(page.value)
} else {
changePage?.(1)
}
}
},
})
const renderAltOrOptlKey = () => {
return isMac() ? '⌥' : 'ALT'
}
</script>
<template>
<div ref="containerElement" class="bg-gray-50 w-full pr-1 border-t-1 border-gray-200 overflow-x-hidden no-scrollbar flex h-9">
<div class="sticky flex items-center bg-gray-50 left-0">
<NcDropdown
:disabled="[UITypes.SpecificDBType, UITypes.ForeignKey].includes(displayFieldComputed.column?.uidt!) || isLocked"
overlay-class-name="max-h-96 relative scroll-container nc-scrollbar-md overflow-auto"
>
<div
v-if="displayFieldComputed.field && displayFieldComputed.column?.id"
class="flex items-center overflow-x-hidden hover:bg-gray-100 cursor-pointer text-gray-500 justify-end transition-all transition-linear px-3 py-2"
:style="{
'min-width': displayFieldComputed?.width,
'max-width': displayFieldComputed?.width,
'width': displayFieldComputed?.width,
}"
>
<div class="flex relative justify-between gap-2 w-full">
<div v-if="isViewDataLoading" class="nc-pagination-skeleton flex justify-center item-center min-h-10 min-w-16 w-16">
<a-skeleton :active="true" :title="true" :paragraph="false" class="w-16 max-w-16" />
</div>
<NcTooltip v-else class="flex sticky items-center h-full">
<template #title> {{ count }} {{ count !== 1 ? $t('objects.records') : $t('objects.record') }} </template>
<span
data-testid="grid-pagination"
class="text-gray-500 text-ellipsis overflow-hidden pl-1 truncate nc-grid-row-count caption text-xs text-nowrap"
>
{{ Intl.NumberFormat('en', { notation: 'compact' }).format(count) }}
{{ count !== 1 ? $t('objects.records') : $t('objects.record') }}
</span>
</NcTooltip>
<template v-if="![UITypes.SpecificDBType, UITypes.ForeignKey].includes(displayFieldComputed.column?.uidt!)">
<div
v-if="!displayFieldComputed.field?.aggregation || displayFieldComputed.field?.aggregation === 'none'"
class="text-gray-500 opacity-0 transition group-hover:opacity-100"
>
<GeneralIcon class="text-gray-500" icon="arrowDown" />
<span class="text-[10px] font-semibold"> Summary </span>
</div>
<NcTooltip
v-else-if="displayFieldComputed.value !== undefined"
:style="{
maxWidth: `${displayFieldComputed?.width}`,
}"
>
<div style="direction: rtl" class="flex gap-2 text-nowrap truncate overflow-hidden items-center">
<span class="text-gray-600 text-[12px] font-semibold">
{{
formatAggregation(
displayFieldComputed.field.aggregation,
displayFieldComputed.value,
displayFieldComputed.column,
)
}}
</span>
<span class="text-gray-500 text-[12px] leading-4">
{{ $t(`aggregation.${displayFieldComputed.field.aggregation}`) }}
</span>
</div>
<template #title>
<div class="flex gap-2 text-nowrap overflow-hidden items-center">
<span class="text-[12px] leading-4">
{{ $t(`aggregation.${displayFieldComputed.field.aggregation}`) }}
</span>
<span class="text-[12px] font-semibold">
{{
formatAggregation(
displayFieldComputed.field.aggregation,
displayFieldComputed.value,
displayFieldComputed.column,
)
}}
</span>
</div>
</template>
</NcTooltip>
</template>
</div>
</div>
<template #overlay>
<NcMenu v-if="displayFieldComputed.field && displayFieldComputed.column?.id">
<NcMenuItem
v-for="(agg, index) in getAggregations(displayFieldComputed.column)"
:key="index"
@click="updateAggregate(displayFieldComputed.column.id, agg)"
>
<div class="flex !w-full text-[13px] text-gray-800 items-center justify-between">
{{ $t(`aggregation_type.${agg}`) }}
<GeneralIcon v-if="displayFieldComputed.field?.aggregation === agg" class="text-brand-500" icon="check" />
</div>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</div>
<template v-for="({ field, width, column, value }, index) in visibleFieldsComputed" :key="index">
<NcDropdown
v-if="field && column?.id"
:disabled="[UITypes.SpecificDBType, UITypes.ForeignKey].includes(column?.uidt!) || isLocked"
overlay-class-name="max-h-96 relative scroll-container nc-scrollbar-md overflow-auto"
>
<div
class="flex items-center overflow-x-hidden justify-end group hover:bg-gray-100 cursor-pointer text-gray-500 transition-all transition-linear px-3 py-2"
:style="{
'min-width': width,
'max-width': width,
'width': width,
}"
>
<template v-if="![UITypes.SpecificDBType, UITypes.ForeignKey].includes(column?.uidt!)">
<div
v-if="field?.aggregation === 'none' || field?.aggregation === null"
class="text-gray-500 opacity-0 transition group-hover:opacity-100"
>
<GeneralIcon class="text-gray-500" icon="arrowDown" />
<span class="text-[10px] font-semibold"> Summary </span>
</div>
<NcTooltip
v-else-if="value !== undefined"
:style="{
maxWidth: `${field?.width}px`,
}"
>
<div class="flex gap-2 truncate text-nowrap overflow-hidden items-center">
<span class="text-gray-500 text-[12px] leading-4">
{{ $t(`aggregation.${field.aggregation}`).replace('Percent ', '') }}
</span>
<span class="text-gray-600 font-semibold text-[12px]">
{{ formatAggregation(field.aggregation, value, column) }}
</span>
</div>
<template #title>
<div class="flex gap-2 text-nowrap overflow-hidden items-center">
<span class="text-[12px] leading-4">
{{ $t(`aggregation.${field.aggregation}`).replace('Percent ', '') }}
</span>
<span class="font-semibold text-[12px]">
{{ formatAggregation(field.aggregation, value, column) }}
</span>
</div>
</template>
</NcTooltip>
</template>
</div>
<template #overlay>
<NcMenu>
<NcMenuItem v-for="(agg, index) in getAggregations(column)" :key="index" @click="updateAggregate(column.id, agg)">
<div class="flex !w-full text-[13px] text-gray-800 items-center justify-between">
{{ $t(`aggregation_type.${agg}`) }}
<GeneralIcon v-if="field?.aggregation === agg" class="text-brand-500" icon="check" />
</div>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</template>
<div class="!pl-8 pr-60 !w-8 h-1"></div>
<div class="fixed h-9 bg-white border-l-1 border-gray-200 px-1 flex items-center right-0">
<NcPaginationV2
v-if="count !== Infinity"
v-model:current="page"
v-model:page-size="size"
class="xs:(mr-2)"
:total="+count"
entity-name="grid"
:prev-page-tooltip="`${renderAltOrOptlKey()}+←`"
:next-page-tooltip="`${renderAltOrOptlKey()}+→`"
:first-page-tooltip="`${renderAltOrOptlKey()}+↓`"
:last-page-tooltip="`${renderAltOrOptlKey()}+↑`"
:show-size-changer="true"
/>
</div>
</div>
</template>
<style scoped lang="scss">
:deep(.nc-menu-item-inner) {
@apply w-full;
}
.nc-grid-pagination-wrapper {
.ant-pagination-item-active {
a {
@apply text-sm !text-gray-700 !hover:text-gray-800;
}
}
}
</style>
<style lang="scss"></style>

313
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -42,6 +42,7 @@ const props = defineProps<{
) => Promise<void>
headerOnly?: boolean
hideHeader?: boolean
hideCheckbox?: boolean
pagination?: {
fixedSize?: number
hideSidebars?: boolean
@ -146,6 +147,8 @@ const { paste } = usePaste()
const { addLTARRef, syncLTARRefs, clearLTARCell, cleaMMCell } = useSmartsheetLtarHelpersOrThrow()
const { loadViewAggregate } = useViewAggregateOrThrow()
// #Refs
const smartTable = ref(null)
@ -165,7 +168,7 @@ const isViewColumnsLoading = computed(() => _isViewColumnsLoading.value || !meta
const resizingColumn = ref(false)
// #Permissions
const { isUIAllowed } = useRoles()
const { isUIAllowed, isDataReadOnly } = useRoles()
const hasEditPermission = computed(() => isUIAllowed('dataEdit'))
const isAddingColumnAllowed = computed(() => !readOnly.value && !isLocked.value && isUIAllowed('fieldAdd') && !isSqlView.value)
@ -208,7 +211,10 @@ const isGridCellMouseDown = ref(false)
// #Context Menu
const _contextMenu = ref(false)
const contextMenu = computed({
get: () => _contextMenu.value,
get: () => {
if (props.data?.some((r) => r.rowMeta.selected) && isDataReadOnly.value) return false
return _contextMenu.value
},
set: (val) => {
_contextMenu.value = val
},
@ -232,7 +238,13 @@ const isKeyDown = ref(false)
// #Cell - 1
async function clearCell(ctx: { row: number; col: number } | null, skipUpdate = false) {
if (!ctx || !hasEditPermission.value || (!isLinksOrLTAR(fields.value[ctx.col]) && isVirtualCol(fields.value[ctx.col]))) return
if (
isDataReadOnly.value ||
!ctx ||
!hasEditPermission.value ||
(!isLinksOrLTAR(fields.value[ctx.col]) && isVirtualCol(fields.value[ctx.col]))
)
return
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if (colMeta.value[ctx.col].isReadonly) return
@ -915,7 +927,7 @@ const onNavigate = (dir: NavigateDir) => {
// #Cell - 2
async function clearSelectedRangeOfCells() {
if (!hasEditPermission.value) return
if (!hasEditPermission.value || isDataReadOnly.value) return
const start = selectedRange.start
const end = selectedRange.end
@ -971,6 +983,8 @@ const colPositions = computed(() => {
const scrollWrapper = computed(() => scrollParent.value || gridWrapper.value)
const scrollLeft = ref()
function scrollToCell(row?: number | null, col?: number | null) {
row = row ?? activeCell.row
col = col ?? activeCell.col
@ -1118,12 +1132,12 @@ const maxGridWidth = computed(() => {
// 64 for the row number column
// count first column twice because it's sticky
// 100 for add new column
return colPositions.value[colPositions.value.length - 1] + colPositions.value[1] + 64 + 100
return colPositions.value[colPositions.value.length - 1] + 64
})
const maxGridHeight = computed(() => {
// 2 extra rows for the add new row and the sticky header
return dataRef.value.length * rowHeightInPx[`${props.rowHeight}`] + 2 * rowHeightInPx[`${props.rowHeight}`]
return dataRef.value.length * rowHeightInPx[`${props.rowHeight}`]
})
const colSlice = ref({
@ -1277,6 +1291,7 @@ const selectedReadonly = computed(
const showFillHandle = computed(
() =>
!isDataReadOnly.value &&
!readOnly.value &&
!editEnabled.value &&
(!selectedRange.isEmpty() || (activeCell.row !== null && activeCell.col !== null)) &&
@ -1356,7 +1371,8 @@ async function reloadViewDataHandler(params: void | { shouldShowLoading?: boolea
let frame: number | null = null
useEventListener(scrollWrapper, 'scroll', () => {
useEventListener(scrollWrapper, 'scroll', (e) => {
scrollLeft.value = e.target.scrollLeft
if (frame) {
cancelAnimationFrame(frame)
}
@ -1508,7 +1524,7 @@ watch(
}
isViewDataLoading.value = true
try {
await loadData?.()
await Promise.allSettled([loadData?.(), loadViewAggregate()])
calculateSlices()
} catch (e) {
if (!axios.isCancel(e)) {
@ -1606,9 +1622,9 @@ onKeyStroke('ArrowDown', onDown)
<div ref="gridWrapper" class="nc-grid-wrapper min-h-0 flex-1 relative" :class="gridWrapperClass">
<div
v-show="isPaginationLoading && !headerOnly"
class="flex items-center justify-center absolute l-0 t-0 w-full h-full z-10 pb-10 pointer-events-none"
class="flex items-center justify-center bg-white/80 absolute l-0 t-0 w-full h-full z-10 pb-10 pointer-events-none"
>
<div class="flex flex-col justify-center gap-2">
<div class="flex flex-col items-center justify-center gap-2">
<GeneralLoader size="xlarge" />
<span class="text-center" v-html="loaderText"></span>
</div>
@ -1658,7 +1674,7 @@ onKeyStroke('ArrowDown', onDown)
}"
>
<div class="w-full h-full flex pl-2 pr-1 items-center" data-testid="nc-check-all">
<template v-if="!readOnly">
<template v-if="!readOnly && !hideCheckbox">
<div class="nc-no-label text-gray-500" :class="{ hidden: vSelectedAllRecords }">#</div>
<div
:class="{
@ -1870,10 +1886,27 @@ onKeyStroke('ArrowDown', onDown)
</a-dropdown>
</div>
</th>
<th
class="!border-0 relative !xs:hidden"
:style="{
borderWidth: '0px !important',
}"
>
<div
class="absolute top-0 w-[40px]"
:class="{
'left-[60px]': isAddingColumnAllowed,
'left-0': !isAddingColumnAllowed,
}"
>
&nbsp;
</div>
</th>
</tr>
</thead>
</table>
<div
v-if="!showSkeleton"
class="table-overlay"
:class="{ 'nc-grid-skeleton-loader': showSkeleton }"
:style="{
@ -1887,8 +1920,8 @@ onKeyStroke('ArrowDown', onDown)
:class="{
'mobile': isMobileMode,
'desktop': !isMobileMode,
'pr-60 pb-12': !headerOnly,
'w-full': dataRef.length === 0,
'pr-60 pb-12': !headerOnly && !isGroupBy,
}"
:style="{
transform: `translateY(${topOffset}px) translateX(${leftOffset}px)`,
@ -1961,10 +1994,7 @@ onKeyStroke('ArrowDown', onDown)
<span
v-if="row.rowMeta?.commentCount && expandForm"
v-e="['c:expanded-form:open']"
class="py-1 px-1 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
:style="{
backgroundColor: getEnumColorByIndex(row.rowMeta.commentCount || 0),
}"
class="px-1 rounded-md rounded-bl-none transition-all border-1 border-brand-200 text-xs cursor-pointer font-sembold select-none leading-5 text-brand-500 bg-brand-50"
@click="expandAndLooseFocus(row, state)"
>
{{ row.rowMeta.commentCount }}
@ -2167,7 +2197,9 @@ onKeyStroke('ArrowDown', onDown)
<template #overlay>
<NcMenu class="!rounded !py-0" @click="contextMenu = false">
<NcMenuItem
v-if="isEeUI && !contextMenuClosing && !contextMenuTarget && data.some((r) => r.rowMeta.selected)"
v-if="
isEeUI && !contextMenuClosing && !contextMenuTarget && data.some((r) => r.rowMeta.selected) && !isDataReadOnly
"
@click="emits('bulkUpdateDlg')"
>
<div v-e="['a:row:update-bulk']" class="flex gap-2 items-center">
@ -2177,7 +2209,7 @@ onKeyStroke('ArrowDown', onDown)
</NcMenuItem>
<NcMenuItem
v-if="!contextMenuClosing && !contextMenuTarget && data.some((r) => r.rowMeta.selected)"
v-if="!contextMenuClosing && !contextMenuTarget && data.some((r) => r.rowMeta.selected) && !isDataReadOnly"
class="nc-base-menu-item !text-red-600 !hover:bg-red-50"
data-testid="nc-delete-row"
@click="deleteSelectedRows"
@ -2224,7 +2256,7 @@ onKeyStroke('ArrowDown', onDown)
</NcMenuItem>
<NcMenuItem
v-if="contextMenuTarget && hasEditPermission"
v-if="contextMenuTarget && hasEditPermission && !isDataReadOnly"
class="nc-base-menu-item"
data-testid="context-menu-item-paste"
:disabled="selectedReadonly"
@ -2243,7 +2275,8 @@ onKeyStroke('ArrowDown', onDown)
contextMenuTarget &&
hasEditPermission &&
selectedRange.isSingleCell() &&
(isLinksOrLTAR(fields[contextMenuTarget.col]) || !cellMeta[0]?.[contextMenuTarget.col].isVirtualCol)
(isLinksOrLTAR(fields[contextMenuTarget.col]) || !cellMeta[0]?.[contextMenuTarget.col].isVirtualCol) &&
!isDataReadOnly
"
class="nc-base-menu-item"
:disabled="selectedReadonly"
@ -2258,7 +2291,7 @@ onKeyStroke('ArrowDown', onDown)
<!-- Clear cell -->
<NcMenuItem
v-else-if="contextMenuTarget && hasEditPermission"
v-else-if="contextMenuTarget && hasEditPermission && !isDataReadOnly"
class="nc-base-menu-item"
:disabled="selectedReadonly"
data-testid="context-menu-item-clear"
@ -2280,7 +2313,7 @@ onKeyStroke('ArrowDown', onDown)
</NcMenuItem>
</template>
<template v-if="hasEditPermission">
<template v-if="hasEditPermission && !isDataReadOnly">
<NcDivider v-if="!(!contextMenuClosing && !contextMenuTarget && data.some((r) => r.rowMeta.selected))" />
<NcMenuItem
v-if="contextMenuTarget && (selectedRange.isSingleCell() || selectedRange.isSingleRow())"
@ -2310,102 +2343,170 @@ onKeyStroke('ArrowDown', onDown)
</NcDropdown>
</div>
<LazySmartsheetPagination
v-if="headerOnly !== true && paginationDataRef"
:key="`nc-pagination-${isMobileMode}`"
v-model:pagination-data="paginationDataRef"
:show-api-timing="!isGroupBy"
align-count-on-right
:align-left="isGroupBy"
:change-page="changePage"
:hide-sidebars="paginationStyleRef?.hideSidebars === true"
:fixed-size="paginationStyleRef?.fixedSize"
:extra-style="paginationStyleRef?.extraStyle"
:show-size-changer="!isGroupBy"
>
<template #add-record>
<div v-if="isAddingEmptyRowAllowed && !showSkeleton" class="flex ml-1">
<div class="relative">
<LazySmartsheetPagination
v-if="headerOnly !== true && paginationDataRef && isGroupBy"
:key="`nc-pagination-${isMobileMode}`"
v-model:pagination-data="paginationDataRef"
:show-api-timing="!isGroupBy"
align-count-on-right
:align-left="isGroupBy"
:change-page="changePage"
:hide-sidebars="paginationStyleRef?.hideSidebars === true"
:fixed-size="paginationStyleRef?.fixedSize"
:extra-style="paginationStyleRef?.extraStyle"
:show-size-changer="!isGroupBy"
>
<template v-if="isAddingEmptyRowAllowed && !showSkeleton" #add-record>
<div class="flex ml-1">
<NcButton
v-if="isMobileMode"
v-e="[isAddNewRecordGridMode ? 'c:row:add:grid' : 'c:row:add:form']"
class="nc-grid-add-new-row"
type="secondary"
:disabled="isPaginationLoading"
@click="onNewRecordToFormClick()"
>
{{ $t('activity.newRecord') }}
</NcButton>
<a-dropdown-button
v-else
v-e="[isAddNewRecordGridMode ? 'c:row:add:grid:toggle' : 'c:row:add:form:toggle']"
class="nc-grid-add-new-row"
placement="top"
:disabled="isPaginationLoading"
@click="isAddNewRecordGridMode ? addEmptyRow() : onNewRecordToFormClick()"
>
<div data-testid="nc-pagination-add-record" class="flex items-center px-2 text-gray-600 hover:text-black">
<span>
<template v-if="isAddNewRecordGridMode">
{{ $t('activity.newRecord') }}
</template>
<template v-else> {{ $t('activity.newRecord') }} - {{ $t('objects.viewType.form') }} </template>
</span>
</div>
<template #overlay>
<div class="relative overflow-visible min-h-17 w-10">
<div
class="absolute -top-21 flex flex-col min-h-34.5 w-70 p-1.5 bg-white rounded-lg border-1 border-gray-200 justify-start overflow-hidden"
style="box-shadow: 0px 4px 6px -2px rgba(0, 0, 0, 0.06), 0px -12px 16px -4px rgba(0, 0, 0, 0.1)"
:class="{
'-left-32.5': !isAddNewRecordGridMode,
'-left-21.5': isAddNewRecordGridMode,
}"
>
<div
v-e="['c:row:add:grid']"
class="px-4 py-3 flex flex-col select-none gap-y-2 cursor-pointer rounded-md hover:bg-gray-100 text-gray-600 nc-new-record-with-grid group"
@click="onNewRecordToGridClick"
>
<div class="flex flex-row items-center justify-between w-full">
<div class="flex flex-row items-center justify-start gap-x-3">
<component :is="viewIcons[ViewTypes.GRID]?.icon" class="nc-view-icon text-inherit" />
{{ $t('activity.newRecord') }} - {{ $t('objects.viewType.grid') }}
</div>
<GeneralIcon v-if="isAddNewRecordGridMode" icon="check" class="w-4 h-4 text-primary" />
</div>
<div class="flex flex-row text-xs text-gray-400 ml-7.25">
{{ $t('labels.addRowGrid') }}
</div>
</div>
<div
v-e="['c:row:add:form']"
class="px-4 py-3 flex flex-col select-none gap-y-2 cursor-pointer rounded-md hover:bg-gray-100 text-gray-600 nc-new-record-with-form group"
@click="onNewRecordToFormClick"
>
<div class="flex flex-row items-center justify-between w-full">
<div class="flex flex-row items-center justify-start gap-x-2.5">
<GeneralIcon class="h-4.5 w-4.5" icon="article" />
{{ $t('activity.newRecord') }} - {{ $t('objects.viewType.form') }}
</div>
<GeneralIcon v-if="!isAddNewRecordGridMode" icon="check" class="w-4 h-4 text-primary" />
</div>
<div class="flex flex-row text-xs text-gray-400 ml-7.05">
{{ $t('labels.addRowForm') }}
</div>
</div>
</div>
</div>
</template>
<template #icon>
<component :is="iconMap.arrowUp" class="text-gray-600 h-4 w-4" />
</template>
</a-dropdown-button>
</div>
</template>
</LazySmartsheetPagination>
<LazySmartsheetGridPaginationV2
v-else-if="paginationDataRef"
v-model:pagination-data="paginationDataRef"
:change-page="changePage"
:scroll-left="scrollLeft"
/>
</div>
<div v-if="headerOnly !== true && paginationDataRef && !isGroupBy" class="absolute bottom-12 left-2">
<NcDropdown v-if="isAddingEmptyRowAllowed && !showSkeleton">
<div class="flex">
<NcButton
v-if="isMobileMode"
v-e="[isAddNewRecordGridMode ? 'c:row:add:grid' : 'c:row:add:form']"
class="nc-grid-add-new-row"
type="secondary"
:disabled="isPaginationLoading"
@click="onNewRecordToFormClick()"
class="!rounded-r-none !border-r-0 nc-grid-add-new-row"
size="small"
type="secondary"
@click.stop="onNewRecordToFormClick()"
>
{{ $t('activity.newRecord') }}
<div class="flex items-center gap-2">
<GeneralIcon icon="plus" />
New Record
</div>
</NcButton>
<a-dropdown-button
v-else
v-e="[isAddNewRecordGridMode ? 'c:row:add:grid:toggle' : 'c:row:add:form:toggle']"
class="nc-grid-add-new-row"
placement="top"
<NcButton
v-e="[isAddNewRecordGridMode ? 'c:row:add:grid' : 'c:row:add:form']"
:disabled="isPaginationLoading"
@click="isAddNewRecordGridMode ? addEmptyRow() : onNewRecordToFormClick()"
class="!rounded-r-none !border-r-0 nc-grid-add-new-row"
size="small"
type="secondary"
@click.stop="isAddNewRecordGridMode ? addEmptyRow() : onNewRecordToFormClick()"
>
<div data-testid="nc-pagination-add-record" class="flex items-center px-2 text-gray-600 hover:text-black">
<span>
<template v-if="isAddNewRecordGridMode">
{{ $t('activity.newRecord') }}
</template>
<template v-else> {{ $t('activity.newRecord') }} - {{ $t('objects.viewType.form') }} </template>
</span>
<div data-testid="nc-pagination-add-record" class="flex items-center gap-2">
<GeneralIcon icon="plus" />
<template v-if="isAddNewRecordGridMode">
{{ $t('activity.newRecord') }}
</template>
<template v-else> {{ $t('activity.newRecord') }} - {{ $t('objects.viewType.form') }} </template>
</div>
</NcButton>
<NcButton v-if="!isMobileMode" size="small" class="!rounded-l-none nc-add-record-more-info" type="secondary">
<GeneralIcon icon="arrowUp" />
</NcButton>
</div>
<template #overlay>
<div class="relative overflow-visible min-h-17 w-10">
<div
class="absolute -top-21 flex flex-col min-h-34.5 w-70 p-1.5 bg-white rounded-lg border-1 border-gray-200 justify-start overflow-hidden"
style="box-shadow: 0px 4px 6px -2px rgba(0, 0, 0, 0.06), 0px -12px 16px -4px rgba(0, 0, 0, 0.1)"
:class="{
'-left-32.5': !isAddNewRecordGridMode,
'-left-21.5': isAddNewRecordGridMode,
}"
>
<div
v-e="['c:row:add:grid']"
class="px-4 py-3 flex flex-col select-none gap-y-2 cursor-pointer rounded-md hover:bg-gray-100 text-gray-600 nc-new-record-with-grid group"
@click="onNewRecordToGridClick"
>
<div class="flex flex-row items-center justify-between w-full">
<div class="flex flex-row items-center justify-start gap-x-3">
<component :is="viewIcons[ViewTypes.GRID]?.icon" class="nc-view-icon text-inherit" />
{{ $t('activity.newRecord') }} - {{ $t('objects.viewType.grid') }}
</div>
<GeneralIcon v-if="isAddNewRecordGridMode" icon="check" class="w-4 h-4 text-primary" />
</div>
<div class="flex flex-row text-xs text-gray-400 ml-7.25">
{{ $t('labels.addRowGrid') }}
</div>
</div>
<div
v-e="['c:row:add:form']"
class="px-4 py-3 flex flex-col select-none gap-y-2 cursor-pointer rounded-md hover:bg-gray-100 text-gray-600 nc-new-record-with-form group"
@click="onNewRecordToFormClick"
>
<div class="flex flex-row items-center justify-between w-full">
<div class="flex flex-row items-center justify-start gap-x-2.5">
<GeneralIcon class="h-4.5 w-4.5" icon="article" />
{{ $t('activity.newRecord') }} - {{ $t('objects.viewType.form') }}
</div>
<template #overlay>
<NcMenu>
<NcMenuItem v-e="['c:row:add:grid']" class="nc-new-record-with-grid group" @click="onNewRecordToGridClick">
<div class="flex flex-row items-center justify-start gap-x-3">
<component :is="viewIcons[ViewTypes.GRID]?.icon" class="nc-view-icon text-inherit" />
{{ $t('activity.newRecord') }} - {{ $t('objects.viewType.grid') }}
</div>
<GeneralIcon v-if="!isAddNewRecordGridMode" icon="check" class="w-4 h-4 text-primary" />
</div>
<div class="flex flex-row text-xs text-gray-400 ml-7.05">
{{ $t('labels.addRowForm') }}
</div>
</div>
</div>
<GeneralIcon v-if="isAddNewRecordGridMode" icon="check" class="w-4 h-4 text-primary" />
</NcMenuItem>
<NcMenuItem v-e="['c:row:add:form']" class="nc-new-record-with-form group" @click="onNewRecordToFormClick">
<div class="flex flex-row items-center justify-start gap-x-3">
<GeneralIcon class="h-4.5 w-4.5" icon="article" />
{{ $t('activity.newRecord') }} - {{ $t('objects.viewType.form') }}
</div>
</template>
<template #icon>
<component :is="iconMap.arrowUp" class="text-gray-600 h-4 w-4" />
</template>
</a-dropdown-button>
</div>
</template>
</LazySmartsheetPagination>
<GeneralIcon v-if="!isAddNewRecordGridMode" icon="check" class="w-4 h-4 text-primary" />
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</div>
</div>
</template>

47
packages/nc-gui/components/smartsheet/grid/index.vue

@ -31,6 +31,8 @@ const expandedFormRowState = ref<Record<string, any>>()
const tableRef = ref<typeof Table>()
useProvideViewAggregate(view, meta, xWhere)
const {
loadData,
paginationData,
@ -146,7 +148,9 @@ const toggleOptimisedQuery = () => {
const { rootGroup, groupBy, isGroupBy, loadGroups, loadGroupData, loadGroupPage, groupWrapperChangePage, redistributeRows } =
useViewGroupByOrThrow()
const coreWrapperRef = ref<HTMLElement>()
const sidebarStore = useSidebarStore()
const { windowSize, leftSidebarWidth } = toRefs(sidebarStore)
const viewWidth = ref(0)
@ -156,17 +160,6 @@ eventBus.on((event) => {
}
})
onMounted(() => {
until(coreWrapperRef)
.toBeTruthy()
.then(() => {
const resizeObserver = new ResizeObserver(() => {
viewWidth.value = coreWrapperRef.value?.clientWidth || 0
})
if (coreWrapperRef.value) resizeObserver.observe(coreWrapperRef.value)
})
})
const goToNextRow = () => {
const currentIndex = getExpandedRowIndex()
/* when last index of current page is reached we should move to next page */
@ -190,14 +183,40 @@ const goToPreviousRow = () => {
navigateToSiblingRow(NavigateDir.PREV)
}
const updateViewWidth = () => {
if (isPublic.value) {
viewWidth.value = windowSize.value
return
}
viewWidth.value = windowSize.value - leftSidebarWidth.value
}
const baseColor = computed(() => {
switch (groupBy.value.length) {
case 1:
return '#F9F9FA'
case 2:
return '#F4F4F5'
case 3:
return '#E7E7E9'
default:
return '#F9F9FA'
}
})
watch([windowSize, leftSidebarWidth], updateViewWidth)
onMounted(() => {
updateViewWidth()
})
</script>
<template>
<div
ref="coreWrapperRef"
class="relative flex flex-col h-full min-h-0 w-full nc-grid-wrapper"
data-testid="nc-grid-wrapper"
style="background-color: var(--nc-grid-bg)"
:style="`width: ${viewWidth}px; background-color: ${isGroupBy ? `${baseColor}` : 'var(--nc-grid-bg)'};`"
>
<Table
v-if="!isGroupBy"

16
packages/nc-gui/components/smartsheet/header/Cell.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { type ColumnReqType, type ColumnType, partialUpdateAllowedTypes, readonlyMetaAllowedTypes } from 'nocodb-sdk'
import { UITypes, UITypesName } from 'nocodb-sdk'
interface Props {
@ -32,7 +32,7 @@ const isDropDownOpen = ref(false)
const column = toRef(props, 'column')
const { isUIAllowed } = useRoles()
const { isUIAllowed, isMetaReadOnly } = useRoles()
provide(ColumnInj, column)
@ -57,10 +57,20 @@ const closeAddColumnDropdown = () => {
editColumnDropdown.value = false
}
const isColumnEditAllowed = computed(() => {
if (
isMetaReadOnly.value &&
!readonlyMetaAllowedTypes.includes(column.value?.uidt) &&
!partialUpdateAllowedTypes.includes(column.value?.uidt)
)
return false
return true
})
const openHeaderMenu = (e?: MouseEvent) => {
if (isLocked.value || (isExpandedForm.value && e?.type === 'dblclick') || isExpandedBulkUpdateForm.value) return
if (!isForm.value && isUIAllowed('fieldEdit') && !isMobileMode.value) {
if (!isForm.value && isUIAllowed('fieldEdit') && !isMobileMode.value && isColumnEditAllowed.value) {
editColumnDropdown.value = true
}
}

127
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { ColumnReqType } from 'nocodb-sdk'
import { type ColumnReqType, partialUpdateAllowedTypes, readonlyMetaAllowedTypes } from 'nocodb-sdk'
import { PlanLimitTypes, RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import { SmartsheetStoreEvents } from '#imports'
@ -43,6 +43,8 @@ const { gridViewCols } = useViewColumnsOrThrow()
const { fieldsToGroupBy, groupByLimit } = useViewGroupByOrThrow(view)
const { isUIAllowed, isMetaReadOnly, isDataReadOnly } = useRoles()
const isLoading = ref<'' | 'hideOrShow' | 'setDisplay'>('')
const setAsDisplayValue = async () => {
@ -323,7 +325,11 @@ const isDeleteAllowed = computed(() => {
return column?.value && !column.value.system
})
const isDuplicateAllowed = computed(() => {
return column?.value && !column.value.system
return (
column?.value &&
!column.value.system &&
((!isMetaReadOnly.value && !isDataReadOnly.value) || readonlyMetaAllowedTypes.includes(column.value?.uidt))
)
})
const isFilterSupported = computed(
() =>
@ -352,6 +358,21 @@ const filterOrGroupByThisField = (event: SmartsheetStoreEvents) => {
}
isOpen.value = false
}
const isColumnUpdateAllowed = computed(() => {
if (isMetaReadOnly.value && !readonlyMetaAllowedTypes.includes(column.value?.uidt)) return false
return true
})
const isColumnEditAllowed = computed(() => {
if (
isMetaReadOnly.value &&
!readonlyMetaAllowedTypes.includes(column.value?.uidt) &&
!partialUpdateAllowedTypes.includes(column.value?.uidt)
)
return false
return true
})
</script>
<template>
@ -374,21 +395,34 @@ const filterOrGroupByThisField = (event: SmartsheetStoreEvents) => {
'min-w-[256px]': isExpandedForm,
}"
>
<NcMenuItem :disabled="column?.pk || isSystemColumn(column)" @click="onEditPress">
<div class="nc-column-edit nc-header-menu-item">
<component :is="iconMap.ncEdit" class="text-gray-700" />
<!-- Edit -->
{{ $t('general.edit') }}
</div>
</NcMenuItem>
<NcMenuItem v-if="isExpandedForm && !column?.pk" :disabled="!isDuplicateAllowed" @click="openDuplicateDlg">
<div v-e="['a:field:duplicate']" class="nc-column-duplicate nc-header-menu-item">
<component :is="iconMap.duplicate" class="text-gray-700" />
<!-- Duplicate -->
{{ t('general.duplicate') }}
</div>
</NcMenuItem>
<a-divider v-if="!column?.pv" class="!my-0" />
<GeneralSourceRestrictionTooltip message="Field properties cannot be edited." :enabled="!isColumnEditAllowed">
<NcMenuItem
v-if="isUIAllowed('fieldAlter')"
:disabled="column?.pk || isSystemColumn(column) || !isColumnEditAllowed"
@click="onEditPress"
>
<div class="nc-column-edit nc-header-menu-item">
<component :is="iconMap.ncEdit" class="text-gray-700" />
<!-- Edit -->
{{ $t('general.edit') }}
</div>
</NcMenuItem>
</GeneralSourceRestrictionTooltip>
<GeneralSourceRestrictionTooltip message="Field cannot be duplicated." :enabled="!isDuplicateAllowed">
<NcMenuItem
v-if="isUIAllowed('duplicateColumn') && isExpandedForm && !column?.pk"
:disabled="!isDuplicateAllowed"
@click="openDuplicateDlg"
>
<div v-e="['a:field:duplicate']" class="nc-column-duplicate nc-header-menu-item">
<component :is="iconMap.duplicate" class="text-gray-700" />
<!-- Duplicate -->
{{ t('general.duplicate') }}
</div>
</NcMenuItem>
</GeneralSourceRestrictionTooltip>
<a-divider v-if="isUIAllowed('fieldAlter') && !column?.pv" class="!my-0" />
<NcMenuItem v-if="!column?.pv" @click="hideOrShowField">
<div v-e="['a:field:hide']" class="nc-column-insert-before nc-header-menu-item">
<GeneralLoader v-if="isLoading === 'hideOrShow'" size="regular" />
@ -465,13 +499,15 @@ const filterOrGroupByThisField = (event: SmartsheetStoreEvents) => {
<NcTooltip
:disabled="(isGroupBySupported && !isGroupByLimitExceeded) || isGroupedByThisField || !(isEeUI && !isPublic)"
>
<template #title>{{
!isGroupBySupported
? "This field type doesn't support grouping"
: isGroupByLimitExceeded
? 'Group by limit exceeded'
: ''
}}</template>
<template #title
>{{
!isGroupBySupported
? "This field type doesn't support grouping"
: isGroupByLimitExceeded
? 'Group by limit exceeded'
: ''
}}
</template>
<NcMenuItem
:disabled="isEeUI && !isPublic && (!isGroupBySupported || isGroupByLimitExceeded) && !isGroupedByThisField"
@click="
@ -489,14 +525,15 @@ const filterOrGroupByThisField = (event: SmartsheetStoreEvents) => {
</NcTooltip>
<a-divider class="!my-0" />
<NcMenuItem v-if="!column?.pk" :disabled="!isDuplicateAllowed" @click="openDuplicateDlg">
<div v-e="['a:field:duplicate']" class="nc-column-duplicate nc-header-menu-item">
<component :is="iconMap.duplicate" class="text-gray-700" />
<!-- Duplicate -->
{{ t('general.duplicate') }}
</div>
</NcMenuItem>
<GeneralSourceRestrictionTooltip message="Field cannot be duplicated." :enabled="!isDuplicateAllowed && isMetaReadOnly">
<NcMenuItem v-if="!column?.pk" :disabled="!isDuplicateAllowed" @click="openDuplicateDlg">
<div v-e="['a:field:duplicate']" class="nc-column-duplicate nc-header-menu-item">
<component :is="iconMap.duplicate" class="text-gray-700" />
<!-- Duplicate -->
{{ t('general.duplicate') }}
</div>
</NcMenuItem>
</GeneralSourceRestrictionTooltip>
<NcMenuItem @click="onInsertAfter">
<div v-e="['a:field:insert:after']" class="nc-column-insert-after nc-header-menu-item">
<component :is="iconMap.colInsertAfter" class="text-gray-700 !w-4.5 !h-4.5" />
@ -513,14 +550,23 @@ const filterOrGroupByThisField = (event: SmartsheetStoreEvents) => {
</NcMenuItem>
</template>
<a-divider v-if="!column?.pv" class="!my-0" />
<NcMenuItem v-if="!column?.pv" :disabled="!isDeleteAllowed" class="!hover:bg-red-50" @click="handleDelete">
<div class="nc-column-delete nc-header-menu-item text-red-600">
<component :is="iconMap.delete" />
<!-- Delete -->
{{ $t('general.delete') }}
</div>
</NcMenuItem>
<GeneralSourceRestrictionTooltip message="Field cannot be deleted." :enabled="!isColumnUpdateAllowed">
<NcMenuItem
v-if="!column?.pv && isUIAllowed('fieldDelete')"
:disabled="!isDeleteAllowed || !isColumnUpdateAllowed"
class="!hover:bg-red-50"
@click="handleDelete"
>
<div
class="nc-column-delete nc-header-menu-item"
:class="{ ' text-red-600': isDeleteAllowed && isColumnUpdateAllowed }"
>
<component :is="iconMap.delete" />
<!-- Delete -->
{{ $t('general.delete') }}
</div>
</NcMenuItem>
</GeneralSourceRestrictionTooltip>
</NcMenu>
</template>
</a-dropdown>
@ -548,6 +594,7 @@ const filterOrGroupByThisField = (event: SmartsheetStoreEvents) => {
:deep(.ant-dropdown-menu-item:not(.ant-dropdown-menu-item-disabled)) {
@apply !hover:text-black text-gray-700;
}
:deep(.ant-dropdown-menu-item.ant-dropdown-menu-item-disabled .nc-icon) {
@apply text-current;
}

10
packages/nc-gui/components/smartsheet/header/VirtualCell.vue

@ -7,6 +7,7 @@ import {
type LookupType,
type RollupType,
isLinksOrLTAR,
readonlyMetaAllowedTypes,
} from 'nocodb-sdk'
import { RelationTypes, UITypes, UITypesName, substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
@ -36,7 +37,7 @@ provide(ColumnInj, column)
const { metas } = useMetas()
const { isUIAllowed } = useRoles()
const { isUIAllowed, isMetaReadOnly } = useRoles()
const meta = inject(MetaInj, ref())
@ -122,7 +123,12 @@ const closeAddColumnDropdown = () => {
const openHeaderMenu = (e?: MouseEvent) => {
if (isLocked.value || (isExpandedForm.value && e?.type === 'dblclick') || isExpandedBulkUpdateForm.value) return
if (!isForm.value && isUIAllowed('fieldEdit') && !isMobileMode.value) {
if (
!isForm.value &&
isUIAllowed('fieldEdit') &&
!isMobileMode.value &&
(!isMetaReadOnly.value || readonlyMetaAllowedTypes.includes(column.value.uidt))
) {
editColumnDropdown.value = true
}
}

2
packages/nc-gui/components/smartsheet/toolbar/Calendar/Range.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { type CalendarRangeType, UITypes, ViewTypes, isSystemColumn } from 'nocodb-sdk'
import { type CalendarRangeType, UITypes, isSystemColumn } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue'
const meta = inject(MetaInj, ref())

19
packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue

@ -63,6 +63,15 @@ const { nestedFilters } = useSmartsheetStoreOrThrow()
const currentFilters = modelValue.value || (!link.value && !webHook.value && nestedFilters.value) || []
const columns = computed(() => meta.value?.columns)
const fieldsToFilter = computed(() =>
(columns.value || []).filter((c) => {
if (link.value && isSystemColumn(c) && !c.pk && !isCreatedOrLastModifiedTimeCol(c)) return false
return !excludedFilterColUidt.includes(c.uidt as UITypes)
}),
)
const {
filters,
nonDeletedFilters,
@ -88,6 +97,7 @@ const {
webHook.value,
link.value,
linkColId,
fieldsToFilter,
)
const { getPlanLimit } = useWorkspace()
@ -99,15 +109,6 @@ const addFiltersRowDomRef = ref<HTMLElement>()
const isMounted = ref(false)
const columns = computed(() => meta.value?.columns)
const fieldsToFilter = computed(() =>
(columns.value || []).filter((c) => {
if (link.value && isSystemColumn(c) && !c.pk && !isCreatedOrLastModifiedTimeCol(c)) return false
return !excludedFilterColUidt.includes(c.uidt as UITypes)
}),
)
const getColumn = (filter: Filter) => {
// extract looked up column if available
return btLookupTypesMap.value[filter.fk_column_id] || columns.value?.find((col: ColumnType) => col.id === filter.fk_column_id)

23
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -422,27 +422,38 @@ useMenuCloseOnEsc(open)
<div class="pl-2 flex text-sm select-none text-gray-600">{{ $t('labels.coverImageField') }}</div>
<div
class="flex-1 nc-dropdown-cover-image-wrapper flex items-stretch border-1 border-gray-200 rounded-lg transition-all duration-0.3s"
class="flex-1 nc-dropdown-cover-image-wrapper flex items-stretch border-1 border-gray-200 rounded-lg transition-all duration-0.3s max-w-[206px]"
>
<a-select
v-model:value="coverImageColumnId"
class="w-full"
dropdown-class-name="nc-dropdown-cover-image !rounded-lg"
class="flex-1 max-w-[calc(100%_-_33px)]"
dropdown-class-name="nc-dropdown-cover-image !rounded-lg "
:bordered="false"
@click.stop
>
<template #suffixIcon><GeneralIcon class="text-gray-700" icon="arrowDown" /></template>
<a-select-option v-for="option of coverOptions" :key="option.value" :value="option.value">
<div class="w-full flex gap-2 items-center justify-between">
<div class="flex items-center gap-1">
<div class="w-full flex gap-2 items-center justify-between max-w-[400px]">
<div
class="flex-1 flex items-center gap-1"
:class="{
'max-w-[calc(100%_-_20px)]': coverImageColumnId === option.value,
'max-w-full': coverImageColumnId !== option.value,
}"
>
<component
:is="getIcon(metaColumnById[option.value])"
v-if="option.value"
class="!w-3.5 !h-3.5 !text-gray-700 !ml-0"
/>
<span> {{ option.label }} </span>
<NcTooltip class="flex-1 max-w-[calc(100%_-_20px)] truncate" show-on-truncate-only>
<template #title>
{{ option.label }}
</template>
<template #default>{{ option.label }}</template>
</NcTooltip>
</div>
<GeneralIcon
v-if="coverImageColumnId === option.value"

174
packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import Draggable from 'vuedraggable'
const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
@ -153,6 +154,22 @@ eventBus.on(async (event, column) => {
await saveGroupBy()
}
})
const onMove = async (event: { moved: { newIndex: number; oldIndex: number } }) => {
const { newIndex, oldIndex } = event.moved
const tempGroups = [..._groupBy.value]
const movedItem = tempGroups.splice(oldIndex, 1)[0]
tempGroups.splice(newIndex, 0, movedItem ?? [])
const updatedGroups = tempGroups.map((group, index) => ({ ...group, order: index + 1 }))
_groupBy.value = [...updatedGroups]
await saveGroupBy()
}
</script>
<template>
@ -193,59 +210,84 @@ eventBus.on(async (event, column) => {
/>
<div
v-else
class="flex flex-col bg-white overflow-auto nc-group-by-list menu-filter-dropdown max-h-[max(80vh,500px)] min-w-102 pt-2 pb-2 pl-4"
class="flex flex-col bg-white overflow-auto nc-group-by-list menu-filter-dropdown w-100 p-6"
data-testid="nc-group-by-menu"
>
<div class="group-by-grid max-h-100 nc-scrollbar-thing pr-4 py-2" @click.stop>
<template v-for="[i, group] of Object.entries(_groupBy)" :key="`grouped-by-${group.fk_column_id}`">
<LazySmartsheetToolbarFieldListAutoCompleteDropdown
v-model="group.fk_column_id"
class="caption nc-sort-field-select w-44 flex flex-grow"
:columns="fieldsToGroupBy"
:allow-empty="true"
:meta="meta"
@change="saveGroupBy"
@click.stop
/>
<NcSelect
ref=""
v-model:value="group.sort"
class="shrink grow-0 nc-sort-dir-select"
:label="$t('labels.operation')"
dropdown-class-name="sort-dir-dropdown nc-dropdown-sort-dir"
:disabled="!group.fk_column_id"
@change="saveGroupBy"
@click.stop
>
<a-select-option
v-for="(option, j) of getSortDirectionOptions(getColumnUidtByID(group.fk_column_id), true)"
:key="j"
:value="option.value"
>
<div class="w-full flex items-center justify-between gap-2">
<div class="truncate flex-1">{{ option.text }}</div>
<component
:is="iconMap.check"
v-if="group.sort === option.value"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
<a-tooltip placement="right" title="Remove" class="flex-none min-w-40">
<NcButton
v-e="['c:group-by:remove']"
class="nc-group-by-item-remove-btn min-w-40"
size="small"
type="text"
@click.stop="removeFieldFromGroupBy(i)"
>
<component :is="iconMap.deleteListItem" />
</NcButton>
</a-tooltip>
</template>
<div class="max-h-100" @click.stop>
<Draggable :model-value="_groupBy" item-key="fk_column_id" ghost-class="bg-gray-50" @change="onMove($event)">
<template #item="{ element: group }">
<div :key="group.fk_column_id" class="flex first:mb-0 !mb-1.5 !last:mb-0 items-center">
<NcButton type="secondary" size="small" class="!border-r-transparent !rounded-r-none">
<component :is="iconMap.drag" />
</NcButton>
<LazySmartsheetToolbarFieldListAutoCompleteDropdown
v-model="group.fk_column_id"
class="caption nc-sort-field-select !w-36"
:columns="fieldsToGroupBy"
:allow-empty="true"
:meta="meta"
@change="saveGroupBy"
@click.stop
/>
<NcSelect
ref=""
v-model:value="group.sort"
class="flex flex-grow-1 w-full nc-sort-dir-select"
:label="$t('labels.operation')"
dropdown-class-name="sort-dir-dropdown nc-dropdown-sort-dir"
:disabled="!group.fk_column_id"
@change="saveGroupBy"
@click.stop
>
<a-select-option
v-for="(option, j) of getSortDirectionOptions(getColumnUidtByID(group.fk_column_id), true)"
:key="j"
:value="option.value"
>
<div class="w-full flex items-center justify-between gap-2">
<div class="truncate flex-1">{{ option.text }}</div>
<component
:is="iconMap.check"
v-if="group.sort === option.value"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
<!-- <NcDropdown :disabled="!isColumnSupportsGroupBySettings(columnByID[group.fk_column_id])" :trigger="['click']">
<NcButton
:disabled="!isColumnSupportsGroupBySettings(columnByID[group.fk_column_id])"
class="!rounded-none !border-gray-200 !border-l-transparent"
type="secondary"
size="small"
>
<GeneralIcon icon="ncSettings" />
</NcButton>
<template #overlay>
<NcMenu>
<NcMenuItem> Hide groups with no records </NcMenuItem>
<NcMenuItem> Show groups with no records </NcMenuItem>
</NcMenu>
</template>
</NcDropdown> -->
<NcTooltip placement="top" title="Remove" class="flex-none">
<NcButton
v-e="['c:group-by:remove']"
class="nc-group-by-item-remove-btn !border-l-transparent !rounded-l-none min-w-40"
size="small"
type="secondary"
@click.stop="removeFieldFromGroupBy(i)"
>
<component :is="iconMap.deleteListItem" />
</NcButton>
</NcTooltip>
</div>
</template>
</Draggable>
</div>
<NcDropdown
v-if="availableColumns.length && fieldsToGroupBy.length > _groupBy.length && _groupBy.length < groupByLimit"
@ -255,7 +297,7 @@ eventBus.on(async (event, column) => {
>
<NcButton
v-e="['c:group-by:add']"
class="nc-add-group-by-btn !text-brand-500 mt-1 mb-2"
class="nc-add-group-by-btn mt-5"
style="width: fit-content"
size="small"
type="text"
@ -281,10 +323,28 @@ eventBus.on(async (event, column) => {
</NcDropdown>
</template>
<style scoped>
.group-by-grid {
display: grid;
grid-template-columns: auto 150px auto;
@apply gap-x-2 gap-y-3;
<style scoped lang="scss">
:deep(.nc-sort-field-select) {
@apply !w-36;
.ant-select-selector {
@apply !rounded-none !border-r-0 !border-gray-200 !shadow-none !w-36;
&.ant-select-focused:not(.ant-select-disabled) {
@apply !border-r-transparent;
}
}
}
:deep(.nc-select:hover) {
&,
.ant-select-selector {
@apply bg-gray-50;
}
}
:deep(.nc-sort-dir-select) {
.ant-select-selector {
@apply !rounded-none !border-gray-200 !shadow-none;
}
}
</style>

15
packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue

@ -146,9 +146,9 @@ const getIcon = (c: ColumnType) =>
{{ $t('activity.kanban.stackedBy') }}
</span>
<div
class="flex items-center rounded-md transition-colors duration-0.3s bg-gray-100 group-hover:bg-gray-200 px-1 min-h-5 text-gray-600"
class="flex items-center rounded-md transition-colors duration-0.3s bg-gray-100 group-hover:bg-gray-200 px-1 min-h-5 text-gray-600 max-w-[108px]"
>
<span class="font-weight-500 text-sm">{{ groupingField }}</span>
<span class="font-weight-500 text-sm truncate !leading-5">{{ groupingField }}</span>
</div>
</div>
</div>
@ -158,7 +158,7 @@ const getIcon = (c: ColumnType) =>
<div v-if="open" class="p-4 w-90 bg-white nc-table-toolbar-menu rounded-lg flex flex-col gap-5" @click.stop>
<div class="flex flex-col gap-2">
<div>
{{ $t('general.groupingField').toLowerCase().replace(/^./, $t('general.groupingField').charAt(0).toUpperCase()) }}
{{ $t('general.groupingField') }}
</div>
<div class="nc-fields-list">
<div class="grouping-field">
@ -173,14 +173,19 @@ const getIcon = (c: ColumnType) =>
<template #suffixIcon><GeneralIcon icon="arrowDown" class="text-gray-700" /></template>
<a-select-option v-for="option of singleSelectFieldOptions" :key="option.value" :value="option.value">
<div class="w-full flex gap-2 items-center justify-between" :title="option.label">
<div class="flex items-center gap-1">
<div class="flex items-center gap-1 max-w-[calc(100%_-_20px)]">
<component
:is="getIcon(metaColumnById[option.value])"
v-if="option.value"
class="!w-3.5 !h-3.5 !text-gray-700 !ml-0"
/>
<span> {{ option.label }} </span>
<NcTooltip class="flex-1 max-w-[calc(100%_-_20px)] truncate" show-on-truncate-only>
<template #title>
{{ option.label }}
</template>
<template #default>{{ option.label }}</template>
</NcTooltip>
</div>
<GeneralIcon
v-if="groupingFieldColumnId === option.value"

5
packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue

@ -16,7 +16,7 @@ const props = withDefaults(
const emits = defineEmits(['rename', 'closeModal', 'delete'])
const { isUIAllowed } = useRoles()
const { isUIAllowed, isDataReadOnly } = useRoles()
const isPublicView = inject(IsPublicInj, ref(false))
@ -103,6 +103,7 @@ function onDuplicate() {
'groupingFieldColumnId': view.value!.view!.fk_grp_col_id,
'views': views,
'calendarRange': view.value!.view!.calendar_range,
'coverImageColumnId': view.value!.view!.fk_cover_image_col_id,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()
@ -192,7 +193,7 @@ const onDelete = async () => {
<template v-if="view.type !== ViewTypes.FORM">
<NcDivider />
<template v-if="isUIAllowed('csvTableImport') && !isPublicView">
<template v-if="isUIAllowed('csvTableImport') && !isPublicView && !isDataReadOnly">
<NcSubMenu key="upload">
<template #title>
<div

16
packages/nc-gui/components/tabs/Smartsheet.vue

@ -39,6 +39,12 @@ const reloadViewMetaEventHook = createEventHook<void | boolean>()
const openNewRecordFormHook = createEventHook<void>()
const { base } = storeToRefs(useBase())
const activeSource = computed(() => {
return meta.value?.source_id && base.value && base.value.sources?.find((source) => source.id === meta.value?.source_id)
})
useProvideKanbanViewStore(meta, activeView)
useProvideMapViewStore(meta, activeView)
useProvideCalendarViewStore(meta, activeView)
@ -52,9 +58,17 @@ provide(ReloadViewMetaHookInj, reloadViewMetaEventHook)
provide(OpenNewRecordFormHookInj, openNewRecordFormHook)
provide(IsFormInj, isForm)
provide(TabMetaInj, activeTab)
provide(ActiveSourceInj, activeSource)
provide(ReloadAggregateHookInj, createEventHook())
provide(
ReadonlyInj,
computed(() => !isUIAllowed('dataEdit')),
computed(
() =>
!isUIAllowed('dataEdit', {
skipSourceCheck: true,
}),
),
)
useExpandedFormDetachedProvider()

2
packages/nc-gui/components/virtual-cell/BelongsTo.vue

@ -87,7 +87,7 @@ watch(value, (next) => {
<template>
<div class="flex w-full chips-wrapper items-center" :class="{ active }">
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex items-center w-full min-h-7.7">
<div class="flex items-center w-full">
<div class="nc-cell-field chips flex items-center flex-1 max-w-[calc(100%_-_16px)]">
<template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)">
<VirtualCellComponentsItemChip

4
packages/nc-gui/components/virtual-cell/HasMany.vue

@ -127,7 +127,7 @@ watch(
<template>
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex items-center gap-1 w-full chips-wrapper min-h-7.7">
<div class="flex items-center gap-1 w-full chips-wrapper min-h-4">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<VirtualCellComponentsItemChip
@ -144,7 +144,7 @@ watch(
</template>
</div>
<div v-if="!isUnderLookup && !isSystemColumn(column)" class="flex justify-end gap-1 min-h-[30px] items-center">
<div v-if="!isUnderLookup && !isSystemColumn(column)" class="flex justify-end gap-1 min-h-4 items-center">
<GeneralIcon
icon="expand"
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"

2
packages/nc-gui/components/virtual-cell/Links.vue

@ -139,7 +139,7 @@ watch(
<template>
<div class="nc-cell-field flex w-full group items-center nc-links-wrapper py-1" @dblclick.stop="openChildList">
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex w-full group items-center min-h-7.7">
<div class="flex w-full group items-center min-h-4">
<div class="block flex-shrink truncate">
<component
:is="isUnderLookup ? 'span' : 'a'"

11
packages/nc-gui/components/virtual-cell/Lookup.vue

@ -90,11 +90,12 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
class="nc-cell-field h-full w-full nc-lookup-cell"
tabindex="-1"
:style="{
height: isGroupByLabel
? undefined
: rowHeight
? `${rowHeight === 1 ? rowHeightInPx['1'] - 4 : rowHeightInPx[`${rowHeight}`] - 18}px`
: `2.85rem`,
height:
isGroupByLabel || (lookupColumn && isAttachment(lookupColumn))
? undefined
: rowHeight
? `${rowHeight === 1 ? rowHeightInPx['1'] - 4 : rowHeightInPx[`${rowHeight}`] - 18}px`
: `2.85rem`,
}"
@dblclick="activateShowEditNonEditableFieldWarning"
>

4
packages/nc-gui/components/virtual-cell/ManyToMany.vue

@ -126,7 +126,7 @@ watch(
<template>
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex items-center gap-1 w-full chips-wrapper min-h-7.7">
<div class="flex items-center gap-1 w-full chips-wrapper min-h-4">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<VirtualCellComponentsItemChip
@ -143,7 +143,7 @@ watch(
</template>
</div>
<div v-if="!isUnderLookup || isForm" class="flex justify-end gap-1 min-h-[30px] items-center">
<div v-if="!isUnderLookup || isForm" class="flex justify-end gap-1 min-h-4 items-center">
<GeneralIcon
icon="expand"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"

4
packages/nc-gui/components/virtual-cell/OneToOne.vue

@ -84,7 +84,7 @@ watch(
<template>
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex w-full chips-wrapper items-center min-h-7.7" :class="{ active }">
<div class="flex w-full chips-wrapper items-center min-h-4" :class="{ active }">
<div class="nc-cell-field chips flex items-center flex-1 max-w-[calc(100%_-_16px)]">
<template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)">
<VirtualCellComponentsItemChip
@ -103,7 +103,7 @@ watch(
<div
v-if="!readOnly && (isUIAllowed('dataEdit') || isForm) && !isUnderLookup"
class="flex justify-end group gap-1 min-h-[30px] items-center"
class="flex justify-end group gap-1 min-h-4 items-center"
tabindex="0"
@keydown.enter.stop="listItemsDlg = true"
>

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

Loading…
Cancel
Save