This commit is contained in:
DCCONSTRUCTIONS 2026-04-23 12:49:43 +03:00
parent c18fa2b7e9
commit 4d6aba098d
10 changed files with 1408 additions and 15 deletions

29
docker/README.md Normal file
View File

@ -0,0 +1,29 @@
# Docker Deployment Snapshot
Эта папка содержит текущий кастомизированный deployment-контур NODE.DC, подготовленный для серверного разворачивания.
Содержимое:
- `docker-compose.yaml` — текущий compose-файл кастомного контура
- `plane.env` — текущий набор переменных окружения
- `plane.env.example` — пример env-файла
- `setup.sh` — локальная копия deployment-скрипта, адаптированная на работу с папкой `docker/`
Базовый запуск:
```bash
cd docker
./setup.sh start
```
Если нужно смотреть или править переменные:
```bash
cd docker
sed -n '1,220p' plane.env
```
Важно:
- Это снимок именно текущего кастомизированного контура для последующего деплоя
- Исходный старый каталог `plane-app/` не тронут и оставлен как есть

260
docker/docker-compose.yaml Normal file
View File

@ -0,0 +1,260 @@
x-db-env: &db-env
PGHOST: ${PGHOST:-plane-db}
PGDATABASE: ${PGDATABASE:-plane}
POSTGRES_USER: ${POSTGRES_USER:-plane}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-plane}
POSTGRES_DB: ${POSTGRES_DB:-plane}
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
PGDATA: ${PGDATA:-/var/lib/postgresql/data}
x-redis-env: &redis-env
REDIS_HOST: ${REDIS_HOST:-plane-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
REDIS_URL: ${REDIS_URL:-redis://plane-redis:6379/}
x-minio-env: &minio-env
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-access-key}
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-secret-key}
x-aws-s3-env: &aws-s3-env
AWS_REGION: ${AWS_REGION:-}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-access-key}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-secret-key}
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
x-proxy-env: &proxy-env
APP_DOMAIN: ${APP_DOMAIN:-localhost}
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
CERT_EMAIL: ${CERT_EMAIL}
CERT_ACME_CA: ${CERT_ACME_CA}
CERT_ACME_DNS: ${CERT_ACME_DNS}
LISTEN_HTTP_PORT: ${LISTEN_HTTP_PORT:-80}
LISTEN_HTTPS_PORT: ${LISTEN_HTTPS_PORT:-443}
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
SITE_ADDRESS: ${SITE_ADDRESS:-:80}
x-mq-env: &mq-env # RabbitMQ Settings
RABBITMQ_HOST: ${RABBITMQ_HOST:-plane-mq}
RABBITMQ_PORT: ${RABBITMQ_PORT:-5672}
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-plane}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-plane}
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:-plane}
RABBITMQ_VHOST: ${RABBITMQ_VHOST:-plane}
x-live-env: &live-env
API_BASE_URL: ${API_BASE_URL:-http://api:8000}
LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY:-2FiJk1U2aiVPEQtzLehYGlTSnTnrs7LW}
x-app-env: &app-env
WEB_URL: ${WEB_URL:-http://localhost}
DEBUG: ${DEBUG:-0}
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
GUNICORN_WORKERS: 1
POSTHOG_API_KEY: ${POSTHOG_API_KEY:-}
POSTHOG_HOST: ${POSTHOG_HOST:-}
INSTANCE_CHANGELOG_URL: ${INSTANCE_CHANGELOG_URL:-}
IS_INTERCOM_ENABLED: ${IS_INTERCOM_ENABLED:-0}
INTERCOM_APP_ID: ${INTERCOM_APP_ID:-}
USE_MINIO: ${USE_MINIO:-1}
DATABASE_URL: ${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
SECRET_KEY: ${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
AMQP_URL: ${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane}
API_KEY_RATE_LIMIT: ${API_KEY_RATE_LIMIT:-60/minute}
MINIO_ENDPOINT_SSL: ${MINIO_ENDPOINT_SSL:-0}
LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY:-2FiJk1U2aiVPEQtzLehYGlTSnTnrs7LW}
services:
web:
image: nodedc/plane-frontend:ru
deploy:
replicas: ${WEB_REPLICAS:-1}
restart_policy:
condition: any
depends_on:
- api
- worker
space:
image: nodedc/plane-space:ru
deploy:
replicas: ${SPACE_REPLICAS:-1}
restart_policy:
condition: any
depends_on:
- api
- worker
- web
admin:
image: nodedc/plane-admin:ru
deploy:
replicas: ${ADMIN_REPLICAS:-1}
restart_policy:
condition: any
depends_on:
- api
- web
live:
image: makeplane/plane-live:${APP_RELEASE:-v1.3.0}
environment:
<<: [*live-env, *redis-env]
deploy:
replicas: ${LIVE_REPLICAS:-1}
restart_policy:
condition: any
depends_on:
- api
- web
api:
image: nodedc/plane-backend:local
command: ./bin/docker-entrypoint-api.sh
deploy:
replicas: ${API_REPLICAS:-1}
restart_policy:
condition: any
volumes:
- logs_api:/code/plane/logs
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- plane-db
- plane-redis
- plane-mq
worker:
image: nodedc/plane-backend:local
command: ./bin/docker-entrypoint-worker.sh
deploy:
replicas: ${WORKER_REPLICAS:-1}
restart_policy:
condition: any
volumes:
- logs_worker:/code/plane/logs
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- api
- plane-db
- plane-redis
- plane-mq
beat-worker:
image: nodedc/plane-backend:local
command: ./bin/docker-entrypoint-beat.sh
deploy:
replicas: ${BEAT_WORKER_REPLICAS:-1}
restart_policy:
condition: any
volumes:
- logs_beat-worker:/code/plane/logs
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- api
- plane-db
- plane-redis
- plane-mq
migrator:
image: nodedc/plane-backend:local
command: ./bin/docker-entrypoint-migrator.sh
deploy:
replicas: 1
restart_policy:
condition: on-failure
volumes:
- logs_migrator:/code/plane/logs
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- plane-db
- plane-redis
# Comment this if you already have a database running
plane-db:
image: postgres:15.7-alpine
command: postgres -c 'max_connections=1000'
deploy:
replicas: 1
restart_policy:
condition: any
environment:
<<: *db-env
volumes:
- pgdata:/var/lib/postgresql/data
plane-redis:
image: valkey/valkey:7.2.11-alpine
deploy:
replicas: 1
restart_policy:
condition: any
volumes:
- redisdata:/data
plane-mq:
image: rabbitmq:3.13.6-management-alpine
deploy:
replicas: 1
restart_policy:
condition: any
environment:
<<: *mq-env
volumes:
- rabbitmq_data:/var/lib/rabbitmq
# Comment this if you using any external s3 compatible storage
plane-minio:
image: minio/minio:latest
command: server /export --console-address ":9090"
deploy:
replicas: 1
restart_policy:
condition: any
environment:
<<: *minio-env
volumes:
- uploads:/export
# Comment this if you already have a reverse proxy running
proxy:
image: makeplane/plane-proxy:${APP_RELEASE:-v1.3.0}
deploy:
replicas: 1
restart_policy:
condition: any
environment:
<<: *proxy-env
ports:
- target: 80
published: ${LISTEN_HTTP_PORT:-80}
protocol: tcp
mode: host
- target: 443
published: ${LISTEN_HTTPS_PORT:-443}
protocol: tcp
mode: host
volumes:
- proxy_config:/config
- proxy_data:/data
depends_on:
- web
- api
- space
- admin
- live
volumes:
pgdata:
redisdata:
uploads:
logs_api:
logs_worker:
logs_beat-worker:
logs_migrator:
rabbitmq_data:
proxy_config:
proxy_data:

90
docker/plane.env Normal file
View File

@ -0,0 +1,90 @@
APP_DOMAIN=localhost
APP_RELEASE=v1.3.0
WEB_REPLICAS=1
SPACE_REPLICAS=1
ADMIN_REPLICAS=1
API_REPLICAS=1
WORKER_REPLICAS=1
BEAT_WORKER_REPLICAS=1
LIVE_REPLICAS=1
LISTEN_HTTP_PORT=8090
LISTEN_HTTPS_PORT=8443
WEB_URL=http://localhost:8090
DEBUG=0
CORS_ALLOWED_ORIGINS=http://localhost:8090
API_BASE_URL=http://api:8000
#DB SETTINGS
PGHOST=plane-db
PGDATABASE=plane
POSTGRES_USER=plane
POSTGRES_PASSWORD=plane
POSTGRES_DB=plane
POSTGRES_PORT=5432
PGDATA=/var/lib/postgresql/data
DATABASE_URL=
# REDIS SETTINGS
REDIS_HOST=plane-redis
REDIS_PORT=6379
REDIS_URL=
# RabbitMQ Settings
RABBITMQ_HOST=plane-mq
RABBITMQ_PORT=5672
RABBITMQ_USER=plane
RABBITMQ_PASSWORD=plane
RABBITMQ_VHOST=plane
AMQP_URL=
# If SSL Cert to be generated, set CERT_EMAIl="email <EMAIL_ADDRESS>"
CERT_ACME_CA=https://acme-v02.api.letsencrypt.org/directory
TRUSTED_PROXIES=0.0.0.0/0
SITE_ADDRESS=:80
CERT_EMAIL=
# For DNS Challenge based certificate generation, set the CERT_ACME_DNS, CERT_EMAIL
# CERT_ACME_DNS="acme_dns <CERT_DNS_PROVIDER> <CERT_DNS_PROVIDER_API_KEY>"
CERT_ACME_DNS=
# Secret Key
SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5
# DATA STORE SETTINGS
USE_MINIO=1
AWS_REGION=
AWS_ACCESS_KEY_ID=access-key
AWS_SECRET_ACCESS_KEY=secret-key
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
AWS_S3_BUCKET_NAME=uploads
FILE_SIZE_LIMIT=5242880
POSTHOG_API_KEY=
POSTHOG_HOST=
INSTANCE_CHANGELOG_URL=
IS_INTERCOM_ENABLED=0
INTERCOM_APP_ID=
# Gunicorn Workers
GUNICORN_WORKERS=1
# UNCOMMENT `DOCKER_PLATFORM` IF YOU ARE ON `ARM64` AND DOCKER IMAGE IS NOT AVAILABLE FOR RESPECTIVE `APP_RELEASE`
# DOCKER_PLATFORM=linux/amd64
# Force HTTPS for handling SSL Termination
MINIO_ENDPOINT_SSL=0
# API key rate limit
API_KEY_RATE_LIMIT=60/minute
# Live server environment variables
# WARNING: You must set a secure value for LIVE_SERVER_SECRET_KEY in production environments.
LIVE_SERVER_SECRET_KEY=
DOCKERHUB_USER=makeplane
PULL_POLICY=if_not_present
CUSTOM_BUILD=false

90
docker/plane.env.example Normal file
View File

@ -0,0 +1,90 @@
APP_DOMAIN=localhost
APP_RELEASE=v1.3.0
WEB_REPLICAS=1
SPACE_REPLICAS=1
ADMIN_REPLICAS=1
API_REPLICAS=1
WORKER_REPLICAS=1
BEAT_WORKER_REPLICAS=1
LIVE_REPLICAS=1
LISTEN_HTTP_PORT=8090
LISTEN_HTTPS_PORT=8443
WEB_URL=http://localhost:8090
DEBUG=0
CORS_ALLOWED_ORIGINS=http://localhost:8090
API_BASE_URL=http://api:8000
#DB SETTINGS
PGHOST=plane-db
PGDATABASE=plane
POSTGRES_USER=plane
POSTGRES_PASSWORD=plane
POSTGRES_DB=plane
POSTGRES_PORT=5432
PGDATA=/var/lib/postgresql/data
DATABASE_URL=
# REDIS SETTINGS
REDIS_HOST=plane-redis
REDIS_PORT=6379
REDIS_URL=
# RabbitMQ Settings
RABBITMQ_HOST=plane-mq
RABBITMQ_PORT=5672
RABBITMQ_USER=plane
RABBITMQ_PASSWORD=plane
RABBITMQ_VHOST=plane
AMQP_URL=
# If SSL Cert to be generated, set CERT_EMAIl="email <EMAIL_ADDRESS>"
CERT_ACME_CA=https://acme-v02.api.letsencrypt.org/directory
TRUSTED_PROXIES=0.0.0.0/0
SITE_ADDRESS=:80
CERT_EMAIL=
# For DNS Challenge based certificate generation, set the CERT_ACME_DNS, CERT_EMAIL
# CERT_ACME_DNS="acme_dns <CERT_DNS_PROVIDER> <CERT_DNS_PROVIDER_API_KEY>"
CERT_ACME_DNS=
# Secret Key
SECRET_KEY=CHANGE_ME_BEFORE_PRODUCTION
# DATA STORE SETTINGS
USE_MINIO=1
AWS_REGION=
AWS_ACCESS_KEY_ID=access-key
AWS_SECRET_ACCESS_KEY=secret-key
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
AWS_S3_BUCKET_NAME=uploads
FILE_SIZE_LIMIT=5242880
POSTHOG_API_KEY=
POSTHOG_HOST=
INSTANCE_CHANGELOG_URL=
IS_INTERCOM_ENABLED=0
INTERCOM_APP_ID=
# Gunicorn Workers
GUNICORN_WORKERS=1
# UNCOMMENT `DOCKER_PLATFORM` IF YOU ARE ON `ARM64` AND DOCKER IMAGE IS NOT AVAILABLE FOR RESPECTIVE `APP_RELEASE`
# DOCKER_PLATFORM=linux/amd64
# Force HTTPS for handling SSL Termination
MINIO_ENDPOINT_SSL=0
# API key rate limit
API_KEY_RATE_LIMIT=60/minute
# Live server environment variables
# WARNING: You must set a secure value for LIVE_SERVER_SECRET_KEY in production environments.
LIVE_SERVER_SECRET_KEY=CHANGE_ME_BEFORE_PRODUCTION
DOCKERHUB_USER=makeplane
PULL_POLICY=if_not_present
CUSTOM_BUILD=false

733
docker/setup.sh Executable file
View File

@ -0,0 +1,733 @@
#!/bin/bash
BRANCH=${BRANCH:-master}
SCRIPT_PATH=$(cd "$(dirname "$0")" && pwd)
SCRIPT_DIR=$(cd "$SCRIPT_PATH/.." && pwd)
DEPLOY_FOLDER=docker
SERVICE_FOLDER=${COMPOSE_PROJECT_NAME:-plane-app}
PLANE_INSTALL_DIR=$SCRIPT_DIR/$DEPLOY_FOLDER
export APP_RELEASE=stable
export DOCKERHUB_USER=makeplane
export PULL_POLICY=${PULL_POLICY:-if_not_present}
export GH_REPO=makeplane/plane
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
export FALLBACK_DOWNLOAD_URL="https://raw.githubusercontent.com/$GH_REPO/$BRANCH/deployments/cli/community"
CPU_ARCH=$(uname -m)
OS_NAME=$(uname)
UPPER_CPU_ARCH=$(tr '[:lower:]' '[:upper:]' <<< "$CPU_ARCH")
mkdir -p $PLANE_INSTALL_DIR/archive
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
function print_header() {
clear
cat <<"EOF"
##+. ##+ .##-
######+.######-.######.
#######. -### +#####+.
#######. + +######.
#######. .#######
#######. .#######
####### + .#######
.+#####+ ###- .#######
.######.-#####+.+######
-##. -## .+##
EOF
}
function spinner() {
local pid=$1
local delay=.5
local spinstr='|/-\'
if ! ps -p "$pid" > /dev/null; then
echo "Invalid PID: $pid"
return 1
fi
while ps -p "$pid" > /dev/null; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr" >&2
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b" >&2
done
printf " \b\b\b\b" >&2
}
function checkLatestRelease(){
echo "Checking for the latest release..." >&2
local latest_release=$(
curl -sSL https://api.github.com/repos/$GH_REPO/releases/latest \
| sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' \
| head -n 1
)
if [ -z "$latest_release" ]; then
echo "Failed to check for the latest release. Exiting..." >&2
exit 1
fi
echo $latest_release
}
function initialize(){
printf "Please wait while we check the availability of Docker images for the selected release ($APP_RELEASE) with ${UPPER_CPU_ARCH} support." >&2
if [ "$CUSTOM_BUILD" == "true" ]; then
echo "" >&2
echo "" >&2
echo "${UPPER_CPU_ARCH} images are not available for selected release ($APP_RELEASE)." >&2
echo "build"
return 1
fi
local IMAGE_NAME=makeplane/plane-proxy
local IMAGE_TAG=${APP_RELEASE}
docker manifest inspect "${IMAGE_NAME}:${IMAGE_TAG}" | grep -q "\"architecture\": \"${CPU_ARCH}\"" &
local pid=$!
spinner "$pid"
echo "" >&2
wait "$pid"
if [ $? -eq 0 ]; then
echo "Plane supports ${CPU_ARCH}" >&2
echo "available"
return 0
else
echo "" >&2
echo "" >&2
echo "${UPPER_CPU_ARCH} images are not available for selected release ($APP_RELEASE)." >&2
echo "" >&2
echo "build"
return 1
fi
}
function getEnvValue() {
local key=$1
local file=$2
if [ -z "$key" ] || [ -z "$file" ]; then
echo "Invalid arguments supplied"
exit 1
fi
if [ -f "$file" ]; then
grep -q "^$key=" "$file"
if [ $? -eq 0 ]; then
local value
value=$(grep "^$key=" "$file" | cut -d'=' -f2)
echo "$value"
else
echo ""
fi
fi
}
function updateEnvFile() {
local key=$1
local value=$2
local file=$3
if [ -z "$key" ] || [ -z "$value" ] || [ -z "$file" ]; then
echo "Invalid arguments supplied"
exit 1
fi
if [ -f "$file" ]; then
# check if key exists in the file
grep -q "^$key=" "$file"
if [ $? -ne 0 ]; then
echo "$key=$value" >> "$file"
return
else
if [ "$OS_NAME" == "Darwin" ]; then
value=$(echo "$value" | sed 's/|/\\|/g')
sed -i '' "s|^$key=.*|$key=$value|g" "$file"
else
value=$(echo "$value" | sed 's/\//\\\//g')
sed -i "s/^$key=.*/$key=$value/g" "$file"
fi
fi
else
echo "File not found: $file"
exit 1
fi
}
function updateCustomVariables(){
echo "Updating custom variables..." >&2
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
echo "Custom variables updated successfully" >&2
}
function syncEnvFile(){
echo "Syncing environment variables..." >&2
if [ -f "$PLANE_INSTALL_DIR/plane.env.bak" ]; then
updateCustomVariables
# READ keys of plane.env and update the values from plane.env.bak
while IFS= read -r line
do
# ignore is the line is empty or starts with #
if [ -z "$line" ] || [[ $line == \#* ]]; then
continue
fi
key=$(echo "$line" | cut -d'=' -f1)
value=$(getEnvValue "$key" "$PLANE_INSTALL_DIR/plane.env.bak")
if [ -n "$value" ]; then
updateEnvFile "$key" "$value" "$DOCKER_ENV_PATH"
fi
done < "$DOCKER_ENV_PATH"
fi
echo "Environment variables synced successfully" >&2
}
function loadComposeEnv() {
if [ -f "$DOCKER_ENV_PATH" ]; then
set -a
source "$DOCKER_ENV_PATH"
set +a
fi
}
function runComposeCmd() {
loadComposeEnv
/bin/bash -c "$COMPOSE_CMD -p $SERVICE_FOLDER -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH $1"
}
function buildYourOwnImage(){
echo "Building images locally..."
export DOCKERHUB_USER="myplane"
export APP_RELEASE="local"
export PULL_POLICY="never"
CUSTOM_BUILD="true"
# checkout the code to ~/tmp/plane folder and build the images
local PLANE_TEMP_CODE_DIR=~/tmp/plane
rm -rf $PLANE_TEMP_CODE_DIR
mkdir -p $PLANE_TEMP_CODE_DIR
REPO=https://github.com/$GH_REPO.git
git clone "$REPO" "$PLANE_TEMP_CODE_DIR" --branch "$BRANCH" --single-branch --depth 1
cp "$PLANE_TEMP_CODE_DIR/deployments/cli/community/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml"
cd "$PLANE_TEMP_CODE_DIR" || exit
/bin/bash -c "$COMPOSE_CMD -f build.yml build --no-cache" >&2
if [ $? -ne 0 ]; then
echo "Build failed. Exiting..."
exit 1
fi
echo "Build completed successfully"
echo ""
echo "You can now start the services by running the command: ./setup.sh start"
echo ""
}
function install() {
echo "Begin Installing Plane"
echo ""
if [ "$APP_RELEASE" == "stable" ]; then
export APP_RELEASE=$(checkLatestRelease)
fi
local build_image=$(initialize)
if [ "$build_image" == "build" ]; then
# ask for confirmation to continue building the images
echo "Do you want to continue with building the Docker images locally?"
read -p "Continue? [y/N]: " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Exiting..."
exit 0
fi
fi
if [ "$build_image" == "build" ]; then
download "true"
else
download "false"
fi
}
function download() {
local LOCAL_BUILD=$1
cd $SCRIPT_DIR
TS=$(date +%s)
if [ -f "$PLANE_INSTALL_DIR/docker-compose.yaml" ]
then
mv $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml
fi
RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml
else
# Fallback to download from the raw github url
RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/docker-compose.yml?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml
else
echo "Failed to download docker-compose.yml. HTTP Status: $STATUS"
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml"
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml $PLANE_INSTALL_DIR/docker-compose.yaml
exit 1
fi
fi
RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
else
# Fallback to download from the raw github url
RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/variables.env?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
else
echo "Failed to download variables.env. HTTP Status: $STATUS"
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env"
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml $PLANE_INSTALL_DIR/docker-compose.yaml
exit 1
fi
fi
if [ -f "$DOCKER_ENV_PATH" ];
then
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/archive/$TS.env"
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/plane.env.bak"
fi
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
syncEnvFile
if [ "$LOCAL_BUILD" == "true" ]; then
export DOCKERHUB_USER="myplane"
export APP_RELEASE="local"
export PULL_POLICY="never"
CUSTOM_BUILD="true"
buildYourOwnImage
if [ $? -ne 0 ]; then
echo ""
echo "Build failed. Exiting..."
exit 1
fi
updateCustomVariables
else
CUSTOM_BUILD="false"
updateCustomVariables
runComposeCmd "pull --policy always"
if [ $? -ne 0 ]; then
echo ""
echo "Failed to pull the images. Exiting..."
exit 1
fi
fi
echo ""
echo "Most recent version of Plane is now available for you to use"
echo ""
echo "In case of 'Upgrade', please check the 'plane.env 'file for any new variables and update them accordingly"
echo ""
}
function startServices() {
runComposeCmd "up -d --pull if_not_present --quiet-pull"
local migrator_container_id=$(docker container ls -aq -f "name=$SERVICE_FOLDER-migrator")
if [ -n "$migrator_container_id" ]; then
local idx=0
while docker inspect --format='{{.State.Status}}' $migrator_container_id | grep -q "running"; do
local message=">> Waiting for Data Migration to finish"
local dots=$(printf '%*s' $idx | tr ' ' '.')
echo -ne "\r$message$dots"
((idx++))
sleep 1
done
fi
printf "\r\033[K"
echo ""
echo " Data Migration completed successfully ✅"
# if migrator exit status is not 0, show error message and exit
if [ -n "$migrator_container_id" ]; then
local migrator_exit_code=$(docker inspect --format='{{.State.ExitCode}}' $migrator_container_id)
if [ $migrator_exit_code -ne 0 ]; then
echo "Plane Server failed to start ❌"
# stopServices
echo
echo "Please check the logs for the 'migrator' service and resolve the issue(s)."
echo "Stop the services by running the command: ./setup.sh stop"
exit 1
fi
fi
local api_container_id=$(docker container ls -q -f "name=$SERVICE_FOLDER-api")
# Verify container exists
if [ -z "$api_container_id" ]; then
echo " Error: API container not found. Please check if services are running."
exit 1
fi
local idx2=0
local api_ready=true # assume success, flip on timeout
local max_wait_time=300 # 5 minutes timeout
local start_time=$(date +%s)
echo " Waiting for API Service to be ready..."
while ! docker exec "$api_container_id" python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/', timeout=3)" > /dev/null 2>&1; do
local current_time=$(date +%s)
local elapsed_time=$((current_time - start_time))
if [ $elapsed_time -gt $max_wait_time ]; then
echo ""
echo " API Service health check timed out after 5 minutes"
echo " Checking if API container is still running..."
if docker ps | grep -q "$SERVICE_FOLDER-api"; then
echo " API container is running but did not pass the health-check. Continuing without marking it ready."
api_ready=false
break
else
echo " API container is not running. Please check logs."
exit 1
fi
fi
local message=">> Waiting for API Service to Start (${elapsed_time}s)"
local dots=$(printf '%*s' $idx2 | tr ' ' '.')
echo -ne "\r$message$dots"
((idx2++))
sleep 1
done
printf "\r\033[K"
if [ "$api_ready" = true ]; then
echo " API Service started successfully ✅"
else
echo " ⚠️ API Service did not respond to health-check please verify manually."
fi
source "${DOCKER_ENV_PATH}"
echo " Plane Server started successfully ✅"
echo ""
echo " You can access the application at $WEB_URL"
echo ""
}
function stopServices() {
runComposeCmd "down"
}
function restartServices() {
stopServices
startServices
}
function upgrade() {
local latest_release=$(checkLatestRelease)
echo ""
echo "Current release: $APP_RELEASE"
if [ "$latest_release" == "$APP_RELEASE" ]; then
echo ""
echo "You are already using the latest release"
exit 0
fi
echo "Latest release: $latest_release"
echo ""
# Check for confirmation to upgrade
echo "Do you want to upgrade to the latest release ($latest_release)?"
read -p "Continue? [y/N]: " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Exiting..."
exit 0
fi
export APP_RELEASE=$latest_release
echo "Upgrading Plane to the latest release..."
echo ""
echo "***** STOPPING SERVICES ****"
stopServices
echo
echo "***** DOWNLOADING STABLE VERSION ****"
install
echo "***** PLEASE VALIDATE AND START SERVICES ****"
}
function viewSpecificLogs(){
local SERVICE_NAME=$1
if runComposeCmd "ps" | grep -q "$SERVICE_NAME"; then
echo "Service '$SERVICE_NAME' is running."
else
echo "Service '$SERVICE_NAME' is not running."
fi
runComposeCmd "logs -f $SERVICE_NAME"
}
function viewLogs(){
ARG_SERVICE_NAME=$2
if [ -z "$ARG_SERVICE_NAME" ];
then
echo
echo "Select a Service you want to view the logs for:"
echo " 1) Web"
echo " 2) Space"
echo " 3) API"
echo " 4) Worker"
echo " 5) Beat-Worker"
echo " 6) Migrator"
echo " 7) Proxy"
echo " 8) Redis"
echo " 9) Postgres"
echo " 10) Minio"
echo " 11) RabbitMQ"
echo " 0) Back to Main Menu"
echo
read -p "Service: " DOCKER_SERVICE_NAME
until (( DOCKER_SERVICE_NAME >= 0 && DOCKER_SERVICE_NAME <= 11 )); do
echo "Invalid selection. Please enter a number between 0 and 11."
read -p "Service: " DOCKER_SERVICE_NAME
done
if [ -z "$DOCKER_SERVICE_NAME" ];
then
echo "INVALID SERVICE NAME SUPPLIED"
else
case $DOCKER_SERVICE_NAME in
1) viewSpecificLogs "web";;
2) viewSpecificLogs "space";;
3) viewSpecificLogs "api";;
4) viewSpecificLogs "worker";;
5) viewSpecificLogs "beat-worker";;
6) viewSpecificLogs "migrator";;
7) viewSpecificLogs "proxy";;
8) viewSpecificLogs "plane-redis";;
9) viewSpecificLogs "plane-db";;
10) viewSpecificLogs "plane-minio";;
11) viewSpecificLogs "plane-mq";;
0) askForAction;;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
fi
elif [ -n "$ARG_SERVICE_NAME" ];
then
ARG_SERVICE_NAME=$(echo "$ARG_SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
case $ARG_SERVICE_NAME in
web) viewSpecificLogs "web";;
space) viewSpecificLogs "space";;
api) viewSpecificLogs "api";;
worker) viewSpecificLogs "worker";;
beat-worker) viewSpecificLogs "beat-worker";;
migrator) viewSpecificLogs "migrator";;
proxy) viewSpecificLogs "proxy";;
redis) viewSpecificLogs "plane-redis";;
postgres) viewSpecificLogs "plane-db";;
minio) viewSpecificLogs "plane-minio";;
rabbitmq) viewSpecificLogs "plane-mq";;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
else
echo "INVALID SERVICE NAME SUPPLIED"
fi
}
function backup_container_dir() {
local BACKUP_FOLDER=$1
local CONTAINER_NAME=$2
local CONTAINER_DATA_DIR=$3
local SERVICE_FOLDER=$4
echo "Backing up $CONTAINER_NAME data..."
local CONTAINER_ID=$(runComposeCmd "ps -q $CONTAINER_NAME")
if [ -z "$CONTAINER_ID" ]; then
echo "Error: $CONTAINER_NAME container not found. Make sure the services are running."
return 1
fi
# Create a temporary directory for the backup
mkdir -p "$BACKUP_FOLDER/$SERVICE_FOLDER"
# Copy the data directory from the running container
echo "Copying $CONTAINER_NAME data directory..."
docker cp -q "$CONTAINER_ID:$CONTAINER_DATA_DIR/." "$BACKUP_FOLDER/$SERVICE_FOLDER/"
local cp_status=$?
if [ $cp_status -ne 0 ]; then
echo "Error: Failed to copy $SERVICE_FOLDER data"
rm -rf $BACKUP_FOLDER/$SERVICE_FOLDER
return 1
fi
# Create tar.gz of the data
cd "$BACKUP_FOLDER"
tar -czf "${SERVICE_FOLDER}.tar.gz" "$SERVICE_FOLDER/"
local tar_status=$?
if [ $tar_status -eq 0 ]; then
rm -rf "$SERVICE_FOLDER/"
fi
cd - > /dev/null
if [ $tar_status -ne 0 ]; then
echo "Error: Failed to create tar archive"
return 1
fi
echo "Successfully backed up $SERVICE_FOLDER data"
}
function backupData() {
local datetime=$(date +"%Y%m%d-%H%M")
local BACKUP_FOLDER=$PLANE_INSTALL_DIR/backup/$datetime
mkdir -p "$BACKUP_FOLDER"
# Check if docker-compose.yml exists
if [ ! -f "$DOCKER_FILE_PATH" ]; then
echo "Error: docker-compose.yml not found at $DOCKER_FILE_PATH"
exit 1
fi
backup_container_dir "$BACKUP_FOLDER" "plane-db" "/var/lib/postgresql/data" "pgdata" || exit 1
backup_container_dir "$BACKUP_FOLDER" "plane-minio" "/export" "uploads" || exit 1
backup_container_dir "$BACKUP_FOLDER" "plane-mq" "/var/lib/rabbitmq" "rabbitmq_data" || exit 1
backup_container_dir "$BACKUP_FOLDER" "plane-redis" "/data" "redisdata" || exit 1
echo ""
echo "Backup completed successfully. Backup files are stored in $BACKUP_FOLDER"
echo ""
}
function askForAction() {
local DEFAULT_ACTION=$1
if [ -z "$DEFAULT_ACTION" ];
then
echo
echo "Select a Action you want to perform:"
echo " 1) Install"
echo " 2) Start"
echo " 3) Stop"
echo " 4) Restart"
echo " 5) Upgrade"
echo " 6) View Logs"
echo " 7) Backup Data"
echo " 8) Exit"
echo
read -p "Action [2]: " ACTION
until [[ -z "$ACTION" || "$ACTION" =~ ^[1-8]$ ]]; do
echo "$ACTION: invalid selection."
read -p "Action [2]: " ACTION
done
if [ -z "$ACTION" ];
then
ACTION=2
fi
echo
fi
if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "install" ];
then
install
# askForAction
elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "start" ];
then
startServices
# askForAction
elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "stop" ];
then
stopServices
# askForAction
elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "restart" ];
then
restartServices
# askForAction
elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ];
then
upgrade
# askForAction
elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ];
then
viewLogs "$@"
askForAction
elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "backup" ];
then
backupData
elif [ "$ACTION" == "8" ]
then
exit 0
else
echo "INVALID ACTION SUPPLIED"
fi
}
# if docker-compose is installed
if command -v docker-compose &> /dev/null
then
COMPOSE_CMD="docker-compose"
else
COMPOSE_CMD="docker compose"
fi
if [ "$CPU_ARCH" == "x86_64" ] || [ "$CPU_ARCH" == "amd64" ]; then
CPU_ARCH="amd64"
elif [ "$CPU_ARCH" == "aarch64" ] || [ "$CPU_ARCH" == "arm64" ]; then
CPU_ARCH="arm64"
fi
if [ -f "$DOCKER_ENV_PATH" ]; then
DOCKERHUB_USER=$(getEnvValue "DOCKERHUB_USER" "$DOCKER_ENV_PATH")
APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH")
PULL_POLICY=$(getEnvValue "PULL_POLICY" "$DOCKER_ENV_PATH")
CUSTOM_BUILD=$(getEnvValue "CUSTOM_BUILD" "$DOCKER_ENV_PATH")
if [ -z "$DOCKERHUB_USER" ]; then
DOCKERHUB_USER=makeplane
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
fi
if [ -z "$APP_RELEASE" ]; then
APP_RELEASE=stable
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
fi
if [ -z "$PULL_POLICY" ]; then
PULL_POLICY=if_not_present
updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
fi
if [ -z "$CUSTOM_BUILD" ]; then
CUSTOM_BUILD=false
updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
fi
fi
print_header
askForAction "$@"

