Merge pull request #179 from makeplane/preview

release: UI changes and Bugfixes
This commit is contained in:
sriram veeraghanta
2024-04-26 20:39:39 +05:30
committed by GitHub
27 changed files with 717 additions and 201 deletions

View File

@@ -18,17 +18,17 @@ jobs:
name: Build-Push Web/Space/API/Proxy Docker Image
runs-on: ubuntu-latest
outputs:
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
build_frontend: ${{ steps.changed_files.outputs.frontend_any_changed }}
build_web: ${{ steps.changed_files.outputs.web_any_changed }}
build_space: ${{ steps.changed_files.outputs.space_any_changed }}
build_backend: ${{ steps.changed_files.outputs.backend_any_changed }}
build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }}
docker_upload_to_s3: ${{ steps.set_env_variables.outputs.docker_upload_to_s3 }}
docker_tar_suffix: ${{ steps.set_env_variables.outputs.docker_tar_suffix }}
artifact_upload_to_s3: ${{ steps.set_env_variables.outputs.artifact_upload_to_s3 }}
artifact_s3_suffix: ${{ steps.set_env_variables.outputs.artifact_s3_suffix }}
steps:
- id: set_env_variables
@@ -45,17 +45,21 @@ jobs:
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
fi
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
BR_NAME=$( echo "${{ env.TARGET_BRANCH }}" | tr / -)
echo "TARGET_BRANCH=$BR_NAME" >> $GITHUB_OUTPUT
if [ "${{ github.event_name }}" == "release" ]; then
echo "docker_upload_to_s3=true" >> $GITHUB_OUTPUT
echo "docker_tar_suffix=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
echo "artifact_upload_to_s3=true" >> $GITHUB_OUTPUT
echo "artifact_s3_suffix=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
echo "docker_upload_to_s3=true" >> $GITHUB_OUTPUT
echo "docker_tar_suffix=latest" >> $GITHUB_OUTPUT
echo "artifact_upload_to_s3=true" >> $GITHUB_OUTPUT
echo "artifact_s3_suffix=latest" >> $GITHUB_OUTPUT
elif [ "${{ env.TARGET_BRANCH }}" == "preview" ] || [ "${{ env.TARGET_BRANCH }}" == "develop" ]; then
echo "artifact_upload_to_s3=true" >> $GITHUB_OUTPUT
echo "artifact_s3_suffix=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
else
echo "docker_upload_to_s3=false" >> $GITHUB_OUTPUT
echo "docker_tar_suffix=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
echo "artifact_upload_to_s3=false" >> $GITHUB_OUTPUT
echo "artifact_s3_suffix=$BR_NAME" >> $GITHUB_OUTPUT
fi
- id: checkout_files
@@ -67,7 +71,7 @@ jobs:
uses: tj-actions/changed-files@v42
with:
files_yaml: |
frontend:
web:
- web/**
- packages/**
- 'package.json'
@@ -86,28 +90,33 @@ jobs:
proxy:
- nginx/**
branch_build_push_frontend:
if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
branch_build_push_web:
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-ee:${{ needs.branch_build_setup.outputs.gh_branch_name }}
WEB_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/web-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
BUILDX_PLATFORMS: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
steps:
- name: Set Frontend Docker Tag
- name: Set Web Docker Tag
run: |
if [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-ee:stable,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-ee:${{ github.event.release.tag_name }}
TAG=${{ secrets.DOCKERHUB_USERNAME }}/web-enterprise:stable
TAG=${TAG},${{ secrets.DOCKERHUB_USERNAME }}/web-enterprise:${{ github.event.release.tag_name }}
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/web-enterprise:stable
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/web-enterprise:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-ee:latest
TAG=${{ secrets.DOCKERHUB_USERNAME }}/web-enterprise:latest
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/web-enterprise:latest
else
TAG=${{ env.FRONTEND_TAG }}
TAG=${{ env.WEB_TAG }}
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/web-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }}
fi
echo "FRONTEND_TAG=${TAG}" >> $GITHUB_ENV
echo "WEB_TAG=${TAG}" >> $GITHUB_ENV
- name: Login to Docker Hub
uses: docker/login-action@v3
@@ -115,6 +124,13 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Harbor
uses: docker/login-action@v3
with:
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_TOKEN }}
registry: ${{ vars.HARBOR_REGISTRY }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
@@ -125,13 +141,13 @@ jobs:
- name: Check out the repo
uses: actions/checkout@v4
- name: Build and Push Frontend to Docker Container Registry
- name: Build and Push Web to Docker Container Registry
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./web/Dockerfile.web
platforms: ${{ env.BUILDX_PLATFORMS }}
tags: ${{ env.FRONTEND_TAG }}
tags: ${{ env.WEB_TAG }}
push: true
env:
DOCKER_BUILDKIT: 1
@@ -139,11 +155,11 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_space:
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space-ee:${{ needs.branch_build_setup.outputs.gh_branch_name }}
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/space-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
@@ -153,11 +169,16 @@ jobs:
- name: Set Space Docker Tag
run: |
if [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space-ee:stable,${{ secrets.DOCKERHUB_USERNAME }}/plane-space-ee:${{ github.event.release.tag_name }}
TAG=${{ secrets.DOCKERHUB_USERNAME }}/space-enterprise:stable
TAG=${TAG},${{ secrets.DOCKERHUB_USERNAME }}/space-enterprise:${{ github.event.release.tag_name }}
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/space-enterprise:stable
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/space-enterprise:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space-ee:latest
TAG=${{ secrets.DOCKERHUB_USERNAME }}/space-enterprise:latest
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/space-enterprise:latest
else
TAG=${{ env.SPACE_TAG }}
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/space-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }}
fi
echo "SPACE_TAG=${TAG}" >> $GITHUB_ENV
@@ -167,6 +188,13 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Harbor
uses: docker/login-action@v3
with:
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_TOKEN }}
registry: ${{ vars.HARBOR_REGISTRY }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
@@ -191,11 +219,11 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_backend:
if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-ee:${{ needs.branch_build_setup.outputs.gh_branch_name }}
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/backend-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
@@ -205,11 +233,16 @@ jobs:
- name: Set Backend Docker Tag
run: |
if [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-ee:stable,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-ee:${{ github.event.release.tag_name }}
TAG=${{ secrets.DOCKERHUB_USERNAME }}/backend-enterprise:stable
TAG=${TAG},${{ secrets.DOCKERHUB_USERNAME }}/backend-enterprise:${{ github.event.release.tag_name }}
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/backend-enterprise:stable
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/backend-enterprise:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-ee:latest
TAG=${{ secrets.DOCKERHUB_USERNAME }}/backend-enterprise:latest
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/backend-enterprise:latest
else
TAG=${{ env.BACKEND_TAG }}
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/backend-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }}
fi
echo "BACKEND_TAG=${TAG}" >> $GITHUB_ENV
@@ -219,6 +252,13 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Harbor
uses: docker/login-action@v3
with:
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_TOKEN }}
registry: ${{ vars.HARBOR_REGISTRY }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
@@ -243,11 +283,11 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_proxy:
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-ee:${{ needs.branch_build_setup.outputs.gh_branch_name }}
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/proxy-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
@@ -257,11 +297,16 @@ jobs:
- name: Set Proxy Docker Tag
run: |
if [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-ee:stable,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-ee:${{ github.event.release.tag_name }}
TAG=${{ secrets.DOCKERHUB_USERNAME }}/proxy-enterprise:stable
TAG=${TAG},${{ secrets.DOCKERHUB_USERNAME }}/proxy-enterprise:${{ github.event.release.tag_name }}
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/proxy-enterprise:stable
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/proxy-enterprise:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-ee:latest
TAG=${{ secrets.DOCKERHUB_USERNAME }}/proxy-enterprise:latest
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/proxy-enterprise:latest
else
TAG=${{ env.PROXY_TAG }}
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/proxy-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }}
fi
echo "PROXY_TAG=${TAG}" >> $GITHUB_ENV
@@ -271,6 +316,13 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Harbor
uses: docker/login-action@v3
with:
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_TOKEN }}
registry: ${{ vars.HARBOR_REGISTRY }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
@@ -294,47 +346,45 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
convert_docker_to_tar:
if: ${{ needs.branch_build_setup.outputs.docker_upload_to_s3 == 'true' }}
name: Convert Docker TAR to S3 Bucket
upload_artifacts_s3:
if: ${{ needs.branch_build_setup.outputs.artifact_upload_to_s3 == 'true' }}
name: Upload artifacts to S3 Bucket
runs-on: ubuntu-latest
needs: [branch_build_setup, branch_build_push_frontend, branch_build_push_space, branch_build_push_backend, branch_build_push_proxy]
needs:
[
branch_build_setup,
branch_build_push_web,
branch_build_push_space,
branch_build_push_backend,
branch_build_push_proxy,
]
container:
image: docker:20.10.7
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
TAR_SUFFIX: ${{ needs.branch_build_setup.outputs.docker_tar_suffix }}
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-ee:${{ needs.branch_build_setup.outputs.docker_tar_suffix }}
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space-ee:${{ needs.branch_build_setup.outputs.docker_tar_suffix }}
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-ee:${{ needs.branch_build_setup.outputs.docker_tar_suffix }}
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-ee:${{ needs.branch_build_setup.outputs.docker_tar_suffix }}
AWS_ACCESS_KEY_ID: ${{ secrets.PLANE_DOCKER_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.PLANE_DOCKER_SECRET_ACCESS_KEY }}
ARTIFACT_SUFFIX: ${{ needs.branch_build_setup.outputs.artifact_s3_suffix }}
AWS_ACCESS_KEY_ID: ${{ secrets.SELF_HOST_BUCKET_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SELF_HOST_BUCKET_SECRET_KEY }}
TARGET_BRANCH: ${{ github.ref_name || github.event.release.target_commitish }}
steps:
- name: Save Image as Tar
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Upload artifacts
run: |
apk update
apk add --no-cache aws-cli
mkdir -p ~/${{ env.TAR_SUFFIX }}
mkdir -p ~/${{ env.ARTIFACT_SUFFIX }}
echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
cp deploy/cli-install/variables.env ~/${{ env.ARTIFACT_SUFFIX }}/variables.env
cp deploy/cli-install/Caddyfile ~/${{ env.ARTIFACT_SUFFIX }}/Caddyfile
sed -e 's@${APP_RELEASE_VERSION}@'${{ env.ARTIFACT_SUFFIX }}'@' deploy/cli-install/docker-compose.yml > ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose.yml
sed -e 's@${APP_RELEASE_VERSION}@'${{ env.ARTIFACT_SUFFIX }}'@' deploy/cli-install/docker-compose-caddy.yml > ~/${{ env.ARTIFACT_SUFFIX }}/docker-compose-caddy.yml
docker pull ${{ env.FRONTEND_TAG }}
docker save -o ~/${{ env.TAR_SUFFIX }}/plane-frontend-ee.tar ${{ env.FRONTEND_TAG }}
aws s3 cp ~/${{ env.ARTIFACT_SUFFIX }} s3://${{ vars.SELF_HOST_BUCKET_NAME }}/plane-enterprise/${{ env.ARTIFACT_SUFFIX }} --recursive
docker pull ${{ env.SPACE_TAG }}
docker save -o ~/${{ env.TAR_SUFFIX }}/plane-space-ee.tar ${{ env.SPACE_TAG }}
docker pull ${{ env.BACKEND_TAG }}
docker save -o ~/${{ env.TAR_SUFFIX }}/plane-backend-ee.tar ${{ env.BACKEND_TAG }}
docker pull ${{ env.PROXY_TAG }}
docker save -o ~/${{ env.TAR_SUFFIX }}/plane-proxy-ee.tar ${{ env.PROXY_TAG }}
aws s3 cp ~/${{ env.TAR_SUFFIX }} s3://${{ secrets.PLANE_DOCKER_BUCKET }}/plane-ee/${{ env.TAR_SUFFIX }} --recursive
rm -rf ~/${{ env.TAR_SUFFIX }}
rm -rf ~/${{ env.ARTIFACT_SUFFIX }}

View File

@@ -0,0 +1,18 @@
{
email {$CERT_EMAIL}
{$CERT_ACME_DNS}
}
{$APP_PROTOCOL}://{$DOMAIN_NAME} {
request_body {
max_size {$FILE_SIZE_LIMIT}
}
reverse_proxy /spaces/* space:3000
reverse_proxy /api/* api:8000
reverse_proxy /{$BUCKET_NAME}/* plane-minio:9000
reverse_proxy /* web:3000
}

View File

@@ -0,0 +1,161 @@
x-proxy-env: &proxy-env
environment:
- DOMAIN_NAME=${DOMAIN_NAME:-localhost}
- CERT_EMAIL=${CERT_EMAIL:-admin@localhost}
- APP_PROTOCOL=${APP_PROTOCOL:-http}
- CERT_ACME_DNS=${CERT_ACME_DNS:-}
- BUCKET_NAME=${BUCKET_NAME:-uploads}
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
- LISTEN_HTTP_PORT=${LISTEN_HTTP_PORT:-80}
- LISTEN_HTTPS_PORT=${LISTEN_HTTPS_PORT:-443}
x-app-env: &app-env
environment:
- NGINX_PORT=${NGINX_PORT:-80}
- WEB_URL=${WEB_URL:-http://localhost}
- DEBUG=${DEBUG:-0}
- SENTRY_DSN=${SENTRY_DSN:-""}
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-}
# Gunicorn Workers
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-2}
#DB SETTINGS
- PGHOST=${PGHOST:-plane-db}
- PGDATABASE=${PGDATABASE:-plane}
- POSTGRES_USER=${POSTGRES_USER:-plane}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-plane}
- POSTGRES_DB=${POSTGRES_DB:-plane}
- PGDATA=${PGDATA:-/var/lib/postgresql/data}
- DATABASE_URL=${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
# REDIS SETTINGS
- REDIS_HOST=${REDIS_HOST:-plane-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_URL=${REDIS_URL:-redis://plane-redis:6379/}
# Application secret
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
# DATA STORE SETTINGS
- USE_MINIO=${USE_MINIO:-1}
- 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}
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-"access-key"}
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-"secret-key"}
- BUCKET_NAME=${BUCKET_NAME:-uploads}
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
services:
web:
<<: *app-env
image: registry.plane.tools/plane-enterprise/web-enterprise:${APP_RELEASE_VERSION}
restart: unless-stopped
command: /usr/local/bin/start.sh web/server.js web
deploy:
replicas: ${WEB_REPLICAS:-1}
depends_on:
- api
- worker
space:
<<: *app-env
image: registry.plane.tools/plane-enterprise/space-enterprise:${APP_RELEASE_VERSION}
restart: unless-stopped
command: /usr/local/bin/start.sh space/server.js space
deploy:
replicas: ${SPACE_REPLICAS:-1}
depends_on:
- api
- worker
- web
api:
<<: *app-env
image: registry.plane.tools/plane-enterprise/backend-enterprise:${APP_RELEASE_VERSION}
restart: unless-stopped
command: ./bin/takeoff
deploy:
replicas: ${API_REPLICAS:-1}
# volumes:
# - ${INSTALL_DIR}/logs/api:/code/plane/logs
depends_on:
- plane-db
- plane-redis
worker:
<<: *app-env
image: registry.plane.tools/plane-enterprise/backend-enterprise:${APP_RELEASE_VERSION}
restart: unless-stopped
command: ./bin/worker
# volumes:
# - ${INSTALL_DIR}/logs/worker:/code/plane/logs
depends_on:
- api
- plane-db
- plane-redis
beat-worker:
<<: *app-env
image: registry.plane.tools/plane-enterprise/backend-enterprise:${APP_RELEASE_VERSION}
restart: unless-stopped
command: ./bin/beat
# volumes:
# - ${INSTALL_DIR}/logs/beat-worker:/code/plane/logs
depends_on:
- api
- plane-db
- plane-redis
migrator:
<<: *app-env
image: registry.plane.tools/plane-enterprise/backend-enterprise:${APP_RELEASE_VERSION}
restart: no
command: >
sh -c "python manage.py wait_for_db &&
python manage.py migrate"
# volumes:
# - ${INSTALL_DIR}/logs/migrator:/code/plane/logs
depends_on:
- plane-db
- plane-redis
plane-db:
<<: *app-env
image: postgres:15.5-alpine
restart: unless-stopped
command: postgres -c 'max_connections=1000'
volumes:
- ${INSTALL_DIR}/data/db:/var/lib/postgresql/data
plane-redis:
<<: *app-env
image: redis:7.2.4-alpine
restart: unless-stopped
volumes:
- ${INSTALL_DIR}/data/redis:/data
plane-minio:
<<: *app-env
image: minio/minio:latest
restart: unless-stopped
command: server /export --console-address ":9090"
volumes:
- ${INSTALL_DIR}/data/minio/uploads:/export
- ${INSTALL_DIR}/data/minio/data:/data
# Comment this if you already have a reverse proxy running
proxy:
<<: *proxy-env
image: makeplane/caddy:latest
restart: unless-stopped
ports:
- ${LISTEN_HTTP_PORT:-80}:80
- ${LISTEN_HTTPS_PORT:-443}:443
volumes:
- ${INSTALL_DIR}/Caddyfile:/etc/caddy/Caddyfile
- ${INSTALL_DIR}/caddy/config:/config
- ${INSTALL_DIR}/caddy/data:/data
depends_on:
- web
- api
- space

View File

@@ -0,0 +1,156 @@
# version: "3.8"
x-app-env: &app-env
environment:
- NGINX_PORT=${NGINX_PORT:-80}
- WEB_URL=${WEB_URL:-http://localhost}
- DEBUG=${DEBUG:-0}
- SENTRY_DSN=${SENTRY_DSN:-""}
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-}
# Gunicorn Workers
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-2}
#DB SETTINGS
- PGHOST=${PGHOST:-plane-db}
- PGDATABASE=${PGDATABASE:-plane}
- POSTGRES_USER=${POSTGRES_USER:-plane}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-plane}
- POSTGRES_DB=${POSTGRES_DB:-plane}
- PGDATA=${PGDATA:-/var/lib/postgresql/data}
- DATABASE_URL=${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
# REDIS SETTINGS
- REDIS_HOST=${REDIS_HOST:-plane-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_URL=${REDIS_URL:-redis://plane-redis:6379/}
# Application secret
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
# DATA STORE SETTINGS
- USE_MINIO=${USE_MINIO:-1}
- 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}
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-"access-key"}
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-"secret-key"}
- BUCKET_NAME=${BUCKET_NAME:-uploads}
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
services:
web:
<<: *app-env
image: registry.plane.tools/plane-enterprise/web-enterprise:${APP_RELEASE_VERSION}
pull_policy: if_not_present
restart: unless-stopped
command: /usr/local/bin/start.sh web/server.js web
deploy:
replicas: ${WEB_REPLICAS:-1}
depends_on:
- api
- worker
space:
<<: *app-env
image: registry.plane.tools/plane-enterprise/space-enterprise:${APP_RELEASE_VERSION}
pull_policy: if_not_present
restart: unless-stopped
command: /usr/local/bin/start.sh space/server.js space
deploy:
replicas: ${SPACE_REPLICAS:-1}
depends_on:
- api
- worker
- web
api:
<<: *app-env
image: registry.plane.tools/plane-enterprise/backend-enterprise:${APP_RELEASE_VERSION}
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/takeoff
deploy:
replicas: ${API_REPLICAS:-1}
# volumes:
# - ${INSTALL_DIR}/logs/api:/code/plane/logs
depends_on:
- plane-db
- plane-redis
worker:
<<: *app-env
image: registry.plane.tools/plane-enterprise/backend-enterprise:${APP_RELEASE_VERSION}
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/worker
# volumes:
# - ${INSTALL_DIR}/logs/worker:/code/plane/logs
depends_on:
- api
- plane-db
- plane-redis
beat-worker:
<<: *app-env
image: registry.plane.tools/plane-enterprise/backend-enterprise:${APP_RELEASE_VERSION}
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/beat
# volumes:
# - ${INSTALL_DIR}/logs/beat-worker:/code/plane/logs
depends_on:
- api
- plane-db
- plane-redis
migrator:
<<: *app-env
image: registry.plane.tools/plane-enterprise/backend-enterprise:${APP_RELEASE_VERSION}
pull_policy: if_not_present
restart: no
command: >
sh -c "python manage.py wait_for_db &&
python manage.py migrate"
# volumes:
# - ${INSTALL_DIR}/logs/migrator:/code/plane/logs
depends_on:
- plane-db
- plane-redis
plane-db:
<<: *app-env
image: postgres:15.5-alpine
pull_policy: if_not_present
restart: unless-stopped
command: postgres -c 'max_connections=1000'
volumes:
- ${INSTALL_DIR}/data/db:/var/lib/postgresql/data
plane-redis:
<<: *app-env
image: redis:7.2.4-alpine
pull_policy: if_not_present
restart: unless-stopped
volumes:
- ${INSTALL_DIR}/data/redis:/data
plane-minio:
<<: *app-env
image: minio/minio:latest
pull_policy: if_not_present
restart: unless-stopped
command: server /export --console-address ":9090"
volumes:
- ${INSTALL_DIR}/data/minio/uploads:/export
- ${INSTALL_DIR}/data/minio/data:/data
# Comment this if you already have a reverse proxy running
proxy:
<<: *app-env
image: registry.plane.tools/plane-enterprise/proxy-enterprise:${APP_RELEASE_VERSION}
pull_policy: if_not_present
restart: unless-stopped
ports:
- ${NGINX_PORT}:80
depends_on:
- web
- api
- space

View File

@@ -0,0 +1,56 @@
INSTALL_DIR=/opt/plane
WEB_REPLICAS=1
SPACE_REPLICAS=1
API_REPLICAS=1
NGINX_PORT=80
LISTEN_HTTP_PORT=80
LISTEN_HTTPS_PORT=443
APP_PROTOCOL=http
# If SSL Cert to be generated, set CERT_EMAIL and APP_PROTOCOL to https
CERT_EMAIL=admin@localhost
# For DNS Challenge based certificate generation, set the CERT_ACME_DNS
# CERT_ACME_DNS=acme_dns CERT_DNS_PROVIDER CERT_DNS_PROVIDER_API_KEY
CERT_ACME_DNS=
WEB_URL=http://localhost
DEBUG=0
SENTRY_DSN=
SENTRY_ENVIRONMENT=production
CORS_ALLOWED_ORIGINS=http://localhost
#DB SETTINGS
PGHOST=plane-db
PGDATABASE=plane
POSTGRES_USER=plane
POSTGRES_PASSWORD=plane
POSTGRES_DB=plane
PGDATA=/var/lib/postgresql/data
DATABASE_URL=
# REDIS SETTINGS
REDIS_HOST=plane-redis
REDIS_PORT=6379
REDIS_URL=
# 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
MINIO_ROOT_USER=access-key
MINIO_ROOT_PASSWORD=secret-key
BUCKET_NAME=uploads
FILE_SIZE_LIMIT=5242880
# Gunicorn Workers
GUNICORN_WORKERS=2

View File

@@ -15,7 +15,7 @@ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>((props, re
// refs
const textAreaRef = useRef<any>(ref);
// auto re-size
useAutoResizeTextArea(textAreaRef);
useAutoResizeTextArea(textAreaRef, value);
return (
<textarea

View File

@@ -1,24 +1,16 @@
import { useEffect } from "react";
import { useLayoutEffect } from "react";
export const useAutoResizeTextArea = (textAreaRef: React.RefObject<HTMLTextAreaElement>) => {
useEffect(() => {
export const useAutoResizeTextArea = (
textAreaRef: React.RefObject<HTMLTextAreaElement>,
value: string | number | readonly string[]
) => {
useLayoutEffect(() => {
const textArea = textAreaRef.current;
if (!textArea) return;
const resizeTextArea = () => {
textArea.style.height = "auto";
const computedHeight = textArea.scrollHeight + "px";
textArea.style.height = computedHeight;
};
const handleInput = () => resizeTextArea();
// resize on mount
resizeTextArea();
textArea.addEventListener("input", handleInput);
return () => {
textArea.removeEventListener("input", handleInput);
};
}, [textAreaRef]);
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
textArea.style.height = "0px";
const scrollHeight = textArea.scrollHeight;
textArea.style.height = scrollHeight + "px";
}, [textAreaRef, value]);
};

View File

@@ -178,7 +178,9 @@ export const CommandModal: React.FC = observer(() => {
return 0;
}}
onKeyDown={(e) => {
// when search is empty and page is undefined
// when search term is not empty, esc should clear the search term
if (e.key === "Escape" && searchTerm) setSearchTerm("");
// when user tries to close the modal with esc
if (e.key === "Escape" && !page && !searchTerm) closePalette();

View File

@@ -4,6 +4,8 @@ import Image from "next/image";
// headless ui
import { Tab } from "@headlessui/react";
import {
IIssueFilterOptions,
IIssueFilters,
IModule,
TAssigneesDistribution,
TCompletionChartDistribution,
@@ -37,6 +39,9 @@ type Props = {
roundedTab?: boolean;
noBackground?: boolean;
isPeekView?: boolean;
isCompleted?: boolean;
filters?: IIssueFilters | undefined;
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
};
export const SidebarProgressStats: React.FC<Props> = ({
@@ -47,6 +52,9 @@ export const SidebarProgressStats: React.FC<Props> = ({
roundedTab,
noBackground,
isPeekView = false,
isCompleted = false,
filters,
handleFiltersUpdate,
}) => {
const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees");
@@ -145,20 +153,11 @@ export const SidebarProgressStats: React.FC<Props> = ({
}
completed={assignee.completed_issues}
total={assignee.total_issues}
{...(!isPeekView && {
onClick: () => {
// TODO: set filters here
// if (filters?.assignees?.includes(assignee.assignee_id ?? ""))
// setFilters({
// assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id),
// });
// else
// setFilters({
// assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""],
// });
},
// selected: filters?.assignees?.includes(assignee.assignee_id ?? ""),
})}
{...(!isPeekView &&
!isCompleted && {
onClick: () => handleFiltersUpdate("assignees", assignee.assignee_id ?? ""),
selected: filters?.filters?.assignees?.includes(assignee.assignee_id ?? ""),
})}
/>
);
else
@@ -192,35 +191,52 @@ export const SidebarProgressStats: React.FC<Props> = ({
className="flex w-full flex-col gap-1.5 overflow-y-auto pt-3.5 vertical-scrollbar scrollbar-sm"
>
{distribution && distribution?.labels.length > 0 ? (
distribution.labels.map((label, index) => (
<SingleProgressStats
key={label.label_id ?? `no-label-${index}`}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full"
style={{
backgroundColor: label.color ?? "transparent",
}}
/>
<span className="text-xs">{label.label_name ?? "No labels"}</span>
</div>
}
completed={label.completed_issues}
total={label.total_issues}
{...(!isPeekView && {
// TODO: set filters here
onClick: () => {
// if (filters.labels?.includes(label.label_id ?? ""))
// setFilters({
// labels: filters?.labels?.filter((l) => l !== label.label_id),
// });
// else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] });
},
// selected: filters?.labels?.includes(label.label_id ?? ""),
})}
/>
))
distribution.labels.map((label, index) => {
if (label.label_id) {
return (
<SingleProgressStats
key={label.label_id}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full"
style={{
backgroundColor: label.color ?? "transparent",
}}
/>
<span className="text-xs">{label.label_name ?? "No labels"}</span>
</div>
}
completed={label.completed_issues}
total={label.total_issues}
{...(!isPeekView &&
!isCompleted && {
onClick: () => handleFiltersUpdate("labels", label.label_id ?? ""),
selected: filters?.filters?.labels?.includes(label.label_id ?? `no-label-${index}`),
})}
/>
);
} else {
return (
<SingleProgressStats
key={`no-label-${index}`}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full"
style={{
backgroundColor: label.color ?? "transparent",
}}
/>
<span className="text-xs">{label.label_name ?? "No labels"}</span>
</div>
}
completed={label.completed_issues}
total={label.total_issues}
/>
);
}
})
) : (
<div className="flex h-full flex-col items-center justify-center gap-2">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-custom-background-80">

View File

@@ -235,7 +235,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
</div>
</div>
</Link>
<div className="absolute right-4 bottom-3.5 z-[5] flex items-center gap-1.5">
<div className="absolute right-4 bottom-3.5 flex items-center gap-1.5">
{isEditingAllowed && (
<FavoriteStar
onClick={(e) => {

View File

@@ -202,7 +202,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
<span className="h-6 w-52 flex-shrink-0" />
</div>
</Link>
<div className="absolute right-5 bottom-8 z-[5] flex items-center gap-1.5">
<div className="absolute right-5 bottom-8 flex items-center gap-1.5">
<div className="relative flex w-full flex-shrink-0 items-center justify-between gap-2.5 md:w-auto md:flex-shrink-0 md:justify-end">
{currentCycle && (
<div

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
@@ -16,7 +16,7 @@ import {
} from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
// types
import { ICycle } from "@plane/types";
import { ICycle, IIssueFilterOptions } from "@plane/types";
// ui
import { Avatar, ArchiveIcon, CustomMenu, Loader, LayersIcon, TOAST_TYPE, setToast, TextArea } from "@plane/ui";
// components
@@ -27,12 +27,13 @@ import { DateRangeDropdown } from "@/components/dropdowns";
// constants
import { CYCLE_STATUS } from "@/constants/cycle";
import { CYCLE_UPDATED } from "@/constants/event-tracker";
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
import { useEventTracker, useCycle, useUser, useMember } from "@/hooks/store";
import { useEventTracker, useCycle, useUser, useMember, useIssues } from "@/hooks/store";
// services
import { CycleService } from "@/services/cycle.service";
@@ -191,25 +192,36 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
}
};
// TODO: refactor this
// const handleFiltersUpdate = useCallback(
// (key: keyof IIssueFilterOptions, value: string | string[]) => {
// if (!workspaceSlug || !projectId) return;
// const newValues = issueFilters?.filters?.[key] ?? [];
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.CYCLE);
// if (Array.isArray(value)) {
// value.forEach((val) => {
// if (!newValues.includes(val)) newValues.push(val);
// });
// } else {
// if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
// else newValues.push(value);
// }
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
// updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { [key]: newValues }, cycleId);
// },
// [workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
// );
if (Array.isArray(value)) {
// this validation is majorly for the filter start_date, target_date custom
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{ [key]: newValues },
cycleId
);
},
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
);
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase();
const isCompleted = cycleStatus === "completed";
@@ -251,8 +263,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
? "0 Issue"
: `${cycleDetails.progress_snapshot.completed_issues}/${cycleDetails.progress_snapshot.total_issues}`
: cycleDetails.total_issues === 0
? "0 Issue"
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
? "0 Issue"
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date);
@@ -551,6 +563,9 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
}}
totalIssues={cycleDetails.progress_snapshot.total_issues}
isPeekView={Boolean(peekCycle)}
isCompleted={isCompleted}
filters={issueFilters}
handleFiltersUpdate={handleFiltersUpdate}
/>
</div>
)}
@@ -570,6 +585,9 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
}}
totalIssues={cycleDetails.total_issues}
isPeekView={Boolean(peekCycle)}
isCompleted={isCompleted}
filters={issueFilters}
handleFiltersUpdate={handleFiltersUpdate}
/>
</div>
)}

View File

@@ -20,8 +20,8 @@ import { useLabel, useMember, useUser, useIssues } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
const GLOBAL_VIEW_LAYOUTS = [
{ key: "list", title: "List", link: "/workspace-views", icon: List },
{ key: "spreadsheet", title: "Spreadsheet", link: "/workspace-views/all-issues", icon: Sheet },
{ key: "list", title: "List", link: "workspace-views", icon: List },
{ key: "spreadsheet", title: "Spreadsheet", link: "workspace-views/all-issues", icon: Sheet },
];
type Props = {

View File

@@ -26,7 +26,7 @@ export const FilterState: React.FC<Props> = observer((props) => {
const sortedOptions = useMemo(() => {
const filteredOptions = (states ?? []).filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase()));
return sortBy(filteredOptions, [(s) => !(appliedFilters ?? []).includes(s.id), (s) => s.name.toLowerCase()]);
return sortBy(filteredOptions, [(s) => !(appliedFilters ?? []).includes(s.id)]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchQuery]);

View File

@@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
// hooks
import { Avatar, Loader } from "@plane/ui";
import { FilterHeader, FilterOption } from "@/components/issues";
import { useMember } from "@/hooks/store";
import { useMember, useUser } from "@/hooks/store";
// components
// ui
@@ -22,19 +22,22 @@ export const FilterLead: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;
const sortedOptions = useMemo(() => {
const filteredOptions = (memberIds || []).filter(
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
const filteredOptions = (memberIds || []).filter((memberId) =>
getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
);
return sortBy(filteredOptions, [
(memberId) => !(appliedFilters ?? []).includes(memberId),
(memberId) => memberId !== currentUser?.id,
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
]);
}, [appliedFilters, getUserDetails, memberIds, , searchQuery]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchQuery]);
const handleViewToggle = () => {
if (!sortedOptions) return;
@@ -65,7 +68,7 @@ export const FilterLead: React.FC<Props> = observer((props: Props) => {
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
title={member.display_name}
title={currentUser?.id === member.id ? "You" : member?.display_name}
/>
);
})}

View File

@@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
// hooks
import { Avatar, Loader } from "@plane/ui";
import { FilterHeader, FilterOption } from "@/components/issues";
import { useMember } from "@/hooks/store";
import { useMember, useUser } from "@/hooks/store";
// components
// ui
@@ -22,16 +22,18 @@ export const FilterMembers: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;
const sortedOptions = useMemo(() => {
const filteredOptions = (memberIds || []).filter(
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
const filteredOptions = (memberIds || []).filter((memberId) =>
getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
);
return sortBy(filteredOptions, [
(memberId) => !(appliedFilters ?? []).includes(memberId),
(memberId) => memberId !== currentUser?.id,
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -66,7 +68,7 @@ export const FilterMembers: React.FC<Props> = observer((props: Props) => {
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
title={member.display_name}
title={currentUser?.id === member.id ? "You" : member?.display_name}
/>
);
})}

View File

@@ -227,7 +227,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
</div>
</div>
</Link>
<div className="absolute right-4 bottom-3.5 z-[5] flex items-center gap-1.5">
<div className="absolute right-4 bottom-3.5 flex items-center gap-1.5">
{isEditingAllowed && (
<FavoriteStar
onClick={(e) => {

View File

@@ -175,7 +175,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
<span className="h-6 w-52 flex-shrink-0" />
</div>
</Link>
<div className="absolute right-5 bottom-8 z-[5] flex items-center gap-1.5">
<div className="absolute right-5 bottom-8 flex items-center gap-1.5">
<div className="flex flex-shrink-0 items-center justify-center">
{moduleStatus && (
<span

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form";
@@ -15,7 +15,7 @@ import {
UserCircle2,
} from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
import { IIssueFilterOptions, ILinkDetails, IModule, ModuleLink } from "@plane/types";
// ui
import {
CustomMenu,
@@ -41,13 +41,14 @@ import {
MODULE_LINK_UPDATED,
MODULE_UPDATED,
} from "@/constants/event-tracker";
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
import { MODULE_STATUS } from "@/constants/module";
import { EUserProjectRoles } from "@/constants/project";
// helpers
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
import { useModule, useUser, useEventTracker } from "@/hooks/store";
import { useModule, useUser, useEventTracker, useIssues } from "@/hooks/store";
// types
const defaultValues: Partial<IModule> = {
@@ -82,6 +83,9 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink, restoreModule } =
useModule();
const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker();
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.MODULE);
const moduleDetails = getModuleById(moduleId);
const moduleState = moduleDetails?.status?.toLocaleLowerCase();
@@ -245,6 +249,33 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
});
}, [moduleDetails, reset]);
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
// this validation is majorly for the filter start_date, target_date custom
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{ [key]: newValues },
moduleId
);
},
[workspaceSlug, projectId, moduleId, issueFilters, updateFilters]
);
const startDate = getDate(moduleDetails?.start_date);
const endDate = getDate(moduleDetails?.target_date);
const isStartValid = startDate && startDate <= new Date();
@@ -599,6 +630,8 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
totalIssues={moduleDetails.total_issues}
module={moduleDetails}
isPeekView={Boolean(peekModule)}
filters={issueFilters}
handleFiltersUpdate={handleFiltersUpdate}
/>
</div>
)}

View File

@@ -10,7 +10,7 @@ export const OutlineHeading1 = ({ marking, onClick }: HeadingProps) => (
<button
type="button"
onClick={onClick}
className="ml-4 cursor-pointer text-sm font-medium text-custom-text-400 hover:text-custom-primary-100 max-md:ml-2.5"
className="text-sm text-left font-medium text-custom-text-300 hover:text-custom-primary-100"
>
{marking.text}
</button>
@@ -20,7 +20,7 @@ export const OutlineHeading2 = ({ marking, onClick }: HeadingProps) => (
<button
type="button"
onClick={onClick}
className="ml-6 cursor-pointer text-xs font-medium text-custom-text-400 hover:text-custom-primary-100"
className="ml-2 text-xs text-left font-medium text-custom-text-300 hover:text-custom-primary-100"
>
{marking.text}
</button>
@@ -30,7 +30,7 @@ export const OutlineHeading3 = ({ marking, onClick }: HeadingProps) => (
<button
type="button"
onClick={onClick}
className="ml-8 cursor-pointer text-xs font-medium text-custom-text-400 hover:text-custom-primary-100"
className="ml-4 text-xs text-left font-medium text-custom-text-300 hover:text-custom-primary-100"
>
{marking.text}
</button>

View File

@@ -22,12 +22,19 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
return (
<>
{readOnly ? (
<h6 className="-mt-2 break-words bg-transparent text-4xl font-bold">{title}</h6>
<h6
className="break-words bg-transparent text-4xl font-bold"
style={{
lineHeight: "1.2",
}}
>
{title}
</h6>
) : (
<>
<TextArea
onChange={(e) => updateTitle(e.target.value)}
className="-mt-2 w-full bg-custom-background text-4xl font-bold outline-none p-0 border-none resize-none rounded-none"
className="w-full bg-custom-background text-4xl font-bold outline-none p-0 border-none resize-none rounded-none"
style={{
lineHeight: "1.2",
}}

View File

@@ -51,7 +51,7 @@ export const PageListBlock: FC<TPageListBlock> = observer((props) => {
>
{/* page title */}
<Tooltip tooltipHeading="Title" tooltipContent={name}>
<h5 className="text-base font-semibold truncate">{name}</h5>
<h5 className="text-base font-medium truncate">{name}</h5>
</Tooltip>
{/* page properties */}

