Browse Source

Merge branch 'develop' into renovate/minor-2

pull/7602/head
աɨռɢӄաօռɢ 9 months ago
parent
commit
0aa10c1a51
  1. 551
      docker-compose/setup-script/noco.sh
  2. BIN
      packages/nc-gui/assets/img/placeholder/api-tokens.png
  3. BIN
      packages/nc-gui/assets/img/placeholder/invite-team.png
  4. BIN
      packages/nc-gui/assets/img/placeholder/link-records.png
  5. BIN
      packages/nc-gui/assets/img/placeholder/multi-field-editor.png
  6. BIN
      packages/nc-gui/assets/img/placeholder/table.png
  7. BIN
      packages/nc-gui/assets/img/placeholder/webhooks.png
  8. 61
      packages/nc-gui/components/account/Token.vue
  9. 20
      packages/nc-gui/components/account/UserList.vue
  10. 6
      packages/nc-gui/components/cell/Currency.vue
  11. 8
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  12. 2
      packages/nc-gui/components/general/JoinCloud.vue
  13. 2
      packages/nc-gui/components/general/ReleaseInfo.vue
  14. 6
      packages/nc-gui/components/general/SocialCard.vue
  15. 12
      packages/nc-gui/components/project/AllTables.vue
  16. 2
      packages/nc-gui/components/smartsheet/Cell.vue
  17. 4
      packages/nc-gui/components/smartsheet/Form.vue
  18. 2
      packages/nc-gui/components/smartsheet/details/Fields.vue
  19. 11
      packages/nc-gui/components/smartsheet/details/Webhooks.vue
  20. 12
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  21. 38
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  22. 53
      packages/nc-gui/components/smartsheet/grid/Table.vue
  23. 12
      packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue
  24. 81
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  25. 3
      packages/nc-gui/components/virtual-cell/components/ListItem.vue
  26. 56
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  27. 9
      packages/nc-gui/components/workspace/CollaboratorsList.vue
  28. 18
      packages/nc-gui/lang/ar.json
  29. 18
      packages/nc-gui/lang/bn_IN.json
  30. 18
      packages/nc-gui/lang/cs.json
  31. 18
      packages/nc-gui/lang/da.json
  32. 18
      packages/nc-gui/lang/de.json
  33. 18
      packages/nc-gui/lang/en.json
  34. 18
      packages/nc-gui/lang/es.json
  35. 18
      packages/nc-gui/lang/eu.json
  36. 18
      packages/nc-gui/lang/fa.json
  37. 18
      packages/nc-gui/lang/fi.json
  38. 210
      packages/nc-gui/lang/fr.json
  39. 18
      packages/nc-gui/lang/he.json
  40. 18
      packages/nc-gui/lang/hi.json
  41. 18
      packages/nc-gui/lang/hr.json
  42. 18
      packages/nc-gui/lang/id.json
  43. 18
      packages/nc-gui/lang/it.json
  44. 18
      packages/nc-gui/lang/ja.json
  45. 18
      packages/nc-gui/lang/ko.json
  46. 18
      packages/nc-gui/lang/lv.json
  47. 18
      packages/nc-gui/lang/nl.json
  48. 18
      packages/nc-gui/lang/no.json
  49. 18
      packages/nc-gui/lang/pl.json
  50. 18
      packages/nc-gui/lang/pt.json
  51. 20
      packages/nc-gui/lang/pt_BR.json
  52. 38
      packages/nc-gui/lang/ru.json
  53. 18
      packages/nc-gui/lang/sk.json
  54. 18
      packages/nc-gui/lang/sl.json
  55. 62
      packages/nc-gui/lang/sv.json
  56. 18
      packages/nc-gui/lang/th.json
  57. 18
      packages/nc-gui/lang/tr.json
  58. 18
      packages/nc-gui/lang/uk.json
  59. 18
      packages/nc-gui/lang/vi.json
  60. 18
      packages/nc-gui/lang/zh-Hans.json
  61. 18
      packages/nc-gui/lang/zh-Hant.json
  62. 6
      packages/nc-gui/package.json
  63. 8
      packages/noco-docs/package-lock.json
  64. 2
      packages/noco-docs/package.json
  65. 626
      packages/nocodb/src/cache/CacheMgr.ts
  66. 3
      packages/nocodb/src/cache/NocoCache.ts
  67. 414
      packages/nocodb/src/cache/RedisCacheMgr.ts
  68. 420
      packages/nocodb/src/cache/RedisMockCacheMgr.ts
  69. 31
      packages/nocodb/src/db/BaseModelSqlv2.ts
  70. 6
      packages/nocodb/src/db/conditionV2.ts
  71. 2
      packages/nocodb/src/db/sortV2.ts
  72. 8
      packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts
  73. 88
      packages/nocodb/src/meta/migrations/v2/nc_039_sqlite_alter_column_types.ts
  74. 28
      packages/nocodb/src/meta/migrations/v2/nc_040_form_view_alter_column_types.ts
  75. 1
      packages/nocodb/src/models/ApiToken.ts
  76. 2
      packages/nocodb/src/models/Base.ts
  77. 14
      packages/nocodb/src/models/Column.ts
  78. 3
      packages/nocodb/src/models/Filter.ts
  79. 2
      packages/nocodb/src/models/Hook.ts
  80. 2
      packages/nocodb/src/models/HookFilter.ts
  81. 4
      packages/nocodb/src/models/Model.ts
  82. 1
      packages/nocodb/src/models/ModelRoleVisibility.ts
  83. 2
      packages/nocodb/src/models/Sort.ts
  84. 3
      packages/nocodb/src/models/Source.ts
  85. 2
      packages/nocodb/src/models/User.ts
  86. 3
      packages/nocodb/src/models/View.ts
  87. 4
      packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts
  88. 5
      packages/nocodb/src/modules/jobs/jobs/at-import/helpers/readAndProcessData.ts
  89. 59
      packages/nocodb/src/schema/swagger-v2.json
  90. 59
      packages/nocodb/src/schema/swagger.json
  91. 2
      packages/nocodb/src/utils/globals.ts
  92. 30
      pnpm-lock.yaml
  93. 2
      tests/playwright/constants/index.ts
  94. 55
      tests/playwright/tests/db/features/keyboardShortcuts.spec.ts

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