Binary file not shown.

Binary file not shown.

45
handoff/README_HANDOFF.md Normal file
View File

@ -0,0 +1,45 @@
# Handoff
Исходный удаленный `origin/master` отстает и сейчас не содержит свежий хвост локальной работы.
Актуальное состояние для передачи:
- Рекомендуемая ветка: `push-fix-tail`
- Commit рекомендуемой ветки: `c18fa2b7e9ce9145b739725192ba750ac0d4462c`
- Локальный `master`: `9f81bc86f233d22efd2c54bf319bf61b0a7721a3`
- Удаленный `origin/master`: `c8a669e01ccbd732099798afee5242c8f3404391`
Почему две локальные ветки:
- `master` содержит исходный тяжелый хвост из 3 непушенных коммитов
- `push-fix-tail` содержит тот же итоговый код, но проблемный commit с обложками разрезан на мелкие commits для безопасной передачи
- Дерево `push-fix-tail` эквивалентно итоговому состоянию прежнего `master`
Файлы handoff:
- `NODEDC_TASKMANAGER-push-fix-tail.bundle` — git bundle с ветками `push-fix-tail` и `master`
- `image-picker-popover-uncommitted.patch` — незакоммиченный локальный diff для `plane-src/apps/web/core/components/core/image-picker-popover.tsx`
## Быстрое восстановление
```bash
git clone NODEDC_TASKMANAGER-push-fix-tail.bundle NODEDC_TASKMANAGER
cd NODEDC_TASKMANAGER
git checkout push-fix-tail
git apply ../image-picker-popover-uncommitted.patch
```
## Если нужно проверить refs в bundle
```bash
git bundle list-heads NODEDC_TASKMANAGER-push-fix-tail.bundle
```
Ожидаемые refs:
- `refs/heads/push-fix-tail` -> `c18fa2b7e9ce9145b739725192ba750ac0d4462c`
- `refs/heads/master` -> `9f81bc86f233d22efd2c54bf319bf61b0a7721a3`
## Что брать в работу
Новому специалисту нужно брать в работу именно `push-fix-tail`, а не удаленный `origin/master`.