View File

@@ -6,7 +6,7 @@ import { Avatar, Loader } from "@plane/ui";
// components
import { FilterHeader, FilterOption } from "@/components/issues";
// hooks
import { useMember } from "@/hooks/store";
import { useMember, useUser } from "@/hooks/store";
type Props = {
appliedFilters: string[] | null;
@@ -22,6 +22,7 @@ export const FilterCreatedBy: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;
@@ -32,9 +33,11 @@ export const FilterCreatedBy: React.FC<Props> = observer((props: Props) => {
return sortBy(filteredOptions, [
(memberId) => !(appliedFilters ?? []).includes(memberId),
(memberId) => memberId !== currentUser?.id,
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
]);
}, [appliedFilters, getUserDetails, memberIds, searchQuery]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchQuery]);
const handleViewToggle = () => {
if (!sortedOptions) return;
@@ -65,7 +68,7 @@ export const FilterCreatedBy: React.FC<Props> = observer((props: Props) => {
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
title={member.display_name}
title={currentUser?.id === member.id ? "You" : member?.display_name}
/>
);
})}

View File

@@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
// hooks
import { Avatar, Loader } from "@plane/ui";
import { FilterHeader, FilterOption } from "@/components/issues";
import { useMember } from "@/hooks/store";
import { useMember, useUser } from "@/hooks/store";
// components
// ui
@@ -22,16 +22,18 @@ export const FilterLead: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;
const sortedOptions = useMemo(() => {
const filteredOptions = (memberIds || []).filter(
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
const filteredOptions = (memberIds || []).filter((memberId) =>
getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
);
return sortBy(filteredOptions, [
(memberId) => !(appliedFilters ?? []).includes(memberId),
(memberId) => memberId !== currentUser?.id,
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -66,7 +68,7 @@ export const FilterLead: React.FC<Props> = observer((props: Props) => {
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
title={member.display_name}
title={currentUser?.id === member.id ? "You" : member?.display_name}
/>
);
})}

View File

@@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
// hooks
import { Avatar, Loader } from "@plane/ui";
import { FilterHeader, FilterOption } from "@/components/issues";
import { useMember } from "@/hooks/store";
import { useMember, useUser } from "@/hooks/store";
// components
// ui
@@ -22,16 +22,18 @@ export const FilterMembers: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;
const sortedOptions = useMemo(() => {
const filteredOptions = (memberIds || []).filter(
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
const filteredOptions = (memberIds || []).filter((memberId) =>
getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
);
return sortBy(filteredOptions, [
(memberId) => !(appliedFilters ?? []).includes(memberId),
(memberId) => memberId !== currentUser?.id,
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -66,7 +68,7 @@ export const FilterMembers: React.FC<Props> = observer((props: Props) => {
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
title={member.display_name}
title={currentUser?.id === member.id ? "You" : member?.display_name}
/>
);
})}

View File

@@ -93,29 +93,24 @@ export const CYCLE_STATUS: {
export const CYCLE_STATE_GROUPS_DETAILS = [
{
key: "backlog_issues",
title: "Backlog",
color: "#F0F0F3",
},
{
key: "unstarted_issues",
title: "Unstarted",
color: "#FB923C",
key: "completed_issues",
title: "Completed",
color: "#6490FE",
},
{
key: "started_issues",
title: "Started",
color: "#FFC53D",
color: "#FDD97F",
},
{
key: "completed_issues",
title: "Completed",
color: "#d687ff",
key: "unstarted_issues",
title: "Unstarted",
color: "#FEB055",
},
{
key: "cancelled_issues",
title: "Cancelled",
color: "#ef4444",
key: "backlog_issues",
title: "Backlog",
color: "#F0F0F3",
},
];

View File

@@ -160,7 +160,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
},
extra_options: {
access: true,
values: ["show_empty_groups"],
values: ["show_empty_groups", "sub_issue"],
},
},
kanban: {