@ -0,0 +1,551 @@
#!/bin/bash
# set -x
# ******************************************************************************
# ***************** HELPER FUNCTIONS START *********************************
# Function to URL encode special characters in a string
urlencode() {
local string="$1"
local strlen=${#string}
local encoded=""
local pos c o
for (( pos=0 ; pos<strlen ; pos++ )); do
c=${string:$pos:1}
case "$c" in
[-_.~a-zA-Z0-9] ) o="$c" ;;
* ) printf -v o '%%%02X' "'$c"
esac
encoded+="$o"
done
echo "$encoded"
}
# function to print a message in a box
print_box_message() {
message=("$@") # Store all arguments in the array "message"
edge="======================================"
padding=" "
echo "$edge"
for element in "${message[@]}"; do
echo "${padding}${element}"
done
echo "$edge"
}
# check command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# install package based on platform
install_package() {
if command_exists yum; then
sudo yum install -y "$1"
elif command_exists apt; then
sudo apt install -y "$1"
elif command_exists brew; then
brew install "$1"
else
echo "Package manager not found. Please install $1 manually."
fi
}
# Function to check if sudo is required for Docker Compose command
check_for_docker_compose_sudo() {
if docker-compose ps >/dev/null 2>&1; then
echo "n"
else
echo "y"
fi
}
# ***************** HELPER FUNCTIONS END ***********************************
# ******************************************************************************
# ******************************************************************************
# ******************** SYSTEM REQUIREMENTS CHECK START *************************
# Check if the following requirements are met:
# a. docker, docker-compose, jq installed
# b. port mapping check : 80,443 are free or being used by nginx container
REQUIRED_PORTS=(80 443)
echo "** Performing nocodb system check and setup. This step may require sudo permissions"
# pre install wget if not found
if ! command_exists wget; then
echo "wget is not installed. Setting up for installation..."
install_package wget
fi
# d. Check if required tools are installed
echo " | Checking if required tools (docker, docker-compose, lsof) are installed..."
for tool in docker docker-compose lsof openssl; do
if ! command_exists "$tool"; then
echo "$tool is not installed. Setting up for installation..."
if [ "$tool" = "docker-compose" ]; then
sudo -E curl -L https://github.com/docker/compose/releases/download/1.29.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
elif [ "$tool" = "docker" ]; then
wget -qO- https://get.docker.com/ | sh
elif [ "$tool" = "lsof" ]; then
install_package lsof
fi
fi
done
# e. Check if NocoDB is already installed and its expected version
# echo "Checking if NocoDB is already installed and its expected version..."
# Replace the following command with the actual command to check NocoDB installation and version
# Example: nocodb_version=$(command_to_get_nocodb_version)
# echo "NocoDB version: $nocodb_install_version"
# f. Port mapping check
echo " | Checking port accessibility..."
for port in "${REQUIRED_PORTS[@]}"; do
if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null; then
echo " | WARNING: Port $port is in use. Please make sure it is free." >&2
else
echo " | Port $port is free."
fi
done
echo "** System check completed successfully. **"
# Define an array to store the messages to be printed at the end
message_arr=()
# extract public ip address
PUBLIC_IP=$(dig +short myip.opendns.com @resolver1.opendns.com)
# Check if the public IP address is not empty, if empty then use the localhost
if [ -z "$PUBLIC_IP" ]; then
PUBLIC_IP="localhost"
fi
# generate a folder for the docker-compose file which is not existing and do the setup within the folder
# Define the folder name
FOLDER_NAME="nocodb_$(date +"%Y%m%d_%H%M%S")"
# prompt for custom folder name and if left empty skip
#echo "Enter a custom folder name or press Enter to use the default folder name ($FOLDER_NAME): "
#read CUSTOM_FOLDER_NAME
message_arr+=("Setup folder: $FOLDER_NAME")
if [ -n "$CUSTOM_FOLDER_NAME" ]; then
FOLDER_NAME="$CUSTOM_FOLDER_NAME"
fi
# Create the folder
mkdir -p "$FOLDER_NAME"
# Navigate into the folder
cd "$FOLDER_NAME" || exit
# ******************** SYSTEM REQUIREMENTS CHECK END **************************
# ******************************************************************************
# ******************** INPUTS FROM USER START ********************************
# ******************************************************************************
echo "Choose Community or Enterprise Edition [CE/EE] (default: CE): "
read EDITION
echo "Do you want to configure SSL [Y/N] (default: N): "
read SSL_ENABLED
if [ -n "$SSL_ENABLED" ] && { [ "$SSL_ENABLED" = "Y" ] || [ "$SSL_ENABLED" = "y" ]; }; then
SSL_ENABLED='y'
echo "Enter the domain name for the SSL certificate: "
read DOMAIN_NAME
if [ -z "$DOMAIN_NAME" ]; then
echo "Domain name is required for SSL configuration"
exit 1
fi
message_arr+=("Domain: $DOMAIN_NAME")
else
# prompt for ip address and if left empty use extracted public ip
echo "Enter the IP address or domain name for the NocoDB instance (default: $PUBLIC_IP): "
read DOMAIN_NAME
if [ -z "$DOMAIN_NAME" ]; then
DOMAIN_NAME="$PUBLIC_IP"
fi
fi
if [ -n "$EDITION" ] && { [ "$EDITION" = "EE" ] || [ "$EDITION" = "ee" ]; }; then
echo "Enter the NocoDB license key: "
read LICENSE_KEY
if [ -z "$LICENSE_KEY" ]; then
echo "License key is required for Enterprise Edition installation"
exit 1
fi
fi
echo "Do you want to enabled Redis for caching [Y/N] (default: Y): "
read REDIS_ENABLED
if [ -z "$REDIS_ENABLED" ] || { [ "$REDIS_ENABLED" != "N" ] && [ "$REDIS_ENABLED" != "n" ]; }; then
message_arr+=("Redis: Enabled")
else
message_arr+=("Redis: Disabled")
fi
echo "Do you want to enabled Watchtower for automatic updates [Y/N] (default: Y): "
read WATCHTOWER_ENABLED
if [ -z "$WATCHTOWER_ENABLED" ] || { [ "$WATCHTOWER_ENABLED" != "N" ] && [ "$WATCHTOWER_ENABLED" != "n" ]; }; then
message_arr+=("Watchtower: Enabled")
else
message_arr+=("Watchtower: Disabled")
fi
# ******************************************************************************
# *********************** INPUTS FROM USER END ********************************
# ******************************************************************************
# *************************** SETUP START *************************************
# Generate a strong random password for PostgreSQL
STRONG_PASSWORD=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9!@#$%^&*()-_+=' | head -c 32)
REDIS_PASSWORD=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 24)
# Encode special characters in the password for JDBC URL usage
ENCODED_PASSWORD=$(urlencode "$STRONG_PASSWORD")
IMAGE="nocodb/nocodb:latest";
# Determine the Docker image to use based on the edition
if [ -n "$EDITION" ] && { [ "$EDITION" = "EE" ] || [ "$EDITION" = "ee" ]; }; then
IMAGE="nocodb/nocodb-ee:latest"
DATABASE_URL="DATABASE_URL=postgres://postgres:${ENCODED_PASSWORD}@db:5432/nocodb"
else
# use NC_DB url until the issue with DATABASE_URL is resolved(encoding)
DATABASE_URL="NC_DB=pg://db:5432?d=nocodb&user=postgres&password=${ENCODED_PASSWORD}"
fi
message_arr+=("Docker image: $IMAGE")
DEPENDS_ON=""
# Add Redis service if enabled
if [ -z "$REDIS_ENABLED" ] || { [ "$REDIS_ENABLED" != "N" ] && [ "$REDIS_ENABLED" != "n" ]; }; then
DEPENDS_ON="- redis"
fi
# Write the Docker Compose file with the updated password
cat <<EOF > docker-compose.yml
version: '3'
services:
nocodb:
image: ${IMAGE}
env_file: docker.env
depends_on:
- db
${DEPENDS_ON}
restart: unless-stopped
volumes:
- ./nocodb:/usr/app/data
labels:
- "com.centurylinklabs.watchtower.enable=true"
networks:
- nocodb-network
db:
image: postgres:16.1
env_file: docker.env
volumes:
- ./postgres:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
interval: 10s
retries: 10
test: "pg_isready -U \"\$\$POSTGRES_USER\" -d \"\$\$POSTGRES_DB\""
timeout: 2s
networks:
- nocodb-network
nginx:
image: nginx:latest
volumes:
- ./nginx:/etc/nginx/conf.d
EOF
if [ "$SSL_ENABLED" = 'y' ] || [ "$SSL_ENABLED" = 'Y' ]; then
cat <<EOF >> docker-compose.yml
- webroot:/var/www/certbot
- ./letsencrypt:/etc/letsencrypt
- letsencrypt-lib:/var/lib/letsencrypt
EOF
fi
cat <<EOF >> docker-compose.yml
ports:
- "80:80"
- "443:443"
depends_on:
- nocodb
restart: unless-stopped
networks:
- nocodb-network
EOF
if [ "$SSL_ENABLED" = 'y' ] || [ "$SSL_ENABLED" = 'Y' ]; then
cat <<EOF >> docker-compose.yml
certbot:
image: certbot/certbot
volumes:
- ./letsencrypt:/etc/letsencrypt
- letsencrypt-lib:/var/lib/letsencrypt
- webroot:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait \$\${!}; done;'"
depends_on:
- nginx
restart: unless-stopped
networks:
- nocodb-network
EOF
fi
if [ -z "$REDIS_ENABLED" ] || { [ "$REDIS_ENABLED" != "N" ] && [ "$REDIS_ENABLED" != "n" ]; }; then
cat <<EOF >> docker-compose.yml
redis:
image: redis:latest
restart: unless-stopped
env_file: docker.env
command:
- /bin/sh
- -c
- redis-server --requirepass "\$\${REDIS_PASSWORD}"
volumes:
- redis:/data
healthcheck:
test: [ "CMD", "redis-cli", "-a", "\$\${REDIS_PASSWORD}", "--raw", "incr", "ping" ]
networks:
- nocodb-network
EOF
fi
if [ -z "$WATCHTOWER_ENABLED" ] || { [ "$WATCHTOWER_ENABLED" != "N" ] && [ "$WATCHTOWER_ENABLED" != "n" ]; }; then
cat <<EOF >> docker-compose.yml
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --schedule "0 2 * * 6" --cleanup
restart: unless-stopped
networks:
- nocodb-network
EOF
fi
if [ "$SSL_ENABLED" = 'y' ] || [ "$SSL_ENABLED" = 'Y' ]; then
cat <<EOF >> docker-compose.yml
volumes:
letsencrypt-lib:
webroot:
EOF
fi
# add the cache volume
if [ -z "$REDIS_ENABLED" ] || { [ "$REDIS_ENABLED" != "N" ] && [ "$REDIS_ENABLED" != "n" ]; }; then
# check ssl enabled
if [ "$SSL_ENABLED" = 'y' ] || [ "$SSL_ENABLED" = 'Y' ]; then
cat <<EOF >> docker-compose.yml
redis:
EOF
else
cat <<EOF >> docker-compose.yml
volumes:
redis:
EOF
fi
fi
# Create the network
cat <<EOF >> docker-compose.yml
networks:
nocodb-network:
driver: bridge
EOF
# Write the docker.env file
cat <<EOF > docker.env
POSTGRES_DB=nocodb
POSTGRES_USER=postgres
POSTGRES_PASSWORD=${STRONG_PASSWORD}
$DATABASE_URL
NC_LICENSE_KEY=${LICENSE_KEY}
EOF
# add redis env if enabled
if [ -z "$REDIS_ENABLED" ] || { [ "$REDIS_ENABLED" != "N" ] && [ "$REDIS_ENABLED" != "n" ]; }; then
cat <<EOF >> docker.env
REDIS_PASSWORD=${REDIS_PASSWORD}
NC_REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
EOF
fi
mkdir -p ./nginx
# Create nginx config with the provided domain name
cat > ./nginx/default.conf <<EOF
server {
listen 80;
EOF
if [ "$SSL_ENABLED" = 'y' ] || [ "$SSL_ENABLED" = 'Y' ]; then
cat >> ./nginx/default.conf <<EOF
server_name $DOMAIN_NAME;
EOF
fi
cat >> ./nginx/default.conf <<EOF
location / {
proxy_pass http://nocodb:8080;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
EOF
if [ "$SSL_ENABLED" = 'y' ] || [ "$SSL_ENABLED" = 'Y' ]; then
cat >> ./nginx/default.conf <<EOF
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
EOF
fi
cat >> ./nginx/default.conf <<EOF
}
EOF
if [ "$SSL_ENABLED" = 'y' ] || [ "$SSL_ENABLED" = 'Y' ]; then
mkdir -p ./nginx-post-config
# Create nginx config with the provided domain name
cat > ./nginx-post-config/default.conf <<EOF
server {
listen 80;
server_name $DOMAIN_NAME;
location / {
return 301 https://\$host\$request_uri;
}
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
}
server {
listen 443 ssl;
server_name $DOMAIN_NAME;
ssl_certificate /etc/letsencrypt/live/$DOMAIN_NAME/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$DOMAIN_NAME/privkey.pem;
location / {
proxy_pass http://nocodb:8080;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
EOF
fi
IS_DOCKER_COMPOSE_REQUIRE_SUDO=$(check_for_docker_compose_sudo)
# Generate the update.sh file for upgrading images
if [ "$IS_DOCKER_COMPOSE_REQUIRE_SUDO" = "y" ]; then
cat > ./update.sh <<EOF
sudo docker-compose pull
sudo docker-compose up -d --force-recreate
sudo docker image prune -a -f
EOF
else
cat > ./update.sh <<EOF
docker-compose pull
docker-compose up -d --force-recreate
docker image prune -a -f
EOF
fi
message_arr+=("Update script: update.sh")
# Pull latest images and start the docker-compose setup
if [ "$IS_DOCKER_COMPOSE_REQUIRE_SUDO" = "y" ]; then
echo "Docker compose requires sudo. Running the docker-compose setup with sudo."
sudo docker-compose pull
sudo docker-compose up -d
else
docker-compose pull
docker-compose up -d
fi
echo 'Waiting for Nginx to start...';
sleep 5
if [ "$SSL_ENABLED" = 'y' ] || [ "$SSL_ENABLED" = 'Y' ]; then
echo 'Starting Letsencrypt certificate request...';
if [ "$IS_DOCKER_COMPOSE_REQUIRE_SUDO" = "y" ]; then
sudo docker-compose exec certbot certbot certonly --webroot --webroot-path=/var/www/certbot -d $DOMAIN_NAME --email contact@$DOMAIN_NAME --agree-tos --no-eff-email && echo "Certificate request successful" || echo "Certificate request failed"
else
docker-compose exec certbot certbot certonly --webroot --webroot-path=/var/www/certbot -d $DOMAIN_NAME --email contact@$DOMAIN_NAME --agree-tos --no-eff-email && echo "Certificate request successful" || echo "Certificate request failed"
fi
# Initial Let's Encrypt certificate request
# Update the nginx config to use the new certificates
rm -rf ./nginx/default.conf
mv ./nginx-post-config/default.conf ./nginx/
rm -r ./nginx-post-config
echo "Restarting nginx to apply the new certificates"
# Reload nginx to apply the new certificates
if [ "$IS_DOCKER_COMPOSE_REQUIRE_SUDO" = "y" ]; then
sudo docker-compose exec nginx nginx -s reload
else
docker-compose exec nginx nginx -s reload
fi
message_arr+=("NocoDB is now available at https://$DOMAIN_NAME")
elif [ -n "$DOMAIN_NAME" ]; then
message_arr+=("NocoDB is now available at http://$DOMAIN_NAME")
else
message_arr+=("NocoDB is now available at http://localhost")
fi
print_box_message "${mecdessage_arr[@]}"
# *************************** SETUP END *************************************
# ******************************************************************************

BIN
packages/nc-gui/assets/img/placeholder/api-tokens.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
packages/nc-gui/assets/img/placeholder/invite-team.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

BIN
packages/nc-gui/assets/img/placeholder/link-records.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
packages/nc-gui/assets/img/placeholder/multi-field-editor.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
packages/nc-gui/assets/img/placeholder/table.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
packages/nc-gui/assets/img/placeholder/webhooks.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

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

@ -45,6 +45,8 @@ const pagination = reactive({
pageSize: 10,
})
const isLoadingAllTokens = ref(true)
const setDefaultTokenName = () => {
selectedTokenData.value.description = extractNextDefaultName(
[...allTokens.value.map((el) => el?.description || '')],
@ -94,7 +96,7 @@ const updateAllTokens = (type: 'delete' | 'add', token: IApiTokenInfo) => {
setDefaultTokenName()
}
const loadTokens = async (page = currentPage.value, limit = currentLimit.value) => {
const loadTokens = async (page = currentPage.value, limit = currentLimit.value, hideShowNewToken = false) => {
currentPage.value = page
try {
const response: any = await api.orgTokens.list({
@ -103,18 +105,30 @@ const loadTokens = async (page = currentPage.value, limit = currentLimit.value)
offset: searchText.value.length === 0 ? (page - 1) * limit : 0,
},
} as RequestParams)
if (!response) return
if (!response) {
isLoadingAllTokens.value = false
return
}
pagination.total = response.pageInfo.totalRows ?? 0
pagination.pageSize = 10
tokens.value = response.list as IApiTokenInfo[]
if (hideShowNewToken) {
showNewTokenModal.value = false
selectedTokenData.value = {}
}
if (!allTokens.value.length) {
await loadAllTokens(pagination.total)
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
if (isLoadingAllTokens.value) {
isLoadingAllTokens.value = false
}
}
}
@ -159,11 +173,10 @@ const generateToken = async () => {
if (!isValidTokenName.value) return
try {
const token = await api.orgTokens.create(selectedTokenData.value)
showNewTokenModal.value = false
// Token generated successfully
// message.success(t('msg.success.tokenGenerated'))
selectedTokenData.value = {}
await loadTokens()
await loadTokens(currentPage.value, currentLimit.value, true)
updateAllTokens('add', token as IApiTokenInfo)
} catch (e: any) {
@ -216,7 +229,7 @@ const handleCancel = () => {
<div class="max-w-202 mx-auto px-4 h-full" data-testid="nc-token-list">
<div class="py-2 flex gap-4 items-baseline justify-between">
<h6 class="text-2xl text-left font-bold" data-rec="true">{{ $t('title.apiTokens') }}</h6>
<NcTooltip :disabled="!(isEeUI && tokens.length)">
<NcTooltip v-if="tokens.length" :disabled="!(isEeUI && tokens.length)">
<template #title>{{ $t('labels.tokenLimit') }}</template>
<NcButton
:disabled="showNewTokenModal || (isEeUI && tokens.length)"
@ -237,7 +250,7 @@ const handleCancel = () => {
</NcTooltip>
</div>
<span data-rec="true">{{ $t('msg.apiTokenCreate') }}</span>
<div class="mt-5 h-[calc(100%-13rem)]">
<div v-if="!isLoadingAllTokens && (tokens.length || showNewTokenModal)" class="mt-5 h-[calc(100%-13rem)]">
<div class="h-full w-full !overflow-hidden rounded-md">
<div class="flex w-full pl-5 bg-gray-50 border-1 rounded-t-md">
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9" data-rec="true">{{ $t('title.tokenName') }}</span>
@ -268,6 +281,7 @@ const handleCancel = () => {
class="!rounded-lg !py-1"
placeholder="Token Name"
data-testid="nc-token-input"
:disabled="isLoading"
@press-enter="generateToken"
/>
<span v-if="!isValidTokenName" class="text-red-500 text-xs font-light mt-1.5 ml-1" data-rec="true"
@ -278,13 +292,7 @@ const handleCancel = () => {
<NcButton v-if="!isLoading" type="secondary" size="small" @click="handleCancel">
{{ $t('general.cancel') }}
</NcButton>
<NcButton
type="primary"
size="sm"
:is-loading="isLoading"
data-testid="nc-token-save-btn"
@click="generateToken"
>
<NcButton type="primary" size="sm" :loading="isLoading" data-testid="nc-token-save-btn" @click="generateToken">
{{ $t('general.save') }}
</NcButton>
</div>
@ -329,15 +337,15 @@ const handleCancel = () => {
@click="hideOrShowToken(el.token as string)"
/>
</NcTooltip>
<NcTooltip placement="top" class="h-4">
<NcTooltip placement="top">
<template #title>{{ $t('general.copy') }}</template>
<component
:is="iconMap.copy"
class="hover::cursor-pointer w-4 h-4 text-gray-600 mt-0.25"
class="hover::cursor-pointer w-4 h-4 text-gray-600"
@click="copyToken(el.token)"
/>
</NcTooltip>
<NcTooltip placement="top" class="mb-0.5">
<NcTooltip placement="top">
<template #title>{{ $t('general.delete') }}</template>
<component
:is="iconMap.delete"
@ -351,6 +359,25 @@ const handleCancel = () => {
</div>
</div>
</div>
<div
v-else-if="!isLoadingAllTokens && !tokens.length && !showNewTokenModal"
class="max-w-[40rem] border px-3 py-6 flex flex-col items-center justify-center gap-6 text-center"
>
<img src="~assets/img/placeholder/api-tokens.png" class="!w-[22rem] flex-none" />
<div class="text-2xl text-gray-800 font-bold">{{ $t('placeholder.noTokenCreated') }}</div>
<div class="text-sm text-gray-700">
{{ $t('placeholder.noTokenCreatedLabel') }}
</div>
<NcButton class="!rounded-lg !py-3 !h-10" data-testid="nc-token-create" type="primary" @click="showNewTokenModal = true">
<span class="hidden md:block" data-rec="true">
{{ $t('title.createNewToken') }}
</span>
<span class="flex items-center justify-center md:hidden" data-rec="true">
<component :is="iconMap.plus" />
</span>
</NcButton>
</div>
<div v-if="pagination.total > 10" class="flex items-center justify-center mt-5">
<a-pagination

20
packages/nc-gui/components/account/UserList.vue

@ -273,9 +273,9 @@ const openDeleteModal = (user: UserType) => {
class="w-4 h-4 text-primary"
/>
</div>
<span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
<div class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgCreator') }}
</span>
</div>
</a-select-option>
<a-select-option
@ -292,9 +292,9 @@ const openDeleteModal = (user: UserType) => {
class="w-4 h-4 text-primary"
/>
</div>
<span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
<div class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgViewer') }}
</span>
</div>
</a-select-option>
</NcSelect>
<div v-else class="font-weight-bold" data-rec="true">
@ -345,6 +345,18 @@ const openDeleteModal = (user: UserType) => {
</div>
</span>
</div>
<div
v-if="sortedUsers.length === 1"
class="user pt-12 pb-4 px-2 flex flex-col items-center gap-6 text-center border-b-1 border-l-1 border-r-1"
>
<div class="text-2xl text-gray-800 font-bold">
{{ $t('placeholder.inviteYourTeam') }}
</div>
<div class="text-sm text-gray-700">
{{ $t('placeholder.inviteYourTeamLabel') }}
</div>
<img src="~assets/img/placeholder/invite-team.png" class="!w-[30rem] flex-none" />
</div>
</section>
</div>
<div v-if="pagination.total > 10" class="flex items-center justify-center mt-4">

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

@ -84,15 +84,15 @@ const submitCurrency = () => {
}
const onBlur = () => {
// triggered by events like forcus-out / pressing enter
// triggered by events like focus-out / pressing enter
// for non-firefox browsers only
submitCurrency()
}
const onKeydownEnter = () => {
// for firefox, onBlur is never executed
// onBlur is never executed for firefox & safari
// we use keydown.enter to trigger submitCurrency
if (/Firefox/.test(navigator.userAgent)) {
if (/(Firefox|Safari)/.test(navigator.userAgent)) {
submitCurrency()
}
}

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

@ -134,7 +134,13 @@ onMounted(() => {
<span class="menu-btn"> {{ $t('labels.community.joinReddit') }} </span>
</NcMenuItem>
</a>
<a v-e="['c:nocodb:twitter']" href="https://twitter.com/nocodb" target="_blank" class="!underline-transparent">
<a
v-e="['c:nocodb:twitter']"
href="https://twitter.com/nocodb"
target="_blank"
class="!underline-transparent"
rel="noopener noreferrer"
>
<NcMenuItem class="social-icon-wrapper group">
<GeneralIcon class="text-gray-500 group-hover:text-gray-800 my-0.5" icon="twitter" />
<span class="menu-btn"> {{ $t('labels.twitter') }} </span>

2
packages/nc-gui/components/general/JoinCloud.vue

@ -28,7 +28,7 @@
</div>
<div class="self-stretch text-gray-500 text-base leading-normal">/ month / workspace</div>
<a href="https://app.nocodb.com/#/signin" target="_blank" class="!no-underline">
<a href="https://app.nocodb.com/#/signin" target="_blank" class="!no-underline" rel="noopener">
<NcButton class="text-gray-700 text-base font-semibold leading-tight py-4 w-full">Start for Free</NcButton>
</a>
<div class="self-stretch text-center text-gray-500 text-xs font-medium leading-none mb-4">

2
packages/nc-gui/components/general/ReleaseInfo.vue

@ -67,7 +67,7 @@ onMounted(async () => await fetchReleaseInfo())
<nuxt-link
no-prefetch
no-rel
rel="noopener"
class="!text-primary !no-underline"
to="https://docs.nocodb.com/getting-started/upgrading"
target="_blank"

6
packages/nc-gui/components/general/SocialCard.vue

@ -33,7 +33,7 @@ function openKeyboardShortcutDialog() {
<nuxt-link
v-e="['e:docs']"
no-prefetch
no-rel
rel="noopener"
class="text-primary !no-underline !text-current"
target="_blank"
to="https://docs.nocodb.com/"
@ -49,7 +49,7 @@ function openKeyboardShortcutDialog() {
<nuxt-link
v-e="['e:api-docs']"
no-prefetch
no-rel
rel="noopener"
class="text-primary !no-underline !text-current"
target="_blank"
to="https://apis.nocodb.com/"
@ -152,7 +152,7 @@ function openKeyboardShortcutDialog() {
<nuxt-link
v-e="['e:hiring']"
no-prefetch
no-rel
rel="noopener"
class="!no-underline !text-current"
target="_blank"
to="http://careers.nocodb.com"

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

@ -121,6 +121,7 @@ const onCreateBaseClick = () => {
</div>
</component>
</div>
<template v-if="activeTables.length">
<div class="flex flex-row w-full text-gray-400 border-b-1 border-gray-50 py-3 px-2.5">
<div class="w-2/5">{{ $t('objects.table') }}</div>
<div class="w-1/5">{{ $t('general.source') }}</div>
@ -159,6 +160,17 @@ const onCreateBaseClick = () => {
</div>
</div>
</div>
</template>
<div v-else class="py-3 flex items-center gap-6 <lg:flex-col">
<img src="~assets/img/placeholder/table.png" class="!w-[23rem] flex-none" />
<div class="text-center lg:text-left">
<div class="text-2xl text-gray-800 font-bold">{{ $t('placeholder.createTable') }}</div>
<div class="text-sm text-gray-700 pt-6">
{{ $t('placeholder.createTableLabel') }}
</div>
</div>
</div>
<ProjectImportModal v-if="defaultBase" v-model:visible="isImportModalOpen" :source="defaultBase" />
<LazyDashboardSettingsDataSourcesCreateBase v-model:open="isNewBaseModalOpen" />
</div>

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

@ -252,7 +252,7 @@ onUnmounted(() => {
<LazyCellJson v-else-if="isJSON(column)" v-model="vModel" />
<LazyCellText v-else v-model="vModel" />
<div
v-if="(isPublic && readOnly && !isForm) || (isSystemColumn(column) && !isAttachment(column) && !isTextArea(column))"
v-if="((isPublic && readOnly && !isForm) || (isSystemColumn(column) && !isAttachment(column))) && !isTextArea(column)"
class="nc-locked-overlay"
/>
</template>

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

@ -119,10 +119,6 @@ const { betaFeatureToggleState } = useBetaFeatureToggle()
const updateView = useDebounceFn(
() => {
if ((formViewData.value?.subheading?.length || 0) > 255) {
return message.error(t('msg.error.formDescriptionTooLong'))
}
updateFormView(formViewData.value)
},
300,

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

@ -1254,7 +1254,7 @@ watch(
@add="onFieldAdd"
/>
<div v-else class="w-[25rem] flex flex-col justify-center p-4 items-center">
<img src="~assets/img/fieldPlaceholder.svg" class="!w-[18rem]" />
<img src="~assets/img/placeholder/multi-field-editor.png" class="!w-[18rem]" />
<div class="text-2xl text-gray-600 font-bold text-center pt-6">{{ $t('labels.multiField.selectField') }}</div>
<div class="text-center text-sm px-2 text-gray-500 pt-6">
{{ $t('labels.multiField.selectFieldLabel') }}

11
packages/nc-gui/components/smartsheet/details/Webhooks.vue

@ -197,13 +197,10 @@ watch(
</NcButton>
</div>
<div v-if="!selectedHookId && !isDraftMode" class="flex flex-col h-full w-full items-center">
<div v-if="hooks.length === 0" class="flex flex-col px-1.5 py-2.5 ml-1 h-full justify-center items-center gap-y-6">
<GeneralIcon icon="webhook" class="flex text-5xl h-10" style="-webkit-text-stroke: 0.5px" />
<div class="flex text-gray-600 font-medium text-lg">{{ $t('msg.createWebhookMsg1') }}</div>
<div class="flex flex-col items-center">
<div class="flex">{{ $t('msg.createWebhookMsg2') }}</div>
<div class="flex">{{ $t('msg.createWebhookMsg3') }}</div>
</div>
<div v-if="hooks.length === 0" class="flex flex-col px-1.5 py-2.5 ml-1 h-full items-center gap-y-6 text-center">
<img src="~assets/img/placeholder/webhooks.png" class="!w-[24rem] flex-none" />
<div class="text-gray-700 font-bold text-2xl">{{ $t('msg.createWebhookMsg1') }}</div>
<div class="text-gray-700 max-w-[24rem]">{{ $t('msg.createWebhookMsg2') }}</div>
<NcButton v-e="['c:actions:webhook']" class="flex max-w-40" type="primary" @click="createWebhook()">
<div class="flex flex-row items-center justify-between w-full">
<span class="ml-1">{{ $t('activity.newWebhook') }}</span>

12
packages/nc-gui/components/smartsheet/expanded-form/Comments.vue

@ -158,8 +158,8 @@ const onClickAudit = () => {
@click="tab = 'comments'"
>
<div class="tab-title nc-tab">
<MdiMessageOutline class="h-4 w-4" />
Comments
<MdiMessageOutline class="h-4 w-4 flex-none" />
<span class="<lg:hidden">Comments</span>
</div>
</div>
<NcTooltip v-if="appInfo.ee" class="tab flex-1">
@ -173,7 +173,7 @@ const onClickAudit = () => {
>
<div class="tab-title nc-tab select-none">
<MdiFileDocumentOutline class="h-4 w-4" />
Audits
<span class="<lg:hidden">Audits</span>
</div>
</div>
</NcTooltip>
@ -188,7 +188,7 @@ const onClickAudit = () => {
>
<div class="tab-title nc-tab">
<MdiFileDocumentOutline class="h-4 w-4" />
Audits
<span class="<lg:hidden">Audits</span>
</div>
</div>
</div>
@ -211,13 +211,13 @@ const onClickAudit = () => {
</div>
<div v-else ref="commentsWrapperEl" class="flex flex-col h-full py-2 pl-2 pr-1 space-y-2 nc-scrollbar-md">
<div v-for="log of comments" :key="log.id">
<div class="bg-white rounded-xl group border-1 gap-2 border-gray-200">
<div class="bg-white rounded-xl group border-1 gap-2 border-gray-200 overflow-hidden">
<div class="flex flex-col p-4 gap-3">
<div class="flex justify-between">
<div class="flex items-center gap-2">
<GeneralUserIcon size="base" :name="log.display_name" :email="log.user" />
<div class="flex flex-col">
<div class="flex flex-col <lg:max-w-22">
<NcTooltip class="truncate max-w-42" show-on-truncate-only>
<template #title>
{{ log.display_name?.trim() || log.user || 'Shared source' }}

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

@ -520,7 +520,7 @@ export default {
<div class="h-[85vh] xs:(max-h-full) max-h-215 flex flex-col p-6">
<div class="flex h-9.5 flex-shrink-0 w-full items-center nc-expanded-form-header relative mb-4 justify-between">
<template v-if="!isMobileMode">
<div class="flex gap-3 w-100">
<div class="flex gap-3 w-100 <lg:max-w-64">
<div class="flex gap-2">
<NcButton
v-if="props.showNextPrevIcons"
@ -560,7 +560,7 @@ export default {
<NcButton
v-if="!isNew && rowId"
type="secondary"
class="!xs:hidden text-gray-700"
class="!<lg:hidden text-gray-700"
:disabled="isLoading"
@click="copyRecordUrl()"
>
@ -582,6 +582,18 @@ export default {
{{ $t('general.reload') }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="!isNew && rowId"
type="secondary"
class="!lg:hidden text-gray-700"
:disabled="isLoading"
@click="copyRecordUrl()"
>
<div v-e="['c:row-expand:copy-url']" data-testid="nc-expanded-form-copy-url" class="flex gap-2 items-center">
<component :is="iconMap.link" class="cursor-pointer" />
{{ $t('labels.copyRecordURL') }}
</div>
</NcMenuItem>
<NcMenuItem v-if="isUIAllowed('dataEdit')" class="text-gray-700" @click="!isNew ? onDuplicateRow() : () => {}">
<div
v-e="['c:row-expand:duplicate']"
@ -664,13 +676,13 @@ export default {
v-for="(col, i) of fields"
v-show="isFormula(col) || !isVirtualCol(col) || !isNew || isLinksOrLTAR(col)"
:key="col.title"
class="nc-expanded-form-row mt-2 py-2 xs:w-full"
class="nc-expanded-form-row mt-2 py-2 <lg:w-full"
:class="`nc-expand-col-${col.title}`"
:col-id="col.id"
:data-testid="`nc-expand-col-${col.title}`"
>
<div class="flex items-start flex-row sm:(gap-x-6) xs:(flex-col w-full) nc-expanded-cell min-h-10">
<div class="w-48 xs:(w-full) mt-0.25 !h-[35px]">
<div class="flex items-start flex-row sm:(gap-x-6) <lg:(flex-col w-full) nc-expanded-cell min-h-10">
<div class="w-48 <lg:(w-full) mt-0.25 !h-[35px]">
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(col)"
class="nc-expanded-cell-header h-full"
@ -696,7 +708,7 @@ export default {
<SmartsheetDivDataCell
v-if="col.title"
:ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="bg-white w-80 xs:w-full px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
class="bg-white w-80 <lg:w-full px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
:class="{
'!bg-gray-50 !select-text nc-system-field': isReadOnlyVirtualCell(col),
}"
@ -725,12 +737,12 @@ export default {
</template>
</div>
</div>
<div v-if="hiddenFields.length > 0" class="flex w-full sm:px-12 xs:(px-1 mt-2) items-center py-3">
<div v-if="hiddenFields.length > 0" class="flex w-full lg:px-12 <lg:(px-1 mt-2) items-center py-3">
<div class="flex-grow h-px mr-1 bg-gray-100"></div>
<NcButton
type="secondary"
:size="isMobileMode ? 'medium' : 'small'"
class="flex-shrink-1 !text-sm"
class="flex-shrink !text-sm overflow-hidden"
@click="toggleHiddenFields"
>
{{ showHiddenFields ? `Hide ${hiddenFields.length} hidden` : `Show ${hiddenFields.length} hidden` }}
@ -744,12 +756,12 @@ export default {
v-for="(col, i) of hiddenFields"
v-show="isFormula(col) || !isVirtualCol(col) || !isNew || isLinksOrLTAR(col)"
:key="col.title"
class="sm:(mt-2) py-2 xs:w-full"
class="sm:(mt-2) py-2 <lg:w-full"
:class="`nc-expand-col-${col.title}`"
:data-testid="`nc-expand-col-${col.title}`"
>
<div class="sm:gap-x-6 flex sm:flex-row xs:(flex-col) items-start min-h-10">
<div class="sm:w-48 xs:w-full scale-110 !h-[35px]">
<div class="sm:gap-x-6 flex sm:flex-row <lg:(flex-col w-full) items-start min-h-10">
<div class="sm:w-48 <lg:w-full scale-110 !h-[35px]">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" class="nc-expanded-cell-header" />
<LazySmartsheetHeaderCell v-else class="nc-expanded-cell-header" :column="col" />
@ -771,7 +783,7 @@ export default {
<LazySmartsheetDivDataCell
v-if="col.title"
:ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="bg-white rounded-lg w-80 border-1 overflow-hidden border-gray-200 px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
class="bg-white rounded-lg w-80 <lg:w-full border-1 overflow-hidden border-gray-200 px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(col)"
@ -862,7 +874,7 @@ export default {
</div>
<div
v-if="showRightSections"
class="nc-comments-drawer border-1 relative border-gray-200 w-1/3 max-w-125 bg-gray-50 rounded-xl min-w-0 overflow-hidden h-full xs:hidden"
class="nc-comments-drawer border-1 relative border-gray-200 w-1/3 max-w-125 bg-gray-50 rounded-xl min-w-50 overflow-hidden h-full xs:hidden"
:class="{ active: commentsDrawer && isUIAllowed('commentList') }"
>
<SmartsheetExpandedFormComments :loading="isLoading" />

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

@ -558,7 +558,7 @@ const {
clearSelectedRangeOfCells,
makeEditable,
scrollToCell,
(e: KeyboardEvent) => {
async (e: KeyboardEvent) => {
// ignore navigating if picker(Date, Time, DateTime, Year)
// or single/multi select options is open
const activePickerOrDropdownEl = document.querySelector(
@ -601,6 +601,42 @@ const {
editEnabled.value = false
return true
}
} else if (e.key === 'Tab') {
if (e.shiftKey && activeCell.row === 0 && activeCell.col === 0 && !paginationDataRef.value?.isFirstPage) {
e.preventDefault()
await resetAndChangePage((paginationDataRef.value?.pageSize ?? 25) - 1, fields.value?.length - 1, -1)
return true
} else if (!e.shiftKey && activeCell.row === dataRef.value.length - 1 && activeCell.col === fields.value?.length - 1) {
e.preventDefault()
if (paginationDataRef.value?.isLastPage && isAddingEmptyRowAllowed.value) {
addEmptyRow()
await resetAndChangePage(dataRef.value.length - 1, 0)
return true
} else if (!paginationDataRef.value?.isLastPage) {
await resetAndChangePage(0, 0, 1)
return true
}
}
} else if (!cmdOrCtrl && !e.shiftKey && e.key === 'ArrowUp') {
if (activeCell.row === 0 && !paginationDataRef.value?.isFirstPage) {
e.preventDefault()
await resetAndChangePage((paginationDataRef.value?.pageSize ?? 25) - 1, activeCell.col!, -1)
return true
}
} else if (!cmdOrCtrl && !e.shiftKey && e.key === 'ArrowDown') {
if (activeCell.row === dataRef.value.length - 1) {
e.preventDefault()
if (paginationDataRef.value?.isLastPage && isAddingEmptyRowAllowed.value) {
addEmptyRow()
await resetAndChangePage(dataRef.value.length - 1, activeCell.col!)
return true
} else if (!paginationDataRef.value?.isLastPage) {
await resetAndChangePage(0, activeCell.col!, 1)
return true
}
}
}
if (cmdOrCtrl) {
@ -938,6 +974,21 @@ function scrollToCell(row?: number | null, col?: number | null) {
}
}
async function resetAndChangePage(row: number, col: number, pageChange?: number) {
clearSelectedRange()
if (pageChange !== undefined && paginationDataRef.value?.page) {
await changePage?.(paginationDataRef.value.page + pageChange)
await nextTick()
makeActive(row, col)
} else {
makeActive(row, col)
await nextTick()
}
scrollToCell?.()
}
const saveOrUpdateRecords = async (args: { metaValue?: TableType; viewMetaValue?: ViewType; data?: any } = {}) => {
let index = -1
for (const currentRow of args.data || dataRef.value) {

12
packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue

@ -1,4 +1,5 @@
<script setup lang="ts">
import { OrgUserRoles, ProjectRoles, extractRolesObj } from 'nocodb-sdk'
import type { GridType } from 'nocodb-sdk'
import { ActiveViewInj, IsLockedInj, iconMap, inject, ref, storeToRefs, useMenuCloseOnEsc, useUndoRedo } from '#imports'
@ -14,6 +15,8 @@ const { $api } = useNuxtApp()
const { addUndo, defineViewScope } = useUndoRedo()
const { user } = useGlobal()
const open = ref(false)
const updateRowHeight = async (rh: number, undo = false) => {
@ -35,7 +38,14 @@ const updateRowHeight = async (rh: number, undo = false) => {
}
try {
if (!isPublic.value && !isSharedBase.value) {
if (
!isPublic.value &&
!isSharedBase.value &&
!(
extractRolesObj(user.value?.roles ?? {})[ProjectRoles.VIEWER] ||
extractRolesObj(user.value?.roles ?? {})[OrgUserRoles.VIEWER]
)
) {
await $api.dbView.gridUpdate(view.value.id, {
row_height: rh,
})

81
packages/nc-gui/components/virtual-cell/components/ListChildItems.vue

@ -1,17 +1,14 @@
<script lang="ts" setup>
import { type ColumnType, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import type { Row } from '#imports'
import InboxIcon from '~icons/nc-icons/inbox'
import {
ColumnInj,
IsFormInj,
IsPublicInj,
ReadonlyInj,
type Row,
computed,
inject,
isPrimary,
onKeyStroke,
ref,
useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow,
@ -41,6 +38,8 @@ const injectedColumn = inject(ColumnInj, ref())
const readOnly = inject(ReadonlyInj, ref(false))
const filterQueryRef = ref<HTMLInputElement>()
const { isSharedBase } = storeToRefs(useBase())
const {
@ -92,8 +91,6 @@ const attachmentCol = computedInject(FieldsInj, (_fields) => {
return (relatedTableMeta.value.columns ?? []).filter((col) => isAttachment(col))[0]
})
const isFocused = ref(false)
const fields = computedInject(FieldsInj, (_fields) => {
return (relatedTableMeta.value.columns ?? [])
.filter((col) => !isSystemColumn(col) && !isPrimary(col) && !isLinksOrLTAR(col) && !isAttachment(col))
@ -129,10 +126,6 @@ watch(expandedFormDlg, () => {
}
})
onKeyStroke('Escape', () => {
vModel.value = false
})
/*
to render same number of skeleton as the number of cards
displayed
@ -175,6 +168,40 @@ const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
linkRow(rowRef, parseInt(id))
}
}
watch([filterQueryRef, isDataExist], () => {
if (readOnly.value || isPublic.value ? isDataExist.value : true) {
filterQueryRef.value?.focus()
}
})
const linkedShortcuts = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
vModel.value = false
} else if (e.key === 'ArrowDown') {
e.preventDefault()
try {
e.target?.nextElementSibling?.focus()
} catch (e) {}
} else if (e.key === 'ArrowUp') {
e.preventDefault()
try {
e.target?.previousElementSibling?.focus()
} catch (e) {}
} else if (e.key !== 'Tab' && e.key !== 'Shift' && e.key !== 'Enter' && e.key !== ' ') {
try {
filterQueryRef.value?.focus()
} catch (e) {}
}
}
onMounted(() => {
window.addEventListener('keydown', linkedShortcuts)
})
onUnmounted(() => {
window.removeEventListener('keydown', linkedShortcuts)
})
</script>
<template>
@ -198,11 +225,8 @@ const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
:display-value="headerDisplayValue"
/>
<div v-if="!isForm" class="flex mt-2 mb-2 items-center gap-2">
<div
class="flex items-center border-1 p-1 rounded-md w-full border-gray-200"
:class="{ '!border-primary': childrenListPagination.query.length !== 0 || isFocused }"
>
<MdiMagnify class="w-5 h-5 ml-2" />
<div class="flex items-center border-1 p-1 rounded-md w-full border-gray-200 !focus-within:border-primary">
<MdiMagnify class="w-5 h-5 ml-2 text-gray-500" />
<a-input
ref="filterQueryRef"
v-model:value="childrenListPagination.query"
@ -210,9 +234,13 @@ const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
class="w-full !sm:rounded-md xs:min-h-8 !xs:rounded-xl"
size="small"
:bordered="false"
@focus="isFocused = true"
@blur="isFocused = false"
@keydown.capture.stop
@keydown.capture.stop="
(e) => {
if (e.key === 'Escape') {
filterQueryRef?.blur()
}
}
"
@change="childrenListPagination.page = 1"
>
</a-input>
@ -264,24 +292,27 @@ const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
:is-linked="childrenList?.list ? isChildrenListLinked[Number.parseInt(id)] : true"
:is-loading="isChildrenListLoading[Number.parseInt(id)]"
@expand="onClick(refRow)"
@keydown.space.prevent="linkOrUnLink(refRow, id)"
@keydown.enter.prevent="() => onClick(refRow, id)"
@click="linkOrUnLink(refRow, id)"
/>
</template>
</div>
</div>
<div v-else class="pt-1 flex flex-col gap-3 my-auto items-center justify-center text-gray-500">
<InboxIcon class="w-16 h-16 mx-auto" />
<p>
{{ $t('msg.noRecordsAreLinkedFromTable') }}
{{ relatedTableMeta?.title }}
</p>
<div v-else class="pt-1 flex flex-col gap-4 my-auto items-center justify-center text-gray-500 text-center">
<img src="~assets/img/placeholder/link-records.png" class="!w-[18.5rem] flex-none" />
<div class="text-2xl text-gray-700 font-bold">{{ $t('msg.noLinkedRecords') }}</div>
<div class="text-gray-700">
{{ $t('msg.clickLinkRecordsToAddLinkFromTable', { tableName: relatedTableMeta?.title }) }}
</div>
<NcButton
v-if="!readOnly && childrenListCount < 1"
v-e="['c:links:link']"
data-testid="nc-child-list-button-link-to"
@click="emit('attachRecord')"
>
<div class="flex items-center gap-1"><MdiPlus /> {{ $t('title.linkMoreRecords') }}</div>
<div class="flex items-center gap-1"><MdiPlus /> {{ $t('title.linkRecords') }}</div>
</NcButton>
</div>
</div>

3
packages/nc-gui/components/virtual-cell/components/ListItem.vue

@ -89,7 +89,8 @@ const displayValue = computed(() => {
<template>
<a-card
class="nc-list-item !border-1 group transition-all !rounded-xl relative !mb-2 !border-gray-200 hover:bg-gray-50"
tabindex="0"
class="nc-list-item !outline-brand-500 !border-1 group transition-all !rounded-xl relative !mb-2 !border-gray-200 hover:bg-gray-50"
:class="{
'!bg-white': isLoading,
'!border-1': isLinked && !isLoading,

56
packages/nc-gui/components/virtual-cell/components/ListItems.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { RelationTypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import InboxIcon from '~icons/nc-icons/inbox'
import {
ColumnInj,
@ -8,7 +8,6 @@ import {
SaveRowInj,
computed,
inject,
onKeyStroke,
ref,
useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow,
@ -27,7 +26,7 @@ const injectedColumn = inject(ColumnInj)
const { isSharedBase } = storeToRefs(useBase())
const filterQueryRef = ref()
const filterQueryRef = ref<HTMLInputElement>()
const { t } = useI18n()
@ -64,8 +63,6 @@ const isForm = inject(IsFormInj, ref(false))
const saveRow = inject(SaveRowInj, () => {})
const isFocused = ref(false)
const linkRow = async (row: Record<string, any>, id: number) => {
if (isNew.value) {
addLTARRef(row, injectedColumn?.value as ColumnType)
@ -171,8 +168,8 @@ watch(expandedFormDlg, () => {
}
})
onKeyStroke('Escape', () => {
vModel.value = false
watch(filterQueryRef, () => {
filterQueryRef.value?.focus()
})
const onClick = (refRow: any, id: string) => {
@ -219,6 +216,34 @@ const onCreatedRecord = (record: any) => {
message.success(msgVNode)
}
const linkedShortcuts = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
vModel.value = false
} else if (e.key === 'ArrowDown') {
e.preventDefault()
try {
e.target?.nextElementSibling?.focus()
} catch (e) {}
} else if (e.key === 'ArrowUp') {
e.preventDefault()
try {
e.target?.previousElementSibling?.focus()
} catch (e) {}
} else if (e.key !== 'Tab' && e.key !== 'Shift' && e.key !== 'Enter' && e.key !== ' ') {
try {
filterQueryRef.value?.focus()
} catch (e) {}
}
}
onMounted(() => {
window.addEventListener('keydown', linkedShortcuts)
})
onUnmounted(() => {
window.removeEventListener('keydown', linkedShortcuts)
})
</script>
<template>
@ -240,10 +265,7 @@ const onCreatedRecord = (record: any) => {
:header="$t('activity.addNewLink')"
/>
<div class="flex mt-2 mb-2 items-center gap-2">
<div
class="flex items-center border-1 p-1 rounded-md w-full border-gray-200"
:class="{ '!border-primary': childrenExcludedListPagination.query.length !== 0 || isFocused }"
>
<div class="flex items-center border-1 p-1 rounded-md w-full border-gray-200 !focus-within:border-primary">
<MdiMagnify class="w-5 h-5 ml-2 text-gray-500" />
<a-input
ref="filterQueryRef"
@ -252,9 +274,13 @@ const onCreatedRecord = (record: any) => {
class="w-full !rounded-md nc-excluded-search xs:min-h-8"
size="small"
:bordered="false"
@focus="isFocused = true"
@blur="isFocused = false"
@keydown.capture.stop
@keydown.capture.stop="
(e) => {
if (e.key === 'Escape') {
filterQueryRef?.blur()
}
}
"
@change="childrenExcludedListPagination.page = 1"
>
</a-input>
@ -325,6 +351,8 @@ const onCreatedRecord = (record: any) => {
expandedFormDlg = true
}
"
@keydown.space.prevent="() => onClick(refRow, id)"
@keydown.enter.prevent="() => onClick(refRow, id)"
@click="() => onClick(refRow, id)"
/>
</template>

9
packages/nc-gui/components/workspace/CollaboratorsList.vue

@ -152,6 +152,15 @@ onMounted(async () => {
</NcDropdown>
</div>
</div>
<div v-if="sortedCollaborators.length === 1" class="pt-12 pb-4 px-2 flex flex-col items-center gap-6 text-center">
<div class="text-2xl text-gray-800 font-bold">
{{ $t('placeholder.inviteYourTeam') }}
</div>
<div class="text-sm text-gray-700">
{{ $t('placeholder.inviteYourTeamLabel') }}
</div>
<img src="~assets/img/placeholder/invite-team.png" class="!w-[30rem] flex-none" />
</div>
</div>
</div>
</div>

18
packages/nc-gui/lang/ar.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/bn_IN.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/cs.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Stáhnout soubor",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Přidat nový token",
"createNewToken": "Create new token",
"accountSettings": "Nastavení účtu",
"resetPasswordMenu": "Změnit heslo",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Vytvoření osobních API tokenů pro použití při automatizaci nebo v externích aplikacích.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/da.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/de.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Zeile durch Scannen eines QR-Codes oder Barcodes finden",
"tokenManagement": "Token Management",
"addNewToken": "Neuen Token hinzufügen",
"createNewToken": "Create new token",
"accountSettings": "Kontoeinstellungen",
"resetPasswordMenu": "Passwort zurücksetzen",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Wert",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Personalisierte API Tokens zur Verwendung in Automatisierungen oder externen Apps erstellen.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Feld zum Gruppieren auswählen",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/en.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find record by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -424,7 +426,7 @@
"audience-entityId": "Audience/ Entity ID",
"redirectUrl": "Redirect URL",
"oidc": "OpenID Connect (OIDC)",
"saml": "Security Assertion Markup Language (SAML)",
"saml": "SAML",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"ssoSettings": "SSO Settings",
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "From scratch or import or connect to external database",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Looks like you haven’t generated any API tokens yet.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Fast track your projects by collaborating on them with your team!"
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power you automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

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

@ -332,6 +332,7 @@
"virtualRelation": "Relación virtual",
"linkMore": "Enlace más",
"linkMoreRecords": "Vincular más registros",
"linkRecords": "Link Records",
"downloadFile": "Descargar archivo",
"renameTable": "Renombrar tabla",
"renamingTable": "Renombrar tabla",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Gestión de Token",
"addNewToken": "Añadir nuevo token",
"createNewToken": "Create new token",
"accountSettings": "Configuración de la cuenta",
"resetPasswordMenu": "Restablecer contraseña",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Campo eliminado",
"incompleteConfiguration": "Configuración incompleta",
"selectField": "Seleccione un campo",
"selectFieldLabel": "Realice cambios en las propiedades de los campos seleccionando un campo de la lista"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "Un único registro de la tabla ",
"tooltip_desc2": " puede vincularse con un único registro de la tabla "
},
"noRecordsAreLinkedFromTable": "No hay registros enlazados desde la tabla",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No hay registros vinculados",
"noLinkedRecords": "No linked records",
"recordsLinked": "registros vinculados",
"acceptOnlyValid": "Sólo acepta",
"apiTokenCreate": "Cree tokens de API personales para utilizarlos en la automatización o en aplicaciones externas.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Seleccione el campo al grupo",
"thereAreNoRecordsInTable": "No hay registros en la tabla",
"createWebhookMsg1": "¡Empieza con web-hooks!",
"createWebhookMsg2": "Crear web-hooks para potenciar tus automatizaciones,",
"createWebhookMsg3": "Reciba notificaciones en cuanto haya cambios en sus datos",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "¿Realmente quieres eliminar lo siguiente?",
"areYouSureUWantToDeleteLabel": "¿Estás seguro de que quieres eliminar la etiqueta {deleteLabel} siguiente?",
"idColumnRequired": "El campo ID es obligatorio, puede renombrarlo más tarde si es necesario.",

18
packages/nc-gui/lang/eu.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokenak",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/fa.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "پیدا کردن ردیف با اسکن کیو آر کد یا بارکد",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/fi.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

210
packages/nc-gui/lang/fr.json

@ -1,45 +1,45 @@
{
"dashboards": {
"create_new_dashboard_project": "Create New Interface",
"create_new_dashboard_project": "Créer une nouvelle interface",
"connect_data_sources": "Connecter des sources de données",
"alert": "Alert",
"alert-message": "No databases have been connected. Connect database bases to build interfaces. Skip this step and add databases from the base home page later.",
"select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "Select Database Bases that you want to link to this Interface.",
"create_interface": "Create interface",
"project_name": "Base Name",
"connect": "Connect",
"alert": "Alerte",
"alert-message": "Aucune base de données n'a été connectée. Connectez les bases de base de données pour construire des interfaces. Ignorez cette étape et ajoutez des bases de données depuis la page d'accueil de base plus tard.",
"select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "Sélectionnez les bases de données que vous souhaitez lier à cette interface.",
"create_interface": "Créer une interface",
"project_name": "Nom de la base",
"connect": "Connecter",
"buttonActionTypes": {
"open_external_url": "Open external link",
"delete_record": "Delete record",
"update_record": "Update record",
"open_layout": "Open layout"
"open_external_url": "Ouvrir le lien externe",
"delete_record": "Supprimer la ligne",
"update_record": "Mettre à jour l'enregistrement",
"open_layout": "Ouvrir la mise en page"
},
"widgets": {
"static_text": "Texte",
"chart": "Graphique",
"table": "Tableau",
"image": "Image",
"map": "Map",
"map": "Carte",
"button": "Bouton",
"number": "Number",
"number": "Nombre",
"bar_chart": "Diagramme à barres",
"line_chart": "Graphique en courbe",
"area_chart": "Graphique en aires",
"pie_chart": "Diagramme circulaire",
"donut_chart": "Donut Chart",
"donut_chart": "Graphique en anneau",
"scatter_plot": "Nuage de points",
"bubble_chart": "Graphique à bulles",
"radar_chart": "Radar Chart",
"polar_area_chart": "Polar Area Chart",
"radial_bar_chart": "Radial Bar Chart",
"heatmap_chart": "Heatmap Chart",
"treemap_chart": "Treemap Chart",
"box_plot_chart": "Box Plot Chart",
"candlestick_chart": "Candlestick Chart"
"radar_chart": "Carte radar",
"polar_area_chart": "Carte des zones polaires",
"radial_bar_chart": "Diagramme à barres radiales",
"heatmap_chart": "Carte thermique",
"treemap_chart": "Diagramme d'arborescence",
"box_plot_chart": "Graphique Boîte à moustache",
"candlestick_chart": "Graphique en chandeliers"
}
},
"general": {
"quit": "Quit",
"quit": "Quitter",
"home": "Accueil",
"load": "Charger",
"open": "Ouvert",
@ -61,39 +61,39 @@
"changeIcon": "Changer l'icône",
"save": "Sauvegarder",
"available": "Disponible",
"abort": "Abort",
"abort": "Abandonner",
"saving": "Sauvegarde",
"cancel": "Annuler",
"null": "Null",
"escape": "Escape",
"hex": "Hex",
"null": "Nul",
"escape": "Échap",
"hex": "Hexadécimal",
"clear": "Effacer",
"slack": "Slack",
"comment": "Comment",
"comment": "Commentaire",
"microsoftTeams": "Microsoft Teams",
"discord": "Discord",
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"quote": "Citation",
"submit": "Soumettre",
"create": "Créer",
"createEntity": "Créer {entity}",
"creating": "Création",
"creatingEntity": "Création de {entity}",
"details": "Détails",
"skip": "Skip",
"skip": "Ignorer",
"code": "Code",
"duplicate": "Dupliquer",
"duplicating": "Duplicating",
"activate": "Activate",
"duplicating": "Dupliquer",
"activate": "Activer",
"action": "Action",
"insert": "Insérer",
"delete": "Supprimer",
"deleteEntity": "Delete {entity}",
"bulkInsert": "Bulk Insert",
"bulkDelete": "Bulk Delete",
"bulkUpdate": "Bulk Update",
"deleteEntity": "Supprimer {entity}",
"bulkInsert": "Insertion en lot",
"bulkDelete": "Supprimer par lot",
"bulkUpdate": "Mise à jour par lot",
"deleting": "Suppression",
"update": "Mettre à jour",
"rename": "Renommer",
@ -101,10 +101,10 @@
"reset": "Réinitialiser",
"install": "Installer",
"show": "Montrer",
"access": "Access",
"access": "Accès",
"visibility": "Visibilité",
"hide": "Cacher",
"deprecated": "Deprecated",
"deprecated": "Déprécié",
"showAll": "Tout afficher",
"hideAll": "Tout cacher",
"notFound": "Introuvable",
@ -136,14 +136,14 @@
"after": "Après",
"before": "Avant",
"search": "Rechercher",
"searchIn": "Search In",
"searchIn": "Chercher dans",
"notification": "Notification",
"reference": "Référence",
"function": "Fonction",
"confirm": "Confirmer",
"generate": "Générer",
"copy": "Copier",
"are": "are",
"are": "sont",
"misc": "Divers",
"lock": "Verrouiller",
"unlock": "Déverrouiller",
@ -157,12 +157,12 @@
"groupingField": "Champ de regroupement",
"insertAfter": "Insérer après",
"insertBefore": "Insérer avant",
"insertAbove": "Insert above",
"insertBelow": "Insert below",
"insertAbove": "Insérer au-dessus",
"insertBelow": "Insérer en-dessous",
"hideField": "Masquer le champ",
"sortAsc": "Trier par ordre croissant",
"sortDesc": "Trier par ordre décroissant",
"move": "Move",
"move": "Déplacer",
"geoDataField": "Champ de données géographiques (GeoData)",
"type": "Type",
"name": "Nom",
@ -184,15 +184,15 @@
"countDistinct": "Compter Distinct",
"sumDistinct": "Somme Distinct",
"avgDistinct": "Moyenne Distinct",
"join": "Join",
"join": "Rejoindre",
"options": "Options",
"primaryValue": "Valeur primaire",
"useSurveyMode": "Utiliser le mode Sondage",
"shift": "Maj",
"enter": "Entrée",
"seconds": "Secondes",
"paste": "Paste",
"restore": "Restore"
"paste": "Coller",
"restore": "Restaurer"
},
"objects": {
"workspace": "Espace de travail",
@ -233,8 +233,8 @@
"editor": "Éditeur",
"commenter": "Commentateur",
"viewer": "Lecture seule",
"noaccess": "No Access",
"superAdmin": "Super Admin",
"noaccess": "Accès interdit",
"superAdmin": "Super administrateur",
"orgLevelCreator": "Créateur au niveau de l'organisation",
"orgLevelViewer": "Visualiseur de niveau d'organisation"
},
@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Renommer la table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Gestion des jetons",
"addNewToken": "Ajouter un nouveau jeton",
"createNewToken": "Create new token",
"accountSettings": "Paramètres du compte",
"resetPasswordMenu": "Réinitialiser le mot de passe",
"tokens": "Jetons",
@ -422,15 +424,15 @@
"cancel": "Cancel",
"metadataUrl": "Metadata URL",
"audience-entityId": "Audience/ Entity ID",
"redirectUrl": "Redirect URL",
"redirectUrl": "URL de redirection",
"oidc": "OpenID Connect (OIDC)",
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"ssoSettings": "SSO Settings",
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"saml": "Langage de balisage d'assertion de sécurité (SAML)",
"newProvider": "Nouveau fournisseur",
"generalSettings": "Paramètres généraux",
"ssoSettings": "Paramètres SSO",
"heading1": "Titre 1",
"heading2": "Titre 2",
"heading3": "Titre 3",
"bold": "Gras",
"italic": "Italique",
"underline": "Souligner",
@ -443,66 +445,66 @@
"noToken": "Aucun jeton",
"tokenLimit": "Un seul jeton par utilisateur est autorisé",
"duplicateAttachment": "Un fichier avec le nom {filename} est déjà attaché",
"viewIdColon": "VIEW ID: {viewId}",
"toAddress": "To Address",
"subject": "Subject",
"body": "Body",
"commaSeparatedMobileNumber": "Comma separated Mobile #",
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
"duplicateRecord": "Duplicate record",
"binaryEncodingFormat": "Binary encoding format",
"syntax": "Syntax",
"viewIdColon": "ID DE VUE : {viewId}",
"toAddress": "Vers l'adresse",
"subject": "Objet",
"body": "Corps",
"commaSeparatedMobileNumber": "Numéro de portable séparé par des virgules",
"headerName": "Nom de l'en-tête",
"icon": "Icône",
"max": "Maximum",
"enableRichText": "Activer le texte enrichi",
"idColon": "Identifiant:",
"copiedRecordURL": "URL de l'enregistrement copié",
"copyRecordURL": "Copier l'URL de l'enregistrement",
"duplicateRecord": "Dupliquer la ligne",
"binaryEncodingFormat": "Format d'encodage binaire",
"syntax": "Syntaxe",
"examples": "Exemples",
"durationInfo": "A duration of time in minutes or seconds (e.g. 1:23).",
"addHeader": "Add Header",
"durationInfo": "Une durée en minutes ou en secondes (par exemple 1:23).",
"addHeader": "Ajouter un en-tête",
"enterDefaultUrlOptional": "Entrez l'URL par défaut (facultatif)",
"negative": "Negative",
"negative": "Négatif",
"discard": "Abandonner",
"default": "Default",
"defaultNumberPercent": "Default Number (%)",
"durationFormat": "Duration Format",
"default": "Par défaut",
"defaultNumberPercent": "Nombre par défaut (%)",
"durationFormat": "Format de durée",
"dateFormat": "Format de date",
"timeFormat": "Format d'heure",
"singularLabel": "Libellé au singulier",
"pluralLabel": "Libellé au pluriel",
"optional": "(Facultatif)",
"clickToMake": "Click to make",
"visibleForRole": "visible for role:",
"inUI": "in UI Dashboard",
"projectSettings": "Base Settings",
"clickToMake": "Cliquez pour faire",
"visibleForRole": "visible pour le rôle :",
"inUI": "accéder au tableau de bord",
"projectSettings": "Réglages de base",
"clickToHide": "Cliquez pour masquer",
"clickToDownload": "Cliquez pour télécharger",
"forRole": "for role",
"clickToCopyViewID": "Click to copy View ID",
"forRole": "pour le rôle",
"clickToCopyViewID": "Cliquer pour copier l'ID de la vue",
"viewMode": "Mode d'affichage",
"searchUsers": "Rechercher des utilisateurs",
"superAdmin": "Super Admin",
"superAdmin": "Super administrateur",
"allTables": "Toutes les tables",
"members": "Membres",
"dataSources": "Sources de données",
"connectDataSource": "Connecter une source de données",
"searchProjects": "Search Bases",
"searchProjects": "Base de recherche",
"createdBy": "Créé par",
"viewingAttachmentsOf": "Visualisation des pièces jointes de",
"readOnly": "Lecture seule",
"dropHere": "Déposer ici",
"createdOn": "Created On",
"createdOn": "Créé le",
"notifyVia": "Notifier via",
"projName": "Nom du projet",
"profile": "Profil",
"accountDetails": "Account Details",
"controlAppearance": "Control your Appearance.",
"accountEmailID": "Account Email ID",
"accountDetails": "Détails du Compte",
"controlAppearance": "Contrôlez votre apparence.",
"accountEmailID": "E-mail du compte",
"backToWorkspace": "Retour à l'espace de travail",
"untitledToken": "Jeton sans titre",
"tableName": "Nom du tableau",
"dashboardName": "Dashboard name",
"dashboardName": "Nom du tableau de bord",
"createView": "Créer une vue",
"creatingView": "Création de la vue",
"duplicateView": "Dupliquer la vue",
@ -524,7 +526,7 @@
"databaseType": "Écrire dans la base de données",
"lengthValue": "Longueur / valeur",
"dbType": "Type de base de données",
"servername": "servername / hostAddr",
"servername": "nom du serveur / adresse de l'hôte",
"sqliteFile": "Fichier SQLite",
"hostAddress": "Adresse de l'hôte",
"port": "Numéro de port",
@ -597,9 +599,9 @@
"childTable": "Table enfant",
"childColumn": "Colonne enfant",
"childField": "Colonne enfant",
"joinCloudForFree": "Join Cloud for Free",
"joinCloudForFree": "Rejoignez gratuitement le Cloud",
"linkToAnotherRecord": "Lien vers un autre enregistrement",
"links": "Links",
"links": "Liens",
"onUpdate": "Mise à jour en cours",
"onDelete": "Suppression en cours",
"account": "Compte",
@ -610,7 +612,7 @@
"requestDataSource": "Demandez une source de données dont vous avez besoin ?",
"apiKey": "Clé d'API",
"personalAccessToken": "Jeton d'accès personnel",
"sharedBaseUrl": "Shared Base URL",
"sharedBaseUrl": "URL de la base partagée",
"importData": "Importer des données",
"importSecondaryViews": "Importer des vues secondaires",
"importRollupColumns": "Importer des colonnes de rollup",
@ -643,9 +645,9 @@
"nextRow": "Rang suivant",
"prevRow": "Rang précédent",
"addRowGrid": "Ajouter manuellement des données dans la vue Grille",
"addRowForm": "Enter record data through a form",
"noAccess": "No access",
"restApis": "Rest APIs",
"addRowForm": "Entrez les données de l'enregistrement via un formulaire",
"noAccess": "Accès interdit",
"restApis": "API REST",
"apis": "APIs",
"includeData": "Include Data",
"includeView": "Include View",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1,0000000",
"decimal8": "1,00000000",
"value": "Valeur",
"key": "Clé"
"key": "Clé",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Cliquer pour copier l'ID du champ",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "Il n'y a aucun enregistrement dans la table",
"createWebhookMsg1": "Commencez à utiliser les web-hooks !",
"createWebhookMsg2": "Créez des Web-hooks pour alimenter vos automatisations,",
"createWebhookMsg3": "Soyez informé dès que des modifications sont apportées à vos données",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Êtes-vous sûr de vouloir supprimer les éléments suivants",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/he.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/hi.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/hr.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/id.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/it.json

@ -332,6 +332,7 @@
"virtualRelation": "Relazione Virtuale",
"linkMore": "Collega Altro",
"linkMoreRecords": "Collega più righe",
"linkRecords": "Link Records",
"downloadFile": "Scarica File",
"renameTable": "Rinomina Tabella",
"renamingTable": "Rinominamento tabella",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Gestione Token",
"addNewToken": "Aggiungi un nuovo token",
"createNewToken": "Create new token",
"accountSettings": "Impostazioni account",
"resetPasswordMenu": "Reimposta Password",
"tokens": "Token",
@ -665,7 +667,7 @@
"deletedField": "Cancella campo",
"incompleteConfiguration": "Configurazione incompleta",
"selectField": "Seleziona un campo",
"selectFieldLabel": "Modifichi le proprietà del campo selezionando un campo dall'elenco"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Valore",
"key": "Chiave"
"key": "Chiave",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Fare clic per copiare l'Id del campo",
@ -1044,8 +1052,9 @@
"tooltip_desc": "Una singola riga dalla tabella ",
"tooltip_desc2": " può essere collegato con una riga della tabella "
},
"noRecordsAreLinkedFromTable": "Nessuna riga è collegata dalla tabella",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "Nessun record collegato",
"noLinkedRecords": "No linked records",
"recordsLinked": "record collegati",
"acceptOnlyValid": "Accetta solo",
"apiTokenCreate": "Creare token API personali da utilizzare nell'automazione o in applicazioni esterne.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Selezionare il campo da raggruppare",
"thereAreNoRecordsInTable": "Non ci sono record nella tabella",
"createWebhookMsg1": "Inizi con i web-hook!",
"createWebhookMsg2": "Crei dei web-hook per alimentare le sue automazioni,",
"createWebhookMsg3": "Ricevi una notifica non appena ci sono modifiche nei tuoi dati",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Sei sicuro di voler eliminare quanto segue",
"areYouSureUWantToDeleteLabel": "Sei sicuro di voler {deleteLabel} quanto segue",
"idColumnRequired": "Il campo ID è obbligatorio, può rinominarlo in seguito, se necessario.",

18
packages/nc-gui/lang/ja.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

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

@ -332,6 +332,7 @@
"virtualRelation": "가상 관계",
"linkMore": "링크 더 보기",
"linkMoreRecords": "더 많은 레코드 연결",
"linkRecords": "Link Records",
"downloadFile": "파일 다운로드",
"renameTable": "테이블 이름 변경",
"renamingTable": "테이블 이름 바꾸기",
@ -398,6 +399,7 @@
"findRowByScanningCode": "QR 또는 바코드를 스캔하여 행 찾기",
"tokenManagement": "토큰 관리",
"addNewToken": "새 토큰 추가",
"createNewToken": "Create new token",
"accountSettings": "계정 설정",
"resetPasswordMenu": "비밀번호 재설정",
"tokens": "토큰",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "값",
"key": "키"
"key": "키",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "클릭하여 필드 ID 복사",
@ -1044,8 +1052,9 @@
"tooltip_desc": "테이블의 단일 레코드 ",
"tooltip_desc2": " 테이블의 단일 레코드와 연결될 수 있습니다."
},
"noRecordsAreLinkedFromTable": "테이블에 연결된 레코드가 없습니다.",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "레코드가 연결되지 않았습니다.",
"noLinkedRecords": "No linked records",
"recordsLinked": "레코드가 연결되었습니다.",
"acceptOnlyValid": "유효한 값만 허용",
"apiTokenCreate": "새 API 토큰 생성",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "테이블에 레코드가 없습니다.",
"createWebhookMsg1": "웹훅을 생성하여 자동화를 구동하고,",
"createWebhookMsg2": "자동화를 지원하는 웹 후크를 만들고,",
"createWebhookMsg3": "데이터에 변경 사항이 있는 경우 즉시 알림 받기",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "삭제하시겠습니까?",
"areYouSureUWantToDeleteLabel": "{deleteLabel}을 삭제하시겠습니까?",
"idColumnRequired": "ID 필드가 필요합니다. 필요한 경우 나중에 이름을 변경할 수 있습니다.",

18
packages/nc-gui/lang/lv.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/nl.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/no.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/pl.json

@ -332,6 +332,7 @@
"virtualRelation": "Relacja wirtualna",
"linkMore": "Połącz więcej",
"linkMoreRecords": "Połącz więcej rekordów",
"linkRecords": "Link Records",
"downloadFile": "Pobierz plik",
"renameTable": "Zmień nazwę tabeli",
"renamingTable": "Zmiana nazwy tabeli",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Znajdź wiersz poprzez skanowanie kodu",
"tokenManagement": "Zarządzanie tokenami",
"addNewToken": "Dodaj nowy token",
"createNewToken": "Create new token",
"accountSettings": "Ustawienia konta",
"resetPasswordMenu": "Menu resetowania hasła",
"tokens": "Tokeny",
@ -665,7 +667,7 @@
"deletedField": "Usunięte pole",
"incompleteConfiguration": "Niekompletna konfiguracja",
"selectField": "Wybierz pole",
"selectFieldLabel": "Dokonaj zmian we właściwościach pola wybierając pole z listy"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Wartość",
"key": "Klucz"
"key": "Klucz",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Kliknij, aby skopiować Identyfikator Pola",
@ -1044,8 +1052,9 @@
"tooltip_desc": "Pojedynczy rekord z tabeli ",
"tooltip_desc2": " może być powiązany z pojedynczym rekordem z tabeli "
},
"noRecordsAreLinkedFromTable": "Żadne rekordy nie są powiązane z tabeli",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "Żadne rekordy nie są powiązane",
"noLinkedRecords": "No linked records",
"recordsLinked": "rekordy powiązane",
"acceptOnlyValid": "Akceptuje tylko",
"apiTokenCreate": "Utwórz osobiste tokeny API do użytku w automatyzacji lub zewnętrznych aplikacjach.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Wybierz pole do grupowania",
"thereAreNoRecordsInTable": "Nie ma rekordów w tabeli",
"createWebhookMsg1": "Rozpocznij pracę z webhookami!",
"createWebhookMsg2": "Utwórz webhoooki do zasilania twoich automatyzacji,",
"createWebhookMsg3": "Bądź informowany jak tylko pojawią się zmiany w twoich danych",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Czy na pewno chcesz usunąć następujące",
"areYouSureUWantToDeleteLabel": "Czy na pewno chcesz {deleteLabel} następujące",
"idColumnRequired": "Pole ID jest wymagane, możesz to później zmienić, jeśli będzie to konieczne.",

18
packages/nc-gui/lang/pt.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

20
packages/nc-gui/lang/pt_BR.json

@ -69,7 +69,7 @@
"hex": "Hex",
"clear": "Clear",
"slack": "Slack",
"comment": "Comment",
"comment": "Comentário",
"microsoftTeams": "Microsoft Teams",
"discord": "Discord",
"matterMost": "Mattermost",
@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

38
packages/nc-gui/lang/ru.json

@ -332,6 +332,7 @@
"virtualRelation": "Виртуальные отношения",
"linkMore": "Ссылка Подробнее",
"linkMoreRecords": "Связать больше записей",
"linkRecords": "Link Records",
"downloadFile": "Скачать файл",
"renameTable": "Переименовать таблицу",
"renamingTable": "Переименование таблицы",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Найти строку путем сканирования QR или штрих-кода",
"tokenManagement": "Управление токенами",
"addNewToken": "Добавить новый токен",
"createNewToken": "Create new token",
"accountSettings": "Настройки аккаунта",
"resetPasswordMenu": "Сбросить пароль",
"tokens": "Токены",
@ -659,13 +661,13 @@
"newFormLoaded": "Новая форма будет загружена после",
"webhook": "Вебхук",
"multiField": {
"newField": "New field",
"saveChanges": "Save changes",
"updatedField": "Updated field",
"deletedField": "Deleted field",
"newField": "Новое поле",
"saveChanges": "Сохранить изменения",
"updatedField": "Обновить поле",
"deletedField": "Удалить поле",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectField": "Выбрать поле",
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -917,10 +919,10 @@
"clientCA": "Выберите файл CA"
},
"placeholder": {
"selectSlackChannels": "Select Slack channels",
"selectTeamsChannels": "Select Microsoft Teams channels",
"selectDiscordChannels": "Select Discord channels",
"selectMattermostChannels": "Select Mattermost channels",
"selectSlackChannels": "Выберите каналы Slack",
"selectTeamsChannels": "Выберите каналы Microsoft Teams",
"selectDiscordChannels": "Выберите каналы Discord",
"selectMattermostChannels": "Выберите каналы Mattermost",
"webhookTitle": "Webhook Title",
"barcodeColumn": "Select a field for the Barcode value",
"notFoundContent": "No valid field Type can be found.",
@ -938,7 +940,7 @@
"confirm": "Подтвердите новый пароль"
},
"selectAColumnForTheQRCodeValue": "Select a field for the QR code value",
"allowNegativeNumbers": "Allow negative numbers",
"allowNegativeNumbers": "Разрешить отрицательные числа",
"searchProjectTree": "Искать таблицы",
"searchFields": "Поиск полей",
"searchColumn": "Поиск {поиск} столбец",
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Значение",
"key": "Ключ"
"key": "Ключ",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/sk.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/sl.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

62
packages/nc-gui/lang/sv.json

@ -52,13 +52,13 @@
"or": "Eller",
"add": "Lägg till",
"edit": "Redigera",
"link": "Link",
"links": "Links",
"link": "Länk",
"links": "Länkar",
"remove": "Avlägsna",
"import": "Import",
"logout": "Log Out",
"logout": "Logga ut",
"empty": "Empty",
"changeIcon": "Change Icon",
"changeIcon": "Ändra ikon",
"save": "Spara",
"available": "Available",
"abort": "Abort",
@ -67,7 +67,7 @@
"null": "Null",
"escape": "Escape",
"hex": "Hex",
"clear": "Clear",
"clear": "Rensa",
"slack": "Slack",
"comment": "Comment",
"microsoftTeams": "Microsoft Teams",
@ -83,7 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"code": "Kod",
"duplicate": "Dubbla",
"duplicating": "Duplicating",
"activate": "Activate",
@ -165,7 +165,7 @@
"move": "Move",
"geoDataField": "GeoData Field",
"type": "Type",
"name": "Name",
"name": "Namn",
"changes": "Changes",
"new": "New",
"old": "Old",
@ -301,7 +301,7 @@
"isNotNull": "är inte noll"
},
"title": {
"sso": "Authentication (SSO)",
"sso": "Autentisering (SSO)",
"docs": "Docs",
"forum": "Forum",
"parameter": "Parameter",
@ -316,8 +316,8 @@
"inDesktop": "in Desktop",
"rowData": "Record data",
"creator": "Creator",
"qrCode": "QR Code",
"termsOfService": "Terms of Service",
"qrCode": "QR-kod",
"termsOfService": "Användarvillkor",
"updateSelectedRows": "Update Selected Records",
"noFiltersAdded": "No filters added",
"editCards": "Edit Cards",
@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -381,7 +382,7 @@
"generateToken": "Generera en token",
"APIsAndSupport": "API:er och stöd",
"helpCenter": "Hjälpcenter",
"noLabels": "No Labels",
"noLabels": "Inga etiketter",
"swaggerDocumentation": "Dokumentation om Swagger",
"quickImportFrom": "Snabb import från",
"quickImport": "Snabb import",
@ -398,12 +399,13 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"createNewToken": "Create new token",
"accountSettings": "Kontoinställningar",
"resetPasswordMenu": "Återställ lösenord",
"tokens": "Tokens",
"userManagement": "User Management",
"accountManagement": "Account management",
"licence": "Licence",
"licence": "Licens",
"allowAllMimeTypes": "Allow All Mime Types",
"defaultView": "Default View",
"relations": "Relations",
@ -418,8 +420,8 @@
}
},
"labels": {
"save": "Save",
"cancel": "Cancel",
"save": "Spara",
"cancel": "Avbryt",
"metadataUrl": "Metadata URL",
"audience-entityId": "Audience/ Entity ID",
"redirectUrl": "Redirect URL",
@ -445,11 +447,11 @@
"duplicateAttachment": "File with name {filename} already attached",
"viewIdColon": "VIEW ID: {viewId}",
"toAddress": "To Address",
"subject": "Subject",
"subject": "Ämne",
"body": "Body",
"commaSeparatedMobileNumber": "Comma separated Mobile #",
"headerName": "Header Name",
"icon": "Icon",
"icon": "Ikon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
@ -495,7 +497,7 @@
"createdOn": "Created On",
"notifyVia": "Meddela via",
"projName": "Projektnamn",
"profile": "Profile",
"profile": "Profil",
"accountDetails": "Account Details",
"controlAppearance": "Control your Appearance.",
"accountEmailID": "Account Email ID",
@ -660,12 +662,12 @@
"webhook": "Webhook",
"multiField": {
"newField": "New field",
"saveChanges": "Save changes",
"saveChanges": "Spara ändringar",
"updatedField": "Updated field",
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,11 +961,17 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"enterPassword": "Ange lösenord",
"bySigningUp": "By signing up, you agree to the",
"subscribeToOurWeeklyNewsletter": "Subscribe to our weekly newsletter",
"verifyingPassword": "Verifying Password",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",
@ -1244,7 +1252,7 @@
"makeLineBreak": "to make a line break",
"goToPrevious": "Go to previous",
"goToNext": "Go to next",
"thankYou": "Thank you!",
"thankYou": "Tack!",
"submittedFormData": "You have successfully submitted the form data.",
"editingSystemKeyNotSupported": "Editing system key not supported",
"notAvailableAtTheMoment": "Not available at the moment"

18
packages/nc-gui/lang/th.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "หาแถวโดยสแกน QR โคด",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

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

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "QR veya Barkod okutarak satır bulun",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/uk.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Знайти рядок, відсканувавши QR-код або штрих-код",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

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

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Thêm mới token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

18
packages/nc-gui/lang/zh-Hans.json

@ -332,6 +332,7 @@
"virtualRelation": "虚拟关系",
"linkMore": "链接更多",
"linkMoreRecords": "链接更多记录",
"linkRecords": "Link Records",
"downloadFile": "下载文件",
"renameTable": "重命名表格",
"renamingTable": "重新命名表格",
@ -398,6 +399,7 @@
"findRowByScanningCode": "通过扫描二维码或条码查找行",
"tokenManagement": "Token 管理",
"addNewToken": "添加新令牌(Token)",
"createNewToken": "Create new token",
"accountSettings": "账户设置",
"resetPasswordMenu": "密码重置",
"tokens": "令牌",
@ -665,7 +667,7 @@
"deletedField": "已删除的字段",
"incompleteConfiguration": "配置不完整",
"selectField": "请设置需要查询的字段",
"selectFieldLabel": "从列表中选择一个字段来更改字段属性"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "值",
"key": "键"
"key": "键",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "点击复制字段ID",
@ -1044,8 +1052,9 @@
"tooltip_desc": "表中的一条记录 ",
"tooltip_desc2": " 可以从表中链接到单条记录 "
},
"noRecordsAreLinkedFromTable": "表中没有链接记录",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "没有链接记录",
"noLinkedRecords": "No linked records",
"recordsLinked": "链接的记录",
"acceptOnlyValid": "仅接受",
"apiTokenCreate": "创建个人 API tokens,以便在自动化或外部应用程序中使用。",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "选择要分组的字段",
"thereAreNoRecordsInTable": "表中没有记录",
"createWebhookMsg1": "开始使用Web Hooks!",
"createWebhookMsg2": "创建Web Hooks来实现自动化",
"createWebhookMsg3": "一旦您的数据发生变化,您将立即收到通知",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "您确定要删除以下内容",
"areYouSureUWantToDeleteLabel": "您确定要 {deleteLabel} 以下内容吗?",
"idColumnRequired": "ID 字段是必填字段,如果需要,您可以稍后重新命名。",

18
packages/nc-gui/lang/zh-Hant.json

@ -332,6 +332,7 @@
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"renameTable": "Rename Table",
"renamingTable": "Renaming Table",
@ -398,6 +399,7 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Token Management",
"addNewToken": "Add new token",
"createNewToken": "Create new token",
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
@ -665,7 +667,7 @@
"deletedField": "Deleted field",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
}
},
"activity": {
@ -959,7 +961,13 @@
"decimal7": "1.0000000",
"decimal8": "1.00000000",
"value": "Value",
"key": "Key"
"key": "Key",
"createTable": "Create your First Table!",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace."
},
"msg": {
"clickToCopyFieldId": "Click to copy Field Id",
@ -1044,8 +1052,9 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"noRecordsAreLinkedFromTable": "No records are linked from table",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
@ -1053,8 +1062,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

6
packages/nc-gui/package.json

@ -106,7 +106,7 @@
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@iconify-json/ant-design": "^1.1.15",
"@iconify-json/bi": "^1.1.23",
"@iconify-json/carbon": "^1.1.29",
"@iconify-json/carbon": "^1.1.30",
"@iconify-json/cil": "^1.1.8",
"@iconify-json/clarity": "^1.1.12",
"@iconify-json/eva": "^1.1.10",
@ -114,13 +114,13 @@
"@iconify-json/ion": "^1.1.15",
"@iconify-json/la": "^1.1.8",
"@iconify-json/logos": "^1.1.42",
"@iconify-json/lucide": "^1.1.163",
"@iconify-json/lucide": "^1.1.165",
"@iconify-json/material-symbols": "^1.1.72",
"@iconify-json/mdi": "^1.1.64",
"@iconify-json/mi": "^1.1.8",
"@iconify-json/ph": "^1.1.11",
"@iconify-json/ri": "^1.1.19",
"@iconify-json/simple-icons": "^1.1.90",
"@iconify-json/simple-icons": "^1.1.91",
"@iconify-json/system-uicons": "^1.1.12",
"@iconify-json/tabler": "^1.1.105",
"@iconify-json/vscode-icons": "^1.1.33",

8
packages/noco-docs/package-lock.json generated

@ -14,7 +14,7 @@
"@docusaurus/plugin-ideal-image": "3.0.1",
"@docusaurus/plugin-sitemap": "3.0.1",
"@docusaurus/preset-classic": "3.0.1",
"@mdx-js/react": "^3.0.0",
"@mdx-js/react": "^3.0.1",
"clsx": "^1.2.1",
"docusaurus-plugin-sass": "^0.2.5",
"docusaurus-theme-search-typesense": "^0.14.1",
@ -3031,9 +3031,9 @@
}
},
"node_modules/@mdx-js/react": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.0.tgz",
"integrity": "sha512-nDctevR9KyYFyV+m+/+S4cpzCWHqj+iHDHq3QrsWezcC+B17uZdIWgCguESUkwFhM3n/56KxWVE3V6EokrmONQ==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.1.tgz",
"integrity": "sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==",
"dependencies": {
"@types/mdx": "^2.0.0"
},

2
packages/noco-docs/package.json

@ -33,7 +33,7 @@
"@docusaurus/plugin-ideal-image": "3.0.1",
"@docusaurus/plugin-sitemap": "3.0.1",
"@docusaurus/preset-classic": "3.0.1",
"@mdx-js/react": "^3.0.0",
"@mdx-js/react": "^3.0.1",
"clsx": "^1.2.1",
"docusaurus-plugin-sass": "^0.2.5",
"docusaurus-theme-search-typesense": "^0.14.1",

626
packages/nocodb/src/cache/CacheMgr.ts vendored

@ -1,36 +1,620 @@
import debug from 'debug';
import { Logger } from '@nestjs/common';
import type IORedis from 'ioredis';
import { CacheDelDirection, CacheGetType } from '~/utils/globals';
const log = debug('nc:cache');
const logger = new Logger('CacheMgr');
/*
- keys are stored as following:
- simple key: nc:<orgs>:<scope>:<model_id_1>
- value: { value: { ... }, parentKeys: [ "nc:<orgs>:<scope>:<model_id_1>:list" ], timestamp: 1234567890 }
- stored as stringified JSON
- list key: nc:<orgs>:<scope>:<model_id_1>:list
- stored as SET
- get returns `value` only
- getRaw returns the whole cache object with metadata
*/
const NC_REDIS_TTL = +process.env.NC_REDIS_TTL || 60 * 60 * 24 * 3; // 3 days
const NC_REDIS_GRACE_TTL = +process.env.NC_REDIS_GRACE_TTL || 60 * 60 * 24 * 1; // 1 day
export default abstract class CacheMgr {
public abstract get(key: string, type: string): Promise<any>;
public abstract set(key: string, value: any): Promise<any>;
public abstract setExpiring(
client: IORedis;
prefix: string;
context: string;
// avoid circular structure to JSON
getCircularReplacer = () => {
const seen = new WeakSet();
return (_, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
// @ts-ignore
async del(key: string[] | string): Promise<any> {
log(`${this.context}::del: deleting key ${key}`);
if (Array.isArray(key)) {
if (key.length) {
return this.client.del(key);
}
} else if (key) {
return this.client.del(key);
}
}
// @ts-ignore
private async getRaw(
key: string,
type?: string,
skipTTL = false,
): Promise<any> {
log(`${this.context}::getRaw: getting key ${key} with type ${type}`);
if (type === CacheGetType.TYPE_ARRAY) {
return this.client.smembers(key);
} else {
const res = await this.client.get(key);
if (res) {
try {
const o = JSON.parse(res);
if (typeof o === 'object') {
if (
o &&
Object.keys(o).length === 0 &&
Object.getPrototypeOf(o) === Object.prototype
) {
log(`${this.context}::get: object is empty!`);
}
if (!skipTTL && o.timestamp) {
const diff = Date.now() - o.timestamp;
if (diff > NC_REDIS_GRACE_TTL * 1000) {
await this.refreshTTL(key);
}
}
return Promise.resolve(o);
}
} catch (e) {
logger.error(`Bad value stored for key ${key} : ${res}`);
return Promise.resolve(res);
}
}
return Promise.resolve(res);
}
}
// @ts-ignore
async get(key: string, type: string): Promise<any> {
return this.getRaw(key, type).then((res) => {
if (res && res.value) {
return res.value;
}
return res;
});
}
// @ts-ignore
async set(
key: string,
value: any,
options: {
// when we prepare beforehand, we don't need to prepare again
skipPrepare?: boolean;
// timestamp for the value, if not provided, it will be set to current time
timestamp?: number;
} = {
skipPrepare: false,
},
): Promise<any> {
const { skipPrepare, timestamp } = options;
if (typeof value !== 'undefined' && value) {
log(`${this.context}::set: setting key ${key} with value ${value}`);
// if provided value is an array store it as a set
if (Array.isArray(value) && value.length) {
return new Promise((resolve) => {
this.client
.pipeline()
.sadd(key, value)
// - 60 seconds to avoid expiring list before any of its children
.expire(key, NC_REDIS_TTL - 60)
.exec((err) => {
if (err) {
logger.error(
`${this.context}::set: error setting key ${key} with value ${value}`,
);
}
resolve(true);
});
});
}
if (!skipPrepare) {
// try to get old key value
const keyValue = await this.getRaw(key);
// prepare new key value
value = this.prepareValue({
value,
parentKeys: this.getParents(keyValue),
timestamp,
});
}
return this.client
.set(
key,
JSON.stringify(value, this.getCircularReplacer()),
'EX',
NC_REDIS_TTL,
)
.then(async () => {
await this.refreshTTL(key, timestamp);
return true;
});
} else {
log(`${this.context}::set: value is empty for ${key}. Skipping ...`);
return Promise.resolve(true);
}
}
// @ts-ignore
async setExpiring(
key: string,
value: any,
seconds: number,
): Promise<any>;
public abstract incrby(key: string, value: number): Promise<any>;
public abstract del(key: string[] | string): Promise<any>;
public abstract getList(
options: {
// when we prepare beforehand, we don't need to prepare again
skipPrepare?: boolean;
// timestamp for the value, if not provided, it will be set to current time
timestamp?: number;
} = {
skipPrepare: false,
},
): Promise<any> {
const { skipPrepare, timestamp } = options;
if (typeof value !== 'undefined' && value) {
log(
`${this.context}::setExpiring: setting key ${key} with value ${value}`,
);
if (Array.isArray(value) && value.length) {
return new Promise((resolve) => {
this.client
.pipeline()
.sadd(key, value)
.expire(key, seconds)
.exec((err) => {
if (err) {
logger.error(
`${this.context}::set: error setting key ${key} with value ${value}`,
);
}
resolve(true);
});
});
}
if (!skipPrepare) {
// try to get old key value
const keyValue = await this.getRaw(key);
// prepare new key value
value = this.prepareValue({
value,
parentKeys: this.getParents(keyValue),
timestamp,
});
}
return this.client.set(
key,
JSON.stringify(value, this.getCircularReplacer()),
'EX',
seconds,
);
} else {
log(`${this.context}::set: value is empty for ${key}. Skipping ...`);
return Promise.resolve(true);
}
}
// @ts-ignore
async incrby(key: string, value = 1): Promise<any> {
return this.client.incrby(key, value);
}
async getList(
scope: string,
list: string[],
subKeys: string[],
): Promise<{
list: any[];
isNoneList: boolean;
}>;
public abstract setList(
}> {
// remove null from arrays
subKeys = subKeys.filter((k) => k);
// e.g. key = nc:<orgs>:<scope>:<project_id_1>:<source_id_1>:list
const key =
subKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subKeys.join(':')}:list`;
// e.g. arr = ["nc:<orgs>:<scope>:<model_id_1>", "nc:<orgs>:<scope>:<model_id_2>"]
const arr = (await this.get(key, CacheGetType.TYPE_ARRAY)) || [];
log(`${this.context}::getList: getting list with key ${key}`);
const isNoneList = arr.length && arr.includes('NONE');
if (isNoneList || !arr.length) {
return Promise.resolve({
list: [],
isNoneList,
});
}
log(`${this.context}::getList: getting list with keys ${arr}`);
const values = await this.client.mget(arr);
if (values.some((v) => v === null)) {
// FALLBACK: a key is missing from list, this should never happen
logger.error(`${this.context}::getList: missing value for ${key}`);
const allParents = [];
// get all parents from children
values.forEach((v) => {
if (v) {
try {
const o = JSON.parse(v);
if (typeof o === 'object') {
allParents.push(...this.getParents(o));
}
} catch (e) {
logger.error(
`${this.context}::getList: Bad value stored for key ${arr[0]} : ${v}`,
);
}
}
});
// remove duplicates
const uniqueParents = [...new Set(allParents)];
// delete all parents and children
await Promise.all(
uniqueParents.map(async (p) => {
await this.deepDel(p, CacheDelDirection.PARENT_TO_CHILD);
}),
);
return Promise.resolve({
list: [],
isNoneList,
});
}
if (values.length) {
try {
const o = JSON.parse(values[0]);
if (typeof o === 'object') {
const diff = Date.now() - o.timestamp;
if (diff > NC_REDIS_GRACE_TTL * 1000) {
await this.refreshTTL(key);
}
}
} catch (e) {
logger.error(
`${this.context}::getList: Bad value stored for key ${arr[0]} : ${values[0]}`,
);
}
}
return {
list: values.map((res) => {
try {
const o = JSON.parse(res);
if (typeof o === 'object') {
return o.value;
}
} catch (e) {
return res;
}
return res;
}),
isNoneList,
};
}
async setList(
scope: string,
subListKeys: string[],
list: any[],
props?: string[],
): Promise<boolean>;
public abstract deepDel(
scope: string,
key: string,
direction: string,
): Promise<boolean>;
public abstract appendToList(
props: string[] = [],
): Promise<boolean> {
// remove null from arrays
subListKeys = subListKeys.filter((k) => k);
// construct key for List
// e.g. nc:<orgs>:<scope>:<project_id_1>:<source_id_1>:list
const listKey =
subListKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
if (!list.length) {
// Set NONE here so that it won't hit the DB on each page load
return this.set(listKey, ['NONE']);
}
// timestamp for list
const timestamp = Date.now();
// remove existing list
await this.deepDel(listKey, CacheDelDirection.PARENT_TO_CHILD);
const listOfGetKeys = [];
for (const o of list) {
// construct key for Get
let getKey = `${this.prefix}:${scope}:${o.id}`;
if (props.length) {
const propValues = props.map((p) => o[p]);
// e.g. nc:<orgs>:<scope>:<prop_value_1>:<prop_value_2>
getKey = `${this.prefix}:${scope}:${propValues.join(':')}`;
}
log(`${this.context}::setList: get key ${getKey}`);
// get key
let rawValue = await this.getRaw(getKey, CacheGetType.TYPE_OBJECT);
if (rawValue) {
log(`${this.context}::setList: preparing key ${getKey}`);
// prepare key
rawValue = this.prepareValue({
value: o,
parentKeys: this.getParents(rawValue),
newKey: listKey,
timestamp,
});
} else {
rawValue = this.prepareValue({
value: o,
parentKeys: [listKey],
timestamp,
});
}
// set key
log(`${this.context}::setList: setting key ${getKey}`);
await this.set(getKey, rawValue, {
skipPrepare: true,
timestamp,
});
// push key to list
listOfGetKeys.push(getKey);
}
// set list
log(`${this.context}::setList: setting list with key ${listKey}`);
return this.set(listKey, listOfGetKeys);
}
async deepDel(key: string, direction: string): Promise<boolean> {
log(`${this.context}::deepDel: choose direction ${direction}`);
if (direction === CacheDelDirection.CHILD_TO_PARENT) {
const childKey = await this.getRaw(key, CacheGetType.TYPE_OBJECT);
// given a child key, delete all keys in corresponding parent lists
const scopeList = this.getParents(childKey);
for (const listKey of scopeList) {
// get target list
let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
if (!list.length) {
continue;
}
// remove target Key
list = list.filter((k) => k !== key);
// delete list
log(`${this.context}::deepDel: remove listKey ${listKey}`);
await this.del(listKey);
if (list.length) {
// set target list
log(`${this.context}::deepDel: set key ${listKey}`);
await this.set(listKey, list);
}
}
log(`${this.context}::deepDel: remove key ${key}`);
return await this.del(key);
} else if (direction === CacheDelDirection.PARENT_TO_CHILD) {
key = /:list$/.test(key) ? key : `${key}:list`;
// given a list key, delete all the children
const listOfChildren = await this.get(key, CacheGetType.TYPE_ARRAY);
// delete each child key
await this.del(listOfChildren);
// delete list key
return await this.del(key);
} else {
log(`Invalid deepDel direction found : ${direction}`);
return Promise.resolve(false);
}
}
async appendToList(
scope: string,
subListKeys: string[],
key: string,
): Promise<boolean>;
public abstract destroy(): Promise<boolean>;
public abstract export(): Promise<any>;
): Promise<boolean> {
// remove null from arrays
subListKeys = subListKeys.filter((k) => k);
// e.g. key = nc:<orgs>:<scope>:<project_id_1>:<source_id_1>:list
const listKey =
subListKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
log(`${this.context}::appendToList: append key ${key} to ${listKey}`);
let list = await this.get(listKey, CacheGetType.TYPE_ARRAY);
if (!list || !list.length) {
return false;
}
if (list.includes('NONE')) {
list = [];
await this.del(listKey);
}
log(`${this.context}::appendToList: get key ${key}`);
// get Get Key
const rawValue = await this.getRaw(key, CacheGetType.TYPE_OBJECT);
log(`${this.context}::appendToList: preparing key ${key}`);
if (!rawValue) {
// FALLBACK: this is to get rid of all keys that would be effected by this (should never happen)
logger.error(`${this.context}::appendToList: value is empty for ${key}`);
const allParents = [];
// get all children
const listValues = await this.getList(scope, subListKeys);
// get all parents from children
listValues.list.forEach((v) => {
allParents.push(...this.getParents(v));
});
// remove duplicates
const uniqueParents = [...new Set(allParents)];
// delete all parents and children
await Promise.all(
uniqueParents.map(async (p) => {
await this.deepDel(p, CacheDelDirection.PARENT_TO_CHILD);
}),
);
return false;
}
// prepare Get Key
const preparedValue = this.prepareValue({
value: rawValue.value ?? rawValue,
parentKeys: this.getParents(rawValue),
newKey: listKey,
});
// set Get Key
log(`${this.context}::appendToList: setting key ${key}`);
await this.set(key, preparedValue, {
skipPrepare: true,
});
list.push(key);
return this.set(listKey, list).then(async (res) => {
await this.refreshTTL(listKey);
return res;
});
}
// wrap value with metadata
prepareValue(args: {
value: any;
parentKeys: string[];
newKey?: string;
timestamp?: number;
}) {
const { value, parentKeys, newKey, timestamp } = args;
if (newKey && !parentKeys.includes(newKey)) {
parentKeys.push(newKey);
}
const cacheObj = {
value,
parentKeys,
timestamp: timestamp || Date.now(),
};
return cacheObj;
}
getParents(rawValue) {
if (rawValue && rawValue.parentKeys) {
return rawValue.parentKeys;
} else if (!rawValue) {
return [];
} else {
logger.error(
`${this.context}::getParents: parentKeys not found ${JSON.stringify(
rawValue,
)}`,
);
return [];
}
}
async refreshTTL(key: string, timestamp?: number): Promise<void> {
log(`${this.context}::refreshTTL: refreshing TTL for ${key}`);
const isParent = /:list$/.test(key);
timestamp = timestamp || Date.now();
if (isParent) {
const list =
(await this.getRaw(key, CacheGetType.TYPE_ARRAY, true)) || [];
if (list && list.length) {
const listValues = await this.client.mget(list);
const pipeline = this.client.pipeline();
for (const [i, v] of listValues.entries()) {
const key = list[i];
if (v) {
try {
const o = JSON.parse(v);
if (typeof o === 'object') {
if (o.timestamp !== timestamp) {
o.timestamp = timestamp;
pipeline.set(
key,
JSON.stringify(o, this.getCircularReplacer()),
'EX',
NC_REDIS_TTL,
);
}
}
} catch (e) {
logger.error(
`${this.context}::refreshTTL: Bad value stored for key ${key} : ${v}`,
);
}
}
}
pipeline.expire(key, NC_REDIS_TTL - 60);
await pipeline.exec();
}
} else {
const rawValue = await this.getRaw(key, null, true);
if (rawValue) {
if (rawValue.parentKeys && rawValue.parentKeys.length) {
for (const parent of rawValue.parentKeys) {
await this.refreshTTL(parent, timestamp);
}
} else {
if (rawValue.timestamp !== timestamp) {
rawValue.timestamp = timestamp;
await this.client.set(
key,
JSON.stringify(rawValue, this.getCircularReplacer()),
'EX',
NC_REDIS_TTL,
);
}
}
}
}
}
async destroy(): Promise<boolean> {
log('${this.context}::destroy: destroy redis');
return this.client.flushdb().then((r) => r === 'OK');
}
async export(): Promise<any> {
log('${this.context}::export: export data');
const data = await this.client.keys('*');
const res = {};
return await Promise.all(
data.map(async (k) => {
res[k] = await this.get(
k,
k.slice(-4) === 'list'
? CacheGetType.TYPE_ARRAY
: CacheGetType.TYPE_OBJECT,
);
}),
).then(() => {
return res;
});
}
}

3
packages/nocodb/src/cache/NocoCache.ts vendored

@ -85,12 +85,11 @@ export default class NocoCache {
}
public static async deepDel(
scope: string,
key: string,
direction: string,
): Promise<boolean> {
if (this.cacheDisabled) return Promise.resolve(true);
return this.client.deepDel(scope, `${this.prefix}:${key}`, direction);
return this.client.deepDel(`${this.prefix}:${key}`, direction);
}
public static async appendToList(

414
packages/nocodb/src/cache/RedisCacheMgr.ts vendored

@ -1,18 +1,10 @@
import debug from 'debug';
import Redis from 'ioredis';
import CacheMgr from './CacheMgr';
import {
CacheDelDirection,
CacheGetType,
CacheListProp,
} from '~/utils/globals';
const log = debug('nc:cache');
const _log = debug('nc:cache');
export default class RedisCacheMgr extends CacheMgr {
client: Redis;
prefix: string;
constructor(config: any) {
super();
this.client = new Redis(config);
@ -20,7 +12,7 @@ export default class RedisCacheMgr extends CacheMgr {
// avoid flushing db in worker container
if (
process.env.NC_WORKER_CONTAINER !== 'true' &&
process.env.NC_CLOUD !== 'true'
(process.env.NC_FLUSH_CACHE === 'true' || process.env.NC_CLOUD !== 'true')
) {
// flush the existing db with selected key (Default: 0)
this.client.flushdb();
@ -29,406 +21,6 @@ export default class RedisCacheMgr extends CacheMgr {
// TODO(cache): fetch orgs once it's implemented
const orgs = 'noco';
this.prefix = `nc:${orgs}`;
}
// avoid circular structure to JSON
getCircularReplacer = () => {
const seen = new WeakSet();
return (_, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
// @ts-ignore
async del(key: string[] | string): Promise<any> {
log(`RedisCacheMgr::del: deleting key ${key}`);
if (Array.isArray(key)) {
if (key.length) {
return this.client.del(key);
}
} else if (key) {
return this.client.del(key);
}
}
// @ts-ignore
async get(key: string, type: string): Promise<any> {
log(`RedisCacheMgr::get: getting key ${key} with type ${type}`);
if (type === CacheGetType.TYPE_ARRAY) {
return this.client.smembers(key);
} else if (type === CacheGetType.TYPE_OBJECT) {
const res = await this.client.get(key);
try {
const o = JSON.parse(res);
if (typeof o === 'object') {
if (
o &&
Object.keys(o).length === 0 &&
Object.getPrototypeOf(o) === Object.prototype
) {
log(`RedisCacheMgr::get: object is empty!`);
}
return Promise.resolve(o);
}
} catch (e) {
return Promise.resolve(res);
}
return Promise.resolve(res);
} else if (type === CacheGetType.TYPE_STRING) {
return await this.client.get(key);
}
log(`Invalid CacheGetType: ${type}`);
return Promise.resolve(false);
}
// @ts-ignore
async set(key: string, value: any): Promise<any> {
if (typeof value !== 'undefined' && value) {
log(`RedisCacheMgr::set: setting key ${key} with value ${value}`);
if (typeof value === 'object') {
if (Array.isArray(value) && value.length) {
return this.client.sadd(key, value);
}
const keyValue = await this.get(key, CacheGetType.TYPE_OBJECT);
if (keyValue) {
value = await this.prepareValue(value, this.getParents(keyValue));
}
return this.client.set(
key,
JSON.stringify(value, this.getCircularReplacer()),
);
}
return this.client.set(key, value);
} else {
log(`RedisCacheMgr::set: value is empty for ${key}. Skipping ...`);
return Promise.resolve(true);
}
}
// @ts-ignore
async setExpiring(key: string, value: any, seconds: number): Promise<any> {
if (typeof value !== 'undefined' && value) {
log(
`RedisCacheMgr::setExpiring: setting key ${key} with value ${value} for ${seconds} seconds`,
);
if (typeof value === 'object') {
if (Array.isArray(value) && value.length) {
return this.client.sadd(key, value);
}
return this.client.set(
key,
JSON.stringify(value, this.getCircularReplacer()),
'EX',
seconds,
);
}
return this.client.set(key, value, 'EX', seconds);
} else {
log(`RedisCacheMgr::set: value is empty for ${key}. Skipping ...`);
return Promise.resolve(true);
}
}
// @ts-ignore
async incrby(key: string, value = 1): Promise<any> {
return this.client.incrby(key, value);
}
async getList(
scope: string,
subKeys: string[],
): Promise<{
list: any[];
isNoneList: boolean;
}> {
// remove null from arrays
subKeys = subKeys.filter((k) => k);
// e.g. key = nc:<orgs>:<scope>:<project_id_1>:<source_id_1>:list
const key =
subKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subKeys.join(':')}:list`;
// e.g. arr = ["nc:<orgs>:<scope>:<model_id_1>", "nc:<orgs>:<scope>:<model_id_2>"]
const arr = (await this.get(key, CacheGetType.TYPE_ARRAY)) || [];
log(`RedisCacheMgr::getList: getting list with key ${key}`);
const isNoneList = arr.length && arr.includes('NONE');
if (isNoneList || !arr.length) {
return Promise.resolve({
list: [],
isNoneList,
});
}
log(`RedisCacheMgr::getList: getting list with keys ${arr}`);
const values = await this.client.mget(arr);
if (values.some((v) => v === null)) {
// FALLBACK: a key is missing from list, this should never happen
console.error(`RedisCacheMgr::getList: missing value for ${key}`);
const allParents = [];
// get all parents from children
values.forEach((v) => {
allParents.push(...this.getParents(v));
});
// remove duplicates
const uniqueParents = [...new Set(allParents)];
// delete all parents and children
await Promise.all(
uniqueParents.map(async (p) => {
await this.deepDel(scope, p, CacheDelDirection.PARENT_TO_CHILD);
}),
);
return Promise.resolve({
list: [],
isNoneList,
});
}
return {
list: values.map((res) => {
try {
const o = JSON.parse(res);
if (typeof o === 'object') {
return o;
}
} catch (e) {
return res;
}
return res;
}),
isNoneList,
};
}
async setList(
scope: string,
subListKeys: string[],
list: any[],
props: string[] = [],
): Promise<boolean> {
// remove null from arrays
subListKeys = subListKeys.filter((k) => k);
// construct key for List
// e.g. nc:<orgs>:<scope>:<project_id_1>:<source_id_1>:list
const listKey =
subListKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
if (!list.length) {
// Set NONE here so that it won't hit the DB on each page load
return this.set(listKey, ['NONE']);
}
// fetch existing list
const listOfGetKeys =
(await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
for (const o of list) {
// construct key for Get
let getKey = `${this.prefix}:${scope}:${o.id}`;
if (props.length) {
const propValues = props.map((p) => o[p]);
// e.g. nc:<orgs>:<scope>:<prop_value_1>:<prop_value_2>
getKey = `${this.prefix}:${scope}:${propValues.join(':')}`;
}
log(`RedisCacheMgr::setList: get key ${getKey}`);
// get Get Key
let value = await this.get(getKey, CacheGetType.TYPE_OBJECT);
if (value) {
log(`RedisCacheMgr::setList: preparing key ${getKey}`);
// prepare Get Key
value = await this.prepareValue(o, this.getParents(value), listKey);
} else {
value = await this.prepareValue(o, [], listKey);
}
// set Get Key
log(`RedisCacheMgr::setList: setting key ${getKey}`);
await this.set(getKey, JSON.stringify(value, this.getCircularReplacer()));
// push Get Key to List
listOfGetKeys.push(getKey);
}
// set List Key
log(`RedisCacheMgr::setList: setting list with key ${listKey}`);
return this.set(listKey, listOfGetKeys);
}
async deepDel(
scope: string,
key: string,
direction: string,
): Promise<boolean> {
log(`RedisCacheMgr::deepDel: choose direction ${direction}`);
if (direction === CacheDelDirection.CHILD_TO_PARENT) {
const childKey = await this.get(key, CacheGetType.TYPE_OBJECT);
// given a child key, delete all keys in corresponding parent lists
const scopeList = this.getParents(childKey);
for (const listKey of scopeList) {
// get target list
let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
if (!list.length) {
continue;
}
// remove target Key
list = list.filter((k) => k !== key);
// delete list
log(`RedisCacheMgr::deepDel: remove listKey ${listKey}`);
await this.del(listKey);
if (list.length) {
// set target list
log(`RedisCacheMgr::deepDel: set key ${listKey}`);
await this.set(listKey, list);
}
}
log(`RedisCacheMgr::deepDel: remove key ${key}`);
return await this.del(key);
} else if (direction === CacheDelDirection.PARENT_TO_CHILD) {
key = /:list$/.test(key) ? key : `${key}:list`;
// given a list key, delete all the children
const listOfChildren = await this.get(key, CacheGetType.TYPE_ARRAY);
// delete each child key
await this.del(listOfChildren);
// delete list key
return await this.del(key);
} else {
log(`Invalid deepDel direction found : ${direction}`);
return Promise.resolve(false);
}
}
async appendToList(
scope: string,
subListKeys: string[],
key: string,
): Promise<boolean> {
// remove null from arrays
subListKeys = subListKeys.filter((k) => k);
// e.g. key = nc:<orgs>:<scope>:<project_id_1>:<source_id_1>:list
const listKey =
subListKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
log(`RedisCacheMgr::appendToList: append key ${key} to ${listKey}`);
let list = await this.get(listKey, CacheGetType.TYPE_ARRAY);
if (!list || !list.length) {
return false;
}
if (list.includes('NONE')) {
list = [];
await this.del(listKey);
}
log(`RedisCacheMgr::appendToList: get key ${key}`);
// get Get Key
const value = await this.get(key, CacheGetType.TYPE_OBJECT);
log(`RedisCacheMgr::appendToList: preparing key ${key}`);
if (!value) {
// FALLBACK: this is to get rid of all keys that would be effected by this (should never happen)
console.error(`RedisCacheMgr::appendToList: value is empty for ${key}`);
const allParents = [];
// get all children
const listValues = await this.getList(scope, subListKeys);
// get all parents from children
listValues.list.forEach((v) => {
allParents.push(...this.getParents(v));
});
// remove duplicates
const uniqueParents = [...new Set(allParents)];
// delete all parents and children
await Promise.all(
uniqueParents.map(async (p) => {
await this.deepDel(scope, p, CacheDelDirection.PARENT_TO_CHILD);
}),
);
return false;
}
// prepare Get Key
const preparedValue = await this.prepareValue(
value,
this.getParents(value),
listKey,
);
// set Get Key
log(`RedisCacheMgr::appendToList: setting key ${key}`);
await this.set(
key,
JSON.stringify(preparedValue, this.getCircularReplacer()),
);
list.push(key);
return this.set(listKey, list);
}
prepareValue(value, listKeys = [], newParent?) {
if (newParent) {
listKeys.push(newParent);
}
if (value && typeof value === 'object') {
value[CacheListProp] = listKeys;
} else if (value && typeof value === 'string') {
const keyHelper = value.split(CacheListProp);
if (listKeys.length) {
value = `${keyHelper[0]}${CacheListProp}${listKeys.join(',')}`;
}
} else if (value) {
console.error(
`RedisCacheMgr::prepareListKey: keyValue is not object or string`,
value,
);
throw new Error(
`RedisCacheMgr::prepareListKey: keyValue is not object or string`,
);
}
return value;
}
getParents(value) {
if (value && typeof value === 'object') {
if (CacheListProp in value) {
const listsForKey = value[CacheListProp];
if (listsForKey && listsForKey.length) {
return listsForKey;
}
}
} else if (value && typeof value === 'string') {
if (value.includes(CacheListProp)) {
const keyHelper = value.split(CacheListProp);
const listsForKey = keyHelper[1].split(',');
if (listsForKey.length) {
return listsForKey;
}
}
}
return [];
}
async destroy(): Promise<boolean> {
log('RedisCacheMgr::destroy: destroy redis');
return this.client.flushdb().then((r) => r === 'OK');
}
async export(): Promise<any> {
log('RedisCacheMgr::export: export data');
const data = await this.client.keys('*');
const res = {};
return await Promise.all(
data.map(async (k) => {
res[k] = await this.get(
k,
k.slice(-4) === 'list'
? CacheGetType.TYPE_ARRAY
: CacheGetType.TYPE_OBJECT,
);
}),
).then(() => {
return res;
});
this.context = 'RedisCacheMgr';
}
}

420
packages/nocodb/src/cache/RedisMockCacheMgr.ts vendored

@ -1,18 +1,10 @@
import debug from 'debug';
import Redis from 'ioredis-mock';
import CacheMgr from './CacheMgr';
import type IORedis from 'ioredis';
import {
CacheDelDirection,
CacheGetType,
CacheListProp,
} from '~/utils/globals';
const log = debug('nc:cache');
export default class RedisMockCacheMgr extends CacheMgr {
client: IORedis;
prefix: string;
const _log = debug('nc:cache');
export default class RedisMockCacheMgr extends CacheMgr {
constructor() {
super();
this.client = new Redis();
@ -22,412 +14,6 @@ export default class RedisMockCacheMgr extends CacheMgr {
// TODO(cache): fetch orgs once it's implemented
const orgs = 'noco';
this.prefix = `nc:${orgs}`;
}
// avoid circular structure to JSON
getCircularReplacer = () => {
const seen = new WeakSet();
return (_, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
// @ts-ignore
async del(key: string[] | string): Promise<any> {
log(`RedisMockCacheMgr::del: deleting key ${key}`);
if (Array.isArray(key)) {
if (key.length) {
return this.client.del(key);
}
} else if (key) {
return this.client.del(key);
}
}
// @ts-ignore
async get(key: string, type: string): Promise<any> {
log(`RedisMockCacheMgr::get: getting key ${key} with type ${type}`);
if (type === CacheGetType.TYPE_ARRAY) {
return this.client.smembers(key);
} else if (type === CacheGetType.TYPE_OBJECT) {
const res = await this.client.get(key);
try {
const o = JSON.parse(res);
if (typeof o === 'object') {
if (
o &&
Object.keys(o).length === 0 &&
Object.getPrototypeOf(o) === Object.prototype
) {
log(`RedisMockCacheMgr::get: object is empty!`);
}
return Promise.resolve(o);
}
} catch (e) {
return Promise.resolve(res);
}
return Promise.resolve(res);
} else if (type === CacheGetType.TYPE_STRING) {
return await this.client.get(key);
}
log(`Invalid CacheGetType: ${type}`);
return Promise.resolve(false);
}
// @ts-ignore
async set(key: string, value: any): Promise<any> {
if (typeof value !== 'undefined' && value) {
log(`RedisMockCacheMgr::set: setting key ${key} with value ${value}`);
if (typeof value === 'object') {
if (Array.isArray(value) && value.length) {
return this.client.sadd(key, value);
}
const keyValue = await this.get(key, CacheGetType.TYPE_OBJECT);
if (keyValue) {
value = await this.prepareValue(value, this.getParents(keyValue));
}
return this.client.set(
key,
JSON.stringify(value, this.getCircularReplacer()),
);
}
return this.client.set(key, value);
} else {
log(`RedisMockCacheMgr::set: value is empty for ${key}. Skipping ...`);
return Promise.resolve(true);
}
}
// @ts-ignore
async setExpiring(key: string, value: any, seconds: number): Promise<any> {
if (typeof value !== 'undefined' && value) {
log(
`RedisMockCacheMgr::setExpiring: setting key ${key} with value ${value} for ${seconds} seconds`,
);
// TODO: better way to handle expiration in mock redis
setTimeout(() => {
this.del(key);
}, seconds * 1000);
if (typeof value === 'object') {
if (Array.isArray(value) && value.length) {
return this.client.sadd(key, value);
}
return this.client.set(
key,
JSON.stringify(value, this.getCircularReplacer()),
);
}
return this.client.set(key, value);
} else {
log(`RedisMockCacheMgr::set: value is empty for ${key}. Skipping ...`);
return Promise.resolve(true);
}
}
// @ts-ignore
async incrby(key: string, value = 1): Promise<any> {
return this.client.incrby(key, value);
}
async getList(
scope: string,
subKeys: string[],
): Promise<{
list: any[];
isNoneList: boolean;
}> {
// remove null from arrays
subKeys = subKeys.filter((k) => k);
// e.g. key = nc:<orgs>:<scope>:<project_id_1>:<source_id_1>:list
const key =
subKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subKeys.join(':')}:list`;
// e.g. arr = ["nc:<orgs>:<scope>:<model_id_1>", "nc:<orgs>:<scope>:<model_id_2>"]
const arr = (await this.get(key, CacheGetType.TYPE_ARRAY)) || [];
log(`RedisMockCacheMgr::getList: getting list with key ${key}`);
const isNoneList = arr.length && arr[0] === 'NONE';
if (isNoneList || !arr.length) {
return Promise.resolve({
list: [],
isNoneList,
});
}
log(`RedisMockCacheMgr::getList: getting list with keys ${arr}`);
const values = await this.client.mget(arr);
if (values.some((v) => v === null)) {
// FALLBACK: a key is missing from list, this should never happen
console.error(`RedisMockCacheMgr::getList: missing value for ${key}`);
const allParents = [];
// get all parents from children
values.forEach((v) => {
allParents.push(...this.getParents(v));
});
// remove duplicates
const uniqueParents = [...new Set(allParents)];
// delete all parents and children
await Promise.all(
uniqueParents.map(async (p) => {
await this.deepDel(scope, p, CacheDelDirection.PARENT_TO_CHILD);
}),
);
return Promise.resolve({
list: [],
isNoneList,
});
}
return {
list: values.map((res) => {
try {
const o = JSON.parse(res);
if (typeof o === 'object') {
return o;
}
} catch (e) {
return res;
}
return res;
}),
isNoneList,
};
}
async setList(
scope: string,
subListKeys: string[],
list: any[],
props: string[] = [],
): Promise<boolean> {
// remove null from arrays
subListKeys = subListKeys.filter((k) => k);
// construct key for List
// e.g. nc:<orgs>:<scope>:<project_id_1>:<source_id_1>:list
const listKey =
subListKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
if (!list.length) {
// Set NONE here so that it won't hit the DB on each page load
return this.set(listKey, ['NONE']);
}
// fetch existing list
const listOfGetKeys =
(await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
for (const o of list) {
// construct key for Get
let getKey = `${this.prefix}:${scope}:${o.id}`;
if (props.length) {
const propValues = props.map((p) => o[p]);
// e.g. nc:<orgs>:<scope>:<prop_value_1>:<prop_value_2>
getKey = `${this.prefix}:${scope}:${propValues.join(':')}`;
}
log(`RedisMockCacheMgr::setList: get key ${getKey}`);
// get Get Key
let value = await this.get(getKey, CacheGetType.TYPE_OBJECT);
if (value) {
log(`RedisMockCacheMgr::setList: preparing key ${getKey}`);
// prepare Get Key
value = await this.prepareValue(o, this.getParents(value), listKey);
} else {
value = await this.prepareValue(o, [], listKey);
}
// set Get Key
log(`RedisMockCacheMgr::setList: setting key ${getKey}`);
await this.set(getKey, JSON.stringify(value, this.getCircularReplacer()));
// push Get Key to List
listOfGetKeys.push(getKey);
}
// set List Key
log(`RedisMockCacheMgr::setList: setting list with key ${listKey}`);
return this.set(listKey, listOfGetKeys);
}
async deepDel(
scope: string,
key: string,
direction: string,
): Promise<boolean> {
log(`RedisMockCacheMgr::deepDel: choose direction ${direction}`);
if (direction === CacheDelDirection.CHILD_TO_PARENT) {
const childKey = await this.get(key, CacheGetType.TYPE_OBJECT);
// given a child key, delete all keys in corresponding parent lists
const scopeList = this.getParents(childKey);
for (const listKey of scopeList) {
// get target list
let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
if (!list.length) {
continue;
}
// remove target Key
list = list.filter((k) => k !== key);
// delete list
log(`RedisMockCacheMgr::deepDel: remove listKey ${listKey}`);
await this.del(listKey);
if (list.length) {
// set target list
log(`RedisMockCacheMgr::deepDel: set key ${listKey}`);
await this.set(listKey, list);
}
}
log(`RedisMockCacheMgr::deepDel: remove key ${key}`);
return await this.del(key);
} else if (direction === CacheDelDirection.PARENT_TO_CHILD) {
key = /:list$/.test(key) ? key : `${key}:list`;
// given a list key, delete all the children
const listOfChildren = await this.get(key, CacheGetType.TYPE_ARRAY);
// delete each child key
await this.del(listOfChildren);
// delete list key
return await this.del(key);
} else {
log(`Invalid deepDel direction found : ${direction}`);
return Promise.resolve(false);
}
}
async appendToList(
scope: string,
subListKeys: string[],
key: string,
): Promise<boolean> {
// remove null from arrays
subListKeys = subListKeys.filter((k) => k);
// e.g. key = nc:<orgs>:<scope>:<project_id_1>:<source_id_1>:list
const listKey =
subListKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
log(`RedisMockCacheMgr::appendToList: append key ${key} to ${listKey}`);
let list = await this.get(listKey, CacheGetType.TYPE_ARRAY);
if (!list || !list.length) {
return false;
}
if (list[0] === 'NONE') {
list = [];
await this.del(listKey);
}
log(`RedisMockCacheMgr::appendToList: get key ${key}`);
// get Get Key
const value = await this.get(key, CacheGetType.TYPE_OBJECT);
log(`RedisMockCacheMgr::appendToList: preparing key ${key}`);
if (!value) {
// FALLBACK: this is to get rid of all keys that would be effected by this (should never happen)
console.error(
`RedisMockCacheMgr::appendToList: value is empty for ${key}`,
);
const allParents = [];
// get all children
const listValues = await this.getList(scope, subListKeys);
// get all parents from children
listValues.list.forEach((v) => {
allParents.push(...this.getParents(v));
});
// remove duplicates
const uniqueParents = [...new Set(allParents)];
// delete all parents and children
await Promise.all(
uniqueParents.map(async (p) => {
await this.deepDel(scope, p, CacheDelDirection.PARENT_TO_CHILD);
}),
);
return false;
}
// prepare Get Key
const preparedValue = await this.prepareValue(
value,
this.getParents(value),
listKey,
);
// set Get Key
log(`RedisMockCacheMgr::appendToList: setting key ${key}`);
await this.set(
key,
JSON.stringify(preparedValue, this.getCircularReplacer()),
);
list.push(key);
return this.set(listKey, list);
}
prepareValue(value, listKeys = [], newParent?) {
if (newParent) {
listKeys.push(newParent);
}
if (value && typeof value === 'object') {
value[CacheListProp] = listKeys;
} else if (value && typeof value === 'string') {
const keyHelper = value.split(CacheListProp);
if (listKeys.length) {
value = `${keyHelper[0]}${CacheListProp}${listKeys.join(',')}`;
}
} else if (value) {
console.error(
`RedisMockCacheMgr::prepareListKey: keyValue is not object or string`,
value,
);
throw new Error(
`RedisMockCacheMgr::prepareListKey: keyValue is not object or string`,
);
}
return value;
}
getParents(value) {
if (value && typeof value === 'object') {
if (CacheListProp in value) {
const listsForKey = value[CacheListProp];
if (listsForKey && listsForKey.length) {
return listsForKey;
}
}
} else if (value && typeof value === 'string') {
if (value.includes(CacheListProp)) {
const keyHelper = value.split(CacheListProp);
const listsForKey = keyHelper[1].split(',');
if (listsForKey.length) {
return listsForKey;
}
}
}
return [];
}
async destroy(): Promise<boolean> {
log('RedisMockCacheMgr::destroy: destroy redis');
return this.client.flushdb().then((r) => r === 'OK');
}
async export(): Promise<any> {
log('RedisMockCacheMgr::export: export data');
const data = await this.client.keys('*');
const res = {};
return await Promise.all(
data.map(async (k) => {
res[k] = await this.get(
k,
k.slice(-4) === 'list'
? CacheGetType.TYPE_ARRAY
: CacheGetType.TYPE_OBJECT,
);
}),
).then(() => {
return res;
});
this.context = 'RedisMockCacheMgr';
}
}

31
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -419,8 +419,11 @@ class BaseModelSqlv2 {
// if autogenerated string sort by created_at column if present
if (this.model.primaryKey && this.model.primaryKey.ai) {
qb.orderBy(this.model.primaryKey.column_name);
} else if (this.model.columns.find((c) => c.column_name === 'created_at')) {
qb.orderBy('created_at');
} else {
const createdCol = this.model.columns.find(
(c) => c.uidt === UITypes.CreatedTime && c.system,
);
if (createdCol) qb.orderBy(createdCol.column_name);
}
if (!ignorePagination) applyPaginate(qb, rest);
@ -5149,7 +5152,7 @@ class BaseModelSqlv2 {
// validate Ids
{
const childRowsQb = this.dbDriver(parentTn)
.select(parentColumn.column_name)
.select(`${parentTable.table_name}.${parentColumn.column_name}`)
.select(`${vTable.table_name}.${vChildCol.column_name}`)
.leftJoin(vTn, (qb) => {
qb.on(
@ -5162,7 +5165,6 @@ class BaseModelSqlv2 {
]),
);
});
if (parentTable.primaryKeys.length > 1) {
childRowsQb.where((qb) => {
for (const childId of childIds) {
@ -5171,7 +5173,7 @@ class BaseModelSqlv2 {
});
} else {
childRowsQb.whereIn(
parentTable.primaryKey.column_name,
`${parentTable.table_name}.${parentTable.primaryKey.column_name}`,
typeof childIds[0] === 'object'
? childIds.map(
(c) =>
@ -5183,7 +5185,9 @@ class BaseModelSqlv2 {
}
if (parentTable.primaryKey.column_name !== parentColumn.column_name)
childRowsQb.select(parentTable.primaryKey.column_name);
childRowsQb.select(
`${parentTable.table_name}.${parentTable.primaryKey.column_name}`,
);
const childRows = await this.execAndParse(childRowsQb, null, {
raw: true,
@ -5441,7 +5445,7 @@ class BaseModelSqlv2 {
});
} else if (typeof childIds[0] === 'object') {
childRowsQb.whereIn(
parentTable.primaryKey.column_name,
`${parentTable.table_name}.${parentTable.primaryKey.column_name}`,
childIds.map(
(c) =>
c[parentTable.primaryKey.title] ||
@ -5449,11 +5453,16 @@ class BaseModelSqlv2 {
),
);
} else {
childRowsQb.whereIn(parentTable.primaryKey.column_name, childIds);
childRowsQb.whereIn(
`${parentTable.table_name}.${parentTable.primaryKey.column_name}`,
childIds,
);
}
if (parentTable.primaryKey.column_name !== parentColumn.column_name)
childRowsQb.select(parentTable.primaryKey.column_name);
childRowsQb.select(
`${parentTable.table_name}.${parentTable.primaryKey.column_name}`,
);
const childRows = await this.execAndParse(childRowsQb, null, {
raw: true,
@ -5492,7 +5501,7 @@ class BaseModelSqlv2 {
.delete();
delQb.whereIn(
vParentCol.column_name,
`${vTable.table_name}.${vParentCol.column_name}`,
typeof childIds[0] === 'object'
? childIds.map(
(c) =>
@ -5774,7 +5783,7 @@ class BaseModelSqlv2 {
const qb = knex(this.getTnPath(model.table_name)).update(updateObject);
for (const rowId of Array.isArray(rowIds) ? rowIds : [rowIds]) {
qb.orWhere(await this._wherePk(rowId));
qb.orWhere(_wherePk(model.primaryKeys, rowId));
}
await this.execAndParse(qb, null, { raw: true });

6
packages/nocodb/src/db/conditionV2.ts

@ -667,7 +667,11 @@ const parseConditionV2 = async (
}
}
if (isNumericCol(column.uidt) && typeof genVal === 'string') {
if (
isNumericCol(column.uidt) &&
typeof genVal === 'string' &&
!isNaN(+genVal)
) {
// convert to number
genVal = +genVal;
}

2
packages/nocodb/src/db/sortV2.ts

@ -69,7 +69,7 @@ export default async function sortV2(
model,
column,
{},
alias
alias,
)
).builder;
qb.orderBy(builder, sort.direction || 'asc', nulls);

8
packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts

@ -25,6 +25,8 @@ import * as nc_035_add_username_to_users from '~/meta/migrations/v2/nc_035_add_u
import * as nc_036_base_deleted from '~/meta/migrations/v2/nc_036_base_deleted';
import * as nc_037_rename_project_and_base from '~/meta/migrations/v2/nc_037_rename_project_and_base';
import * as nc_038_formula_parsed_tree_column from '~/meta/migrations/v2/nc_038_formula_parsed_tree_column';
import * as nc_039_sqlite_alter_column_types from '~/meta/migrations/v2/nc_039_sqlite_alter_column_types';
import * as nc_040_form_view_alter_column_types from '~/meta/migrations/v2/nc_040_form_view_alter_column_types';
// Create a custom migration source class
export default class XcMigrationSourcev2 {
@ -61,6 +63,8 @@ export default class XcMigrationSourcev2 {
'nc_036_base_deleted',
'nc_037_rename_project_and_base',
'nc_038_formula_parsed_tree_column',
'nc_039_sqlite_alter_column_types',
'nc_040_form_view_alter_column_types',
]);
}
@ -124,6 +128,10 @@ export default class XcMigrationSourcev2 {
return nc_037_rename_project_and_base;
case 'nc_038_formula_parsed_tree_column':
return nc_038_formula_parsed_tree_column;
case 'nc_039_sqlite_alter_column_types':
return nc_039_sqlite_alter_column_types;
case 'nc_040_form_view_alter_column_types':
return nc_040_form_view_alter_column_types;
}
}
}

88
packages/nocodb/src/meta/migrations/v2/nc_039_sqlite_alter_column_types.ts

@ -0,0 +1,88 @@
import type { Knex } from 'knex';
import { MetaTable } from '~/utils/globals';
const up = async (knex: Knex) => {
if (knex.client.config.client === 'sqlite3') {
//nc_012_alter_colum_data_types.ts
await knex.schema.alterTable(MetaTable.COLUMNS, (table) => {
table.text('cdf').alter();
});
await knex.schema.alterTable(MetaTable.COLUMNS, (table) => {
table.text('dtxp').alter();
});
await knex.schema.alterTable(MetaTable.COLUMNS, (table) => {
table.text('cc').alter();
});
await knex.schema.alterTable(MetaTable.COLUMNS, (table) => {
table.text('ct').alter();
});
//nc_014_alter_colum_data_types.ts
await knex.schema.alterTable(MetaTable.FORM_VIEW, (table) => {
table.text('success_msg').alter();
});
await knex.schema.alterTable(MetaTable.FORM_VIEW, (table) => {
table.text('redirect_url').alter();
});
await knex.schema.alterTable(MetaTable.FORM_VIEW, (table) => {
table.text('banner_image_url').alter();
});
await knex.schema.alterTable(MetaTable.FORM_VIEW, (table) => {
table.text('logo_url').alter();
});
await knex.schema.alterTable(MetaTable.FORM_VIEW_COLUMNS, (table) => {
table.text('description').alter();
});
//nc_016_alter_hooklog_payload_types.ts
await knex.schema.alterTable(MetaTable.HOOK_LOGS, (table) => {
table.text('payload').alter();
});
//nc_029_webhook.ts
await knex.schema.alterTable(MetaTable.HOOK_LOGS, (table) => {
table.text('response').alter();
});
}
};
const down = async (knex) => {
if (knex.client.config.client === 'sqlite3') {
//nc_012_alter_colum_data_types.ts
await knex.schema.alterTable(MetaTable.COLUMNS, (table) => {
table.string('cdf').alter();
});
await knex.schema.alterTable(MetaTable.COLUMNS, (table) => {
table.string('dtxp').alter();
});
await knex.schema.alterTable(MetaTable.COLUMNS, (table) => {
table.string('cc').alter();
});
await knex.schema.alterTable(MetaTable.COLUMNS, (table) => {
table.string('ct').alter();
});
//nc_014_alter_colum_data_types.ts
await knex.schema.alterTable(MetaTable.FORM_VIEW, (table) => {
table.string('success_msg').alter();
});
await knex.schema.alterTable(MetaTable.FORM_VIEW, (table) => {
table.string('redirect_url').alter();
});
await knex.schema.alterTable(MetaTable.FORM_VIEW, (table) => {
table.string('banner_image_url').alter();
});
await knex.schema.alterTable(MetaTable.FORM_VIEW, (table) => {
table.string('logo_url').alter();
});
await knex.schema.alterTable(MetaTable.FORM_VIEW_COLUMNS, (table) => {
table.string('description').alter();
});
//nc_016_alter_hooklog_payload_types.ts
await knex.schema.alterTable(MetaTable.HOOK_LOGS, (table) => {
table.boolean('payload').alter();
});
//nc_029_webhook.ts
await knex.schema.alterTable(MetaTable.HOOK_LOGS, (table) => {
table.boolean('response').alter();
});
}
};
export { up, down };

28
packages/nocodb/src/meta/migrations/v2/nc_040_form_view_alter_column_types.ts

@ -0,0 +1,28 @@
import type { Knex } from 'knex';
import { MetaTable } from '~/utils/globals';
const up = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.FORM_VIEW, (table) => {
table.text('subheading').alter();
});
await knex.schema.alterTable(MetaTable.FORM_VIEW_COLUMNS, (table) => {
table.text('label').alter();
});
await knex.schema.alterTable(MetaTable.FORM_VIEW_COLUMNS, (table) => {
table.text('help').alter();
});
};
const down = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.FORM_VIEW, (table) => {
table.string('subheading').alter();
});
await knex.schema.alterTable(MetaTable.FORM_VIEW_COLUMNS, (table) => {
table.string('label').alter();
});
await knex.schema.alterTable(MetaTable.FORM_VIEW_COLUMNS, (table) => {
table.string('help').alter();
});
};
export { up, down };

1
packages/nocodb/src/models/ApiToken.ts

@ -56,7 +56,6 @@ export default class ApiToken implements ApiTokenType {
static async delete(token, ncMeta = Noco.ncMeta) {
await NocoCache.deepDel(
CacheScope.API_TOKEN,
`${CacheScope.API_TOKEN}:${token}`,
CacheDelDirection.CHILD_TO_PARENT,
);

2
packages/nocodb/src/models/Base.ts

@ -240,7 +240,6 @@ export default class Base implements BaseType {
// remove item in cache list
await NocoCache.deepDel(
CacheScope.PROJECT,
`${CacheScope.PROJECT}:${baseId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -353,7 +352,6 @@ export default class Base implements BaseType {
}
await NocoCache.deepDel(
CacheScope.PROJECT,
`${CacheScope.PROJECT}:${baseId}`,
CacheDelDirection.CHILD_TO_PARENT,
);

14
packages/nocodb/src/models/Column.ts

@ -481,7 +481,6 @@ export default class Column<T = any> implements ColumnType {
public static async clearList({ fk_model_id }) {
await NocoCache.deepDel(
CacheScope.COLUMN,
`${CacheScope.COLUMN}:${fk_model_id}:list`,
CacheDelDirection.PARENT_TO_CHILD,
);
@ -822,7 +821,6 @@ export default class Column<T = any> implements ColumnType {
fk_column_id: col.id,
});
await NocoCache.deepDel(
cacheScopeName,
`${cacheScopeName}:${col.id}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -838,7 +836,6 @@ export default class Column<T = any> implements ColumnType {
);
if (gridViewColumnId) {
await NocoCache.deepDel(
CacheScope.GRID_VIEW_COLUMN,
`${CacheScope.GRID_VIEW_COLUMN}:${gridViewColumnId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -854,7 +851,6 @@ export default class Column<T = any> implements ColumnType {
);
if (formViewColumnId) {
await NocoCache.deepDel(
CacheScope.FORM_VIEW_COLUMN,
`${CacheScope.FORM_VIEW_COLUMN}:${formViewColumnId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -870,7 +866,6 @@ export default class Column<T = any> implements ColumnType {
);
if (kanbanViewColumnId) {
await NocoCache.deepDel(
CacheScope.KANBAN_VIEW_COLUMN,
`${CacheScope.KANBAN_VIEW_COLUMN}:${kanbanViewColumnId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -886,7 +881,6 @@ export default class Column<T = any> implements ColumnType {
);
if (galleryViewColumnId) {
await NocoCache.deepDel(
CacheScope.GALLERY_VIEW_COLUMN,
`${CacheScope.GALLERY_VIEW_COLUMN}:${galleryViewColumnId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -917,7 +911,6 @@ export default class Column<T = any> implements ColumnType {
// Columns
await ncMeta.metaDelete(null, null, MetaTable.COLUMNS, col.id);
await NocoCache.deepDel(
CacheScope.COLUMN,
`${CacheScope.COLUMN}:${col.id}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -944,7 +937,6 @@ export default class Column<T = any> implements ColumnType {
fk_column_id: colId,
});
await NocoCache.deepDel(
CacheScope.COL_LOOKUP,
`${CacheScope.COL_LOOKUP}:${colId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -955,7 +947,6 @@ export default class Column<T = any> implements ColumnType {
fk_column_id: colId,
});
await NocoCache.deepDel(
CacheScope.COL_ROLLUP,
`${CacheScope.COL_ROLLUP}:${colId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -967,7 +958,6 @@ export default class Column<T = any> implements ColumnType {
fk_column_id: colId,
});
await NocoCache.deepDel(
CacheScope.COL_RELATION,
`${CacheScope.COL_RELATION}:${colId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -979,7 +969,6 @@ export default class Column<T = any> implements ColumnType {
});
await NocoCache.deepDel(
CacheScope.COL_FORMULA,
`${CacheScope.COL_FORMULA}:${colId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -991,7 +980,6 @@ export default class Column<T = any> implements ColumnType {
});
await NocoCache.deepDel(
CacheScope.COL_QRCODE,
`${CacheScope.COL_QRCODE}:${colId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -1004,7 +992,6 @@ export default class Column<T = any> implements ColumnType {
});
await NocoCache.deepDel(
CacheScope.COL_BARCODE,
`${CacheScope.COL_BARCODE}:${colId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -1018,7 +1005,6 @@ export default class Column<T = any> implements ColumnType {
});
await NocoCache.deepDel(
CacheScope.COL_SELECT_OPTION,
`${CacheScope.COL_SELECT_OPTION}:${colId}:list`,
CacheDelDirection.PARENT_TO_CHILD,
);

3
packages/nocodb/src/models/Filter.ts

@ -276,7 +276,6 @@ export default class Filter implements FilterType {
await deleteRecursively(f);
await ncMeta.metaDelete(null, null, MetaTable.FILTER_EXP, filter.id);
await NocoCache.deepDel(
CacheScope.FILTER_EXP,
`${CacheScope.FILTER_EXP}:${filter.id}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -435,7 +434,6 @@ export default class Filter implements FilterType {
if (filter.id) {
await ncMeta.metaDelete(null, null, MetaTable.FILTER_EXP, filter.id);
await NocoCache.deepDel(
CacheScope.FILTER_EXP,
`${CacheScope.FILTER_EXP}:${filter.id}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -459,7 +457,6 @@ export default class Filter implements FilterType {
if (filter.id) {
await ncMeta.metaDelete(null, null, MetaTable.FILTER_EXP, filter.id);
await NocoCache.deepDel(
CacheScope.FILTER_EXP,
`${CacheScope.FILTER_EXP}:${filter.id}`,
CacheDelDirection.CHILD_TO_PARENT,
);

2
packages/nocodb/src/models/Hook.ts

@ -238,7 +238,6 @@ export default class Hook implements HookType {
);
for (const filter of filterList) {
await NocoCache.deepDel(
CacheScope.FILTER_EXP,
`${CacheScope.FILTER_EXP}:${filter.id}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -246,7 +245,6 @@ export default class Hook implements HookType {
}
// Delete Hook
await NocoCache.deepDel(
CacheScope.HOOK,
`${CacheScope.HOOK}:${hookId}`,
CacheDelDirection.CHILD_TO_PARENT,
);

2
packages/nocodb/src/models/HookFilter.ts

@ -174,7 +174,6 @@ export default class Filter {
await deleteRecursively(f);
await ncMeta.metaDelete(null, null, MetaTable.FILTER_EXP, filter.id);
await NocoCache.deepDel(
CacheScope.FILTER_EXP,
`${CacheScope.FILTER_EXP}:${filter.id}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -307,7 +306,6 @@ export default class Filter {
// if (filter.id) {
// await ncMeta.metaDelete(null, null, MetaTable.FILTER_EXP, filter.id);
// await NocoCache.deepDel(
// CacheScope.FILTER_EXP,
// `${CacheScope.FILTER_EXP}:${filter.id}`,
// CacheDelDirection.CHILD_TO_PARENT
// );

4
packages/nocodb/src/models/Model.ts

@ -463,7 +463,6 @@ export default class Model implements TableType {
fk_column_id: col.id,
});
await NocoCache.deepDel(
cacheScopeName,
`${cacheScopeName}:${col.id}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -484,7 +483,6 @@ export default class Model implements TableType {
for (const col of leftOverColumns) {
await NocoCache.deepDel(
CacheScope.COL_RELATION,
`${CacheScope.COL_RELATION}:${col.fk_column_id}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -496,7 +494,6 @@ export default class Model implements TableType {
}
await NocoCache.deepDel(
CacheScope.COLUMN,
`${CacheScope.COLUMN}:${this.id}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -505,7 +502,6 @@ export default class Model implements TableType {
});
await NocoCache.deepDel(
CacheScope.MODEL,
`${CacheScope.MODEL}:${this.id}`,
CacheDelDirection.CHILD_TO_PARENT,
);

1
packages/nocodb/src/models/ModelRoleVisibility.ts

@ -124,7 +124,6 @@ export default class ModelRoleVisibility implements ModelRoleVisibilityType {
},
);
await NocoCache.deepDel(
CacheScope.MODEL_ROLE_VISIBILITY,
`${CacheScope.MODEL_ROLE_VISIBILITY}:${fk_view_id}:${role}`,
CacheDelDirection.CHILD_TO_PARENT,
);

2
packages/nocodb/src/models/Sort.ts

@ -27,7 +27,6 @@ export default class Sort {
public static async deleteAll(viewId: string, ncMeta = Noco.ncMeta) {
await NocoCache.deepDel(
CacheScope.SORT,
`${CacheScope.SORT}:${viewId}`,
CacheDelDirection.PARENT_TO_CHILD,
);
@ -187,7 +186,6 @@ export default class Sort {
await ncMeta.metaDelete(null, null, MetaTable.SORT, sortId);
await NocoCache.deepDel(
CacheScope.SORT,
`${CacheScope.SORT}:${sortId}`,
CacheDelDirection.CHILD_TO_PARENT,
);

3
packages/nocodb/src/models/Source.ts

@ -414,7 +414,6 @@ export default class Source implements SourceType {
fk_column_id: relCol.col.id,
});
await NocoCache.deepDel(
relCol.cacheScopeName,
`${relCol.cacheScopeName}:${relCol.col.id}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -434,7 +433,6 @@ export default class Source implements SourceType {
const res = await ncMeta.metaDelete(null, null, MetaTable.BASES, this.id);
await NocoCache.deepDel(
CacheScope.BASE,
`${CacheScope.BASE}:${this.id}`,
CacheDelDirection.CHILD_TO_PARENT,
);
@ -460,7 +458,6 @@ export default class Source implements SourceType {
);
await NocoCache.deepDel(
CacheScope.BASE,
`${CacheScope.BASE}:${this.id}`,
CacheDelDirection.CHILD_TO_PARENT,
);

2
packages/nocodb/src/models/User.ts

@ -75,7 +75,6 @@ export default class User implements UserType {
const bases = await Base.list({}, ncMeta);
for (const base of bases) {
await NocoCache.deepDel(
CacheScope.BASE_USER,
`${CacheScope.BASE_USER}:${base.id}:list`,
CacheDelDirection.PARENT_TO_CHILD,
);
@ -299,7 +298,6 @@ export default class User implements UserType {
for (const base of bases) {
await NocoCache.deepDel(
CacheScope.BASE_USER,
`${CacheScope.BASE_USER}:${base.id}:list`,
CacheDelDirection.PARENT_TO_CHILD,
);

3
packages/nocodb/src/models/View.ts

@ -1139,17 +1139,14 @@ export default class View implements ViewType {
});
await ncMeta.metaDelete(null, null, MetaTable.VIEWS, viewId);
await NocoCache.deepDel(
tableScope,
`${tableScope}:${viewId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
await NocoCache.deepDel(
columnTableScope,
`${columnTableScope}:${viewId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
await NocoCache.deepDel(
CacheScope.VIEW,
`${CacheScope.VIEW}:${viewId}`,
CacheDelDirection.CHILD_TO_PARENT,
);

4
packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts

@ -2077,12 +2077,12 @@ export class AtImportProcessor {
filter.operator = 'isNoneOf';
}
// if array, break it down to multiple filters
if (Array.isArray(filter.value)) {
for (let j = 0; j < filter.value.length; j++) {
filter.value[j] = await sMap.getNcNameFromAtId(filter.value[j]);
}
// if array, break it down to multiple filters
if (Array.isArray(filter.value)) {
const fx = {
fk_column_id: columnId,
logical_op: f.conjunction,

5
packages/nocodb/src/modules/jobs/jobs/at-import/helpers/readAndProcessData.ts

@ -414,8 +414,9 @@ export async function importLTARData({
[assocMeta.refCol.title]: id,
});
// links can be [] & hence assocTableDta[assocMeta.modelMeta.id] can be [].
if (
assocTableData[assocMeta.modelMeta.id].length >=
assocTableData[assocMeta.modelMeta.id]?.length >=
BULK_LINK_BATCH_COUNT
) {
let insertArray = assocTableData[
@ -469,7 +470,7 @@ export async function importLTARData({
for (const assocMeta of assocTableMetas) {
// insert remaining data
if (assocTableData[assocMeta.modelMeta.id].length >= 0) {
if (assocTableData[assocMeta.modelMeta.id]?.length >= 0) {
logBasic(
`:: Importing '${table.title}' LTAR data :: ${importedCount} - ${
importedCount + assocTableData[assocMeta.modelMeta.id].length

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

@ -12947,7 +12947,7 @@
}
},
"banner_image_url": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Banner Image URL. Not in use currently."
},
"columns": {
@ -12990,7 +12990,7 @@
"example": "collaborative"
},
"logo_url": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Logo URL. Not in use currently."
},
"meta": {
@ -13002,7 +13002,7 @@
"description": "The numbers of seconds to redirect after form submission"
},
"redirect_url": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "URL to redirect after submission"
},
"show_blank_form": {
@ -13010,7 +13010,7 @@
"description": "Show `Blank Form` after 5 seconds"
},
"subheading": {
"type": "string",
"$ref": "#/components/schemas/TextOrNull",
"description": "The subheading of the form",
"example": "My Form Subheading"
},
@ -13019,7 +13019,7 @@
"description": "Show `Submit Another Form` button"
},
"success_msg": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Custom message after the form is successfully submitted"
},
"title": {
@ -13053,7 +13053,7 @@
"type": "object",
"properties": {
"banner_image_url": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Banner Image URL. Not in use currently."
},
"email": {
@ -13067,7 +13067,7 @@
"type": "string"
},
"logo_url": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Logo URL. Not in use currently."
},
"meta": {
@ -13079,7 +13079,7 @@
"description": "The numbers of seconds to redirect after form submission"
},
"redirect_url": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "URL to redirect after submission"
},
"show_blank_form": {
@ -13087,15 +13087,16 @@
"description": "Show `Blank Form` after 5 seconds"
},
"subheading": {
"$ref": "#/components/schemas/StringOrNull",
"description": "The subheading of the form"
"$ref": "#/components/schemas/TextOrNull",
"description": "The subheading of the form",
"example": "My Form Subheading"
},
"submit_another_form": {
"$ref": "#/components/schemas/Bool",
"description": "Show `Submit Another Form` button"
},
"success_msg": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Custom message after the form is successfully submitted"
}
},
@ -13147,8 +13148,8 @@
"description": "Unique ID"
},
"description": {
"$ref": "#/components/schemas/StringOrNull",
"description": "Form Column Description (Not in use)"
"$ref": "#/components/schemas/TextOrNull",
"description": "Form Column Description"
},
"fk_column_id": {
"$ref": "#/components/schemas/Id",
@ -13159,11 +13160,11 @@
"description": "Foreign Key to View"
},
"help": {
"$ref": "#/components/schemas/StringOrNull",
"description": "Form Column Help Text"
"$ref": "#/components/schemas/TextOrNull",
"description": "Form Column Help Text (Not in use)"
},
"label": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Form Column Label"
},
"meta": {
@ -13232,15 +13233,15 @@
},
"properties": {
"description": {
"$ref": "#/components/schemas/StringOrNull",
"description": "Form Column Description (Not in use)"
"$ref": "#/components/schemas/TextOrNull",
"description": "Form Column Description"
},
"help": {
"$ref": "#/components/schemas/StringOrNull",
"description": "Form Column Help Text"
"$ref": "#/components/schemas/TextOrNull",
"description": "Form Column Help Text (Not in use)"
},
"label": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Form Column Label"
},
"meta": {
@ -16751,6 +16752,22 @@
"id": "8v8qzwm3w4v11"
}
},
"TextOrNull": {
"description": "Model for TextOrNull",
"examples": [
"string"
],
"oneOf": [
{
"maxLength": 8192,
"type": "string"
},
{
"type": "null"
}
],
"title": "TextOrNull Model"
},
"StringOrNull": {
"description": "Model for StringOrNull",
"examples": [

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

@ -18193,7 +18193,7 @@
}
},
"banner_image_url": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Banner Image URL. Not in use currently."
},
"columns": {
@ -18236,7 +18236,7 @@
"example": "collaborative"
},
"logo_url": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Logo URL. Not in use currently."
},
"meta": {
@ -18248,7 +18248,7 @@
"description": "The numbers of seconds to redirect after form submission"
},
"redirect_url": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "URL to redirect after submission"
},
"show_blank_form": {
@ -18256,7 +18256,7 @@
"description": "Show `Blank Form` after 5 seconds"
},
"subheading": {
"type": "string",
"$ref": "#/components/schemas/TextOrNull",
"description": "The subheading of the form",
"example": "My Form Subheading"
},
@ -18265,7 +18265,7 @@
"description": "Show `Submit Another Form` button"
},
"success_msg": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Custom message after the form is successfully submitted"
},
"title": {
@ -18299,7 +18299,7 @@
"type": "object",
"properties": {
"banner_image_url": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Banner Image URL. Not in use currently."
},
"email": {
@ -18313,7 +18313,7 @@
"type": "string"
},
"logo_url": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Logo URL. Not in use currently."
},
"meta": {
@ -18325,7 +18325,7 @@
"description": "The numbers of seconds to redirect after form submission"
},
"redirect_url": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "URL to redirect after submission"
},
"show_blank_form": {
@ -18333,15 +18333,16 @@
"description": "Show `Blank Form` after 5 seconds"
},
"subheading": {
"$ref": "#/components/schemas/StringOrNull",
"description": "The subheading of the form"
"$ref": "#/components/schemas/TextOrNull",
"description": "The subheading of the form",
"example": "My Form Subheading"
},
"submit_another_form": {
"$ref": "#/components/schemas/Bool",
"description": "Show `Submit Another Form` button"
},
"success_msg": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Custom message after the form is successfully submitted"
}
},
@ -18393,8 +18394,8 @@
"description": "Unique ID"
},
"description": {
"$ref": "#/components/schemas/StringOrNull",
"description": "Form Column Description (Not in use)"
"$ref": "#/components/schemas/TextOrNull",
"description": "Form Column Description"
},
"fk_column_id": {
"$ref": "#/components/schemas/Id",
@ -18405,11 +18406,11 @@
"description": "Foreign Key to View"
},
"help": {
"$ref": "#/components/schemas/StringOrNull",
"description": "Form Column Help Text"
"$ref": "#/components/schemas/TextOrNull",
"description": "Form Column Help Text (Not in use)"
},
"label": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Form Column Label"
},
"meta": {
@ -18478,15 +18479,15 @@
},
"properties": {
"description": {
"$ref": "#/components/schemas/StringOrNull",
"description": "Form Column Description (Not in use)"
"$ref": "#/components/schemas/TextOrNull",
"description": "Form Column Description"
},
"help": {
"$ref": "#/components/schemas/StringOrNull",
"description": "Form Column Help Text"
"$ref": "#/components/schemas/TextOrNull",
"description": "Form Column Help Text (Not in use)"
},
"label": {
"$ref": "#/components/schemas/StringOrNull",
"$ref": "#/components/schemas/TextOrNull",
"description": "Form Column Label"
},
"meta": {
@ -22007,6 +22008,22 @@
"id": "8v8qzwm3w4v11"
}
},
"TextOrNull": {
"description": "Model for TextOrNull",
"examples": [
"string"
],
"oneOf": [
{
"maxLength": 8192,
"type": "string"
},
{
"type": "null"
}
],
"title": "TextOrNull Model"
},
"StringOrNull": {
"description": "Model for StringOrNull",
"examples": [

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

@ -176,8 +176,6 @@ export enum CacheDelDirection {
CHILD_TO_PARENT = 'CHILD_TO_PARENT',
}
export const CacheListProp = '__nc_list__';
export const GROUPBY_COMPARISON_OPS = <const>[
// these are used for groupby
'gb_eq',

30
pnpm-lock.yaml

@ -233,8 +233,8 @@ importers:
specifier: ^1.1.23
version: 1.1.23
'@iconify-json/carbon':
specifier: ^1.1.29
version: 1.1.29
specifier: ^1.1.30
version: 1.1.30
'@iconify-json/cil':
specifier: ^1.1.8
version: 1.1.8
@ -257,8 +257,8 @@ importers:
specifier: ^1.1.42
version: 1.1.42
'@iconify-json/lucide':
specifier: ^1.1.163
version: 1.1.163
specifier: ^1.1.165
version: 1.1.165
'@iconify-json/material-symbols':
specifier: ^1.1.72
version: 1.1.72
@ -275,8 +275,8 @@ importers:
specifier: ^1.1.19
version: 1.1.19
'@iconify-json/simple-icons':
specifier: ^1.1.90
version: 1.1.90
specifier: ^1.1.91
version: 1.1.91
'@iconify-json/system-uicons':
specifier: ^1.1.12
version: 1.1.12
@ -4563,8 +4563,8 @@ packages:
'@iconify/types': 2.0.0
dev: true
/@iconify-json/carbon@1.1.29:
resolution: {integrity: sha512-zfyvX/kPItpBEU0fV0FhMW8Ln8PJX6is/L/GX7z9OOoVWEz1k8IlbK3KoBBH6ODZ8HHKG7HQ9FZ4nNl6RDe0Lw==}
/@iconify-json/carbon@1.1.30:
resolution: {integrity: sha512-tEvEmxCO0J0t0p2NT2IvJ+iiSNqqnabygSo/S8wkeh2Guhc4tQOgl9dxvDMpy6RqwtKRTKsaROPtnRfu/bl+Tg==}
dependencies:
'@iconify/types': 2.0.0
dev: true
@ -4611,8 +4611,8 @@ packages:
'@iconify/types': 2.0.0
dev: true
/@iconify-json/lucide@1.1.163:
resolution: {integrity: sha512-zZWM3FJfsUk6RQOrh3rd9DEek4QLCndiFGHyC6sRTMZQgCQ3muI3q20OQJ5QlvX2Md87OKcxPLgFiY2+Z/wA0w==}
/@iconify-json/lucide@1.1.165:
resolution: {integrity: sha512-JkfIpIxKKpvJ2Xx7mGpUkYSpxVy3WrJTp3F/dhmX3ShxE47e90YMFfl8TyANNLSVgxSC/LMnY4I4Xh+7h34h9w==}
dependencies:
'@iconify/types': 2.0.0
dev: true
@ -4647,8 +4647,8 @@ packages:
'@iconify/types': 2.0.0
dev: true
/@iconify-json/simple-icons@1.1.90:
resolution: {integrity: sha512-IYDCQD3VJ5verAwWwMgnbJ6SvPzEqH0SG6JsI2ySuI64d7qVbcJMFycc0kGPfSsi2yAPYHk+tD6Iln17y1MuNA==}
/@iconify-json/simple-icons@1.1.91:
resolution: {integrity: sha512-hFWxeQWjCh26nObKnEm+AMB5W+bh4pXtmT3PnecS7rP2Crh0AHi5QBHPtH+6L8R6xZtBk5I2TLoA0TRzCgrF8A==}
dependencies:
'@iconify/types': 2.0.0
dev: true
@ -23474,9 +23474,6 @@ packages:
/sqlite3@5.1.6:
resolution: {integrity: sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==}
requiresBuild: true
peerDependenciesMeta:
node-gyp:
optional: true
dependencies:
'@mapbox/node-pre-gyp': 1.0.11
node-addon-api: 4.3.0
@ -23491,9 +23488,6 @@ packages:
/sqlite3@5.1.7:
resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==}
requiresBuild: true
peerDependenciesMeta:
node-gyp:
optional: true
dependencies:
bindings: 1.5.0
node-addon-api: 7.0.0

2
tests/playwright/constants/index.ts

@ -1,4 +1,4 @@
const airtableApiKey = 'keyn1MR87qgyUsYg4';
const airtableApiKey = 'patnDizoItL6GsweQ.88f74da91af272b42326802c212e382d39dd38be9b81f4ad25beaae7de793535';
const airtableApiBase = 'https://airtable.com/shr4z0qmh6dg5s3eB';
const defaultBaseName = 'Base';

55
tests/playwright/tests/db/features/keyboardShortcuts.spec.ts

@ -137,6 +137,61 @@ test.describe('Verify shortcuts', () => {
await page.keyboard.press((await grid.isMacOs()) ? 'Meta+Enter' : 'Control+Enter');
await page.reload();
await grid.cell.verify({ index: 1, columnHeader: 'Country', value: 'NewAlgeria' });
// Tab:
// If current page is not last page and and current cell is last column of last row and user press `Tab` then current page will be incremented by 1
await grid.cell.click({ index: 24, columnHeader: 'Cities' });
await page.keyboard.press('Tab');
await grid.verifyActivePage({ pageNumber: '2' });
await grid.cell.verifyCellActiveSelected({ index: 0, columnHeader: 'Country' });
// If current page is last page and and current cell is last column of last row and user press `Tab` then new empty row will be added
await grid.clickPagination({ type: 'last-page' });
await grid.cell.click({ index: 8, columnHeader: 'Cities' });
await page.keyboard.press('Tab');
await grid.verifyRowCount({ count: 10 });
await grid.cell.verifyCellActiveSelected({ index: 9, columnHeader: 'Country' });
// If current page is not first page and and current cell is first column of first row and user press `Shift+Tab` then current page will be decremented by 1
await grid.cell.click({ index: 0, columnHeader: 'Country' });
await page.keyboard.press('Shift+Tab');
await grid.verifyActivePage({ pageNumber: '4' });
await grid.cell.verifyCellActiveSelected({ index: 24, columnHeader: 'Cities' });
// If current page is first page and and current cell is first column of first row and user press `Shift+Tab` then current page will not change
await grid.clickPagination({ type: 'first-page' });
await grid.cell.click({ index: 0, columnHeader: 'Country' });
await page.keyboard.press('Shift+Tab');
await grid.verifyActivePage({ pageNumber: '1' });
await grid.cell.verifyCellActiveSelected({ index: 0, columnHeader: 'Country' });
// ArrowDown:
// If current page is not last page and and current cell is in last row and user press `ArrowDown` then current page will be incremented by 1
await grid.cell.click({ index: 24, columnHeader: 'Cities' });
await page.keyboard.press('ArrowDown');
await grid.verifyActivePage({ pageNumber: '2' });
await grid.cell.verifyCellActiveSelected({ index: 0, columnHeader: 'Cities' });
// If current page is last page and and current cell is in last row and user press `ArrowDown` then new empty row will be added
await grid.clickPagination({ type: 'last-page' });
await grid.cell.click({ index: 8, columnHeader: 'Cities' });
await page.keyboard.press('ArrowDown');
await grid.verifyRowCount({ count: 10 });
await grid.cell.verifyCellActiveSelected({ index: 9, columnHeader: 'Country' });
// ArrowUp:
// If current page is not first page and and current cell is in first row and user press `ArrwoUp` then current page will be decremented by 1
await grid.cell.click({ index: 0, columnHeader: 'Cities' });
await page.keyboard.press('ArrowUp');
await grid.verifyActivePage({ pageNumber: '4' });
await grid.cell.verifyCellActiveSelected({ index: 24, columnHeader: 'Cities' });
// If current page is first page and and current cell is in first row and user press `ArrwoUp` then current page will not change
await grid.clickPagination({ type: 'first-page' });
await grid.cell.click({ index: 0, columnHeader: 'Cities' });
await page.keyboard.press('ArrowUp');
await grid.verifyActivePage({ pageNumber: '1' });
await grid.cell.verifyCellActiveSelected({ index: 0, columnHeader: 'Cities' });
});
});

Loading…
Cancel
Save