View File

@ -0,0 +1,130 @@
diff --git a/plane-src/apps/web/core/components/core/image-picker-popover.tsx b/plane-src/apps/web/core/components/core/image-picker-popover.tsx
index 431143a..0927b6e 100644
--- a/plane-src/apps/web/core/components/core/image-picker-popover.tsx
+++ b/plane-src/apps/web/core/components/core/image-picker-popover.tsx
@@ -12,6 +12,7 @@ import type { Control } from "react-hook-form";
import { Controller } from "react-hook-form";
import useSWR from "swr";
import { Popover } from "@headlessui/react";
+import { Check, UploadCloud } from "lucide-react";
// plane imports
import { ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
@@ -113,6 +114,7 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
);
const imagePickerRef = useRef<HTMLDivElement>(null);
+ const selectedCoverImageUrl = value ? getCoverImageDisplayURL(value, null) : null;
const onDrop = useCallback((acceptedFiles: File[]) => {
setImage(acceptedFiles[0]);
@@ -215,17 +217,17 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
{isOpen && (
<Popover.Panel
- className="nodedc-glass-modal nodedc-glass-popup-surface absolute right-0 z-20 mt-3 overflow-hidden rounded-[1.75rem]"
+ className="nodedc-glass-modal nodedc-glass-popup-surface absolute right-0 z-20 mt-3 overflow-hidden rounded-[1.9rem]"
static
>
<div
ref={imagePickerRef}
- className="flex h-[32rem] w-[21rem] flex-col overflow-hidden rounded-[1.75rem] md:h-[38rem] md:w-[38rem]"
+ className="nodedc-cover-picker flex h-[33rem] w-[21.5rem] flex-col overflow-hidden rounded-[1.9rem] md:h-[39rem] md:w-[42rem]"
>
<Tabs defaultValue={enabledTabs[0]?.key || "images"} className="flex h-full flex-col px-4 pt-4 pb-3">
- <Tabs.List className="rounded-[1rem] bg-layer-3/80 p-1">
+ <Tabs.List className="nodedc-cover-picker-tabs">
{enabledTabs.map((tab) => (
- <Tabs.Trigger key={tab.key} value={tab.key} size="md" className="rounded-[0.875rem]">
+ <Tabs.Trigger key={tab.key} value={tab.key} size="md" className="nodedc-cover-picker-tab">
{tab.title}
</Tabs.Trigger>
))}
@@ -273,7 +275,7 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
{unsplashImages.map((image) => (
<div
key={image.id}
- className="nodedc-modal-field relative col-span-2 aspect-video overflow-hidden rounded-[1rem] p-0 md:col-span-1"
+ className="nodedc-cover-picker-tile group relative col-span-2 aspect-video overflow-hidden rounded-[1.2rem] p-0 md:col-span-1"
onClick={() => {
setIsOpen(false);
onChange(image.urls.regular);
@@ -282,7 +284,7 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
<img
src={image.urls.small}
alt={image.alt_description}
- className="absolute inset-0 h-full w-full cursor-pointer rounded-[1rem] object-cover transition-transform duration-200 hover:scale-[1.02]"
+ className="absolute inset-0 h-full w-full cursor-pointer rounded-[1.2rem] object-cover transition-transform duration-200 group-hover:scale-[1.02]"
/>
</div>
))}
@@ -308,17 +310,25 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
<Tabs.Content value="images" className="h-full w-full space-y-4">
<div className="grid grid-cols-4 gap-3">
{Object.values(STATIC_COVER_IMAGES).map((imageUrl, index) => (
- <div
+ <button
key={imageUrl}
- className="nodedc-modal-field relative col-span-2 aspect-video overflow-hidden rounded-[1rem] p-0 md:col-span-1"
+ type="button"
+ className="nodedc-cover-picker-tile group relative col-span-2 aspect-video overflow-hidden rounded-[1.2rem] p-0 text-left md:col-span-1"
+ data-selected={selectedCoverImageUrl === imageUrl}
onClick={() => handleStaticImageSelect(imageUrl)}
>
<img
src={imageUrl}
alt={t("image_picker.cover_image_alt", { index: index + 1 })}
- className="absolute inset-0 h-full w-full cursor-pointer rounded-[1rem] object-cover transition-transform duration-200 hover:scale-[1.02]"
+ className="absolute inset-0 h-full w-full cursor-pointer rounded-[1.2rem] object-cover transition-transform duration-200 group-hover:scale-[1.02]"
/>
- </div>
+ <div className="absolute inset-0 rounded-[1.2rem] bg-black/0 transition-colors duration-200 group-hover:bg-black/8" />
+ {selectedCoverImageUrl === imageUrl && (
+ <div className="absolute top-3 right-3 grid h-8 w-8 place-items-center rounded-full bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))] shadow-[0_10px_24px_rgba(0,0,0,0.28)]">
+ <Check className="h-4 w-4" strokeWidth={2.4} />
+ </div>
+ )}
+ </button>
))}
</div>
</Tabs.Content>
@@ -327,9 +337,9 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
<div className="flex w-full flex-1 items-center gap-3">
<div
{...getRootProps()}
- className={`nodedc-modal-field relative grid h-full w-full cursor-pointer place-items-center overflow-hidden rounded-[1.35rem] p-6 text-center ${
+ className={`nodedc-cover-picker-upload relative grid h-full w-full cursor-pointer place-items-center overflow-hidden rounded-[1.35rem] p-6 text-center ${
(image === null && isDragActive) || !value
- ? "border-2 border-dashed border-subtle/80 hover:bg-white/6"
+ ? "border-2 border-dashed border-white/10 hover:bg-white/6"
: ""
}`}
>
@@ -348,10 +358,16 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
/>
</>
) : (
- <div>
- <span className="mt-2 block text-13 font-medium text-secondary">
+ <div className="flex max-w-[18rem] flex-col items-center gap-3">
+ <div className="grid h-12 w-12 place-items-center rounded-full bg-white/6 text-secondary">
+ <UploadCloud className="h-5 w-5" />
+ </div>
+ <span className="block text-13 font-medium text-secondary">
{isDragActive ? t("image_picker.drop_here_to_upload") : t("image_picker.drag_and_drop_here")}
</span>
+ <span className="text-12 text-tertiary">
+ {t("image_picker.supported_formats")}
+ </span>
</div>
)}
@@ -368,7 +384,7 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
<p className="text-13 text-secondary">{t("image_picker.supported_formats")}</p>
- <div className="mt-auto flex items-start justify-end gap-3 border-t border-subtle/70 pt-4">
+ <div className="nodedc-cover-picker-footer mt-auto flex items-start justify-end gap-3 border-t border-subtle/70 pt-4">
<Button
variant="secondary"
onClick={() => {

View File

@ -12,6 +12,7 @@ import type { Control } from "react-hook-form";
import { Controller } from "react-hook-form";
import useSWR from "swr";
import { Popover } from "@headlessui/react";
import { Check, UploadCloud } from "lucide-react";
// plane imports
import { ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
@ -113,6 +114,7 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
);
const imagePickerRef = useRef<HTMLDivElement>(null);
const selectedCoverImageUrl = value ? getCoverImageDisplayURL(value, null) : null;
const onDrop = useCallback((acceptedFiles: File[]) => {
setImage(acceptedFiles[0]);
@ -215,17 +217,17 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
{isOpen && (
<Popover.Panel
className="nodedc-glass-modal nodedc-glass-popup-surface absolute right-0 z-20 mt-3 overflow-hidden rounded-[1.75rem]"
className="nodedc-glass-modal nodedc-glass-popup-surface absolute right-0 z-20 mt-3 overflow-hidden rounded-[1.9rem]"
static
>
<div
ref={imagePickerRef}
className="flex h-[32rem] w-[21rem] flex-col overflow-hidden rounded-[1.75rem] md:h-[38rem] md:w-[38rem]"
className="nodedc-cover-picker flex h-[33rem] w-[21.5rem] flex-col overflow-hidden rounded-[1.9rem] md:h-[39rem] md:w-[42rem]"
>
<Tabs defaultValue={enabledTabs[0]?.key || "images"} className="flex h-full flex-col px-4 pt-4 pb-3">
<Tabs.List className="rounded-[1rem] bg-layer-3/80 p-1">
<Tabs.List className="nodedc-cover-picker-tabs">
{enabledTabs.map((tab) => (
<Tabs.Trigger key={tab.key} value={tab.key} size="md" className="rounded-[0.875rem]">
<Tabs.Trigger key={tab.key} value={tab.key} size="md" className="nodedc-cover-picker-tab">
{tab.title}
</Tabs.Trigger>
))}
@ -273,7 +275,7 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
{unsplashImages.map((image) => (
<div
key={image.id}
className="nodedc-modal-field relative col-span-2 aspect-video overflow-hidden rounded-[1rem] p-0 md:col-span-1"
className="nodedc-cover-picker-tile group relative col-span-2 aspect-video overflow-hidden rounded-[1.2rem] p-0 md:col-span-1"
onClick={() => {
setIsOpen(false);
onChange(image.urls.regular);
@ -282,7 +284,7 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
<img
src={image.urls.small}
alt={image.alt_description}
className="absolute inset-0 h-full w-full cursor-pointer rounded-[1rem] object-cover transition-transform duration-200 hover:scale-[1.02]"
className="absolute inset-0 h-full w-full cursor-pointer rounded-[1.2rem] object-cover transition-transform duration-200 group-hover:scale-[1.02]"
/>
</div>
))}
@ -308,17 +310,25 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
<Tabs.Content value="images" className="h-full w-full space-y-4">
<div className="grid grid-cols-4 gap-3">
{Object.values(STATIC_COVER_IMAGES).map((imageUrl, index) => (
<div
<button
key={imageUrl}
className="nodedc-modal-field relative col-span-2 aspect-video overflow-hidden rounded-[1rem] p-0 md:col-span-1"
type="button"
className="nodedc-cover-picker-tile group relative col-span-2 aspect-video overflow-hidden rounded-[1.2rem] p-0 text-left md:col-span-1"
data-selected={selectedCoverImageUrl === imageUrl}
onClick={() => handleStaticImageSelect(imageUrl)}
>
<img
src={imageUrl}
alt={t("image_picker.cover_image_alt", { index: index + 1 })}
className="absolute inset-0 h-full w-full cursor-pointer rounded-[1rem] object-cover transition-transform duration-200 hover:scale-[1.02]"
className="absolute inset-0 h-full w-full cursor-pointer rounded-[1.2rem] object-cover transition-transform duration-200 group-hover:scale-[1.02]"
/>
<div className="absolute inset-0 rounded-[1.2rem] bg-black/0 transition-colors duration-200 group-hover:bg-black/8" />
{selectedCoverImageUrl === imageUrl && (
<div className="absolute top-3 right-3 grid h-8 w-8 place-items-center rounded-full bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))] shadow-[0_10px_24px_rgba(0,0,0,0.28)]">
<Check className="h-4 w-4" strokeWidth={2.4} />
</div>
)}
</button>
))}
</div>
</Tabs.Content>
@ -327,9 +337,9 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
<div className="flex w-full flex-1 items-center gap-3">
<div
{...getRootProps()}
className={`nodedc-modal-field relative grid h-full w-full cursor-pointer place-items-center overflow-hidden rounded-[1.35rem] p-6 text-center ${
className={`nodedc-cover-picker-upload relative grid h-full w-full cursor-pointer place-items-center overflow-hidden rounded-[1.35rem] p-6 text-center ${
(image === null && isDragActive) || !value
? "border-2 border-dashed border-subtle/80 hover:bg-white/6"
? "border-2 border-dashed border-white/10 hover:bg-white/6"
: ""
}`}
>
@ -348,10 +358,16 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
/>
</>
) : (
<div>
<span className="mt-2 block text-13 font-medium text-secondary">
<div className="flex max-w-[18rem] flex-col items-center gap-3">
<div className="grid h-12 w-12 place-items-center rounded-full bg-white/6 text-secondary">
<UploadCloud className="h-5 w-5" />
</div>
<span className="block text-13 font-medium text-secondary">
{isDragActive ? t("image_picker.drop_here_to_upload") : t("image_picker.drag_and_drop_here")}
</span>
<span className="text-12 text-tertiary">
{t("image_picker.supported_formats")}
</span>
</div>
)}
@ -368,7 +384,7 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
<p className="text-13 text-secondary">{t("image_picker.supported_formats")}</p>
<div className="mt-auto flex items-start justify-end gap-3 border-t border-subtle/70 pt-4">
<div className="nodedc-cover-picker-footer mt-auto flex items-start justify-end gap-3 border-t border-subtle/70 pt-4">
<Button
variant="secondary"
onClick={() => {